Skip to content

Commit 96ca41e

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 f37e9c6 commit 96ca41e

File tree

5 files changed

+71
-2
lines changed

5 files changed

+71
-2
lines changed

gitoxide-core/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ async-client = ["gix/async-network-client-async-std", "gix-transport-configurati
4646
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
4747
serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]
4848

49-
5049
[dependencies]
5150
# deselect everything else (like "performance") as this should be controllable by the parent application.
52-
gix = { version = "^0.66.0", path = "../gix", default-features = false, features = ["blob-merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
51+
gix = { version = "^0.66.0", path = "../gix", default-features = false, features = ["blob-merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk", "blame"] }
5352
gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.53.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] }
5453
gix-transport-configuration-only = { package = "gix-transport", version = "^0.42.3", path = "../gix-transport", default-features = false }
5554
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.15.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use std::{ffi::OsStr, path::PathBuf, str::Lines};
2+
3+
use gix::bstr::BStr;
4+
5+
pub fn blame_file(mut repo: gix::Repository, file: &OsStr, out: impl std::io::Write) -> anyhow::Result<()> {
6+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
7+
8+
let suspect = repo.head()?.peel_to_commit_in_place()?;
9+
let traverse: Vec<_> = gix::traverse::commit::Simple::new(Some(suspect.id), &repo.objects).collect();
10+
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
11+
12+
let work_dir: PathBuf = repo.work_dir().expect("TODO").into();
13+
let file_path: &BStr = gix::path::os_str_into_bstr(file)?;
14+
15+
let blame_entries = gix::blame::blame_file(
16+
&repo.objects,
17+
traverse,
18+
&mut resource_cache,
19+
suspect.id,
20+
work_dir.clone(),
21+
file_path,
22+
)
23+
.expect("TODO");
24+
25+
let absolute_path = work_dir.join(file);
26+
let file_content = std::fs::read_to_string(absolute_path).expect("TODO");
27+
let lines = file_content.lines();
28+
29+
write_blame_entries(out, lines, blame_entries)?;
30+
31+
Ok(())
32+
}
33+
34+
fn write_blame_entries(
35+
mut out: impl std::io::Write,
36+
mut lines: Lines<'_>,
37+
blame_entries: Vec<gix::blame::BlameEntry>,
38+
) -> Result<(), std::io::Error> {
39+
for blame_entry in blame_entries {
40+
for line_number in blame_entry.range_in_blamed_file {
41+
let line = lines.next().unwrap();
42+
43+
writeln!(
44+
out,
45+
"{} {} {}",
46+
blame_entry.commit_id.to_hex_with_len(8),
47+
// `line_number` is 0-based, but we want to show 1-based line numbers (as `git`
48+
// does).
49+
line_number + 1,
50+
line
51+
)?;
52+
}
53+
}
54+
55+
Ok(())
56+
}

gitoxide-core/src/repository/mod.rs

Lines changed: 1 addition & 0 deletions
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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,15 @@ pub fn main() -> Result<()> {
14601460
},
14611461
),
14621462
},
1463+
Subcommands::Blame { file } => prepare_and_run(
1464+
"blame",
1465+
trace,
1466+
verbose,
1467+
progress,
1468+
progress_keep_open,
1469+
None,
1470+
move |_progress, out, _err| core::repository::blame::blame_file(repository(Mode::Lenient)?, &file, out),
1471+
),
14631472
Subcommands::Completions { shell, out_dir } => {
14641473
let mut app = Args::command();
14651474

src/plumbing/options/mod.rs

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

0 commit comments

Comments
 (0)