git_policy_cli/rule_commit_sequencer.rs
1//! What:
2//! The pure core of the "is a merge,
3//! cherry-pick or revert awaiting its commit" check.
4//! Why:
5//! The caller runs real Git and probes the filesystem;
6//! this module says which query to
7//! run,
8//! reads its output exactly,
9//! and decides from three presence facts.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! // sequencerState({ mergeHead, cherryPickHead, revertHead }): 'in-progress' | 'none'
14//! ```
15
16/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
17use std::ffi::OsString;
18
19/// What:
20/// Whether an operation is awaiting its concluding commit.
21/// Why:
22/// While one is,
23/// Git forbids partial commits,
24/// so a pathless `git commit` is the
25/// documented conclusion and the commit-only transform lets it through.
26///
27/// In TS you'd write (pseudocode):
28/// ```ts
29/// type SequencerState = 'in-progress' | 'none';
30/// ```
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum SequencerState {
33 /// A merge,
34 /// cherry-pick or revert head file exists.
35 InProgress,
36 /// No head file exists,
37 /// or Git could not answer;
38 /// normal enforcement applies.
39 NotInProgress,
40}
41
42/// Git-directory files whose presence marks an operation awaiting its concluding commit.
43pub const SEQUENCER_HEAD_FILES: &[&str] = &["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
44
45/// What:
46/// Existence of each head file,
47/// as the caller observed it.
48/// `bool` is true or false.
49/// Why:
50/// These three facts are everything the decision needs from the repository.
51///
52/// In TS you'd write (pseudocode):
53/// ```ts
54/// type SequencerFacts = { mergeHead: boolean; cherryPickHead: boolean; revertHead: boolean };
55/// ```
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub struct SequencerFacts {
58 /// `MERGE_HEAD` exists.
59 pub merge_head: bool,
60 /// `CHERRY_PICK_HEAD` exists.
61 pub cherry_pick_head: bool,
62 /// `REVERT_HEAD` exists.
63 pub revert_head: bool,
64}
65
66/// What:
67/// Why the path query's output could not be read.
68/// Why:
69/// Output that is not exactly three paths in one directory is reported,
70/// never guessed.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// class SequencerOutputError extends Error {}
75/// ```
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub struct SequencerOutputError;
78
79/// `impl std::fmt::Display` supplies Rust's "print me" interface for the error.
80impl std::fmt::Display for SequencerOutputError {
81 /// `&self` borrows the error read-only;
82 /// `&mut` lends the formatter for writing.
83 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 return formatter.write_str(
85 "git rev-parse --git-path did not print the three sequencer head paths of one Git directory",
86 );
87 }
88}
89
90/// An empty `impl` marks the type as a standard error value for generic handling.
91impl std::error::Error for SequencerOutputError {}
92
93/// What:
94/// Build the real-Git argument list:
95/// the caller's global options,
96/// then one
97/// `--git-path` per head file.
98/// `Vec<OsString>` is the owned result.
99/// Why:
100/// `--git-path` resolves linked worktrees and `GIT_DIR`,
101/// which a lexical join cannot.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// const args = [...pre, 'rev-parse', '--path-format=absolute', '--git-path', 'MERGE_HEAD', /* ... */];
106/// ```
107pub fn sequencer_query_arguments(global_prefix: &[OsString]) -> Vec<OsString> {
108 // `.to_vec()` copies the borrowed prefix into a list this function owns.
109 let mut arguments: Vec<OsString> = global_prefix.to_vec();
110 arguments.push(OsString::from("rev-parse"));
111 arguments.push(OsString::from("--path-format=absolute"));
112 // `for name in SEQUENCER_HEAD_FILES` borrows each file name in order.
113 for name in SEQUENCER_HEAD_FILES {
114 arguments.push(OsString::from("--git-path"));
115 arguments.push(OsString::from(name));
116 }
117 return arguments;
118}
119
120/// What:
121/// Read the query output as three paths without splitting on newlines.
122/// `Result<Vec<Vec<u8>>, _>` is "three byte paths,
123/// or a refusal".
124/// Why:
125/// A Git directory path may contain a newline.
126/// The three files share one directory,
127/// so its length follows from the output length and the result is checked byte for byte.
128///
129/// In TS you'd write (pseudocode):
130/// ```ts
131/// const directoryLength = (stdout.length - fixedLength) / 3; // then verify every byte
132/// ```
133pub fn sequencer_head_paths(stdout: &[u8]) -> Result<Vec<Vec<u8>>, SequencerOutputError> {
134 // Bytes that are not the directory: each name plus its trailing newline.
135 let mut fixed: usize = 0;
136 for name in SEQUENCER_HEAD_FILES {
137 fixed += name.len() + 1;
138 }
139 if stdout.len() < fixed {
140 // `Err(x)` is the failure case of `Result`.
141 return Err(SequencerOutputError);
142 }
143 // `.is_multiple_of(n)` is true when dividing by `n` leaves no remainder.
144 if !(stdout.len() - fixed).is_multiple_of(SEQUENCER_HEAD_FILES.len()) {
145 return Err(SequencerOutputError);
146 }
147 // The shared directory prefix, including its trailing separator.
148 let directory_length: usize = (stdout.len() - fixed) / SEQUENCER_HEAD_FILES.len();
149 // `&stdout[..n]` borrows the first `n` bytes.
150 let directory: &[u8] = &stdout[..directory_length];
151 let mut paths: Vec<Vec<u8>> = Vec::<Vec<u8>>::new();
152 let mut offset: usize = 0;
153 for name in SEQUENCER_HEAD_FILES {
154 // `.to_vec()` copies the directory bytes so the path can grow.
155 let mut path: Vec<u8> = directory.to_vec();
156 path.extend_from_slice(name.as_bytes());
157 let end: usize = offset + path.len();
158 if &stdout[offset..end] != path.as_slice() || stdout[end] != b'\n' {
159 return Err(SequencerOutputError);
160 }
161 offset = end + 1;
162 paths.push(path);
163 }
164 // `Ok(x)` is the success case of `Result`.
165 return Ok(paths);
166}
167
168/// What:
169/// Decide from the three presence facts.
170/// Why:
171/// Any one head file means Git is waiting for the concluding commit.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// return present.includes(true) ? 'in-progress' : 'none';
176/// ```
177pub fn sequencer_state(facts: SequencerFacts) -> SequencerState {
178 if facts.merge_head || facts.cherry_pick_head || facts.revert_head {
179 return SequencerState::InProgress;
180 }
181 return SequencerState::NotInProgress;
182}
183
184/// What:
185/// The state to use when the path query itself failed.
186/// Why:
187/// The incumbent falls back to normal enforcement and lets real Git report its error.
188///
189/// In TS you'd write (pseudocode):
190/// ```ts
191/// catch (error) { if (error instanceof SubprocessError) return 'none'; throw error; }
192/// ```
193pub fn sequencer_state_when_query_fails() -> SequencerState {
194 return SequencerState::NotInProgress;
195}
196
197/// Output parsing,
198/// the decision and real-Git controls of the query.
199#[cfg(test)]
200#[path = "rule_commit_sequencer_tests.rs"]
201mod tests;