diff --git a/radicle-cli/examples/rad-diff.md b/radicle-cli/examples/rad-diff.md new file mode 100644 index 00000000..3c07e63c --- /dev/null +++ b/radicle-cli/examples/rad-diff.md @@ -0,0 +1,83 @@ +Exploring `rad diff`. + +``` ./main.c +#include + +int main(void) { + printf("Hello World!\n"); + return 0; +} +``` + +``` +$ ls +README +main.c +``` + +``` +$ git mv README README.md +$ git add main.c +$ git commit -m "Make changes" +[master 5f771e0] Make changes + 2 files changed, 6 insertions(+) + rename README => README.md (100%) + create mode 100644 main.c +``` + +``` +$ rad diff HEAD^ HEAD +╭────────────────────────────────────────────╮ +│ README -> README.md ❲moved❳ │ +╰────────────────────────────────────────────╯ + +╭────────────────────────────────────────────╮ +│ main.c +6 ❲created❳ │ +├────────────────────────────────────────────┤ +│ @@ -0,0 +1,6 @@ │ +│ 1 + #include │ +│ 2 + │ +│ 3 + int main(void) { │ +│ 4 + printf("Hello World!/n"); │ +│ 5 + return 0; │ +│ 6 + } │ +╰────────────────────────────────────────────╯ + +``` + +``` +$ sed -i 's/Hello World/Hello Radicle/' main.c +$ rad diff +╭──────────────────────────────────────────────╮ +│ main.c -1 +1 │ +├──────────────────────────────────────────────┤ +│ @@ -1,6 +1,6 @@ │ +│ 1 1 #include │ +│ 2 2 │ +│ 3 3 int main(void) { │ +│ 4 - printf("Hello World!/n"); │ +│ 4 + printf("Hello Radicle!/n"); │ +│ 5 5 return 0; │ +│ 6 6 } │ +╰──────────────────────────────────────────────╯ + +``` + +``` +$ git add main.c +$ rad diff +$ rad diff --staged +╭──────────────────────────────────────────────╮ +│ main.c -1 +1 │ +├──────────────────────────────────────────────┤ +│ @@ -1,6 +1,6 @@ │ +│ 1 1 #include │ +│ 2 2 │ +│ 3 3 int main(void) { │ +│ 4 - printf("Hello World!/n"); │ +│ 4 + printf("Hello Radicle!/n"); │ +│ 5 5 return 0; │ +│ 6 6 } │ +╰──────────────────────────────────────────────╯ + +``` diff --git a/radicle-cli/src/commands/diff.rs b/radicle-cli/src/commands/diff.rs index 55182958..e82dcf31 100644 --- a/radicle-cli/src/commands/diff.rs +++ b/radicle-cli/src/commands/diff.rs @@ -82,7 +82,7 @@ impl Args for Options { } pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> { - let (repo, _) = rad::cwd()?; + let repo = rad::repo()?; let oids = options .commits .into_iter() diff --git a/radicle-cli/src/commands/fork.rs b/radicle-cli/src/commands/fork.rs index fb54e13a..379dcdac 100644 --- a/radicle-cli/src/commands/fork.rs +++ b/radicle-cli/src/commands/fork.rs @@ -1,5 +1,4 @@ use std::ffi::OsString; -use std::path::Path; use anyhow::Context as _; @@ -60,8 +59,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let rid = match options.rid { Some(rid) => rid, None => { - let (_, rid) = radicle::rad::repo(Path::new(".")) - .context("Current directory is not a radicle project")?; + let (_, rid) = + radicle::rad::cwd().context("Current directory is not a radicle project")?; rid } diff --git a/radicle-cli/src/commands/inspect.rs b/radicle-cli/src/commands/inspect.rs index 8508c59a..257341a6 100644 --- a/radicle-cli/src/commands/inspect.rs +++ b/radicle-cli/src/commands/inspect.rs @@ -107,7 +107,7 @@ impl Args for Options { if let Ok(val) = Id::from_str(&val) { rid = Some(val); } else if let Ok(val) = PathBuf::from_str(&val) { - rid = radicle::rad::repo(val) + rid = radicle::rad::at(val) .map(|(_, id)| Some(id)) .context("Supplied argument is not a valid path")?; } else { diff --git a/radicle-cli/src/commands/sync.rs b/radicle-cli/src/commands/sync.rs index 1db0f0ec..74f14e62 100644 --- a/radicle-cli/src/commands/sync.rs +++ b/radicle-cli/src/commands/sync.rs @@ -1,5 +1,4 @@ use std::ffi::OsString; -use std::path::Path; use std::time; use anyhow::{anyhow, Context as _}; @@ -208,8 +207,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let rid = match options.rid { Some(rid) => rid, None => { - let (_, rid) = radicle::rad::repo(Path::new(".")) - .context("Current directory is not a radicle project")?; + let (_, rid) = + radicle::rad::cwd().context("Current directory is not a radicle project")?; rid } diff --git a/radicle-cli/src/git/pretty_diff.rs b/radicle-cli/src/git/pretty_diff.rs index ffd988e5..b92ca2a9 100644 --- a/radicle-cli/src/git/pretty_diff.rs +++ b/radicle-cli/src/git/pretty_diff.rs @@ -104,10 +104,22 @@ impl ToPretty for FileHeader { ) -> Self::Output { match self { FileHeader::Added { path, .. } => term::Line::new(path.display().to_string()), - FileHeader::Moved { new_path, .. } => term::Line::new(new_path.display().to_string()), + FileHeader::Moved { + old_path, new_path, .. + } => term::Line::spaced([ + term::label(old_path.display().to_string()), + term::label("->".to_string()), + term::label(new_path.display().to_string()), + ]), FileHeader::Deleted { path, .. } => term::Line::new(path.display().to_string()), FileHeader::Modified { path, .. } => term::Line::new(path.display().to_string()), - FileHeader::Copied { new_path, .. } => term::Line::new(new_path.display().to_string()), + FileHeader::Copied { + old_path, new_path, .. + } => term::Line::spaced([ + term::label(old_path.display().to_string()), + term::label("->".to_string()), + term::label(new_path.display().to_string()), + ]), } } } @@ -149,20 +161,31 @@ impl ToPretty for DiffContent { let header = FileHeader::from(context); let theme = Theme::default(); - let (old, new) = match context { - FileDiff::Added(f) => (None, Some((f.new.oid, f.path.clone()))), + let (old, new, badge) = match context { + FileDiff::Added(f) => ( + None, + Some((f.new.oid, f.path.clone())), + Some(term::format::badge_positive("created")), + ), FileDiff::Moved(f) => ( Some((f.old.oid, f.old_path.clone())), Some((f.new.oid, f.new_path.clone())), + Some(term::format::badge_secondary("moved")), + ), + FileDiff::Deleted(f) => ( + Some((f.old.oid, f.path.clone())), + None, + Some(term::format::badge_negative("deleted")), ), - FileDiff::Deleted(f) => (Some((f.old.oid, f.path.clone())), None), FileDiff::Modified(f) => ( Some((f.old.oid, f.path.clone())), Some((f.new.oid, f.path.clone())), + None, ), FileDiff::Copied(f) => ( Some((f.old.oid, f.old_path.clone())), Some((f.old.oid, f.new_path.clone())), + Some(term::format::badge_secondary("copied")), ), }; let mut header = header.pretty(hi, &(), repo); @@ -172,11 +195,18 @@ impl ToPretty for DiffContent { } else { (0, 0) }; + if deletions > 0 { - header.push(term::label(format!(" -{deletions}")).fg(theme.color("negative.light"))); + header.push(term::Label::space()); + header.push(term::label(format!("-{deletions}")).fg(theme.color("negative.light"))); } if additions > 0 { - header.push(term::label(format!(" +{additions}")).fg(theme.color("positive.light"))); + header.push(term::Label::space()); + header.push(term::label(format!("+{additions}")).fg(theme.color("positive.light"))); + } + if let Some(badge) = badge { + header.push(term::Label::space()); + header.push(badge); } let old = old.and_then(|(oid, path)| repo.blob(oid).ok().or_else(|| repo.file(&path))); @@ -191,25 +221,31 @@ impl ToPretty for DiffContent { } let mut vstack = term::VStack::default() .border(Some(term::colors::FAINT)) - .padding(0) - .child(term::Line::new(term::Label::space()).extend(header)) - .divider(); + .padding(1) + .child(term::Line::default().extend(header)); - match self { - DiffContent::Plain { hunks, .. } => { - for (i, h) in hunks.iter().enumerate() { - vstack.push(h.pretty(hi, &blobs, repo)); - if i != hunks.0.len() - 1 { - vstack = vstack.divider(); + match context { + FileDiff::Moved(_) | FileDiff::Copied(_) => {} + FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => { + vstack = vstack.divider(); + + match self { + DiffContent::Plain { hunks, .. } => { + for (i, h) in hunks.iter().enumerate() { + vstack.push(h.pretty(hi, &blobs, repo)); + if i != hunks.0.len() - 1 { + vstack = vstack.divider(); + } + } + } + DiffContent::Empty => { + vstack.push(term::Line::new(term::format::italic("Empty file"))); + } + DiffContent::Binary => { + vstack.push(term::Line::new(term::format::italic("Binary file"))); } } } - DiffContent::Empty => { - vstack.push(term::Line::new(term::format::italic("Empty file"))); - } - DiffContent::Binary => { - vstack.push(term::Line::new(term::format::italic("Binary file"))); - } } vstack } diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 10fdd0dc..665e254c 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -869,6 +869,15 @@ fn rad_fork() { .unwrap(); } +#[test] +fn rad_diff() { + let working = tempfile::tempdir().unwrap(); + + fixtures::repository(&working); + + test("examples/rad-diff.md", working, None, []).unwrap(); +} + #[test] // User tries to clone; no seeds are available, but user has the repo locally. fn test_clone_without_seeds() { diff --git a/radicle-tools/src/rad-set-canonical-refs.rs b/radicle-tools/src/rad-set-canonical-refs.rs index 34c38513..5d6d6cd0 100644 --- a/radicle-tools/src/rad-set-canonical-refs.rs +++ b/radicle-tools/src/rad-set-canonical-refs.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use radicle::{ storage::{WriteRepository, WriteStorage}, Profile, @@ -8,7 +6,7 @@ use radicle::{ fn main() -> anyhow::Result<()> { let profile = Profile::load()?; - let (_, rid) = radicle::rad::repo(Path::new("."))?; + let (_, rid) = radicle::rad::cwd()?; let repo = profile.storage.repository_mut(rid)?; let id_oid = repo.set_identity_head()?; diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index c7fc0e31..f062f5d4 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -279,26 +279,34 @@ pub fn remove_remote(repo: &git2::Repository) -> Result<(), RemoteError> { /// want to modify the wrong repository in the case that it found a /// Git repository that is not a Radicle repository. pub fn cwd() -> Result<(git2::Repository, Id), RemoteError> { - let mut flags = git2::RepositoryOpenFlags::empty(); - // Allow to search upwards. - flags.set(git2::RepositoryOpenFlags::NO_SEARCH, false); - // Allow to use `GIT_DIR` env. - flags.set(git2::RepositoryOpenFlags::FROM_ENV, true); - let ceilings: &[&str] = &[]; - let repo = git2::Repository::open_ext(Path::new("."), flags, ceilings)?; + let repo = repo()?; let (_, id) = remote(&repo)?; Ok((repo, id)) } /// Get the repository of project in specified directory -pub fn repo(path: impl AsRef) -> Result<(git2::Repository, Id), RemoteError> { +pub fn at(path: impl AsRef) -> Result<(git2::Repository, Id), RemoteError> { let repo = git2::Repository::open(path)?; let (_, id) = remote(&repo)?; Ok((repo, id)) } +/// Get the current Git repository. +pub fn repo() -> Result { + let mut flags = git2::RepositoryOpenFlags::empty(); + // Allow to search upwards. + flags.set(git2::RepositoryOpenFlags::NO_SEARCH, false); + // Allow to use `GIT_DIR` env. + flags.set(git2::RepositoryOpenFlags::FROM_ENV, true); + + let ceilings: &[&str] = &[]; + let repo = git2::Repository::open_ext(Path::new("."), flags, ceilings)?; + + Ok(repo) +} + /// Setup patch upstream branch such that `git push` updates the patch. pub fn setup_patch_upstream<'a>( patch: &ObjectId,