Skip to main content

dkdc_sh/
git.rs

1//! Git command abstractions (sync).
2
3use std::path::Path;
4use std::process::Command;
5
6use crate::{Error, require};
7
8/// Run a git command in a directory and return stdout.
9pub fn cmd(dir: &Path, args: &[&str]) -> Result<String, Error> {
10    cmd_with_env(dir, args, &[])
11}
12
13/// Run a git command with extra environment variables.
14///
15/// When `GIT_ASKPASS` is present in `env`, credential helpers are disabled
16/// to prevent interception by system keychains.
17pub fn cmd_with_env(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> Result<String, Error> {
18    require("git")?;
19
20    let has_askpass = env.iter().any(|(k, _)| *k == "GIT_ASKPASS");
21
22    let mut command = Command::new("git");
23    if has_askpass {
24        command.args(["-c", "credential.helper="]);
25    }
26    command.args(args).current_dir(dir);
27    for (k, v) in env {
28        command.env(k, v);
29    }
30    let output = command.output()?;
31
32    if !output.status.success() {
33        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
34        return Err(Error::CommandFailed {
35            cmd: format!("git {}", args.first().unwrap_or(&"")),
36            detail: stderr,
37        });
38    }
39
40    Ok(String::from_utf8_lossy(&output.stdout).to_string())
41}
42
43/// Shallow-clone a repo (single branch, depth 1).
44pub fn clone_shallow(url: &str, dest: &Path, branch: &str) -> Result<(), Error> {
45    clone_shallow_with_env(url, dest, branch, &[])
46}
47
48/// Shallow-clone a repo with extra environment variables.
49pub fn clone_shallow_with_env(
50    url: &str,
51    dest: &Path,
52    branch: &str,
53    env: &[(&str, &str)],
54) -> Result<(), Error> {
55    require("git")?;
56
57    let has_askpass = env.iter().any(|(k, _)| *k == "GIT_ASKPASS");
58
59    let mut command = Command::new("git");
60    if has_askpass {
61        command.args(["-c", "credential.helper="]);
62    }
63    command.args([
64        "clone",
65        "--depth",
66        "1",
67        "--branch",
68        branch,
69        url,
70        &dest.to_string_lossy(),
71    ]);
72    for (k, v) in env {
73        command.env(k, v);
74    }
75    let output = command.output()?;
76
77    if !output.status.success() {
78        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
79        return Err(Error::CommandFailed {
80            cmd: "git clone".to_string(),
81            detail: stderr,
82        });
83    }
84
85    Ok(())
86}
87
88/// Clone from a local repo directory (fast, shares objects via hardlinks).
89pub fn clone_local(source: &Path, dest: &Path, branch: &str) -> Result<(), Error> {
90    require("git")?;
91
92    let output = Command::new("git")
93        .args([
94            "clone",
95            "--branch",
96            branch,
97            "--single-branch",
98            &source.to_string_lossy(),
99            &dest.to_string_lossy(),
100        ])
101        .output()?;
102
103    if !output.status.success() {
104        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
105        return Err(Error::CommandFailed {
106            cmd: "git clone (local)".to_string(),
107            detail: stderr,
108        });
109    }
110
111    Ok(())
112}
113
114/// Create and switch to a new branch.
115pub fn checkout_new_branch(dir: &Path, branch: &str) -> Result<(), Error> {
116    cmd(dir, &["checkout", "-b", branch])?;
117    Ok(())
118}
119
120/// Set a git config key in a repo.
121pub fn config_set(dir: &Path, key: &str, value: &str) -> Result<(), Error> {
122    cmd(dir, &["config", key, value])?;
123    Ok(())
124}