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
//! What:
//! The reversible, flat, case-insensitive-safe file name of one branch's push coordination files.
//! Why:
//! The per-branch push lock and `last-pushed` record live under `<git-common-dir>/cli-git/push/`,
//! and the incumbent (`src/auto-push-branch-key.ts`) and this wrapper must name them identically,
//! because both wrappers contend on the same lock while both are installed.
//! Uppercase letters are escaped too, so refs that differ only in case never share a file
//! on a case-insensitive filesystem.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! encodeBranchKey('refs/heads/Main'); // 'refs%2Fheads%2F%4Dain'
//! ```
/// What:
/// A push coordination key that `encode_branch_key` never produces.
/// Why:
/// Decoding a key that the incumbent did not write must stop the caller,
/// never guess a ref name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class BranchKeyError extends Error {}
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BranchKeyError {
/// Diagnostic naming the key and the position of the fault.
pub message: String,
}
/// Displays the diagnostic only.
impl std::fmt::Display for BranchKeyError {
/// Writes the diagnostic text.
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(&self.message);
}
}
/// Marks the key failure as a standard error.
impl std::error::Error for BranchKeyError {}
/// The escape introducer `%`.
const ESCAPE: u8 = b'%';
/// Whether a byte is kept verbatim: lowercase ASCII letters, digits, `-`, `_` and `.`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const VERBATIM_CHARACTERS = new Set('abcdefghijklmnopqrstuvwxyz0123456789-_.');
/// ```
fn is_verbatim(byte: u8) -> bool {
return byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| byte == b'-'
|| byte == b'_'
|| byte == b'.';
}
/// What:
/// Encode a full ref name as one flat file name component.
/// `&str` is borrowed UTF-8 text.
/// Why:
/// Each UTF-8 byte outside the verbatim set becomes `%XX` with uppercase hexadecimal digits,
/// so every ref maps to one reversible name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function encodeBranchKey(refName: string): string;
/// ```
pub fn encode_branch_key(ref_name: &str) -> String {
let mut key = String::with_capacity(ref_name.len());
for &byte in ref_name.as_bytes() {
if is_verbatim(byte) {
key.push(char::from(byte));
} else {
key.push_str(&format!("%{byte:02X}"));
}
}
return key;
}
/// What:
/// Read one `%XX` escape starting at `index`, the position of the introducer.
/// Why:
/// Only the uppercase form `encode_branch_key` writes is accepted,
/// and an escape of a verbatim byte is refused,
/// so every ref name has exactly one accepted key.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function decodeEscape({ key, index }): number; // throws BranchKeyError
/// ```
fn decode_escape(key: &[u8], index: usize) -> Result<u8, BranchKeyError> {
let digits = key.get(index + 1..index + 3).unwrap_or(&[]);
if digits.len() != 2
|| !digits.iter().all(|digit| {
return is_upper_hex(*digit);
})
{
return Err(BranchKeyError {
message: format!(
"Push coordination key has a malformed escape at {index}: {}",
String::from_utf8_lossy(key)
),
});
}
let high = hex_value(digits[0]);
let low = hex_value(digits[1]);
let byte = high * 16 + low;
if is_verbatim(byte) {
return Err(BranchKeyError {
message: format!(
"Push coordination key escapes a verbatim character at {index}: {}",
String::from_utf8_lossy(key)
),
});
}
return Ok(byte);
}
/// Whether a byte is an uppercase hexadecimal digit, the only digits `encode_branch_key` writes.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// /[0-9A-F]/.test(digit)
/// ```
fn is_upper_hex(digit: u8) -> bool {
return digit.is_ascii_digit() || (b'A'..=b'F').contains(&digit);
}
/// The value of one uppercase hexadecimal digit; the caller has checked the digit.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// Number.parseInt(digit, 16)
/// ```
fn hex_value(digit: u8) -> u8 {
if digit.is_ascii_digit() {
return digit - b'0';
}
return digit - b'A' + 10;
}
/// What:
/// Reverse `encode_branch_key` in one left-to-right pass.
/// `Result<A, B>` is either the decoded ref name or a `BranchKeyError`.
/// Why:
/// A key with an unescaped non-verbatim character, a malformed escape,
/// or bytes that are not UTF-8 names no branch and must stop the caller.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function decodeBranchKey(key: string): string; // throws BranchKeyError
/// ```
pub fn decode_branch_key(key: &str) -> Result<String, BranchKeyError> {
let bytes = key.as_bytes();
let mut decoded: Vec<u8> = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if byte == ESCAPE {
decoded.push(decode_escape(bytes, index)?);
index += 3;
} else if is_verbatim(byte) {
decoded.push(byte);
index += 1;
} else {
return Err(BranchKeyError {
message: format!(
"Push coordination key holds an unescaped character at {index}: {key}"
),
});
}
}
return match String::from_utf8(decoded) {
Ok(text) => Ok(text),
Err(error) => Err(BranchKeyError {
message: format!("Push coordination key is not UTF-8 ({error}): {key}"),
}),
};
}
/// Encoding and decoding behave as the incumbent's branch key codec.
#[cfg(test)]
#[path = "auto_push_branch_key_tests.rs"]
mod tests;