Skip to content

Add gix diff file #1880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions gitoxide-core/src/repository/diff.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use anyhow::Context;
use gix::bstr::{BString, ByteSlice};
use gix::diff::blob::intern::TokenSource;
use gix::diff::blob::unified_diff::{ContextSize, NewlineSeparator};
use gix::diff::blob::UnifiedDiff;
use gix::objs::tree::EntryMode;
use gix::odb::store::RefreshMode;
use gix::prelude::ObjectIdExt;
Expand Down Expand Up @@ -111,3 +115,86 @@ fn typed_location(mut location: BString, mode: EntryMode) -> BString {
}
location
}

pub fn file(
mut repo: gix::Repository,
out: &mut dyn std::io::Write,
old_revspec: BString,
new_revspec: BString,
) -> Result<(), anyhow::Error> {
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
repo.objects.refresh = RefreshMode::Never;

let old_resolved_revspec = repo.rev_parse(old_revspec.as_bstr())?;
let new_resolved_revspec = repo.rev_parse(new_revspec.as_bstr())?;

let old_blob_id = old_resolved_revspec
.single()
.context(format!("rev-spec '{old_revspec}' must resolve to a single object"))?;
let new_blob_id = new_resolved_revspec
.single()
.context(format!("rev-spec '{new_revspec}' must resolve to a single object"))?;

let (old_path, _) = old_resolved_revspec
.path_and_mode()
.context(format!("rev-spec '{old_revspec}' must contain a path"))?;
let (new_path, _) = new_resolved_revspec
.path_and_mode()
.context(format!("rev-spec '{new_revspec}' must contain a path"))?;

let mut resource_cache = repo.diff_resource_cache(
gix::diff::blob::pipeline::Mode::ToGitUnlessBinaryToTextIsPresent,
Default::default(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be worktree-roots - one must set the root of source and/or destinations if any of these have a null-id. Then it will be read from the worktree instead.
Please note that set_resource() probably still is a bit unintuitive, but it should suffice for this one-shot version of diffing files.

)?;

resource_cache.set_resource(
old_blob_id.into(),
gix::object::tree::EntryKind::Blob,
old_path,
gix::diff::blob::ResourceKind::OldOrSource,
&repo.objects,
)?;
resource_cache.set_resource(
new_blob_id.into(),
gix::object::tree::EntryKind::Blob,
new_path,
gix::diff::blob::ResourceKind::NewOrDestination,
&repo.objects,
)?;

let outcome = resource_cache.prepare_diff()?;

use gix::diff::blob::platform::prepare_diff::Operation;

let algorithm = match outcome.operation {
Operation::InternalDiff { algorithm } => algorithm,
Operation::ExternalCommand { .. } => {
unreachable!("We disabled that")
}
Operation::SourceOrDestinationIsBinary => {
anyhow::bail!("Source or destination is binary and we can't diff that")
}
};

let interner = gix::diff::blob::intern::InternedInput::new(
tokens_for_diffing(outcome.old.data.as_slice().unwrap_or_default()),
tokens_for_diffing(outcome.new.data.as_slice().unwrap_or_default()),
);

let unified_diff = UnifiedDiff::new(
&interner,
String::new(),
NewlineSeparator::AfterHeaderAndLine("\n"),
ContextSize::symmetrical(3),
);

let unified_diff = gix::diff::blob::diff(algorithm, &interner, unified_diff)?;

out.write_all(unified_diff.as_bytes())?;

Ok(())
}

pub(crate) fn tokens_for_diffing(data: &[u8]) -> impl TokenSource<Token = &[u8]> {
gix::diff::blob::sources::byte_lines(data)
}
14 changes: 14 additions & 0 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,20 @@ pub fn main() -> Result<()> {
core::repository::diff::tree(repository(Mode::Lenient)?, out, old_treeish, new_treeish)
},
),
crate::plumbing::options::diff::SubCommands::File {
old_revspec,
new_revspec,
} => prepare_and_run(
"diff-file",
trace,
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, _err| {
core::repository::diff::file(repository(Mode::Lenient)?, out, old_revspec, new_revspec)
},
),
},
Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => prepare_and_run(
"log",
Expand Down
9 changes: 9 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,15 @@ pub mod diff {
#[clap(value_parser = crate::shared::AsBString)]
new_treeish: BString,
},
/// Diff two versions of a file.
File {
/// A rev-spec representing the 'before' or old state of the file, like '@~100:file'
#[clap(value_parser = crate::shared::AsBString)]
old_revspec: BString,
/// A rev-spec representing the 'after' or new state of the file, like ':file'
#[clap(value_parser = crate::shared::AsBString)]
new_revspec: BString,
},
}
}

Expand Down
Loading