use crate::shadow_git::shadow_request;
use crate::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
use crate::private_storage::write_private_file_unsynced;
use crate::transaction_journal::{Base, RefFormat, SymbolicHead};
use std::path::{Path, PathBuf};
pub const PACKED_REFS_HEADER: &str = "# pack-refs with: sorted \n";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RefSnapshotEntry {
pub name: String,
pub oid: String,
pub symref: String,
}
pub fn list_real_refs(context: &GitContext, cwd: &Path) -> Result<Vec<RefSnapshotEntry>, GitFailure> {
let mut request: GitRequest = GitRequest::new(
cwd,
&["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"],
);
request.without_prefix = true;
let output: GitOutput = run_git_checked(context, &request)?;
let text: &str = std::str::from_utf8(output.stdout.as_slice())
.map_err(|_| { return GitFailure(String::from("git for-each-ref returned non-UTF-8 output.")); })?;
let mut refs: Vec<RefSnapshotEntry> = Vec::new();
for line in text.split('\n').filter(|line| { return !line.is_empty(); }) {
let fields: Vec<&str> = line.split('\0').collect();
let [name, oid, symref] = fields.as_slice() else {
return Err(GitFailure(format!(
"git for-each-ref returned a malformed line: {line:?}"
)));
};
refs.push(RefSnapshotEntry {
name: String::from(*name),
oid: String::from(*oid),
symref: String::from(*symref),
});
}
refs.sort_by(|left, right| { return left.name.as_bytes().cmp(right.name.as_bytes()); });
return Ok(refs);
}
pub fn format_packed_refs(refs: &[RefSnapshotEntry], target_ref: &str) -> String {
let mut text: String = String::from(PACKED_REFS_HEADER);
for entry in refs
.iter()
.filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
{
text.push_str(format!("{} {}\n", entry.oid, entry.name).as_str());
}
return text;
}
fn write_shadow_head(
context: &GitContext,
shadow: &Path,
symbolic_head: &SymbolicHead,
base: &Base,
) -> Result<(), GitFailure> {
match symbolic_head {
SymbolicHead::Branch(reference) => {
if let Base::Commit(oid) = base {
run_git_checked(
context,
&shadow_request(shadow, shadow, &["update-ref", reference.as_str(), oid.as_str()]),
)?;
}
run_git_checked(
context,
&shadow_request(shadow, shadow, &["symbolic-ref", "HEAD", reference.as_str()]),
)?;
return Ok(());
}
SymbolicHead::Detached => {
let Base::Commit(oid) = base else {
return Err(GitFailure(String::from(
"A detached HEAD has no unborn preparation base.",
)));
};
run_git_checked(
context,
&shadow_request(shadow, shadow, &["update-ref", "--no-deref", "HEAD", oid.as_str()]),
)?;
return Ok(());
}
}
}
pub fn write_shadow_refs(
context: &GitContext,
cwd: &Path,
shadow: &Path,
ref_format: RefFormat,
target_ref: &str,
symbolic_head: &SymbolicHead,
base: &Base,
) -> Result<(), GitFailure> {
let refs: Vec<RefSnapshotEntry> = list_real_refs(context, cwd)?;
match ref_format {
RefFormat::Files => {
write_private_file_unsynced(
PathBuf::from(shadow).join("packed-refs").as_path(),
format_packed_refs(&refs, target_ref).as_bytes(),
)
.map_err(|error| { return GitFailure(format!("writing packed-refs failed: {error}")); })?;
}
RefFormat::Reftable => {
let mut lines: String = String::new();
for entry in refs
.iter()
.filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
{
lines.push_str(format!("create {} {}\n", entry.name, entry.oid).as_str());
}
let mut request: GitRequest = shadow_request(shadow, shadow, &["update-ref", "--stdin"]);
request.input = Some(lines.into_bytes());
run_git_checked(context, &request)?;
}
}
for entry in refs.iter().filter(|entry| { return !entry.symref.is_empty(); }) {
run_git_checked(
context,
&shadow_request(shadow, shadow, &["symbolic-ref", entry.name.as_str(), entry.symref.as_str()]),
)?;
}
return write_shadow_head(context, shadow, symbolic_head, base);
}
#[cfg(test)]
#[path = "shadow_refs_tests.rs"]
mod tests;