diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 447 | ||||
-rw-r--r-- | src/suggestion.rs | 456 |
2 files changed, 459 insertions, 444 deletions
@@ -1,18 +1,15 @@ #![warn(rust_2018_idioms)] +pub mod suggestion; + mod url; +pub use crate::suggestion::Suggestion; pub use crate::url::SuggestionUrl; -use std::io::{BufRead, BufReader, BufWriter, Write}; -use std::path::Path; - -use git2::{Patch, Repository}; use github_rs::client::{Executor, Github}; -use regex::Regex; -use serde::Deserialize; use serde_json::Value; use thiserror::Error; @@ -63,182 +60,6 @@ impl<'a> Client<'a> { } } -#[derive(Debug, PartialEq)] -enum LineEnding { - Lf, - CrLf, -} - -#[derive(Debug, Deserialize)] -pub struct Suggestion { - #[serde(rename = "diff_hunk")] - diff: String, - - #[serde(rename = "body")] - comment: String, - - #[serde(rename = "original_commit_id")] - commit: String, - - path: String, - - original_start_line: Option<usize>, - - #[serde(rename = "original_line")] - original_end_line: usize, -} - -impl Suggestion { - // TODO: Rename to `diff` - pub fn patch(&self) -> String { - let mut diff: Vec<_> = self.diff.lines() - .filter(|l| !l.starts_with("-")) - .map(|l| { - if l.starts_with("+") { - return l.replacen("+", " ", 1); - } - - l.to_owned() - }) - .collect(); - - let last = diff.len() - 1; - diff[last] = diff.last().unwrap() - .replacen(" ", "-", 1); - - diff.push(self.suggestion_patch()); - - diff.join("\n") - } - - pub fn diff(&self) -> String { - let repo = Repository::open(".").unwrap(); - - self.diff_with_repo(&repo) - } - - fn diff_with_repo(&self, repo: &Repository) -> String { - let commit = repo.find_commit(self.commit.parse().unwrap()).unwrap(); - - let path = Path::new(&self.path); - - let object = commit - .tree().unwrap() - .get_path(path).unwrap() - .to_object(&repo).unwrap(); - - let blob = object.as_blob().unwrap(); - - let blob_reader = BufReader::new(blob.content()); - let mut new = BufWriter::new(Vec::new()); - self.apply_to(blob_reader, &mut new).unwrap(); - let new_buffer = new.into_inner().unwrap(); - - let mut diff = Patch::from_blob_and_buffer( - blob, - Some(&path), - &new_buffer, - Some(&path), - None, - ).unwrap(); - - diff.to_buf() - .unwrap() - .as_str() - .unwrap_or("") - .to_owned() - } - - fn suggestion_patch(&self) -> String { - let re = Regex::new(r"(?s).*(?-s)```\s*suggestion.*\n").unwrap(); - let s = re.replace(&self.comment, "+"); - s.replace("```", "") - } - - fn suggestion(&self) -> String { - self.suggestion_with_line_ending(&LineEnding::Lf) - } - - fn suggestion_with_line_ending(&self, line_ending: &LineEnding) -> String { - let re = Regex::new(r"(?s).*(?-s)```\s*suggestion.*\n").unwrap(); - let s = re.replace(&self.comment, ""); - let s = s.replace("```", ""); - - // Suggestion blocks use CRLF by default. - if *line_ending == LineEnding::Lf { - return s.replace('\r', ""); - } - - s - } - - pub fn apply(&self) -> Result<(), Error> { - let repo = Repository::open(".").unwrap(); - - let diff_text = self.diff_with_repo(&repo); - let diff = git2::Diff::from_buffer(diff_text.as_bytes()).unwrap(); - - repo.apply( - &diff, - git2::ApplyLocation::WorkDir, - None, - ).unwrap(); - - Ok(()) - } - - fn apply_to<R: BufRead, W: Write>( - &self, - reader: R, - writer: &mut W, - ) -> Result<(), Error> { - let mut line_ending = LineEnding::Lf; - - for (i, line) in reader.lines().enumerate() { - let line_number = i + 1; - - match line { - Ok(l) => { - // Determine which line endings the file uses by looking at - // the first line. - if line_number == 1 && is_line_crlf(&l) { - line_ending = LineEnding::CrLf; - } - - if line_number == self.original_end_line { - write!( - writer, - "{}", - self.suggestion_with_line_ending(&line_ending), - ).unwrap(); - } else if self.original_start_line.is_none() - || line_number < self.original_start_line.unwrap() - || line_number > self.original_end_line { - writeln!(writer, "{}", l).unwrap(); - } - }, - Err(e) => panic!(e), - } - } - - Ok(()) - } -} - -/// Determine the line ending for `line`. -/// -/// If the second-to-last character on the first line is "\r", assume CRLF. -/// Otherwise, default to LF. -fn is_line_crlf(line: &str) -> bool { - if let Some(c) = line.chars().rev().nth(2) { - if c == '\r' { - return true; - } - } - - false -} - #[cfg(test)] mod tests { use super::*; @@ -256,266 +77,4 @@ mod tests { println!("{:?}", suggestion); } - - #[test] - fn suggestion_patch_generates_patch() { - // Diff from gabgodBB (https://github.com/gabgodBB) and suggestion from - // probablycorey (https://github.com/probablycorey) in this pull - // request: https://github.com/cli/cli/pull/1123 - - let suggestion = Suggestion { - diff: r#"@@ -1, 9 +1, 11 @@ - package command - - import ( -+ "bufio" // used to input comment - "errors" - "fmt" - "io" -+ "os" // used to input comment"#.to_owned(), - comment: r#"It's ok to leave these uncommented - -```suggestion - "os" -```"#.to_owned(), - commit: "".to_owned(), - path: "".to_owned(), - original_start_line: Some(8), - original_end_line: 8, - }; - - assert_eq!( - suggestion.patch(), - r#"@@ -1, 9 +1, 11 @@ - package command - - import ( - "bufio" // used to input comment - "errors" - "fmt" - "io" -- "os" // used to input comment -+ "os" -"#, - ); - } - - #[test] - fn unified_diff() { - use unidiff::PatchSet; - - let diff = r#"--- a/command/pr.go -+++ b/command/pr.go -@@ -1,9 +1,11 @@ - package command - - import ( -+ "bufio" // used to input comment - "errors" - "fmt" - "io" -+ "os" // used to input comment -"#; - - let mut patch = PatchSet::new(); - patch.parse(diff).unwrap(); - - println!("{:?}", patch); - println!("{}", patch); - - let lines = patch.files_mut()[0].hunks_mut()[0].lines_mut(); - - // for line in &lines { - // if line.is_removed() { - // } else if line.is_added() { - // line.line_type = unidiff::LINE_TYPE_CONTEXT.to_owned(); - // } - // } - - lines - .iter_mut() - .filter(|l| !l.is_removed()) - // .map(|l| { - .for_each(|l| { - if l.is_added() { - l.line_type = unidiff::LINE_TYPE_CONTEXT.to_owned(); - } - }); - - lines[lines.len() - 2].line_type = unidiff::LINE_TYPE_REMOVED.to_owned(); - - patch.files_mut()[0].hunks_mut()[0].append(unidiff::Line::new( - r#" "os""#, - unidiff::LINE_TYPE_ADDED, - )); - - println!("{}", patch); - } - - #[test] - fn read_git_blob() { - use std::path::Path; - - use git2::Repository; - - let repo = Repository::open("./private/suggestion-test").unwrap(); - let commit = repo.find_commit("b58be52880a0a0c0d397052351be31f19acdeca4".parse().unwrap()).unwrap(); - - let object = commit - .tree().unwrap() - .get_path(Path::new("src/server.rs")).unwrap() - .to_object(&repo).unwrap(); - - let blob = object - .as_blob().unwrap() - .content(); - - println!("{:?}", commit); - println!("{}", std::str::from_utf8(blob).unwrap()); - } - - #[test] - fn suggestion_diff_with_repo_generates_diff() { - use tempfile::tempdir; - - - let git_root = tempdir().unwrap(); - let repo = Repository::init(git_root.path()).unwrap(); - - let file = r#" - ‘Beware the Jabberwock, my son! - The jaws that bite, the claws that catch! - Beware the Jubjub bird, and shun - The frumious Bandersnatch!’ - - He took his vorpal blade in hand: - Long time the manxome foe he sought-- - So rested he by the Tumtum tree, - And stood awhile in thought. -"#; - - let path = "poems/Jabberwocky.txt"; - - let mut index = repo.index().unwrap(); - index.add_frombuffer( - &git2::IndexEntry { - ctime: git2::IndexTime::new(0, 0), - mtime: git2::IndexTime::new(0, 0), - dev: 0, - ino: 0, - mode: 0o100644, - uid: 0, - gid: 0, - file_size: file.len() as u32, - id: git2::Oid::zero(), - flags: 0, - flags_extended: 0, - path: path.as_bytes().to_vec(), - }, - file.as_bytes(), - ).unwrap(); - let tree_oid = index.write_tree().unwrap(); - let tree = repo.find_tree(tree_oid).unwrap(); - - let author = git2::Signature::now( - "Oshino Shinobu", - "oshino.shinobu@example.com", - ).unwrap(); - - let commit = repo.commit( - Some("HEAD"), - &author, - &author, - "Sample commit", - &tree, - &[], - ).unwrap(); - - let suggestion = Suggestion { - diff: "".to_owned(), - comment: r#"``` suggestion - He took his vorpal sword in hand: - Long time the manxome foe he sought— -```"#.to_owned(), - commit: commit.to_string(), - path: path.to_owned(), - original_start_line: Some(7), - original_end_line: 8, - }; - - let expected = r#"diff --git a/poems/Jabberwocky.txt b/poems/Jabberwocky.txt -index 89840a2..06acdfc 100644 ---- a/poems/Jabberwocky.txt -+++ b/poems/Jabberwocky.txt -@@ -4,7 +4,7 @@ - Beware the Jubjub bird, and shun - The frumious Bandersnatch!’ - -- He took his vorpal blade in hand: -- Long time the manxome foe he sought-- -+ He took his vorpal sword in hand: -+ Long time the manxome foe he sought— - So rested he by the Tumtum tree, - And stood awhile in thought. -"#; - - assert_eq!( - suggestion.diff_with_repo(&repo), - expected, - ); - } - - #[test] - fn suggestion_apply_to_writes_patch_to_writer() { - use std::io::Cursor; - - - let mut original_buffer = Vec::new(); - let original = r#" - ‘Beware the Jabberwock, my son! - The jaws that bite, the claws that catch! - Beware the Jubjub bird, and shun - The frumious Bandersnatch!’ - - He took his vorpal blade in hand: - Long time the manxome foe he sought-- - So rested he by the Tumtum tree, - And stood awhile in thought. -"#; - - write!(original_buffer, "{}", original).unwrap(); - - let suggestion = Suggestion { - diff: "".to_owned(), - comment: r#"``` suggestion - He took his vorpal sword in hand: - Long time the manxome foe he sought— -```"#.to_owned(), - commit: "".to_owned(), - path: "".to_owned(), - original_start_line: Some(7), - original_end_line: 8, - }; - - let expected = r#" - ‘Beware the Jabberwock, my son! - The jaws that bite, the claws that catch! - Beware the Jubjub bird, and shun - The frumious Bandersnatch!’ - - He took his vorpal sword in hand: - Long time the manxome foe he sought— - So rested he by the Tumtum tree, - And stood awhile in thought. -"#; - - let original_reader = Cursor::new(original_buffer); - let mut actual = Cursor::new(Vec::new()); - suggestion.apply_to(original_reader, &mut actual).unwrap(); - - assert_eq!( - std::str::from_utf8(&actual.into_inner()).unwrap(), - expected, - ); - } } diff --git a/src/suggestion.rs b/src/suggestion.rs new file mode 100644 index 0000000..68a2430 --- /dev/null +++ b/src/suggestion.rs @@ -0,0 +1,456 @@ +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::Path; + +use git2::{Patch, Repository}; +use regex::Regex; +use serde::Deserialize; +use thiserror::Error; + + +#[derive(Debug, Error)] +pub enum Error { +} + +#[derive(Debug, PartialEq)] +enum LineEnding { + Lf, + CrLf, +} + +#[derive(Debug, Deserialize)] +pub struct Suggestion { + #[serde(rename = "diff_hunk")] + diff: String, + + #[serde(rename = "body")] + comment: String, + + #[serde(rename = "original_commit_id")] + commit: String, + + path: String, + + original_start_line: Option<usize>, + + #[serde(rename = "original_line")] + original_end_line: usize, +} + +impl Suggestion { + // TODO: Rename to `diff` + pub fn patch(&self) -> String { + let mut diff: Vec<_> = self.diff.lines() + .filter(|l| !l.starts_with("-")) + .map(|l| { + if l.starts_with("+") { + return l.replacen("+", " ", 1); + } + + l.to_owned() + }) + .collect(); + + let last = diff.len() - 1; + diff[last] = diff.last().unwrap() + .replacen(" ", "-", 1); + + diff.push(self.suggestion_patch()); + + diff.join("\n") + } + + pub fn diff(&self) -> String { + let repo = Repository::open(".").unwrap(); + + self.diff_with_repo(&repo) + } + + fn diff_with_repo(&self, repo: &Repository) -> String { + let commit = repo.find_commit(self.commit.parse().unwrap()).unwrap(); + + let path = Path::new(&self.path); + + let object = commit + .tree().unwrap() + .get_path(path).unwrap() + .to_object(&repo).unwrap(); + + let blob = object.as_blob().unwrap(); + + let blob_reader = BufReader::new(blob.content()); + let mut new = BufWriter::new(Vec::new()); + self.apply_to(blob_reader, &mut new).unwrap(); + let new_buffer = new.into_inner().unwrap(); + + let mut diff = Patch::from_blob_and_buffer( + blob, + Some(&path), + &new_buffer, + Some(&path), + None, + ).unwrap(); + + diff.to_buf() + .unwrap() + .as_str() + .unwrap_or("") + .to_owned() + } + + fn suggestion_patch(&self) -> String { + let re = Regex::new(r"(?s).*(?-s)```\s*suggestion.*\n").unwrap(); + let s = re.replace(&self.comment, "+"); + s.replace("```", "") + } + + fn suggestion(&self) -> String { + self.suggestion_with_line_ending(&LineEnding::Lf) + } + + fn suggestion_with_line_ending(&self, line_ending: &LineEnding) -> String { + let re = Regex::new(r"(?s).*(?-s)```\s*suggestion.*\n").unwrap(); + let s = re.replace(&self.comment, ""); + let s = s.replace("```", ""); + + // Suggestion blocks use CRLF by default. + if *line_ending == LineEnding::Lf { + return s.replace('\r', ""); + } + + s + } + + pub fn apply(&self) -> Result<(), Error> { + let repo = Repository::open(".").unwrap(); + + let diff_text = self.diff_with_repo(&repo); + let diff = git2::Diff::from_buffer(diff_text.as_bytes()).unwrap(); + + repo.apply( + &diff, + git2::ApplyLocation::WorkDir, + None, + ).unwrap(); + + Ok(()) + } + + fn apply_to<R: BufRead, W: Write>( + &self, + reader: R, + writer: &mut W, + ) -> Result<(), Error> { + let mut line_ending = LineEnding::Lf; + + for (i, line) in reader.lines().enumerate() { + let line_number = i + 1; + + match line { + Ok(l) => { + // Determine which line endings the file uses by looking at + // the first line. + if line_number == 1 && is_line_crlf(&l) { + line_ending = LineEnding::CrLf; + } + + if line_number == self.original_end_line { + write!( + writer, + "{}", + self.suggestion_with_line_ending(&line_ending), + ).unwrap(); + } else if self.original_start_line.is_none() + || line_number < self.original_start_line.unwrap() + || line_number > self.original_end_line { + writeln!(writer, "{}", l).unwrap(); + } + }, + Err(e) => panic!(e), + } + } + + Ok(()) + } +} + +/// Determine the line ending for `line`. +/// +/// If the second-to-last character on the first line is "\r", assume CRLF. +/// Otherwise, default to LF. +fn is_line_crlf(line: &str) -> bool { + if let Some(c) = line.chars().rev().nth(2) { + if c == '\r' { + return true; + } + } + + false +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn suggestion_patch_generates_patch() { + // Diff from gabgodBB (https://github.com/gabgodBB) and suggestion from + // probablycorey (https://github.com/probablycorey) in this pull + // request: https://github.com/cli/cli/pull/1123 + + let suggestion = Suggestion { + diff: r#"@@ -1, 9 +1, 11 @@ + package command + + import ( ++ "bufio" // used to input comment + "errors" + "fmt" + "io" ++ "os" // used to input comment"#.to_owned(), + comment: r#"It's ok to leave these uncommented + +```suggestion + "os" +```"#.to_owned(), + commit: "".to_owned(), + path: "".to_owned(), + original_start_line: Some(8), + original_end_line: 8, + }; + + assert_eq!( + suggestion.patch(), + r#"@@ -1, 9 +1, 11 @@ + package command + + import ( + "bufio" // used to input comment + "errors" + "fmt" + "io" +- "os" // used to input comment ++ "os" +"#, + ); + } + + #[test] + fn unified_diff() { + use unidiff::PatchSet; + + let diff = r#"--- a/command/pr.go ++++ b/command/pr.go +@@ -1,9 +1,11 @@ + package command + + import ( ++ "bufio" // used to input comment + "errors" + "fmt" + "io" ++ "os" // used to input comment +"#; + + let mut patch = PatchSet::new(); + patch.parse(diff).unwrap(); + + println!("{:?}", patch); + println!("{}", patch); + + let lines = patch.files_mut()[0].hunks_mut()[0].lines_mut(); + + // for line in &lines { + // if line.is_removed() { + // } else if line.is_added() { + // line.line_type = unidiff::LINE_TYPE_CONTEXT.to_owned(); + // } + // } + + lines + .iter_mut() + .filter(|l| !l.is_removed()) + // .map(|l| { + .for_each(|l| { + if l.is_added() { + l.line_type = unidiff::LINE_TYPE_CONTEXT.to_owned(); + } + }); + + lines[lines.len() - 2].line_type = unidiff::LINE_TYPE_REMOVED.to_owned(); + + patch.files_mut()[0].hunks_mut()[0].append(unidiff::Line::new( + r#" "os""#, + unidiff::LINE_TYPE_ADDED, + )); + + println!("{}", patch); + } + + #[test] + fn read_git_blob() { + use std::path::Path; + + use git2::Repository; + + let repo = Repository::open("./private/suggestion-test").unwrap(); + let commit = repo.find_commit("b58be52880a0a0c0d397052351be31f19acdeca4".parse().unwrap()).unwrap(); + + let object = commit + .tree().unwrap() + .get_path(Path::new("src/server.rs")).unwrap() + .to_object(&repo).unwrap(); + + let blob = object + .as_blob().unwrap() + .content(); + + println!("{:?}", commit); + println!("{}", std::str::from_utf8(blob).unwrap()); + } + + #[test] + fn suggestion_diff_with_repo_generates_diff() { + use tempfile::tempdir; + + + let git_root = tempdir().unwrap(); + let repo = Repository::init(git_root.path()).unwrap(); + + let file = r#" + ‘Beware the Jabberwock, my son! + The jaws that bite, the claws that catch! + Beware the Jubjub bird, and shun + The frumious Bandersnatch!’ + + He took his vorpal blade in hand: + Long time the manxome foe he sought-- + So rested he by the Tumtum tree, + And stood awhile in thought. +"#; + + let path = "poems/Jabberwocky.txt"; + + let mut index = repo.index().unwrap(); + index.add_frombuffer( + &git2::IndexEntry { + ctime: git2::IndexTime::new(0, 0), + mtime: git2::IndexTime::new(0, 0), + dev: 0, + ino: 0, + mode: 0o100644, + uid: 0, + gid: 0, + file_size: file.len() as u32, + id: git2::Oid::zero(), + flags: 0, + flags_extended: 0, + path: path.as_bytes().to_vec(), + }, + file.as_bytes(), + ).unwrap(); + let tree_oid = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_oid).unwrap(); + + let author = git2::Signature::now( + "Oshino Shinobu", + "oshino.shinobu@example.com", + ).unwrap(); + + let commit = repo.commit( + Some("HEAD"), + &author, + &author, + "Sample commit", + &tree, + &[], + ).unwrap(); + + let suggestion = Suggestion { + diff: "".to_owned(), + comment: r#"``` suggestion + He took his vorpal sword in hand: + Long time the manxome foe he sought— +```"#.to_owned(), + commit: commit.to_string(), + path: path.to_owned(), + original_start_line: Some(7), + original_end_line: 8, + }; + + let expected = r#"diff --git a/poems/Jabberwocky.txt b/poems/Jabberwocky.txt +index 89840a2..06acdfc 100644 +--- a/poems/Jabberwocky.txt ++++ b/poems/Jabberwocky.txt +@@ -4,7 +4,7 @@ + Beware the Jubjub bird, and shun + The frumious Bandersnatch!’ + +- He took his vorpal blade in hand: +- Long time the manxome foe he sought-- ++ He took his vorpal sword in hand: ++ Long time the manxome foe he sought— + So rested he by the Tumtum tree, + And stood awhile in thought. +"#; + + assert_eq!( + suggestion.diff_with_repo(&repo), + expected, + ); + } + + #[test] + fn suggestion_apply_to_writes_patch_to_writer() { + use std::io::Cursor; + + + let mut original_buffer = Vec::new(); + let original = r#" + ‘Beware the Jabberwock, my son! + The jaws that bite, the claws that catch! + Beware the Jubjub bird, and shun + The frumious Bandersnatch!’ + + He took his vorpal blade in hand: + Long time the manxome foe he sought-- + So rested he by the Tumtum tree, + And stood awhile in thought. +"#; + + write!(original_buffer, "{}", original).unwrap(); + + let suggestion = Suggestion { + diff: "".to_owned(), + comment: r#"``` suggestion + He took his vorpal sword in hand: + Long time the manxome foe he sought— +```"#.to_owned(), + commit: "".to_owned(), + path: "".to_owned(), + original_start_line: Some(7), + original_end_line: 8, + }; + + let expected = r#" + ‘Beware the Jabberwock, my son! + The jaws that bite, the claws that catch! + Beware the Jubjub bird, and shun + The frumious Bandersnatch!’ + + He took his vorpal sword in hand: + Long time the manxome foe he sought— + So rested he by the Tumtum tree, + And stood awhile in thought. +"#; + + let original_reader = Cursor::new(original_buffer); + let mut actual = Cursor::new(Vec::new()); + suggestion.apply_to(original_reader, &mut actual).unwrap(); + + assert_eq!( + std::str::from_utf8(&actual.into_inner()).unwrap(), + expected, + ); + } +} |