cli: Add `rad patch diff` command

This commit is contained in:
cloudhead 2024-01-03 12:08:09 +01:00
parent 04bfbca15d
commit 91684cc9ca
No known key found for this signature in database
9 changed files with 190 additions and 19 deletions

View File

@ -15,6 +15,7 @@ rad-patch - Manage radicle patches.
*rad patch* [<option>...] + *rad patch* [<option>...] +
*rad patch* _list_ [--all|--merged|--open|--archived|--draft] [<option>...] + *rad patch* _list_ [--all|--merged|--open|--archived|--draft] [<option>...] +
*rad patch* _show_ <patch-id> [<option>...] + *rad patch* _show_ <patch-id> [<option>...] +
*rad patch* _diff_ <patch-id> [--revision <revision-id>] [<option>...] +
*rad patch* _archive_ <patch-id> [<option>...] + *rad patch* _archive_ <patch-id> [<option>...] +
*rad patch* _update_ <patch-id> [<option>...] + *rad patch* _update_ <patch-id> [<option>...] +
*rad patch* _checkout_ <patch-id> [<option>...] + *rad patch* _checkout_ <patch-id> [<option>...] +
@ -52,6 +53,13 @@ Shows information on the given patch.
*--patch*, *-p*:: Show the patch changes in git patch format *--patch*, *-p*:: Show the patch changes in git patch format
*--verbose*, *-v*:: Show additional information about the patch *--verbose*, *-v*:: Show additional information about the patch
=== diff
Outputs the patch diff, using Radicle's diffing tool.
*<patch-id>*:: The patch to diff
*--revision*, *-r <revision-id>*:: The revision to diff (default: latest)
=== edit === edit
Edits a patch revision comment. To edit the patch title or description, pass Edits a patch revision comment. To edit the patch title or description, pass

View File

@ -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! │
╰───────────────────────────╯
```

View File

@ -10,6 +10,8 @@ mod comment;
mod common; mod common;
#[path = "patch/delete.rs"] #[path = "patch/delete.rs"]
mod delete; mod delete;
#[path = "patch/diff.rs"]
mod diff;
#[path = "patch/edit.rs"] #[path = "patch/edit.rs"]
mod edit; mod edit;
#[path = "patch/label.rs"] #[path = "patch/label.rs"]
@ -53,6 +55,7 @@ Usage
rad patch [<option>...] rad patch [<option>...]
rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...] rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...]
rad patch show <patch-id> [<option>...] rad patch show <patch-id> [<option>...]
rad patch diff <patch-id> [<option>...]
rad patch archive <patch-id> [<option>...] rad patch archive <patch-id> [<option>...]
rad patch update <patch-id> [<option>...] rad patch update <patch-id> [<option>...]
rad patch checkout <patch-id> [<option>...] rad patch checkout <patch-id> [<option>...]
@ -71,6 +74,10 @@ Show options
-p, --patch Show the actual patch diff -p, --patch Show the actual patch diff
-v, --verbose Show additional information about the patch -v, --verbose Show additional information about the patch
Diff options
-r, --revision <id> The revision to diff (default: latest)
Comment options Comment options
-m, --message <string> Provide a comment message via the command-line -m, --message <string> Provide a comment message via the command-line
@ -81,14 +88,13 @@ Edit options
Review options Review options
--revision <id> Review the given revision of the patch -r, --revision <id> Review the given revision of the patch
-p, --patch Review by patch hunks -p, --patch Review by patch hunks
--hunk <index> Only review a specific hunk --hunk <index> Only review a specific hunk
--accept Accept a patch or set of hunks --accept Accept a patch or set of hunks
--reject Reject a patch or set of hunks --reject Reject a patch or set of hunks
-U, --unified <n> Generate diffs with <n> lines of context instead of the usual three -U, --unified <n> Generate diffs with <n> lines of context instead of the usual three
-d, --delete Delete a review draft -d, --delete Delete a review draft
-r, --revision Review a patch revision
-m, --message [<string>] Provide a comment with the review (default: prompt) -m, --message [<string>] Provide a comment with the review (default: prompt)
Assign options Assign options
@ -146,6 +152,7 @@ Other options
pub enum OperationName { pub enum OperationName {
Assign, Assign,
Show, Show,
Diff,
Update, Update,
Archive, Archive,
Delete, Delete,
@ -200,6 +207,10 @@ pub enum Operation {
patch_id: Rev, patch_id: Rev,
diff: bool, diff: bool,
}, },
Diff {
patch_id: Rev,
revision_id: Option<Rev>,
},
Update { Update {
patch_id: Rev, patch_id: Rev,
base_id: Option<Rev>, base_id: Option<Rev>,
@ -267,7 +278,10 @@ impl Operation {
| Operation::Edit { .. } | Operation::Edit { .. }
| Operation::Redact { .. } | Operation::Redact { .. }
| Operation::Set { .. } => true, | 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); reply_to = Some(rev);
} }
// Review options. // Review/diff options.
Long("revision") if op == Some(OperationName::Review) => { Long("revision") | Short('r')
if op == Some(OperationName::Review) || op == Some(OperationName::Diff) =>
{
let val = parser.value()?; let val = parser.value()?;
let rev = term::args::rev(&val)?; let rev = term::args::rev(&val)?;
@ -511,6 +527,7 @@ impl Args for Options {
"y" | "ready" => op = Some(OperationName::Ready), "y" | "ready" => op = Some(OperationName::Ready),
"e" | "edit" => op = Some(OperationName::Edit), "e" | "edit" => op = Some(OperationName::Edit),
"r" | "redact" => op = Some(OperationName::Redact), "r" | "redact" => op = Some(OperationName::Redact),
"diff" => op = Some(OperationName::Diff),
"assign" => op = Some(OperationName::Assign), "assign" => op = Some(OperationName::Assign),
"label" => op = Some(OperationName::Label), "label" => op = Some(OperationName::Label),
"comment" => op = Some(OperationName::Comment), "comment" => op = Some(OperationName::Comment),
@ -526,6 +543,7 @@ impl Args for Options {
if patch_id.is_none() if patch_id.is_none()
&& [ && [
Some(OperationName::Show), Some(OperationName::Show),
Some(OperationName::Diff),
Some(OperationName::Update), Some(OperationName::Update),
Some(OperationName::Delete), Some(OperationName::Delete),
Some(OperationName::Archive), Some(OperationName::Archive),
@ -553,6 +571,10 @@ impl Args for Options {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
diff, diff,
}, },
OperationName::Diff => Operation::Diff {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
revision_id,
},
OperationName::Delete => Operation::Delete { OperationName::Delete => Operation::Delete {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, 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, &workdir,
)?; )?;
} }
Operation::Diff {
patch_id,
revision_id,
} => {
let patch_id = patch_id.resolve(&repository.backend)?;
let revision_id = revision_id
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()?
.map(patch::RevisionId::from);
diff::run(&patch_id, revision_id, &repository)?;
}
Operation::Update { Operation::Update {
ref patch_id, ref patch_id,
ref base_id, ref base_id,

View File

@ -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<patch::RevisionId>,
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(())
}

View File

@ -100,7 +100,7 @@ pub fn row(
) -> anyhow::Result<[term::Line; 9]> { ) -> anyhow::Result<[term::Line; 9]> {
let state = patch.state(); let state = patch.state();
let (_, revision) = patch.latest(); 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 stats = common::diff_stats(repository.raw(), &from, &to)?;
let author = patch.author().id; let author = patch.author().id;
let (alias, did) = Author::new(&author, profile).labels(); let (alias, did) = Author::new(&author, profile).labels();

View File

@ -13,7 +13,7 @@ use crate::terminal as term;
use super::*; use super::*;
fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<()> { 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); let range = format!("{}..{}", from, to);
process::Command::new("git") 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<Vec<term::Line>> { fn patch_commits(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<Vec<term::Line>> {
let (from, to) = patch.range(stored)?; let (from, to) = patch.range()?;
let range = format!("{}..{}", from, to); let range = format!("{}..{}", from, to);
let mut revwalk = stored.revwalk(*patch.head())?; let mut revwalk = stored.revwalk(*patch.head())?;

View File

@ -13,8 +13,10 @@ use crate::terminal::highlight::{Highlighter, Theme};
use super::unified_diff::{Decode, HunkHeader}; use super::unified_diff::{Decode, HunkHeader};
/// Blob returned by the [`Repo`] trait. /// Blob returned by the [`Repo`] trait.
#[derive(PartialEq, Eq)]
pub enum Blob { pub enum Blob {
Binary, Binary,
Empty,
Plain(Vec<u8>), Plain(Vec<u8>),
} }
@ -32,10 +34,16 @@ impl Repo for git::raw::Repository {
if blob.is_binary() { if blob.is_binary() {
Ok(Blob::Binary) Ok(Blob::Binary)
} else {
let content = blob.content();
if content.is_empty() {
Ok(Blob::Empty)
} else { } else {
Ok(Blob::Plain(blob.content().to_vec())) Ok(Blob::Plain(blob.content().to_vec()))
} }
} }
}
fn file(&self, path: &Path) -> Option<Blob> { fn file(&self, path: &Path) -> Option<Blob> {
self.workdir() self.workdir()
@ -226,6 +234,14 @@ impl ToPretty for DiffContent {
match context { match context {
FileDiff::Moved(_) | FileDiff::Copied(_) => {} 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(_) => { FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => {
vstack = vstack.divider(); vstack = vstack.divider();

View File

@ -464,6 +464,20 @@ fn rad_patch() {
test("examples/rad-patch.md", working.path(), Some(home), []).unwrap(); 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] #[test]
fn rad_patch_checkout() { fn rad_patch_checkout() {
let mut environment = Environment::new(); let mut environment = Environment::new();

View File

@ -99,7 +99,7 @@ pub enum Error {
#[error("identity doc failed to load: {0}")] #[error("identity doc failed to load: {0}")]
Doc(#[from] DocError), Doc(#[from] DocError),
/// Identity document is missing. /// Identity document is missing.
#[error("missing identity docuemnt")] #[error("missing identity document")]
MissingIdentity, MissingIdentity,
/// Error loading the document payload. /// Error loading the document payload.
#[error("payload failed to load: {0}")] #[error("payload failed to load: {0}")]
@ -118,6 +118,9 @@ pub enum Error {
/// An illegal action. /// An illegal action.
#[error("action is not allowed: {0}")] #[error("action is not allowed: {0}")]
NotAllowed(EntryId), NotAllowed(EntryId),
/// Revision not found.
#[error("revision not found: {0}")]
RevisionNotFound(RevisionId),
/// Initialization failed. /// Initialization failed.
#[error("initialization failed: {0}")] #[error("initialization failed: {0}")]
Init(&'static str), Init(&'static str),
@ -521,15 +524,8 @@ impl Patch {
} }
/// Get the commit range of this patch. /// Get the commit range of this patch.
pub fn range<R: ReadRepository>( pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
&self, return Ok((*self.base(), *self.head()));
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()))
}
} }
/// Index of latest revision in the revisions list. /// Index of latest revision in the revisions list.
@ -1331,6 +1327,11 @@ impl Revision {
self.oid 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. /// When this revision was created.
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
self.timestamp self.timestamp