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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! What:
//! The pure core of the "is a merge,
//! cherry-pick or revert awaiting its commit" check.
//! Why:
//! The caller runs real Git and probes the filesystem;
//! this module says which query to
//! run,
//! reads its output exactly,
//! and decides from three presence facts.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // sequencerState({ mergeHead, cherryPickHead, revertHead }): 'in-progress' | 'none'
//! ```
/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
use std::ffi::OsString;
/// What:
/// Whether an operation is awaiting its concluding commit.
/// Why:
/// While one is,
/// Git forbids partial commits,
/// so a pathless `git commit` is the
/// documented conclusion and the commit-only transform lets it through.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type SequencerState = 'in-progress' | 'none';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SequencerState {
/// A merge,
/// cherry-pick or revert head file exists.
InProgress,
/// No head file exists,
/// or Git could not answer;
/// normal enforcement applies.
NotInProgress,
}
/// Git-directory files whose presence marks an operation awaiting its concluding commit.
pub const SEQUENCER_HEAD_FILES: &[&str] = &["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
/// What:
/// Existence of each head file,
/// as the caller observed it.
/// `bool` is true or false.
/// Why:
/// These three facts are everything the decision needs from the repository.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type SequencerFacts = { mergeHead: boolean; cherryPickHead: boolean; revertHead: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SequencerFacts {
/// `MERGE_HEAD` exists.
pub merge_head: bool,
/// `CHERRY_PICK_HEAD` exists.
pub cherry_pick_head: bool,
/// `REVERT_HEAD` exists.
pub revert_head: bool,
}
/// What:
/// Why the path query's output could not be read.
/// Why:
/// Output that is not exactly three paths in one directory is reported,
/// never guessed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class SequencerOutputError extends Error {}
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SequencerOutputError;
/// `impl std::fmt::Display` supplies Rust's "print me" interface for the error.
impl std::fmt::Display for SequencerOutputError {
/// `&self` borrows the error read-only;
/// `&mut` lends the formatter for writing.
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(
"git rev-parse --git-path did not print the three sequencer head paths of one Git directory",
);
}
}
/// An empty `impl` marks the type as a standard error value for generic handling.
impl std::error::Error for SequencerOutputError {}
/// What:
/// Build the real-Git argument list:
/// the caller's global options,
/// then one
/// `--git-path` per head file.
/// `Vec<OsString>` is the owned result.
/// Why:
/// `--git-path` resolves linked worktrees and `GIT_DIR`,
/// which a lexical join cannot.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const args = [...pre, 'rev-parse', '--path-format=absolute', '--git-path', 'MERGE_HEAD', /* ... */];
/// ```
pub fn sequencer_query_arguments(global_prefix: &[OsString]) -> Vec<OsString> {
// `.to_vec()` copies the borrowed prefix into a list this function owns.
let mut arguments: Vec<OsString> = global_prefix.to_vec();
arguments.push(OsString::from("rev-parse"));
arguments.push(OsString::from("--path-format=absolute"));
// `for name in SEQUENCER_HEAD_FILES` borrows each file name in order.
for name in SEQUENCER_HEAD_FILES {
arguments.push(OsString::from("--git-path"));
arguments.push(OsString::from(name));
}
return arguments;
}
/// What:
/// Read the query output as three paths without splitting on newlines.
/// `Result<Vec<Vec<u8>>, _>` is "three byte paths,
/// or a refusal".
/// Why:
/// A Git directory path may contain a newline.
/// The three files share one directory,
/// so its length follows from the output length and the result is checked byte for byte.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const directoryLength = (stdout.length - fixedLength) / 3; // then verify every byte
/// ```
pub fn sequencer_head_paths(stdout: &[u8]) -> Result<Vec<Vec<u8>>, SequencerOutputError> {
// Bytes that are not the directory: each name plus its trailing newline.
let mut fixed: usize = 0;
for name in SEQUENCER_HEAD_FILES {
fixed += name.len() + 1;
}
if stdout.len() < fixed {
// `Err(x)` is the failure case of `Result`.
return Err(SequencerOutputError);
}
// `.is_multiple_of(n)` is true when dividing by `n` leaves no remainder.
if !(stdout.len() - fixed).is_multiple_of(SEQUENCER_HEAD_FILES.len()) {
return Err(SequencerOutputError);
}
// The shared directory prefix, including its trailing separator.
let directory_length: usize = (stdout.len() - fixed) / SEQUENCER_HEAD_FILES.len();
// `&stdout[..n]` borrows the first `n` bytes.
let directory: &[u8] = &stdout[..directory_length];
let mut paths: Vec<Vec<u8>> = Vec::<Vec<u8>>::new();
let mut offset: usize = 0;
for name in SEQUENCER_HEAD_FILES {
// `.to_vec()` copies the directory bytes so the path can grow.
let mut path: Vec<u8> = directory.to_vec();
path.extend_from_slice(name.as_bytes());
let end: usize = offset + path.len();
if &stdout[offset..end] != path.as_slice() || stdout[end] != b'\n' {
return Err(SequencerOutputError);
}
offset = end + 1;
paths.push(path);
}
// `Ok(x)` is the success case of `Result`.
return Ok(paths);
}
/// What:
/// Decide from the three presence facts.
/// Why:
/// Any one head file means Git is waiting for the concluding commit.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// return present.includes(true) ? 'in-progress' : 'none';
/// ```
pub fn sequencer_state(facts: SequencerFacts) -> SequencerState {
if facts.merge_head || facts.cherry_pick_head || facts.revert_head {
return SequencerState::InProgress;
}
return SequencerState::NotInProgress;
}
/// What:
/// The state to use when the path query itself failed.
/// Why:
/// The incumbent falls back to normal enforcement and lets real Git report its error.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// catch (error) { if (error instanceof SubprocessError) return 'none'; throw error; }
/// ```
pub fn sequencer_state_when_query_fails() -> SequencerState {
return SequencerState::NotInProgress;
}
/// Output parsing,
/// the decision and real-Git controls of the query.
#[cfg(test)]
#[path = "rule_commit_sequencer_tests.rs"]
mod tests;