cli: Use pretty diffs for code review

We use the pretty diff renderer for code review.
This commit is contained in:
cloudhead 2024-07-12 16:13:20 +02:00
parent 823ece875e
commit 297646412a
No known key found for this signature in database
4 changed files with 486 additions and 197 deletions

View File

@ -33,3 +33,7 @@ resolver = "2"
inherits = "release"
debug = true
incremental = false
[workspace.lints]
clippy.type_complexity = "allow"
clippy.enum_variant_names = "allow"

View File

@ -71,3 +71,6 @@ path = "../radicle-term"
pretty_assertions = { version = "1.3.0" }
radicle = { version = "0.11.0", path = "../radicle", features = ["test"] }
radicle-node = { path = "../radicle-node", features = ["test"] }
[lints]
workspace = true

View File

@ -71,46 +71,47 @@ match `git diff master -W100% -U5 --patience`:
```
$ rad patch review --patch -U5 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 7937fb3..0000000
--- a/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.draft
diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt
new file mode 100644
index 0000000..2b5bd86
--- /dev/null
+++ b/DISCLAIMER.txt
@@ -0,0 +1 @@
+All food is served as-is, with no warranty!
diff --git a/MENU.txt b/MENU.txt
index 867958c..3af9741 100644
--- a/MENU.txt
+++ b/MENU.txt
@@ -1,7 +1,8 @@
Classics
--------
+Baked Brie
Salmon Tartare
Mac & Cheese
[..]
Comfort Food
------------
@@ -9,6 +10,7 @@ Reuben Sandwich
Club Sandwich
Fried Shrimp Basket
[..]
Sides
-----
-French Fries
+French Fries!
+Garlic Green Beans
diff --git a/INSTRUCTIONS.txt b/notes/INSTRUCTIONS.txt
similarity index 100%
rename from INSTRUCTIONS.txt
rename to notes/INSTRUCTIONS.txt
╭──────────────────────╮
│ .gitignore deleted
├──────────────────────┤
│ @@ -1,1 +0,0 @@ │
│ 1 - *.draft │
╰──────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ DISCLAIMER.txt created
├──────────────────────────────────────────────────────────┤
│ @@ -0,0 +1,1 @@ │
│ 1 + All food is served as-is, with no warranty! │
╰──────────────────────────────────────────────────────────╯
╭─────────────────────────────╮
│ MENU.txt │
├─────────────────────────────┤
│ @@ -1,7 +1,8 @@ │
│ 1 1 Classics │
│ 2 2 -------- │
│ 3 + Baked Brie │
│ 3 4 Salmon Tartare │
│ 4 5 Mac & Cheese │
│ 5 6 │
│ 6 7 Comfort Food │
│ 7 8 ------------ │
╰─────────────────────────────╯
╭──────────────────────────────────╮
│ MENU.txt │
├──────────────────────────────────┤
│ @@ -9,6 +10,7 @@ Reuben Sandwich │
│ 9 10 Club Sandwich │
│ 10 11 Fried Shrimp Basket │
│ 11 12 │
│ 12 13 Sides │
│ 13 14 ----- │
│ 14 - French Fries │
│ 15 + French Fries! │
│ 16 + Garlic Green Beans │
╰──────────────────────────────────╯
╭────────────────────────────────────────────────────╮
│ INSTRUCTIONS.txt -> notes/INSTRUCTIONS.txt moved
╰────────────────────────────────────────────────────╯
```
Now let's accept these hunks one by one..
@ -118,67 +119,64 @@ Now let's accept these hunks one by one..
```
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 7937fb3..0000000
--- a/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.draft
╭──────────────────────╮
│ .gitignore deleted
├──────────────────────┤
│ @@ -1,1 +0,0 @@ │
│ 1 - *.draft │
╰──────────────────────╯
✓ Updated review tree to a5fccf0e977225ff13c3f74c43faf4cb679bf835
```
```
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt
new file mode 100644
index 0000000..2b5bd86
--- /dev/null
+++ b/DISCLAIMER.txt
@@ -0,0 +1 @@
+All food is served as-is, with no warranty!
╭──────────────────────────────────────────────────────────╮
│ DISCLAIMER.txt created
├──────────────────────────────────────────────────────────┤
│ @@ -0,0 +1,1 @@ │
│ 1 + All food is served as-is, with no warranty! │
╰──────────────────────────────────────────────────────────╯
✓ Updated review tree to 2cdb82ea726e64d3b52847c7699d0d4759198f5c
```
```
$ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
diff --git a/MENU.txt b/MENU.txt
index 867958c..3af9741 100644
--- a/MENU.txt
+++ b/MENU.txt
@@ -1,5 +1,6 @@
Classics
--------
+Baked Brie
Salmon Tartare
Mac & Cheese
[..]
╭─────────────────────────────╮
│ MENU.txt │
├─────────────────────────────┤
│ @@ -1,5 +1,6 @@ │
│ 1 1 Classics │
│ 2 2 -------- │
│ 3 + Baked Brie │
│ 3 4 Salmon Tartare │
│ 4 5 Mac & Cheese │
│ 5 6 │
╰─────────────────────────────╯
✓ Updated review tree to d4aecbb859a802a3215def0b538358bf63593953
```
```
$ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
diff --git a/MENU.txt b/MENU.txt
index 4e2e828..3af9741 100644
--- a/MENU.txt
+++ b/MENU.txt
@@ -12,4 +12,5 @@ Fried Shrimp Basket
[..]
Sides
-----
-French Fries
+French Fries!
+Garlic Green Beans
╭───────────────────────────────────────╮
│ MENU.txt │
├───────────────────────────────────────┤
│ @@ -12,4 +12,5 @@ Fried Shrimp Basket │
│ 12 12 │
│ 13 13 Sides │
│ 14 14 ----- │
│ 15 - French Fries │
│ 15 + French Fries! │
│ 16 + Garlic Green Beans │
╰───────────────────────────────────────╯
✓ Updated review tree to 59cee720b0642b1491b241400912b35926a76c3f
```
```
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
diff --git a/INSTRUCTIONS.txt b/notes/INSTRUCTIONS.txt
similarity index 100%
rename from INSTRUCTIONS.txt
rename to notes/INSTRUCTIONS.txt
╭────────────────────────────────────────────────────╮
│ INSTRUCTIONS.txt -> notes/INSTRUCTIONS.txt moved
╰────────────────────────────────────────────────────╯
✓ Updated review tree to 3effc8f6462fa2573697072245e57708c4dcbe62
```

View File

@ -13,9 +13,8 @@
//!
use std::collections::VecDeque;
use std::fmt::Write as _;
use std::io::IsTerminal as _;
use std::ops::{Not, Range};
use std::path::PathBuf;
use std::ops::{Deref, Not, Range};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fmt, io};
@ -26,10 +25,14 @@ use radicle::prelude::*;
use radicle::storage::git::Repository;
use radicle_git_ext::Oid;
use radicle_surf::diff::*;
use radicle_term::{Element, VStack};
use crate::git::unified_diff;
use crate::git::pretty_diff::ToPretty;
use crate::git::pretty_diff::{Blob, Blobs, Repo};
use crate::git::unified_diff::{self, FileHeader};
use crate::git::unified_diff::{Encode, HunkHeader};
use crate::terminal as term;
use crate::terminal::highlight::Highlighter;
/// Help message shown to user.
const HELP: &str = "\
@ -42,6 +45,24 @@ s - split the current hunk into smaller hunks
q - quit; do not accept this hunk nor any of the remaining ones
? - print help";
/// A terminal or file where the review UI output can be written to.
trait PromptWriter: io::Write {
/// Is the writer a terminal?
fn is_terminal(&self) -> bool;
}
impl PromptWriter for Box<dyn PromptWriter> {
fn is_terminal(&self) -> bool {
self.deref().is_terminal()
}
}
impl<T: io::Write + io::IsTerminal> PromptWriter for T {
fn is_terminal(&self) -> bool {
<Self as io::IsTerminal>::is_terminal(self)
}
}
/// The actions that a user can carry out on a review item.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReviewAction {
@ -113,58 +134,345 @@ impl FromStr for ReviewAction {
}
/// A single review item. Can be a hunk or eg. a file move.
pub struct ReviewItem<'a> {
file: &'a FileDiff,
hunk: Option<&'a Hunk<Modification>>,
/// Files are usually split into multiple review items.
#[derive(Debug)]
pub enum ReviewItem {
FileAdded {
path: PathBuf,
header: FileHeader,
new: DiffFile,
hunk: Option<Hunk<Modification>>,
},
FileDeleted {
path: PathBuf,
header: FileHeader,
old: DiffFile,
hunk: Option<Hunk<Modification>>,
},
FileModified {
path: PathBuf,
header: FileHeader,
old: DiffFile,
new: DiffFile,
hunk: Option<Hunk<Modification>>,
},
FileMoved {
moved: Moved,
},
FileCopied {
copied: Copied,
},
FileEofChanged {
path: PathBuf,
header: FileHeader,
old: DiffFile,
new: DiffFile,
eof: EofNewLine,
},
FileModeChanged {
path: PathBuf,
header: FileHeader,
old: DiffFile,
new: DiffFile,
},
}
impl ReviewItem {
fn hunk(&self) -> Option<&Hunk<Modification>> {
match self {
Self::FileAdded { hunk, .. } => hunk.as_ref(),
Self::FileDeleted { hunk, .. } => hunk.as_ref(),
Self::FileModified { hunk, .. } => hunk.as_ref(),
_ => None,
}
}
fn hunk_header(&self) -> Option<HunkHeader> {
self.hunk().and_then(|h| HunkHeader::try_from(h).ok())
}
fn paths(&self) -> (Option<(&Path, Oid)>, Option<(&Path, Oid)>) {
match self {
Self::FileAdded { path, new, .. } => (None, Some((&path, new.oid))),
Self::FileDeleted { path, old, .. } => (Some((&path, old.oid)), None),
Self::FileMoved { moved } => (
Some((&moved.old_path, moved.old.oid)),
Some((&moved.new_path, moved.new.oid)),
),
Self::FileCopied { copied } => (
Some((&copied.old_path, copied.old.oid)),
Some((&copied.new_path, copied.new.oid)),
),
Self::FileModified { path, old, new, .. } => {
(Some((path, old.oid)), Some((path, new.oid)))
}
Self::FileEofChanged { path, old, new, .. } => {
(Some((path, old.oid)), Some((path, new.oid)))
}
Self::FileModeChanged { path, old, new, .. } => {
(Some((path, old.oid)), Some((path, new.oid)))
}
}
}
fn file_header(&self) -> FileHeader {
match self {
Self::FileAdded { header, .. } => header.clone(),
Self::FileDeleted { header, .. } => header.clone(),
Self::FileMoved { moved } => FileHeader::Moved {
old_path: moved.old_path.clone(),
new_path: moved.new_path.clone(),
},
Self::FileCopied { copied } => FileHeader::Copied {
old_path: copied.old_path.clone(),
new_path: copied.new_path.clone(),
},
Self::FileModified { header, .. } => header.clone(),
Self::FileEofChanged { header, .. } => header.clone(),
Self::FileModeChanged { header, .. } => header.clone(),
}
}
fn blobs<R: Repo>(&self, repo: &R) -> Blobs<(PathBuf, Blob)> {
let (old, new) = self.paths();
Blobs::from_paths(old, new, repo)
}
fn pretty<R: Repo>(&self, repo: &R) -> Box<dyn Element> {
let mut hi = Highlighter::default();
let blobs = self.blobs(repo);
let highlighted = blobs.highlight(&mut hi);
let header = self.file_header();
match self {
Self::FileMoved { moved } => moved.pretty(&mut hi, &header, repo),
Self::FileCopied { copied } => copied.pretty(&mut hi, &header, repo),
Self::FileModified { hunk, .. }
| Self::FileAdded { hunk, .. }
| Self::FileDeleted { hunk, .. } => {
let header = header.pretty(&mut hi, &None, repo);
let vstack = term::VStack::default()
.border(Some(term::colors::FAINT))
.padding(1)
.child(header);
if let Some(hunk) = hunk {
let hunk = hunk.pretty(&mut hi, &highlighted, repo);
if !hunk.is_empty() {
return vstack.divider().merge(hunk).boxed();
}
}
vstack
}
Self::FileEofChanged { eof, .. } => match eof {
EofNewLine::NewMissing => {
VStack::default().child(term::Label::new("`\\n` missing at end-of-file"))
}
EofNewLine::OldMissing => {
VStack::default().child(term::Label::new("`\\n` added at end-of-file"))
}
_ => VStack::default(),
},
Self::FileModeChanged { .. } => VStack::default(),
}
.boxed()
}
}
/// Queue of items (usually hunks) left to review.
#[derive(Default)]
pub struct ReviewQueue<'a> {
queue: VecDeque<(usize, ReviewItem<'a>)>,
pub struct ReviewQueue {
/// Hunks left to review.
queue: VecDeque<(usize, ReviewItem)>,
}
impl<'a> ReviewQueue<'a> {
/// Push an item to the queue.
fn push(&mut self, file: &'a FileDiff, hunks: Option<&'a Hunks<Modification>>) {
let mut queue_item = |hunk| {
self.queue
.push_back((self.queue.len(), ReviewItem { file, hunk }))
};
impl ReviewQueue {
/// Add a file to the queue.
/// Mostly splits files into individual review items (eg. hunks) to review.
fn add_file(&mut self, file: FileDiff) {
let header = FileHeader::from(&file);
if let Some(hunks) = hunks {
for hunk in hunks.iter() {
queue_item(Some(hunk));
match file {
FileDiff::Moved(moved) => {
self.add_item(ReviewItem::FileMoved { moved });
}
FileDiff::Copied(copied) => {
self.add_item(ReviewItem::FileCopied { copied });
}
FileDiff::Added(a) => {
self.add_item(ReviewItem::FileAdded {
path: a.path,
header: header.clone(),
new: a.new,
hunk: if let DiffContent::Plain {
hunks: Hunks(mut hs),
..
} = a.diff
{
hs.pop()
} else {
None
},
});
}
FileDiff::Deleted(d) => {
self.add_item(ReviewItem::FileDeleted {
path: d.path,
header: header.clone(),
old: d.old,
hunk: if let DiffContent::Plain {
hunks: Hunks(mut hs),
..
} = d.diff
{
hs.pop()
} else {
None
},
});
}
FileDiff::Modified(m) => {
if m.old.mode != m.new.mode {
self.add_item(ReviewItem::FileModeChanged {
path: m.path.clone(),
header: header.clone(),
old: m.old.clone(),
new: m.new.clone(),
});
}
match m.diff {
DiffContent::Empty => {
// Likely a file mode change, which is handled above.
}
DiffContent::Binary => {
self.add_item(ReviewItem::FileModified {
path: m.path.clone(),
header: header.clone(),
old: m.old.clone(),
new: m.new.clone(),
hunk: None,
});
}
DiffContent::Plain {
hunks: Hunks(hunks),
eof,
..
} => {
for hunk in hunks {
self.add_item(ReviewItem::FileModified {
path: m.path.clone(),
header: header.clone(),
old: m.old.clone(),
new: m.new.clone(),
hunk: Some(hunk),
});
}
if let EofNewLine::OldMissing | EofNewLine::NewMissing = eof {
self.add_item(ReviewItem::FileEofChanged {
path: m.path.clone(),
header: header.clone(),
old: m.old.clone(),
new: m.new.clone(),
eof,
})
}
}
}
}
} else {
queue_item(None);
}
}
fn add_item(&mut self, item: ReviewItem) {
self.queue.push_back((self.queue.len(), item));
}
}
impl<'a> std::ops::Deref for ReviewQueue<'a> {
type Target = VecDeque<(usize, ReviewItem<'a>)>;
impl std::ops::Deref for ReviewQueue {
type Target = VecDeque<(usize, ReviewItem)>;
fn deref(&self) -> &Self::Target {
&self.queue
}
}
impl<'a> std::ops::DerefMut for ReviewQueue<'a> {
impl std::ops::DerefMut for ReviewQueue {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.queue
}
}
impl<'a> Iterator for ReviewQueue<'a> {
type Item = (usize, ReviewItem<'a>);
impl Iterator for ReviewQueue {
type Item = (usize, ReviewItem);
fn next(&mut self) -> Option<Self::Item> {
self.queue.pop_front()
}
}
/// Builds a patch review interactively.
/// Builds a review for a single file.
pub struct FileReviewBuilder {
header: FileHeader,
delta: i32,
}
impl FileReviewBuilder {
fn new(item: &ReviewItem) -> Self {
Self {
header: item.file_header(),
delta: 0,
}
}
fn set_item(&mut self, item: &ReviewItem) -> &mut Self {
let header = item.file_header();
if self.header != header {
self.header = header;
self.delta = 0;
}
self
}
fn ignore_item(&mut self, item: &ReviewItem) {
if let Some(h) = item.hunk_header() {
self.delta += h.new_size as i32 - h.old_size as i32;
}
}
fn apply_item<'a>(
&mut self,
item: ReviewItem,
brain: &mut git::raw::Tree<'a>,
repo: &'a git::raw::Repository,
) -> Result<(), Error> {
let mut buf = Vec::new();
let mut writer = unified_diff::Writer::new(&mut buf);
writer.encode(&self.header)?;
if let (Some(h), Some(mut header)) = (item.hunk(), item.hunk_header()) {
header.old_line_no -= self.delta as u32;
header.new_line_no -= self.delta as u32;
let h = Hunk {
header: header.to_unified_string()?.as_bytes().to_owned().into(),
lines: h.lines.clone(),
old: h.old.clone(),
new: h.new.clone(),
};
writer.encode(&h)?;
}
drop(writer);
let diff = git::raw::Diff::from_buffer(&buf)?;
let mut index = repo.apply_to_tree(brain, &diff, None)?;
let brain_oid = index.write_tree_to(repo)?;
*brain = repo.find_tree(brain_oid)?;
Ok(())
}
}
/// Builds a patch review interactively, across multiple files.
pub struct ReviewBuilder<'a> {
/// Patch being reviewed.
patch_id: PatchId,
@ -215,8 +523,13 @@ impl<'a> ReviewBuilder<'a> {
commit.tree()?
};
let stdout = io::stdout().lock();
let mut stdin = io::stdin().lock();
let mut stderr = io::stderr().lock();
let mut writer: Box<dyn PromptWriter> = if self.hunk.is_some() || !stdout.is_terminal() {
Box::new(stdout)
} else {
Box::new(io::stderr().lock())
};
let mut review = if let Ok(c) = self.current() {
term::success!(
"Loaded existing review {} for patch {}",
@ -237,51 +550,22 @@ impl<'a> ReviewBuilder<'a> {
repo.find_commit(oid)?
};
let mut brain = review.tree()?;
let mut writer = unified_diff::Writer::new(io::stdout()).styled(true);
let mut queue = ReviewQueue::default(); // Queue of hunks to review.
let mut current = None; // File of the current hunk.
let mut queue = ReviewQueue::default();
let diff = self.diff(&brain, &tree, repo, opts)?;
let mut find_opts = git::raw::DiffFindOptions::new();
find_opts.exact_match_only(true);
find_opts.all(true);
find_opts.copies(false); // We don't support finding copies at the moment.
let mut diff = repo.diff_tree_to_tree(Some(&brain), Some(&tree), Some(opts))?;
diff.find_similar(Some(&mut find_opts))?;
if diff.deltas().next().is_none() {
// Build the review queue.
for file in diff.into_files() {
queue.add_file(file);
}
if queue.is_empty() {
term::success!("All hunks have been reviewed");
return Ok(());
}
let diff = Diff::try_from(diff)?;
for file in diff.files() {
match file {
FileDiff::Modified(f) => match &f.diff {
DiffContent::Plain { hunks, .. } => queue.push(file, Some(hunks)),
DiffContent::Binary => queue.push(file, None),
DiffContent::Empty => {}
},
FileDiff::Added(f) => match &f.diff {
DiffContent::Plain { hunks, .. } => queue.push(file, Some(hunks)),
DiffContent::Binary => queue.push(file, None),
DiffContent::Empty => {}
},
FileDiff::Deleted(f) => match &f.diff {
DiffContent::Plain { hunks, .. } => queue.push(file, Some(hunks)),
DiffContent::Binary => queue.push(file, None),
DiffContent::Empty => {}
},
FileDiff::Moved(_) => queue.push(file, None),
FileDiff::Copied(_) => {
// Copies are not supported and should never be generated due to the diff
// options we pass.
panic!("ReviewBuilder::by_hunk: copy diffs are not supported");
}
}
}
// File review for the current file. Starts out as `None` and is set on the first hunk.
// Keeps track of deltas for hunk offsets.
let mut file: Option<FileReviewBuilder> = None;
let total = queue.len();
let mut delta: i32 = 0;
while let Some((ix, item)) = queue.next() {
if let Some(hunk) = self.hunk {
@ -290,66 +574,42 @@ impl<'a> ReviewBuilder<'a> {
}
}
let progress = term::format::secondary(format!("({}/{total})", ix + 1));
let ReviewItem { file, hunk } = item;
let file = match file.as_mut() {
Some(fr) => fr.set_item(&item),
None => file.insert(FileReviewBuilder::new(&item)),
};
term::element::write_to(
&item.pretty(repo),
&mut writer,
term::Constraint::from_env().unwrap_or_default(),
)?;
if current.map_or(true, |c| c != file) {
writer.encode(&unified_diff::FileHeader::from(file))?;
current = Some(file);
delta = 0;
}
let header = hunk
.map(|h| {
let header = unified_diff::HunkHeader::try_from(h)?;
writer.encode(h)?;
Ok::<_, anyhow::Error>(header)
})
.transpose()?;
match self.prompt(&mut stdin, &mut stderr, progress) {
// Prompts the user for action on the above hunk.
match self.prompt(&mut stdin, &mut writer, progress) {
// When a hunk is accepted, we convert it to unified diff format,
// and apply it to the `brain`.
Some(ReviewAction::Accept) => {
let mut buf = Vec::new();
{
let mut writer = unified_diff::Writer::new(&mut buf);
writer.encode(&unified_diff::FileHeader::from(file))?;
if let (Some(h), Some(mut header)) = (hunk, header) {
header.old_line_no -= delta as u32;
header.new_line_no -= delta as u32;
let h = Hunk {
header: header.to_unified_string()?.as_bytes().to_owned().into(),
lines: h.lines.clone(),
old: h.old.clone(),
new: h.new.clone(),
};
writer.encode(&h)?;
}
}
let diff = git::raw::Diff::from_buffer(&buf)?;
let mut index = repo.apply_to_tree(&brain, &diff, None)?;
let brain_oid = index.write_tree_to(repo)?;
brain = repo.find_tree(brain_oid)?;
let oid =
// Update brain with accepted hunk.
file.apply_item(item, &mut brain, repo)?;
// Update review with new brain.
let review_oid =
review.amend(Some(&self.refname), None, None, None, None, Some(&brain))?;
review = repo.find_commit(oid)?;
review = repo.find_commit(review_oid)?;
if self.hunk.is_some() {
term::success!("Updated review tree to {brain_oid}");
term::success!("Updated review tree to {}", brain.id());
}
}
Some(ReviewAction::Ignore) => {
// Do nothing. Hunk will be reviewable again next time.
if let Some(h) = header {
delta += h.new_size as i32 - h.old_size as i32;
}
file.ignore_item(&item);
}
Some(ReviewAction::Comment) => {
if let Some(hunk) = hunk {
let mut builder =
CommentBuilder::new(revision.head(), item.file.path().to_path_buf());
let (old, new) = item.paths();
let path = old.or(new);
if let (Some(hunk), Some((path, _))) = (item.hunk(), path) {
let mut builder = CommentBuilder::new(revision.head(), path.to_path_buf());
builder.edit(hunk)?;
let _comments = builder.comments();
@ -406,10 +666,30 @@ impl<'a> ReviewBuilder<'a> {
Ok(())
}
fn diff(
&self,
brain: &git::raw::Tree<'_>,
tree: &git::raw::Tree<'_>,
repo: &'a git::raw::Repository,
opts: &mut git::raw::DiffOptions,
) -> Result<Diff, Error> {
let mut find_opts = git::raw::DiffFindOptions::new();
find_opts.exact_match_only(true);
find_opts.all(true);
find_opts.copies(false); // We don't support finding copies at the moment.
let mut diff = repo.diff_tree_to_tree(Some(brain), Some(tree), Some(opts))?;
diff.find_similar(Some(&mut find_opts))?;
let diff = Diff::try_from(diff)?;
Ok(diff)
}
fn prompt(
&self,
mut input: impl io::BufRead,
mut output: &mut io::StderrLock,
mut output: &mut impl PromptWriter,
progress: impl fmt::Display,
) -> Option<ReviewAction> {
if let Some(v) = self.verdict {
@ -446,9 +726,13 @@ enum Error {
#[error(transparent)]
Diff(#[from] unified_diff::Error),
#[error(transparent)]
Surf(#[from] radicle_surf::diff::git::error::Diff),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Format(#[from] std::fmt::Error),
#[error(transparent)]
Git(#[from] git::raw::Error),
}
#[derive(Debug)]