Skip to content

Commit 80e5804

Browse files
cruesslerByron
andcommitted
feat: add gix blame to the CLI
That way it's possible to see the `blame` result of any file in the repository. Co-authored-by: Sebastian Thiel <[email protected]>
1 parent 25efbfb commit 80e5804

File tree

7 files changed

+87
-1
lines changed

7 files changed

+87
-1
lines changed

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

+9
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,15 @@ Check out the [performance discussion][gix-diff-performance] as well.
361361
* [x] API documentation
362362
* [ ] Examples
363363

364+
### gix-blame
365+
366+
* [ ] commit-annotations for a single file
367+
- [ ] progress
368+
- [ ] interruptability
369+
- [ ] streaming
370+
* [x] API documentation
371+
* [ ] Examples
372+
364373
### gix-traverse
365374

366375
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

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::{ffi::OsStr, path::PathBuf, str::Lines};
2+
3+
use anyhow::anyhow;
4+
use gix::bstr::BStr;
5+
6+
pub fn blame_file(mut repo: gix::Repository, file: &OsStr, out: impl std::io::Write) -> anyhow::Result<()> {
7+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
8+
9+
let suspect = repo.head()?.peel_to_commit_in_place()?;
10+
let traverse: Vec<_> =
11+
gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [suspect.id], None::<Vec<gix::ObjectId>>)
12+
.build()?
13+
.collect();
14+
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
15+
16+
let work_dir: PathBuf = repo
17+
.work_dir()
18+
.ok_or_else(|| anyhow!("blame needs a workdir, but there is none"))?
19+
.into();
20+
let file_path: &BStr = gix::path::os_str_into_bstr(file)?;
21+
22+
let blame_entries = gix::blame::blame_file(
23+
&repo.objects,
24+
traverse,
25+
&mut resource_cache,
26+
suspect.id,
27+
work_dir.clone(),
28+
file_path,
29+
)?;
30+
31+
let absolute_path = work_dir.join(file);
32+
let file_content = std::fs::read_to_string(absolute_path)?;
33+
let lines = file_content.lines();
34+
35+
write_blame_entries(out, lines, blame_entries)?;
36+
37+
Ok(())
38+
}
39+
40+
fn write_blame_entries(
41+
mut out: impl std::io::Write,
42+
mut lines: Lines<'_>,
43+
blame_entries: Vec<gix::blame::BlameEntry>,
44+
) -> Result<(), std::io::Error> {
45+
for blame_entry in blame_entries {
46+
for line_number in blame_entry.range_in_blamed_file {
47+
let line = lines.next().unwrap();
48+
49+
writeln!(
50+
out,
51+
"{} {} {}",
52+
blame_entry.commit_id.to_hex_with_len(8),
53+
// `line_number` is 0-based, but we want to show 1-based line numbers (as `git`
54+
// does).
55+
line_number + 1,
56+
line
57+
)?;
58+
}
59+
}
60+
61+
Ok(())
62+
}

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;

src/plumbing/main.rs

+9
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,15 @@ pub fn main() -> Result<()> {
15331533
},
15341534
),
15351535
},
1536+
Subcommands::Blame { file } => prepare_and_run(
1537+
"blame",
1538+
trace,
1539+
verbose,
1540+
progress,
1541+
progress_keep_open,
1542+
None,
1543+
move |_progress, out, _err| core::repository::blame::blame_file(repository(Mode::Lenient)?, &file, out),
1544+
),
15361545
Subcommands::Completions { shell, out_dir } => {
15371546
let mut app = Args::command();
15381547

src/plumbing/options/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ pub enum Subcommands {
151151
/// Subcommands that need no git repository to run.
152152
#[clap(subcommand)]
153153
Free(free::Subcommands),
154+
/// Blame lines in a file
155+
Blame {
156+
file: std::ffi::OsString,
157+
},
154158
/// Generate shell completions to stdout or a directory.
155159
#[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]
156160
Completions {

0 commit comments

Comments
 (0)