From 3f08051bda2d6b3aa4d9a421f87fde976e2dd3d1 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 21:27:31 +0200 Subject: [PATCH] lifecycle: seed applied repo at template commit, not main, for first-spawn diff --- hive-c0re/src/lifecycle.rs | 27 ++++++++++++++++++++++++++- hive-c0re/src/manager_server.rs | 7 ++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 2f67992..fd3986a 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -465,6 +465,12 @@ pub async fn setup_applied( }; git(applied_dir, &["init", "--initial-branch=main"]).await?; let proposed_str = proposed.display().to_string(); + // Seed the applied repo at the root (template) commit of proposed, + // not at `main`. This ensures `deployed/0` is the template baseline + // so the first ApplyCommit diff shows the manager's real changes + // rather than an empty diff (which happens when the manager has + // already committed their config and proposed/main == proposal/). + let root_sha = git_root_commit(proposed).await?; git( applied_dir, // --update-head-ok lets us fetch into refs/heads/main while @@ -477,7 +483,7 @@ pub async fn setup_applied( "--no-tags", "--update-head-ok", &proposed_str, - "main:refs/heads/main", + &format!("{root_sha}:refs/heads/main"), ], ) .await?; @@ -555,6 +561,25 @@ pub fn initial_flake_nix() -> &'static str { "{\n description = \"hyperhive agent\";\n inputs = { };\n outputs =\n { self, ... }@inputs:\n {\n nixosModules.default = {\n imports = [ ./agent.nix ];\n _module.args.flakeInputs = builtins.removeAttrs inputs [ \"self\" ];\n };\n };\n}\n" } +/// Return the SHA of the root (oldest, no-parent) commit in a repo. +/// Used to seed the applied repo at the template baseline rather than at +/// `main`, so the first `ApplyCommit` diff shows the manager's real changes. +async fn git_root_commit(dir: &Path) -> Result { + let out = git_command() + .current_dir(dir) + .args(["rev-list", "--max-parents=0", "HEAD"]) + .output() + .await + .with_context(|| format!("git rev-list --max-parents=0 HEAD in {}", dir.display()))?; + if !out.status.success() { + anyhow::bail!( + "git rev-list --max-parents=0 failed: {}", + String::from_utf8_lossy(&out.stderr).trim() + ); + } + Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned()) +} + async fn git_commit(dir: &Path, message: &str) -> Result<()> { git( dir, diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 152ba7c..7e68211 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -562,9 +562,10 @@ async fn submit_apply_commit( } if !applied_dir.join(".git").exists() { // First deploy: seed the applied repo from proposed so we can plant - // the proposal/ tag below. The applied repo starts at the - // template commit (deployed/0); run_apply_commit will fast-forward - // main to the manager's commit on approval and create the container. + // the proposal/ tag below. setup_applied seeds at the root + // (template) commit of proposed, not at main, so deployed/0 is the + // template baseline. This makes the diff mara sees on approval + // show the manager's actual changes rather than an empty diff. lifecycle::setup_applied(&applied_dir, Some(&proposed_dir), agent) .await .context("seed applied repo for first spawn")?;