127 lines · 4133 bytes
1 use chrono::Utc;
2 use sqlx::{SqlitePool, Transaction, Sqlite};
3 use std::{fs, process::Command};
4
5 use crate::validate::validate_repo_name;
6
7 pub async fn repo_count(pool: &SqlitePool, owner_id: i64) -> Result<i64, sqlx::Error> {
8 let r = sqlx::query!("SELECT COUNT(*) as \"c!: i64\" FROM repos WHERE owner_id = ?", owner_id)
9 .fetch_one(pool).await?;
10 Ok(r.c)
11 }
12
13 pub async fn repo_exists(pool: &SqlitePool, owner: &str, repo: &str) -> bool {
14 sqlx::query!(
15 r#"SELECT repos.id as "id!: i64"
16 FROM repos
17 JOIN users ON users.id = repos.owner_id
18 WHERE users.username = ? AND repos.name = ?"#,
19 owner, repo
20 ).fetch_optional(pool).await.ok().flatten().is_some()
21 }
22
23 pub async fn create_repo(
24 pool: &SqlitePool,
25 repos_root: &str,
26 hook_path: &str,
27 max_repos_per_user: i64,
28 owner_id: i64,
29 owner_username: &str,
30 repo_name: &str,
31 ) -> Result<(), String> {
32 if !validate_repo_name(repo_name) { return Err("invalid_repo_name".into()); }
33
34 let count = repo_count(pool, owner_id).await.map_err(|_| "db_error".to_string())?;
35 if count >= max_repos_per_user {
36 return Err("repo_quota_exceeded".into());
37 }
38
39 let mut tx: Transaction<'_, Sqlite> = pool.begin().await.map_err(|_| "db_error".to_string())?;
40 let now = Utc::now().to_rfc3339();
41
42 let ins = sqlx::query!(
43 "INSERT INTO repos(owner_id, name, created_at) VALUES(?, ?, ?)",
44 owner_id, repo_name, now
45 ).execute(&mut *tx).await;
46
47 if ins.is_err() {
48 tx.rollback().await.ok();
49 return Err("repo_exists".into());
50 }
51
52 let owner_dir = std::path::Path::new(repos_root).join(owner_username);
53 fs::create_dir_all(&owner_dir).map_err(|_| "fs_error".to_string())?;
54
55 let repo_dir = owner_dir.join(format!("{repo_name}.git"));
56 fs::create_dir(&repo_dir).map_err(|_| "fs_error".to_string())?;
57
58 let st = Command::new("git")
59 .arg("init").arg("--bare")
60 .arg("--initial-branch=main")
61 .arg(&repo_dir)
62 .status()
63 .map_err(|_| "git_init_failed".to_string())?;
64
65 if !st.success() {
66 tx.rollback().await.ok();
67 let _ = fs::remove_dir_all(&repo_dir);
68 return Err("git_init_failed".into());
69 }
70
71 // Enable HTTP push
72 let _ = Command::new("git")
73 .arg("-C").arg(&repo_dir)
74 .arg("config").arg("http.receivepack").arg("true")
75 .status();
76
77 let hooks_dir = repo_dir.join("hooks");
78 let _ = fs::create_dir_all(&hooks_dir);
79
80 let pre_receive = hooks_dir.join("pre-receive");
81 #[cfg(unix)]
82 {
83 use std::os::unix::fs::symlink;
84 let _ = fs::remove_file(&pre_receive);
85 symlink(hook_path, &pre_receive).map_err(|_| "hook_install_failed".to_string())?;
86 }
87 #[cfg(not(unix))]
88 {
89 fs::copy(hook_path, &pre_receive).map_err(|_| "hook_install_failed".to_string())?;
90 }
91
92 tx.commit().await.map_err(|_| "db_error".to_string())?;
93 Ok(())
94 }
95
96 pub fn head_hash(repos_root: &str, owner: &str, repo: &str) -> Option<String> {
97 let repo_dir = std::path::Path::new(repos_root).join(owner).join(format!("{repo}.git"));
98 let output = Command::new("git")
99 .arg("-C").arg(&repo_dir)
100 .arg("rev-parse").arg("HEAD")
101 .output()
102 .ok()?;
103 if output.status.success() {
104 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
105 } else {
106 None
107 }
108 }
109
110 pub async fn delete_repo(pool: &SqlitePool, repos_root: &str, owner: &str, repo: &str) -> Result<(), String> {
111 let res = sqlx::query!(
112 r#"DELETE FROM repos
113 WHERE id IN (
114 SELECT repos.id
115 FROM repos
116 JOIN users ON users.id = repos.owner_id
117 WHERE users.username = ? AND repos.name = ?
118 )"#,
119 owner, repo
120 ).execute(pool).await.map_err(|_| "db_error".to_string())?;
121
122 if res.rows_affected() == 0 { return Err("not_found".into()); }
123
124 let repo_dir = std::path::Path::new(repos_root).join(owner).join(format!("{repo}.git"));
125 std::fs::remove_dir_all(repo_dir).map_err(|_| "fs_error".to_string())?;
126 Ok(())
127 }