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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! What:
//! A private copy of the real index,
//! in its own directory beside it,
//! removed when
//! the copy is dropped.
//! Why:
//! Predicting what `git add` would stage means running it against an index nobody
//! else reads,
//! so the real index never changes before the policies have answered.
//! The copy keeps the real index's access and modification times:
//! Git re-reads an
//! entry whose stat data is not older than the index file,
//! and a copy with a fresh
//! time would make it trust stat data from the same second as a later edit of the
//! same size,
//! so the prediction could miss that edit (the installed wrapper's
//! `index-file-timestamps.ts`,
//! issue #544).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // await using index = await createPrivateIndex(realIndexPath); // removed on dispose
//! ```
/// Import the layer's failure type and its closed list of causes.
use super::candidate_error::{CandidateError, CandidateFailure};
/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
use std::path::{Path, PathBuf};
/// What:
/// `AtomicU64` is a counter that several threads may increase without a lock;
/// `Ordering::Relaxed` asks only that each increase is counted once.
/// Why:
/// Two private indexes made by one process in the same nanosecond still get
/// different directory names.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// let created = 0n;
/// ```
use std::sync::atomic::{AtomicU64, Ordering};
/// The name prefix of every private index directory;
/// the installed wrapper uses the same one.
pub const PRIVATE_INDEX_PREFIX: &str = "cli-git-add-policy-";
/// How many unique names this process has made,
/// for private index directories and sibling files.
static CREATED: AtomicU64 = AtomicU64::new(0);
/// What:
/// A name part no other file of this process or another one chose:
/// the process ID,
/// the time in nanoseconds and this process's count,
/// joined by `-`.
/// Why:
/// Private index directories and the direct fix's sibling files are created beside
/// files other processes use,
/// so their names must not collide.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const uniqueSuffix = () => `${process.pid}-${nowNanoseconds()}-${created++}`;
/// ```
pub fn unique_suffix() -> String {
// What: `.fetch_add(1, ..)` returns the old count and stores one more;
// `.as_nanos()` is the time since the epoch in nanoseconds.
// Why: Process ID, time and count together name a file no one else chose.
//
// In TS you'd write (pseudocode):
// ```ts
// const count = created++;
// ```
let count: u64 = CREATED.fetch_add(1, Ordering::Relaxed);
let nanoseconds: u128 = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
{
Ok(elapsed) => elapsed.as_nanos(),
// A clock before 1970 still leaves the process ID and the count to tell names apart.
Err(_) => 0,
};
return format!("{}-{nanoseconds}-{count}", std::process::id());
}
/// What:
/// A private index file in a directory this value owns.
/// Fields are private,
/// so the
/// directory can be removed only by dropping the value.
/// Why:
/// Every path out of a prediction,
/// failures included,
/// removes the directory.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class PrivateIndex { readonly #directory: string; readonly #index: string; [Symbol.dispose]() {} }
/// ```
#[derive(Debug)]
pub struct PrivateIndex {
/// The directory that holds the copy and nothing else of the repository.
directory: PathBuf,
/// The private index file Git is pointed at.
index: PathBuf,
}
/// What:
/// Build the failure of a private index that could not be prepared.
/// `&std::io::Error` borrows the operating system's reason.
/// Why:
/// The message names the step and the reason;
/// the directory is under the Git
/// directory,
/// never a candidate pathname.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function privateIndexFailure(step: string, error: Error): CandidateError;
/// ```
fn private_index_failure(step: &str, error: &std::io::Error) -> CandidateError {
return CandidateError::new(
CandidateFailure::PrivateIndexUnavailable,
format!("cli-git could not prepare a private copy of the index: {step}: {error}.").as_str(),
);
}
/// What:
/// Create a directory only this user can read,
/// failing when it already exists.
/// Why:
/// The copy holds the repository's staged entries;
/// a fresh directory also proves
/// no other process chose the same name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await mkdir(directory, { mode: 0o700 }); // throws EEXIST
/// ```
fn create_private_directory(directory: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
/// `DirBuilderExt` adds `mode`,
/// the permissions a new directory is created with.
use std::os::unix::fs::DirBuilderExt;
// `.mode(0o700)` configures the temporary builder; `.create` makes the directory.
return std::fs::DirBuilder::new().mode(0o700).create(directory);
}
#[cfg(not(unix))]
{
// Other systems have no mode bits to set; the directory is created as it is.
return std::fs::DirBuilder::new().create(directory);
}
}
/// What:
/// Copy the real index into `destination` and give the copy the real index's times.
/// `Ok(false)` means the real index does not exist,
/// which Git reads as an empty index.
/// Why:
/// A missing index needs no copy:
/// Git treats a missing index file as empty,
/// so the
/// private index path can simply name a file that does not exist yet.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function copyIndex(source: string, destination: string): Promise<boolean>;
/// ```
fn copy_index(source: &Path, destination: &Path) -> std::io::Result<bool> {
// `match` on the metadata read: a missing file is an answer, every other error is a failure.
let metadata: std::fs::Metadata = match std::fs::metadata(source) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error),
};
// A trailing `?` returns the copy failure to our caller.
std::fs::copy(source, destination)?;
// What: `FileTimes::new().set_accessed(..).set_modified(..)` collects both times;
// `.set_times(..)` writes them to the open copy.
// Why: Git's racy-entry protection compares entry times with the index file's time.
//
// In TS you'd write (pseudocode):
// ```ts
// await utimes(destination, stats.atime, stats.mtime);
// ```
let times: std::fs::FileTimes = std::fs::FileTimes::new()
.set_accessed(metadata.accessed()?)
.set_modified(metadata.modified()?);
std::fs::File::options()
.write(true)
.open(destination)?
.set_times(times)?;
return Ok(true);
}
/// What:
/// `impl PrivateIndex { ... }` attaches the constructor and the accessor.
/// Why:
/// The directory is created and filled in one step,
/// so no caller sees a half-made copy.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class PrivateIndex { static create(realIndex: string): PrivateIndex; get path(): string }
/// ```
impl PrivateIndex {
/// What:
/// Create the directory beside `real_index` and copy the index into it.
/// Why:
/// Beside the real index keeps the copy on the same filesystem and inside the
/// Git directory,
/// where the installed wrapper also put it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// static async create(realIndex: string): Promise<PrivateIndex>;
/// ```
pub fn create(real_index: &Path) -> Result<PrivateIndex, CandidateError> {
let Some(parent) = real_index.parent() else {
return Err(CandidateError::new(
CandidateFailure::PrivateIndexUnavailable,
"cli-git could not prepare a private copy of the index: Git named an index path without a directory.",
));
};
let directory: PathBuf = parent.join(format!("{PRIVATE_INDEX_PREFIX}{}", unique_suffix()));
if let Err(error) = create_private_directory(directory.as_path()) {
return Err(private_index_failure("creating its directory", &error));
}
// From here on the value owns the directory, so a failure below removes it on drop.
let private: PrivateIndex = PrivateIndex {
index: directory.join("index"),
directory,
};
if let Err(error) = copy_index(real_index, private.index.as_path()) {
return Err(private_index_failure("copying the index", &error));
}
return Ok(private);
}
/// What:
/// The private index file Git is pointed at.
/// Why:
/// The caller sets `GIT_INDEX_FILE` to it for every command of the prediction.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// get path(): string
/// ```
pub fn path(&self) -> &Path {
return self.index.as_path();
}
}
/// What:
/// `impl Drop for PrivateIndex` runs `drop` when the value goes out of scope,
/// like
/// a `[Symbol.dispose]` that the language calls on every exit path.
/// Why:
/// The directory and everything Git wrote into it are removed whether the
/// prediction succeeded or failed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// [Symbol.dispose]() { rmSync(this.#directory, { recursive: true, force: true }); }
/// ```
impl Drop for PrivateIndex {
/// What:
/// Remove the directory and everything in it.
/// `&mut self` lends the value for
/// its last use.
/// Why:
/// Called by the language on every exit path,
/// failures included.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// [Symbol.dispose](): void
/// ```
fn drop(&mut self) {
// What: `let _ = ...` discards the removal's `Result` on purpose.
// Why: A drop cannot report a failure; a leftover directory under the Git
// directory is inert and carries the prefix that names its owner.
//
// In TS you'd write (pseudocode):
// ```ts
// try { rmSync(directory, { recursive: true }); } catch { /* inert leftover */ }
// ```
let _ = std::fs::remove_dir_all(self.directory.as_path());
}
}
/// Copy,
/// timestamp and removal controls stay out of the release executable.
#[cfg(test)]
#[path = "candidate_private_index_tests.rs"]
mod tests;