1use std::borrow::Cow;
8use std::ffi::OsStr;
9use std::fmt::{self, Display};
10use std::io;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13
14use log::debug;
15use thiserror::Error;
16
17use crate::prepare::{GitWorkArea, WorkAreaResult};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct CommitId(String);
22
23#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum GitError {
27 #[error("failed to construct 'git {}' command", subcommand)]
29 Subcommand {
30 subcommand: &'static str,
32 #[source]
34 source: io::Error,
35 },
36 #[error("git error: '{}'", msg)]
38 Git {
39 msg: Cow<'static, str>,
41 #[source]
43 source: Option<io::Error>,
44 },
45 #[error("invalid git ref: '{}'", ref_)]
47 InvalidRef {
48 ref_: Cow<'static, str>,
50 },
51}
52
53impl GitError {
54 pub fn subcommand(subcommand: &'static str, source: io::Error) -> Self {
56 GitError::Subcommand {
57 subcommand,
58 source,
59 }
60 }
61
62 pub(crate) fn git<M>(msg: M) -> Self
63 where
64 M: Into<Cow<'static, str>>,
65 {
66 GitError::Git {
67 msg: msg.into(),
68 source: None,
69 }
70 }
71
72 pub(crate) fn git_with_source<M>(msg: M, source: io::Error) -> Self
73 where
74 M: Into<Cow<'static, str>>,
75 {
76 GitError::Git {
77 msg: msg.into(),
78 source: Some(source),
79 }
80 }
81
82 pub(crate) fn invalid_ref<R>(ref_: R) -> Self
83 where
84 R: Into<Cow<'static, str>>,
85 {
86 GitError::InvalidRef {
87 ref_: ref_.into(),
88 }
89 }
90}
91
92pub(crate) type GitResult<T> = Result<T, GitError>;
93
94impl CommitId {
95 pub fn new<I>(id: I) -> Self
97 where
98 I: Into<String>,
99 {
100 CommitId(id.into())
101 }
102
103 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107}
108
109impl Display for CommitId {
110 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111 write!(f, "{}", self.as_str())
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct GitContext {
118 gitdir: PathBuf,
122 config: Option<PathBuf>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct Identity {
129 pub name: String,
131 pub email: String,
133}
134
135impl Identity {
136 pub fn new<N, E>(name: N, email: E) -> Self
138 where
139 N: Into<String>,
140 E: Into<String>,
141 {
142 Self {
143 name: name.into(),
144 email: email.into(),
145 }
146 }
147}
148
149impl Display for Identity {
150 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151 write!(f, "{} <{}>", self.name, self.email)
152 }
153}
154
155#[derive(Debug)]
157pub enum MergeStatus {
158 NoCommonHistory,
160 AlreadyMerged,
162 Mergeable(Vec<CommitId>),
164}
165
166impl Display for MergeStatus {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 write!(
169 f,
170 "{}",
171 match *self {
172 MergeStatus::NoCommonHistory => "no common history",
173 MergeStatus::AlreadyMerged => "already merged",
174 MergeStatus::Mergeable(_) => "mergeable",
175 },
176 )
177 }
178}
179
180impl GitContext {
181 pub fn new<P>(gitdir: P) -> Self
183 where
184 P: Into<PathBuf>,
185 {
186 Self {
187 gitdir: gitdir.into(),
188 config: None,
189 }
190 }
191
192 pub fn new_with_config<P, C>(gitdir: P, config: C) -> Self
194 where
195 P: Into<PathBuf>,
196 C: Into<PathBuf>,
197 {
198 Self {
199 gitdir: gitdir.into(),
200 config: Some(config.into()),
201 }
202 }
203
204 pub fn git(&self) -> Command {
208 let mut git = Command::new("git");
209
210 git.env("GIT_DIR", &self.gitdir);
211
212 self.config
213 .as_ref()
214 .map(|config| git.env("GIT_CONFIG", config));
215
216 git
217 }
218
219 pub fn fetch<R, I, N>(&self, remote: R, refnames: I) -> GitResult<()>
223 where
224 R: AsRef<str>,
225 I: IntoIterator<Item = N>,
226 N: AsRef<OsStr>,
227 {
228 let fetch = self
229 .git()
230 .arg("fetch")
231 .arg(remote.as_ref())
232 .args(refnames.into_iter())
233 .output()
234 .map_err(|err| GitError::subcommand("fetch", err))?;
235 if !fetch.status.success() {
236 return Err(GitError::git(format!(
237 "fetch from {} failed: {}",
238 remote.as_ref(),
239 String::from_utf8_lossy(&fetch.stderr)
240 )));
241 }
242
243 Ok(())
244 }
245
246 pub fn fetch_into<R, N, T>(&self, remote: R, refname: N, target: T) -> GitResult<()>
248 where
249 R: AsRef<str>,
250 N: AsRef<str>,
251 T: AsRef<str>,
252 {
253 self.fetch(
254 remote,
255 [&format!("{}:{}", refname.as_ref(), target.as_ref())],
256 )
257 }
258
259 pub fn force_fetch_into<R, N, T>(&self, remote: R, refname: N, target: T) -> GitResult<()>
261 where
262 R: AsRef<str>,
263 N: AsRef<str>,
264 T: AsRef<str>,
265 {
266 self.fetch_into(remote, format!("+{}", refname.as_ref()), target)
267 }
268
269 pub fn default_branch(&self) -> GitResult<Option<String>> {
271 let default_branch_name = self
273 .git()
274 .arg("config")
275 .arg("--get")
276 .arg("init.defaultBranchName")
277 .output()
278 .map_err(|err| GitError::subcommand("config --get init.defaultBranchName", err))?;
279 if default_branch_name.status.success() {
280 return Ok(Some(
281 String::from_utf8_lossy(&default_branch_name.stdout)
282 .trim()
283 .into(),
284 ));
285 }
286
287 let origin_head = self
289 .git()
290 .arg("symbolic-ref")
291 .arg("--short")
292 .arg("refs/remotes/origin/HEAD")
293 .output()
294 .map_err(|err| GitError::subcommand("symbolic-ref origin/HEAD", err))?;
295 if origin_head.status.success() {
296 const ORIGIN_PREFIX: &str = "origin/";
297 let full_refname = String::from_utf8_lossy(&origin_head.stdout);
298 let refname = full_refname.trim();
299 if let Some(on_origin) = refname.strip_prefix(ORIGIN_PREFIX) {
300 return Ok(Some(on_origin.into()));
301 }
302 }
303
304 let ls_origin_head = self
306 .git()
307 .arg("ls-remote")
308 .arg("--symref")
309 .arg("origin")
310 .arg("HEAD")
311 .output()
312 .map_err(|err| GitError::subcommand("ls-remote --symref origin", err))?;
313 if ls_origin_head.status.success() {
314 const SYMREF_PREFIX: &str = "ref: refs/heads/";
315 const SYMREF_SUFFIX: &str = "\tHEAD";
316 let full_output = String::from_utf8_lossy(&ls_origin_head.stdout);
317 for line in full_output.lines() {
318 if line.starts_with(SYMREF_PREFIX) && line.ends_with(SYMREF_SUFFIX) {
319 let refname = &line[SYMREF_PREFIX.len()..(line.len() - SYMREF_SUFFIX.len())];
320 return Ok(Some(refname.into()));
321 }
322 }
323 }
324
325 let head = self
327 .git()
328 .arg("symbolic-ref")
329 .arg("--short")
330 .arg("HEAD")
331 .output()
332 .map_err(|err| GitError::subcommand("symbolic-ref HEAD", err))?;
333 if head.status.success() {
334 return Ok(Some(String::from_utf8_lossy(&head.stdout).trim().into()));
335 }
336
337 Ok(None)
338 }
339
340 pub fn prepare(&self, rev: &CommitId) -> WorkAreaResult<GitWorkArea> {
342 GitWorkArea::new(self.clone(), rev)
343 }
344
345 pub fn reserve_ref<N>(&self, name: N, commit: &CommitId) -> GitResult<(String, usize)>
352 where
353 N: AsRef<str>,
354 {
355 let ref_prefix = format!("refs/{}/heads", name.as_ref());
356
357 debug!(target: "git", "reserving ref under {}", ref_prefix);
358
359 loop {
360 let for_each_ref = self
361 .git()
362 .arg("for-each-ref")
363 .arg("--format=%(refname)")
364 .arg("--")
365 .arg(&ref_prefix)
366 .output()
367 .map_err(|err| GitError::subcommand("for-each-ref", err))?;
368 if !for_each_ref.status.success() {
369 return Err(GitError::git(format!(
370 "listing all {} refs: {}",
371 ref_prefix,
372 String::from_utf8_lossy(&for_each_ref.stderr)
373 )));
374 }
375 let refs = String::from_utf8_lossy(&for_each_ref.stdout);
376
377 let nrefs = refs.lines().count();
378 let new_ref = format!("{}/{}", ref_prefix, nrefs);
379
380 debug!(target: "git", "trying to reserve ref {}", new_ref);
381
382 let lock_ref = self
383 .git()
384 .arg("update-ref")
385 .arg(&new_ref)
386 .arg(commit.as_str())
387 .arg("0000000000000000000000000000000000000000")
388 .stdout(Stdio::null())
389 .output()
390 .map_err(|err| GitError::git_with_source("update-ref", err))?;
391
392 if lock_ref.status.success() {
393 debug!(target: "git", "successfully reserved {}", new_ref);
394
395 return Ok((new_ref, nrefs));
396 }
397
398 let err = String::from_utf8_lossy(&lock_ref.stderr);
399 if err.contains("with nonexistent object") {
400 return Err(GitError::invalid_ref("no such commit"));
401 } else if err.contains("not a valid SHA1") {
402 return Err(GitError::invalid_ref("invalid SHA"));
403 }
404 }
405 }
406
407 pub fn reserve_refs<N>(&self, name: N, commit: &CommitId) -> GitResult<(String, String)>
419 where
420 N: AsRef<str>,
421 {
422 let (new_ref, id) = self.reserve_ref(name.as_ref(), commit)?;
423 let new_base = format!("refs/{}/bases/{}", name.as_ref(), id);
424
425 debug!(target: "git", "successfully reserved {} and {}", new_ref, new_base);
426
427 Ok((new_ref, new_base))
428 }
429
430 pub fn mergeable(&self, base: &CommitId, topic: &CommitId) -> GitResult<MergeStatus> {
432 let merge_base = self
433 .git()
434 .arg("merge-base")
435 .arg("--all") .arg(base.as_str())
437 .arg(topic.as_str())
438 .output()
439 .map_err(|err| GitError::subcommand("merge-base", err))?;
440 if !merge_base.status.success() {
441 return Ok(MergeStatus::NoCommonHistory);
442 }
443 let bases = String::from_utf8_lossy(&merge_base.stdout);
444 let bases = bases
445 .split_whitespace()
446 .map(CommitId::new)
447 .collect::<Vec<_>>();
448
449 Ok(if Some(topic) == bases.first() {
450 MergeStatus::AlreadyMerged
451 } else {
452 MergeStatus::Mergeable(bases)
453 })
454 }
455
456 pub fn gitdir(&self) -> &Path {
458 &self.gitdir
459 }
460}