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
//! What:
//! Which `git checkout` and `git restore` command lines take one side of a conflicted file, and the paths they name.
//! Why:
//! During a merge, a rebase, a cherry-pick or a revert the usual way to settle a conflict is
//! `git checkout --ours|--theirs -- <path>`, `git checkout <rev> -- <path>` or the `git restore` forms of the same.
//! The linked-worktree policy rejects them as discarding forms; the conflict-side guard lets them through for conflicted
//! paths after saving the file. Only a command line whose every token is understood qualifies: a form with `--force`,
//! `--patch`, `--pathspec-from-file`, a branch option, an abbreviated `git restore` option, an index-only restore, or no
//! `--` before the paths stays rejected, because what it would discard is not known from the arguments alone.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! conflictSideRequest('checkout', ['--theirs', '--', 'a.rs']) // { paths: ['a.rs'] }
//! conflictSideRequest('checkout', ['-f', '--', 'a.rs']) // undefined
//! ```
use super::command_checkout_table::{
CHECKOUT_TABLE, DETACH, FORCE, NEW_BRANCH, NEW_BRANCH_FORCE, ORPHAN, PATCH, PATHSPEC_FROM_FILE,
};
use super::command_options::{Boundary, DEFAULT_MODE, ParsedOptions, parse_options};
use super::command_options_query::is_stated;
use std::ffi::OsString;
/// The paths a qualifying command line names, as typed.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SideRequest {
/// The pathspec tokens after `--`, in order, none of them resolved or checked yet.
pub paths: Vec<OsString>,
}
/// What:
/// Read a `git checkout` region for the conflict-side forms.
/// Why:
/// The form is `checkout [options] [<tree-ish>] -- <path>...`, where the options are the ones that choose a side or
/// how a file is written. Any option that creates or moves a branch, forces, patches or reads paths from a file
/// disqualifies the line.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function checkoutSideRequest(region: string[]): SideRequest | undefined;
/// ```
fn checkout_request(region: &[OsString]) -> Option<SideRequest> {
let parsed: ParsedOptions = parse_options(region, CHECKOUT_TABLE, DEFAULT_MODE, &[]).ok()?;
let Boundary::DashDash(at) = parsed.boundary else {
return None;
};
for forbidden in [NEW_BRANCH, NEW_BRANCH_FORCE, ORPHAN, DETACH, PATCH, FORCE, PATHSPEC_FROM_FILE] {
if is_stated(&parsed, forbidden) {
return None;
}
}
if parsed.leading.len() > 1 {
return None;
}
let paths: Vec<OsString> = region[at + 1..].to_vec();
if paths.is_empty() {
return None;
}
return Some(SideRequest { paths });
}
/// The spellings of `git restore` options that take no value and keep the command a worktree restore of named paths.
const RESTORE_PLAIN: [&[u8]; 14] = [
b"--ours",
b"--theirs",
b"-m",
b"--merge",
b"-W",
b"--worktree",
b"-q",
b"--quiet",
b"--progress",
b"--no-progress",
b"--overlay",
b"--no-overlay",
b"--ignore-unmerged",
b"--ignore-skip-worktree-bits",
];
/// The spellings of `git restore` options that take a value, as `--name=value` or as the next token.
const RESTORE_VALUED: [&[u8]; 3] = [b"--source", b"-s", b"--conflict"];
/// What:
/// Read a `git restore` region for the conflict-side forms.
/// Why:
/// `git restore` has no option table in this crate, and Git accepts abbreviations, so the reading is exact: a token
/// that is not one of the listed spellings disqualifies the line. A restore that names the index and not the worktree
/// (`--staged` alone) writes no file and is not a way to take a side, so it stays rejected as before.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function restoreSideRequest(region: string[]): SideRequest | undefined;
/// ```
fn restore_request(region: &[OsString]) -> Option<SideRequest> {
let mut at: usize = 0;
while at < region.len() {
let token: &[u8] = region[at].as_encoded_bytes();
if token == b"--" {
let paths: Vec<OsString> = region[at + 1..].to_vec();
if paths.is_empty() {
return None;
}
return Some(SideRequest { paths });
}
if RESTORE_PLAIN.contains(&token) {
at += 1;
continue;
}
// `--staged` beside `--worktree` still writes files; `--staged` alone does not, and is not accepted.
if token == b"-S" || token == b"--staged" {
let writes_worktree: bool = region.iter().take_while(|each| { return each.as_encoded_bytes() != b"--"; }).any(|each| {
return matches!(each.as_encoded_bytes(), b"-W" | b"--worktree");
});
if !writes_worktree {
return None;
}
at += 1;
continue;
}
let name: &[u8] = token.split(|byte| { return *byte == b'='; }).next().unwrap_or_default();
if RESTORE_VALUED.contains(&name) {
// `--source=<tree>` carries its value; `--source <tree>` takes the next token.
at += if name.len() < token.len() { 1 } else { 2 };
continue;
}
return None;
}
// No `--`: the paths could be mistaken for option values, so this form is left to the usual rejection.
return None;
}
/// What:
/// The conflict-side reading of a subcommand and its region, or nothing when the command line does not qualify.
/// Why:
/// The caller asks only for `checkout` and `restore`; every other command word is not a way to take a side.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function conflictSideRequest(word: string, region: string[]): SideRequest | undefined;
/// ```
pub fn conflict_side_request(word: &[u8], region: &[OsString]) -> Option<SideRequest> {
if word == b"checkout" {
return checkout_request(region);
}
if word == b"restore" {
return restore_request(region);
}
return None;
}
/// Each spelling that qualifies and each that does not.
#[cfg(test)]
#[path = "command_conflict_side_tests.rs"]
mod tests;