lifecycle: git helpers for tag-driven applied repo
new plumbing for the upcoming flow: git_fetch_to_tag (pulls a sha from proposed into applied and pins it as a tag in one shot), git_rev_parse (normalises shas + reads back tag targets), git_tag / git_tag_annotated (lightweight vs body- carrying for failed/denied), git_read_tree_reset (replace working tree without moving HEAD — lets main stay on last known-good across an in-flight build), git_update_ref (ff main on deploy). annotated tag bodies go via stdin to avoid escape games. all dead-code-allowed; callers land in subsequent commits.
This commit is contained in:
parent
b32c3d4f98
commit
63ef69674b
1 changed files with 93 additions and 0 deletions
|
|
@ -457,6 +457,99 @@ async fn git(dir: &Path, args: &[&str]) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch `sha` from the `src` git repo into `dst` and pin it as
|
||||
/// `refs/tags/<tag>`. Used at request_apply_commit time so hive-c0re
|
||||
/// captures an immutable handle on the manager's commit; subsequent
|
||||
/// amendments / force-pushes in `src` no longer affect what gets
|
||||
/// built. Returns the resolved sha (which equals `sha` on success
|
||||
/// but normalised — short shas get expanded).
|
||||
#[allow(dead_code)] // wired up by manager_server in a later commit
|
||||
pub async fn git_fetch_to_tag(dst: &Path, src: &Path, sha: &str, tag: &str) -> Result<String> {
|
||||
let src_str = src.display().to_string();
|
||||
let refspec = format!("{sha}:refs/tags/{tag}");
|
||||
git(dst, &["fetch", "--no-tags", &src_str, &refspec]).await?;
|
||||
git_rev_parse(dst, &format!("refs/tags/{tag}")).await
|
||||
}
|
||||
|
||||
/// Resolve `refname` (a tag, branch, or sha) in `dir` to its full sha.
|
||||
#[allow(dead_code)]
|
||||
pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
|
||||
let out = git_command()
|
||||
.current_dir(dir)
|
||||
.args(["rev-parse", refname])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("git rev-parse {refname} in {}", dir.display()))?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"git rev-parse {refname} failed ({}): {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
||||
}
|
||||
|
||||
/// Plant a lightweight tag at `target`. Errors if the tag already
|
||||
/// exists — we want loud failures on id reuse, not silent
|
||||
/// overwrites.
|
||||
#[allow(dead_code)]
|
||||
pub async fn git_tag(dir: &Path, name: &str, target: &str) -> Result<()> {
|
||||
git(dir, &["tag", name, target]).await
|
||||
}
|
||||
|
||||
/// Plant an annotated tag with `body` as the message. Used for
|
||||
/// `failed/<id>` (body = build error) and `denied/<id>` (body =
|
||||
/// operator note). Multi-line bodies handled via stdin so we don't
|
||||
/// have to escape anything.
|
||||
#[allow(dead_code)]
|
||||
pub async fn git_tag_annotated(dir: &Path, name: &str, target: &str, body: &str) -> Result<()> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut child = git_command()
|
||||
.current_dir(dir)
|
||||
.args(["tag", "-a", name, target, "-F", "-"])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("spawn git tag -a {name} in {}", dir.display()))?;
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(body.as_bytes())
|
||||
.await
|
||||
.context("write tag body to git stdin")?;
|
||||
// Drop closes stdin so git can finish reading.
|
||||
drop(stdin);
|
||||
}
|
||||
let out = child.wait_with_output().await.context("wait git tag -a")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"git tag -a {name} failed ({}): {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace working tree + index with the tree at `target` without
|
||||
/// moving HEAD. `applied/main` stays pointing at the last known-good
|
||||
/// `deployed/*` while we let `nixos-container update` evaluate the
|
||||
/// candidate. On build failure callers reset back to HEAD; on
|
||||
/// success they fast-forward main to `target`.
|
||||
#[allow(dead_code)]
|
||||
pub async fn git_read_tree_reset(dir: &Path, target: &str) -> Result<()> {
|
||||
git(dir, &["read-tree", "--reset", "-u", target]).await
|
||||
}
|
||||
|
||||
/// Hard-set a ref to `target`. Used to fast-forward `refs/heads/main`
|
||||
/// to the just-deployed proposal commit. Uses `update-ref`, not
|
||||
/// `branch -f`, so it works regardless of where HEAD currently sits.
|
||||
#[allow(dead_code)]
|
||||
pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<()> {
|
||||
git(dir, &["update-ref", refname, target]).await
|
||||
}
|
||||
|
||||
/// Returns true if the command exits 0.
|
||||
async fn git_status(dir: &Path, args: &[&str]) -> Result<bool> {
|
||||
let st = git_command()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue