Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 806a744

Browse files
committedMay 20, 2025·
Draft PoC of blame using gitoxide
1 parent fe5e780 commit 806a744

File tree

3 files changed

+264
-73
lines changed

3 files changed

+264
-73
lines changed
 

‎Cargo.lock

Lines changed: 186 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎asyncgit/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ git2-hooks = { path = "../git2-hooks", version = ">=0.4" }
2222
gix = { version = "0.71.0", default-features = false, features = [
2323
"max-performance",
2424
"revision",
25+
"blob-diff"
2526
] }
27+
gix-blame = "0.0.0"
2628
log = "0.4"
2729
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}
2830
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]}

‎asyncgit/src/sync/blame.rs

Lines changed: 76 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
//! Sync git API for fetching a file blame
22
33
use super::{utils, CommitId, RepoPath};
4-
use crate::{
5-
error::{Error, Result},
6-
sync::{get_commits_info, repository::repo},
7-
};
8-
use git2::BlameOptions;
4+
use crate::{error::Result, sync::get_commits_info};
95
use scopetime::scope_time;
106
use std::collections::{HashMap, HashSet};
11-
use std::io::{BufRead, BufReader};
12-
use std::path::Path;
137

148
/// A `BlameHunk` contains all the information that will be shown to the user.
159
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
@@ -40,19 +34,6 @@ pub struct FileBlame {
4034
pub lines: Vec<(Option<BlameHunk>, String)>,
4135
}
4236

43-
/// fixup `\` windows path separators to git compatible `/`
44-
fn fixup_windows_path(path: &str) -> String {
45-
#[cfg(windows)]
46-
{
47-
path.replace('\\', "/")
48-
}
49-
50-
#[cfg(not(windows))]
51-
{
52-
path.to_string()
53-
}
54-
}
55-
5637
///
5738
pub fn blame_file(
5839
repo_path: &RepoPath,
@@ -61,35 +42,52 @@ pub fn blame_file(
6142
) -> Result<FileBlame> {
6243
scope_time!("blame_file");
6344

64-
let repo = repo(repo_path)?;
45+
let repo: gix::Repository =
46+
gix::ThreadSafeRepository::discover_with_environment_overrides(repo_path.gitpath())
47+
.map(Into::into)?;
48+
let tip: gix::ObjectId = match commit_id {
49+
Some(commit_id) => gix::ObjectId::from_bytes_or_panic(
50+
commit_id.get_oid().as_bytes(),
51+
),
52+
_ => repo.head()?.peel_to_commit_in_place()?.id,
53+
};
54+
let traverse = gix::traverse::commit::topo::Builder::from_iters(
55+
&repo.objects,
56+
[tip],
57+
None::<Vec<gix::ObjectId>>,
58+
)
59+
.build()
60+
.expect("TODO");
61+
62+
let mut resource_cache =
63+
repo.diff_resource_cache_for_tree_diff().expect("TODO");
64+
65+
let outcome = gix_blame::file(
66+
&repo.objects,
67+
traverse,
68+
&mut resource_cache,
69+
file_path.into(),
70+
None,
71+
)
72+
.expect("TODO");
6573

6674
let commit_id = if let Some(commit_id) = commit_id {
6775
commit_id
6876
} else {
77+
let repo = crate::sync::repo(repo_path)?;
78+
6979
utils::get_head_repo(&repo)?
7080
};
7181

72-
let spec =
73-
format!("{}:{}", commit_id, fixup_windows_path(file_path));
74-
75-
let object = repo.revparse_single(&spec)?;
76-
let blob = repo.find_blob(object.id())?;
77-
78-
if blob.is_binary() {
79-
return Err(Error::NoBlameOnBinaryFile);
80-
}
81-
82-
let mut opts = BlameOptions::new();
83-
opts.newest_commit(commit_id.into());
84-
85-
let blame =
86-
repo.blame_file(Path::new(file_path), Some(&mut opts))?;
87-
88-
let reader = BufReader::new(blob.content());
89-
90-
let unique_commit_ids: HashSet<_> = blame
82+
let unique_commit_ids: HashSet<_> = outcome
83+
.entries
9184
.iter()
92-
.map(|hunk| CommitId::new(hunk.final_commit_id()))
85+
.map(|entry| {
86+
CommitId::new(
87+
git2::Oid::from_bytes(entry.commit_id.as_bytes())
88+
.expect("TODO"),
89+
)
90+
})
9391
.collect();
9492
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
9593
commit_ids.extend(unique_commit_ids);
@@ -100,40 +98,45 @@ pub fn blame_file(
10098
.map(|commit_info| (commit_info.id, commit_info))
10199
.collect();
102100

103-
let lines: Vec<(Option<BlameHunk>, String)> = reader
104-
.lines()
105-
.enumerate()
106-
.map(|(i, line)| {
107-
// Line indices in a `FileBlame` are 1-based.
108-
let corresponding_hunk = blame.get_line(i + 1);
109-
110-
if let Some(hunk) = corresponding_hunk {
111-
let commit_id = CommitId::new(hunk.final_commit_id());
112-
// Line indices in a `BlameHunk` are 1-based.
113-
let start_line =
114-
hunk.final_start_line().saturating_sub(1);
115-
let end_line =
116-
start_line.saturating_add(hunk.lines_in_hunk());
117-
118-
if let Some(commit_info) =
119-
unique_commit_infos.get(&commit_id)
120-
{
121-
let hunk = BlameHunk {
122-
commit_id,
123-
author: commit_info.author.clone(),
124-
time: commit_info.time,
125-
start_line,
126-
end_line,
101+
// TODO
102+
// The shape of data as returned by `entries_with_lines` is preferable to the one chosen here
103+
// because the former is much closer to what the UI is going to need in the end.
104+
let lines: Vec<(Option<BlameHunk>, String)> = outcome
105+
.entries_with_lines()
106+
.flat_map(|(entry, lines)| {
107+
let commit_id = CommitId::new(
108+
git2::Oid::from_bytes(entry.commit_id.as_bytes())
109+
.expect("TODO"),
110+
);
111+
let start_in_blamed_file =
112+
entry.start_in_blamed_file as usize;
113+
114+
lines
115+
.iter()
116+
.enumerate()
117+
.map(|(i, line)| {
118+
// TODO
119+
let trimmed_line =
120+
line.to_string().trim_end().to_string();
121+
122+
if let Some(commit_info) =
123+
unique_commit_infos.get(&commit_id)
124+
{
125+
return (
126+
Some(BlameHunk {
127+
commit_id,
128+
author: commit_info.author.clone(),
129+
time: commit_info.time,
130+
start_line: start_in_blamed_file + i,
131+
end_line: start_in_blamed_file + i,
132+
}),
133+
trimmed_line,
134+
);
127135
};
128136

129-
return (
130-
Some(hunk),
131-
line.unwrap_or_else(|_| String::new()),
132-
);
133-
}
134-
}
135-
136-
(None, line.unwrap_or_else(|_| String::new()))
137+
(None, trimmed_line)
138+
})
139+
.collect::<Vec<_>>()
137140
})
138141
.collect();
139142

0 commit comments

Comments
 (0)
Please sign in to comment.