refactor: extract signed_edit from the settings-edit web write path
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
No reviews of this commit yet — record a verdict below.
Start a review
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")