Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
90 linesCodeBlameRaw
1//! Running git, and reading what the current repository says.
2
3use std::process::{Command, Stdio};
4
5use crate::error::{Error, Result};
6
7/// Runs git and returns its output, trimmed.
8pub fn run(args: &[&str]) -> Result<String> {
9 let output = Command::new("git")
10 .args(args)
11 .stdin(Stdio::null())
12 .output()
13 .map_err(|e| Error::failed(format!("Could not run git: {e}")))?;
14 if !output.status.success() {
15 let stderr = String::from_utf8_lossy(&output.stderr);
16 let line = stderr
17 .lines()
18 .find(|l| !l.trim().is_empty())
19 .unwrap_or("")
20 .trim();
21 return Err(Error::failed(format!(
22 "git {} failed: {line}",
23 args.join(" ")
24 )));
25 }
26 Ok(String::from_utf8_lossy(&output.stdout)
27 .trim_end()
28 .to_owned())
29}
30
31/// Runs git with its output on the terminal, as for a clone or a push.
32pub fn run_visible(args: &[&str]) -> Result<()> {
33 let status = Command::new("git")
34 .args(args)
35 .status()
36 .map_err(|e| Error::failed(format!("Could not run git: {e}")))?;
37 if !status.success() {
38 return Err(Error::exit(crate::error::Kind::Failed));
39 }
40 Ok(())
41}
42
43pub fn ok(args: &[&str]) -> bool {
44 Command::new("git")
45 .args(args)
46 .stdin(Stdio::null())
47 .stdout(Stdio::null())
48 .stderr(Stdio::null())
49 .status()
50 .is_ok_and(|s| s.success())
51}
52
53/// The remotes, in the order git lists them, with their fetch URLs.
54pub fn remotes() -> Vec<(String, String)> {
55 let Ok(out) = run(&["remote", "-v"]) else {
56 return Vec::new();
57 };
58 let mut seen = Vec::<(String, String)>::new();
59 for line in out.lines() {
60 let mut parts = line.split_whitespace();
61 let (Some(name), Some(url)) = (parts.next(), parts.next()) else {
62 continue;
63 };
64 if line.ends_with("(fetch)") && !seen.iter().any(|(n, _)| n == name) {
65 seen.push((name.to_owned(), url.to_owned()));
66 }
67 }
68 seen
69}
70
71pub fn current_branch() -> Option<String> {
72 run(&["symbolic-ref", "--quiet", "--short", "HEAD"])
73 .ok()
74 .filter(|b| !b.is_empty())
75}
76
77/// The commits on `head` that `base` lacks, oldest first: (subject, body).
78pub fn commits_between(base: &str, head: &str) -> Vec<(String, String)> {
79 let range = format!("{base}..{head}");
80 let Ok(out) = run(&["log", "--reverse", "--format=%s%x1f%b%x1e", &range]) else {
81 return Vec::new();
82 };
83 out.split('\u{1e}')
84 .filter_map(|entry| {
85 let entry = entry.trim_matches(['\n', '\r']);
86 let (subject, body) = entry.split_once('\u{1f}')?;
87 Some((subject.trim().to_owned(), body.trim().to_owned()))
88 })
89 .collect()
90}