| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- use std::{
- fs,
- io::Write,
- path::Path,
- process::{Command, Stdio},
- };
- use crate::error::{AppError, AppResult};
- #[derive(Debug, Clone)]
- pub struct CompareCommit {
- pub id: String,
- pub summary: String,
- pub author_name: String,
- pub author_email: String,
- }
- #[derive(Debug, Clone)]
- pub struct CompareFile {
- pub path: String,
- pub additions: i64,
- pub deletions: i64,
- }
- #[derive(Debug, Clone)]
- pub struct CompareResult {
- pub merge_base: String,
- pub head_commit_id: String,
- pub commits: Vec<CompareCommit>,
- pub files: Vec<CompareFile>,
- }
- pub fn init_bare_repo_with_binary(
- git_binary: &str,
- repo_path: &Path,
- default_branch: &str,
- ) -> AppResult<()> {
- if let Some(parent) = repo_path.parent() {
- fs::create_dir_all(parent)?;
- }
- run(Command::new(git_binary)
- .arg("init")
- .arg("--bare")
- .arg(repo_path))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("symbolic-ref")
- .arg("HEAD")
- .arg(format!("refs/heads/{default_branch}")))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("update-server-info"))?;
- Ok(())
- }
- pub fn create_initial_commit_with_binary(
- git_binary: &str,
- repo_path: &Path,
- default_branch: &str,
- readme: &str,
- author_name: &str,
- author_email: &str,
- ) -> AppResult<()> {
- let worktree = repo_path.with_extension("work");
- if worktree.exists() {
- fs::remove_dir_all(&worktree)?;
- }
- fs::create_dir_all(&worktree)?;
- fs::write(worktree.join("README.md"), readme)?;
- run(Command::new(git_binary)
- .arg("init")
- .arg("--initial-branch")
- .arg(default_branch)
- .arg(&worktree))?;
- run(Command::new(git_binary)
- .current_dir(&worktree)
- .env("GIT_AUTHOR_NAME", author_name)
- .env("GIT_AUTHOR_EMAIL", author_email)
- .env("GIT_COMMITTER_NAME", author_name)
- .env("GIT_COMMITTER_EMAIL", author_email)
- .arg("add")
- .arg("README.md"))?;
- run(Command::new(git_binary)
- .current_dir(&worktree)
- .env("GIT_AUTHOR_NAME", author_name)
- .env("GIT_AUTHOR_EMAIL", author_email)
- .env("GIT_COMMITTER_NAME", author_name)
- .env("GIT_COMMITTER_EMAIL", author_email)
- .arg("commit")
- .arg("-m")
- .arg("Initial commit"))?;
- run(Command::new(git_binary)
- .current_dir(&worktree)
- .arg("remote")
- .arg("add")
- .arg("origin")
- .arg(repo_path))?;
- run(Command::new(git_binary)
- .current_dir(&worktree)
- .arg("push")
- .arg("origin")
- .arg(default_branch))?;
- fs::remove_dir_all(&worktree)?;
- Ok(())
- }
- pub fn clone_bare_repo_with_binary(
- git_binary: &str,
- source_repo_path: &Path,
- target_repo_path: &Path,
- ) -> AppResult<()> {
- if let Some(parent) = target_repo_path.parent() {
- fs::create_dir_all(parent)?;
- }
- run(Command::new(git_binary)
- .arg("clone")
- .arg("--bare")
- .arg(source_repo_path)
- .arg(target_repo_path))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(target_repo_path)
- .arg("update-server-info"))?;
- Ok(())
- }
- pub fn list_branches_with_binary(git_binary: &str, repo_path: &Path) -> AppResult<Vec<String>> {
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("for-each-ref")
- .arg("--format=%(refname:short)")
- .arg("refs/heads")
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- Ok(stdout
- .lines()
- .map(str::trim)
- .filter(|line| !line.is_empty())
- .map(ToOwned::to_owned)
- .collect())
- }
- pub fn branch_exists_with_binary(
- git_binary: &str,
- repo_path: &Path,
- branch: &str,
- ) -> AppResult<bool> {
- let branches = list_branches_with_binary(git_binary, repo_path)?;
- Ok(branches.iter().any(|name| name == branch))
- }
- pub fn merge_base_with_binary(
- git_binary: &str,
- base_repo_path: &Path,
- base_branch: &str,
- head_repo_path: &Path,
- head_branch: &str,
- ) -> AppResult<String> {
- let temp = std::env::temp_dir().join(format!(
- "gitr-merge-base-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_nanos()
- ));
- run(Command::new(git_binary)
- .arg("clone")
- .arg("--bare")
- .arg(base_repo_path)
- .arg(&temp))?;
- let result = (|| -> AppResult<String> {
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(&temp)
- .arg("remote")
- .arg("add")
- .arg("head_repo")
- .arg(head_repo_path))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(&temp)
- .arg("fetch")
- .arg("head_repo")
- .arg(head_branch))?;
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(&temp)
- .arg("merge-base")
- .arg(format!("refs/heads/{base_branch}"))
- .arg("FETCH_HEAD")
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
- })();
- let _ = fs::remove_dir_all(&temp);
- result
- }
- pub fn test_merge_with_binary(
- git_binary: &str,
- base_repo_path: &Path,
- base_branch: &str,
- head_repo_path: &Path,
- head_branch: &str,
- ) -> AppResult<bool> {
- let temp = temp_git_path("gitr-test-merge");
- run(Command::new(git_binary)
- .arg("clone")
- .arg("-b")
- .arg(base_branch)
- .arg(base_repo_path)
- .arg(&temp))?;
- let result = (|| -> AppResult<bool> {
- run(Command::new(git_binary)
- .current_dir(&temp)
- .arg("remote")
- .arg("add")
- .arg("head_repo")
- .arg(head_repo_path))?;
- run(Command::new(git_binary)
- .current_dir(&temp)
- .arg("fetch")
- .arg("head_repo")
- .arg(head_branch))?;
- let output = Command::new(git_binary)
- .current_dir(&temp)
- .arg("merge")
- .arg("--no-ff")
- .arg("--no-commit")
- .arg("FETCH_HEAD")
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- Ok(output.status.success())
- })();
- let _ = fs::remove_dir_all(&temp);
- result
- }
- pub fn merge_pull_request_with_binary(
- git_binary: &str,
- base_repo_path: &Path,
- base_branch: &str,
- head_repo_path: &Path,
- head_branch: &str,
- author_name: &str,
- author_email: &str,
- message: &str,
- ) -> AppResult<String> {
- let temp = temp_git_path("gitr-merge-pr");
- run(Command::new(git_binary)
- .arg("clone")
- .arg("-b")
- .arg(base_branch)
- .arg(base_repo_path)
- .arg(&temp))?;
- let result = (|| -> AppResult<String> {
- run(Command::new(git_binary)
- .current_dir(&temp)
- .arg("remote")
- .arg("add")
- .arg("head_repo")
- .arg(head_repo_path))?;
- run(Command::new(git_binary)
- .current_dir(&temp)
- .arg("fetch")
- .arg("head_repo")
- .arg(head_branch))?;
- let head_commit_id = rev_parse_with_binary(git_binary, &temp.join(".git"), "FETCH_HEAD")?;
- let merge = Command::new(git_binary)
- .current_dir(&temp)
- .arg("merge")
- .arg("--no-ff")
- .arg("--no-commit")
- .arg("FETCH_HEAD")
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !merge.status.success() {
- let stderr = String::from_utf8_lossy(&merge.stderr).trim().to_string();
- let stdout = String::from_utf8_lossy(&merge.stdout).trim().to_string();
- let detail = if !stderr.is_empty() { stderr } else { stdout };
- return Err(AppError::Conflict(format!(
- "pull request cannot be merged automatically: {detail}"
- )));
- }
- run(Command::new(git_binary)
- .current_dir(&temp)
- .env("GIT_AUTHOR_NAME", author_name)
- .env("GIT_AUTHOR_EMAIL", author_email)
- .env("GIT_COMMITTER_NAME", author_name)
- .env("GIT_COMMITTER_EMAIL", author_email)
- .arg("commit")
- .arg("-m")
- .arg(message))?;
- run(Command::new(git_binary)
- .current_dir(&temp)
- .arg("push")
- .arg("origin")
- .arg(base_branch))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(base_repo_path)
- .arg("update-server-info"))?;
- Ok(head_commit_id)
- })();
- let _ = fs::remove_dir_all(&temp);
- result
- }
- pub fn compare_refs_with_binary(
- git_binary: &str,
- base_repo_path: &Path,
- base_branch: &str,
- head_repo_path: &Path,
- head_branch: &str,
- ) -> AppResult<CompareResult> {
- let temp = temp_git_path("gitr-compare-refs");
- run(Command::new(git_binary)
- .arg("clone")
- .arg("--bare")
- .arg(base_repo_path)
- .arg(&temp))?;
- let result = (|| -> AppResult<CompareResult> {
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(&temp)
- .arg("remote")
- .arg("add")
- .arg("head_repo")
- .arg(head_repo_path))?;
- run(Command::new(git_binary)
- .arg("--git-dir")
- .arg(&temp)
- .arg("fetch")
- .arg("head_repo")
- .arg(head_branch))?;
- let merge_base = merge_base_for_repo(git_binary, &temp, base_branch, "FETCH_HEAD")?;
- let head_commit_id = rev_parse_with_binary(git_binary, &temp, "FETCH_HEAD")?;
- compare_rev_range_with_binary(git_binary, &temp, &merge_base, &head_commit_id)
- })();
- let _ = fs::remove_dir_all(&temp);
- result
- }
- pub fn compare_rev_range_with_binary(
- git_binary: &str,
- repo_path: &Path,
- merge_base: &str,
- head_commit_id: &str,
- ) -> AppResult<CompareResult> {
- let commits =
- list_commits_between_with_binary(git_binary, repo_path, merge_base, head_commit_id)?;
- let files = if merge_base == head_commit_id {
- Vec::new()
- } else {
- diff_numstat_with_binary(git_binary, repo_path, merge_base, head_commit_id)?
- };
- Ok(CompareResult {
- merge_base: merge_base.to_string(),
- head_commit_id: head_commit_id.to_string(),
- commits,
- files,
- })
- }
- pub fn rev_parse_with_binary(git_binary: &str, repo_path: &Path, rev: &str) -> AppResult<String> {
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("rev-parse")
- .arg(rev)
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
- }
- fn run(command: &mut Command) -> AppResult<()> {
- let output = command
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if output.status.success() {
- return Ok(());
- }
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
- let message = if !stderr.is_empty() { stderr } else { stdout };
- Err(AppError::Git(message))
- }
- fn temp_git_path(prefix: &str) -> std::path::PathBuf {
- std::env::temp_dir().join(format!(
- "{prefix}-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_nanos()
- ))
- }
- fn merge_base_for_repo(
- git_binary: &str,
- repo_path: &Path,
- base_branch: &str,
- head_rev: &str,
- ) -> AppResult<String> {
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("merge-base")
- .arg(format!("refs/heads/{base_branch}"))
- .arg(head_rev)
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
- }
- fn list_commits_between_with_binary(
- git_binary: &str,
- repo_path: &Path,
- merge_base: &str,
- head_commit_id: &str,
- ) -> AppResult<Vec<CompareCommit>> {
- if merge_base == head_commit_id {
- return Ok(Vec::new());
- }
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("log")
- .arg("--reverse")
- .arg("--format=%H%x1f%s%x1f%an%x1f%ae%x1e")
- .arg(format!("{merge_base}..{head_commit_id}"))
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- let mut commits = Vec::new();
- for record in stdout.split('\x1e') {
- let record = record.trim();
- if record.is_empty() {
- continue;
- }
- let mut fields = record.split('\x1f');
- let id = fields.next().unwrap_or_default().to_string();
- let summary = fields.next().unwrap_or_default().to_string();
- let author_name = fields.next().unwrap_or_default().to_string();
- let author_email = fields.next().unwrap_or_default().to_string();
- commits.push(CompareCommit {
- id,
- summary,
- author_name,
- author_email,
- });
- }
- Ok(commits)
- }
- fn diff_numstat_with_binary(
- git_binary: &str,
- repo_path: &Path,
- merge_base: &str,
- head_commit_id: &str,
- ) -> AppResult<Vec<CompareFile>> {
- let output = Command::new(git_binary)
- .arg("--git-dir")
- .arg(repo_path)
- .arg("diff")
- .arg("--numstat")
- .arg(merge_base)
- .arg(head_commit_id)
- .output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- let mut files = Vec::new();
- for line in stdout.lines() {
- if line.trim().is_empty() {
- continue;
- }
- let mut parts = line.splitn(3, '\t');
- let additions = parse_numstat_value(parts.next().unwrap_or_default());
- let deletions = parse_numstat_value(parts.next().unwrap_or_default());
- let path = parts.next().unwrap_or_default().to_string();
- files.push(CompareFile {
- path,
- additions,
- deletions,
- });
- }
- Ok(files)
- }
- fn parse_numstat_value(value: &str) -> i64 {
- value.parse::<i64>().unwrap_or(0)
- }
- pub struct GitHttpBackendRequest<'a> {
- pub git_binary: &'a str,
- pub project_root: &'a Path,
- pub path_info: &'a str,
- pub method: &'a str,
- pub query_string: &'a str,
- pub content_type: Option<&'a str>,
- pub remote_user: Option<&'a str>,
- pub body: &'a [u8],
- }
- pub struct GitHttpBackendResponse {
- pub status_code: u16,
- pub headers: Vec<(String, String)>,
- pub body: Vec<u8>,
- }
- pub fn run_git_http_backend(req: GitHttpBackendRequest<'_>) -> AppResult<GitHttpBackendResponse> {
- let mut command = Command::new(req.git_binary);
- command
- .arg("http-backend")
- .env("GIT_PROJECT_ROOT", req.project_root)
- .env("GIT_HTTP_EXPORT_ALL", "1")
- .env("PATH_INFO", req.path_info)
- .env("REQUEST_METHOD", req.method)
- .env("QUERY_STRING", req.query_string)
- .env("CONTENT_LENGTH", req.body.len().to_string())
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped());
- if let Some(content_type) = req.content_type {
- command.env("CONTENT_TYPE", content_type);
- }
- if let Some(remote_user) = req.remote_user {
- command.env("REMOTE_USER", remote_user);
- }
- let mut child = command
- .spawn()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if let Some(mut stdin) = child.stdin.take() {
- stdin.write_all(req.body)?;
- }
- let output = child
- .wait_with_output()
- .map_err(|err| AppError::Git(err.to_string()))?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
- return Err(AppError::Git(stderr));
- }
- parse_git_http_backend_output(&output.stdout)
- }
- fn parse_git_http_backend_output(stdout: &[u8]) -> AppResult<GitHttpBackendResponse> {
- let split = stdout
- .windows(4)
- .position(|w| w == b"\r\n\r\n")
- .map(|i| (i, 4))
- .or_else(|| stdout.windows(2).position(|w| w == b"\n\n").map(|i| (i, 2)))
- .ok_or_else(|| AppError::Git("invalid git-http-backend response".to_string()))?;
- let header_bytes = &stdout[..split.0];
- let body = stdout[split.0 + split.1..].to_vec();
- let header_text = String::from_utf8_lossy(header_bytes);
- let mut status_code = 200;
- let mut headers = Vec::new();
- for line in header_text.lines() {
- if line.is_empty() {
- continue;
- }
- if let Some(value) = line.strip_prefix("Status:") {
- let code = value
- .split_whitespace()
- .next()
- .and_then(|v| v.parse::<u16>().ok())
- .ok_or_else(|| AppError::Git(format!("invalid status line: {line}")))?;
- status_code = code;
- continue;
- }
- let (name, value) = line
- .split_once(':')
- .ok_or_else(|| AppError::Git(format!("invalid header line: {line}")))?;
- headers.push((name.trim().to_string(), value.trim().to_string()));
- }
- Ok(GitHttpBackendResponse {
- status_code,
- headers,
- body,
- })
- }
|