anvilsign in

collin/anvil

1//! Repository records: creation (DB row + on-disk bare repo) and lookup.
2
3use std::path::Path;
4
5use crate::error::{Error, Result};
6use crate::models::{Repository, User};
7use crate::storage;
8
9/// A repository joined with its owner's username, for listing pages.
10pub struct RepoWithOwner {
11 pub owner_id: i64,
12 pub owner: String,
13 pub name: String,
14 pub description: String,
15 pub is_private: bool,
16 pub default_branch: String,
17}
18
19/// Create a repository owned by `owner`: insert the database row and initialize
20/// the bare repository on disk. If the on-disk init fails, the row is rolled
21/// back (deleted) so no orphan remains.
22pub async fn create(
23 db: &toasty::Db,
24 repositories_dir: &Path,
25 owner: &User,
26 name: &str,
27 description: &str,
28 is_private: bool,
29) -> Result<Repository> {
30 validate_name(name)?;
31 if find(db, owner.id, name).await?.is_some() {
32 return Err(Error::AlreadyExists(format!(
33 "repository {}/{name}",
34 owner.username
35 )));
36 }
37
38 let mut conn = db.clone();
39 let repo = toasty::create!(Repository {
40 owner_id: owner.id,
41 name: name,
42 description: description,
43 is_private: is_private,
44 default_branch: "main",
45 created_at: crate::now(),
46 })
47 .exec(&mut conn)
48 .await?;
49
50 // Create the bare repo on disk; on failure, remove the row we just wrote.
51 if let Err(e) = storage::create_bare(
52 repositories_dir,
53 &owner.username,
54 name,
55 &repo.default_branch,
56 ) {
57 let _ = repo.delete().exec(&mut conn).await;
58 return Err(e);
59 }
60 Ok(repo)
61}
62
63/// Find a repository by its id.
64pub async fn find_by_id(db: &toasty::Db, id: i64) -> Result<Option<Repository>> {
65 let mut db = db.clone();
66 let repo = Repository::filter(Repository::fields().id().eq(id))
67 .first()
68 .exec(&mut db)
69 .await?;
70 Ok(repo)
71}
72
73/// Update a repository's description and visibility.
74pub async fn update_settings(
75 db: &toasty::Db,
76 repo_id: i64,
77 description: &str,
78 is_private: bool,
79) -> Result<()> {
80 let mut conn = db.clone();
81 let Some(mut repo) = Repository::filter(Repository::fields().id().eq(repo_id))
82 .first()
83 .exec(&mut conn)
84 .await?
85 else {
86 return Err(Error::NotFound(format!("repository id {repo_id}")));
87 };
88 let mut conn = db.clone();
89 repo.update()
90 .description(description)
91 .is_private(is_private)
92 .exec(&mut conn)
93 .await?;
94 Ok(())
95}
96
97/// Find a repository by owner id and name.
98pub async fn find(db: &toasty::Db, owner_id: i64, name: &str) -> Result<Option<Repository>> {
99 let mut db = db.clone();
100 let repo = Repository::filter(Repository::fields().owner_id().eq(owner_id))
101 .filter(Repository::fields().name().eq(name))
102 .first()
103 .exec(&mut db)
104 .await?;
105 Ok(repo)
106}
107
108/// List all repositories owned by `owner_id`.
109pub async fn list_by_owner(db: &toasty::Db, owner_id: i64) -> Result<Vec<Repository>> {
110 let mut db = db.clone();
111 let repos = Repository::filter(Repository::fields().owner_id().eq(owner_id))
112 .exec(&mut db)
113 .await?;
114 Ok(repos)
115}
116
117/// List all repositories with their owner usernames, ordered by owner then name.
118pub async fn list_all_with_owner(db: &toasty::Db) -> Result<Vec<RepoWithOwner>> {
119 let mut conn = db.clone();
120 let users = User::all().exec(&mut conn).await?;
121 let owner_name: std::collections::HashMap<i64, String> =
122 users.into_iter().map(|u| (u.id, u.username)).collect();
123
124 let mut conn = db.clone();
125 let repos = Repository::all().exec(&mut conn).await?;
126 let mut out: Vec<RepoWithOwner> = repos
127 .into_iter()
128 .map(|r| RepoWithOwner {
129 owner: owner_name.get(&r.owner_id).cloned().unwrap_or_default(),
130 owner_id: r.owner_id,
131 name: r.name,
132 description: r.description,
133 is_private: r.is_private,
134 default_branch: r.default_branch,
135 })
136 .collect();
137 out.sort_by(|a, b| (&a.owner, &a.name).cmp(&(&b.owner, &b.name)));
138 Ok(out)
139}
140
141/// Reject names that are empty or contain path separators / traversal.
142fn validate_name(name: &str) -> Result<()> {
143 if name.is_empty() {
144 return Err(Error::Invalid("repository name must not be empty".into()));
145 }
146 if name.contains('/') || name.contains('\\') || name.contains("..") {
147 return Err(Error::Invalid(format!("invalid repository name: {name:?}")));
148 }
149 Ok(())
150}