libgir/
git.rs

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// Only build.rs uses this
89#[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    // Find the subsection that defines the module for the given path:
96    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}