Skip to content

Commit 6ed9976

Browse files
authored
Merge pull request #1453 from cruessler/gix-blame
Explore gix APIs, experiment with gix-blame API
2 parents 7659a65 + e951e7d commit 6ed9976

File tree

19 files changed

+3178
-28
lines changed

19 files changed

+3178
-28
lines changed

Cargo.lock

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ is usable to some extent.
139139
* [gix-shallow](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-shallow)
140140
* `gitoxide-core`
141141
* **very early** _(possibly without any documentation and many rough edges)_
142+
* [gix-blame](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-blame)
142143
* **idea** _(just a name placeholder)_
143144
* [gix-note](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-note)
144145
* [gix-fetchhead](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-fetchhead)

crate-status.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ The top-level crate that acts as hub to all functionality provided by the `gix-*
293293
* [x] safe with cycles and recursive configurations
294294
* [x] multi-line with comments and quotes
295295
* **promisor**
296-
* It's vague, but these seems to be like index files allowing to fetch objects from a server on demand.
296+
* It's vague, but these seem to be like index files allowing to fetch objects from a server on demand.
297297
* [x] API documentation
298298
* [ ] Some examples
299299

@@ -361,6 +361,25 @@ Check out the [performance discussion][gix-diff-performance] as well.
361361
* [x] API documentation
362362
* [ ] Examples
363363

364+
### gix-blame
365+
366+
* [x] commit-annotations for a single file
367+
- [ ] progress
368+
- [ ] interruptibility
369+
- [ ] streaming
370+
- [ ] support for worktree changes (creates virtual commit on top of `HEAD`)
371+
- [ ] shallow-history support
372+
- [ ] rename tracking (track different paths through history)
373+
- [ ] commits to ignore
374+
- [ ] pass all blame-cornercases (from Git)
375+
* **Performance-Improvements**
376+
* Without the following the performance isn't competitive with Git.
377+
1. Implement custom graph walk which won't run down parents that don't have the path in question.
378+
2. Implement access of trees from commit-graph and fill that information into the traversal info by default.
379+
3. commit-graph with bloom filter, used to quickly check if a commit has a path.
380+
* [x] API documentation
381+
* [ ] Examples
382+
364383
### gix-traverse
365384

366385
Check out the [performance discussion][gix-traverse-performance] as well.

gitoxide-core/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]
4949

5050
[dependencies]
5151
# deselect everything else (like "performance") as this should be controllable by the parent application.
52-
gix = { version = "^0.69.1", path = "../gix", default-features = false, features = ["merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
52+
gix = { version = "^0.69.1", path = "../gix", default-features = false, features = ["merge", "blob-diff", "blame", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
5353
gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.56.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] }
5454
gix-transport-configuration-only = { package = "gix-transport", version = "^0.44.0", path = "../gix-transport", default-features = false }
5555
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.18.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }

gitoxide-core/src/repository/blame.rs

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use gix::bstr::ByteSlice;
2+
use gix::config::tree;
3+
use std::ffi::OsStr;
4+
5+
pub fn blame_file(
6+
mut repo: gix::Repository,
7+
file: &OsStr,
8+
out: impl std::io::Write,
9+
err: Option<&mut dyn std::io::Write>,
10+
) -> anyhow::Result<()> {
11+
{
12+
let mut config = repo.config_snapshot_mut();
13+
if config.string(&tree::Core::DELTA_BASE_CACHE_LIMIT).is_none() {
14+
config.set_value(&tree::Core::DELTA_BASE_CACHE_LIMIT, "100m")?;
15+
}
16+
}
17+
let index = repo.index_or_empty()?;
18+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&index));
19+
20+
let file = gix::path::os_str_into_bstr(file)?;
21+
let specs = repo.pathspec(
22+
false,
23+
[file],
24+
true,
25+
&index,
26+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
27+
)?;
28+
// TODO: there should be a way to normalize paths without going through patterns, at least in this case maybe?
29+
// `Search` actually sorts patterns by excluding or not, all that can lead to strange results.
30+
let file = specs
31+
.search()
32+
.patterns()
33+
.map(|p| p.path().to_owned())
34+
.next()
35+
.expect("exactly one pattern");
36+
37+
let suspect = repo.head()?.peel_to_commit_in_place()?;
38+
let traverse =
39+
gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [suspect.id], None::<Vec<gix::ObjectId>>)
40+
.with_commit_graph(repo.commit_graph_if_enabled()?)
41+
.build()?;
42+
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
43+
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr())?;
44+
let statistics = outcome.statistics;
45+
write_blame_entries(out, outcome)?;
46+
47+
if let Some(err) = err {
48+
writeln!(err, "{statistics:#?}")?;
49+
}
50+
Ok(())
51+
}
52+
53+
fn write_blame_entries(mut out: impl std::io::Write, outcome: gix::blame::Outcome) -> Result<(), std::io::Error> {
54+
for (entry, lines_in_hunk) in outcome.entries_with_lines() {
55+
for ((actual_lno, source_lno), line) in entry
56+
.range_in_blamed_file()
57+
.zip(entry.range_in_source_file())
58+
.zip(lines_in_hunk)
59+
{
60+
write!(
61+
out,
62+
"{short_id} {line_no} {src_line_no} {line}",
63+
line_no = actual_lno + 1,
64+
src_line_no = source_lno + 1,
65+
short_id = entry.commit_id.to_hex_with_len(8),
66+
)?;
67+
}
68+
}
69+
70+
Ok(())
71+
}

gitoxide-core/src/repository/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub enum PathsOrPatterns {
2121
pub mod archive;
2222
pub mod cat;
2323
pub use cat::function::cat;
24+
pub mod blame;
2425
pub mod commit;
2526
pub mod config;
2627
mod credential;

gix-blame/Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name = "gix-blame"
55
version = "0.0.0"
66
repository = "https://github.com/GitoxideLabs/gitoxide"
77
license = "MIT OR Apache-2.0"
8-
description = "A crate of the gitoxide project dedicated implementing a 'blame' algorithm"
8+
description = "A crate of the gitoxide project dedicated to implementing a 'blame' algorithm"
99
authors = ["Christoph Rüßler <[email protected]>", "Sebastian Thiel <[email protected]>"]
1010
edition = "2021"
1111
rust-version = "1.65"
@@ -14,6 +14,19 @@ rust-version = "1.65"
1414
doctest = false
1515

1616
[dependencies]
17+
gix-trace = { version = "^0.1.11", path = "../gix-trace" }
18+
gix-diff = { version = "^0.49.0", path = "../gix-diff", default-features = false, features = ["blob"] }
19+
gix-object = { version = "^0.46.0", path = "../gix-object" }
20+
gix-hash = { version = "^0.15.0", path = "../gix-hash" }
21+
gix-worktree = { version = "^0.38.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
22+
gix-traverse = { version = "^0.43.0", path = "../gix-traverse" }
23+
24+
thiserror = "2.0.0"
1725

1826
[dev-dependencies]
27+
gix-ref = { version = "^0.49.0", path = "../gix-ref" }
28+
gix-filter = { version = "^0.16.0", path = "../gix-filter" }
29+
gix-fs = { version = "^0.12.0", path = "../gix-fs" }
30+
gix-index = { version = "^0.37.0", path = "../gix-index" }
31+
gix-odb = { version = "^0.66.0", path = "../gix-odb" }
1932
gix-testtools = { path = "../tests/tools" }

gix-blame/src/error.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use gix_object::bstr::BString;
2+
3+
/// The error returned by [file()](crate::file()).
4+
#[derive(Debug, thiserror::Error)]
5+
#[allow(missing_docs)]
6+
pub enum Error {
7+
#[error("No commit was given")]
8+
EmptyTraversal,
9+
#[error(transparent)]
10+
BlobDiffSetResource(#[from] gix_diff::blob::platform::set_resource::Error),
11+
#[error(transparent)]
12+
BlobDiffPrepare(#[from] gix_diff::blob::platform::prepare_diff::Error),
13+
#[error("The file to blame at '{file_path}' wasn't found in the first commit at {commit_id}")]
14+
FileMissing {
15+
/// The file-path to the object to blame.
16+
file_path: BString,
17+
/// The commit whose tree didn't contain `file_path`.
18+
commit_id: gix_hash::ObjectId,
19+
},
20+
#[error("Couldn't find commit or tree in the object database")]
21+
FindObject(#[from] gix_object::find::Error),
22+
#[error("Could not find existing blob or commit")]
23+
FindExistingObject(#[from] gix_object::find::existing_object::Error),
24+
#[error("Could not find existing iterator over a tree")]
25+
FindExistingIter(#[from] gix_object::find::existing_iter::Error),
26+
#[error("Failed to obtain the next commit in the commit-graph traversal")]
27+
Traverse(#[source] Box<dyn std::error::Error + Send + Sync>),
28+
#[error(transparent)]
29+
DiffTree(#[from] gix_diff::tree::Error),
30+
}

0 commit comments

Comments
 (0)