zuka
zuka/src/store/mod.rs

worklyn / zukapublic

Agent-first git hosting. One Rust binary: git over HTTP and SSH, a REST API, MCP, CI, and multi-tenant isolation.

Get a copy: git clone https://zuka.worklyn.com/worklyn/zuka.git
zuka/src/store/mod.rs
RSmod.rs4.9 KBDownload
1// Metadata store.
2//
3// Standalone keeps metadata on the same local disk as the repositories, so a
4// self-hoster needs no external service. Control and tenant modes get a remote KV
5// implementation behind this same surface when the control plane lands; nothing
6// above this module knows which is in use.
7
8pub mod cached;
9pub mod fs;
10pub mod quota;
11
12use serde::{Deserialize, Serialize};
13
14/// Where a repository is in its lifecycle.
15///
16/// `Creating` is written *before* `git init` and flipped to `Ready` after. Without
17/// it, a crash between the two leaves a name that is simultaneously 404 (no
18/// metadata) and 409 (directory present) with no way to resolve it.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum RepoState {
22 Creating,
23 Ready,
24}
25
26/// Who may read a repository.
27///
28/// Private is the default and the value every record written before this field
29/// existed deserialises to, because the alternative — absent meaning public — would
30/// silently expose every repository on the first upgrade that understood the field.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
32#[serde(rename_all = "lowercase")]
33pub enum Visibility {
34 #[default]
35 Private,
36 /// Readable by anyone, with no credential. Read only: there is no anonymous
37 /// write, and making a repository public never grants a stranger a ref update.
38 Public,
39}
40
41impl Visibility {
42 pub fn is_public(self) -> bool {
43 self == Visibility::Public
44 }
45
46 pub fn as_str(self) -> &'static str {
47 match self {
48 Visibility::Private => "private",
49 Visibility::Public => "public",
50 }
51 }
52}
53
54impl std::str::FromStr for Visibility {
55 type Err = crate::error::Error;
56
57 fn from_str(s: &str) -> crate::error::Result<Self> {
58 match s {
59 "private" => Ok(Visibility::Private),
60 "public" => Ok(Visibility::Public),
61 other => Err(crate::error::Error::invalid(
62 "invalid-visibility",
63 format!("visibility must be private or public, not {other:?}"),
64 )),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RepoRecord {
71 /// Canonical (case-folded) account name. Also the path component.
72 pub account: String,
73 /// Canonical (case-folded) repository name. Also the path component.
74 pub name: String,
75 /// The name as the caller typed it, for display only. Never a path component.
76 pub display_name: String,
77 pub default_branch: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub description: Option<String>,
80 pub created_at: u64,
81 #[serde(default = "ready")]
82 pub state: RepoState,
83 /// Absent means private. See `Visibility`.
84 #[serde(default)]
85 pub visibility: Visibility,
86}
87
88fn ready() -> RepoState {
89 RepoState::Ready
90}
91
92impl RepoRecord {
93 pub fn is_ready(&self) -> bool {
94 self.state == RepoState::Ready
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 /// The record every repository written before visibility existed looks like.
103 const LEGACY: &str = r#"{
104 "account": "alice",
105 "name": "site",
106 "display_name": "site",
107 "default_branch": "refs/heads/main",
108 "created_at": 0,
109 "state": "ready"
110 }"#;
111
112 #[test]
113 fn a_record_written_before_visibility_existed_is_private() {
114 // The upgrade that first understands this field reads every existing record
115 // on disk. If absence meant public, deploying it would publish every
116 // repository on the host at once, silently and irreversibly.
117 let record: RepoRecord = serde_json::from_str(LEGACY).unwrap();
118 assert_eq!(record.visibility, Visibility::Private);
119 assert!(!record.visibility.is_public());
120 }
121
122 #[test]
123 fn visibility_survives_a_round_trip() {
124 for expected in [Visibility::Private, Visibility::Public] {
125 let record: RepoRecord = serde_json::from_str(LEGACY).unwrap();
126 let record = RepoRecord {
127 visibility: expected,
128 ..record
129 };
130 let encoded = serde_json::to_string(&record).unwrap();
131 let decoded: RepoRecord = serde_json::from_str(&encoded).unwrap();
132 assert_eq!(decoded.visibility, expected, "{encoded}");
133 }
134 }
135
136 #[test]
137 fn only_the_two_documented_visibilities_parse() {
138 assert_eq!(
139 "private".parse::<Visibility>().unwrap(),
140 Visibility::Private
141 );
142 assert_eq!("public".parse::<Visibility>().unwrap(), Visibility::Public);
143 // Anything unrecognised must be an error, never a default. A typo like
144 // "Public" quietly becoming private is a support ticket; quietly becoming
145 // public is a disclosure.
146 for bad in ["Public", "PUBLIC", "world", "", "true"] {
147 assert!(bad.parse::<Visibility>().is_err(), "{bad:?} must not parse");
148 }
149 }
150}