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