service.rs 35 KB

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