Skip to main content

git_sprout/
git.rs

1// ABOUTME: Runs git and reads its answers, keeping git the authority on git behaviour.
2// ABOUTME: Carries the caller's global options so every child sees the same repository.
3
4use std::ffi::{OsStr, OsString};
5use std::io;
6use std::io::Write;
7use std::path::Path;
8use std::process::{Command, ExitStatus, Stdio};
9
10/// Invokes git with a fixed set of global options in front of every subcommand.
11#[derive(Debug, Clone)]
12pub struct Git {
13    globals: Vec<OsString>,
14}
15
16impl Git {
17    pub fn new(globals: Vec<OsString>) -> Self {
18        Git { globals }
19    }
20
21    /// The caller's globals come first so that a `-C` of ours, which is always absolute,
22    /// still decides where git runs: repeated `-C` options are applied in order.
23    fn command(&self, directory: Option<&Path>) -> Command {
24        let mut command = Command::new("git");
25        command.args(&self.globals);
26        if let Some(directory) = directory {
27            command.arg("-C").arg(directory);
28        }
29        command
30    }
31
32    /// Runs git and returns its stdout, or an error if it failed or could not start.
33    pub fn capture<I, S>(&self, directory: Option<&Path>, args: I) -> io::Result<Vec<u8>>
34    where
35        I: IntoIterator<Item = S>,
36        S: AsRef<OsStr>,
37    {
38        let output = self
39            .command(directory)
40            .args(args)
41            .stdin(Stdio::null())
42            .stderr(Stdio::null())
43            .output()?;
44        if !output.status.success() {
45            return Err(io::Error::other("git exited with a failure status"));
46        }
47        Ok(output.stdout)
48    }
49
50    /// Runs git with the caller's stdio, so its output is indistinguishable from ours.
51    pub fn passthrough<I, S>(&self, directory: Option<&Path>, args: I) -> io::Result<ExitStatus>
52    where
53        I: IntoIterator<Item = S>,
54        S: AsRef<OsStr>,
55    {
56        self.command(directory).args(args).status()
57    }
58
59    /// Runs git with `input` on its stdin and returns its stdout.
60    pub fn capture_with_input<I, S>(
61        &self,
62        directory: Option<&Path>,
63        args: I,
64        input: &[u8],
65    ) -> io::Result<Vec<u8>>
66    where
67        I: IntoIterator<Item = S>,
68        S: AsRef<OsStr>,
69    {
70        let mut child = self
71            .command(directory)
72            .args(args)
73            .stdin(Stdio::piped())
74            .stdout(Stdio::piped())
75            .stderr(Stdio::null())
76            .spawn()?;
77        // The input has to be written while the output is being read: git streams its
78        // answers as it consumes paths, so writing everything first deadlocks as soon as
79        // either pipe fills.
80        let stdin = child.stdin.take();
81        let input = input.to_vec();
82        let writer = std::thread::spawn(move || {
83            if let Some(mut stdin) = stdin {
84                let _ = stdin.write_all(&input);
85            }
86        });
87        let output = child.wait_with_output()?;
88        let _ = writer.join();
89        if !output.status.success() {
90            return Err(io::Error::other("git exited with a failure status"));
91        }
92        Ok(output.stdout)
93    }
94
95    /// Reads a single-line answer such as an object id, with the trailing newline removed.
96    pub fn capture_line<I, S>(&self, directory: Option<&Path>, args: I) -> io::Result<String>
97    where
98        I: IntoIterator<Item = S>,
99        S: AsRef<OsStr>,
100    {
101        let bytes = self.capture(directory, args)?;
102        let text =
103            String::from_utf8(bytes).map_err(|_| io::Error::other("git printed non-UTF-8"))?;
104        Ok(text.trim_end_matches(['\n', '\r']).to_string())
105    }
106
107    /// Reads a configuration value as git resolves it for the given worktree.
108    pub fn config(&self, directory: &Path, key: &str) -> Option<String> {
109        self.capture_line(Some(directory), ["config", "--get", key])
110            .ok()
111    }
112}