diff --git a/gitoxide-core/src/repository/diff.rs b/gitoxide-core/src/repository/diff.rs new file mode 100644 index 0000000000..18ca60f9e8 --- /dev/null +++ b/gitoxide-core/src/repository/diff.rs @@ -0,0 +1,111 @@ +use gix::bstr::{BString, ByteSlice}; +use gix::objs::tree::EntryMode; +use gix::prelude::ObjectIdExt; + +pub fn tree( + mut repo: gix::Repository, + out: &mut dyn std::io::Write, + old_treeish: BString, + new_treeish: BString, +) -> anyhow::Result<()> { + repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?)); + + let old_tree_id = repo.rev_parse_single(old_treeish.as_bstr())?; + let new_tree_id = repo.rev_parse_single(new_treeish.as_bstr())?; + + let old_tree = old_tree_id.object()?.peel_to_tree()?; + let new_tree = new_tree_id.object()?.peel_to_tree()?; + + let changes = repo.diff_tree_to_tree(&old_tree, &new_tree, None)?; + + writeln!( + out, + "Diffing trees `{old_treeish}` ({old_tree_id}) -> `{new_treeish}` ({new_tree_id})\n" + )?; + write_changes(&repo, out, changes)?; + + Ok(()) +} + +fn write_changes( + repo: &gix::Repository, + mut out: impl std::io::Write, + changes: Vec, +) -> Result<(), std::io::Error> { + for change in changes { + match change { + gix::diff::tree_with_rewrites::Change::Addition { + location, + id, + entry_mode, + .. + } => { + writeln!(out, "A: {}", typed_location(location, entry_mode))?; + writeln!(out, " {}", id.attach(repo).shorten_or_id())?; + writeln!(out, " -> {:o}", entry_mode.0)?; + } + gix::diff::tree_with_rewrites::Change::Deletion { + location, + id, + entry_mode, + .. + } => { + writeln!(out, "D: {}", typed_location(location, entry_mode))?; + writeln!(out, " {}", id.attach(repo).shorten_or_id())?; + writeln!(out, " {:o} ->", entry_mode.0)?; + } + gix::diff::tree_with_rewrites::Change::Modification { + location, + previous_id, + id, + previous_entry_mode, + entry_mode, + } => { + writeln!(out, "M: {}", typed_location(location, entry_mode))?; + writeln!( + out, + " {previous_id} -> {id}", + previous_id = previous_id.attach(repo).shorten_or_id(), + id = id.attach(repo).shorten_or_id() + )?; + if previous_entry_mode != entry_mode { + writeln!(out, " {:o} -> {:o}", previous_entry_mode.0, entry_mode.0)?; + } + } + gix::diff::tree_with_rewrites::Change::Rewrite { + source_location, + source_id, + id, + location, + source_entry_mode, + entry_mode, + .. + } => { + writeln!( + out, + "R: {source} -> {dest}", + source = typed_location(source_location, source_entry_mode), + dest = typed_location(location, entry_mode) + )?; + writeln!( + out, + " {source_id} -> {id}", + source_id = source_id.attach(repo).shorten_or_id(), + id = id.attach(repo).shorten_or_id() + )?; + if source_entry_mode != entry_mode { + writeln!(out, " {:o} -> {:o}", source_entry_mode.0, entry_mode.0)?; + } + } + }; + } + + Ok(()) +} + +fn typed_location(mut location: BString, mode: EntryMode) -> BString { + if mode.is_tree() { + location.push(b'/'); + } + location +} diff --git a/gitoxide-core/src/repository/mod.rs b/gitoxide-core/src/repository/mod.rs index ba8c35ef08..489d5c32e6 100644 --- a/gitoxide-core/src/repository/mod.rs +++ b/gitoxide-core/src/repository/mod.rs @@ -28,6 +28,7 @@ pub use credential::function as credential; pub mod attributes; #[cfg(feature = "clean")] pub mod clean; +pub mod diff; pub mod dirty; #[cfg(feature = "clean")] pub use clean::function::clean; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 983faea77d..59df7c431e 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -189,6 +189,22 @@ pub fn main() -> Result<()> { core::repository::merge_base(repository(Mode::Lenient)?, first, others, out, format) }, ), + Subcommands::Diff(crate::plumbing::options::diff::Platform { cmd }) => match cmd { + crate::plumbing::options::diff::SubCommands::Tree { + old_treeish, + new_treeish, + } => prepare_and_run( + "diff-tree", + trace, + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::diff::tree(repository(Mode::Lenient)?, out, old_treeish, new_treeish) + }, + ), + }, Subcommands::Worktree(crate::plumbing::options::worktree::Platform { cmd }) => match cmd { crate::plumbing::options::worktree::SubCommands::List => prepare_and_run( "worktree-list", diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index c1813336b6..b6b1b005f1 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -145,6 +145,7 @@ pub enum Subcommands { Corpus(corpus::Platform), MergeBase(merge_base::Command), Merge(merge::Platform), + Diff(diff::Platform), Worktree(worktree::Platform), /// Subcommands that need no git repository to run. #[clap(subcommand)] @@ -371,19 +372,43 @@ pub mod merge { #[clap(long, short = 'c')] resolve_with: Option, - /// A path or revspec to our file + /// A path or revspec to our file. #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] ours: BString, - /// A path or revspec to the base for both ours and theirs + /// A path or revspec to the base for both ours and theirs. #[clap(value_name = "BASE", value_parser = crate::shared::AsBString)] base: BString, - /// A path or revspec to their file + /// A path or revspec to their file. #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] theirs: BString, }, } } +pub mod diff { + use gix::bstr::BString; + + /// Print all changes between two objects + #[derive(Debug, clap::Parser)] + pub struct Platform { + #[clap(subcommand)] + pub cmd: SubCommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum SubCommands { + /// Diff two trees. + Tree { + /// A rev-spec representing the 'before' or old tree. + #[clap(value_parser = crate::shared::AsBString)] + old_treeish: BString, + /// A rev-spec representing the 'after' or new tree. + #[clap(value_parser = crate::shared::AsBString)] + new_treeish: BString, + }, + } +} + pub mod config { use gix::bstr::BString;