OXYGENHUB

A bare repository's default bookmark is its host's to protect 8c30d814f2cc

The core refused to delete a repository's configured bookmark. A forge
keeps its default-bookmark setting apart from that config, so the core now
protects only a non-bare repository's checked-out bookmark, and a bare
one's default comes from the host through Backend::default_bookmark
(oxy serve: the repository's configured bookmark). HTTP test for both.

closes #13
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-28 22:37 UTC
Signature
✔ signed by Andy McCall
Hash
8c30d814f2cc6160bd620a48f70c002991c2ef13fa46565a77151c2854b0c13d
Depends on
750251b21a96 75167672ebcd 982864068a59 e02bf0cd0533 f8c695764c95 ff0af1ee299d
Depended on by
67a4fe1aa2d5 cb8b8082fa73 be451356e3ef
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
8 atoms, 0 tree operations, 4 files

Browse the tree at this change

modified crates/oxy-core/src/repo/sync.rs +6 −4
…
605 unchanged lines
606 606
    }
607 607
608 608
    /// A deletion (spec §16.4): `new_state` is zero. The bookmark must exist
609 -
    /// at `old_state`, is never the repository's own bookmark (its default),
610 -
    /// and under a policy needs an attestation by a key that may update it.
609 +
    /// at `old_state`, is never the bookmark a non-bare repository has
610 +
    /// checked out (a bare one's default is its host's to protect, see
611 +
    /// `oxy_server::Backend::default_bookmark`), and under a policy needs
612 +
    /// an attestation by a key that may update it.
611 613
    /// The deletion's attestation stays the bookmark's latest, so an older
612 614
    /// one cannot be replayed to bring it back.
613 615
    fn apply_deletion(
…
8 unchanged lines
622 624
        if current != u.old_state {
623 625
            return Err("stale: the remote bookmark has moved; pull first".into());
624 626
        }
625 -
        if u.name == self.config.bookmark {
627 +
        if !self.is_bare() && u.name == self.config.bookmark {
626 628
            return Err(format!(
627 -
                "{:?} is this repository's default bookmark and cannot be deleted",
629 +
                "{:?} is this repository's default bookmark (checked out there) and cannot be deleted",
628 630
                u.name
629 631
            ));
630 632
        }
…
1014 unchanged lines
modified crates/oxy-server/src/lib.rs +4 −0
…
320 unchanged lines
321 321
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
322 322
    }
323 323
324 +
    fn default_bookmark(&self, name: &str) -> Option<String> {
325 +
        self.open(name).ok().map(|r| r.config.bookmark.clone())
326 +
    }
327 +
324 328
    fn authorize(&self, _name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> {
325 329
        if access == Access::Read || self.tokens.is_empty() {
326 330
            return Ok(());
…
85 unchanged lines
modified crates/oxy-server/tests/http.rs +29 −0
…
614 unchanged lines
615 615
        matches!(local.authenticate_change(&theirs).unwrap(), SignatureStatus::Invalid(why) if why.contains("neither the author nor a delegate"))
616 616
    );
617 617
}
618 +
619 +
/// `oxy push --delete` over HTTP: a bare repository's bookmark goes, and
620 +
/// its default (the served repository's own bookmark) is refused by the
621 +
/// backend (spec §16.4).
622 +
#[test]
623 +
fn bookmarks_are_deleted_over_http_but_never_the_default() {
624 +
    hermetic();
625 +
    let dir = tempfile::tempdir().unwrap();
626 +
    let root = dir.path();
627 +
    let served = root.join("served");
628 +
    let hosted = Repo::init_bare(&served.join("lib")).unwrap();
629 +
    let base = start(&served, vec![]);
630 +
    let work = root.join("work");
631 +
    let client = Repo::clone(&format!("{base}/lib"), &work).unwrap();
632 +
    write(&work, "a.txt", b"a\n");
633 +
    client.commit("a", &[]).unwrap();
634 +
    assert!(client.push("origin", false).unwrap()[0].ok);
635 +
    client.bookmark_create("feature/x", None).unwrap();
636 +
    assert!(client.push_bookmark("origin", "feature/x", false).unwrap()[0].ok);
637 +
    let r = client.push_delete("origin", "feature/x").unwrap();
638 +
    assert!(r[0].ok, "{r:?}");
639 +
    assert!(!hosted.view().unwrap().bookmarks.contains_key("feature/x"));
640 +
    let r = client.push_delete("origin", "main").unwrap();
641 +
    assert!(
642 +
        !r[0].ok && r[0].reason.contains("default bookmark"),
643 +
        "{r:?}"
644 +
    );
645 +
    assert!(hosted.view().unwrap().bookmarks.contains_key("main"));
646 +
}
modified docs/spec.md +1 −1
…
742 unchanged lines
743 743
744 744
On push, the server MUST, in order: verify that every change has a valid signature or seal from a key embedded in policy and satisfies the author-or-delegate rule (§14.4), verify the attestation for each reference, check that `old_state` matches its current state (compare-and-swap), check policy rules for the reference including `allow_conflicts`, check that every dependency of every change is either included or already stored, and only then update references atomically. A failure rejects the whole push.
745 745
746 -
**Deleting a reference.** An update whose `new_state` is the zero ObjectRef deletes the bookmark (`oxy push REMOTE --delete NAME`). The bookmark MUST exist at `old_state`, and a server MUST refuse to delete its default bookmark. The update is attested like any other: under a policy the attestation is required and its key MUST be authorized for the reference. The deletion's attestation stays the reference's latest, so an older attestation cannot be replayed to bring the bookmark back. The first attestation of a reference the remote does not list takes the client's clock in milliseconds as its serial, which is past the serial of any deleted namesake; later ones increment it. Changes of a deleted bookmark stay until unreachable (§21).
746 +
**Deleting a reference.** An update whose `new_state` is the zero ObjectRef deletes the bookmark (`oxy push REMOTE --delete NAME`). The bookmark MUST exist at `old_state`. A server MUST refuse to delete its default bookmark: in a bare repository that is the host's to name (the reference server's is the repository's configured bookmark, a forge's its default-bookmark setting), and in a non-bare repository it is the checked-out bookmark. The update is attested like any other: under a policy the attestation is required and its key MUST be authorized for the reference. The deletion's attestation stays the reference's latest, so an older attestation cannot be replayed to bring the bookmark back. The first attestation of a reference the remote does not list takes the client's clock in milliseconds as its serial, which is past the serial of any deleted namesake; later ones increment it. Changes of a deleted bookmark stay until unreachable (§21).
747 747
748 748
## 17. Partial, sparse, and shallow repositories
749 749
…
262 unchanged lines