git-ents.gitmain
⌘K
foforge
commit 6e25e89
refactor: extract signed_edit from the settings-edit web write path

The stage-on-a-throwaway-ref, git push --signed, always-clean-up sequence was written inline in edit_config, specific to Config. Pulling it out as signed_edit, generic over any Facet value, names the operation’s contract explicitly (staging ref always deleted, author is the signed-in human, committer is the server, lands through the same pre-receive gate a CLI push traverses) so the next authenticated write path (issues, say) reuses it instead of re-deriving the same sequence. config::store_to_ref/ store_to_ref_authored are now dead now that the web path calls git_store::Store::store_authored directly, so config::store folds back to a single call.

refactor: add web::write::signed_edit, generic over Facet, with stage_and_push as its internal step refactor: fold config::store_to_ref/store_to_ref_authored back into config::store Assisted-by: Claude:claude-sonnet-4-6

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/git-ents/src/config.rs @@ -45,32 +45,13 @@ /// Write `config` to [`CONFIG_REF`] in `repo`, replacing any existing value as /// a new commit. -pub fn store(repo: &Path, config: &Config) -> Result<(), git_store::Error> { - store_to_ref(repo, CONFIG_REF, config) -} - -/// Build the configuration commit on `refname` in `repo` — chaining on that -/// ref's own tip — without touching [`CONFIG_REF`]. /// -/// The web write path stages an edit on a throwaway ref pointed at the current -/// config tip, then lands it onto [`CONFIG_REF`] through a signed push, so the -/// `pre-receive` gate judges the change rather than this writing the live ref -/// directly. -pub fn store_to_ref(repo: &Path, refname: &str, config: &Config) -> Result<(), git_store::Error> { - git_store::Store::open(repo)?.store(refname, config, "Update configuration") -} - -/// Like [`store_to_ref`], but recording `author` (a `(name, email)` pair) as -/// the commit's author while the committer stays the git-ents system identity. -/// The web write path uses this so an edit landed by the server still names the -/// human who made it. -pub fn store_to_ref_authored( - repo: &Path, - refname: &str, - config: &Config, - author: (&str, &str), -) -> Result<(), git_store::Error> { - git_store::Store::open(repo)?.store_authored(refname, config, "Update configuration", author) +/// The web write path does not call this directly: it lands an edit through +/// `git_ents_server::web::write::signed_edit`, which stages the commit on a +/// throwaway ref and pushes it onto [`CONFIG_REF`] through a signed push, so +/// the `pre-receive` gate judges the change rather than a direct write. +pub fn store(repo: &Path, config: &Config) -> Result<(), git_store::Error> { + git_store::Store::open(repo)?.store(CONFIG_REF, config, "Update configuration") } #[cfg(test)]
crates/git-ents-server/src/web/write.rs @@ -233,31 +233,82 @@ config.homepage = edit.homepage.clone(); config.topics = edit.topics.clone(); - let staging = format!("refs/web-staging/{}", random_token()?); - let result = stage_and_push(repo, &staging, &config, &username, signing_key, seed, hooks); - // Clean up the staging ref whether or not the push was accepted. - let _cleanup = git(repo, &["update-ref", "-d", &staging]); - result + signed_edit( + repo, + git_ents::config::CONFIG_REF, + &config, + "Update configuration", + &username, + signing_key, + seed, + hooks, + ) } -/// Point `staging` at the current config tip, build the new config commit on it -/// authored by `username`, then push it signed with the server's key onto -/// `refs/meta/config`. -fn stage_and_push( +/// Land `value` onto `target_ref` as a real `git push --signed`, authored by +/// `username` and signed with the server's own `signing_key`, through the +/// same `pre-receive` gate a CLI push traverses — the one landing operation +/// every authenticated browser write shares, whatever meta-ref it targets. +/// +/// The contract: `value` is built on a fresh ref staged at `target_ref`'s +/// current tip (so the push is a clean fast-forward), the staging ref is +/// *always* deleted before returning — whether or not the push was accepted, +/// so a rejected edit never leaves a zombie ref behind — and the commit that +/// lands is authored by `username` while the server is the committer. +#[expect( + clippy::too_many_arguments, + reason = "the server identity a signed edit requires" +)] +fn signed_edit<T: for<'a> facet::Facet<'a>>( repo: &Path, - staging: &str, - config: &git_ents::config::Config, + target_ref: &str, + value: &T, + message: &str, username: &str, signing_key: &Path, seed: &str, hooks: &Path, ) -> Result<(), String> { - if let Some(tip) = rev_parse(repo, git_ents::config::CONFIG_REF) { + let staging = format!("refs/web-staging/{}", random_token()?); + let result = stage_and_push( + repo, + &staging, + target_ref, + value, + message, + username, + signing_key, + seed, + hooks, + ); + // Clean up the staging ref whether or not the push was accepted. + let _cleanup = git(repo, &["update-ref", "-d", &staging]); + result +} + +/// Point `staging` at `target_ref`'s current tip, build the new commit on it +/// authored by `username`, then push it signed with the server's key onto +/// `target_ref`. +#[expect(clippy::too_many_arguments, reason = "internal step of signed_edit")] +fn stage_and_push<T: for<'a> facet::Facet<'a>>( + repo: &Path, + staging: &str, + target_ref: &str, + value: &T, + message: &str, + username: &str, + signing_key: &Path, + seed: &str, + hooks: &Path, +) -> Result<(), String> { + if let Some(tip) = rev_parse(repo, target_ref) { git(repo, &["update-ref", staging, &tip]) .map_err(|e| format!("could not stage the edit: {e}"))?; } let email = format!("{username}@web"); - git_ents::config::store_to_ref_authored(repo, staging, config, (username, &email)) + git_store::Store::open(repo) + .map_err(|e| format!("could not open store: {e}"))? + .store_authored(staging, value, message, (username, &email)) .map_err(|e| format!("could not build the edit: {e}"))?; let signer = signing_key @@ -270,7 +321,7 @@ "git -c receive.certNonceSeed={seed} -c receive.certNonceSlop=60 -c core.hooksPath={hooks} receive-pack" ); let url = format!("file://{}", repo.display()); - let refspec = format!("{staging}:{}", git_ents::config::CONFIG_REF); + let refspec = format!("{staging}:{target_ref}"); let output = Command::new("git") .arg("-C")