use std::path::{Path, PathBuf};
use super::git::Git;
use super::location::LedgerLocation;
pub const TIPS_FILE: &str = "remote-tips";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteHeads {
pub ledger: Option<String>,
pub docs: Option<String>,
}
pub fn remote_heads(git: &Git, location: &LedgerLocation) -> Result<RemoteHeads, String> {
let docs = location.docs();
let (args, output) = git.output([
"ls-remote",
location.remote(),
location.ref_name(),
docs.ref_name(),
])?;
if !output.status.success() {
return Err(super::git::failure_message(&args, &output));
}
Ok(parse_heads(
&String::from_utf8_lossy(&output.stdout),
location.ref_name(),
docs.ref_name(),
))
}
pub fn parse_heads(listing: &str, ledger_ref: &str, docs_ref: &str) -> RemoteHeads {
let mut heads = RemoteHeads {
ledger: None,
docs: None,
};
for line in listing.lines() {
let mut fields = line.split_whitespace();
let (Some(commit), Some(name)) = (fields.next(), fields.next()) else {
continue;
};
if name == ledger_ref {
heads.ledger = Some(commit.to_string());
} else if name == docs_ref {
heads.docs = Some(commit.to_string());
}
}
heads
}
pub struct Tips {
file: PathBuf,
}
impl Tips {
pub fn new(clone_dir: &Path) -> Tips {
Tips {
file: clone_dir.join(TIPS_FILE),
}
}
fn read(&self) -> Vec<(String, String, String)> {
let Ok(text) = std::fs::read_to_string(&self.file) else {
return Vec::new();
};
text.lines()
.filter_map(|line| {
let mut fields = line.splitn(3, '\t');
Some((
fields.next()?.to_string(),
fields.next()?.to_string(),
fields.next()?.to_string(),
))
})
.collect()
}
pub fn get(&self, remote: &str, ref_name: &str) -> Option<String> {
self.read()
.into_iter()
.find(|(r, n, _)| r == remote && n == ref_name)
.map(|(_, _, commit)| commit)
}
pub fn set(&self, remote: &str, ref_name: &str, commit: Option<&str>) {
let mut entries: Vec<(String, String, String)> = self
.read()
.into_iter()
.filter(|(r, n, _)| !(r == remote && n == ref_name))
.collect();
if let Some(commit) = commit {
entries.push((remote.to_string(), ref_name.to_string(), commit.to_string()));
}
let text: String = entries
.iter()
.map(|(r, n, c)| format!("{}\t{}\t{}\n", r, n, c))
.collect();
if let Some(parent) = self.file.parent() {
if std::fs::create_dir_all(parent).is_err() {
return;
}
}
let temp = self
.file
.with_extension(format!("tmp{}", std::process::id()));
if std::fs::write(&temp, text).is_ok() && std::fs::rename(&temp, &self.file).is_err() {
let _ = std::fs::remove_file(&temp);
}
}
}
pub fn commit_is_local(git: &Git, commit: &str) -> bool {
let spec = format!("{}^{{commit}}", commit);
git.output(["cat-file", "-e", spec.as_str()])
.is_ok_and(|(_, output)| output.status.success())
}
#[cfg(test)]
mod tests {
use super::*;
const LEDGER: &str = "refs/heads/tsk/ledger";
const DOCS: &str = "refs/heads/tsk/docs";
#[test]
fn the_listing_gives_the_commit_of_each_exact_ref() {
let listing = format!(
"{}\t{}\n{}\t{}\n",
"a".repeat(40),
LEDGER,
"b".repeat(40),
DOCS
);
let heads = parse_heads(&listing, LEDGER, DOCS);
assert_eq!(heads.ledger.as_deref(), Some("a".repeat(40).as_str()));
assert_eq!(heads.docs.as_deref(), Some("b".repeat(40).as_str()));
}
#[test]
fn a_ref_that_only_ends_in_the_pattern_is_not_the_ref() {
let listing = format!("{}\trefs/heads/x/refs/heads/tsk/ledger\n", "a".repeat(40));
let heads = parse_heads(&listing, LEDGER, DOCS);
assert_eq!(heads.ledger, None);
assert_eq!(heads.docs, None);
assert_eq!(
parse_heads("", LEDGER, DOCS),
RemoteHeads {
ledger: None,
docs: None
}
);
}
#[test]
fn a_tip_is_recorded_replaced_and_forgotten_per_remote_and_ref() {
let dir = tempfile::tempdir().unwrap();
let tips = Tips::new(&dir.path().join("repos").join("id"));
assert_eq!(tips.get("origin", LEDGER), None);
tips.set("origin", LEDGER, Some("1111"));
tips.set("origin", DOCS, Some("2222"));
tips.set("https://nexus", LEDGER, Some("3333"));
assert_eq!(tips.get("origin", LEDGER).as_deref(), Some("1111"));
assert_eq!(tips.get("origin", DOCS).as_deref(), Some("2222"));
tips.set("origin", LEDGER, Some("4444"));
assert_eq!(tips.get("origin", LEDGER).as_deref(), Some("4444"));
assert_eq!(tips.get("https://nexus", LEDGER).as_deref(), Some("3333"));
tips.set("origin", LEDGER, None);
assert_eq!(tips.get("origin", LEDGER), None);
assert_eq!(tips.get("origin", DOCS).as_deref(), Some("2222"));
}
}