db.rs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  1. use std::{
  2. path::Path,
  3. sync::{Arc, Mutex},
  4. time::{SystemTime, UNIX_EPOCH},
  5. };
  6. use rusqlite::{Connection, OptionalExtension, Transaction, params};
  7. use crate::{
  8. error::{AppError, AppResult},
  9. models::{
  10. AccessMode, AccessToken, Collaboration, CollaboratorResponse, PullRequest,
  11. PullRequestStatus, Repository, RepositoryWithOwner, User,
  12. },
  13. };
  14. #[derive(Clone)]
  15. pub struct Database {
  16. conn: Arc<Mutex<Connection>>,
  17. }
  18. impl Database {
  19. pub fn open(path: &Path) -> AppResult<Self> {
  20. let conn = Connection::open(path)?;
  21. conn.execute_batch(
  22. r#"
  23. PRAGMA journal_mode = WAL;
  24. PRAGMA foreign_keys = ON;
  25. PRAGMA synchronous = NORMAL;
  26. PRAGMA temp_store = MEMORY;
  27. "#,
  28. )?;
  29. Ok(Self {
  30. conn: Arc::new(Mutex::new(conn)),
  31. })
  32. }
  33. pub fn init_schema(&self) -> AppResult<()> {
  34. let conn = self.lock()?;
  35. conn.execute_batch(
  36. r#"
  37. CREATE TABLE IF NOT EXISTS user (
  38. id INTEGER PRIMARY KEY AUTOINCREMENT,
  39. lower_name TEXT NOT NULL UNIQUE,
  40. name TEXT NOT NULL UNIQUE,
  41. full_name TEXT NOT NULL DEFAULT '',
  42. email TEXT NOT NULL UNIQUE,
  43. password_hash TEXT NOT NULL,
  44. is_active INTEGER NOT NULL DEFAULT 1,
  45. is_admin INTEGER NOT NULL DEFAULT 0,
  46. created_unix INTEGER NOT NULL,
  47. updated_unix INTEGER NOT NULL
  48. );
  49. CREATE TABLE IF NOT EXISTS repository (
  50. id INTEGER PRIMARY KEY AUTOINCREMENT,
  51. owner_id INTEGER NOT NULL,
  52. lower_name TEXT NOT NULL,
  53. name TEXT NOT NULL,
  54. description TEXT NOT NULL DEFAULT '',
  55. default_branch TEXT NOT NULL,
  56. is_private INTEGER NOT NULL DEFAULT 0,
  57. is_bare INTEGER NOT NULL DEFAULT 1,
  58. is_fork INTEGER NOT NULL DEFAULT 0,
  59. fork_id INTEGER NOT NULL DEFAULT 0,
  60. created_unix INTEGER NOT NULL,
  61. updated_unix INTEGER NOT NULL,
  62. UNIQUE(owner_id, lower_name),
  63. FOREIGN KEY(owner_id) REFERENCES user(id) ON DELETE CASCADE
  64. );
  65. CREATE TABLE IF NOT EXISTS access_token (
  66. id INTEGER PRIMARY KEY AUTOINCREMENT,
  67. user_id INTEGER NOT NULL,
  68. name TEXT NOT NULL,
  69. token_hash TEXT NOT NULL UNIQUE,
  70. created_unix INTEGER NOT NULL,
  71. updated_unix INTEGER NOT NULL DEFAULT 0,
  72. FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
  73. );
  74. CREATE UNIQUE INDEX IF NOT EXISTS idx_access_token_user_name
  75. ON access_token (user_id, name);
  76. CREATE TABLE IF NOT EXISTS access (
  77. id INTEGER PRIMARY KEY AUTOINCREMENT,
  78. user_id INTEGER NOT NULL,
  79. repo_id INTEGER NOT NULL,
  80. mode INTEGER NOT NULL,
  81. UNIQUE(user_id, repo_id),
  82. FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
  83. FOREIGN KEY(repo_id) REFERENCES repository(id) ON DELETE CASCADE
  84. );
  85. CREATE TABLE IF NOT EXISTS collaboration (
  86. id INTEGER PRIMARY KEY AUTOINCREMENT,
  87. user_id INTEGER NOT NULL,
  88. repo_id INTEGER NOT NULL,
  89. mode INTEGER NOT NULL,
  90. UNIQUE(user_id, repo_id),
  91. FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
  92. FOREIGN KEY(repo_id) REFERENCES repository(id) ON DELETE CASCADE
  93. );
  94. CREATE TABLE IF NOT EXISTS pull_request (
  95. id INTEGER PRIMARY KEY AUTOINCREMENT,
  96. index_in_repo INTEGER NOT NULL,
  97. title TEXT NOT NULL,
  98. body TEXT NOT NULL DEFAULT '',
  99. status INTEGER NOT NULL,
  100. head_repo_id INTEGER NOT NULL,
  101. base_repo_id INTEGER NOT NULL,
  102. head_user_name TEXT NOT NULL,
  103. head_branch TEXT NOT NULL,
  104. base_branch TEXT NOT NULL,
  105. merge_base TEXT NOT NULL,
  106. merged_commit_id TEXT NOT NULL DEFAULT '',
  107. poster_id INTEGER NOT NULL,
  108. has_merged INTEGER NOT NULL DEFAULT 0,
  109. is_closed INTEGER NOT NULL DEFAULT 0,
  110. created_unix INTEGER NOT NULL,
  111. updated_unix INTEGER NOT NULL,
  112. FOREIGN KEY(head_repo_id) REFERENCES repository(id) ON DELETE CASCADE,
  113. FOREIGN KEY(base_repo_id) REFERENCES repository(id) ON DELETE CASCADE,
  114. FOREIGN KEY(poster_id) REFERENCES user(id) ON DELETE CASCADE
  115. );
  116. CREATE UNIQUE INDEX IF NOT EXISTS idx_pull_request_base_repo_index
  117. ON pull_request (base_repo_id, index_in_repo);
  118. "#,
  119. )?;
  120. ensure_column_exists(
  121. &conn,
  122. "pull_request",
  123. "merged_commit_id",
  124. "TEXT NOT NULL DEFAULT ''",
  125. )?;
  126. ensure_column_exists(
  127. &conn,
  128. "access_token",
  129. "updated_unix",
  130. "INTEGER NOT NULL DEFAULT 0",
  131. )?;
  132. Ok(())
  133. }
  134. pub fn create_user(&self, new_user: NewUser<'_>) -> AppResult<User> {
  135. let conn = self.lock()?;
  136. let tx = conn.unchecked_transaction()?;
  137. let lower_name = new_user.username.to_ascii_lowercase();
  138. let email = new_user.email.trim().to_ascii_lowercase();
  139. if self.user_exists_by_lower_name(&tx, &lower_name)? {
  140. return Err(AppError::Conflict(format!(
  141. "user already exists: {}",
  142. new_user.username
  143. )));
  144. }
  145. if self.user_exists_by_email(&tx, &email)? {
  146. return Err(AppError::Conflict(format!("email already used: {email}")));
  147. }
  148. let now = now_unix();
  149. tx.execute(
  150. r#"
  151. INSERT INTO user (
  152. lower_name, name, full_name, email, password_hash,
  153. is_active, is_admin, created_unix, updated_unix
  154. ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
  155. "#,
  156. params![
  157. lower_name,
  158. new_user.username,
  159. new_user.full_name,
  160. email,
  161. new_user.password_hash,
  162. new_user.is_active,
  163. new_user.is_admin,
  164. now,
  165. now
  166. ],
  167. )?;
  168. let id = tx.last_insert_rowid();
  169. tx.commit()?;
  170. drop(conn);
  171. self.get_user_by_id(id)?
  172. .ok_or_else(|| AppError::NotFound(format!("user disappeared after create: {id}")))
  173. }
  174. pub fn user_count(&self) -> AppResult<i64> {
  175. let conn = self.lock()?;
  176. conn.query_row("SELECT COUNT(*) FROM user", [], |row| row.get::<_, i64>(0))
  177. .map_err(Into::into)
  178. }
  179. pub fn get_user_by_id(&self, id: i64) -> AppResult<Option<User>> {
  180. let conn = self.lock()?;
  181. let mut stmt = conn.prepare(
  182. r#"
  183. SELECT id, lower_name, name, full_name, email, password_hash,
  184. is_active, is_admin, created_unix, updated_unix
  185. FROM user WHERE id = ?1
  186. "#,
  187. )?;
  188. stmt.query_row(params![id], row_to_user)
  189. .optional()
  190. .map_err(Into::into)
  191. }
  192. pub fn get_user_by_username(&self, username: &str) -> AppResult<Option<User>> {
  193. let conn = self.lock()?;
  194. let mut stmt = conn.prepare(
  195. r#"
  196. SELECT id, lower_name, name, full_name, email, password_hash,
  197. is_active, is_admin, created_unix, updated_unix
  198. FROM user WHERE lower_name = ?1
  199. "#,
  200. )?;
  201. stmt.query_row(params![username.to_ascii_lowercase()], row_to_user)
  202. .optional()
  203. .map_err(Into::into)
  204. }
  205. pub fn get_user_by_email(&self, email: &str) -> AppResult<Option<User>> {
  206. let conn = self.lock()?;
  207. let mut stmt = conn.prepare(
  208. r#"
  209. SELECT id, lower_name, name, full_name, email, password_hash,
  210. is_active, is_admin, created_unix, updated_unix
  211. FROM user WHERE email = ?1
  212. "#,
  213. )?;
  214. stmt.query_row(params![email.trim().to_ascii_lowercase()], row_to_user)
  215. .optional()
  216. .map_err(Into::into)
  217. }
  218. pub fn create_repository(&self, new_repo: NewRepository<'_>) -> AppResult<Repository> {
  219. let conn = self.lock()?;
  220. let tx = conn.unchecked_transaction()?;
  221. let lower_name = new_repo.name.to_ascii_lowercase();
  222. if self.repo_exists_by_name(&tx, new_repo.owner_id, &lower_name)? {
  223. return Err(AppError::Conflict(format!(
  224. "repository already exists: {}/{}",
  225. new_repo.owner_name, new_repo.name
  226. )));
  227. }
  228. let now = now_unix();
  229. tx.execute(
  230. r#"
  231. INSERT INTO repository (
  232. owner_id, lower_name, name, description, default_branch,
  233. is_private, is_bare, is_fork, fork_id, created_unix, updated_unix
  234. ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
  235. "#,
  236. params![
  237. new_repo.owner_id,
  238. lower_name,
  239. new_repo.name,
  240. new_repo.description,
  241. new_repo.default_branch,
  242. new_repo.is_private,
  243. new_repo.is_bare,
  244. new_repo.is_fork,
  245. new_repo.fork_id,
  246. now,
  247. now
  248. ],
  249. )?;
  250. let id = tx.last_insert_rowid();
  251. tx.commit()?;
  252. drop(conn);
  253. self.get_repository_by_id(id)?
  254. .ok_or_else(|| AppError::NotFound(format!("repository disappeared after create: {id}")))
  255. }
  256. pub fn get_repository_by_id(&self, id: i64) -> AppResult<Option<Repository>> {
  257. let conn = self.lock()?;
  258. let mut stmt = conn.prepare(
  259. r#"
  260. SELECT id, owner_id, lower_name, name, description, default_branch,
  261. is_private, is_bare, is_fork, fork_id, created_unix, updated_unix
  262. FROM repository WHERE id = ?1
  263. "#,
  264. )?;
  265. stmt.query_row(params![id], row_to_repo)
  266. .optional()
  267. .map_err(Into::into)
  268. }
  269. pub fn get_repository_by_name(
  270. &self,
  271. owner_id: i64,
  272. name: &str,
  273. ) -> AppResult<Option<Repository>> {
  274. let conn = self.lock()?;
  275. let mut stmt = conn.prepare(
  276. r#"
  277. SELECT id, owner_id, lower_name, name, description, default_branch,
  278. is_private, is_bare, is_fork, fork_id, created_unix, updated_unix
  279. FROM repository
  280. WHERE owner_id = ?1 AND lower_name = ?2
  281. "#,
  282. )?;
  283. stmt.query_row(params![owner_id, name.to_ascii_lowercase()], row_to_repo)
  284. .optional()
  285. .map_err(Into::into)
  286. }
  287. pub fn list_repositories_with_owners(&self) -> AppResult<Vec<RepositoryWithOwner>> {
  288. let conn = self.lock()?;
  289. let mut stmt = conn.prepare(
  290. r#"
  291. SELECT
  292. r.id, r.owner_id, r.lower_name, r.name, r.description, r.default_branch,
  293. r.is_private, r.is_bare, r.is_fork, r.fork_id, r.created_unix, r.updated_unix,
  294. u.id, u.lower_name, u.name, u.full_name, u.email, u.password_hash,
  295. u.is_active, u.is_admin, u.created_unix, u.updated_unix
  296. FROM repository r
  297. JOIN user u ON u.id = r.owner_id
  298. ORDER BY u.lower_name ASC, r.lower_name ASC
  299. "#,
  300. )?;
  301. let rows = stmt.query_map([], row_to_repository_with_owner)?;
  302. let mut repos = Vec::new();
  303. for row in rows {
  304. repos.push(row?);
  305. }
  306. Ok(repos)
  307. }
  308. pub fn list_repositories_with_owners_by_owner(
  309. &self,
  310. owner_id: i64,
  311. ) -> AppResult<Vec<RepositoryWithOwner>> {
  312. let conn = self.lock()?;
  313. let mut stmt = conn.prepare(
  314. r#"
  315. SELECT
  316. r.id, r.owner_id, r.lower_name, r.name, r.description, r.default_branch,
  317. r.is_private, r.is_bare, r.is_fork, r.fork_id, r.created_unix, r.updated_unix,
  318. u.id, u.lower_name, u.name, u.full_name, u.email, u.password_hash,
  319. u.is_active, u.is_admin, u.created_unix, u.updated_unix
  320. FROM repository r
  321. JOIN user u ON u.id = r.owner_id
  322. WHERE r.owner_id = ?1
  323. ORDER BY r.lower_name ASC
  324. "#,
  325. )?;
  326. let rows = stmt.query_map(params![owner_id], row_to_repository_with_owner)?;
  327. let mut repos = Vec::new();
  328. for row in rows {
  329. repos.push(row?);
  330. }
  331. Ok(repos)
  332. }
  333. pub fn has_forked_by(&self, repo_id: i64, user_id: i64) -> AppResult<bool> {
  334. let conn = self.lock()?;
  335. let mut stmt =
  336. conn.prepare("SELECT 1 FROM repository WHERE owner_id = ?1 AND fork_id = ?2 LIMIT 1")?;
  337. let found = stmt
  338. .query_row(params![user_id, repo_id], |row| row.get::<_, i64>(0))
  339. .optional()?;
  340. Ok(found.is_some())
  341. }
  342. pub fn delete_repository_by_id(&self, id: i64) -> AppResult<()> {
  343. let conn = self.lock()?;
  344. conn.execute("DELETE FROM repository WHERE id = ?1", params![id])?;
  345. Ok(())
  346. }
  347. pub fn access_mode(
  348. &self,
  349. user_id: i64,
  350. repo_id: i64,
  351. owner_id: i64,
  352. private: bool,
  353. ) -> AppResult<AccessMode> {
  354. if repo_id <= 0 {
  355. return Ok(AccessMode::None);
  356. }
  357. let mut mode = if private {
  358. AccessMode::None
  359. } else {
  360. AccessMode::Read
  361. };
  362. if user_id <= 0 {
  363. return Ok(mode);
  364. }
  365. if user_id == owner_id {
  366. return Ok(AccessMode::Owner);
  367. }
  368. let conn = self.lock()?;
  369. let mut stmt =
  370. conn.prepare("SELECT mode FROM access WHERE user_id = ?1 AND repo_id = ?2")?;
  371. let found = stmt
  372. .query_row(params![user_id, repo_id], |row| row.get::<_, i64>(0))
  373. .optional()?;
  374. if let Some(value) = found {
  375. mode = access_mode_from_i64(value);
  376. }
  377. Ok(mode)
  378. }
  379. pub fn authorize(
  380. &self,
  381. user_id: i64,
  382. repo_id: i64,
  383. desired: AccessMode,
  384. owner_id: i64,
  385. private: bool,
  386. ) -> AppResult<bool> {
  387. Ok((desired as i64) <= (self.access_mode(user_id, repo_id, owner_id, private)? as i64))
  388. }
  389. pub fn set_repo_perms(&self, repo_id: i64, access_map: &[(i64, AccessMode)]) -> AppResult<()> {
  390. let conn = self.lock()?;
  391. let tx = conn.unchecked_transaction()?;
  392. tx.execute("DELETE FROM access WHERE repo_id = ?1", params![repo_id])?;
  393. for (user_id, mode) in access_map {
  394. tx.execute(
  395. "INSERT INTO access (user_id, repo_id, mode) VALUES (?1, ?2, ?3)",
  396. params![user_id, repo_id, *mode as i64],
  397. )?;
  398. }
  399. tx.commit()?;
  400. Ok(())
  401. }
  402. pub fn upsert_collaboration(
  403. &self,
  404. repo_id: i64,
  405. user_id: i64,
  406. mode: AccessMode,
  407. ) -> AppResult<Collaboration> {
  408. let conn = self.lock()?;
  409. let tx = conn.unchecked_transaction()?;
  410. tx.execute(
  411. r#"
  412. INSERT INTO collaboration (user_id, repo_id, mode)
  413. VALUES (?1, ?2, ?3)
  414. ON CONFLICT(user_id, repo_id) DO UPDATE SET mode = excluded.mode
  415. "#,
  416. params![user_id, repo_id, mode as i64],
  417. )?;
  418. tx.execute(
  419. r#"
  420. INSERT INTO access (user_id, repo_id, mode)
  421. VALUES (?1, ?2, ?3)
  422. ON CONFLICT(user_id, repo_id) DO UPDATE SET mode = excluded.mode
  423. "#,
  424. params![user_id, repo_id, mode as i64],
  425. )?;
  426. let id = tx.query_row(
  427. "SELECT id FROM collaboration WHERE user_id = ?1 AND repo_id = ?2",
  428. params![user_id, repo_id],
  429. |row| row.get::<_, i64>(0),
  430. )?;
  431. tx.commit()?;
  432. drop(conn);
  433. self.get_collaboration_by_id(id)?.ok_or_else(|| {
  434. AppError::NotFound(format!("collaboration disappeared after upsert: {id}"))
  435. })
  436. }
  437. pub fn get_collaboration_by_id(&self, id: i64) -> AppResult<Option<Collaboration>> {
  438. let conn = self.lock()?;
  439. let mut stmt =
  440. conn.prepare("SELECT id, user_id, repo_id, mode FROM collaboration WHERE id = ?1")?;
  441. stmt.query_row(params![id], row_to_collaboration)
  442. .optional()
  443. .map_err(Into::into)
  444. }
  445. pub fn get_collaborator(
  446. &self,
  447. repo_id: i64,
  448. user_id: i64,
  449. ) -> AppResult<Option<CollaboratorResponse>> {
  450. let conn = self.lock()?;
  451. let mut stmt = conn.prepare(
  452. r#"
  453. SELECT
  454. u.id, u.lower_name, u.name, u.full_name, u.email, u.password_hash,
  455. u.is_active, u.is_admin, u.created_unix, u.updated_unix,
  456. c.mode
  457. FROM collaboration c
  458. JOIN user u ON u.id = c.user_id
  459. WHERE c.repo_id = ?1 AND c.user_id = ?2
  460. LIMIT 1
  461. "#,
  462. )?;
  463. stmt.query_row(params![repo_id, user_id], row_to_collaborator_response)
  464. .optional()
  465. .map_err(Into::into)
  466. }
  467. pub fn list_collaborators(&self, repo_id: i64) -> AppResult<Vec<CollaboratorResponse>> {
  468. let conn = self.lock()?;
  469. let mut stmt = conn.prepare(
  470. r#"
  471. SELECT
  472. u.id, u.lower_name, u.name, u.full_name, u.email, u.password_hash,
  473. u.is_active, u.is_admin, u.created_unix, u.updated_unix,
  474. c.mode
  475. FROM collaboration c
  476. JOIN user u ON u.id = c.user_id
  477. WHERE c.repo_id = ?1
  478. ORDER BY u.lower_name ASC
  479. "#,
  480. )?;
  481. let rows = stmt.query_map(params![repo_id], row_to_collaborator_response)?;
  482. let mut collaborators = Vec::new();
  483. for row in rows {
  484. collaborators.push(row?);
  485. }
  486. Ok(collaborators)
  487. }
  488. pub fn delete_collaboration(&self, repo_id: i64, user_id: i64) -> AppResult<()> {
  489. let conn = self.lock()?;
  490. let tx = conn.unchecked_transaction()?;
  491. tx.execute(
  492. "DELETE FROM collaboration WHERE repo_id = ?1 AND user_id = ?2",
  493. params![repo_id, user_id],
  494. )?;
  495. tx.execute(
  496. "DELETE FROM access WHERE repo_id = ?1 AND user_id = ?2",
  497. params![repo_id, user_id],
  498. )?;
  499. tx.commit()?;
  500. Ok(())
  501. }
  502. pub fn create_access_token(
  503. &self,
  504. user_id: i64,
  505. name: &str,
  506. token_hash: &str,
  507. ) -> AppResult<AccessToken> {
  508. let conn = self.lock()?;
  509. let tx = conn.unchecked_transaction()?;
  510. if self.access_token_exists_by_name(&tx, user_id, name)? {
  511. return Err(AppError::Conflict(format!(
  512. "access token already exists: {name}"
  513. )));
  514. }
  515. let now = now_unix();
  516. tx.execute(
  517. r#"
  518. INSERT INTO access_token (user_id, name, token_hash, created_unix, updated_unix)
  519. VALUES (?1, ?2, ?3, ?4, 0)
  520. "#,
  521. params![user_id, name, token_hash, now],
  522. )?;
  523. let id = tx.last_insert_rowid();
  524. tx.commit()?;
  525. drop(conn);
  526. self.get_access_token_by_id(id)?.ok_or_else(|| {
  527. AppError::NotFound(format!("access token disappeared after create: {id}"))
  528. })
  529. }
  530. pub fn get_access_token_by_id(&self, id: i64) -> AppResult<Option<AccessToken>> {
  531. let conn = self.lock()?;
  532. let mut stmt = conn.prepare(
  533. r#"
  534. SELECT id, user_id, name, token_hash, created_unix, updated_unix
  535. FROM access_token WHERE id = ?1
  536. "#,
  537. )?;
  538. stmt.query_row(params![id], row_to_access_token)
  539. .optional()
  540. .map_err(Into::into)
  541. }
  542. pub fn get_access_token_by_hash(&self, token_hash: &str) -> AppResult<Option<AccessToken>> {
  543. let conn = self.lock()?;
  544. let mut stmt = conn.prepare(
  545. r#"
  546. SELECT id, user_id, name, token_hash, created_unix, updated_unix
  547. FROM access_token WHERE token_hash = ?1
  548. "#,
  549. )?;
  550. stmt.query_row(params![token_hash], row_to_access_token)
  551. .optional()
  552. .map_err(Into::into)
  553. }
  554. pub fn list_access_tokens_by_user(&self, user_id: i64) -> AppResult<Vec<AccessToken>> {
  555. let conn = self.lock()?;
  556. let mut stmt = conn.prepare(
  557. r#"
  558. SELECT id, user_id, name, token_hash, created_unix, updated_unix
  559. FROM access_token
  560. WHERE user_id = ?1
  561. ORDER BY id ASC
  562. "#,
  563. )?;
  564. let rows = stmt.query_map(params![user_id], row_to_access_token)?;
  565. let mut tokens = Vec::new();
  566. for row in rows {
  567. tokens.push(row?);
  568. }
  569. Ok(tokens)
  570. }
  571. pub fn delete_access_token_by_id(&self, user_id: i64, token_id: i64) -> AppResult<bool> {
  572. let conn = self.lock()?;
  573. let affected = conn.execute(
  574. "DELETE FROM access_token WHERE id = ?1 AND user_id = ?2",
  575. params![token_id, user_id],
  576. )?;
  577. Ok(affected > 0)
  578. }
  579. pub fn touch_access_token(&self, token_id: i64) -> AppResult<()> {
  580. let conn = self.lock()?;
  581. conn.execute(
  582. "UPDATE access_token SET updated_unix = ?2 WHERE id = ?1",
  583. params![token_id, now_unix()],
  584. )?;
  585. Ok(())
  586. }
  587. pub fn create_pull_request(&self, new_pull: NewPullRequest<'_>) -> AppResult<PullRequest> {
  588. let conn = self.lock()?;
  589. let tx = conn.unchecked_transaction()?;
  590. let now = now_unix();
  591. let index = tx.query_row(
  592. "SELECT COALESCE(MAX(index_in_repo), 0) + 1 FROM pull_request WHERE base_repo_id = ?1",
  593. params![new_pull.base_repo_id],
  594. |row| row.get::<_, i64>(0),
  595. )?;
  596. tx.execute(
  597. r#"
  598. INSERT INTO pull_request (
  599. index_in_repo, title, body, status, head_repo_id, base_repo_id,
  600. head_user_name, head_branch, base_branch, merge_base, merged_commit_id, poster_id,
  601. has_merged, is_closed, created_unix, updated_unix
  602. ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, '', ?11, 0, 0, ?12, ?13)
  603. "#,
  604. params![
  605. index,
  606. new_pull.title,
  607. new_pull.body,
  608. new_pull.status as i64,
  609. new_pull.head_repo_id,
  610. new_pull.base_repo_id,
  611. new_pull.head_user_name,
  612. new_pull.head_branch,
  613. new_pull.base_branch,
  614. new_pull.merge_base,
  615. new_pull.poster_id,
  616. now,
  617. now
  618. ],
  619. )?;
  620. let id = tx.last_insert_rowid();
  621. tx.commit()?;
  622. drop(conn);
  623. self.get_pull_request_by_id(id)?.ok_or_else(|| {
  624. AppError::NotFound(format!("pull request disappeared after create: {id}"))
  625. })
  626. }
  627. pub fn get_pull_request_by_id(&self, id: i64) -> AppResult<Option<PullRequest>> {
  628. let conn = self.lock()?;
  629. let mut stmt = conn.prepare(
  630. r#"
  631. SELECT id, index_in_repo, title, body, status, head_repo_id, base_repo_id,
  632. head_user_name, head_branch, base_branch, merge_base, merged_commit_id, poster_id,
  633. has_merged, is_closed, created_unix, updated_unix
  634. FROM pull_request WHERE id = ?1
  635. "#,
  636. )?;
  637. stmt.query_row(params![id], row_to_pull_request)
  638. .optional()
  639. .map_err(Into::into)
  640. }
  641. pub fn get_unmerged_pull_request(
  642. &self,
  643. head_repo_id: i64,
  644. base_repo_id: i64,
  645. head_branch: &str,
  646. base_branch: &str,
  647. ) -> AppResult<Option<PullRequest>> {
  648. let conn = self.lock()?;
  649. let mut stmt = conn.prepare(
  650. r#"
  651. SELECT id, index_in_repo, title, body, status, head_repo_id, base_repo_id,
  652. head_user_name, head_branch, base_branch, merge_base, merged_commit_id, poster_id,
  653. has_merged, is_closed, created_unix, updated_unix
  654. FROM pull_request
  655. WHERE head_repo_id = ?1 AND base_repo_id = ?2
  656. AND head_branch = ?3 AND base_branch = ?4
  657. AND has_merged = 0 AND is_closed = 0
  658. LIMIT 1
  659. "#,
  660. )?;
  661. stmt.query_row(
  662. params![head_repo_id, base_repo_id, head_branch, base_branch],
  663. row_to_pull_request,
  664. )
  665. .optional()
  666. .map_err(Into::into)
  667. }
  668. pub fn get_pull_request_by_base_repo_and_index(
  669. &self,
  670. base_repo_id: i64,
  671. index: i64,
  672. ) -> AppResult<Option<PullRequest>> {
  673. let conn = self.lock()?;
  674. let mut stmt = conn.prepare(
  675. r#"
  676. SELECT id, index_in_repo, title, body, status, head_repo_id, base_repo_id,
  677. head_user_name, head_branch, base_branch, merge_base, merged_commit_id, poster_id,
  678. has_merged, is_closed, created_unix, updated_unix
  679. FROM pull_request
  680. WHERE base_repo_id = ?1 AND index_in_repo = ?2
  681. LIMIT 1
  682. "#,
  683. )?;
  684. stmt.query_row(params![base_repo_id, index], row_to_pull_request)
  685. .optional()
  686. .map_err(Into::into)
  687. }
  688. pub fn list_pull_requests_by_base_repo(
  689. &self,
  690. base_repo_id: i64,
  691. ) -> AppResult<Vec<PullRequest>> {
  692. let conn = self.lock()?;
  693. let mut stmt = conn.prepare(
  694. r#"
  695. SELECT id, index_in_repo, title, body, status, head_repo_id, base_repo_id,
  696. head_user_name, head_branch, base_branch, merge_base, merged_commit_id, poster_id,
  697. has_merged, is_closed, created_unix, updated_unix
  698. FROM pull_request
  699. WHERE base_repo_id = ?1
  700. ORDER BY index_in_repo ASC
  701. "#,
  702. )?;
  703. let rows = stmt.query_map(params![base_repo_id], row_to_pull_request)?;
  704. let mut pulls = Vec::new();
  705. for row in rows {
  706. pulls.push(row?);
  707. }
  708. Ok(pulls)
  709. }
  710. pub fn mark_pull_request_merged(
  711. &self,
  712. id: i64,
  713. merged_commit_id: &str,
  714. ) -> AppResult<PullRequest> {
  715. let conn = self.lock()?;
  716. let now = now_unix();
  717. conn.execute(
  718. r#"
  719. UPDATE pull_request
  720. SET has_merged = 1, is_closed = 1, merged_commit_id = ?2, updated_unix = ?3
  721. WHERE id = ?1
  722. "#,
  723. params![id, merged_commit_id, now],
  724. )?;
  725. drop(conn);
  726. self.get_pull_request_by_id(id)?.ok_or_else(|| {
  727. AppError::NotFound(format!("pull request disappeared after merge: {id}"))
  728. })
  729. }
  730. pub fn update_pull_request_open_state(
  731. &self,
  732. id: i64,
  733. is_closed: bool,
  734. status: PullRequestStatus,
  735. ) -> AppResult<PullRequest> {
  736. let conn = self.lock()?;
  737. let now = now_unix();
  738. conn.execute(
  739. r#"
  740. UPDATE pull_request
  741. SET is_closed = ?2, status = ?3, updated_unix = ?4
  742. WHERE id = ?1
  743. "#,
  744. params![id, is_closed, status as i64, now],
  745. )?;
  746. drop(conn);
  747. self.get_pull_request_by_id(id)?.ok_or_else(|| {
  748. AppError::NotFound(format!("pull request disappeared after state update: {id}"))
  749. })
  750. }
  751. fn user_exists_by_lower_name(&self, tx: &Transaction<'_>, lower_name: &str) -> AppResult<bool> {
  752. let found = tx
  753. .query_row(
  754. "SELECT 1 FROM user WHERE lower_name = ?1 LIMIT 1",
  755. params![lower_name],
  756. |row| row.get::<_, i64>(0),
  757. )
  758. .optional()?;
  759. Ok(found.is_some())
  760. }
  761. fn user_exists_by_email(&self, tx: &Transaction<'_>, email: &str) -> AppResult<bool> {
  762. let found = tx
  763. .query_row(
  764. "SELECT 1 FROM user WHERE email = ?1 LIMIT 1",
  765. params![email],
  766. |row| row.get::<_, i64>(0),
  767. )
  768. .optional()?;
  769. Ok(found.is_some())
  770. }
  771. fn repo_exists_by_name(
  772. &self,
  773. tx: &Transaction<'_>,
  774. owner_id: i64,
  775. lower_name: &str,
  776. ) -> AppResult<bool> {
  777. let found = tx
  778. .query_row(
  779. "SELECT 1 FROM repository WHERE owner_id = ?1 AND lower_name = ?2 LIMIT 1",
  780. params![owner_id, lower_name],
  781. |row| row.get::<_, i64>(0),
  782. )
  783. .optional()?;
  784. Ok(found.is_some())
  785. }
  786. fn access_token_exists_by_name(
  787. &self,
  788. tx: &Transaction<'_>,
  789. user_id: i64,
  790. name: &str,
  791. ) -> AppResult<bool> {
  792. let found = tx
  793. .query_row(
  794. "SELECT 1 FROM access_token WHERE user_id = ?1 AND name = ?2 LIMIT 1",
  795. params![user_id, name],
  796. |row| row.get::<_, i64>(0),
  797. )
  798. .optional()?;
  799. Ok(found.is_some())
  800. }
  801. fn lock(&self) -> AppResult<std::sync::MutexGuard<'_, Connection>> {
  802. self.conn
  803. .lock()
  804. .map_err(|_| AppError::Db(rusqlite::Error::InvalidQuery))
  805. }
  806. }
  807. pub struct NewUser<'a> {
  808. pub username: &'a str,
  809. pub full_name: &'a str,
  810. pub email: &'a str,
  811. pub password_hash: &'a str,
  812. pub is_active: bool,
  813. pub is_admin: bool,
  814. }
  815. pub struct NewRepository<'a> {
  816. pub owner_id: i64,
  817. pub owner_name: &'a str,
  818. pub name: &'a str,
  819. pub description: &'a str,
  820. pub default_branch: &'a str,
  821. pub is_private: bool,
  822. pub is_bare: bool,
  823. pub is_fork: bool,
  824. pub fork_id: i64,
  825. }
  826. pub struct NewPullRequest<'a> {
  827. pub title: &'a str,
  828. pub body: &'a str,
  829. pub status: PullRequestStatus,
  830. pub head_repo_id: i64,
  831. pub base_repo_id: i64,
  832. pub head_user_name: &'a str,
  833. pub head_branch: &'a str,
  834. pub base_branch: &'a str,
  835. pub merge_base: &'a str,
  836. pub poster_id: i64,
  837. }
  838. fn row_to_user(row: &rusqlite::Row<'_>) -> rusqlite::Result<User> {
  839. Ok(User {
  840. id: row.get(0)?,
  841. lower_name: row.get(1)?,
  842. name: row.get(2)?,
  843. full_name: row.get(3)?,
  844. email: row.get(4)?,
  845. password_hash: row.get(5)?,
  846. is_active: row.get(6)?,
  847. is_admin: row.get(7)?,
  848. created_unix: row.get(8)?,
  849. updated_unix: row.get(9)?,
  850. })
  851. }
  852. fn row_to_user_at(row: &rusqlite::Row<'_>, offset: usize) -> rusqlite::Result<User> {
  853. Ok(User {
  854. id: row.get(offset)?,
  855. lower_name: row.get(offset + 1)?,
  856. name: row.get(offset + 2)?,
  857. full_name: row.get(offset + 3)?,
  858. email: row.get(offset + 4)?,
  859. password_hash: row.get(offset + 5)?,
  860. is_active: row.get(offset + 6)?,
  861. is_admin: row.get(offset + 7)?,
  862. created_unix: row.get(offset + 8)?,
  863. updated_unix: row.get(offset + 9)?,
  864. })
  865. }
  866. fn row_to_repo(row: &rusqlite::Row<'_>) -> rusqlite::Result<Repository> {
  867. Ok(Repository {
  868. id: row.get(0)?,
  869. owner_id: row.get(1)?,
  870. lower_name: row.get(2)?,
  871. name: row.get(3)?,
  872. description: row.get(4)?,
  873. default_branch: row.get(5)?,
  874. is_private: row.get(6)?,
  875. is_bare: row.get(7)?,
  876. is_fork: row.get(8)?,
  877. fork_id: row.get(9)?,
  878. created_unix: row.get(10)?,
  879. updated_unix: row.get(11)?,
  880. })
  881. }
  882. fn row_to_repo_at(row: &rusqlite::Row<'_>, offset: usize) -> rusqlite::Result<Repository> {
  883. Ok(Repository {
  884. id: row.get(offset)?,
  885. owner_id: row.get(offset + 1)?,
  886. lower_name: row.get(offset + 2)?,
  887. name: row.get(offset + 3)?,
  888. description: row.get(offset + 4)?,
  889. default_branch: row.get(offset + 5)?,
  890. is_private: row.get(offset + 6)?,
  891. is_bare: row.get(offset + 7)?,
  892. is_fork: row.get(offset + 8)?,
  893. fork_id: row.get(offset + 9)?,
  894. created_unix: row.get(offset + 10)?,
  895. updated_unix: row.get(offset + 11)?,
  896. })
  897. }
  898. fn row_to_repository_with_owner(row: &rusqlite::Row<'_>) -> rusqlite::Result<RepositoryWithOwner> {
  899. Ok(RepositoryWithOwner {
  900. repo: row_to_repo_at(row, 0)?,
  901. owner: row_to_user_at(row, 12)?,
  902. })
  903. }
  904. fn row_to_access_token(row: &rusqlite::Row<'_>) -> rusqlite::Result<AccessToken> {
  905. Ok(AccessToken {
  906. id: row.get(0)?,
  907. user_id: row.get(1)?,
  908. name: row.get(2)?,
  909. token_hash: row.get(3)?,
  910. created_unix: row.get(4)?,
  911. updated_unix: row.get(5)?,
  912. })
  913. }
  914. fn row_to_collaborator_response(
  915. row: &rusqlite::Row<'_>,
  916. ) -> rusqlite::Result<CollaboratorResponse> {
  917. Ok(CollaboratorResponse {
  918. user: row_to_user_at(row, 0)?,
  919. mode: access_mode_from_i64(row.get::<_, i64>(10)?),
  920. })
  921. }
  922. fn row_to_pull_request(row: &rusqlite::Row<'_>) -> rusqlite::Result<PullRequest> {
  923. Ok(PullRequest {
  924. id: row.get(0)?,
  925. index: row.get(1)?,
  926. title: row.get(2)?,
  927. body: row.get(3)?,
  928. status: pull_request_status_from_i64(row.get::<_, i64>(4)?),
  929. head_repo_id: row.get(5)?,
  930. base_repo_id: row.get(6)?,
  931. head_user_name: row.get(7)?,
  932. head_branch: row.get(8)?,
  933. base_branch: row.get(9)?,
  934. merge_base: row.get(10)?,
  935. merged_commit_id: row.get(11)?,
  936. poster_id: row.get(12)?,
  937. has_merged: row.get(13)?,
  938. is_closed: row.get(14)?,
  939. created_unix: row.get(15)?,
  940. updated_unix: row.get(16)?,
  941. })
  942. }
  943. fn row_to_collaboration(row: &rusqlite::Row<'_>) -> rusqlite::Result<Collaboration> {
  944. Ok(Collaboration {
  945. id: row.get(0)?,
  946. user_id: row.get(1)?,
  947. repo_id: row.get(2)?,
  948. mode: access_mode_from_i64(row.get::<_, i64>(3)?),
  949. })
  950. }
  951. fn access_mode_from_i64(value: i64) -> AccessMode {
  952. match value {
  953. 1 => AccessMode::Read,
  954. 2 => AccessMode::Write,
  955. 3 => AccessMode::Admin,
  956. 4 => AccessMode::Owner,
  957. _ => AccessMode::None,
  958. }
  959. }
  960. fn pull_request_status_from_i64(value: i64) -> PullRequestStatus {
  961. match value {
  962. 0 => PullRequestStatus::Conflict,
  963. 1 => PullRequestStatus::Checking,
  964. _ => PullRequestStatus::Mergeable,
  965. }
  966. }
  967. fn now_unix() -> i64 {
  968. SystemTime::now()
  969. .duration_since(UNIX_EPOCH)
  970. .unwrap_or_default()
  971. .as_secs() as i64
  972. }
  973. fn ensure_column_exists(
  974. conn: &Connection,
  975. table_name: &str,
  976. column_name: &str,
  977. column_sql: &str,
  978. ) -> AppResult<()> {
  979. let mut stmt = conn.prepare(&format!("PRAGMA table_info({table_name})"))?;
  980. let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
  981. for row in rows {
  982. if row? == column_name {
  983. return Ok(());
  984. }
  985. }
  986. conn.execute(
  987. &format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {column_sql}"),
  988. [],
  989. )?;
  990. Ok(())
  991. }