use std::path::{Path, PathBuf};
use gix::prelude::*;
use super::{
BlameHunk, ChangeKind, Commit, CommitId, FileChange, GitBackend, Signature, WalkOptions,
};
use crate::RpoError;
pub struct GixBackend {
repo: gix::ThreadSafeRepository,
}
fn map_err(e: impl std::fmt::Display) -> RpoError {
RpoError::Backend(e.to_string())
}
fn oid_to_commit_id(id: gix::ObjectId) -> CommitId {
let mut bytes = [0u8; 20];
bytes.copy_from_slice(id.as_bytes());
CommitId(bytes)
}
#[allow(dead_code)] fn commit_id_to_oid(id: &CommitId) -> gix::ObjectId {
gix::ObjectId::from_bytes_or_panic(&id.0)
}
fn to_signature(s: gix::actor::SignatureRef<'_>) -> Signature {
Signature {
name: s.name.to_string(),
email: s.email.to_string(),
time_ms: s.time().map(|t| t.seconds * 1_000).unwrap_or(0),
}
}
fn subject_of(message: &gix::bstr::BStr) -> String {
let slice: &[u8] = message.as_ref();
let end = slice
.iter()
.position(|&b| b == b'\n')
.unwrap_or(slice.len());
String::from_utf8_lossy(&slice[..end]).trim().to_string()
}
fn added_line_count(data: &[u8]) -> u64 {
if data.contains(&0) || data.is_empty() {
return 0;
}
let newlines = data.iter().filter(|&&b| b == b'\n').count() as u64;
if data.ends_with(b"\n") {
newlines
} else {
newlines + 1
}
}
impl GixBackend {
fn synthesize_additions(&self, tree: &gix::Tree<'_>) -> Result<Vec<FileChange>, RpoError> {
let local = self.repo.to_thread_local();
let mut out = Vec::new();
let mut blob_buf = Vec::new();
let mut recorder = gix::traverse::tree::Recorder::default();
tree.traverse()
.breadthfirst(&mut recorder)
.map_err(map_err)?;
for entry in recorder.records {
if !entry.mode.is_blob() {
continue;
}
let blob = local
.objects
.find_blob(&entry.oid, &mut blob_buf)
.map_err(map_err)?;
let insertions = added_line_count(blob.data);
out.push(FileChange {
path: PathBuf::from(entry.filepath.to_string()),
old_path: None,
kind: ChangeKind::Added,
insertions,
deletions: 0,
});
}
Ok(out)
}
}
impl GitBackend for GixBackend {
fn open(path: &Path) -> Result<Self, RpoError> {
let repo = gix::ThreadSafeRepository::open(path).map_err(|e| match e {
gix::open::Error::NotARepository { .. } => RpoError::NotARepo {
path: path.to_path_buf(),
},
other => map_err(other),
})?;
Ok(Self { repo })
}
fn head_commit(&self) -> Result<CommitId, RpoError> {
let local = self.repo.to_thread_local();
let id = local.head_id().map_err(map_err)?.detach();
Ok(oid_to_commit_id(id))
}
fn iter_commits<'a>(
&'a self,
opts: WalkOptions,
) -> Box<dyn Iterator<Item = Result<Commit, RpoError>> + 'a> {
let local = self.repo.to_thread_local();
let head = match local.head_id() {
Ok(h) => h,
Err(e) => return Box::new(std::iter::once(Err(map_err(e)))),
};
let mut walker = local.rev_walk([head.detach()]);
if opts.first_parent_only {
walker = walker.first_parent_only();
}
let walk = match walker.all() {
Ok(it) => it,
Err(e) => return Box::new(std::iter::once(Err(map_err(e)))),
};
let mut out: Vec<Result<Commit, RpoError>> = Vec::new();
for info_res in walk {
let info = match info_res {
Ok(i) => i,
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
let oid = info.id;
let object = match oid.attach(&local).object() {
Ok(o) => o,
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
let commit = match object.try_into_commit() {
Ok(c) => c,
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
let parent_ids: Vec<CommitId> = commit
.parent_ids()
.map(|p| oid_to_commit_id(p.detach()))
.collect();
if !opts.include_merges && parent_ids.len() >= 2 {
continue;
}
let decoded = match commit.decode() {
Ok(d) => d,
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
let author = match decoded.author() {
Ok(a) => to_signature(a),
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
let committer = match decoded.committer() {
Ok(c) => to_signature(c),
Err(e) => {
out.push(Err(map_err(e)));
continue;
}
};
out.push(Ok(Commit {
id: oid_to_commit_id(oid),
author,
committer,
parent_ids,
message_subject: subject_of(decoded.message),
}));
}
Box::new(out.into_iter())
}
fn diff_tree(
&self,
parent: Option<&CommitId>,
child: &CommitId,
) -> Result<Vec<FileChange>, RpoError> {
use gix::object::tree::diff::Change;
let local = self.repo.to_thread_local();
let child_commit = local
.find_object(commit_id_to_oid(child))
.map_err(map_err)?
.try_into_commit()
.map_err(map_err)?;
let child_tree = child_commit.tree().map_err(map_err)?;
let parent_tree = if let Some(pid) = parent {
local
.find_object(commit_id_to_oid(pid))
.map_err(map_err)?
.try_into_commit()
.map_err(map_err)?
.tree()
.map_err(map_err)?
} else {
return self.synthesize_additions(&child_tree);
};
let mut resource_cache = local
.diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGit, Default::default())
.map_err(map_err)?;
let mut out: Vec<FileChange> = Vec::new();
parent_tree
.changes()
.map_err(map_err)?
.for_each_to_obtain_tree(
&child_tree,
|change: Change<'_, '_, '_>| -> Result<_, RpoError> {
let is_blob = match &change {
Change::Addition { entry_mode, .. }
| Change::Deletion { entry_mode, .. }
| Change::Modification { entry_mode, .. }
| Change::Rewrite { entry_mode, .. } => entry_mode.is_blob(),
};
if !is_blob {
return Ok(std::ops::ControlFlow::Continue(()));
}
let (path, old_path, kind) = match &change {
Change::Addition { location, .. } => {
(PathBuf::from(location.to_string()), None, ChangeKind::Added)
}
Change::Deletion { location, .. } => (
PathBuf::from(location.to_string()),
None,
ChangeKind::Deleted,
),
Change::Modification { location, .. } => (
PathBuf::from(location.to_string()),
None,
ChangeKind::Modified,
),
Change::Rewrite {
source_location,
location,
copy,
..
} => (
PathBuf::from(location.to_string()),
Some(PathBuf::from(source_location.to_string())),
if *copy {
ChangeKind::Copied
} else {
ChangeKind::Renamed
},
),
};
let (ins, del) = match change.diff(&mut resource_cache) {
Ok(mut platform) => match platform.line_counts() {
Ok(Some(counts)) => (counts.insertions as u64, counts.removals as u64),
Ok(None) => (0, 0), Err(_) => (0, 0), },
Err(_) => (0, 0),
};
resource_cache.clear_resource_cache_keep_allocation();
out.push(FileChange {
path,
old_path,
kind,
insertions: ins,
deletions: del,
});
Ok(std::ops::ControlFlow::Continue(()))
},
)
.map_err(map_err)?;
Ok(out)
}
fn list_tree_paths(&self, commit: &CommitId) -> Result<Vec<PathBuf>, RpoError> {
let local = self.repo.to_thread_local();
let commit_obj = local
.find_object(commit_id_to_oid(commit))
.map_err(map_err)?
.try_into_commit()
.map_err(map_err)?;
let tree = commit_obj.tree().map_err(map_err)?;
let mut recorder = gix::traverse::tree::Recorder::default();
tree.traverse()
.breadthfirst(&mut recorder)
.map_err(map_err)?;
let mut out = Vec::with_capacity(recorder.records.len());
for entry in recorder.records {
if !entry.mode.is_blob() {
continue;
}
out.push(PathBuf::from(entry.filepath.to_string()));
}
Ok(out)
}
fn blame_file(&self, commit: &CommitId, path: &Path) -> Result<Vec<BlameHunk>, RpoError> {
let local = self.repo.to_thread_local();
let suspect = commit_id_to_oid(commit);
let file_path: &gix::bstr::BStr = path
.to_str()
.ok_or_else(|| RpoError::Backend(format!("non-utf8 path: {}", path.display())))?
.as_bytes()
.into();
let mut resource_cache = local
.diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGit, Default::default())
.map_err(map_err)?;
let outcome = gix_blame::file(
&local.objects,
gix_blame::Start::Commit(suspect),
None,
&mut resource_cache,
file_path,
gix_blame::Options::default(),
)
.map_err(|e| RpoError::BlameFailed {
path: path.to_path_buf(),
rev: commit.to_hex(),
reason: e.to_string(),
})?;
let mut hunks = Vec::with_capacity(outcome.entries.len());
for entry in outcome.entries {
hunks.push(BlameHunk {
start_line: entry.start_in_blamed_file + 1, line_count: entry.len.get(),
commit_id: oid_to_commit_id(entry.commit_id),
});
}
Ok(hunks)
}
fn resolve_rev(&self, rev: &str) -> Result<CommitId, RpoError> {
let local = self.repo.to_thread_local();
if rev.is_empty() {
return Err(RpoError::RevisionNotFound { rev: rev.into() });
}
let id = local.rev_parse_single(rev).map_err(map_err)?;
Ok(oid_to_commit_id(id.detach()))
}
fn commit_meta(&self, id: &CommitId) -> Result<Commit, RpoError> {
let local = self.repo.to_thread_local();
let oid = commit_id_to_oid(id);
let object = local
.find_object(oid)
.map_err(|_| RpoError::RevisionNotFound { rev: id.to_hex() })?;
let commit = object
.try_into_commit()
.map_err(|_| RpoError::RevisionNotFound { rev: id.to_hex() })?;
let parent_ids: Vec<CommitId> = commit
.parent_ids()
.map(|p| oid_to_commit_id(p.detach()))
.collect();
let decoded = commit.decode().map_err(map_err)?;
let author = decoded.author().map(to_signature).map_err(map_err)?;
let committer = decoded.committer().map(to_signature).map_err(map_err)?;
Ok(Commit {
id: oid_to_commit_id(oid),
author,
committer,
parent_ids,
message_subject: subject_of(decoded.message),
})
}
fn tags(&self) -> Result<Vec<(String, CommitId)>, RpoError> {
let local = self.repo.to_thread_local();
let mut out = Vec::new();
let platform = local.references().map_err(map_err)?;
for reference in platform.tags().map_err(map_err)? {
let r = reference.map_err(map_err)?;
let name = r.name().shorten().to_string();
let target = r.clone().into_fully_peeled_id().map_err(map_err)?.detach();
out.push((name, oid_to_commit_id(target)));
}
Ok(out)
}
fn mailmap_bytes(&self) -> Result<Option<Vec<u8>>, RpoError> {
let local = self.repo.to_thread_local();
let path = local
.workdir()
.ok_or_else(|| {
RpoError::Backend("bare repos have no working tree for .mailmap lookup".into())
})?
.to_path_buf();
let file = path.join(".mailmap");
match std::fs::read(&file) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(RpoError::Io(e)),
}
}
fn gitattributes_bytes(&self) -> Result<Option<Vec<u8>>, RpoError> {
let local = self.repo.to_thread_local();
let path = local
.workdir()
.ok_or_else(|| {
RpoError::Backend(
"bare repos have no working tree for .gitattributes lookup".into(),
)
})?
.to_path_buf();
let file = path.join(".gitattributes");
match std::fs::read(&file) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(RpoError::Io(e)),
}
}
fn thread_handle(&self) -> Result<Self, RpoError> {
Ok(Self {
repo: self.repo.clone(),
})
}
}