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