1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use std::process::Command;
4
5pub fn bare_clone(url: &str, dest: &Path) -> Result<()> {
6 if let Some(parent) = dest.parent() {
7 std::fs::create_dir_all(parent)
8 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
9 }
10
11 let status = Command::new("git")
12 .args(["clone", "--bare", url])
13 .arg(dest)
14 .status()
15 .context("Failed to run `git clone --bare`")?;
16
17 if !status.success() {
18 bail!("git clone --bare failed for {url}");
19 }
20
21 let fetch_refspec = "+refs/heads/*:refs/remotes/origin/*";
24 let status = Command::new("git")
25 .args(["-C"])
26 .arg(dest)
27 .args(["config", "remote.origin.fetch", fetch_refspec])
28 .status()
29 .context("Failed to configure remote.origin.fetch")?;
30
31 if !status.success() {
32 bail!("Failed to set remote.origin.fetch");
33 }
34
35 let status = Command::new("git")
37 .args(["-C"])
38 .arg(dest)
39 .args(["fetch", "origin"])
40 .status()
41 .context("Failed to run `git fetch origin`")?;
42
43 if !status.success() {
44 bail!("git fetch origin failed after bare clone");
45 }
46
47 Ok(())
48}
49
50pub fn git_fetch(bare: &Path) -> Result<()> {
51 let status = Command::new("git")
52 .args(["-C"])
53 .arg(bare)
54 .args(["fetch", "origin"])
55 .status()
56 .context("Failed to run `git fetch`")?;
57
58 if !status.success() {
59 bail!("git fetch origin failed");
60 }
61 Ok(())
62}
63
64pub fn detect_default_branch(bare: &Path) -> Result<String> {
65 let output = Command::new("git")
67 .args(["-C"])
68 .arg(bare)
69 .args(["symbolic-ref", "refs/remotes/origin/HEAD"])
70 .output()
71 .context("Failed to run `git symbolic-ref`")?;
72
73 if output.status.success() {
74 let full = String::from_utf8_lossy(&output.stdout);
75 if let Some(branch) = full.trim().strip_prefix("refs/remotes/origin/") {
77 return Ok(branch.to_string());
78 }
79 }
80
81 let output = Command::new("git")
83 .args(["-C"])
84 .arg(bare)
85 .args(["remote", "show", "origin"])
86 .output()
87 .context("Failed to run `git remote show origin`")?;
88
89 if output.status.success() {
90 let text = String::from_utf8_lossy(&output.stdout);
91 for line in text.lines() {
92 let line = line.trim();
93 if let Some(branch) = line.strip_prefix("HEAD branch: ") {
94 return Ok(branch.to_string());
95 }
96 }
97 }
98
99 for candidate in ["main", "master", "develop"] {
101 let output = Command::new("git")
102 .args(["-C"])
103 .arg(bare)
104 .args(["rev-parse", "--verify", &format!("refs/remotes/origin/{candidate}")])
105 .output()
106 .context("Failed to run `git rev-parse`")?;
107 if output.status.success() {
108 return Ok(candidate.to_string());
109 }
110 }
111
112 bail!("Could not detect default branch for the repository");
113}
114
115pub fn branch_exists_remote(bare: &Path, branch: &str) -> bool {
116 Command::new("git")
117 .args(["-C"])
118 .arg(bare)
119 .args(["rev-parse", "--verify", &format!("refs/remotes/origin/{branch}")])
120 .output()
121 .map(|o| o.status.success())
122 .unwrap_or(false)
123}
124
125pub fn create_worktree(
126 bare: &Path,
127 dest: &Path,
128 branch: &str,
129 base_branch: &str,
130 branch_exists: bool,
131) -> Result<()> {
132 let mut cmd = Command::new("git");
133 cmd.args(["-C"]).arg(bare).arg("worktree").arg("add");
134
135 if branch_exists {
136 cmd.arg(dest)
138 .arg("--track")
139 .arg(format!("origin/{branch}"));
140 } else {
141 cmd.arg(dest)
143 .arg("-b")
144 .arg(branch)
145 .arg(format!("origin/{base_branch}"));
146 }
147
148 let status = cmd.status().context("Failed to run `git worktree add`")?;
149
150 if !status.success() {
151 bail!("git worktree add failed for branch {branch}");
152 }
153 Ok(())
154}