use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
ffi::OsStr,
io::Write,
path::{Path, PathBuf},
process::{Command, Stdio},
};
use bstr::{BString, ByteSlice};
use gix_hash::{Kind, ObjectId};
use super::{Commit, Head, IndexEntry, Reference, ReferenceTarget, State};
use crate::Result;
pub(super) fn snapshot(path: &Path) -> Result<State> {
let hash = object_hash(path)?;
let common_dir = common_dir(path)?;
let config = std::fs::read(common_dir.join("config"))?.into();
let symbolic_head = git_optional(path, ["symbolic-ref", "-q", "HEAD"])?.map(|out| out.trim().to_owned());
let head_id = git_optional(path, ["rev-parse", "--verify", "HEAD"])?
.map(|out| parse_id(out.trim(), hash))
.transpose()?;
let head = match (symbolic_head, head_id) {
(Some(name), Some(id)) => Head::Symbolic { name: name.into(), id },
(Some(name), None) => Head::Unborn(name.into()),
(None, Some(id)) => Head::Detached(id),
(None, None) => return Err("HEAD is neither symbolic, detached, nor unborn".into()),
};
let (references, mut roots) = references(path, hash)?;
roots.extend(head_id);
let shallow = shallow_commits(path, hash)?;
let commits = commits(path, roots, hash, &shallow)?;
let index = index(path, hash)?;
let index_tree = index_tree(path, &index, hash)?;
let worktree_root = worktree_root(path)?;
let worktree = super::worktree(worktree_root.as_deref())?;
Ok(State {
head,
config,
references,
commits,
index,
index_tree,
worktree,
normalization_root: common_dir,
show_object_ids: false,
})
}
fn object_hash(path: &Path) -> Result<Kind> {
match git(path, ["rev-parse", "--show-object-format"])?.trim() {
#[cfg(feature = "sha1")]
b"sha1" => Ok(Kind::Sha1),
#[cfg(feature = "sha256")]
b"sha256" => Ok(Kind::Sha256),
value => Err(format!("unsupported object format: {}", value.as_bstr()).into()),
}
}
fn references(path: &Path, hash: Kind) -> Result<(Vec<Reference>, Vec<ObjectId>)> {
let output = git(
path,
[
"for-each-ref",
"--format=%(refname)%00%(symref)%00%(objectname)",
"refs/",
],
)?;
let mut out = Vec::new();
let mut roots = Vec::new();
for record in output.lines().filter(|line| !line.is_empty()) {
let mut fields = record.split(|byte| *byte == 0);
let name = fields.next().ok_or("reference output lacks a name")?;
let symbolic = fields.next().ok_or("reference output lacks a symbolic target")?;
let id = fields.next().ok_or("reference output lacks an object target")?;
let resolved = (!id.is_empty()).then(|| parse_id(id, hash)).transpose()?;
roots.extend(resolved);
let target = if symbolic.is_empty() {
ReferenceTarget::Object(resolved.ok_or("a direct reference lacks an object target")?)
} else {
ReferenceTarget::Symbolic(symbolic.into())
};
out.push(Reference {
name: name.into(),
target,
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok((out, roots))
}
fn common_dir(path: &Path) -> Result<PathBuf> {
let common_dir = git(path, ["rev-parse", "--git-common-dir"])?;
let common_dir = PathBuf::from(String::from_utf8(common_dir.trim().to_owned())?);
Ok(if common_dir.is_absolute() {
common_dir
} else {
path.join(common_dir)
})
}
fn shallow_commits(path: &Path, hash: Kind) -> Result<BTreeSet<ObjectId>> {
let data = match std::fs::read(common_dir(path)?.join("shallow")) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
Err(err) => return Err(err.into()),
};
data.lines().map(|line| parse_id(line, hash)).collect()
}
fn commits(path: &Path, roots: Vec<ObjectId>, hash: Kind, shallow: &BTreeSet<ObjectId>) -> Result<Vec<Commit>> {
let mut pending = Vec::new();
for id in roots {
if let Some(id) = peel_to_commit(path, id, hash)? {
pending.push(id);
}
}
let mut seen = BTreeSet::new();
let mut commits = Vec::new();
while let Some(id) = pending.pop() {
if !seen.insert(id) {
continue;
}
let hex = id.to_string();
let data = git(path, ["cat-file", "commit", &hex])?;
if !shallow.contains(&id) {
for parent in data
.lines()
.take_while(|line| !line.is_empty())
.filter_map(|line| line.strip_prefix(b"parent "))
{
pending.push(parse_id(parent, hash)?);
}
}
commits.push(Commit { id, data });
}
commits.sort_by_key(|commit| commit.id);
Ok(commits)
}
fn peel_to_commit(path: &Path, mut id: ObjectId, hash: Kind) -> Result<Option<ObjectId>> {
let mut seen = BTreeSet::new();
loop {
if !seen.insert(id) {
return Err(format!("tag cycle at {id}").into());
}
let hex = id.to_string();
match git(path, ["cat-file", "-t", &hex])?.trim() {
b"commit" => return Ok(Some(id)),
b"tag" => {
let data = git(path, ["cat-file", "tag", &hex])?;
let target = data
.lines()
.next()
.and_then(|line| line.strip_prefix(b"object "))
.ok_or("tag object lacks its target")?;
id = parse_id(target, hash)?;
}
_ => return Ok(None),
}
}
}
fn index(path: &Path, hash: Kind) -> Result<Vec<IndexEntry>> {
let output = git(path, ["ls-files", "--stage", "-z"])?;
output
.split(|byte| *byte == 0)
.filter(|record| !record.is_empty())
.map(|record| {
let (metadata, path) = record.split_once_str(b"\t").ok_or("index entry lacks a path")?;
let mut fields = metadata.split(|byte| *byte == b' ');
let mode = u32::from_str_radix(
std::str::from_utf8(fields.next().ok_or("index entry lacks a mode")?)?,
8,
)?;
let id = parse_id(fields.next().ok_or("index entry lacks an object ID")?, hash)?;
let stage = std::str::from_utf8(fields.next().ok_or("index entry lacks a stage")?)?.parse::<u8>()?;
Ok(IndexEntry {
mode,
id,
stage,
path: path.into(),
})
})
.collect()
}
#[derive(Default)]
struct Tree {
entries: BTreeMap<BString, TreeEntry>,
}
enum TreeEntry {
Tree(Tree),
Leaf { mode: u32, id: ObjectId },
}
fn index_tree(path: &Path, entries: &[IndexEntry], hash: Kind) -> Result<Option<ObjectId>> {
if entries.iter().any(|entry| entry.stage != 0) {
return Ok(None);
}
let mut root = Tree::default();
for entry in entries {
insert(&mut root, entry.path.as_ref(), entry.mode, entry.id)?;
}
Ok(Some(hash_tree(path, &root, hash)?))
}
fn insert(tree: &mut Tree, path: &[u8], mode: u32, id: ObjectId) -> Result<()> {
let mut components = path.split(|byte| *byte == b'/').peekable();
let mut tree = tree;
while let Some(component) = components.next() {
if components.peek().is_none() {
tree.entries.insert(component.into(), TreeEntry::Leaf { mode, id });
return Ok(());
}
tree = match tree
.entries
.entry(component.into())
.or_insert_with(|| TreeEntry::Tree(Tree::default()))
{
TreeEntry::Tree(tree) => tree,
TreeEntry::Leaf { .. } => return Err("an index path traverses a non-tree entry".into()),
};
}
Err("an index entry has an empty path".into())
}
fn hash_tree(path: &Path, tree: &Tree, hash: Kind) -> Result<ObjectId> {
let mut entries: Vec<_> = tree.entries.iter().collect();
entries.sort_by(|(a, a_entry), (b, b_entry)| tree_name_cmp(a, a_entry, b, b_entry));
let mut data = Vec::new();
for (name, entry) in entries {
let (mode, id) = match entry {
TreeEntry::Tree(tree) => (0o40000, hash_tree(path, tree, hash)?),
TreeEntry::Leaf { mode, id } => (*mode, *id),
};
data.extend_from_slice(format!("{mode:o}").as_bytes());
data.push(b' ');
data.extend_from_slice(name);
data.push(0);
data.extend_from_slice(id.as_bytes());
}
let id = git_with_input(path, ["hash-object", "-t", "tree", "--stdin"], &data)?;
parse_id(id.trim(), hash)
}
fn tree_name_cmp(a: &[u8], a_entry: &TreeEntry, b: &[u8], b_entry: &TreeEntry) -> Ordering {
let mut a = a
.iter()
.copied()
.chain(std::iter::once(if matches!(a_entry, TreeEntry::Tree(_)) {
b'/'
} else {
0
}));
let mut b = b
.iter()
.copied()
.chain(std::iter::once(if matches!(b_entry, TreeEntry::Tree(_)) {
b'/'
} else {
0
}));
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) if a == b => {}
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
}
}
}
fn worktree_root(path: &Path) -> Result<Option<PathBuf>> {
git_optional(path, ["rev-parse", "--show-toplevel"])?
.map(|root| String::from_utf8(root.trim().to_owned()).map(PathBuf::from))
.transpose()
.map_err(Into::into)
}
fn parse_id(input: &[u8], hash: Kind) -> Result<ObjectId> {
let id = ObjectId::from_hex(input)?;
if id.kind() != hash {
return Err("object ID uses the wrong hash kind".into());
}
Ok(id)
}
fn git<const N: usize>(path: &Path, args: [&str; N]) -> Result<Vec<u8>> {
git_os(path, args.iter().map(OsStr::new))
}
fn git_os<'a>(path: &Path, args: impl IntoIterator<Item = &'a OsStr>) -> Result<Vec<u8>> {
let output = Command::new(gix_path::env::exe_invocation())
.arg("-C")
.arg(path)
.args(args)
.env("GIT_OPTIONAL_LOCKS", "0")
.env("GIT_NO_REPLACE_OBJECTS", "1")
.output()?;
if output.status.success() {
return Ok(output.stdout);
}
Err(format!(
"git command failed with {}: {}",
output.status,
output.stderr.as_bstr().trim().as_bstr()
)
.into())
}
fn git_with_input<const N: usize>(path: &Path, args: [&str; N], input: &[u8]) -> Result<Vec<u8>> {
let mut child = Command::new(gix_path::env::exe_invocation())
.arg("-C")
.arg(path)
.args(args)
.env("GIT_OPTIONAL_LOCKS", "0")
.env("GIT_NO_REPLACE_OBJECTS", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
child.stdin.take().expect("configured as piped").write_all(input)?;
let output = child.wait_with_output()?;
if output.status.success() {
return Ok(output.stdout);
}
Err(format!(
"git command failed with {}: {}",
output.status,
output.stderr.as_bstr().trim().as_bstr()
)
.into())
}
fn git_optional<const N: usize>(path: &Path, args: [&str; N]) -> Result<Option<Vec<u8>>> {
let output = Command::new(gix_path::env::exe_invocation())
.arg("-C")
.arg(path)
.args(args)
.env("GIT_OPTIONAL_LOCKS", "0")
.env("GIT_NO_REPLACE_OBJECTS", "1")
.output()?;
Ok(output.status.success().then_some(output.stdout))
}