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 |