Skip to main content

release_kit/migrate/
git.rs

1//! The git a migration reads the repository through.
2//!
3//! Every call answers for the `-C` target alone: the variables a running
4//! hook exports are scrubbed, so an inherited `GIT_DIR` cannot redirect a
5//! call at another repository, and the locale is pinned to `C`, so the
6//! diagnostics stay untranslated.
7
8use std::process::{Command, Output};
9
10use camino::Utf8Path;
11
12use crate::diagnostic::{Diagnostic, Reason};
13use crate::error::RkError;
14
15/// One git call against `target`, its output whatever the status.
16///
17/// # Errors
18///
19/// A spawn failure.
20pub fn run(target: &Utf8Path, args: &[&str]) -> Result<Output, RkError> {
21    let mut command = Command::new(crate::probes::git_bin());
22    for var in crate::maintenance::GIT_HOOK_VARS {
23        command.env_remove(var);
24    }
25    command
26        .env("LC_ALL", "C")
27        .env_remove("LANGUAGE")
28        .arg("-C")
29        .arg(target)
30        .args(args)
31        .output()
32        .map_err(|error| {
33            RkError::subprocess(
34                Diagnostic::new(
35                    Reason::SubprocessSpawn,
36                    format!("git could not be spawned: {error}"),
37                )
38                .expected("git on PATH, or RK_GIT_BIN naming it"),
39            )
40        })
41}
42
43/// The stdout of one git call that must succeed.
44///
45/// # Errors
46///
47/// A spawn failure, or a call that exits non-zero, naming its stderr.
48pub fn stdout(target: &Utf8Path, args: &[&str]) -> Result<Vec<u8>, RkError> {
49    let out = run(target, args)?;
50    if !out.status.success() {
51        return Err(RkError::subprocess(
52            Diagnostic::new(
53                Reason::SubprocessFailed,
54                format!(
55                    "git {} failed at {target}: {}",
56                    args.join(" "),
57                    String::from_utf8_lossy(&out.stderr).trim()
58                ),
59            )
60            .expected("a git repository with at least one commit"),
61        ));
62    }
63    Ok(out.stdout)
64}
65
66/// The non-empty lines of one git call that must succeed.
67///
68/// # Errors
69///
70/// As [`stdout`].
71pub fn lines(target: &Utf8Path, args: &[&str]) -> Result<Vec<String>, RkError> {
72    Ok(String::from_utf8_lossy(&stdout(target, args)?)
73        .lines()
74        .filter(|line| !line.is_empty())
75        .map(str::to_owned)
76        .collect())
77}
78
79/// The regular files `rev` holds among `paths`, each with its blob id:
80/// mode 100644 or 100755, never a symlink or a submodule.
81///
82/// # Errors
83///
84/// As [`stdout`], where `rev` names no tree.
85pub fn regular_blobs(
86    target: &Utf8Path,
87    rev: &str,
88    paths: &[&str],
89) -> Result<std::collections::BTreeMap<String, String>, RkError> {
90    let mut args = vec!["ls-tree", "-r", "-z", rev, "--"];
91    args.extend_from_slice(paths);
92    Ok(stdout(target, &args)?
93        .split(|byte| *byte == 0)
94        .filter_map(|record| {
95            let (meta, path) = std::str::from_utf8(record).ok()?.split_once('\t')?;
96            let mut fields = meta.split(' ');
97            let mode = fields.next()?;
98            let oid = fields.nth(1)?;
99            matches!(mode, "100644" | "100755").then(|| (path.to_owned(), oid.to_owned()))
100        })
101        .collect())
102}
103
104/// The blob id of the bytes at each of `paths`, read raw, with no
105/// filter applied, in the order given.
106///
107/// # Errors
108///
109/// As [`stdout`], where a path cannot be read.
110pub fn raw_blob_ids(target: &Utf8Path, paths: &[&str]) -> Result<Vec<String>, RkError> {
111    let mut args = vec!["hash-object", "--no-filters", "--"];
112    args.extend_from_slice(paths);
113    lines(target, &args)
114}