mdbook_gitinfo/
git.rs

1//! Utility module for running Git commands.
2//!
3//! This module provides helpers for interacting with a Git repository,
4//! primarily to extract metadata (commit hash, tag, timestamp, branch).
5//!
6//! All functions return [`mdbook::errors::Error`] on failure so they can be
7//! integrated directly into the `mdbook` preprocessor error flow.
8//!
9//! See also:
10//! - [`get_git_output`] — Run arbitrary Git commands and capture output.
11//! - [`verify_branch`] — Convenience wrapper to check branch existence.
12
13use mdbook::errors::Error;
14use std::ffi::OsStr;
15use std::path::Path;
16use std::process::{Command, Stdio};
17
18/// Run a Git command and return the trimmed `stdout` output as a [`String`].
19///
20/// This is the central utility for invoking Git. It is used by the
21/// `mdbook-gitinfo` preprocessor to fetch commit information such as:
22/// - short or long commit hash
23/// - nearest tag
24/// - commit date/time
25///
26/// See also: [`verify_branch`], which builds on this function to check
27/// if a branch exists locally.
28///
29/// # Type Parameters
30///
31/// - `I`: An iterator of arguments (e.g., a string slice array).
32/// - `S`: Each argument, convertible to [`OsStr`].
33///
34/// # Arguments
35///
36/// * `args` — Git command-line arguments (e.g., `["rev-parse", "HEAD"]`).
37/// * `dir` — Path to the Git repository root or working directory.
38///
39/// # Returns
40///
41/// * `Ok(String)` — Trimmed `stdout` output from Git.
42/// * `Err(Error)` — If Git fails to launch or exits with non-zero status.
43///
44/// # Errors
45///
46/// This function returns an [`Error`] if:
47/// - The `git` binary is missing or fails to start.
48/// - The command returns a non-zero exit code.
49/// - The output cannot be decoded as UTF-8.
50///
51/// # Example
52///
53/// ```no_run
54/// use std::path::Path;
55/// use mdbook_gitinfo::git::get_git_output;
56///
57/// let hash = get_git_output(["rev-parse", "--short", "HEAD"], Path::new("."))
58///     .expect("failed to get commit hash");
59/// println!("Current short commit hash: {}", hash);
60/// ```
61pub fn get_git_output<I, S>(args: I, dir: &Path) -> Result<String, Error>
62where
63    I: IntoIterator<Item = S>,
64    S: AsRef<OsStr>,
65{
66    let output = Command::new("git")
67        .args(args)
68        .current_dir(dir)
69        .stdout(Stdio::piped())
70        .output()
71        .map_err(|e| Error::msg(format!("Git command failed: {e}")))?;
72
73    if output.status.success() {
74        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
75    } else {
76        Err(Error::msg("Git command returned non-zero exit code"))
77    }
78}
79
80/// Verify that a branch exists locally in the given repository.
81///
82/// Internally runs:
83/// ```text
84/// git rev-parse --verify <branch>
85/// ```
86///
87/// This is a thin wrapper around [`get_git_output`], returning `true` if the
88/// Git call succeeds and `false` otherwise.
89///
90/// # Arguments
91///
92/// * `branch` — The name of the branch to check.
93/// * `dir` — Path to the Git repository root or working directory.
94///
95/// # Returns
96///
97/// * `true` if the branch exists locally.
98/// * `false` otherwise.
99///
100/// # Example
101///
102/// ```no_run
103/// use std::path::Path;
104/// use mdbook_gitinfo::git::verify_branch;
105///
106/// let dir = Path::new(".");
107/// if !verify_branch("dev", dir) {
108///     eprintln!("Branch 'dev' not found, falling back to 'main'");
109/// }
110/// ```
111pub fn verify_branch(branch: &str, dir: &Path) -> bool {
112    get_git_output(["rev-parse", "--verify", branch], dir).is_ok()
113}
114
115
116/// Return the latest tag name, preferring tags reachable from the given branch's HEAD.
117/// Falls back to global (by creator date) when describe fails.
118/// Returns "No tags found" if not tag found
119pub fn latest_tag_for_branch(branch: &str, dir: &std::path::Path) -> String {
120    // Prefer a tag reachable from branch HEAD
121    if let Ok(t) = get_git_output(["describe", "--tags", "--abbrev=0", branch], dir) {
122        if !t.trim().is_empty() {
123            return t;
124        }
125    }
126
127    // Fallback: newest tag by creator date
128    match get_git_output(["tag", "--sort=-creatordate"], dir) {
129        Ok(list) => {
130            if let Some(first) = list.lines().find(|l| !l.trim().is_empty()) {
131                return first.trim().to_string();
132            }
133        }
134        Err(_) => {}
135    }
136
137    "No tags found".to_string()
138}
139
140
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use std::path::PathBuf;
146
147    #[test]
148    fn returns_error_on_invalid_git_command() {
149        let result = get_git_output(["non-existent-command"], &PathBuf::from("."));
150        assert!(result.is_err());
151    }
152}