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}