Skip to content

Commit ae7012f

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 3c7eff8 commit ae7012f

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
@@ -19,6 +19,7 @@ pub enum PathsOrPatterns {
1919

2020
#[cfg(feature = "archive")]
2121
pub mod archive;
22+
pub mod blame;
2223
pub mod commit;
2324
pub mod config;
2425
mod credential;

src/plumbing/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,15 @@ pub fn main() -> Result<()> {
14351435
},
14361436
),
14371437
},
1438+
Subcommands::Blame { file } => prepare_and_run(
1439+
"blame",
1440+
trace,
1441+
verbose,
1442+
progress,
1443+
progress_keep_open,
1444+
None,
1445+
move |_progress, out, _err| core::repository::blame::blame_file(repository(Mode::Lenient)?, &file, out),
1446+
),
14381447
Subcommands::Completions { shell, out_dir } => {
14391448
let mut app = Args::command();
14401449

src/plumbing/options/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ pub enum Subcommands {
144144
/// Subcommands that need no git repository to run.
145145
#[clap(subcommand)]
146146
Free(free::Subcommands),
147+
/// Blame lines in a file
148+
Blame {
149+
file: std::ffi::OsString,
150+
},
147151
/// Generate shell completions to stdout or a directory.
148152
#[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]
149153
Completions {

0 commit comments

Comments
 (0)