anvilsign in

collin/anvil

1//! On-disk repository storage.
2//!
3//! Repositories are bare git repositories laid out as
4//! `<repositories_dir>/<owner>/<name>.git`. This module owns the mapping
5//! between forge identities and filesystem paths, plus creation/opening via
6//! `gix`. No database access happens here.
7
8use std::path::{
9 Path,
10 PathBuf,
11};
12
13use crate::error::{
14 Error,
15 Result,
16};
17
18/// Compute the on-disk path of a bare repository.
19pub fn repo_path(repositories_dir: &Path, owner: &str, name: &str) -> PathBuf {
20 repositories_dir.join(owner).join(format!("{name}.git"))
21}
22
23/// Directory holding one commit's CI artifacts
24/// (`<artifacts_dir>/<repo_id>/<commit>` — see `docs/ci-artifacts.md`).
25pub fn artifact_commit_dir(artifacts_dir: &Path, repo_id: i64, commit: &str) -> PathBuf {
26 artifacts_dir.join(repo_id.to_string()).join(commit)
27}
28
29/// Create a new bare repository on disk, returning the opened handle.
30///
31/// `HEAD` is pointed at `refs/heads/<default_branch>` so the on-disk repository
32/// agrees with the forge's recorded default branch (gix otherwise defaults to
33/// whatever `init.defaultBranch` resolves to, often `master`).
34///
35/// Fails if a repository already exists at the target path. The owner
36/// directory is created as needed.
37pub fn create_bare(
38 repositories_dir: &Path,
39 owner: &str,
40 name: &str,
41 default_branch: &str,
42) -> Result<gix::Repository> {
43 let path = repo_path(repositories_dir, owner, name);
44 if path.exists() {
45 return Err(Error::AlreadyExists(format!(
46 "repository on disk at {}",
47 path.display()
48 )));
49 }
50 if let Some(parent) = path.parent() {
51 std::fs::create_dir_all(parent)?;
52 }
53 let repo = gix::init_bare(&path)
54 .map_err(|e| Error::Storage(format!("init bare {}: {e}", path.display())))?;
55 set_head_branch(&repo, default_branch)?;
56 Ok(repo)
57}
58
59/// Point `HEAD` at `refs/heads/<branch>` as a symbolic reference.
60fn set_head_branch(repo: &gix::Repository, branch: &str) -> Result<()> {
61 use gix::refs::{
62 Target,
63 transaction::{
64 Change,
65 LogChange,
66 PreviousValue,
67 RefEdit,
68 },
69 };
70
71 let target_name: gix::refs::FullName = format!("refs/heads/{branch}")
72 .try_into()
73 .map_err(|e| Error::Storage(format!("invalid branch name {branch:?}: {e}")))?;
74 let head_name: gix::refs::FullName = "HEAD"
75 .try_into()
76 .map_err(|e| Error::Storage(format!("HEAD ref name: {e}")))?;
77
78 repo.edit_reference(RefEdit {
79 change: Change::Update {
80 log: LogChange::default(),
81 expected: PreviousValue::Any,
82 new: Target::Symbolic(target_name),
83 },
84 name: head_name,
85 deref: false,
86 })
87 .map_err(|e| Error::Storage(format!("set HEAD to {branch}: {e}")))?;
88 Ok(())
89}
90
91/// Open an existing bare repository.
92pub fn open(repositories_dir: &Path, owner: &str, name: &str) -> Result<gix::Repository> {
93 let path = repo_path(repositories_dir, owner, name);
94 gix::open(&path).map_err(|e| Error::Storage(format!("open {}: {e}", path.display())))
95}