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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//! What:
//! Pure decision of the linked-worktree-only policy:
//! commands that can discard
//! worktree state run only in a linked worktree.
//! Why:
//! `git stash`,
//! a deleting `git clean` and a `git reset` that rewrites files can throw
//! away work in the primary checkout.
//! The policy confines them to linked worktrees.
//! This file decides from the arguments alone whether the command is guarded,
//! and
//! from the worktree kind the caller measured whether to reject it.
//! It starts no
//! process.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // decideLinkedWorktree(['clean', '-fd']) => { kind: 'needs-effective-target', command: 'clean' }
//! ```
/// What:
/// `use` brings names from sibling files into this file;
/// `super::` means "the parent
/// module",
/// where every sibling file of this crate is declared.
/// Why:
/// Whether a clean deletes and whether a reset rewrites files come from Git's own
/// option tables of those commands.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { cleanChangesWorktree, parseCleanRegion } from './command_clean.ts';
/// ```
use super::command_clean::{CleanRegion, clean_changes_worktree, parse_clean_region};
use super::command_options::OptionError;
use super::command_reset::{ResetRegion, parse_reset_region, reset_changes_worktree};
use super::effective_target::EffectiveTarget;
use super::global_arguments::command_tokens;
/// What:
/// `OsString` is owned operating-system text of raw bytes.
/// Sibling the reader might
/// expect:
/// `String`,
/// which must be valid UTF-8.
/// Why:
/// Arguments are compared as bytes and never decoded.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string, but byte-preserving.
/// ```
use std::ffi::OsString;
/// What:
/// The finding code of a linked-worktree rejection.
/// `&str` is borrowed text baked
/// into the program.
/// Why:
/// Callers identify the finding by this stable code.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const LINKED_WORKTREE_REQUIRED_CODE = 'linked-worktree-required';
/// ```
pub const LINKED_WORKTREE_REQUIRED_CODE: &str = "linked-worktree-required";
/// What:
/// The commands the policy guards.
/// An `enum` is a closed set of named alternatives.
/// `#[derive(...)]` asks the compiler to generate copying,
/// debug printing and `==`.
/// Why:
/// Each has its own rejection text.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type GuardedCommand = 'stash' | 'clean' | 'reset';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GuardedCommand {
/// Every `git stash` form.
Stash,
/// A `git clean` that is not a dry run.
Clean,
/// A `git reset` with `--hard`,
/// `--merge` or `--keep`.
Reset,
}
/// What:
/// The outcome of looking at the argument list only.
/// Why:
/// Most commands are not guarded and need no repository query at all.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type LinkedWorktreeDecision = { kind: 'pass' } | { kind: 'needs-effective-target'; command: GuardedCommand };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LinkedWorktreeDecision {
/// Not a guarded command,
/// a harmless form,
/// or a region Git refuses.
Pass,
/// The caller classifies the worktree the command targets,
/// then calls
/// `resolve_linked_worktree`.
NeedsEffectiveTarget(GuardedCommand),
}
/// What:
/// Whether a guarded region can change worktree files.
/// `&[u8]` borrows the
/// subcommand word;
/// `Option<T>` is "a value or nothing":
/// nothing for another command.
/// `Result<A, B>` is "either success `A` or failure `B`".
/// Why:
/// `stash` is guarded in every form,
/// as the incumbent guards it;
/// `clean` and `reset`
/// only in the forms Git lets touch files.
/// Gotcha:
/// A trailing `?` returns Git's refusal to our caller,
/// or unwraps the facts.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function guardedCommand(word: string, region: string[]): GuardedCommand | undefined; // throws OptionError
/// ```
fn guarded_command(
word: &[u8],
region: &[OsString],
) -> Result<Option<GuardedCommand>, OptionError> {
if word == b"stash" {
// `Ok(Some(x))` is success carrying a present value.
return Ok(Some(GuardedCommand::Stash));
}
if word == b"clean" {
// `&[]` is an empty flag list: wrapper controls were removed before this point.
let facts: CleanRegion = parse_clean_region(region, &[])?;
// `&facts` lends the facts read-only.
if clean_changes_worktree(&facts) {
return Ok(Some(GuardedCommand::Clean));
}
}
if word == b"reset" {
let facts: ResetRegion = parse_reset_region(region, &[])?;
if reset_changes_worktree(&facts) {
return Ok(Some(GuardedCommand::Reset));
}
}
// `Ok(None)` is success carrying "not guarded".
return Ok(None);
}
/// What:
/// Decide from the argument list.
/// `&[OsString]` borrows the arguments,
/// already free
/// of wrapper controls.
/// Why:
/// A command that is not guarded,
/// or that Git itself refuses,
/// needs no measurement.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function decideLinkedWorktree(args: string[]): LinkedWorktreeDecision;
/// ```
pub fn decide_linked_worktree(arguments: &[OsString]) -> LinkedWorktreeDecision {
// `let Some((a, b)) = ... else { ... };` unpacks the command word and the tokens after
// it, or returns when Git runs no subcommand.
let Some((word, region)) = command_tokens(arguments) else {
return LinkedWorktreeDecision::Pass;
};
// `.as_encoded_bytes()` lends the raw bytes of the subcommand word.
let guarded: Result<Option<GuardedCommand>, OptionError> =
guarded_command(word.as_encoded_bytes(), region);
// `if let Ok(Some(command)) = ...` runs only for an accepted, guarded region.
if let Ok(Some(command)) = guarded {
return LinkedWorktreeDecision::NeedsEffectiveTarget(command);
}
return LinkedWorktreeDecision::Pass;
}
/// What:
/// The rejection text for a command run where there is no worktree.
/// `&'static str`
/// is text baked into the program.
/// Why:
/// The text says what the command could destroy and how to run it safely.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function outsideWorktreeMessage({ command }): string;
/// ```
pub fn outside_worktree_message(command: GuardedCommand) -> &'static str {
// `match` picks one arm per variant; the compiler refuses a forgotten variant.
match command {
GuardedCommand::Stash => {
return "cli-git: git stash requires the effective working directory to be inside a linked git worktree. \
Refusing to run from outside a worktree because git stash can revert filesystem state outside what the caller expected. \
cd to a linked worktree root or pass -C <linked-worktree-root> before stash.";
}
GuardedCommand::Clean => {
return "cli-git: state-changing git clean requires the effective working directory to be inside a linked git worktree. \
Refusing to run from outside a worktree because git clean can delete filesystem state outside what the caller expected. \
cd to a linked worktree root or pass -C <linked-worktree-root> before clean, or use --dry-run to inspect.";
}
GuardedCommand::Reset => {
return "cli-git: destructive git reset modes require the effective working directory to be inside a linked git worktree. \
Refusing to run from outside a worktree because git reset --hard, --merge, and --keep can rewrite tracked files outside what the caller expected. \
cd to a linked worktree root or pass -C <linked-worktree-root> before reset.";
}
}
}
/// What:
/// The rejection text for a command run in the main worktree.
/// Why:
/// The text says what the command could destroy and where to run it instead.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function mainWorktreeMessage({ command }): string;
/// ```
pub fn main_worktree_message(command: GuardedCommand) -> &'static str {
match command {
GuardedCommand::Stash => {
return "cli-git: git stash is rejected in the main git worktree. \
Refusing to run because git stash can revert primary checkout filesystem state outside what the caller expected. \
Use a linked worktree for stash operations.";
}
GuardedCommand::Clean => {
return "cli-git: state-changing git clean is rejected in the main git worktree. \
Refusing to run because git clean can delete primary checkout filesystem state outside what the caller expected. \
Use a linked worktree for state-changing clean operations, or use --dry-run to inspect.";
}
GuardedCommand::Reset => {
return "cli-git: destructive git reset modes are rejected in the main git worktree. \
Refusing to run because git reset --hard, --merge, and --keep can rewrite primary checkout files outside what the caller expected. \
Use a linked worktree for destructive reset operations.";
}
}
}
/// What:
/// Decide from the measured worktree kind:
/// the rejection text,
/// or nothing to let the
/// command through.
/// Why:
/// A linked worktree and an allowlisted tool cache are the two places where these
/// commands may run.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function resolveLinkedWorktree(command: GuardedCommand, target: EffectiveTarget): string | undefined;
/// ```
pub fn resolve_linked_worktree(
command: GuardedCommand,
target: EffectiveTarget,
) -> Option<&'static str> {
match target {
// `Some(x)` is the "present" case of `Option`; `None` is the "absent" case.
EffectiveTarget::OutsideWorktree => return Some(outside_worktree_message(command)),
EffectiveTarget::MainWorktree => return Some(main_worktree_message(command)),
EffectiveTarget::LinkedWorktree | EffectiveTarget::Allowlisted => return None,
}
}
/// Guarded forms,
/// harmless forms and every rejection text.
#[cfg(test)]
#[path = "rule_linked_worktree_tests.rs"]
mod tests;