feat(cli): scaffolds make the initial commit — provenance resolves from run one

aura new and aura nodes new follow their git init with a best-effort
initial commit (inline committer identity, warning-never-fatal, skipped
whenever the init itself was skipped per #204), so a fresh project's
first run stamps a real commit instead of an empty provenance block.

closes #243
This commit is contained in:
2026-07-12 19:30:26 +02:00
parent bb58442087
commit 66c3e9d6f3
2 changed files with 69 additions and 2 deletions
+41 -2
View File
@@ -283,7 +283,9 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
return Ok(()); return Ok(());
} }
match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() { match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() {
Ok(o) if o.status.success() => {} Ok(o) if o.status.success() => {
commit_all(&spec.target_dir, "aura new scaffold");
}
_ => eprintln!("aura: warning: git init failed (project created without a repo)"), _ => eprintln!("aura: warning: git init failed (project created without a repo)"),
} }
Ok(()) Ok(())
@@ -322,7 +324,9 @@ pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> {
return Ok(()); return Ok(());
} }
match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() { match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() {
Ok(o) if o.status.success() => {} Ok(o) if o.status.success() => {
commit_all(&spec.target_dir, "aura nodes new scaffold");
}
_ => eprintln!("aura: warning: git init failed (node crate created without a repo)"), _ => eprintln!("aura: warning: git init failed (node crate created without a repo)"),
} }
Ok(()) Ok(())
@@ -344,6 +348,41 @@ pub fn append_nodes_pointer(project_root: &Path, pointer: &str) -> Result<(), St
.map_err(|e| format!("writing `{}`: {e}", path.display())) .map_err(|e| format!("writing `{}`: {e}", path.display()))
} }
/// Stage everything and make the initial commit in a freshly-`git init`ed
/// scaffold, so `git rev-parse HEAD` resolves from the very first run
/// (otherwise HEAD stays unborn and provenance stamping sees no commit). A
/// committer identity is passed inline via `-c` so this works on a box with
/// no global git config; best-effort — a failure is a warning, never fatal,
/// and never runs when `git init` itself was skipped or failed.
fn commit_all(dir: &std::path::Path, message: &str) {
let add = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(dir)
.output();
if !matches!(add, Ok(o) if o.status.success()) {
eprintln!("aura: warning: git add failed (scaffold created without an initial commit)");
return;
}
let commit = std::process::Command::new("git")
.args([
"-c",
"user.email=aura@localhost",
"-c",
"user.name=aura",
"commit",
"-q",
"-m",
message,
])
.current_dir(dir)
.output();
if !matches!(commit, Ok(o) if o.status.success()) {
eprintln!(
"aura: warning: git commit failed (scaffold created without an initial commit)"
);
}
}
/// Whether `dir` already sits inside a git work tree (`git rev-parse`, the /// Whether `dir` already sits inside a git work tree (`git rev-parse`, the
/// same oracle cargo-new consults). A missing git binary or any failure reads /// same oracle cargo-new consults). A missing git binary or any failure reads
/// as "not in a work tree" — the init stays best-effort either way. /// as "not in a work tree" — the init stays best-effort either way.
+28
View File
@@ -37,6 +37,34 @@ fn nodes_new_scaffolds_a_sibling_crate_and_attaches_it() {
assert!(toml.contains("../lab-nodes"), "{toml}"); assert!(toml.contains("../lab-nodes"), "{toml}");
} }
/// Property (#243): a freshly scaffolded node crate stamps resolvable
/// provenance too — `aura nodes new` follows the same `git init` with an
/// initial commit as `aura new` (`scaffold_node_crate`, `scaffold.rs`), so
/// `git rev-parse HEAD` resolves from the crate's very first run rather than
/// leaving HEAD unborn. This half of #243's property runs and is asserted
/// separately from `new_outside_a_work_tree_leaves_a_resolvable_head`
/// (`project_new.rs`), which covers the `aura new` project half only.
#[test]
fn nodes_new_leaves_a_resolvable_head() {
let cwd = temp_cwd("resolvable-head");
assert!(aura(&["new", "lab"], &cwd).status.success());
let proj = cwd.join("lab");
let engine = env!("CARGO_MANIFEST_DIR").to_string() + "/../..";
let out = aura(&["nodes", "new", "lab-nodes", "--engine-path", &engine], &proj);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let crate_dir = cwd.join("lab-nodes");
let head = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&crate_dir)
.output()
.expect("run git rev-parse HEAD");
assert!(
head.status.success(),
"fresh node-crate scaffold has no HEAD (`git rev-parse HEAD` failed): {}",
String::from_utf8_lossy(&head.stderr)
);
}
/// Property (#241): `--namespace` sets the vocabulary prefix independently /// Property (#241): `--namespace` sets the vocabulary prefix independently
/// of the crate's own (directory) name — a crate can be named anything, but /// of the crate's own (directory) name — a crate can be named anything, but
/// the blueprints that reference its nodes address them under the namespace /// the blueprints that reference its nodes address them under the namespace