Skip to content

Draft PoC of blame using gitoxide #2591

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

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
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
190 changes: 190 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions asyncgit/Cargo.toml
Original file line number Diff line number Diff line change
@@ -22,7 +22,9 @@ git2-hooks = { path = "../git2-hooks", version = ">=0.4" }
gix = { version = "0.71.0", default-features = false, features = [
"max-performance",
"revision",
"blob-diff"
] }
gix-blame = "0.1.0"
log = "0.4"
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]}
34 changes: 34 additions & 0 deletions asyncgit/src/error.rs
Original file line number Diff line number Diff line change
@@ -113,6 +113,32 @@ pub enum Error {
#[error("gix::revision::walk error: {0}")]
GixRevisionWalk(#[from] gix::revision::walk::Error),

///
#[error("gix::traverse::commit::topo error: {0}")]
GixTraverseCommitTopo(#[from] gix::traverse::commit::topo::Error),

///
#[error("gix::repository::diff_resource_cache error: {0}")]
GixRepositoryDiffResourceCache(
#[from] Box<gix::repository::diff_resource_cache::Error>,
),

///
#[error("gix::repository::commit_graph_if_enabled error: {0}")]
GixRepositoryCommitGraphIfEnabled(
#[from] gix::repository::commit_graph_if_enabled::Error,
),

///
#[error("gix::config::diff::algorithm error: {0}")]
GixConfigDiffAlgorithm(
#[from] gix::config::diff::algorithm::Error,
),

///
#[error("gix_blame error: {0}")]
GixBlame(#[from] gix_blame::Error),

///
#[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")]
SignAmendNonLastCommit,
@@ -146,3 +172,11 @@ impl From<gix::discover::Error> for Error {
Self::GixDiscover(Box::new(error))
}
}

impl From<gix::repository::diff_resource_cache::Error> for Error {
fn from(
error: gix::repository::diff_resource_cache::Error,
) -> Self {
Self::GixRepositoryDiffResourceCache(Box::new(error))
}
}
159 changes: 86 additions & 73 deletions asyncgit/src/sync/blame.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
//! Sync git API for fetching a file blame
use super::{utils, CommitId, RepoPath};
use crate::{
error::{Error, Result},
sync::{get_commits_info, repository::repo},
};
use git2::BlameOptions;
use crate::{error::Result, sync::get_commits_info};
use scopetime::scope_time;
use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader};
use std::path::Path;

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

/// fixup `\` windows path separators to git compatible `/`
fn fixup_windows_path(path: &str) -> String {
#[cfg(windows)]
{
path.replace('\\', "/")
}

#[cfg(not(windows))]
{
path.to_string()
}
fn object_id_to_oid(object_id: gix::ObjectId) -> git2::Oid {
// TODO
// This should not fail. It will also become obsolete once `gix::ObjectId` is used throughout
// `gitui`.
#[allow(clippy::expect_used)]
git2::Oid::from_bytes(object_id.as_bytes())
.expect("ObjectId could not be converted to Oid")
}

///
@@ -61,35 +51,54 @@ pub fn blame_file(
) -> Result<FileBlame> {
scope_time!("blame_file");

let repo = repo(repo_path)?;

let commit_id = if let Some(commit_id) = commit_id {
commit_id
} else {
utils::get_head_repo(&repo)?
let file_path: &gix::bstr::BStr = file_path.into();
let file_path =
gix::path::to_unix_separators_on_windows(file_path);

let repo: gix::Repository =
gix::ThreadSafeRepository::discover_with_environment_overrides(repo_path.gitpath())
.map(Into::into)?;
let tip: gix::ObjectId = match commit_id {
Some(commit_id) => gix::ObjectId::from_bytes_or_panic(
commit_id.get_oid().as_bytes(),
),
_ => repo.head()?.peel_to_commit_in_place()?.id,
};

let spec =
format!("{}:{}", commit_id, fixup_windows_path(file_path));
let cache: Option<gix::commitgraph::Graph> =
repo.commit_graph_if_enabled()?;
let mut resource_cache =
repo.diff_resource_cache_for_tree_diff()?;

let object = repo.revparse_single(&spec)?;
let blob = repo.find_blob(object.id())?;
let diff_algorithm = repo.diff_algorithm()?;

if blob.is_binary() {
return Err(Error::NoBlameOnBinaryFile);
}
let options = gix_blame::Options {
diff_algorithm,
range: None,
since: None,
};

let mut opts = BlameOptions::new();
opts.newest_commit(commit_id.into());
let outcome = gix_blame::file(
&repo.objects,
tip,
cache,
&mut resource_cache,
&file_path,
options,
)?;

let blame =
repo.blame_file(Path::new(file_path), Some(&mut opts))?;
let commit_id = if let Some(commit_id) = commit_id {
commit_id
} else {
let repo = crate::sync::repo(repo_path)?;

let reader = BufReader::new(blob.content());
utils::get_head_repo(&repo)?
};

let unique_commit_ids: HashSet<_> = blame
let unique_commit_ids: HashSet<_> = outcome
.entries
.iter()
.map(|hunk| CommitId::new(hunk.final_commit_id()))
.map(|entry| CommitId::new(object_id_to_oid(entry.commit_id)))
.collect();
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
commit_ids.extend(unique_commit_ids);
@@ -100,46 +109,50 @@ pub fn blame_file(
.map(|commit_info| (commit_info.id, commit_info))
.collect();

let lines: Vec<(Option<BlameHunk>, String)> = reader
.lines()
.enumerate()
.map(|(i, line)| {
// Line indices in a `FileBlame` are 1-based.
let corresponding_hunk = blame.get_line(i + 1);

if let Some(hunk) = corresponding_hunk {
let commit_id = CommitId::new(hunk.final_commit_id());
// Line indices in a `BlameHunk` are 1-based.
let start_line =
hunk.final_start_line().saturating_sub(1);
let end_line =
start_line.saturating_add(hunk.lines_in_hunk());

if let Some(commit_info) =
unique_commit_infos.get(&commit_id)
{
let hunk = BlameHunk {
commit_id,
author: commit_info.author.clone(),
time: commit_info.time,
start_line,
end_line,
};

return (
Some(hunk),
line.unwrap_or_else(|_| String::new()),
);
}
}

(None, line.unwrap_or_else(|_| String::new()))
// TODO
// The shape of data as returned by `entries_with_lines` is preferable to the one chosen here
// because the former is much closer to what the UI is going to need in the end.
let lines: Vec<(Option<BlameHunk>, String)> = outcome
.entries_with_lines()
.flat_map(|(entry, lines)| {
let commit_id =
CommitId::new(object_id_to_oid(entry.commit_id));
let start_in_blamed_file =
entry.start_in_blamed_file as usize;

lines
.iter()
.enumerate()
.map(|(i, line)| {
// TODO
let trimmed_line =
line.to_string().trim_end().to_string();

if let Some(commit_info) =
unique_commit_infos.get(&commit_id)
{
return (
Some(BlameHunk {
commit_id,
author: commit_info.author.clone(),
time: commit_info.time,
start_line: start_in_blamed_file + i,
end_line: start_in_blamed_file
+ i + 1,
}),
trimmed_line,
);
}

(None, trimmed_line)
})
.collect::<Vec<_>>()
})
.collect();

let file_blame = FileBlame {
commit_id,
path: file_path.into(),
path: file_path.to_string(),
lines,
};