use anyhow::{Context, Result, bail};
use std::process::Command;
use crate::record::{Record, parse_note};
pub const NOTES_REF: &str = "refs/notes/tak";
const REMOTE_REF: &str = "refs/notes/tak-remote";
const FETCH_REFSPEC: &str = "+refs/notes/tak:refs/notes/tak-remote";
const PUSH_REFSPEC: &str = "refs/notes/tak:refs/notes/tak";
const USER_FETCH_REFSPEC: &str = "+refs/notes/tak:refs/notes/tak";
const PUSH_ATTEMPTS: u32 = 5;
fn git(args: &[&str]) -> Result<String> {
let out = Command::new("git")
.args(args)
.output()
.with_context(|| format!("failed to run `git {}`", args.join(" ")))?;
if !out.status.success() {
bail!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&out.stdout).trim_end().to_string())
}
fn identity_args() -> Vec<&'static str> {
let configured = |key: &str| {
Command::new("git")
.args(["config", "--get", key])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
let mut args = Vec::new();
if !configured("user.name") {
args.extend_from_slice(&["-c", "user.name=tak"]);
}
if !configured("user.email") {
args.extend_from_slice(&["-c", "user.email=tak@localhost"]);
}
args
}
fn git_committing(args: &[&str]) -> Result<String> {
let mut full = identity_args();
full.extend_from_slice(args);
git(&full)
}
fn git_ok(args: &[&str]) -> Result<(bool, String)> {
let out = Command::new("git")
.args(args)
.output()
.with_context(|| format!("failed to run `git {}`", args.join(" ")))?;
let text = if out.status.success() {
String::from_utf8_lossy(&out.stdout).trim_end().to_string()
} else {
String::from_utf8_lossy(&out.stderr).trim_end().to_string()
};
Ok((out.status.success(), text))
}
pub fn fetch(remote: &str) -> Result<bool> {
let (ok, _) = git_ok(&["fetch", "--quiet", "--depth", "1", remote, FETCH_REFSPEC])?;
if !ok {
return Ok(false);
}
absorb_remote()?;
Ok(true)
}
fn absorb_remote() -> Result<()> {
let (has_local, _) = git_ok(&["rev-parse", "--verify", "--quiet", NOTES_REF])?;
if !has_local {
git(&["update-ref", NOTES_REF, REMOTE_REF])?;
return Ok(());
}
git_committing(&[
"notes",
"--ref",
NOTES_REF,
"merge",
"-s",
"cat_sort_uniq",
REMOTE_REF,
])?;
Ok(())
}
pub fn read(remote: Option<&str>, commit: &str) -> Result<Vec<Record>> {
if let Some(r) = remote {
let _ = fetch(r);
}
let (ok, body) = git_ok(&["notes", "--ref", NOTES_REF, "show", commit])?;
if !ok {
return Ok(vec![]);
}
Ok(parse_note(&body))
}
pub fn append(commit: &str, records: &[Record]) -> Result<()> {
if records.is_empty() {
return Ok(());
}
let mut lines: Vec<String> = Vec::new();
let (ok, existing) = git_ok(&["notes", "--ref", NOTES_REF, "show", commit])?;
if ok {
lines.extend(existing.lines().map(str::to_string));
}
for r in records {
lines.push(r.to_line()?);
}
lines.sort();
lines.dedup();
git_committing(&[
"notes",
"--ref",
NOTES_REF,
"add",
"-f",
"-m",
&lines.join("\n"),
commit,
])?;
Ok(())
}
pub fn push(remote: &str) -> Result<()> {
for attempt in 1..=PUSH_ATTEMPTS {
let (ok, err) = git_ok(&["push", "--quiet", remote, PUSH_REFSPEC])?;
if ok {
return Ok(());
}
if attempt == PUSH_ATTEMPTS {
bail!("could not push {NOTES_REF} after {PUSH_ATTEMPTS} attempts: {err}");
}
git(&["fetch", "--quiet", remote, FETCH_REFSPEC])?;
absorb_remote()?;
}
unreachable!()
}
pub fn rev_parse(rev: &str) -> Result<String> {
git(&["rev-parse", rev])
}
pub fn install_refspec(remote: &str) -> Result<()> {
let key = format!("remote.{remote}.fetch");
let (_, existing) = git_ok(&["config", "--get-all", &key])?;
if existing.lines().any(|l| l.trim() == USER_FETCH_REFSPEC) {
return Ok(());
}
git(&["config", "--add", &key, USER_FETCH_REFSPEC])?;
Ok(())
}