cli: Improve `rad diff`
* Properly show the user when a file was moved or copied. * Don't require a radicle project in the current repo.
This commit is contained in:
parent
947f1bfc01
commit
9b7c48ab40
|
|
@ -0,0 +1,83 @@
|
||||||
|
Exploring `rad diff`.
|
||||||
|
|
||||||
|
``` ./main.c
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
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 <stdio.h> │
|
||||||
|
│ 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 <stdio.h> │
|
||||||
|
│ 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 <stdio.h> │
|
||||||
|
│ 2 2 │
|
||||||
|
│ 3 3 int main(void) { │
|
||||||
|
│ 4 - printf("Hello World!/n"); │
|
||||||
|
│ 4 + printf("Hello Radicle!/n"); │
|
||||||
|
│ 5 5 return 0; │
|
||||||
|
│ 6 6 } │
|
||||||
|
╰──────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -82,7 +82,7 @@ impl Args for Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
|
pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let (repo, _) = rad::cwd()?;
|
let repo = rad::repo()?;
|
||||||
let oids = options
|
let oids = options
|
||||||
.commits
|
.commits
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
|
@ -60,8 +59,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let rid = match options.rid {
|
let rid = match options.rid {
|
||||||
Some(rid) => rid,
|
Some(rid) => rid,
|
||||||
None => {
|
None => {
|
||||||
let (_, rid) = radicle::rad::repo(Path::new("."))
|
let (_, rid) =
|
||||||
.context("Current directory is not a radicle project")?;
|
radicle::rad::cwd().context("Current directory is not a radicle project")?;
|
||||||
|
|
||||||
rid
|
rid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ impl Args for Options {
|
||||||
if let Ok(val) = Id::from_str(&val) {
|
if let Ok(val) = Id::from_str(&val) {
|
||||||
rid = Some(val);
|
rid = Some(val);
|
||||||
} else if let Ok(val) = PathBuf::from_str(&val) {
|
} else if let Ok(val) = PathBuf::from_str(&val) {
|
||||||
rid = radicle::rad::repo(val)
|
rid = radicle::rad::at(val)
|
||||||
.map(|(_, id)| Some(id))
|
.map(|(_, id)| Some(id))
|
||||||
.context("Supplied argument is not a valid path")?;
|
.context("Supplied argument is not a valid path")?;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::Path;
|
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _};
|
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 {
|
let rid = match options.rid {
|
||||||
Some(rid) => rid,
|
Some(rid) => rid,
|
||||||
None => {
|
None => {
|
||||||
let (_, rid) = radicle::rad::repo(Path::new("."))
|
let (_, rid) =
|
||||||
.context("Current directory is not a radicle project")?;
|
radicle::rad::cwd().context("Current directory is not a radicle project")?;
|
||||||
|
|
||||||
rid
|
rid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,22 @@ impl ToPretty for FileHeader {
|
||||||
) -> Self::Output {
|
) -> Self::Output {
|
||||||
match self {
|
match self {
|
||||||
FileHeader::Added { path, .. } => term::Line::new(path.display().to_string()),
|
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::Deleted { path, .. } => term::Line::new(path.display().to_string()),
|
||||||
FileHeader::Modified { 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 header = FileHeader::from(context);
|
||||||
let theme = Theme::default();
|
let theme = Theme::default();
|
||||||
|
|
||||||
let (old, new) = match context {
|
let (old, new, badge) = match context {
|
||||||
FileDiff::Added(f) => (None, Some((f.new.oid, f.path.clone()))),
|
FileDiff::Added(f) => (
|
||||||
|
None,
|
||||||
|
Some((f.new.oid, f.path.clone())),
|
||||||
|
Some(term::format::badge_positive("created")),
|
||||||
|
),
|
||||||
FileDiff::Moved(f) => (
|
FileDiff::Moved(f) => (
|
||||||
Some((f.old.oid, f.old_path.clone())),
|
Some((f.old.oid, f.old_path.clone())),
|
||||||
Some((f.new.oid, f.new_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) => (
|
FileDiff::Modified(f) => (
|
||||||
Some((f.old.oid, f.path.clone())),
|
Some((f.old.oid, f.path.clone())),
|
||||||
Some((f.new.oid, f.path.clone())),
|
Some((f.new.oid, f.path.clone())),
|
||||||
|
None,
|
||||||
),
|
),
|
||||||
FileDiff::Copied(f) => (
|
FileDiff::Copied(f) => (
|
||||||
Some((f.old.oid, f.old_path.clone())),
|
Some((f.old.oid, f.old_path.clone())),
|
||||||
Some((f.old.oid, f.new_path.clone())),
|
Some((f.old.oid, f.new_path.clone())),
|
||||||
|
Some(term::format::badge_secondary("copied")),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let mut header = header.pretty(hi, &(), repo);
|
let mut header = header.pretty(hi, &(), repo);
|
||||||
|
|
@ -172,11 +195,18 @@ impl ToPretty for DiffContent {
|
||||||
} else {
|
} else {
|
||||||
(0, 0)
|
(0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
if deletions > 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 {
|
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)));
|
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()
|
let mut vstack = term::VStack::default()
|
||||||
.border(Some(term::colors::FAINT))
|
.border(Some(term::colors::FAINT))
|
||||||
.padding(0)
|
.padding(1)
|
||||||
.child(term::Line::new(term::Label::space()).extend(header))
|
.child(term::Line::default().extend(header));
|
||||||
.divider();
|
|
||||||
|
|
||||||
match self {
|
match context {
|
||||||
DiffContent::Plain { hunks, .. } => {
|
FileDiff::Moved(_) | FileDiff::Copied(_) => {}
|
||||||
for (i, h) in hunks.iter().enumerate() {
|
FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => {
|
||||||
vstack.push(h.pretty(hi, &blobs, repo));
|
vstack = vstack.divider();
|
||||||
if i != hunks.0.len() - 1 {
|
|
||||||
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
|
vstack
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -869,6 +869,15 @@ fn rad_fork() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_diff() {
|
||||||
|
let working = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fixtures::repository(&working);
|
||||||
|
|
||||||
|
test("examples/rad-diff.md", working, None, []).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// User tries to clone; no seeds are available, but user has the repo locally.
|
// User tries to clone; no seeds are available, but user has the repo locally.
|
||||||
fn test_clone_without_seeds() {
|
fn test_clone_without_seeds() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use radicle::{
|
use radicle::{
|
||||||
storage::{WriteRepository, WriteStorage},
|
storage::{WriteRepository, WriteStorage},
|
||||||
Profile,
|
Profile,
|
||||||
|
|
@ -8,7 +6,7 @@ use radicle::{
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let profile = Profile::load()?;
|
let profile = Profile::load()?;
|
||||||
|
|
||||||
let (_, rid) = radicle::rad::repo(Path::new("."))?;
|
let (_, rid) = radicle::rad::cwd()?;
|
||||||
let repo = profile.storage.repository_mut(rid)?;
|
let repo = profile.storage.repository_mut(rid)?;
|
||||||
|
|
||||||
let id_oid = repo.set_identity_head()?;
|
let id_oid = repo.set_identity_head()?;
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// want to modify the wrong repository in the case that it found a
|
||||||
/// Git repository that is not a Radicle repository.
|
/// Git repository that is not a Radicle repository.
|
||||||
pub fn cwd() -> Result<(git2::Repository, Id), RemoteError> {
|
pub fn cwd() -> Result<(git2::Repository, Id), RemoteError> {
|
||||||
let mut flags = git2::RepositoryOpenFlags::empty();
|
let repo = repo()?;
|
||||||
// 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 (_, id) = remote(&repo)?;
|
let (_, id) = remote(&repo)?;
|
||||||
|
|
||||||
Ok((repo, id))
|
Ok((repo, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the repository of project in specified directory
|
/// Get the repository of project in specified directory
|
||||||
pub fn repo(path: impl AsRef<Path>) -> Result<(git2::Repository, Id), RemoteError> {
|
pub fn at(path: impl AsRef<Path>) -> Result<(git2::Repository, Id), RemoteError> {
|
||||||
let repo = git2::Repository::open(path)?;
|
let repo = git2::Repository::open(path)?;
|
||||||
let (_, id) = remote(&repo)?;
|
let (_, id) = remote(&repo)?;
|
||||||
|
|
||||||
Ok((repo, id))
|
Ok((repo, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current Git repository.
|
||||||
|
pub fn repo() -> Result<git2::Repository, git2::Error> {
|
||||||
|
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.
|
/// Setup patch upstream branch such that `git push` updates the patch.
|
||||||
pub fn setup_patch_upstream<'a>(
|
pub fn setup_patch_upstream<'a>(
|
||||||
patch: &ObjectId,
|
patch: &ObjectId,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue