Skip to main content

git_policy_cli/
shadow_config.rs

1//! What:
2//!  The shadow repository's `config` file, written by cli-git itself,
3//!       and the repository format facts it copies from the real common config.
4//! Why:
5//!  Git does not apply `core.repositoryformatversion` and `extensions.*` from an included file,
6//!  so they are copied explicitly.
7//!  `core.hooksPath` precedes the include so a real `core.hooksPath` still overrides it;
8//!  `core.worktree`, `gc.auto` and `maintenance.auto` follow it,
9//!  so the real config cannot redirect the worktree or start maintenance in the shadow
10//!  (`src/shadow-repository/shadow-config.ts`).
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! formatShadowConfig({ format, commonDir, worktreeRoot });
15//! ```
16
17/// One required Git start, and its failure text.
18use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
19/// `Path` is a borrowed filesystem path.
20use std::path::Path;
21
22/// What:
23///  Repository format facts the shadow copies from the real common config.
24/// Why:
25///   A shadow with a different object format or ref backend would misread the real store.
26///
27/// In TS you'd write (pseudocode):
28/// ```ts
29/// type RepositoryFormat = { version: string; extensions: [string, string][] };
30/// ```
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct RepositoryFormat {
33    /// `core.repositoryformatversion`, `0` when unset.
34    pub version: String,
35    /// Every `extensions.*` key with its value, in file order, the prefix removed.
36    pub extensions: Vec<(String, String)>,
37}
38
39/// What:
40///  Quotes one value under Git's config syntax.
41/// Why:
42///   The value is always double-quoted,
43///  so `;`, `#` and surrounding whitespace stay literal;
44///  backslash, double quote, newline and tab use Git's escapes.
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// quoteGitConfigValue('/a "b"'); // '"/a \\"b\\""'
49/// ```
50pub fn quote_git_config_value(value: &str) -> String {
51    let mut body: String = String::with_capacity(value.len());
52    for character in value.chars() {
53        match character {
54            '\\' => body.push_str(r"\\"),
55            '"' => body.push_str(r#"\""#),
56            '\n' => body.push_str(r"\n"),
57            '\t' => body.push_str(r"\t"),
58            other => body.push(other),
59        }
60    }
61    return format!("\"{body}\"");
62}
63
64/// What:
65///  Reads the format facts from the real common config file alone.
66/// Why:
67///   `git config --file config --list` reads only that file,
68///  so an included or worktree config never leaks into the copied format.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// await readRepositoryFormat({ gitPath, commonDir });
73/// ```
74pub fn read_repository_format(
75    context: &GitContext,
76    common_dir: &Path,
77) -> Result<RepositoryFormat, GitFailure> {
78    let mut request: GitRequest = GitRequest::new(
79        common_dir,
80        &["config", "--file", "config", "--null", "--list"],
81    );
82    request.without_prefix = true;
83    let output = run_git_checked(context, &request)?;
84    let mut version: Option<String> = None;
85    let mut extensions: Vec<(String, String)> = Vec::new();
86    for entry in output.stdout.split(|byte| { return *byte == 0; }).filter(|entry| { return !entry.is_empty(); }) {
87        // A key without a value line is a valueless boolean, which reads as `true`.
88        let (key, value): (&[u8], &[u8]) = match entry.iter().position(|byte| { return *byte == b'\n'; }) {
89            Some(newline) => (&entry[..newline], &entry[newline + 1..]),
90            None => (entry, &b"true"[..]),
91        };
92        let key_text: String = String::from_utf8_lossy(key).into_owned();
93        let value_text: String = String::from_utf8_lossy(value).into_owned();
94        if key_text.to_lowercase() == "core.repositoryformatversion" {
95            version = Some(value_text.clone());
96        }
97        if key_text.is_char_boundary(11) && key_text[..11].to_lowercase() == "extensions." {
98            extensions.push((key_text[11..].to_string(), value_text));
99        }
100    }
101    return Ok(RepositoryFormat {
102        version: version.unwrap_or_else(|| { return String::from("0"); }),
103        extensions,
104    });
105}
106
107/// What:
108///  Formats the shadow `config` file.
109/// Why:
110///   The line order is the incumbent's:
111///  hooks path before the include, then the worktree, gc and maintenance overrides.
112///
113/// In TS you'd write (pseudocode):
114/// ```ts
115/// formatShadowConfig({ format, commonDir, worktreeRoot });
116/// ```
117pub fn format_shadow_config(format: &RepositoryFormat, common_dir: &Path, worktree_root: &Path) -> String {
118    let common: String = common_dir.display().to_string();
119    let mut lines: Vec<String> = vec![
120        String::from("[core]"),
121        format!("\trepositoryformatversion = {}", quote_git_config_value(format.version.as_str())),
122        format!("\thooksPath = {}", quote_git_config_value(format!("{common}/hooks").as_str())),
123    ];
124    if !format.extensions.is_empty() {
125        lines.push(String::from("[extensions]"));
126        for (name, value) in &format.extensions {
127            lines.push(format!("\t{name} = {}", quote_git_config_value(value.as_str())));
128        }
129    }
130    lines.push(String::from("[include]"));
131    lines.push(format!(
132        "\tpath = {}",
133        quote_git_config_value(format!("{common}/config").as_str())
134    ));
135    lines.push(String::from("[core]"));
136    lines.push(format!(
137        "\tworktree = {}",
138        quote_git_config_value(worktree_root.display().to_string().as_str())
139    ));
140    lines.push(String::from("[gc]"));
141    lines.push(String::from("\tauto = 0"));
142    lines.push(String::from("[maintenance]"));
143    lines.push(String::from("\tauto = false"));
144    lines.push(String::new());
145    return lines.join("\n");
146}
147
148/// Shadow config controls stay out of the release executable.
149#[cfg(test)]
150#[path = "shadow_config_tests.rs"]
151mod tests;