Skip to main content

worktree_io/
git.rs

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    // Set up the remote tracking so `git fetch` and `symbolic-ref` work correctly
22    // for a bare clone we need to configure remote.origin.fetch
23    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    // Fetch so that refs/remotes/origin/HEAD is populated
36    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    // Try symbolic-ref first (works when remote HEAD is set)
66    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        // Output looks like "refs/remotes/origin/main\n"
76        if let Some(branch) = full.trim().strip_prefix("refs/remotes/origin/") {
77            return Ok(branch.to_string());
78        }
79    }
80
81    // Fall back: try `git remote show origin` to detect the default branch name
82    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    // Last resort: try common names
100    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        // Check out the existing remote branch, tracking it locally
137        cmd.arg(dest)
138            .arg("--track")
139            .arg(format!("origin/{branch}"));
140    } else {
141        // Create a new branch from the default base
142        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}