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
//! What:
//! The deterministic, parent-first list of entries a copy transfers,
//! read without following symbolic links,
//! and the application of the retained permission bits to a staged copy.
//! Why:
//! The stage and the source are compared entry by entry,
//! so both sides must enumerate in the same order and classify the same way.
//! Sockets, FIFOs and device nodes stop the walk before any of them is opened.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const entries = await collectEntryManifest({ root, selectedRoots, excludedRoots });
//! await applyEntryModes({ root: stageRoot, entries });
//! ```
use super::worktree_copy_error::raw_failure;
use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause};
use super::worktree_copy_selection::{filesystem_path, js_order};
use crate::json_record::quote;
use std::path::{Path, PathBuf};
/// What:
/// The permission and special-mode bits the copy contract retains.
/// Why:
/// Only these bits are compared and applied; file-type bits are never copied.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const PERMISSION_BITS = 0o7777;
/// ```
#[cfg(unix)]
const PERMISSION_BITS: u32 = 0o7777;
/// What:
/// The kind of one copied entry.
/// Why:
/// Each kind is copied, compared and modified differently.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type WorktreeCopyEntryKind = 'directory' | 'file' | 'symlink';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntryKind {
/// A directory, recreated rather than copied.
Directory,
/// A regular file, copied byte for byte.
File,
/// A symbolic link, recreated with its exact target text.
Symlink,
}
/// What:
/// One entry of a manifest: its kind, its retained mode and its repository path.
/// Why:
/// A manifest is compared with another manifest entry by entry.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type WorktreeCopyEntry = { kind: WorktreeCopyEntryKind; mode: number; relativePath: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorktreeCopyEntry {
/// Copy kind of the entry.
pub kind: EntryKind,
/// Retained permission bits;
/// zero where the platform has no such bits.
pub mode: u32,
/// Slash-separated repository path.
pub relative_path: String,
}
/// What:
/// One pending walk item: its native path and its repository path.
/// Why:
/// The walk keeps both, because exclusion works on native paths and output on repository paths.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type WalkItem = { filesystemPath: string; relativePath: string };
/// ```
struct WalkItem {
/// Native path under the walked root.
filesystem_path: PathBuf,
/// Repository path retained in the manifest.
relative_path: String,
}
/// What:
/// The copy kind of one no-follow filesystem entry,
/// or the failure for a socket, FIFO or device node.
/// Why:
/// Only three kinds can be copied; anything else must fail before it is opened.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function entryKind({ stats, path }): WorktreeCopyEntryKind;
/// ```
fn entry_kind(file_type: std::fs::FileType, path: &Path) -> Result<EntryKind, WorktreeCopyError> {
// `if` chains keep each supported kind next to its name; the last branch refuses the rest.
if file_type.is_dir() {
return Ok(EntryKind::Directory);
}
if file_type.is_file() {
return Ok(EntryKind::File);
}
if file_type.is_symlink() {
return Ok(EntryKind::Symlink);
}
let message: String = format!(
"cli-git: ignored path {} has unsupported filesystem type.",
quote(path.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), None));
}
/// What:
/// The permission bits of one no-follow entry, as the copy contract retains them.
/// Why:
/// The mode is read from the entry itself, never from what a symbolic link points to.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// stats.mode & 0o7777
/// ```
#[cfg(unix)]
pub fn retained_mode(metadata: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::MetadataExt;
return metadata.mode() & PERMISSION_BITS;
}
/// What:
/// The permission bits of one entry on a platform without Unix mode bits.
/// Why:
/// The copy is Unix-only in this lane; the zero mode keeps non-Unix builds compiling
/// without claiming the mode parity the incumbent provides there.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// stats.mode & 0o7777
/// ```
#[cfg(not(unix))]
pub fn retained_mode(_metadata: &std::fs::Metadata) -> u32 {
return 0;
}
/// What:
/// The complete manifest of a set of selected roots, parent before child, in JavaScript sort order.
/// Why:
/// A deterministic order makes the stage, the comparison and the journal reproducible.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function collectEntryManifest({ root, selectedRoots, excludedRoots }): Promise<readonly WorktreeCopyEntry[]>;
/// ```
pub fn collect_entry_manifest(
root: &Path,
selected_roots: &[String],
excluded_roots: &[PathBuf],
) -> Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> {
// The stack is filled in reverse so the lexically first root is popped first.
let mut pending: Vec<WalkItem> = Vec::new();
for relative_path in selected_roots.iter().rev() {
pending.push(WalkItem {
filesystem_path: filesystem_path(root, relative_path.as_str())?,
relative_path: relative_path.clone(),
});
}
let mut entries: Vec<WorktreeCopyEntry> = Vec::new();
while let Some(item) = pending.pop() {
if excluded_roots
.iter()
.any(|excluded| return item.filesystem_path.starts_with(excluded.as_path()))
{
continue;
}
let metadata: std::fs::Metadata = std::fs::symlink_metadata(item.filesystem_path.as_path())
.map_err(|error| return raw_failure(&error))?;
let kind: EntryKind = entry_kind(metadata.file_type(), item.filesystem_path.as_path())?;
entries.push(WorktreeCopyEntry {
kind,
mode: retained_mode(&metadata),
relative_path: item.relative_path.clone(),
});
if kind != EntryKind::Directory {
continue;
}
let mut names: Vec<String> = Vec::new();
for listed in std::fs::read_dir(item.filesystem_path.as_path())
.map_err(|error| return raw_failure(&error))?
{
let child: std::fs::DirEntry = listed.map_err(|error| return raw_failure(&error))?;
names.push(child.file_name().to_string_lossy().into_owned());
}
names.sort_by(|left, right| return js_order(left, right));
for name in names.iter().rev() {
let child_relative: String = format!("{}/{name}", item.relative_path);
pending.push(WalkItem {
filesystem_path: filesystem_path(root, child_relative.as_str())?,
relative_path: child_relative,
});
}
}
return Ok(entries);
}
/// What:
/// Applies each staged entry's retained mode, files first and then directories deepest first.
/// Why:
/// A restrictive directory mode would block changes to its children, so directories change last.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function applyEntryModes({ root, entries }): Promise<void>;
/// ```
#[cfg(unix)]
pub fn apply_entry_modes(
root: &Path,
entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
use std::os::unix::fs::PermissionsExt;
let files = entries
.iter()
.filter(|entry| return entry.kind == EntryKind::File);
let directories = entries
.iter()
.rev()
.filter(|entry| return entry.kind == EntryKind::Directory);
for entry in files.chain(directories) {
let path: PathBuf = filesystem_path(root, entry.relative_path.as_str())?;
std::fs::set_permissions(path.as_path(), std::fs::Permissions::from_mode(entry.mode))
.map_err(|error| return raw_failure(&error))?;
}
return Ok(());
}
/// What:
/// Applies modes on a platform without Unix mode bits, which leaves them untouched.
/// Why:
/// Keeps the call sites identical on every platform.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function applyEntryModes({ root, entries }): Promise<void>;
/// ```
#[cfg(not(unix))]
pub fn apply_entry_modes(
_root: &Path,
_entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
return Ok(());
}
/// Manifest controls stay out of the release executable.
#[cfg(test)]
#[path = "worktree_copy_manifest_tests.rs"]
mod tests;