diff --git a/rad-patch.1.adoc b/rad-patch.1.adoc index f8637b2b..e368a737 100644 --- a/rad-patch.1.adoc +++ b/rad-patch.1.adoc @@ -15,6 +15,7 @@ rad-patch - Manage radicle patches. *rad patch* [...] + *rad patch* _list_ [--all|--merged|--open|--archived|--draft] [...] + *rad patch* _show_ [...] + +*rad patch* _diff_ [--revision ] [...] + *rad patch* _archive_ [...] + *rad patch* _update_ [...] + *rad patch* _checkout_ [...] + @@ -52,6 +53,13 @@ Shows information on the given patch. *--patch*, *-p*:: Show the patch changes in git patch format *--verbose*, *-v*:: Show additional information about the patch +=== diff + +Outputs the patch diff, using Radicle's diffing tool. + +**:: The patch to diff +*--revision*, *-r *:: The revision to diff (default: latest) + === edit Edits a patch revision comment. To edit the patch title or description, pass diff --git a/radicle-cli/examples/rad-patch-diff.md b/radicle-cli/examples/rad-patch-diff.md new file mode 100644 index 00000000..3d7db547 --- /dev/null +++ b/radicle-cli/examples/rad-patch-diff.md @@ -0,0 +1,63 @@ +Using `rad patch diff`, we can output the patch diff: + +``` ./README.md +Hello World! +``` +``` +$ git checkout -b feature/1 +$ git add README.md +$ git commit -m "Add README" -q +$ git push rad HEAD:refs/patches +``` +``` +$ rad patch diff e432b04 +╭───────────────────────────╮ +│ README.md +1 ❲created❳ │ +├───────────────────────────┤ +│ @@ -0,0 +1,1 @@ │ +│ 1 + Hello World! │ +╰───────────────────────────╯ + +``` + +If we add another file and update the patch, we can see it in the diff. + +``` ./RADICLE.md +Hello Radicle! +``` +``` +$ git add RADICLE.md +$ git commit --amend -q +$ git push -f +``` +``` +$ rad patch diff e432b04 +╭─────────────────────────────╮ +│ RADICLE.md +1 ❲created❳ │ +├─────────────────────────────┤ +│ @@ -0,0 +1,1 @@ │ +│ 1 + Hello Radicle! │ +╰─────────────────────────────╯ + +╭─────────────────────────────╮ +│ README.md +1 ❲created❳ │ +├─────────────────────────────┤ +│ @@ -0,0 +1,1 @@ │ +│ 1 + Hello World! │ +╰─────────────────────────────╯ + +``` + +Buf if we only want to see the changes from the first revision, we can do that +too. + +``` +$ rad patch diff e432b04 --revision e432b04 +╭───────────────────────────╮ +│ README.md +1 ❲created❳ │ +├───────────────────────────┤ +│ @@ -0,0 +1,1 @@ │ +│ 1 + Hello World! │ +╰───────────────────────────╯ + +``` diff --git a/radicle-cli/src/commands/patch.rs b/radicle-cli/src/commands/patch.rs index 65d8d888..0dc12c78 100644 --- a/radicle-cli/src/commands/patch.rs +++ b/radicle-cli/src/commands/patch.rs @@ -10,6 +10,8 @@ mod comment; mod common; #[path = "patch/delete.rs"] mod delete; +#[path = "patch/diff.rs"] +mod diff; #[path = "patch/edit.rs"] mod edit; #[path = "patch/label.rs"] @@ -53,6 +55,7 @@ Usage rad patch [...] rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author ]... [...] rad patch show [...] + rad patch diff [...] rad patch archive [...] rad patch update [...] rad patch checkout [...] @@ -71,6 +74,10 @@ Show options -p, --patch Show the actual patch diff -v, --verbose Show additional information about the patch +Diff options + + -r, --revision The revision to diff (default: latest) + Comment options -m, --message Provide a comment message via the command-line @@ -81,14 +88,13 @@ Edit options Review options - --revision Review the given revision of the patch + -r, --revision Review the given revision of the patch -p, --patch Review by patch hunks --hunk Only review a specific hunk --accept Accept a patch or set of hunks --reject Reject a patch or set of hunks -U, --unified Generate diffs with lines of context instead of the usual three -d, --delete Delete a review draft - -r, --revision Review a patch revision -m, --message [] Provide a comment with the review (default: prompt) Assign options @@ -146,6 +152,7 @@ Other options pub enum OperationName { Assign, Show, + Diff, Update, Archive, Delete, @@ -200,6 +207,10 @@ pub enum Operation { patch_id: Rev, diff: bool, }, + Diff { + patch_id: Rev, + revision_id: Option, + }, Update { patch_id: Rev, base_id: Option, @@ -267,7 +278,10 @@ impl Operation { | Operation::Edit { .. } | Operation::Redact { .. } | Operation::Set { .. } => true, - Operation::Show { .. } | Operation::Checkout { .. } | Operation::List { .. } => false, + Operation::Show { .. } + | Operation::Diff { .. } + | Operation::Checkout { .. } + | Operation::List { .. } => false, } } } @@ -360,8 +374,10 @@ impl Args for Options { reply_to = Some(rev); } - // Review options. - Long("revision") if op == Some(OperationName::Review) => { + // Review/diff options. + Long("revision") | Short('r') + if op == Some(OperationName::Review) || op == Some(OperationName::Diff) => + { let val = parser.value()?; let rev = term::args::rev(&val)?; @@ -511,6 +527,7 @@ impl Args for Options { "y" | "ready" => op = Some(OperationName::Ready), "e" | "edit" => op = Some(OperationName::Edit), "r" | "redact" => op = Some(OperationName::Redact), + "diff" => op = Some(OperationName::Diff), "assign" => op = Some(OperationName::Assign), "label" => op = Some(OperationName::Label), "comment" => op = Some(OperationName::Comment), @@ -526,6 +543,7 @@ impl Args for Options { if patch_id.is_none() && [ Some(OperationName::Show), + Some(OperationName::Diff), Some(OperationName::Update), Some(OperationName::Delete), Some(OperationName::Archive), @@ -553,6 +571,10 @@ impl Args for Options { patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, diff, }, + OperationName::Diff => Operation::Diff { + patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, + revision_id, + }, OperationName::Delete => Operation::Delete { patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, }, @@ -652,6 +674,17 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { &workdir, )?; } + Operation::Diff { + patch_id, + revision_id, + } => { + let patch_id = patch_id.resolve(&repository.backend)?; + let revision_id = revision_id + .map(|rev| rev.resolve::(&repository.backend)) + .transpose()? + .map(patch::RevisionId::from); + diff::run(&patch_id, revision_id, &repository)?; + } Operation::Update { ref patch_id, ref base_id, diff --git a/radicle-cli/src/commands/patch/diff.rs b/radicle-cli/src/commands/patch/diff.rs new file mode 100644 index 00000000..a7030532 --- /dev/null +++ b/radicle-cli/src/commands/patch/diff.rs @@ -0,0 +1,36 @@ +use std::process; + +use radicle::cob::patch; +use radicle::storage::git::Repository; + +use super::*; + +pub fn run( + patch_id: &PatchId, + revision_id: Option, + stored: &Repository, +) -> anyhow::Result<()> { + let patches = patch::Patches::open(stored)?; + let Some(patch) = patches.get(patch_id)? else { + anyhow::bail!("Patch `{patch_id}` not found"); + }; + let revision = if let Some(r) = revision_id { + patch + .revision(&r) + .ok_or(anyhow!("revision `{r}` not found"))? + } else { + let (_, r) = patch.latest(); + r + }; + let (from, to) = revision.range(); + + process::Command::new("rad") + .current_dir(stored.path()) + .args(["diff", from.to_string().as_str(), to.to_string().as_str()]) + .stdout(process::Stdio::inherit()) + .stderr(process::Stdio::inherit()) + .spawn()? + .wait()?; + + Ok(()) +} diff --git a/radicle-cli/src/commands/patch/list.rs b/radicle-cli/src/commands/patch/list.rs index b167ad4e..1765f1c2 100644 --- a/radicle-cli/src/commands/patch/list.rs +++ b/radicle-cli/src/commands/patch/list.rs @@ -100,7 +100,7 @@ pub fn row( ) -> anyhow::Result<[term::Line; 9]> { let state = patch.state(); let (_, revision) = patch.latest(); - let (from, to) = patch.range(repository)?; + let (from, to) = revision.range(); let stats = common::diff_stats(repository.raw(), &from, &to)?; let author = patch.author().id; let (alias, did) = Author::new(&author, profile).labels(); diff --git a/radicle-cli/src/commands/patch/show.rs b/radicle-cli/src/commands/patch/show.rs index b67c3cf3..33ab0854 100644 --- a/radicle-cli/src/commands/patch/show.rs +++ b/radicle-cli/src/commands/patch/show.rs @@ -13,7 +13,7 @@ use crate::terminal as term; use super::*; fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<()> { - let (from, to) = patch.range(stored)?; + let (from, to) = patch.range()?; let range = format!("{}..{}", from, to); process::Command::new("git") @@ -28,7 +28,7 @@ fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result< } fn patch_commits(patch: &patch::Patch, stored: &Repository) -> anyhow::Result> { - let (from, to) = patch.range(stored)?; + let (from, to) = patch.range()?; let range = format!("{}..{}", from, to); let mut revwalk = stored.revwalk(*patch.head())?; diff --git a/radicle-cli/src/git/pretty_diff.rs b/radicle-cli/src/git/pretty_diff.rs index b92ca2a9..aec270f1 100644 --- a/radicle-cli/src/git/pretty_diff.rs +++ b/radicle-cli/src/git/pretty_diff.rs @@ -13,8 +13,10 @@ use crate::terminal::highlight::{Highlighter, Theme}; use super::unified_diff::{Decode, HunkHeader}; /// Blob returned by the [`Repo`] trait. +#[derive(PartialEq, Eq)] pub enum Blob { Binary, + Empty, Plain(Vec), } @@ -33,7 +35,13 @@ impl Repo for git::raw::Repository { if blob.is_binary() { Ok(Blob::Binary) } else { - Ok(Blob::Plain(blob.content().to_vec())) + let content = blob.content(); + + if content.is_empty() { + Ok(Blob::Empty) + } else { + Ok(Blob::Plain(blob.content().to_vec())) + } } } @@ -226,6 +234,14 @@ impl ToPretty for DiffContent { match context { FileDiff::Moved(_) | FileDiff::Copied(_) => {} + FileDiff::Added(_) if blobs.new.is_none() => { + vstack = vstack.divider(); + vstack.push(term::Line::new(term::format::italic("Empty file"))); + } + FileDiff::Deleted(_) if blobs.old.is_none() => { + vstack = vstack.divider(); + vstack.push(term::Line::new(term::format::italic("Empty file"))); + } FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => { vstack = vstack.divider(); diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 554c4880..ff515823 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -464,6 +464,20 @@ fn rad_patch() { test("examples/rad-patch.md", working.path(), Some(home), []).unwrap(); } +#[test] +fn rad_patch_diff() { + let mut environment = Environment::new(); + let profile = environment.profile(config::profile("alice")); + let working = tempfile::tempdir().unwrap(); + let home = &profile.home; + + // Setup a test repository. + fixtures::repository(working.path()); + + test("examples/rad-init.md", working.path(), Some(home), []).unwrap(); + test("examples/rad-patch-diff.md", working.path(), Some(home), []).unwrap(); +} + #[test] fn rad_patch_checkout() { let mut environment = Environment::new(); diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 6506b654..4da82886 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -99,7 +99,7 @@ pub enum Error { #[error("identity doc failed to load: {0}")] Doc(#[from] DocError), /// Identity document is missing. - #[error("missing identity docuemnt")] + #[error("missing identity document")] MissingIdentity, /// Error loading the document payload. #[error("payload failed to load: {0}")] @@ -118,6 +118,9 @@ pub enum Error { /// An illegal action. #[error("action is not allowed: {0}")] NotAllowed(EntryId), + /// Revision not found. + #[error("revision not found: {0}")] + RevisionNotFound(RevisionId), /// Initialization failed. #[error("initialization failed: {0}")] Init(&'static str), @@ -521,15 +524,8 @@ impl Patch { } /// Get the commit range of this patch. - pub fn range( - &self, - repo: &R, - ) -> Result<(git::Oid, git::Oid), git::ext::Error> { - if self.is_merged() { - Ok((*self.base(), *self.head())) - } else { - Ok((self.merge_base(repo)?, *self.head())) - } + pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> { + return Ok((*self.base(), *self.head())); } /// Index of latest revision in the revisions list. @@ -1331,6 +1327,11 @@ impl Revision { self.oid } + /// Get the commit range of this revision. + pub fn range(&self) -> (git::Oid, git::Oid) { + (self.base, self.oid) + } + /// When this revision was created. pub fn timestamp(&self) -> Timestamp { self.timestamp