use crate::git_metadata::{MetadataOutput, path_from_git_bytes, run_metadata_git, strip_git_line};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorktreeIdentity {
OutsideWorktree,
BareRepository {
common_dir: PathBuf,
git_dir: PathBuf,
},
MainWorktree {
common_dir: PathBuf,
git_dir: PathBuf,
worktree_root: PathBuf,
},
LinkedWorktree {
common_dir: PathBuf,
git_dir: PathBuf,
worktree_root: PathBuf,
},
}
pub const IDENTITY_QUERY: &[&str] = &[
"rev-parse",
"--path-format=absolute",
"--is-bare-repository",
"--git-dir",
"--git-common-dir",
"--show-toplevel",
];
fn malformed(detail: &str) -> std::io::Error {
return std::io::Error::other(format!(
"cli-git could not classify the Git worktree: git rev-parse returned {detail}. \
Repository paths containing line feeds are not supported."
));
}
pub fn parse_identity_output(success: bool, stdout: &[u8]) -> std::io::Result<WorktreeIdentity> {
if stdout.is_empty() {
if success {
return Err(malformed("no output"));
}
return Ok(WorktreeIdentity::OutsideWorktree);
}
let mut lines: Vec<&[u8]> = Vec::<&[u8]>::new();
for line in strip_git_line(stdout).split(is_line_feed) {
lines.push(line.strip_suffix(b"\r").unwrap_or(line));
}
let expected: usize = if success { 4 } else { 3 };
if lines.len() != expected {
return Err(malformed(
format!("{} lines where {expected} were expected", lines.len()).as_str(),
));
}
let bare: bool = lines[0] == b"true";
if !bare && lines[0] != b"false" {
return Err(malformed("an unknown bare-repository flag"));
}
let (Some(git_dir), Some(common_dir)) =
(path_from_git_bytes(lines[1]), path_from_git_bytes(lines[2]))
else {
return Err(malformed("an empty Git directory"));
};
if !success {
if bare {
return Ok(WorktreeIdentity::BareRepository {
common_dir,
git_dir,
});
}
return Ok(WorktreeIdentity::OutsideWorktree);
}
let Some(worktree_root) = path_from_git_bytes(lines[3]) else {
return Err(malformed("an empty worktree top level"));
};
if git_dir == common_dir {
return Ok(WorktreeIdentity::MainWorktree {
common_dir,
git_dir,
worktree_root,
});
}
return Ok(WorktreeIdentity::LinkedWorktree {
common_dir,
git_dir,
worktree_root,
});
}
fn is_line_feed(byte: &u8) -> bool {
return *byte == b'\n';
}
pub fn resolve_worktree_identity(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
) -> std::io::Result<WorktreeIdentity> {
let mut arguments: Vec<OsString> = global_prefix.to_vec();
for part in IDENTITY_QUERY {
arguments.push(OsString::from(part));
}
let output: MetadataOutput = run_metadata_git(real_git, arguments.as_slice(), overlay)?;
return parse_identity_output(output.success, output.stdout.as_slice());
}
pub fn worktree_root(identity: &WorktreeIdentity) -> Option<&Path> {
match identity {
WorktreeIdentity::MainWorktree { worktree_root, .. }
| WorktreeIdentity::LinkedWorktree { worktree_root, .. } => {
return Some(worktree_root.as_path());
}
WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => {
return None;
}
}
}
pub fn common_directory(identity: &WorktreeIdentity) -> Option<&Path> {
match identity {
WorktreeIdentity::BareRepository { common_dir, .. }
| WorktreeIdentity::MainWorktree { common_dir, .. }
| WorktreeIdentity::LinkedWorktree { common_dir, .. } => {
return Some(common_dir.as_path());
}
WorktreeIdentity::OutsideWorktree => {
return None;
}
}
}
pub fn git_directory(identity: &WorktreeIdentity) -> Option<&Path> {
match identity {
WorktreeIdentity::BareRepository { git_dir, .. }
| WorktreeIdentity::MainWorktree { git_dir, .. }
| WorktreeIdentity::LinkedWorktree { git_dir, .. } => {
return Some(git_dir.as_path());
}
WorktreeIdentity::OutsideWorktree => {
return None;
}
}
}
#[cfg(test)]
#[path = "worktree_identity_tests.rs"]
mod tests;