git_policy_cli/sequencer_marker.rs
1//! What:
2//! Proof, without a Git process, that a sequencer marker such as `MERGE_HEAD` names nothing.
3//! Why:
4//! A commit asks `git rev-parse --verify --quiet <marker>` for each of three markers,
5//! and in almost every commit none exists.
6//! On the files ref storage that question reads a fixed list of places,
7//! so when every one of them is empty the answer is "absent" without a process;
8//! any entry in any place, of any kind or content,
9//! says "maybe present" and the caller asks Git,
10//! which alone decides whether the entry resolves (a dangling marker still counts as a conclusion).
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const maybePresent = names.map((name) => lookupPlaces(layout, name).some(exists) || packedRefs.includes(name));
15//! ```
16
17/// Where one worktree's ref files live and what format they are in.
18use super::ref_files::RefFilesLayout;
19/// The ref storage backend.
20use super::transaction_journal::RefFormat;
21/// Borrowed and owned filesystem paths.
22use std::path::{Path, PathBuf};
23
24/// The most `packed-refs` bytes this check reads;
25/// a bigger file is left to Git.
26const PACKED_REFS_LIMIT: u64 = 64 * 1024 * 1024;
27
28/// What:
29/// The paths `git rev-parse --verify <name>` looks at for a loose entry,
30/// in Git's order.
31/// Why:
32/// Git's lookup rules try the name itself (a pseudoref in the worktree's Git directory),
33/// then `refs/`, `refs/tags/`, `refs/heads/`, `refs/remotes/` and `refs/remotes/<name>/HEAD`
34/// in the common directory.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// [join(gitDir, name), join(commonDir, 'refs', name), join(commonDir, 'refs/tags', name), ...]
39/// ```
40fn lookup_paths(layout: &RefFilesLayout<'_>, name: &str) -> Vec<PathBuf> {
41 return vec![
42 layout.git_dir.join(name),
43 layout.common_dir.join(format!("refs/{name}")),
44 layout.common_dir.join(format!("refs/tags/{name}")),
45 layout.common_dir.join(format!("refs/heads/{name}")),
46 layout.common_dir.join(format!("refs/remotes/{name}")),
47 layout.common_dir.join(format!("refs/remotes/{name}/HEAD")),
48 ];
49}
50
51/// What:
52/// Whether something exists at `path`,
53/// where anything but a clean "nothing here" counts as existing.
54/// Why:
55/// A link, a directory, a file of any content and an unreadable parent all send the caller to Git.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// await lstat(path).then(() => true, (error) => error.code !== 'ENOENT')
60/// ```
61fn something_is_at(path: &Path) -> bool {
62 return match std::fs::symlink_metadata(path) {
63 Ok(_) => true,
64 Err(error) => error.kind() != std::io::ErrorKind::NotFound,
65 };
66}
67
68/// What:
69/// The bytes of `packed-refs`,
70/// nothing when there is no such file,
71/// or `None` when the file cannot be read as a whole.
72/// Why:
73/// A missing `packed-refs` holds no ref;
74/// an unreadable or oversized one is not proof of anything.
75///
76/// In TS you'd write (pseudocode):
77/// ```ts
78/// await readFile(join(commonDir, 'packed-refs')).catch((e) => (e.code === 'ENOENT' ? Buffer.alloc(0) : undefined))
79/// ```
80fn packed_refs_bytes(common_dir: &Path) -> Option<Vec<u8>> {
81 let path: PathBuf = common_dir.join("packed-refs");
82 let metadata: std::fs::Metadata = match std::fs::metadata(&path) {
83 Ok(found) => found,
84 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(Vec::new()),
85 Err(_) => return None,
86 };
87 if !metadata.is_file() || metadata.len() > PACKED_REFS_LIMIT {
88 return None;
89 }
90 return match std::fs::read(&path) {
91 Ok(bytes) => Some(bytes),
92 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Some(Vec::new()),
93 Err(_) => None,
94 };
95}
96
97/// What:
98/// Whether `needle` occurs anywhere in `haystack`.
99/// Why:
100/// Any occurrence of the marker's name in `packed-refs` is a hit,
101/// a superset of the lines that name one of the lookup spellings;
102/// the extra hits only cost a Git process.
103///
104/// In TS you'd write (pseudocode):
105/// ```ts
106/// haystack.includes(needle)
107/// ```
108fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
109 if needle.is_empty() {
110 return true;
111 }
112 return haystack.windows(needle.len()).any(|window| { return window == needle; });
113}
114
115/// What:
116/// For each name, whether a marker of that name may exist:
117/// `false` is proof that `git rev-parse --verify --quiet <name>` exits non-zero.
118/// Why:
119/// The files backend resolves a name only through a loose entry at one of six paths
120/// or a line of `packed-refs`; reftable storage and anything unreadable answer "maybe".
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// names.map((name) => layout.refFormat !== 'files' || maybePresent(layout, name))
125/// ```
126pub fn markers_maybe_present(layout: &RefFilesLayout<'_>, names: &[&str]) -> Vec<bool> {
127 if layout.ref_format != RefFormat::Files {
128 return vec![true; names.len()];
129 }
130 let packed: Option<Vec<u8>> = packed_refs_bytes(layout.common_dir);
131 let mut answers: Vec<bool> = Vec::with_capacity(names.len());
132 for name in names {
133 let loose: bool = lookup_paths(layout, name).iter().any(|path| { return something_is_at(path.as_path()); });
134 let packed_hit: bool = match &packed {
135 Some(bytes) => contains_bytes(bytes.as_slice(), name.as_bytes()),
136 None => true,
137 };
138 answers.push(loose || packed_hit);
139 }
140 return answers;
141}
142
143/// Sequencer marker controls stay out of the release executable.
144#[cfg(test)]
145#[path = "sequencer_marker_tests.rs"]
146mod tests;