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