fabrica

hanna/fabrica

build: initialize workspace and nix flake

58e3d63 · hanna committed on 2026-07-25

Phase 1 scaffold: a Cargo workspace with the root `fabrica` binary and eleven
empty in-tree crates (config, store, model, git, highlight, auth, mail, ssh,
web, api, cli), wired with the enforced one-way dependency direction.

- flake.nix (flake-parts + fenix nightly + crane) exposing packages.{default,
  fabrica,container}, checks.{clippy,fmt,test,deny,doc}, a dev shell, and a
  formatter; the binary is wrapped with git on PATH.
- rust-toolchain.toml (nightly, for non-Nix users), edition 2024, resolver 3.
- Workspace lints: forbid unsafe_code; deny clippy::all/unwrap_used/panic/todo.
- justfile `check` runs the four-command quality gate.
- MPL-2.0 LICENSE with per-file headers; deny.toml; seeded docs/decisions.md.

`nix flake check` is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
33 files changed · +1183 −0UnifiedSplit
.gitignore +8 −0
@@ -0,0 +1,8 @@
1+/target
2+result
3+result-*
4+.direnv/
5+*.db
6+*.db-wal
7+*.db-shm
8+.env
Cargo.lock +54 −0
@@ -0,0 +1,54 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
3+version = 4
4+
5+[[package]]
6+name = "api"
7+version = "0.1.0"
8+
9+[[package]]
10+name = "auth"
11+version = "0.1.0"
12+
13+[[package]]
14+name = "cli"
15+version = "0.1.0"
16+
17+[[package]]
18+name = "config"
19+version = "0.1.0"
20+
21+[[package]]
22+name = "fabrica"
23+version = "0.1.0"
24+dependencies = [
25+ "cli",
26+]
27+
28+[[package]]
29+name = "git"
30+version = "0.1.0"
31+
32+[[package]]
33+name = "highlight"
34+version = "0.1.0"
35+
36+[[package]]
37+name = "mail"
38+version = "0.1.0"
39+
40+[[package]]
41+name = "model"
42+version = "0.1.0"
43+
44+[[package]]
45+name = "ssh"
46+version = "0.1.0"
47+
48+[[package]]
49+name = "store"
50+version = "0.1.0"
51+
52+[[package]]
53+name = "web"
54+version = "0.1.0"
Cargo.toml +65 −0
@@ -0,0 +1,65 @@
1+[package]
2+name = "fabrica"
3+description = "A self-hosted git server: a single binary serving repos over HTTPS and SSH with a themeable web UI."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[[bin]]
13+name = "fabrica"
14+path = "src/main.rs"
15+
16+[dependencies]
17+cli = { path = "crates/cli" }
18+
19+[lints]
20+workspace = true
21+
22+[workspace]
23+members = ["crates/*"]
24+resolver = "3"
25+
26+[workspace.package]
27+version = "0.1.0"
28+edition = "2024"
29+rust-version = "1.85"
30+license = "MPL-2.0"
31+authors = ["fabrica contributors"]
32+repository = "https://git.example.com/fabrica"
33+publish = false
34+
35+[workspace.dependencies]
36+# In-tree crates. Dependency direction is strictly one-way and enforced:
37+# model <- store / git / auth <- web / api / ssh / cli
38+# No crate may depend on `web`; `model` depends on nothing in-tree.
39+config = { path = "crates/config" }
40+store = { path = "crates/store" }
41+model = { path = "crates/model" }
42+git = { path = "crates/git" }
43+highlight = { path = "crates/highlight" }
44+auth = { path = "crates/auth" }
45+mail = { path = "crates/mail" }
46+ssh = { path = "crates/ssh" }
47+web = { path = "crates/web" }
48+api = { path = "crates/api" }
49+cli = { path = "crates/cli" }
50+
51+[workspace.lints.rust]
52+unsafe_code = "forbid"
53+missing_docs = "warn"
54+
55+[workspace.lints.clippy]
56+all = { level = "deny", priority = -1 }
57+pedantic = { level = "warn", priority = -1 }
58+unwrap_used = "deny"
59+expect_used = "warn"
60+panic = "deny"
61+todo = "deny"
62+
63+[profile.release]
64+strip = true
65+lto = "thin"
LICENSE +373 −0
@@ -0,0 +1,373 @@
1+Mozilla Public License Version 2.0
2+==================================
3+
4+1. Definitions
5+--------------
6+
7+1.1. "Contributor"
8+ means each individual or legal entity that creates, contributes to
9+ the creation of, or owns Covered Software.
10+
11+1.2. "Contributor Version"
12+ means the combination of the Contributions of others (if any) used
13+ by a Contributor and that particular Contributor's Contribution.
14+
15+1.3. "Contribution"
16+ means Covered Software of a particular Contributor.
17+
18+1.4. "Covered Software"
19+ means Source Code Form to which the initial Contributor has attached
20+ the notice in Exhibit A, the Executable Form of such Source Code
21+ Form, and Modifications of such Source Code Form, in each case
22+ including portions thereof.
23+
24+1.5. "Incompatible With Secondary Licenses"
25+ means
26+
27+ (a) that the initial Contributor has attached the notice described
28+ in Exhibit B to the Covered Software; or
29+
30+ (b) that the Covered Software was made available under the terms of
31+ version 1.1 or earlier of the License, but not also under the
32+ terms of a Secondary License.
33+
34+1.6. "Executable Form"
35+ means any form of the work other than Source Code Form.
36+
37+1.7. "Larger Work"
38+ means a work that combines Covered Software with other material, in
39+ a separate file or files, that is not Covered Software.
40+
41+1.8. "License"
42+ means this document.
43+
44+1.9. "Licensable"
45+ means having the right to grant, to the maximum extent possible,
46+ whether at the time of the initial grant or subsequently, any and
47+ all of the rights conveyed by this License.
48+
49+1.10. "Modifications"
50+ means any of the following:
51+
52+ (a) any file in Source Code Form that results from an addition to,
53+ deletion from, or modification of the contents of Covered
54+ Software; or
55+
56+ (b) any new file in Source Code Form that contains any Covered
57+ Software.
58+
59+1.11. "Patent Claims" of a Contributor
60+ means any patent claim(s), including without limitation, method,
61+ process, and apparatus claims, in any patent Licensable by such
62+ Contributor that would be infringed, but for the grant of the
63+ License, by the making, using, selling, offering for sale, having
64+ made, import, or transfer of either its Contributions or its
65+ Contributor Version.
66+
67+1.12. "Secondary License"
68+ means either the GNU General Public License, Version 2.0, the GNU
69+ Lesser General Public License, Version 2.1, the GNU Affero General
70+ Public License, Version 3.0, or any later versions of those
71+ licenses.
72+
73+1.13. "Source Code Form"
74+ means the form of the work preferred for making modifications.
75+
76+1.14. "You" (or "Your")
77+ means an individual or a legal entity exercising rights under this
78+ License. For legal entities, "You" includes any entity that
79+ controls, is controlled by, or is under common control with You. For
80+ purposes of this definition, "control" means (a) the power, direct
81+ or indirect, to cause the direction or management of such entity,
82+ whether by contract or otherwise, or (b) ownership of more than
83+ fifty percent (50%) of the outstanding shares or beneficial
84+ ownership of such entity.
85+
86+2. License Grants and Conditions
87+--------------------------------
88+
89+2.1. Grants
90+
91+Each Contributor hereby grants You a world-wide, royalty-free,
92+non-exclusive license:
93+
94+(a) under intellectual property rights (other than patent or trademark)
95+ Licensable by such Contributor to use, reproduce, make available,
96+ modify, display, perform, distribute, and otherwise exploit its
97+ Contributions, either on an unmodified basis, with Modifications, or
98+ as part of a Larger Work; and
99+
100+(b) under Patent Claims of such Contributor to make, use, sell, offer
101+ for sale, have made, import, and otherwise transfer either its
102+ Contributions or its Contributor Version.
103+
104+2.2. Effective Date
105+
106+The licenses granted in Section 2.1 with respect to any Contribution
107+become effective for each Contribution on the date the Contributor first
108+distributes such Contribution.
109+
110+2.3. Limitations on Grant Scope
111+
112+The licenses granted in this Section 2 are the only rights granted under
113+this License. No additional rights or licenses will be implied from the
114+distribution or licensing of Covered Software under this License.
115+Notwithstanding Section 2.1(b) above, no patent license is granted by a
116+Contributor:
117+
118+(a) for any code that a Contributor has removed from Covered Software;
119+ or
120+
121+(b) for infringements caused by: (i) Your and any other third party's
122+ modifications of Covered Software, or (ii) the combination of its
123+ Contributions with other software (except as part of its Contributor
124+ Version); or
125+
126+(c) under Patent Claims infringed by Covered Software in the absence of
127+ its Contributions.
128+
129+This License does not grant any rights in the trademarks, service marks,
130+or logos of any Contributor (except as may be necessary to comply with
131+the notice requirements in Section 3.4).
132+
133+2.4. Subsequent Licenses
134+
135+No Contributor makes additional grants as a result of Your choice to
136+distribute the Covered Software under a subsequent version of this
137+License (see Section 10.2) or under the terms of a Secondary License (if
138+permitted under the terms of Section 3.3).
139+
140+2.5. Representation
141+
142+Each Contributor represents that the Contributor believes its
143+Contributions are its original creation(s) or it has sufficient rights
144+to grant the rights to its Contributions conveyed by this License.
145+
146+2.6. Fair Use
147+
148+This License is not intended to limit any rights You have under
149+applicable copyright doctrines of fair use, fair dealing, or other
150+equivalents.
151+
152+2.7. Conditions
153+
154+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155+in Section 2.1.
156+
157+3. Responsibilities
158+-------------------
159+
160+3.1. Distribution of Source Form
161+
162+All distribution of Covered Software in Source Code Form, including any
163+Modifications that You create or to which You contribute, must be under
164+the terms of this License. You must inform recipients that the Source
165+Code Form of the Covered Software is governed by the terms of this
166+License, and how they can obtain a copy of this License. You may not
167+attempt to alter or restrict the recipients' rights in the Source Code
168+Form.
169+
170+3.2. Distribution of Executable Form
171+
172+If You distribute Covered Software in Executable Form then:
173+
174+(a) such Covered Software must also be made available in Source Code
175+ Form, as described in Section 3.1, and You must inform recipients of
176+ the Executable Form how they can obtain a copy of such Source Code
177+ Form by reasonable means in a timely manner, at a charge no more
178+ than the cost of distribution to the recipient; and
179+
180+(b) You may distribute such Executable Form under the terms of this
181+ License, or sublicense it under different terms, provided that the
182+ license for the Executable Form does not attempt to limit or alter
183+ the recipients' rights in the Source Code Form under this License.
184+
185+3.3. Distribution of a Larger Work
186+
187+You may create and distribute a Larger Work under terms of Your choice,
188+provided that You also comply with the requirements of this License for
189+the Covered Software. If the Larger Work is a combination of Covered
190+Software with a work governed by one or more Secondary Licenses, and the
191+Covered Software is not Incompatible With Secondary Licenses, this
192+License permits You to additionally distribute such Covered Software
193+under the terms of such Secondary License(s), so that the recipient of
194+the Larger Work may, at their option, further distribute the Covered
195+Software under the terms of either this License or such Secondary
196+License(s).
197+
198+3.4. Notices
199+
200+You may not remove or alter the substance of any license notices
201+(including copyright notices, patent notices, disclaimers of warranty,
202+or limitations of liability) contained within the Source Code Form of
203+the Covered Software, except that You may alter any license notices to
204+the extent required to remedy known factual inaccuracies.
205+
206+3.5. Application of Additional Terms
207+
208+You may choose to offer, and to charge a fee for, warranty, support,
209+indemnity or liability obligations to one or more recipients of Covered
210+Software. However, You may do so only on Your own behalf, and not on
211+behalf of any Contributor. You must make it absolutely clear that any
212+such warranty, support, indemnity, or liability obligation is offered by
213+You alone, and You hereby agree to indemnify every Contributor for any
214+liability incurred by such Contributor as a result of warranty, support,
215+indemnity or liability terms You offer. You may include additional
216+disclaimers of warranty and limitations of liability specific to any
217+jurisdiction.
218+
219+4. Inability to Comply Due to Statute or Regulation
220+---------------------------------------------------
221+
222+If it is impossible for You to comply with any of the terms of this
223+License with respect to some or all of the Covered Software due to
224+statute, judicial order, or regulation then You must: (a) comply with
225+the terms of this License to the maximum extent possible; and (b)
226+describe the limitations and the code they affect. Such description must
227+be placed in a text file included with all distributions of the Covered
228+Software under this License. Except to the extent prohibited by statute
229+or regulation, such description must be sufficiently detailed for a
230+recipient of ordinary skill to be able to understand it.
231+
232+5. Termination
233+--------------
234+
235+5.1. The rights granted under this License will terminate automatically
236+if You fail to comply with any of its terms. However, if You become
237+compliant, then the rights granted under this License from a particular
238+Contributor are reinstated (a) provisionally, unless and until such
239+Contributor explicitly and finally terminates Your grants, and (b) on an
240+ongoing basis, if such Contributor fails to notify You of the
241+non-compliance by some reasonable means prior to 60 days after You have
242+come back into compliance. Moreover, Your grants from a particular
243+Contributor are reinstated on an ongoing basis if such Contributor
244+notifies You of the non-compliance by some reasonable means, this is the
245+first time You have received notice of non-compliance with this License
246+from such Contributor, and You become compliant prior to 30 days after
247+Your receipt of the notice.
248+
249+5.2. If You initiate litigation against any entity by asserting a patent
250+infringement claim (excluding declaratory judgment actions,
251+counter-claims, and cross-claims) alleging that a Contributor Version
252+directly or indirectly infringes any patent, then the rights granted to
253+You by any and all Contributors for the Covered Software under Section
254+2.1 of this License shall terminate.
255+
256+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257+end user license agreements (excluding distributors and resellers) which
258+have been validly granted by You or Your distributors under this License
259+prior to termination shall survive termination.
260+
261+************************************************************************
262+* *
263+* 6. Disclaimer of Warranty *
264+* ------------------------- *
265+* *
266+* Covered Software is provided under this License on an "as is" *
267+* basis, without warranty of any kind, either expressed, implied, or *
268+* statutory, including, without limitation, warranties that the *
269+* Covered Software is free of defects, merchantable, fit for a *
270+* particular purpose or non-infringing. The entire risk as to the *
271+* quality and performance of the Covered Software is with You. *
272+* Should any Covered Software prove defective in any respect, You *
273+* (not any Contributor) assume the cost of any necessary servicing, *
274+* repair, or correction. This disclaimer of warranty constitutes an *
275+* essential part of this License. No use of any Covered Software is *
276+* authorized under this License except under this disclaimer. *
277+* *
278+************************************************************************
279+
280+************************************************************************
281+* *
282+* 7. Limitation of Liability *
283+* -------------------------- *
284+* *
285+* Under no circumstances and under no legal theory, whether tort *
286+* (including negligence), contract, or otherwise, shall any *
287+* Contributor, or anyone who distributes Covered Software as *
288+* permitted above, be liable to You for any direct, indirect, *
289+* special, incidental, or consequential damages of any character *
290+* including, without limitation, damages for lost profits, loss of *
291+* goodwill, work stoppage, computer failure or malfunction, or any *
292+* and all other commercial damages or losses, even if such party *
293+* shall have been informed of the possibility of such damages. This *
294+* limitation of liability shall not apply to liability for death or *
295+* personal injury resulting from such party's negligence to the *
296+* extent applicable law prohibits such limitation. Some *
297+* jurisdictions do not allow the exclusion or limitation of *
298+* incidental or consequential damages, so this exclusion and *
299+* limitation may not apply to You. *
300+* *
301+************************************************************************
302+
303+8. Litigation
304+-------------
305+
306+Any litigation relating to this License may be brought only in the
307+courts of a jurisdiction where the defendant maintains its principal
308+place of business and such litigation shall be governed by laws of that
309+jurisdiction, without reference to its conflict-of-law provisions.
310+Nothing in this Section shall prevent a party's ability to bring
311+cross-claims or counter-claims.
312+
313+9. Miscellaneous
314+----------------
315+
316+This License represents the complete agreement concerning the subject
317+matter hereof. If any provision of this License is held to be
318+unenforceable, such provision shall be reformed only to the extent
319+necessary to make it enforceable. Any law or regulation which provides
320+that the language of a contract shall be construed against the drafter
321+shall not be used to construe this License against a Contributor.
322+
323+10. Versions of the License
324+---------------------------
325+
326+10.1. New Versions
327+
328+Mozilla Foundation is the license steward. Except as provided in Section
329+10.3, no one other than the license steward has the right to modify or
330+publish new versions of this License. Each version will be given a
331+distinguishing version number.
332+
333+10.2. Effect of New Versions
334+
335+You may distribute the Covered Software under the terms of the version
336+of the License under which You originally received the Covered Software,
337+or under the terms of any subsequent version published by the license
338+steward.
339+
340+10.3. Modified Versions
341+
342+If you create software not governed by this License, and you want to
343+create a new license for such software, you may create and use a
344+modified version of this License if you rename the license and remove
345+any references to the name of the license steward (except to note that
346+such modified license differs from this License).
347+
348+10.4. Distributing Source Code Form that is Incompatible With Secondary
349+Licenses
350+
351+If You choose to distribute Source Code Form that is Incompatible With
352+Secondary Licenses under the terms of this version of the License, the
353+notice described in Exhibit B of this License must be attached.
354+
355+Exhibit A - Source Code Form License Notice
356+-------------------------------------------
357+
358+ This Source Code Form is subject to the terms of the Mozilla Public
359+ License, v. 2.0. If a copy of the MPL was not distributed with this
360+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
361+
362+If it is not possible or desirable to put the notice in a particular
363+file, then You may include the notice in a location (such as a LICENSE
364+file in a relevant directory) where a recipient would be likely to look
365+for such a notice.
366+
367+You may add additional accurate notices of copyright ownership.
368+
369+Exhibit B - "Incompatible With Secondary Licenses" Notice
370+---------------------------------------------------------
371+
372+ This Source Code Form is "Incompatible With Secondary Licenses", as
373+ defined by the Mozilla Public License, v. 2.0.
crates/api/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "api"
3+description = "JSON API served under /api/v1."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/api/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The JSON API surface.
crates/auth/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "auth"
3+description = "Password hashing, sessions, JWTs, and the permission model."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/auth/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Authentication and authorization: argon2id, sessions, JWTs, and `access()`.
crates/cli/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "cli"
3+description = "clap command tree and subcommand implementations."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/cli/src/lib.rs +20 −0
@@ -0,0 +1,20 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The command-line interface for fabrica.
6+//!
7+//! Every subcommand operates directly on the store and filesystem; there is no
8+//! IPC with a running `serve`. [`run`] is the single entry point invoked by the
9+//! `fabrica` binary.
10+
11+use std::process::ExitCode;
12+
13+/// Parse arguments and dispatch to the requested subcommand.
14+///
15+/// Returns the process exit code. The command tree is built out over the
16+/// implementation phases; today this is a placeholder that exits successfully.
17+#[must_use]
18+pub fn run() -> ExitCode {
19+ ExitCode::SUCCESS
20+}
crates/config/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "config"
3+description = "Configuration loading and validation (TOML + environment overrides)."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/config/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Configuration loading, validation, and path resolution.
crates/git/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "git"
3+description = "Git object reads, attribute resolution, signatures, and pack-transport spawning."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/git/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The git layer: in-process object reads via libgit2 and spawned pack transport.
crates/highlight/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "highlight"
3+description = "Syntax highlighting with line-addressable, diff-aware output."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/highlight/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Syntax highlighting producing independently-valid HTML per line.
crates/mail/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "mail"
3+description = "SMTP delivery and message templates."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/mail/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Outbound mail: templates rendered to text and HTML, sent best-effort.
crates/model/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "model"
3+description = "Shared domain types with no I/O."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/model/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Shared domain types. This crate performs no I/O and depends on nothing else in-tree.
crates/ssh/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "ssh"
3+description = "SSH server exposing git pack transport over the exec channel."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/ssh/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The SSH front-end: publickey auth and `exec` dispatch into pack processes.
crates/store/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "store"
3+description = "Database schema, migrations, and queries for SQLite and Postgres."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/store/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Persistence layer: schema, migrations, and portable queries over SQLite and Postgres.
crates/web/Cargo.toml +15 −0
@@ -0,0 +1,15 @@
1+[package]
2+name = "web"
3+description = "Axum router, maud templates, htmx partials, assets, and theming."
4+version.workspace = true
5+edition.workspace = true
6+rust-version.workspace = true
7+license.workspace = true
8+authors.workspace = true
9+repository.workspace = true
10+publish.workspace = true
11+
12+[dependencies]
13+
14+[lints]
15+workspace = true
crates/web/src/lib.rs +5 −0
@@ -0,0 +1,5 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The web UI: server-rendered maud templates enhanced with htmx.
deny.toml +37 −0
@@ -0,0 +1,37 @@
1+# cargo-deny configuration. Run via `nix flake check` (checks.deny) or
2+# `cargo deny check` in the dev shell.
3+
4+[graph]
5+all-features = true
6+
7+[advisories]
8+version = 2
9+yanked = "deny"
10+
11+[licenses]
12+version = 2
13+# Permit the common permissive licenses plus MPL-2.0 (our own).
14+allow = [
15+ "MPL-2.0",
16+ "MIT",
17+ "Apache-2.0",
18+ "Apache-2.0 WITH LLVM-exception",
19+ "BSD-2-Clause",
20+ "BSD-3-Clause",
21+ "ISC",
22+ "Unicode-3.0",
23+ "Zlib",
24+ "CC0-1.0",
25+]
26+confidence-threshold = 0.9
27+
28+[bans]
29+multiple-versions = "warn"
30+wildcards = "deny"
31+# In-tree crates are referenced by path without a version; that is not a real
32+# wildcard dependency.
33+allow-wildcard-paths = true
34+
35+[sources]
36+unknown-registry = "deny"
37+unknown-git = "deny"
docs/decisions.md +87 −0
@@ -0,0 +1,87 @@
1+# Decisions log
2+
3+A running log of design decisions: date, decision, alternatives considered, and
4+rationale. Seeded with the decisions the spec (`spec.md`) already makes. Update
5+this file on any deviation from the spec (a **SHOULD** deviation **MUST** be
6+logged here).
7+
8+---
9+
10+## 2026-07-24 — Pack transport via `git` subprocess
11+
12+**Decision:** Server-side clone/fetch/push is handled by spawning the `git`
13+binary (`git upload-pack` / `git receive-pack`), not by libgit2.
14+
15+**Alternatives:** Implement the smart pack protocol on top of libgit2's object
16+layer; use a pure-Rust reimplementation (`gitoxide`).
17+
18+**Rationale:** libgit2 is client-only for transport — it has no server-side
19+`upload-pack`/`receive-pack`. Spawning `git` is the supported, correct path.
20+libgit2 (via `git2`) is retained for in-process reads (refs, trees, blobs, log,
21+diff, signatures, `init_bare`). Consequence: `git` (≥ 2.41) MUST be on `PATH` at
22+runtime; the Nix derivation wraps the binary with git on PATH.
23+
24+## 2026-07-24 — Repositories stored by id, not name
25+
26+**Decision:** On disk, repos live at `{repo_dir}/{id[0..2]}/{id}.git` where `id`
27+is a lowercase ULID. The database is the only name→path authority.
28+
29+**Alternatives:** Store repos by `{owner}/{name}.git` mirroring the URL.
30+
31+**Rationale:** Renames and group moves become metadata-only (a DB update), never
32+touching the filesystem. Tradeoff: the tree is not human-browsable;
33+`fabrica repo path <user> <name>` resolves it for operators.
34+
35+## 2026-07-24 — `/-/` route separator
36+
37+**Decision:** Web and API routes disambiguate variable-length repo paths from
38+sub-resources with the GitLab `/-/` convention: everything before `/-/` is the
39+repo path, everything after is the view.
40+
41+**Alternatives:** Fixed-depth paths; a query parameter for the view.
42+
43+**Rationale:** Group nesting makes repo paths of unknown depth. `/-/` is an
44+unambiguous, well-understood separator.
45+
46+## 2026-07-24 — Timestamps as epoch milliseconds
47+
48+**Decision:** All timestamps are stored as `BIGINT` unix milliseconds (UTC). No
49+`DATETIME` / `TIMESTAMPTZ`.
50+
51+**Alternatives:** Native date/time column types per dialect.
52+
53+**Rationale:** One portable representation across SQLite and Postgres, ordered by
54+plain integer comparison. Ids are `TEXT` ULIDs (lexicographically sortable) used
55+for tie-breaking and ordering.
56+
57+## 2026-07-24 — `serve` runs in the foreground; no `stop` subcommand
58+
59+**Decision:** `fabrica serve` runs in the foreground by default and is stopped by
60+signalling its pid (`SIGTERM`/`SIGINT`). `--detach` double-forks and writes a pid
61+file. There is no `server start` / `server stop` pair.
62+
63+**Alternatives:** A daemonizing `start`/`stop`/`restart` command set.
64+
65+**Rationale:** Foreground is correct for systemd and Docker, which own process
66+lifecycle. This deviates from an earlier `server start`/`server stop` sketch.
67+
68+## 2026-07-24 — Linear search, no index
69+
70+**Decision:** Search (in-repo and global) is a linear scan over repo contents at
71+request time, with worker-pool, time-budget, and result-count bounds.
72+
73+**Alternatives:** Maintain a `tantivy` full-text index.
74+
75+**Rationale:** Appropriate for a small, private instance. A `tantivy` index is
76+the intended path if linear scan stops being fast enough.
77+
78+## 2026-07-24 — Nix flake uses flake-parts
79+
80+**Decision:** The flake is structured with `flake-parts` (module system,
81+`perSystem`) rather than `flake-utils`.
82+
83+**Alternatives:** `flake-utils.lib.eachDefaultSystem`.
84+
85+**Rationale:** flake-parts scales better as the flake grows (NixOS module,
86+container image, multiple checks) and gives a cleaner multi-output structure. The
87+spec permits either; picked one and stay consistent.
flake.lock +116 −0
@@ -0,0 +1,116 @@
1+{
2+ "nodes": {
3+ "crane": {
4+ "locked": {
5+ "lastModified": 1784564969,
6+ "narHash": "sha256-TkTWNhJV/8Wk3czlbz4bwHZkFCaCHPsOy+oJe4PUiXg=",
7+ "owner": "ipetkov",
8+ "repo": "crane",
9+ "rev": "7930f6c291de6f83c257839d434592aa085f290a",
10+ "type": "github"
11+ },
12+ "original": {
13+ "owner": "ipetkov",
14+ "repo": "crane",
15+ "type": "github"
16+ }
17+ },
18+ "fenix": {
19+ "inputs": {
20+ "nixpkgs": [
21+ "nixpkgs"
22+ ],
23+ "rust-analyzer-src": "rust-analyzer-src"
24+ },
25+ "locked": {
26+ "lastModified": 1784882000,
27+ "narHash": "sha256-uexExdN1itvHYHFek4bpvRqURvxYLKWUetpekP+udbk=",
28+ "owner": "nix-community",
29+ "repo": "fenix",
30+ "rev": "42d2c5f54f90dd11ebd5dc7732f004aa900cfecd",
31+ "type": "github"
32+ },
33+ "original": {
34+ "owner": "nix-community",
35+ "repo": "fenix",
36+ "type": "github"
37+ }
38+ },
39+ "flake-parts": {
40+ "inputs": {
41+ "nixpkgs-lib": "nixpkgs-lib"
42+ },
43+ "locked": {
44+ "lastModified": 1782949081,
45+ "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
46+ "owner": "hercules-ci",
47+ "repo": "flake-parts",
48+ "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
49+ "type": "github"
50+ },
51+ "original": {
52+ "owner": "hercules-ci",
53+ "repo": "flake-parts",
54+ "type": "github"
55+ }
56+ },
57+ "nixpkgs": {
58+ "locked": {
59+ "lastModified": 1784796856,
60+ "narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
61+ "owner": "NixOS",
62+ "repo": "nixpkgs",
63+ "rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
64+ "type": "github"
65+ },
66+ "original": {
67+ "owner": "NixOS",
68+ "ref": "nixos-unstable",
69+ "repo": "nixpkgs",
70+ "type": "github"
71+ }
72+ },
73+ "nixpkgs-lib": {
74+ "locked": {
75+ "lastModified": 1782614948,
76+ "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
77+ "owner": "nix-community",
78+ "repo": "nixpkgs.lib",
79+ "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
80+ "type": "github"
81+ },
82+ "original": {
83+ "owner": "nix-community",
84+ "repo": "nixpkgs.lib",
85+ "type": "github"
86+ }
87+ },
88+ "root": {
89+ "inputs": {
90+ "crane": "crane",
91+ "fenix": "fenix",
92+ "flake-parts": "flake-parts",
93+ "nixpkgs": "nixpkgs"
94+ }
95+ },
96+ "rust-analyzer-src": {
97+ "flake": false,
98+ "locked": {
99+ "lastModified": 1784667699,
100+ "narHash": "sha256-853teJQOgTTKRfNxcIJ53ziJOGNeg3c74PvQl5l61Jc=",
101+ "owner": "rust-lang",
102+ "repo": "rust-analyzer",
103+ "rev": "1174734d9c6453d13d2b5e3d578512c579ca37f1",
104+ "type": "github"
105+ },
106+ "original": {
107+ "owner": "rust-lang",
108+ "ref": "nightly",
109+ "repo": "rust-analyzer",
110+ "type": "github"
111+ }
112+ }
113+ },
114+ "root": "root",
115+ "version": 7
116+}
flake.nix +155 −0
@@ -0,0 +1,155 @@
1+{
2+ description = "fabrica — a self-hosted git server (single binary, HTTPS + SSH, themeable web UI)";
3+
4+ inputs = {
5+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6+ flake-parts.url = "github:hercules-ci/flake-parts";
7+ fenix = {
8+ url = "github:nix-community/fenix";
9+ inputs.nixpkgs.follows = "nixpkgs";
10+ };
11+ crane.url = "github:ipetkov/crane";
12+ };
13+
14+ outputs =
15+ inputs@{ flake-parts, ... }:
16+ flake-parts.lib.mkFlake { inherit inputs; } {
17+ systems = [
18+ "x86_64-linux"
19+ "aarch64-linux"
20+ "x86_64-darwin"
21+ "aarch64-darwin"
22+ ];
23+
24+ perSystem =
25+ { self'
26+ , pkgs
27+ , system
28+ , ...
29+ }:
30+ let
31+ # Nightly toolchain via fenix, pinned through flake.lock. The `complete`
32+ # profile carries clippy, rustfmt, rust-src, and rust-analyzer.
33+ fenixToolchain = inputs.fenix.packages.${system}.complete.toolchain;
34+
35+ craneLib = (inputs.crane.mkLib pkgs).overrideToolchain fenixToolchain;
36+
37+ commonArgs = {
38+ src = craneLib.cleanCargoSource ./.;
39+ strictDeps = true;
40+ nativeBuildInputs = [
41+ pkgs.pkg-config
42+ pkgs.makeWrapper
43+ ];
44+ buildInputs = [
45+ pkgs.libgit2
46+ pkgs.zlib
47+ pkgs.openssl
48+ ];
49+ # Use the system libgit2 rather than the vendored copy.
50+ LIBGIT2_NO_VENDOR = "1";
51+ };
52+
53+ cargoArtifacts = craneLib.buildDepsOnly commonArgs;
54+
55+ fabrica = craneLib.buildPackage (
56+ commonArgs
57+ // {
58+ inherit cargoArtifacts;
59+ doCheck = false; # tests run as their own flake check
60+ # The git plumbing must always be reachable at runtime.
61+ postInstall = ''
62+ wrapProgram $out/bin/fabrica \
63+ --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.git ]}
64+ '';
65+ }
66+ );
67+ in
68+ {
69+ packages = {
70+ default = fabrica;
71+ fabrica = fabrica;
72+
73+ container = pkgs.dockerTools.buildLayeredImage {
74+ name = "fabrica";
75+ tag = "latest";
76+ contents = [
77+ fabrica
78+ pkgs.git
79+ pkgs.cacert
80+ ];
81+ config = {
82+ Entrypoint = [ "${fabrica}/bin/fabrica" ];
83+ Cmd = [ "serve" ];
84+ ExposedPorts = {
85+ "8080/tcp" = { };
86+ "2222/tcp" = { };
87+ };
88+ Volumes = {
89+ "/var/lib/fabrica" = { };
90+ };
91+ };
92+ };
93+ };
94+
95+ checks = {
96+ inherit fabrica;
97+
98+ clippy = craneLib.cargoClippy (
99+ commonArgs
100+ // {
101+ inherit cargoArtifacts;
102+ cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
103+ }
104+ );
105+
106+ fmt = craneLib.cargoFmt {
107+ src = craneLib.cleanCargoSource ./.;
108+ };
109+
110+ test = craneLib.cargoNextest (
111+ commonArgs
112+ // {
113+ inherit cargoArtifacts;
114+ partitions = 1;
115+ partitionType = "count";
116+ # Crates have no tests during early scaffolding; do not treat an
117+ # empty test set as a failure.
118+ cargoNextestExtraArgs = "--no-tests=pass";
119+ }
120+ );
121+
122+ deny = craneLib.cargoDeny {
123+ src = craneLib.cleanCargoSource ./.;
124+ };
125+
126+ doc = craneLib.cargoDoc (
127+ commonArgs
128+ // {
129+ inherit cargoArtifacts;
130+ }
131+ );
132+ };
133+
134+ devShells.default = craneLib.devShell {
135+ checks = self'.checks;
136+ packages = [
137+ pkgs.sqlx-cli
138+ pkgs.sqlite
139+ pkgs.postgresql
140+ pkgs.git
141+ pkgs.openssh
142+ pkgs.gnupg
143+ pkgs.cargo-nextest
144+ pkgs.cargo-deny
145+ pkgs.just
146+ pkgs.pkg-config
147+ ];
148+ buildInputs = commonArgs.buildInputs;
149+ LIBGIT2_NO_VENDOR = "1";
150+ };
151+
152+ formatter = pkgs.nixpkgs-fmt;
153+ };
154+ };
155+}
justfile +33 −0
@@ -0,0 +1,33 @@
1+# fabrica task runner. `just check` is the quality gate that MUST pass after
2+# every unit of work. `nix flake check` is the superset.
3+
4+# List available recipes.
5+default:
6+ @just --list
7+
8+# The quality gate: format, check, lint, test. Run after every unit of work.
9+check: fmt-check build clippy test
10+
11+# Verify formatting without modifying files.
12+fmt-check:
13+ cargo fmt --all -- --check
14+
15+# Format the workspace in place.
16+fmt:
17+ cargo fmt --all
18+
19+# Type-check the whole workspace, including tests and examples.
20+build:
21+ cargo check --workspace --all-targets
22+
23+# Lint with clippy, denying all warnings.
24+clippy:
25+ cargo clippy --workspace --all-targets -- -D warnings
26+
27+# Run the test suite.
28+test:
29+ cargo test --workspace
30+
31+# Run the full Nix gate (checks.{clippy,fmt,test,deny,doc}).
32+flake-check:
33+ nix flake check
rust-toolchain.toml +6 −0
@@ -0,0 +1,6 @@
1+# Nightly is a currency preference only: no `#![feature(...)]` gates, code must
2+# compile on stable. This file is for non-Nix users; Nix pins nightly via fenix
3+# through flake.lock.
4+[toolchain]
5+channel = "nightly"
6+components = ["rustfmt", "clippy", "rust-src", "rust-analyzer"]
src/main.rs +14 −0
@@ -0,0 +1,14 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! `fabrica` — a self-hosted git server.
6+//!
7+//! The binary is deliberately thin: it parses arguments and dispatches into the
8+//! [`cli`] crate, and nothing else lives here.
9+
10+use std::process::ExitCode;
11+
12+fn main() -> ExitCode {
13+ cli::run()
14+}