1use std::ffi::OsStr;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::process::{Command, Stdio};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct GitError(pub String);
15
16impl std::fmt::Display for GitError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.write_str(&self.0)
19 }
20}
21
22#[derive(Debug, Clone)]
24pub struct Git {
25 dir: PathBuf,
26}
27
28struct Output {
30 ok: bool,
31 code: Option<i32>,
32 stdout: Vec<u8>,
33 stderr: String,
34}
35
36impl Git {
37 #[must_use]
39 pub fn at(dir: &Path) -> Self {
40 Self {
41 dir: dir.to_path_buf(),
42 }
43 }
44
45 #[must_use]
47 pub fn dir(&self) -> &Path {
48 &self.dir
49 }
50
51 fn exec(
52 &self,
53 args: &[&str],
54 env: &[(&str, &OsStr)],
55 stdin: Option<&[u8]>,
56 ) -> Result<Output, GitError> {
57 let mut cmd = Command::new("git");
58 cmd.args([
61 "-c",
62 "core.fsmonitor=false",
63 "-c",
64 "core.hooksPath=/dev/null",
65 ])
66 .args(args)
67 .current_dir(&self.dir)
68 .env("GIT_TERMINAL_PROMPT", "0")
69 .env("GIT_OPTIONAL_LOCKS", "0")
70 .env("LC_ALL", "C")
71 .stdin(if stdin.is_some() {
72 Stdio::piped()
73 } else {
74 Stdio::null()
75 })
76 .stdout(Stdio::piped())
77 .stderr(Stdio::piped());
78 for (k, v) in env {
79 cmd.env(k, v);
80 }
81 let mut child = cmd
82 .spawn()
83 .map_err(|e| GitError(format!("cannot run git: {e}")))?;
84 if let (Some(input), Some(mut pipe)) = (stdin, child.stdin.take()) {
85 pipe.write_all(input)
86 .map_err(|e| GitError(format!("git stdin: {e}")))?;
87 }
88 let out = child
89 .wait_with_output()
90 .map_err(|e| GitError(format!("git: {e}")))?;
91 Ok(Output {
92 ok: out.status.success(),
93 code: out.status.code(),
94 stdout: out.stdout,
95 stderr: String::from_utf8_lossy(&out.stderr).trim().to_owned(),
96 })
97 }
98
99 fn checked(
100 &self,
101 args: &[&str],
102 env: &[(&str, &OsStr)],
103 stdin: Option<&[u8]>,
104 ) -> Result<Vec<u8>, GitError> {
105 let out = self.exec(args, env, stdin)?;
106 if out.ok {
107 Ok(out.stdout)
108 } else {
109 Err(GitError(format!("git {}: {}", args.join(" "), out.stderr)))
110 }
111 }
112
113 pub fn run(&self, args: &[&str]) -> Result<Vec<u8>, GitError> {
115 self.checked(args, &[], None)
116 }
117
118 pub fn run_with_env(&self, args: &[&str], env: &[(&str, &OsStr)]) -> Result<Vec<u8>, GitError> {
120 self.checked(args, env, None)
121 }
122
123 pub fn run_with_stdin(&self, args: &[&str], input: &[u8]) -> Result<Vec<u8>, GitError> {
125 self.checked(args, &[], Some(input))
126 }
127
128 pub fn run_with_index(
130 &self,
131 index: &Path,
132 args: &[&str],
133 stdin: Option<&[u8]>,
134 ) -> Result<Vec<u8>, GitError> {
135 self.checked(args, &[("GIT_INDEX_FILE", index.as_os_str())], stdin)
136 }
137
138 pub fn line(&self, args: &[&str]) -> Result<String, GitError> {
140 let out = self.run(args)?;
141 Ok(String::from_utf8_lossy(&out).trim().to_owned())
142 }
143
144 pub fn succeeds(&self, args: &[&str]) -> Result<bool, GitError> {
146 Ok(self.exec(args, &[], None)?.ok)
147 }
148
149 pub fn toplevel(&self) -> Result<Option<PathBuf>, GitError> {
151 let out = self.exec(&["rev-parse", "--show-toplevel"], &[], None)?;
152 if !out.ok {
153 return Ok(None);
154 }
155 let path = String::from_utf8_lossy(&out.stdout).trim().to_owned();
156 Ok(Some(PathBuf::from(path)))
157 }
158
159 pub fn head(&self) -> Result<Option<String>, GitError> {
161 let out = self.exec(&["rev-parse", "--verify", "-q", "HEAD^{commit}"], &[], None)?;
162 match (out.ok, out.code) {
163 (true, _) => Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_owned())),
164 (false, Some(1)) => Ok(None),
165 (false, _) => Err(GitError(format!("git rev-parse HEAD: {}", out.stderr))),
166 }
167 }
168
169 pub fn git_path(&self, name: &str) -> Result<PathBuf, GitError> {
171 let p = PathBuf::from(self.line(&["rev-parse", "--git-path", name])?);
172 Ok(if p.is_absolute() { p } else { self.dir.join(p) })
173 }
174}
175
176#[must_use]
178pub fn nul_fields(out: &[u8]) -> Vec<String> {
179 out.split(|b| *b == 0)
180 .filter(|f| !f.is_empty())
181 .map(|f| String::from_utf8_lossy(f).into_owned())
182 .collect()
183}