Skip to content

Explore gix APIs, experiment with gix-blame API #1453

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 16 commits into from
Dec 25, 2024
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
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ is usable to some extent.
* [gix-shallow](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-shallow)
* `gitoxide-core`
* **very early** _(possibly without any documentation and many rough edges)_
* [gix-blame](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-blame)
* **idea** _(just a name placeholder)_
* [gix-note](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-note)
* [gix-fetchhead](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-fetchhead)
Expand Down
21 changes: 20 additions & 1 deletion crate-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ The top-level crate that acts as hub to all functionality provided by the `gix-*
* [x] safe with cycles and recursive configurations
* [x] multi-line with comments and quotes
* **promisor**
* It's vague, but these seems to be like index files allowing to fetch objects from a server on demand.
* It's vague, but these seem to be like index files allowing to fetch objects from a server on demand.
* [x] API documentation
* [ ] Some examples

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

### gix-blame

* [x] commit-annotations for a single file
- [ ] progress
- [ ] interruptibility
- [ ] streaming
- [ ] support for worktree changes (creates virtual commit on top of `HEAD`)
- [ ] shallow-history support
- [ ] rename tracking (track different paths through history)
- [ ] commits to ignore
- [ ] pass all blame-cornercases (from Git)
* **Performance-Improvements**
* Without the following the performance isn't competitive with Git.
1. Implement custom graph walk which won't run down parents that don't have the path in question.
2. Implement access of trees from commit-graph and fill that information into the traversal info by default.
3. commit-graph with bloom filter, used to quickly check if a commit has a path.
* [x] API documentation
* [ ] Examples

### gix-traverse

Check out the [performance discussion][gix-traverse-performance] as well.
Expand Down
2 changes: 1 addition & 1 deletion gitoxide-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]

[dependencies]
# deselect everything else (like "performance") as this should be controllable by the parent application.
gix = { version = "^0.69.1", path = "../gix", default-features = false, features = ["merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
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"] }
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"] }
gix-transport-configuration-only = { package = "gix-transport", version = "^0.44.0", path = "../gix-transport", default-features = false }
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.18.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }
Expand Down
71 changes: 71 additions & 0 deletions gitoxide-core/src/repository/blame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use gix::bstr::ByteSlice;
use gix::config::tree;
use std::ffi::OsStr;

pub fn blame_file(
mut repo: gix::Repository,
file: &OsStr,
out: impl std::io::Write,
err: Option<&mut dyn std::io::Write>,
) -> anyhow::Result<()> {
{
let mut config = repo.config_snapshot_mut();
if config.string(&tree::Core::DELTA_BASE_CACHE_LIMIT).is_none() {
config.set_value(&tree::Core::DELTA_BASE_CACHE_LIMIT, "100m")?;
}
}
let index = repo.index_or_empty()?;
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&index));

let file = gix::path::os_str_into_bstr(file)?;
let specs = repo.pathspec(
false,
[file],
true,
&index,
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
)?;
// TODO: there should be a way to normalize paths without going through patterns, at least in this case maybe?
// `Search` actually sorts patterns by excluding or not, all that can lead to strange results.
let file = specs
.search()
.patterns()
.map(|p| p.path().to_owned())
.next()
.expect("exactly one pattern");

let suspect = repo.head()?.peel_to_commit_in_place()?;
let traverse =
gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [suspect.id], None::<Vec<gix::ObjectId>>)
.with_commit_graph(repo.commit_graph_if_enabled()?)
.build()?;
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr())?;
let statistics = outcome.statistics;
write_blame_entries(out, outcome)?;

if let Some(err) = err {
writeln!(err, "{statistics:#?}")?;
}
Ok(())
}

fn write_blame_entries(mut out: impl std::io::Write, outcome: gix::blame::Outcome) -> Result<(), std::io::Error> {
for (entry, lines_in_hunk) in outcome.entries_with_lines() {
for ((actual_lno, source_lno), line) in entry
.range_in_blamed_file()
.zip(entry.range_in_source_file())
.zip(lines_in_hunk)
{
write!(
out,
"{short_id} {line_no} {src_line_no} {line}",
line_no = actual_lno + 1,
src_line_no = source_lno + 1,
short_id = entry.commit_id.to_hex_with_len(8),
)?;
}
}

Ok(())
}
1 change: 1 addition & 0 deletions gitoxide-core/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum PathsOrPatterns {
pub mod archive;
pub mod cat;
pub use cat::function::cat;
pub mod blame;
pub mod commit;
pub mod config;
mod credential;
Expand Down
15 changes: 14 additions & 1 deletion gix-blame/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name = "gix-blame"
version = "0.0.0"
repository = "https://github.com/GitoxideLabs/gitoxide"
license = "MIT OR Apache-2.0"
description = "A crate of the gitoxide project dedicated implementing a 'blame' algorithm"
description = "A crate of the gitoxide project dedicated to implementing a 'blame' algorithm"
authors = ["Christoph Rüßler <[email protected]>", "Sebastian Thiel <[email protected]>"]
edition = "2021"
rust-version = "1.65"
Expand All @@ -14,6 +14,19 @@ rust-version = "1.65"
doctest = false

[dependencies]
gix-trace = { version = "^0.1.11", path = "../gix-trace" }
gix-diff = { version = "^0.49.0", path = "../gix-diff", default-features = false, features = ["blob"] }
gix-object = { version = "^0.46.0", path = "../gix-object" }
gix-hash = { version = "^0.15.0", path = "../gix-hash" }
gix-worktree = { version = "^0.38.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
gix-traverse = { version = "^0.43.0", path = "../gix-traverse" }

thiserror = "2.0.0"

[dev-dependencies]
gix-ref = { version = "^0.49.0", path = "../gix-ref" }
gix-filter = { version = "^0.16.0", path = "../gix-filter" }
gix-fs = { version = "^0.12.0", path = "../gix-fs" }
gix-index = { version = "^0.37.0", path = "../gix-index" }
gix-odb = { version = "^0.66.0", path = "../gix-odb" }
gix-testtools = { path = "../tests/tools" }
30 changes: 30 additions & 0 deletions gix-blame/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use gix_object::bstr::BString;

/// The error returned by [file()](crate::file()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("No commit was given")]
EmptyTraversal,
#[error(transparent)]
BlobDiffSetResource(#[from] gix_diff::blob::platform::set_resource::Error),
#[error(transparent)]
BlobDiffPrepare(#[from] gix_diff::blob::platform::prepare_diff::Error),
#[error("The file to blame at '{file_path}' wasn't found in the first commit at {commit_id}")]
FileMissing {
/// The file-path to the object to blame.
file_path: BString,
/// The commit whose tree didn't contain `file_path`.
commit_id: gix_hash::ObjectId,
},
#[error("Couldn't find commit or tree in the object database")]
FindObject(#[from] gix_object::find::Error),
#[error("Could not find existing blob or commit")]
FindExistingObject(#[from] gix_object::find::existing_object::Error),
#[error("Could not find existing iterator over a tree")]
FindExistingIter(#[from] gix_object::find::existing_iter::Error),
#[error("Failed to obtain the next commit in the commit-graph traversal")]
Traverse(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
DiffTree(#[from] gix_diff::tree::Error),
}
Loading
Loading