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
//! What:
//! Links every non-private entry of the real common Git directory into a shadow repository,
//! and copies the per-worktree entries (`config.worktree`, a linked worktree's sparse checkout).
//! Why:
//! Shared entries such as `hooks`, `info`, `rr-cache`, `lfs` and `modules` must be the real ones,
//! so hooks and native Git see them;
//! transaction-private entries (refs, index, objects, conclusion state, lock files) stay the shadow's own
//! (`src/shadow-repository/shadow-links.ts`).
//! On Windows the incumbent uses junctions and copies;
//! this port uses directory symbolic links and copies, because the standard library has no junction API.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await linkShadowEntries({ shadowPath, commonDir, gitDir });
//! ```
/// The conclusion state names a shadow keeps privately.
use super::conclusion_cleanup::{CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY};
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};
/// Real common-directory entries the shadow never links:
/// its ref store, index, config, object store, message file, copied conclusion state,
/// and cli-git's own state, which contains the shadow itself.
pub const PRIVATE_SHADOW_ENTRIES: [&str; 13] = [
"HEAD",
"refs",
"packed-refs",
"reftable",
"logs",
"index",
"config",
"config.worktree",
"objects",
"COMMIT_EDITMSG",
"cli-git",
"cli-git-transactions",
"cli-git-transaction",
];
/// What:
/// Reports whether an entry must stay private to the shadow.
/// Why:
/// A shadow Git that locks the same name as the real owner would collide with it,
/// so lock files are never linked.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// isPrivateShadowEntry(name)
/// ```
pub fn is_private_shadow_entry(name: &str) -> bool {
return PRIVATE_SHADOW_ENTRIES.contains(&name)
|| CONCLUSION_STATE_FILES.contains(&name)
|| name == SEQUENCER_DIRECTORY
|| name.ends_with(".lock");
}
/// What:
/// Removes whatever sits at a shadow entry name, without following a link.
/// Why:
/// A link in the shadow is removed itself, never its target in the real repository.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await rm(destination, { recursive: true, force: true });
/// ```
fn remove_entry(path: &Path) -> std::io::Result<()> {
return match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_dir() => std::fs::remove_dir_all(path),
Ok(_) => std::fs::remove_file(path),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
};
}
/// What:
/// Shares one real entry with the shadow.
/// Why:
/// Unix links every entry;
/// Windows links directories and copies files, since a file link needs a privilege.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await symlink(source, destination);
/// ```
fn share_entry(source: &Path, destination: &Path, directory: bool) -> std::io::Result<()> {
#[cfg(unix)]
{
let _ = directory;
return std::os::unix::fs::symlink(source, destination);
}
#[cfg(windows)]
{
if directory {
return std::os::windows::fs::symlink_dir(source, destination);
}
return std::fs::copy(source, destination).map(|_| ());
}
}
/// What:
/// Links every shared entry of one real directory into one shadow directory.
/// Why:
/// Entries are typed without following links,
/// so a link in the real directory is shared as a link.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await linkDirectoryEntries({ sourceDirectory, shadowDirectory, skip });
/// ```
fn link_directory_entries(
source_directory: &Path,
shadow_directory: &Path,
skip: impl Fn(&str) -> bool,
) -> std::io::Result<()> {
for entry_result in std::fs::read_dir(source_directory)? {
let entry: std::fs::DirEntry = entry_result?;
let name: String = entry.file_name().to_string_lossy().into_owned();
if skip(name.as_str()) {
continue;
}
let destination: PathBuf = shadow_directory.join(&name);
// `git init` may have created a template entry of the same name; the real one replaces it.
remove_entry(destination.as_path())?;
share_entry(
entry.path().as_path(),
destination.as_path(),
entry.file_type()?.is_dir(),
)?;
}
return Ok(());
}
/// What:
/// Reports whether a regular per-worktree file exists.
/// Why:
/// A missing file is an answer, every other failure is an error.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await regularFileExists(path);
/// ```
fn regular_file_exists(path: &Path) -> std::io::Result<bool> {
return match std::fs::symlink_metadata(path) {
Ok(metadata) => Ok(metadata.is_file()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
};
}
/// What:
/// Links the real common directory into the shadow and copies the per-worktree entries.
/// Why:
/// A linked worktree's `info/sparse-checkout` lives outside the common directory,
/// so its `info` directory is private and only its shared entries are linked.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await linkShadowEntries({ shadowPath, commonDir, gitDir });
/// ```
pub fn link_shadow_entries(shadow: &Path, common_dir: &Path, git_dir: &Path) -> std::io::Result<()> {
let per_worktree_sparse: PathBuf = git_dir.join("info").join("sparse-checkout");
let mixed_info: bool =
git_dir != common_dir && regular_file_exists(per_worktree_sparse.as_path())?;
link_directory_entries(common_dir, shadow, |name| {
return is_private_shadow_entry(name) || (mixed_info && name == "info");
})?;
let worktree_config: PathBuf = git_dir.join("config.worktree");
if regular_file_exists(worktree_config.as_path())? {
std::fs::copy(&worktree_config, shadow.join("config.worktree"))?;
}
if !mixed_info {
return Ok(());
}
let shadow_info: PathBuf = shadow.join("info");
remove_entry(shadow_info.as_path())?;
std::fs::create_dir(&shadow_info)?;
set_private_directory(shadow_info.as_path())?;
let shared_info: PathBuf = common_dir.join("info");
match link_directory_entries(shared_info.as_path(), shadow_info.as_path(), |name| {
return name == "sparse-checkout";
}) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
std::fs::copy(&per_worktree_sparse, shadow_info.join("sparse-checkout"))?;
return Ok(());
}
/// What:
/// Gives a directory the private mode of the transaction's directories.
/// Why:
/// The shadow's private `info` holds a copy of per-worktree state.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await mkdir(shadowInfo, { mode: 0o700 });
/// ```
#[cfg(unix)]
fn set_private_directory(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
}
/// Windows has no mode bits for this directory; the incumbent's ACL work is not ported.
#[cfg(windows)]
fn set_private_directory(_path: &Path) -> std::io::Result<()> {
return Ok(());
}
/// Shadow link controls stay out of the release executable.
#[cfg(test)]
#[path = "shadow_links_tests.rs"]
mod tests;