1use std::{
2 io::Result,
3 path::{Path, PathBuf},
4 process::{Command, Output},
5};
6
7fn git_command(path: impl AsRef<Path>, subcommand: &[&str]) -> Result<Output> {
8 let git_path = path
9 .as_ref()
10 .to_str()
11 .expect("Repository path must be a valid UTF-8 string");
12
13 let mut args = vec!["-C", git_path];
14 args.extend(subcommand);
15
16 Command::new("git").args(&args).output()
17}
18
19pub fn repo_hash(path: impl AsRef<Path>) -> Option<String> {
20 let output = git_command(path.as_ref(), &["rev-parse", "--short=12", "HEAD"]).ok()?;
21 if !output.status.success() {
22 return None;
23 }
24 let hash = String::from_utf8(output.stdout).ok()?;
25 let hash = hash.trim_end_matches('\n');
26
27 if dirty(path) {
28 Some(format!("{hash}+"))
29 } else {
30 Some(hash.into())
31 }
32}
33
34fn dirty(path: impl AsRef<Path>) -> bool {
35 match git_command(path.as_ref(), &["ls-files", "-m"]) {
36 Ok(modified_files) => !modified_files.stdout.is_empty(),
37 Err(_) => false,
38 }
39}
40
41fn gitmodules_config(subcommand: &[&str]) -> Option<String> {
42 let mut args = vec!["config", "-f", ".gitmodules", "-z"];
43 args.extend(subcommand);
44 let output = git_command(Path::new("."), &args).ok()?;
45
46 if !output.status.success() {
47 return None;
48 }
49
50 let mut result = String::from_utf8(output.stdout).ok()?;
51 assert_eq!(result.pop(), Some('\0'));
52 Some(result)
53}
54
55fn path_command(path: impl AsRef<Path>, subcommand: &[&str]) -> Option<PathBuf> {
56 let mut output = git_command(path, subcommand).ok()?;
57
58 if !output.status.success() {
59 return None;
60 }
61
62 assert_eq!(
63 output
64 .stdout
65 .pop()
66 .map(u32::from)
67 .and_then(std::char::from_u32),
68 Some('\n')
69 );
70 Some(path_from_output(output.stdout))
71}
72
73#[cfg(unix)]
74fn path_from_output(output: Vec<u8>) -> PathBuf {
75 use std::{ffi::OsString, os::unix::prelude::OsStringExt};
76 OsString::from_vec(output).into()
77}
78
79#[cfg(not(unix))]
80fn path_from_output(output: Vec<u8>) -> PathBuf {
81 String::from_utf8(output).unwrap().into()
82}
83
84pub fn toplevel(path: impl AsRef<Path>) -> Option<PathBuf> {
85 path_command(path, &["rev-parse", "--show-toplevel"])
86}
87
88#[allow(dead_code)]
90pub fn git_dir(path: impl AsRef<Path>) -> Option<PathBuf> {
91 path_command(path, &["rev-parse", "--git-dir"])
92}
93
94pub(crate) fn repo_remote_url(path: impl AsRef<Path>) -> Option<String> {
95 let key_for_path = gitmodules_config(&[
97 "--name-only",
98 "--get-regexp",
99 r"submodule\..+\.path",
100 &format!("^{}$", path.as_ref().display()),
101 ])?;
102
103 let subsection = key_for_path
104 .strip_suffix(".path")
105 .expect("submodule.<subsection>.path should end with '.path'");
106
107 gitmodules_config(&["--get", &format!("{subsection}.url")])
108}