use crate::service_protocol::FailureCode;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::path::Path;
#[derive(Debug, Eq, PartialEq)]
enum HeadFile {
Branch(String),
Detached(String),
Unreadable,
}
const REFTABLE_PLACEHOLDER: &str = "refs/heads/.invalid";
fn interpret(text: &str) -> HeadFile {
let line: &str = text.lines().next().unwrap_or("").trim_end();
return match line.strip_prefix("ref: ") {
Some(reference) if reference == REFTABLE_PLACEHOLDER => HeadFile::Unreadable,
Some(reference) => HeadFile::Branch(reference.to_string()),
None if line.is_empty() => HeadFile::Unreadable,
None => HeadFile::Detached(line.to_string()),
};
}
fn belongs_to(directory: &Path, common_dir: &Path) -> bool {
let Ok(expected) = std::fs::canonicalize(common_dir) else { return false };
if std::fs::canonicalize(directory).is_ok_and(|found| { return found == expected; }) {
return true;
}
let Ok(pointer) = std::fs::read_to_string(directory.join("commondir")) else { return false };
return std::fs::canonicalize(directory.join(pointer.trim_end())).is_ok_and(|found| { return found == expected; });
}
fn head_file(git_dir: &Path, common_dir: &Path) -> HeadFile {
if !belongs_to(git_dir, common_dir) {
return HeadFile::Unreadable;
}
return std::fs::read_to_string(git_dir.join("HEAD")).ok().map_or(HeadFile::Unreadable, |text| { return interpret(text.as_str()); });
}
fn symbolic_head(git: &GitContext, repository: &Path) -> Result<Option<String>, FailureCode> {
let output = run_git(git, &GitRequest::new(repository, &["symbolic-ref", "--quiet", "HEAD"])).map_err(|_| { return FailureCode::Unavailable; })?;
return match output.code {
Some(0) => Ok(Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string())),
Some(1) => Ok(None),
_ => Err(FailureCode::Unavailable),
};
}
pub fn follows_move(git: &GitContext, repository: &Path, git_dir: &Path, common_dir: &Path, reference: &str, new: &str) -> Result<bool, FailureCode> {
if reference != "HEAD" {
return head_names(git, repository, git_dir, common_dir, reference);
}
if let HeadFile::Detached(value) = head_file(git_dir, common_dir) {
return Ok(value == new);
}
let output = run_git(git, &GitRequest::new(repository, &["rev-parse", "--verify", "--quiet", "HEAD"])).map_err(|_| { return FailureCode::Unavailable; })?;
return match output.code {
Some(0) => Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim() == new),
Some(1) => Ok(false),
_ => Err(FailureCode::Unavailable),
};
}
pub fn head_names(git: &GitContext, repository: &Path, git_dir: &Path, common_dir: &Path, reference: &str) -> Result<bool, FailureCode> {
match head_file(git_dir, common_dir) {
HeadFile::Branch(named) => return Ok(named == reference),
HeadFile::Detached(_) => return Ok(false),
HeadFile::Unreadable => {}
}
return match symbolic_head(git, repository)? {
Some(named) => Ok(named == reference),
None => Ok(false),
};
}
pub fn still_checked_out(git: &GitContext, repository: &Path, git_dir: &Path, common_dir: &Path, reference: &str) -> Result<bool, FailureCode> {
if reference != "HEAD" {
return head_names(git, repository, git_dir, common_dir, reference);
}
match head_file(git_dir, common_dir) {
HeadFile::Branch(_) => return Ok(false),
HeadFile::Detached(_) => return Ok(true),
HeadFile::Unreadable => {}
}
return Ok(symbolic_head(git, repository)?.is_none());
}
#[cfg(test)]
#[path = "service_head_tests.rs"]
mod tests;