git.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. use std::{
  2. fs,
  3. io::Write,
  4. path::Path,
  5. process::{Command, Stdio},
  6. };
  7. use crate::error::{AppError, AppResult};
  8. #[derive(Debug, Clone)]
  9. pub struct CompareCommit {
  10. pub id: String,
  11. pub summary: String,
  12. pub author_name: String,
  13. pub author_email: String,
  14. }
  15. #[derive(Debug, Clone)]
  16. pub struct CompareFile {
  17. pub path: String,
  18. pub additions: i64,
  19. pub deletions: i64,
  20. }
  21. #[derive(Debug, Clone)]
  22. pub struct CompareResult {
  23. pub merge_base: String,
  24. pub head_commit_id: String,
  25. pub commits: Vec<CompareCommit>,
  26. pub files: Vec<CompareFile>,
  27. }
  28. pub fn init_bare_repo_with_binary(
  29. git_binary: &str,
  30. repo_path: &Path,
  31. default_branch: &str,
  32. ) -> AppResult<()> {
  33. if let Some(parent) = repo_path.parent() {
  34. fs::create_dir_all(parent)?;
  35. }
  36. run(Command::new(git_binary)
  37. .arg("init")
  38. .arg("--bare")
  39. .arg(repo_path))?;
  40. run(Command::new(git_binary)
  41. .arg("--git-dir")
  42. .arg(repo_path)
  43. .arg("symbolic-ref")
  44. .arg("HEAD")
  45. .arg(format!("refs/heads/{default_branch}")))?;
  46. run(Command::new(git_binary)
  47. .arg("--git-dir")
  48. .arg(repo_path)
  49. .arg("update-server-info"))?;
  50. Ok(())
  51. }
  52. pub fn create_initial_commit_with_binary(
  53. git_binary: &str,
  54. repo_path: &Path,
  55. default_branch: &str,
  56. readme: &str,
  57. author_name: &str,
  58. author_email: &str,
  59. ) -> AppResult<()> {
  60. let worktree = repo_path.with_extension("work");
  61. if worktree.exists() {
  62. fs::remove_dir_all(&worktree)?;
  63. }
  64. fs::create_dir_all(&worktree)?;
  65. fs::write(worktree.join("README.md"), readme)?;
  66. run(Command::new(git_binary)
  67. .arg("init")
  68. .arg("--initial-branch")
  69. .arg(default_branch)
  70. .arg(&worktree))?;
  71. run(Command::new(git_binary)
  72. .current_dir(&worktree)
  73. .env("GIT_AUTHOR_NAME", author_name)
  74. .env("GIT_AUTHOR_EMAIL", author_email)
  75. .env("GIT_COMMITTER_NAME", author_name)
  76. .env("GIT_COMMITTER_EMAIL", author_email)
  77. .arg("add")
  78. .arg("README.md"))?;
  79. run(Command::new(git_binary)
  80. .current_dir(&worktree)
  81. .env("GIT_AUTHOR_NAME", author_name)
  82. .env("GIT_AUTHOR_EMAIL", author_email)
  83. .env("GIT_COMMITTER_NAME", author_name)
  84. .env("GIT_COMMITTER_EMAIL", author_email)
  85. .arg("commit")
  86. .arg("-m")
  87. .arg("Initial commit"))?;
  88. run(Command::new(git_binary)
  89. .current_dir(&worktree)
  90. .arg("remote")
  91. .arg("add")
  92. .arg("origin")
  93. .arg(repo_path))?;
  94. run(Command::new(git_binary)
  95. .current_dir(&worktree)
  96. .arg("push")
  97. .arg("origin")
  98. .arg(default_branch))?;
  99. fs::remove_dir_all(&worktree)?;
  100. Ok(())
  101. }
  102. pub fn clone_bare_repo_with_binary(
  103. git_binary: &str,
  104. source_repo_path: &Path,
  105. target_repo_path: &Path,
  106. ) -> AppResult<()> {
  107. if let Some(parent) = target_repo_path.parent() {
  108. fs::create_dir_all(parent)?;
  109. }
  110. run(Command::new(git_binary)
  111. .arg("clone")
  112. .arg("--bare")
  113. .arg(source_repo_path)
  114. .arg(target_repo_path))?;
  115. run(Command::new(git_binary)
  116. .arg("--git-dir")
  117. .arg(target_repo_path)
  118. .arg("update-server-info"))?;
  119. Ok(())
  120. }
  121. pub fn list_branches_with_binary(git_binary: &str, repo_path: &Path) -> AppResult<Vec<String>> {
  122. let output = Command::new(git_binary)
  123. .arg("--git-dir")
  124. .arg(repo_path)
  125. .arg("for-each-ref")
  126. .arg("--format=%(refname:short)")
  127. .arg("refs/heads")
  128. .output()
  129. .map_err(|err| AppError::Git(err.to_string()))?;
  130. if !output.status.success() {
  131. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  132. return Err(AppError::Git(stderr));
  133. }
  134. let stdout = String::from_utf8_lossy(&output.stdout);
  135. Ok(stdout
  136. .lines()
  137. .map(str::trim)
  138. .filter(|line| !line.is_empty())
  139. .map(ToOwned::to_owned)
  140. .collect())
  141. }
  142. pub fn branch_exists_with_binary(
  143. git_binary: &str,
  144. repo_path: &Path,
  145. branch: &str,
  146. ) -> AppResult<bool> {
  147. let branches = list_branches_with_binary(git_binary, repo_path)?;
  148. Ok(branches.iter().any(|name| name == branch))
  149. }
  150. pub fn merge_base_with_binary(
  151. git_binary: &str,
  152. base_repo_path: &Path,
  153. base_branch: &str,
  154. head_repo_path: &Path,
  155. head_branch: &str,
  156. ) -> AppResult<String> {
  157. let temp = std::env::temp_dir().join(format!(
  158. "gitr-merge-base-{}",
  159. std::time::SystemTime::now()
  160. .duration_since(std::time::UNIX_EPOCH)
  161. .unwrap_or_default()
  162. .as_nanos()
  163. ));
  164. run(Command::new(git_binary)
  165. .arg("clone")
  166. .arg("--bare")
  167. .arg(base_repo_path)
  168. .arg(&temp))?;
  169. let result = (|| -> AppResult<String> {
  170. run(Command::new(git_binary)
  171. .arg("--git-dir")
  172. .arg(&temp)
  173. .arg("remote")
  174. .arg("add")
  175. .arg("head_repo")
  176. .arg(head_repo_path))?;
  177. run(Command::new(git_binary)
  178. .arg("--git-dir")
  179. .arg(&temp)
  180. .arg("fetch")
  181. .arg("head_repo")
  182. .arg(head_branch))?;
  183. let output = Command::new(git_binary)
  184. .arg("--git-dir")
  185. .arg(&temp)
  186. .arg("merge-base")
  187. .arg(format!("refs/heads/{base_branch}"))
  188. .arg("FETCH_HEAD")
  189. .output()
  190. .map_err(|err| AppError::Git(err.to_string()))?;
  191. if !output.status.success() {
  192. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  193. return Err(AppError::Git(stderr));
  194. }
  195. Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
  196. })();
  197. let _ = fs::remove_dir_all(&temp);
  198. result
  199. }
  200. pub fn test_merge_with_binary(
  201. git_binary: &str,
  202. base_repo_path: &Path,
  203. base_branch: &str,
  204. head_repo_path: &Path,
  205. head_branch: &str,
  206. ) -> AppResult<bool> {
  207. let temp = temp_git_path("gitr-test-merge");
  208. run(Command::new(git_binary)
  209. .arg("clone")
  210. .arg("-b")
  211. .arg(base_branch)
  212. .arg(base_repo_path)
  213. .arg(&temp))?;
  214. let result = (|| -> AppResult<bool> {
  215. run(Command::new(git_binary)
  216. .current_dir(&temp)
  217. .arg("remote")
  218. .arg("add")
  219. .arg("head_repo")
  220. .arg(head_repo_path))?;
  221. run(Command::new(git_binary)
  222. .current_dir(&temp)
  223. .arg("fetch")
  224. .arg("head_repo")
  225. .arg(head_branch))?;
  226. let output = Command::new(git_binary)
  227. .current_dir(&temp)
  228. .arg("merge")
  229. .arg("--no-ff")
  230. .arg("--no-commit")
  231. .arg("FETCH_HEAD")
  232. .output()
  233. .map_err(|err| AppError::Git(err.to_string()))?;
  234. Ok(output.status.success())
  235. })();
  236. let _ = fs::remove_dir_all(&temp);
  237. result
  238. }
  239. pub fn merge_pull_request_with_binary(
  240. git_binary: &str,
  241. base_repo_path: &Path,
  242. base_branch: &str,
  243. head_repo_path: &Path,
  244. head_branch: &str,
  245. author_name: &str,
  246. author_email: &str,
  247. message: &str,
  248. ) -> AppResult<String> {
  249. let temp = temp_git_path("gitr-merge-pr");
  250. run(Command::new(git_binary)
  251. .arg("clone")
  252. .arg("-b")
  253. .arg(base_branch)
  254. .arg(base_repo_path)
  255. .arg(&temp))?;
  256. let result = (|| -> AppResult<String> {
  257. run(Command::new(git_binary)
  258. .current_dir(&temp)
  259. .arg("remote")
  260. .arg("add")
  261. .arg("head_repo")
  262. .arg(head_repo_path))?;
  263. run(Command::new(git_binary)
  264. .current_dir(&temp)
  265. .arg("fetch")
  266. .arg("head_repo")
  267. .arg(head_branch))?;
  268. let head_commit_id = rev_parse_with_binary(git_binary, &temp.join(".git"), "FETCH_HEAD")?;
  269. let merge = Command::new(git_binary)
  270. .current_dir(&temp)
  271. .arg("merge")
  272. .arg("--no-ff")
  273. .arg("--no-commit")
  274. .arg("FETCH_HEAD")
  275. .output()
  276. .map_err(|err| AppError::Git(err.to_string()))?;
  277. if !merge.status.success() {
  278. let stderr = String::from_utf8_lossy(&merge.stderr).trim().to_string();
  279. let stdout = String::from_utf8_lossy(&merge.stdout).trim().to_string();
  280. let detail = if !stderr.is_empty() { stderr } else { stdout };
  281. return Err(AppError::Conflict(format!(
  282. "pull request cannot be merged automatically: {detail}"
  283. )));
  284. }
  285. run(Command::new(git_binary)
  286. .current_dir(&temp)
  287. .env("GIT_AUTHOR_NAME", author_name)
  288. .env("GIT_AUTHOR_EMAIL", author_email)
  289. .env("GIT_COMMITTER_NAME", author_name)
  290. .env("GIT_COMMITTER_EMAIL", author_email)
  291. .arg("commit")
  292. .arg("-m")
  293. .arg(message))?;
  294. run(Command::new(git_binary)
  295. .current_dir(&temp)
  296. .arg("push")
  297. .arg("origin")
  298. .arg(base_branch))?;
  299. run(Command::new(git_binary)
  300. .arg("--git-dir")
  301. .arg(base_repo_path)
  302. .arg("update-server-info"))?;
  303. Ok(head_commit_id)
  304. })();
  305. let _ = fs::remove_dir_all(&temp);
  306. result
  307. }
  308. pub fn compare_refs_with_binary(
  309. git_binary: &str,
  310. base_repo_path: &Path,
  311. base_branch: &str,
  312. head_repo_path: &Path,
  313. head_branch: &str,
  314. ) -> AppResult<CompareResult> {
  315. let temp = temp_git_path("gitr-compare-refs");
  316. run(Command::new(git_binary)
  317. .arg("clone")
  318. .arg("--bare")
  319. .arg(base_repo_path)
  320. .arg(&temp))?;
  321. let result = (|| -> AppResult<CompareResult> {
  322. run(Command::new(git_binary)
  323. .arg("--git-dir")
  324. .arg(&temp)
  325. .arg("remote")
  326. .arg("add")
  327. .arg("head_repo")
  328. .arg(head_repo_path))?;
  329. run(Command::new(git_binary)
  330. .arg("--git-dir")
  331. .arg(&temp)
  332. .arg("fetch")
  333. .arg("head_repo")
  334. .arg(head_branch))?;
  335. let merge_base = merge_base_for_repo(git_binary, &temp, base_branch, "FETCH_HEAD")?;
  336. let head_commit_id = rev_parse_with_binary(git_binary, &temp, "FETCH_HEAD")?;
  337. compare_rev_range_with_binary(git_binary, &temp, &merge_base, &head_commit_id)
  338. })();
  339. let _ = fs::remove_dir_all(&temp);
  340. result
  341. }
  342. pub fn compare_rev_range_with_binary(
  343. git_binary: &str,
  344. repo_path: &Path,
  345. merge_base: &str,
  346. head_commit_id: &str,
  347. ) -> AppResult<CompareResult> {
  348. let commits =
  349. list_commits_between_with_binary(git_binary, repo_path, merge_base, head_commit_id)?;
  350. let files = if merge_base == head_commit_id {
  351. Vec::new()
  352. } else {
  353. diff_numstat_with_binary(git_binary, repo_path, merge_base, head_commit_id)?
  354. };
  355. Ok(CompareResult {
  356. merge_base: merge_base.to_string(),
  357. head_commit_id: head_commit_id.to_string(),
  358. commits,
  359. files,
  360. })
  361. }
  362. pub fn rev_parse_with_binary(git_binary: &str, repo_path: &Path, rev: &str) -> AppResult<String> {
  363. let output = Command::new(git_binary)
  364. .arg("--git-dir")
  365. .arg(repo_path)
  366. .arg("rev-parse")
  367. .arg(rev)
  368. .output()
  369. .map_err(|err| AppError::Git(err.to_string()))?;
  370. if !output.status.success() {
  371. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  372. return Err(AppError::Git(stderr));
  373. }
  374. Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
  375. }
  376. fn run(command: &mut Command) -> AppResult<()> {
  377. let output = command
  378. .output()
  379. .map_err(|err| AppError::Git(err.to_string()))?;
  380. if output.status.success() {
  381. return Ok(());
  382. }
  383. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  384. let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
  385. let message = if !stderr.is_empty() { stderr } else { stdout };
  386. Err(AppError::Git(message))
  387. }
  388. fn temp_git_path(prefix: &str) -> std::path::PathBuf {
  389. std::env::temp_dir().join(format!(
  390. "{prefix}-{}",
  391. std::time::SystemTime::now()
  392. .duration_since(std::time::UNIX_EPOCH)
  393. .unwrap_or_default()
  394. .as_nanos()
  395. ))
  396. }
  397. fn merge_base_for_repo(
  398. git_binary: &str,
  399. repo_path: &Path,
  400. base_branch: &str,
  401. head_rev: &str,
  402. ) -> AppResult<String> {
  403. let output = Command::new(git_binary)
  404. .arg("--git-dir")
  405. .arg(repo_path)
  406. .arg("merge-base")
  407. .arg(format!("refs/heads/{base_branch}"))
  408. .arg(head_rev)
  409. .output()
  410. .map_err(|err| AppError::Git(err.to_string()))?;
  411. if !output.status.success() {
  412. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  413. return Err(AppError::Git(stderr));
  414. }
  415. Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
  416. }
  417. fn list_commits_between_with_binary(
  418. git_binary: &str,
  419. repo_path: &Path,
  420. merge_base: &str,
  421. head_commit_id: &str,
  422. ) -> AppResult<Vec<CompareCommit>> {
  423. if merge_base == head_commit_id {
  424. return Ok(Vec::new());
  425. }
  426. let output = Command::new(git_binary)
  427. .arg("--git-dir")
  428. .arg(repo_path)
  429. .arg("log")
  430. .arg("--reverse")
  431. .arg("--format=%H%x1f%s%x1f%an%x1f%ae%x1e")
  432. .arg(format!("{merge_base}..{head_commit_id}"))
  433. .output()
  434. .map_err(|err| AppError::Git(err.to_string()))?;
  435. if !output.status.success() {
  436. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  437. return Err(AppError::Git(stderr));
  438. }
  439. let stdout = String::from_utf8_lossy(&output.stdout);
  440. let mut commits = Vec::new();
  441. for record in stdout.split('\x1e') {
  442. let record = record.trim();
  443. if record.is_empty() {
  444. continue;
  445. }
  446. let mut fields = record.split('\x1f');
  447. let id = fields.next().unwrap_or_default().to_string();
  448. let summary = fields.next().unwrap_or_default().to_string();
  449. let author_name = fields.next().unwrap_or_default().to_string();
  450. let author_email = fields.next().unwrap_or_default().to_string();
  451. commits.push(CompareCommit {
  452. id,
  453. summary,
  454. author_name,
  455. author_email,
  456. });
  457. }
  458. Ok(commits)
  459. }
  460. fn diff_numstat_with_binary(
  461. git_binary: &str,
  462. repo_path: &Path,
  463. merge_base: &str,
  464. head_commit_id: &str,
  465. ) -> AppResult<Vec<CompareFile>> {
  466. let output = Command::new(git_binary)
  467. .arg("--git-dir")
  468. .arg(repo_path)
  469. .arg("diff")
  470. .arg("--numstat")
  471. .arg(merge_base)
  472. .arg(head_commit_id)
  473. .output()
  474. .map_err(|err| AppError::Git(err.to_string()))?;
  475. if !output.status.success() {
  476. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  477. return Err(AppError::Git(stderr));
  478. }
  479. let stdout = String::from_utf8_lossy(&output.stdout);
  480. let mut files = Vec::new();
  481. for line in stdout.lines() {
  482. if line.trim().is_empty() {
  483. continue;
  484. }
  485. let mut parts = line.splitn(3, '\t');
  486. let additions = parse_numstat_value(parts.next().unwrap_or_default());
  487. let deletions = parse_numstat_value(parts.next().unwrap_or_default());
  488. let path = parts.next().unwrap_or_default().to_string();
  489. files.push(CompareFile {
  490. path,
  491. additions,
  492. deletions,
  493. });
  494. }
  495. Ok(files)
  496. }
  497. fn parse_numstat_value(value: &str) -> i64 {
  498. value.parse::<i64>().unwrap_or(0)
  499. }
  500. pub struct GitHttpBackendRequest<'a> {
  501. pub git_binary: &'a str,
  502. pub project_root: &'a Path,
  503. pub path_info: &'a str,
  504. pub method: &'a str,
  505. pub query_string: &'a str,
  506. pub content_type: Option<&'a str>,
  507. pub remote_user: Option<&'a str>,
  508. pub body: &'a [u8],
  509. }
  510. pub struct GitHttpBackendResponse {
  511. pub status_code: u16,
  512. pub headers: Vec<(String, String)>,
  513. pub body: Vec<u8>,
  514. }
  515. pub fn run_git_http_backend(req: GitHttpBackendRequest<'_>) -> AppResult<GitHttpBackendResponse> {
  516. let mut command = Command::new(req.git_binary);
  517. command
  518. .arg("http-backend")
  519. .env("GIT_PROJECT_ROOT", req.project_root)
  520. .env("GIT_HTTP_EXPORT_ALL", "1")
  521. .env("PATH_INFO", req.path_info)
  522. .env("REQUEST_METHOD", req.method)
  523. .env("QUERY_STRING", req.query_string)
  524. .env("CONTENT_LENGTH", req.body.len().to_string())
  525. .stdin(Stdio::piped())
  526. .stdout(Stdio::piped())
  527. .stderr(Stdio::piped());
  528. if let Some(content_type) = req.content_type {
  529. command.env("CONTENT_TYPE", content_type);
  530. }
  531. if let Some(remote_user) = req.remote_user {
  532. command.env("REMOTE_USER", remote_user);
  533. }
  534. let mut child = command
  535. .spawn()
  536. .map_err(|err| AppError::Git(err.to_string()))?;
  537. if let Some(mut stdin) = child.stdin.take() {
  538. stdin.write_all(req.body)?;
  539. }
  540. let output = child
  541. .wait_with_output()
  542. .map_err(|err| AppError::Git(err.to_string()))?;
  543. if !output.status.success() {
  544. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  545. return Err(AppError::Git(stderr));
  546. }
  547. parse_git_http_backend_output(&output.stdout)
  548. }
  549. fn parse_git_http_backend_output(stdout: &[u8]) -> AppResult<GitHttpBackendResponse> {
  550. let split = stdout
  551. .windows(4)
  552. .position(|w| w == b"\r\n\r\n")
  553. .map(|i| (i, 4))
  554. .or_else(|| stdout.windows(2).position(|w| w == b"\n\n").map(|i| (i, 2)))
  555. .ok_or_else(|| AppError::Git("invalid git-http-backend response".to_string()))?;
  556. let header_bytes = &stdout[..split.0];
  557. let body = stdout[split.0 + split.1..].to_vec();
  558. let header_text = String::from_utf8_lossy(header_bytes);
  559. let mut status_code = 200;
  560. let mut headers = Vec::new();
  561. for line in header_text.lines() {
  562. if line.is_empty() {
  563. continue;
  564. }
  565. if let Some(value) = line.strip_prefix("Status:") {
  566. let code = value
  567. .split_whitespace()
  568. .next()
  569. .and_then(|v| v.parse::<u16>().ok())
  570. .ok_or_else(|| AppError::Git(format!("invalid status line: {line}")))?;
  571. status_code = code;
  572. continue;
  573. }
  574. let (name, value) = line
  575. .split_once(':')
  576. .ok_or_else(|| AppError::Git(format!("invalid header line: {line}")))?;
  577. headers.push((name.trim().to_string(), value.trim().to_string()));
  578. }
  579. Ok(GitHttpBackendResponse {
  580. status_code,
  581. headers,
  582. body,
  583. })
  584. }