service.rs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194
  1. use argon2::{
  2. Argon2, PasswordHash, PasswordVerifier,
  3. password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
  4. };
  5. use rand_core::RngCore;
  6. use sha2::{Digest, Sha256};
  7. use crate::{
  8. app::AppState,
  9. db::{NewRepository, NewUser},
  10. error::{AppError, AppResult},
  11. git,
  12. models::{
  13. AccessMode, AccessTokenResponse, Branch, CollaboratorResponse, CompareCommit,
  14. CompareFile, CompareRequest, CompareResponse, CreateAccessTokenRequest,
  15. CreateAccessTokenResponse, CreatePullRequestRequest, CreateRepositoryRequest,
  16. CreateUserRequest, ForkRepositoryRequest, LoginRequest, LoginResponse,
  17. MergePullRequestRequest, PullRequest, PullRequestDetailResponse, PullRequestResponse,
  18. PullRequestStatus, RepositoryPermission, RepositoryWithOwner, UpsertCollaboratorRequest,
  19. User,
  20. },
  21. repox,
  22. };
  23. const TOKEN_TTL_DAYS: i64 = 365;
  24. const MAX_PER_PAGE: i64 = 100;
  25. pub fn create_user(state: &AppState, req: CreateUserRequest) -> AppResult<User> {
  26. validate_user_name(&req.username)?;
  27. validate_email(&req.email)?;
  28. if req.password.len() < 8 {
  29. return Err(AppError::Validation(
  30. "password must be at least 8 characters".to_string(),
  31. ));
  32. }
  33. let salt = SaltString::generate(&mut OsRng);
  34. let password_hash = Argon2::default()
  35. .hash_password(req.password.as_bytes(), &salt)
  36. .map_err(|err| AppError::Validation(format!("password hashing failed: {err}")))?
  37. .to_string();
  38. state.db.create_user(NewUser {
  39. username: &req.username,
  40. full_name: &req.full_name,
  41. email: &req.email,
  42. password_hash: &password_hash,
  43. is_active: req.is_active,
  44. is_admin: req.is_admin,
  45. })
  46. }
  47. pub fn get_user(state: &AppState, username: &str) -> AppResult<User> {
  48. state
  49. .db
  50. .get_user_by_username(username)?
  51. .ok_or_else(|| AppError::NotFound(format!("user not found: {username}")))
  52. }
  53. pub fn should_allow_bootstrap_admin(state: &AppState) -> AppResult<bool> {
  54. Ok(state.db.user_count()? == 0)
  55. }
  56. pub fn login(state: &AppState, req: LoginRequest) -> AppResult<LoginResponse> {
  57. let user = authenticate_password(state, &req.login, &req.password)?;
  58. let token = issue_login_access_token(state, user.id)?;
  59. Ok(LoginResponse {
  60. token: token.token,
  61. user,
  62. })
  63. }
  64. fn issue_login_access_token(state: &AppState, user_id: i64) -> AppResult<CreateAccessTokenResponse> {
  65. let token = random_token();
  66. let token_hash = hash_token(&token);
  67. let record = state.db.replace_access_token(user_id, "login", &token_hash)?;
  68. Ok(CreateAccessTokenResponse {
  69. id: record.id,
  70. name: record.name,
  71. token,
  72. created_unix: record.created_unix,
  73. updated_unix: record.updated_unix,
  74. })
  75. }
  76. pub fn issue_access_token(
  77. state: &AppState,
  78. user_id: i64,
  79. req: CreateAccessTokenRequest,
  80. ) -> AppResult<CreateAccessTokenResponse> {
  81. if req.name.trim().is_empty() {
  82. return Err(AppError::Validation(
  83. "token name cannot be empty".to_string(),
  84. ));
  85. }
  86. let token = random_token();
  87. let token_hash = hash_token(&token);
  88. let record = state
  89. .db
  90. .create_access_token(user_id, req.name.trim(), &token_hash)?;
  91. Ok(CreateAccessTokenResponse {
  92. id: record.id,
  93. name: record.name,
  94. token,
  95. created_unix: record.created_unix,
  96. updated_unix: record.updated_unix,
  97. })
  98. }
  99. pub fn list_access_tokens(
  100. state: &AppState,
  101. user_id: i64,
  102. page: i64,
  103. per_page: i64,
  104. ) -> AppResult<Vec<AccessTokenResponse>> {
  105. let (limit, offset) = pagination(page, per_page);
  106. let tokens = state.db.list_access_tokens_by_user(user_id, limit, offset)?;
  107. Ok(tokens
  108. .into_iter()
  109. .map(|token| AccessTokenResponse {
  110. id: token.id,
  111. name: token.name,
  112. created_unix: token.created_unix,
  113. updated_unix: token.updated_unix,
  114. })
  115. .collect())
  116. }
  117. pub fn delete_access_token(state: &AppState, user_id: i64, token_id: i64) -> AppResult<()> {
  118. if state.db.delete_access_token_by_id(user_id, token_id)? {
  119. return Ok(());
  120. }
  121. Err(AppError::NotFound(format!(
  122. "access token not found: {token_id}"
  123. )))
  124. }
  125. pub fn authenticate_token(state: &AppState, bearer_token: &str) -> AppResult<User> {
  126. let token_hash = hash_token(bearer_token);
  127. let token = state
  128. .db
  129. .get_access_token_by_hash(&token_hash)?
  130. .ok_or_else(|| AppError::Unauthorized("invalid access token".to_string()))?;
  131. if is_token_expired(&token) {
  132. return Err(AppError::Unauthorized("access token expired".to_string()));
  133. }
  134. if let Err(err) = state.db.touch_access_token(token.id) {
  135. eprintln!("warning: touch_access_token({}) failed: {err}", token.id);
  136. }
  137. let user = state
  138. .db
  139. .get_user_by_id(token.user_id)?
  140. .ok_or_else(|| AppError::Unauthorized("token owner not found".to_string()))?;
  141. if !user.is_active {
  142. return Err(AppError::Unauthorized("inactive user".to_string()));
  143. }
  144. Ok(user)
  145. }
  146. pub fn authenticate_http_basic(state: &AppState, login: &str, secret: &str) -> AppResult<User> {
  147. match login_with_password(state, login, secret) {
  148. Ok(user) => Ok(user),
  149. Err(AppError::Unauthorized(_)) => {
  150. authenticate_token(state, secret).or_else(|_| authenticate_token(state, login))
  151. }
  152. Err(err) => Err(err),
  153. }
  154. }
  155. pub fn create_repository(
  156. state: &AppState,
  157. owner: &User,
  158. req: CreateRepositoryRequest,
  159. ) -> AppResult<RepositoryWithOwner> {
  160. validate_repo_name(&req.name)?;
  161. let repo_path = repox::repository_path(&state.config.repository.root, &owner.name, &req.name);
  162. if repo_path.exists() {
  163. return Err(AppError::Conflict(format!(
  164. "repository directory already exists: {}",
  165. repo_path.display()
  166. )));
  167. }
  168. let repo = state.db.create_repository(NewRepository {
  169. owner_id: owner.id,
  170. owner_name: &owner.name,
  171. name: &req.name,
  172. description: &req.description,
  173. default_branch: &state.config.repository.default_branch,
  174. is_private: req.is_private,
  175. is_bare: !req.auto_init,
  176. is_fork: false,
  177. fork_id: 0,
  178. })?;
  179. if let Err(err) = git::init_bare_repo_with_binary(
  180. &state.config.repository.git_binary,
  181. &repo_path,
  182. &state.config.repository.default_branch,
  183. ) {
  184. let _ = std::fs::remove_dir_all(&repo_path);
  185. let _ = state.db.delete_repository_by_id(repo.id);
  186. return Err(err);
  187. }
  188. if req.auto_init {
  189. let readme = format!("# {}\n", req.name);
  190. if let Err(err) = git::create_initial_commit_with_binary(
  191. &state.config.repository.git_binary,
  192. &repo_path,
  193. &state.config.repository.default_branch,
  194. &readme,
  195. &owner.name,
  196. &owner.email,
  197. ) {
  198. let _ = std::fs::remove_dir_all(&repo_path);
  199. let _ = state.db.delete_repository_by_id(repo.id);
  200. return Err(err);
  201. }
  202. }
  203. Ok(RepositoryWithOwner {
  204. repo,
  205. owner: owner.clone(),
  206. })
  207. }
  208. pub fn fork_repository(
  209. state: &AppState,
  210. acting_user: &User,
  211. base_owner_name: &str,
  212. base_repo_name: &str,
  213. req: ForkRepositoryRequest,
  214. ) -> AppResult<RepositoryWithOwner> {
  215. let base_repo = get_repository(state, base_owner_name, base_repo_name)?;
  216. ensure_repo_access(state, Some(acting_user), &base_repo, AccessMode::Read)?;
  217. if acting_user.id == base_repo.owner.id {
  218. return Err(AppError::Validation(
  219. "cannot fork to the same owner".to_string(),
  220. ));
  221. }
  222. if state.db.has_forked_by(base_repo.repo.id, acting_user.id)? {
  223. return Err(AppError::Conflict(
  224. "repository already forked by this user".to_string(),
  225. ));
  226. }
  227. validate_repo_name(&req.name)?;
  228. let repo = state.db.create_repository(NewRepository {
  229. owner_id: acting_user.id,
  230. owner_name: &acting_user.name,
  231. name: &req.name,
  232. description: &req.description,
  233. default_branch: &base_repo.repo.default_branch,
  234. is_private: base_repo.repo.is_private,
  235. is_bare: true,
  236. is_fork: true,
  237. fork_id: base_repo.repo.id,
  238. })?;
  239. let source_repo_path = repox::repository_path(
  240. &state.config.repository.root,
  241. &base_repo.owner.name,
  242. &base_repo.repo.name,
  243. );
  244. let target_repo_path =
  245. repox::repository_path(&state.config.repository.root, &acting_user.name, &req.name);
  246. if let Err(err) = git::clone_bare_repo_with_binary(
  247. &state.config.repository.git_binary,
  248. &source_repo_path,
  249. &target_repo_path,
  250. ) {
  251. let _ = std::fs::remove_dir_all(&target_repo_path);
  252. let _ = state.db.delete_repository_by_id(repo.id);
  253. return Err(err);
  254. }
  255. Ok(RepositoryWithOwner {
  256. repo,
  257. owner: acting_user.clone(),
  258. })
  259. }
  260. pub fn upsert_collaborator(
  261. state: &AppState,
  262. acting_user: &User,
  263. owner_name: &str,
  264. repo_name: &str,
  265. req: UpsertCollaboratorRequest,
  266. ) -> AppResult<CollaboratorResponse> {
  267. let repo = get_repository(state, owner_name, repo_name)?;
  268. ensure_repo_owner(acting_user, &repo)?;
  269. let collaborator = get_user(state, &req.username)?;
  270. if collaborator.id == repo.owner.id {
  271. return Err(AppError::Validation(
  272. "repository owner cannot be added as collaborator".to_string(),
  273. ));
  274. }
  275. let mode = AccessMode::parse_permission(&req.permission).ok_or_else(|| {
  276. AppError::Validation("permission must be one of: read, write, admin".to_string())
  277. })?;
  278. state
  279. .db
  280. .upsert_collaboration(repo.repo.id, collaborator.id, mode)?;
  281. Ok(CollaboratorResponse {
  282. user: collaborator,
  283. mode,
  284. })
  285. }
  286. pub fn remove_collaborator(
  287. state: &AppState,
  288. acting_user: &User,
  289. owner_name: &str,
  290. repo_name: &str,
  291. collaborator_name: &str,
  292. ) -> AppResult<()> {
  293. let repo = get_repository(state, owner_name, repo_name)?;
  294. ensure_repo_owner(acting_user, &repo)?;
  295. let collaborator = get_user(state, collaborator_name)?;
  296. state.db.delete_collaboration(repo.repo.id, collaborator.id)
  297. }
  298. pub fn list_collaborators(
  299. state: &AppState,
  300. requesting_user: Option<&User>,
  301. owner_name: &str,
  302. repo_name: &str,
  303. page: i64,
  304. per_page: i64,
  305. ) -> AppResult<Vec<CollaboratorResponse>> {
  306. let repo = get_repository(state, owner_name, repo_name)?;
  307. ensure_repo_readable(state, requesting_user, &repo)?;
  308. let (limit, offset) = pagination(page, per_page);
  309. state.db.list_collaborators(repo.repo.id, limit, offset)
  310. }
  311. pub fn get_collaborator(
  312. state: &AppState,
  313. requesting_user: Option<&User>,
  314. owner_name: &str,
  315. repo_name: &str,
  316. collaborator_name: &str,
  317. ) -> AppResult<CollaboratorResponse> {
  318. let repo = get_repository(state, owner_name, repo_name)?;
  319. ensure_repo_readable(state, requesting_user, &repo)?;
  320. let collaborator = get_user(state, collaborator_name)?;
  321. state
  322. .db
  323. .get_collaborator(repo.repo.id, collaborator.id)?
  324. .ok_or_else(|| AppError::NotFound(format!("collaborator not found: {collaborator_name}")))
  325. }
  326. pub fn list_repositories_by_owner(
  327. state: &AppState,
  328. requesting_user: Option<&User>,
  329. owner_name: &str,
  330. query: &str,
  331. page: i64,
  332. per_page: i64,
  333. ) -> AppResult<Vec<RepositoryWithOwner>> {
  334. let owner = get_user(state, owner_name)?;
  335. let (limit, offset) = pagination(page, per_page);
  336. let repos = state.db.list_repositories_with_owners_by_owner(owner.id, limit, offset)?;
  337. filter_visible_repositories(state, requesting_user, repos, query)
  338. }
  339. pub fn list_visible_repositories(
  340. state: &AppState,
  341. requesting_user: Option<&User>,
  342. query: &str,
  343. page: i64,
  344. per_page: i64,
  345. ) -> AppResult<Vec<RepositoryWithOwner>> {
  346. let (limit, offset) = pagination(page, per_page);
  347. let repos = state.db.list_repositories_with_owners(limit, offset)?;
  348. filter_visible_repositories(state, requesting_user, repos, query)
  349. }
  350. pub fn repository_permission(
  351. state: &AppState,
  352. requesting_user: Option<&User>,
  353. repo: &RepositoryWithOwner,
  354. ) -> AppResult<RepositoryPermission> {
  355. let mode = effective_access_mode(state, requesting_user, repo)?;
  356. Ok(RepositoryPermission {
  357. mode,
  358. can_read: (mode as i64) >= (AccessMode::Read as i64),
  359. can_write: (mode as i64) >= (AccessMode::Write as i64),
  360. can_admin: (mode as i64) >= (AccessMode::Admin as i64),
  361. is_owner: mode == AccessMode::Owner,
  362. })
  363. }
  364. pub fn list_branches(
  365. state: &AppState,
  366. requesting_user: Option<&User>,
  367. owner_name: &str,
  368. repo_name: &str,
  369. ) -> AppResult<Vec<Branch>> {
  370. let repo = get_repository(state, owner_name, repo_name)?;
  371. ensure_repo_access(state, requesting_user, &repo, AccessMode::Read)?;
  372. let repo_path = repox::repository_path(&state.config.repository.root, owner_name, repo_name);
  373. let branches = git::list_branches_with_binary(&state.config.repository.git_binary, &repo_path)?;
  374. Ok(branches.into_iter().map(|name| Branch { name }).collect())
  375. }
  376. pub fn create_pull_request(
  377. state: &AppState,
  378. acting_user: &User,
  379. base_owner_name: &str,
  380. base_repo_name: &str,
  381. req: CreatePullRequestRequest,
  382. ) -> AppResult<PullRequestResponse> {
  383. if req.title.trim().is_empty() {
  384. return Err(AppError::Validation(
  385. "pull request title cannot be empty".to_string(),
  386. ));
  387. }
  388. let base_repo = get_repository(state, base_owner_name, base_repo_name)?;
  389. ensure_repo_access(state, Some(acting_user), &base_repo, AccessMode::Read)?;
  390. let base_repo_path = repox::repository_path(
  391. &state.config.repository.root,
  392. &base_repo.owner.name,
  393. &base_repo.repo.name,
  394. );
  395. if !git::branch_exists_with_binary(
  396. &state.config.repository.git_binary,
  397. &base_repo_path,
  398. &req.base_branch,
  399. )? {
  400. return Err(AppError::NotFound(format!(
  401. "base branch not found: {}",
  402. req.base_branch
  403. )));
  404. }
  405. let head_repo = get_repository(state, &req.head_owner, &req.head_repo)?;
  406. let same_repo = head_repo.repo.id == base_repo.repo.id;
  407. if same_repo && req.head_branch == req.base_branch {
  408. return Err(AppError::Validation(
  409. "head and base branch cannot be the same".to_string(),
  410. ));
  411. }
  412. if !same_repo && (!head_repo.repo.is_fork || head_repo.repo.fork_id != base_repo.repo.id) {
  413. return Err(AppError::NotFound(
  414. "head repository is not a fork of base repository".to_string(),
  415. ));
  416. }
  417. ensure_repo_access(state, Some(acting_user), &head_repo, AccessMode::Write)?;
  418. let head_repo_path = repox::repository_path(
  419. &state.config.repository.root,
  420. &head_repo.owner.name,
  421. &head_repo.repo.name,
  422. );
  423. if !git::branch_exists_with_binary(
  424. &state.config.repository.git_binary,
  425. &head_repo_path,
  426. &req.head_branch,
  427. )? {
  428. return Err(AppError::NotFound(format!(
  429. "head branch not found: {}",
  430. req.head_branch
  431. )));
  432. }
  433. if state
  434. .db
  435. .get_unmerged_pull_request(
  436. head_repo.repo.id,
  437. base_repo.repo.id,
  438. &req.head_branch,
  439. &req.base_branch,
  440. )?
  441. .is_some()
  442. {
  443. return Err(AppError::Conflict(
  444. "an open pull request already exists for this head/base branch pair".to_string(),
  445. ));
  446. }
  447. let merge_base = git::merge_base_with_binary(
  448. &state.config.repository.git_binary,
  449. &base_repo_path,
  450. &req.base_branch,
  451. &head_repo_path,
  452. &req.head_branch,
  453. )?;
  454. if merge_base.is_empty() {
  455. return Err(AppError::Validation(
  456. "no merge base between branches".to_string(),
  457. ));
  458. }
  459. let status = if git::test_merge_with_binary(
  460. &state.config.repository.git_binary,
  461. &base_repo_path,
  462. &req.base_branch,
  463. &head_repo_path,
  464. &req.head_branch,
  465. )? {
  466. PullRequestStatus::Mergeable
  467. } else {
  468. PullRequestStatus::Conflict
  469. };
  470. let pull_request = state.db.create_pull_request(crate::db::NewPullRequest {
  471. title: req.title.trim(),
  472. body: req.body.trim(),
  473. status,
  474. head_repo_id: head_repo.repo.id,
  475. base_repo_id: base_repo.repo.id,
  476. head_user_name: head_repo.owner.name.as_str(),
  477. head_branch: req.head_branch.as_str(),
  478. base_branch: req.base_branch.as_str(),
  479. merge_base: merge_base.as_str(),
  480. poster_id: acting_user.id,
  481. })?;
  482. Ok(PullRequestResponse {
  483. pull_request,
  484. head_repo,
  485. base_repo,
  486. })
  487. }
  488. pub fn merge_pull_request(
  489. state: &AppState,
  490. acting_user: &User,
  491. base_owner_name: &str,
  492. base_repo_name: &str,
  493. index: i64,
  494. req: MergePullRequestRequest,
  495. ) -> AppResult<PullRequestResponse> {
  496. let base_repo = get_repository(state, base_owner_name, base_repo_name)?;
  497. ensure_repo_access(state, Some(acting_user), &base_repo, AccessMode::Write)?;
  498. let pull = state
  499. .db
  500. .get_pull_request_by_base_repo_and_index(base_repo.repo.id, index)?
  501. .ok_or_else(|| AppError::NotFound(format!("pull request not found: {index}")))?;
  502. if pull.has_merged || pull.is_closed {
  503. return Err(AppError::Conflict(
  504. "pull request is already closed".to_string(),
  505. ));
  506. }
  507. if pull.status != PullRequestStatus::Mergeable {
  508. return Err(AppError::Conflict(
  509. "pull request cannot be merged automatically".to_string(),
  510. ));
  511. }
  512. let head_repo = get_repository_by_id(state, pull.head_repo_id)?;
  513. let base_repo_path = repox::repository_path(
  514. &state.config.repository.root,
  515. &base_repo.owner.name,
  516. &base_repo.repo.name,
  517. );
  518. let head_repo_path = repox::repository_path(
  519. &state.config.repository.root,
  520. &head_repo.owner.name,
  521. &head_repo.repo.name,
  522. );
  523. let message = if req.message.trim().is_empty() {
  524. format!(
  525. "Merge branch '{}' of {}/{} into {}",
  526. pull.head_branch, head_repo.owner.name, head_repo.repo.name, pull.base_branch
  527. )
  528. } else {
  529. req.message.trim().to_string()
  530. };
  531. let merged_commit_id = git::merge_pull_request_with_binary(
  532. &state.config.repository.git_binary,
  533. &base_repo_path,
  534. &pull.base_branch,
  535. &head_repo_path,
  536. &pull.head_branch,
  537. &acting_user.name,
  538. &acting_user.email,
  539. &message,
  540. )?;
  541. let pull_request = state
  542. .db
  543. .mark_pull_request_merged(pull.id, &merged_commit_id)?;
  544. build_pull_request_response(state, pull_request, Some(head_repo), Some(base_repo))
  545. }
  546. pub fn close_pull_request(
  547. state: &AppState,
  548. acting_user: &User,
  549. base_owner_name: &str,
  550. base_repo_name: &str,
  551. index: i64,
  552. ) -> AppResult<PullRequestResponse> {
  553. let base_repo = get_repository(state, base_owner_name, base_repo_name)?;
  554. let pull = state
  555. .db
  556. .get_pull_request_by_base_repo_and_index(base_repo.repo.id, index)?
  557. .ok_or_else(|| AppError::NotFound(format!("pull request not found: {index}")))?;
  558. ensure_pull_request_admin(state, acting_user, &base_repo, &pull)?;
  559. if pull.has_merged {
  560. return Err(AppError::Conflict(
  561. "merged pull request cannot be closed".to_string(),
  562. ));
  563. }
  564. if pull.is_closed {
  565. return Err(AppError::Conflict(
  566. "pull request is already closed".to_string(),
  567. ));
  568. }
  569. let pull_request = state
  570. .db
  571. .update_pull_request_open_state(pull.id, true, pull.status)?;
  572. build_pull_request_response(state, pull_request, None, Some(base_repo))
  573. }
  574. pub fn reopen_pull_request(
  575. state: &AppState,
  576. acting_user: &User,
  577. base_owner_name: &str,
  578. base_repo_name: &str,
  579. index: i64,
  580. ) -> AppResult<PullRequestResponse> {
  581. let base_repo = get_repository(state, base_owner_name, base_repo_name)?;
  582. let pull = state
  583. .db
  584. .get_pull_request_by_base_repo_and_index(base_repo.repo.id, index)?
  585. .ok_or_else(|| AppError::NotFound(format!("pull request not found: {index}")))?;
  586. ensure_pull_request_admin(state, acting_user, &base_repo, &pull)?;
  587. if pull.has_merged {
  588. return Err(AppError::Conflict(
  589. "merged pull request cannot be reopened".to_string(),
  590. ));
  591. }
  592. if !pull.is_closed {
  593. return Err(AppError::Conflict(
  594. "pull request is already open".to_string(),
  595. ));
  596. }
  597. if let Some(existing) = state.db.get_unmerged_pull_request(
  598. pull.head_repo_id,
  599. pull.base_repo_id,
  600. &pull.head_branch,
  601. &pull.base_branch,
  602. )? {
  603. if existing.id != pull.id {
  604. return Err(AppError::Conflict(
  605. "an open pull request already exists for this head/base branch pair".to_string(),
  606. ));
  607. }
  608. }
  609. let head_repo = get_repository_by_id(state, pull.head_repo_id)?;
  610. let base_repo_path = repox::repository_path(
  611. &state.config.repository.root,
  612. &base_repo.owner.name,
  613. &base_repo.repo.name,
  614. );
  615. let head_repo_path = repox::repository_path(
  616. &state.config.repository.root,
  617. &head_repo.owner.name,
  618. &head_repo.repo.name,
  619. );
  620. if !git::branch_exists_with_binary(
  621. &state.config.repository.git_binary,
  622. &base_repo_path,
  623. &pull.base_branch,
  624. )? {
  625. return Err(AppError::NotFound(format!(
  626. "base branch not found: {}",
  627. pull.base_branch
  628. )));
  629. }
  630. if !git::branch_exists_with_binary(
  631. &state.config.repository.git_binary,
  632. &head_repo_path,
  633. &pull.head_branch,
  634. )? {
  635. return Err(AppError::NotFound(format!(
  636. "head branch not found: {}",
  637. pull.head_branch
  638. )));
  639. }
  640. let merge_base = git::merge_base_with_binary(
  641. &state.config.repository.git_binary,
  642. &base_repo_path,
  643. &pull.base_branch,
  644. &head_repo_path,
  645. &pull.head_branch,
  646. )?;
  647. if merge_base.is_empty() {
  648. return Err(AppError::Validation(
  649. "no merge base between branches".to_string(),
  650. ));
  651. }
  652. let status = if git::test_merge_with_binary(
  653. &state.config.repository.git_binary,
  654. &base_repo_path,
  655. &pull.base_branch,
  656. &head_repo_path,
  657. &pull.head_branch,
  658. )? {
  659. PullRequestStatus::Mergeable
  660. } else {
  661. PullRequestStatus::Conflict
  662. };
  663. let pull_request = state
  664. .db
  665. .update_pull_request_open_state(pull.id, false, status)?;
  666. build_pull_request_response(state, pull_request, Some(head_repo), Some(base_repo))
  667. }
  668. pub fn list_pull_requests(
  669. state: &AppState,
  670. requesting_user: Option<&User>,
  671. owner_name: &str,
  672. repo_name: &str,
  673. page: i64,
  674. per_page: i64,
  675. ) -> AppResult<Vec<PullRequestResponse>> {
  676. let base_repo = get_repository(state, owner_name, repo_name)?;
  677. ensure_repo_access(state, requesting_user, &base_repo, AccessMode::Read)?;
  678. let (limit, offset) = pagination(page, per_page);
  679. let pulls = state
  680. .db
  681. .list_pull_requests_by_base_repo(base_repo.repo.id, limit, offset)?;
  682. let mut result = Vec::with_capacity(pulls.len());
  683. for pull in pulls {
  684. result.push(build_pull_request_response(
  685. state,
  686. pull,
  687. None,
  688. Some(base_repo.clone()),
  689. )?);
  690. }
  691. Ok(result)
  692. }
  693. pub fn get_pull_request_detail(
  694. state: &AppState,
  695. requesting_user: Option<&User>,
  696. owner_name: &str,
  697. repo_name: &str,
  698. index: i64,
  699. ) -> AppResult<PullRequestDetailResponse> {
  700. let base_repo = get_repository(state, owner_name, repo_name)?;
  701. ensure_repo_readable(state, requesting_user, &base_repo)?;
  702. let pull_request = state
  703. .db
  704. .get_pull_request_by_base_repo_and_index(base_repo.repo.id, index)?
  705. .ok_or_else(|| AppError::NotFound(format!("pull request not found: {index}")))?;
  706. let head_repo = get_repository_by_id(state, pull_request.head_repo_id)?;
  707. let compare = build_compare_for_pull_request(state, &base_repo, &head_repo, &pull_request)?;
  708. Ok(PullRequestDetailResponse {
  709. pull_request,
  710. head_repo,
  711. base_repo,
  712. compare,
  713. })
  714. }
  715. pub fn compare_repositories(
  716. state: &AppState,
  717. acting_user: &User,
  718. owner_name: &str,
  719. repo_name: &str,
  720. req: CompareRequest,
  721. ) -> AppResult<CompareResponse> {
  722. let base_repo = get_repository(state, owner_name, repo_name)?;
  723. ensure_repo_access(state, Some(acting_user), &base_repo, AccessMode::Read)?;
  724. let head_repo = get_repository(state, &req.head_owner, &req.head_repo)?;
  725. let same_repo = head_repo.repo.id == base_repo.repo.id;
  726. if !same_repo && (!head_repo.repo.is_fork || head_repo.repo.fork_id != base_repo.repo.id) {
  727. return Err(AppError::NotFound(
  728. "head repository is not a fork of base repository".to_string(),
  729. ));
  730. }
  731. ensure_repo_access(state, Some(acting_user), &head_repo, AccessMode::Read)?;
  732. build_compare_for_refs(
  733. state,
  734. &base_repo,
  735. &head_repo,
  736. req.base.trim(),
  737. req.head_branch.trim(),
  738. )
  739. }
  740. pub fn get_repository(
  741. state: &AppState,
  742. owner_name: &str,
  743. repo_name: &str,
  744. ) -> AppResult<RepositoryWithOwner> {
  745. let owner = get_user(state, owner_name)?;
  746. let repo = state
  747. .db
  748. .get_repository_by_name(owner.id, repo_name)?
  749. .ok_or_else(|| {
  750. AppError::NotFound(format!("repository not found: {owner_name}/{repo_name}"))
  751. })?;
  752. Ok(RepositoryWithOwner { repo, owner })
  753. }
  754. pub fn get_repository_by_id(state: &AppState, repo_id: i64) -> AppResult<RepositoryWithOwner> {
  755. let repo = state
  756. .db
  757. .get_repository_by_id(repo_id)?
  758. .ok_or_else(|| AppError::NotFound(format!("repository not found: {repo_id}")))?;
  759. let owner = state.db.get_user_by_id(repo.owner_id)?.ok_or_else(|| {
  760. AppError::NotFound(format!("repository owner not found: {}", repo.owner_id))
  761. })?;
  762. Ok(RepositoryWithOwner { repo, owner })
  763. }
  764. pub fn get_repository_for_read(
  765. state: &AppState,
  766. requesting_user: Option<&User>,
  767. owner_name: &str,
  768. repo_name: &str,
  769. ) -> AppResult<RepositoryWithOwner> {
  770. let repo = get_repository(state, owner_name, repo_name)?;
  771. ensure_repo_readable(state, requesting_user, &repo)?;
  772. Ok(repo)
  773. }
  774. fn build_pull_request_response(
  775. state: &AppState,
  776. pull_request: PullRequest,
  777. head_repo: Option<RepositoryWithOwner>,
  778. base_repo: Option<RepositoryWithOwner>,
  779. ) -> AppResult<PullRequestResponse> {
  780. let head_repo = match head_repo {
  781. Some(repo) => repo,
  782. None => get_repository_by_id(state, pull_request.head_repo_id)?,
  783. };
  784. let base_repo = match base_repo {
  785. Some(repo) => repo,
  786. None => get_repository_by_id(state, pull_request.base_repo_id)?,
  787. };
  788. Ok(PullRequestResponse {
  789. pull_request,
  790. head_repo,
  791. base_repo,
  792. })
  793. }
  794. fn build_compare_for_pull_request(
  795. state: &AppState,
  796. base_repo: &RepositoryWithOwner,
  797. head_repo: &RepositoryWithOwner,
  798. pull_request: &PullRequest,
  799. ) -> AppResult<CompareResponse> {
  800. if pull_request.has_merged && !pull_request.merged_commit_id.is_empty() {
  801. let base_repo_path = repox::repository_path(
  802. &state.config.repository.root,
  803. &base_repo.owner.name,
  804. &base_repo.repo.name,
  805. );
  806. let result = git::compare_rev_range_with_binary(
  807. &state.config.repository.git_binary,
  808. &base_repo_path,
  809. &pull_request.merge_base,
  810. &pull_request.merged_commit_id,
  811. )?;
  812. Ok(map_compare_result(
  813. pull_request.base_branch.as_str(),
  814. pull_request.head_branch.as_str(),
  815. if result.files.is_empty() && result.commits.is_empty() {
  816. PullRequestStatus::Mergeable
  817. } else {
  818. pull_request.status
  819. },
  820. result,
  821. ))
  822. } else {
  823. build_compare_for_refs(
  824. state,
  825. base_repo,
  826. head_repo,
  827. &pull_request.base_branch,
  828. &pull_request.head_branch,
  829. )
  830. }
  831. }
  832. fn build_compare_for_refs(
  833. state: &AppState,
  834. base_repo: &RepositoryWithOwner,
  835. head_repo: &RepositoryWithOwner,
  836. base_branch: &str,
  837. head_branch: &str,
  838. ) -> AppResult<CompareResponse> {
  839. let base_repo_path = repox::repository_path(
  840. &state.config.repository.root,
  841. &base_repo.owner.name,
  842. &base_repo.repo.name,
  843. );
  844. if !git::branch_exists_with_binary(
  845. &state.config.repository.git_binary,
  846. &base_repo_path,
  847. base_branch,
  848. )? {
  849. return Err(AppError::NotFound(format!(
  850. "base branch not found: {base_branch}"
  851. )));
  852. }
  853. let head_repo_path = repox::repository_path(
  854. &state.config.repository.root,
  855. &head_repo.owner.name,
  856. &head_repo.repo.name,
  857. );
  858. if !git::branch_exists_with_binary(
  859. &state.config.repository.git_binary,
  860. &head_repo_path,
  861. head_branch,
  862. )? {
  863. return Err(AppError::NotFound(format!(
  864. "head branch not found: {head_branch}"
  865. )));
  866. }
  867. let result = git::compare_refs_with_binary(
  868. &state.config.repository.git_binary,
  869. &base_repo_path,
  870. base_branch,
  871. &head_repo_path,
  872. head_branch,
  873. )?;
  874. let status = if result.merge_base == result.head_commit_id {
  875. PullRequestStatus::Mergeable
  876. } else if git::test_merge_with_binary(
  877. &state.config.repository.git_binary,
  878. &base_repo_path,
  879. base_branch,
  880. &head_repo_path,
  881. head_branch,
  882. )? {
  883. PullRequestStatus::Mergeable
  884. } else {
  885. PullRequestStatus::Conflict
  886. };
  887. Ok(map_compare_result(base_branch, head_branch, status, result))
  888. }
  889. fn map_compare_result(
  890. base_branch: &str,
  891. head_branch: &str,
  892. status: PullRequestStatus,
  893. result: git::CompareResult,
  894. ) -> CompareResponse {
  895. let is_nothing_to_compare = result.merge_base == result.head_commit_id;
  896. CompareResponse {
  897. base_branch: base_branch.to_string(),
  898. head_branch: head_branch.to_string(),
  899. merge_base: result.merge_base,
  900. head_commit_id: result.head_commit_id,
  901. status,
  902. commits: result
  903. .commits
  904. .into_iter()
  905. .map(|commit| CompareCommit {
  906. id: commit.id,
  907. summary: commit.summary,
  908. author_name: commit.author_name,
  909. author_email: commit.author_email,
  910. })
  911. .collect(),
  912. files: result
  913. .files
  914. .into_iter()
  915. .map(|file| CompareFile {
  916. path: file.path,
  917. additions: file.additions,
  918. deletions: file.deletions,
  919. })
  920. .collect(),
  921. is_nothing_to_compare,
  922. }
  923. }
  924. fn ensure_repo_readable(
  925. state: &AppState,
  926. requesting_user: Option<&User>,
  927. repo: &RepositoryWithOwner,
  928. ) -> AppResult<()> {
  929. ensure_repo_access(state, requesting_user, repo, AccessMode::Read)
  930. }
  931. fn ensure_repo_access(
  932. state: &AppState,
  933. requesting_user: Option<&User>,
  934. repo: &RepositoryWithOwner,
  935. desired: AccessMode,
  936. ) -> AppResult<()> {
  937. let mode = effective_access_mode(state, requesting_user, repo)?;
  938. if (mode as i64) >= (desired as i64) {
  939. return Ok(());
  940. }
  941. if repo.repo.is_private {
  942. return Err(AppError::NotFound(format!(
  943. "repository not found: {}/{}",
  944. repo.owner.name, repo.repo.name
  945. )));
  946. }
  947. Err(AppError::Forbidden("repository access denied".to_string()))
  948. }
  949. fn ensure_pull_request_admin(
  950. state: &AppState,
  951. acting_user: &User,
  952. base_repo: &RepositoryWithOwner,
  953. pull: &PullRequest,
  954. ) -> AppResult<()> {
  955. let is_writer = state.db.authorize(
  956. acting_user.id,
  957. base_repo.repo.id,
  958. AccessMode::Write,
  959. base_repo.owner.id,
  960. base_repo.repo.is_private,
  961. )?;
  962. if is_writer || acting_user.id == pull.poster_id {
  963. return Ok(());
  964. }
  965. Err(AppError::Forbidden(
  966. "pull request status change denied".to_string(),
  967. ))
  968. }
  969. pub fn login_with_password(state: &AppState, login: &str, password: &str) -> AppResult<User> {
  970. authenticate_password(state, login, password)
  971. }
  972. fn authenticate_password(state: &AppState, login: &str, password: &str) -> AppResult<User> {
  973. let user = if login.contains('@') {
  974. state
  975. .db
  976. .get_user_by_email(login)?
  977. .ok_or_else(|| AppError::Unauthorized("invalid credentials".to_string()))?
  978. } else {
  979. state
  980. .db
  981. .get_user_by_username(login)?
  982. .ok_or_else(|| AppError::Unauthorized("invalid credentials".to_string()))?
  983. };
  984. if !user.is_active {
  985. return Err(AppError::Unauthorized("inactive user".to_string()));
  986. }
  987. let parsed_hash = PasswordHash::new(&user.password_hash)
  988. .map_err(|_| AppError::Unauthorized("invalid credentials".to_string()))?;
  989. Argon2::default()
  990. .verify_password(password.as_bytes(), &parsed_hash)
  991. .map_err(|_| AppError::Unauthorized("invalid credentials".to_string()))?;
  992. Ok(user)
  993. }
  994. fn is_token_expired(token: &crate::models::AccessToken) -> bool {
  995. crate::db::now_unix() - token.created_unix > TOKEN_TTL_DAYS * 86400
  996. }
  997. fn pagination(page: i64, per_page: i64) -> (i64, i64) {
  998. let page = page.max(1);
  999. let per_page = per_page.clamp(1, MAX_PER_PAGE);
  1000. let limit = per_page;
  1001. let offset = (page - 1) * per_page;
  1002. (limit, offset)
  1003. }
  1004. fn validate_user_name(name: &str) -> AppResult<()> {
  1005. validate_name(name, &[".git", ".wiki"], "username")
  1006. }
  1007. fn validate_repo_name(name: &str) -> AppResult<()> {
  1008. validate_name(name, &[".git", ".wiki"], "repository name")
  1009. }
  1010. fn validate_name(name: &str, forbidden_suffixes: &[&str], field: &str) -> AppResult<()> {
  1011. if name.is_empty() {
  1012. return Err(AppError::Validation(format!("{field} cannot be empty")));
  1013. }
  1014. if matches!(name, "." | "..") {
  1015. return Err(AppError::Validation(format!("{field} is reserved")));
  1016. }
  1017. if forbidden_suffixes
  1018. .iter()
  1019. .any(|suffix| name.ends_with(suffix))
  1020. {
  1021. return Err(AppError::Validation(format!("{field} has reserved suffix")));
  1022. }
  1023. if !name
  1024. .chars()
  1025. .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
  1026. {
  1027. return Err(AppError::Validation(format!(
  1028. "{field} must contain only ASCII letters, digits, '-', '_' or '.'"
  1029. )));
  1030. }
  1031. Ok(())
  1032. }
  1033. fn validate_email(email: &str) -> AppResult<()> {
  1034. if email.contains('@') && !email.starts_with('@') && !email.ends_with('@') {
  1035. return Ok(());
  1036. }
  1037. Err(AppError::Validation("email is invalid".to_string()))
  1038. }
  1039. fn ensure_repo_owner(acting_user: &User, repo: &RepositoryWithOwner) -> AppResult<()> {
  1040. if acting_user.id != repo.owner.id {
  1041. return Err(AppError::Forbidden(
  1042. "only repository owner can manage collaborators".to_string(),
  1043. ));
  1044. }
  1045. Ok(())
  1046. }
  1047. fn filter_visible_repositories(
  1048. state: &AppState,
  1049. requesting_user: Option<&User>,
  1050. repos: Vec<RepositoryWithOwner>,
  1051. query: &str,
  1052. ) -> AppResult<Vec<RepositoryWithOwner>> {
  1053. let query = query.trim().to_ascii_lowercase();
  1054. let mut visible = Vec::new();
  1055. for repo in repos {
  1056. let mode = effective_access_mode(state, requesting_user, &repo)?;
  1057. if mode == AccessMode::None {
  1058. continue;
  1059. }
  1060. if !query.is_empty() && !repository_matches_query(&repo, &query) {
  1061. continue;
  1062. }
  1063. visible.push(repo);
  1064. }
  1065. Ok(visible)
  1066. }
  1067. fn repository_matches_query(repo: &RepositoryWithOwner, query: &str) -> bool {
  1068. repo.repo.lower_name.contains(query)
  1069. || repo.owner.lower_name.contains(query)
  1070. || repo
  1071. .repo
  1072. .description
  1073. .to_ascii_lowercase()
  1074. .contains(query)
  1075. || format!("{}/{}", repo.owner.lower_name, repo.repo.lower_name).contains(query)
  1076. }
  1077. fn effective_access_mode(
  1078. state: &AppState,
  1079. requesting_user: Option<&User>,
  1080. repo: &RepositoryWithOwner,
  1081. ) -> AppResult<AccessMode> {
  1082. let user_id = requesting_user.map(|user| user.id).unwrap_or(0);
  1083. state
  1084. .db
  1085. .access_mode(user_id, repo.repo.id, repo.owner.id, repo.repo.is_private)
  1086. }
  1087. fn random_token() -> String {
  1088. let mut bytes = [0_u8; 32];
  1089. OsRng.fill_bytes(&mut bytes);
  1090. hex::encode(bytes)
  1091. }
  1092. fn hash_token(token: &str) -> String {
  1093. hex::encode(Sha256::digest(token.as_bytes()))
  1094. }