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::{Path, PathBuf};
9
10use crate::error::{Error, Result};
11
12/// Compute the on-disk path of a bare repository.
13pub fn repo_path(repositories_dir: &Path, owner: &str, name: &str) -> PathBuf {
14 repositories_dir.join(owner).join(format!("{name}.git"))
15}
16
17/// Create a new bare repository on disk, returning the opened handle.
18///
19/// `HEAD` is pointed at `refs/heads/<default_branch>` so the on-disk repository
20/// agrees with the forge's recorded default branch (gix otherwise defaults to
21/// whatever `init.defaultBranch` resolves to, often `master`).
22///
23/// Fails if a repository already exists at the target path. The owner
24/// directory is created as needed.
25pub fn create_bare(
26 repositories_dir: &Path,
27 owner: &str,
28 name: &str,
29 default_branch: &str,
30) -> Result<gix::Repository> {
31 let path = repo_path(repositories_dir, owner, name);
32 if path.exists() {
33 return Err(Error::AlreadyExists(format!(
34 "repository on disk at {}",
35 path.display()
36 )));
37 }
38 if let Some(parent) = path.parent() {
39 std::fs::create_dir_all(parent)?;
40 }
41 let repo = gix::init_bare(&path)
42 .map_err(|e| Error::Storage(format!("init bare {}: {e}", path.display())))?;
43 set_head_branch(&repo, default_branch)?;
44 Ok(repo)
45}
46
47/// Point `HEAD` at `refs/heads/<branch>` as a symbolic reference.
48fn set_head_branch(repo: &gix::Repository, branch: &str) -> Result<()> {
49 use gix::refs::Target;
50 use gix::refs::transaction::{Change, LogChange, PreviousValue, RefEdit};
51
52 let target_name: gix::refs::FullName = format!("refs/heads/{branch}")
53 .try_into()
54 .map_err(|e| Error::Storage(format!("invalid branch name {branch:?}: {e}")))?;
55 let head_name: gix::refs::FullName = "HEAD"
56 .try_into()
57 .map_err(|e| Error::Storage(format!("HEAD ref name: {e}")))?;
58
59 repo.edit_reference(RefEdit {
60 change: Change::Update {
61 log: LogChange::default(),
62 expected: PreviousValue::Any,
63 new: Target::Symbolic(target_name),
64 },
65 name: head_name,
66 deref: false,
67 })
68 .map_err(|e| Error::Storage(format!("set HEAD to {branch}: {e}")))?;
69 Ok(())
70}
71
72/// Open an existing bare repository.
73pub fn open(repositories_dir: &Path, owner: &str, name: &str) -> Result<gix::Repository> {
74 let path = repo_path(repositories_dir, owner, name);
75 gix::open(&path).map_err(|e| Error::Storage(format!("open {}: {e}", path.display())))
76}