git_policy_cli/worktree_copying/worktree_copy_registry.rs
1//! What:
2//! The linked worktree a `git worktree add` registered at the destination its command line names:
3//! whether that destination was a registered worktree before real Git ran, and whether it is one registered by this
4//! command after Git returned, with the canonical root of every registered worktree.
5//! Why:
6//! The destination is read from the command line and its registration is found by following the pointer in its `.git`
7//! file, so a concurrent add in the same repository can neither be mistaken for this one's worktree nor make this one wait
8//! (the earlier comparison of the whole set of registrations needed a lock to keep other adds out of its window).
9//! A worktree that Git registered before failing still receives the ignored files,
10//! so the check runs after Git returns whatever its status was.
11//! The main worktree is observed only through `observe_copy_source`, which the caller reaches only for a command that
12//! creates a worktree, so its other invocations cost no filesystem reads.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const observation = await observeWorktreeRepository({ args, gitPath });
17//! const { created, registeredRoots } = await findCreatedWorktree({ observation, destination, registeredBefore, gitPath });
18//! ```
19
20use crate::git_metadata::path_from_git_bytes;
21use crate::transaction_git::{GitContext, GitRequest, run_git};
22use crate::worktree_add_binding::bind_registration;
23use crate::worktree_copy_error::{WorktreeCopyError, raw_failure};
24use crate::worktree_copy_selection::CopySelection;
25use crate::worktree_identity::WorktreeIdentity;
26use std::path::{Path, PathBuf};
27
28/// What:
29/// The facts captured before real Git runs for an applicable invocation.
30/// Why:
31/// The copy after Git needs the common directory that owns the journals, the source worktree and which of its ignored
32/// paths are taken; nothing of the repository's other worktrees is read, so a concurrent add is none of its business.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// type WorktreeCopyObservation = { commonDir; effectiveCwd; sourceRoot?; selection };
37/// ```
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct WorktreeCopyObservation {
40 /// Canonical common Git directory, which owns the journals.
41 pub common_dir: PathBuf,
42 /// Working directory the Git queries run in.
43 pub effective_cwd: PathBuf,
44 /// Canonical source worktree root;
45 /// absent for a bare repository.
46 pub source_root: Option<PathBuf>,
47 /// Which of the source's ignored paths the copy takes.
48 pub selection: CopySelection,
49}
50
51/// What:
52/// One linked worktree this command registered.
53/// Why:
54/// Its root is where the copy lands.
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// type CreatedWorktree = { root: string };
59/// ```
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct CreatedWorktree {
62 /// Canonical created worktree root.
63 pub root: PathBuf,
64}
65
66/// What:
67/// The worktree this command created, if any, and every registered root that recursive copying must exclude.
68/// Why:
69/// A nested registered worktree is never copied into its parent's copy.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// { created: CreatedWorktree[]; registeredRoots: string[] }
74/// ```
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct CreatedWorktrees {
77 /// The linked worktree registered at the destination, absent when this command registered none.
78 pub created: Vec<CreatedWorktree>,
79 /// Every registered root that exists on the filesystem.
80 pub registered_roots: Vec<PathBuf>,
81}
82
83/// What:
84/// Captures what the copy after Git needs for one repository identity.
85/// Why:
86/// The main worktree and a repository outside any worktree return `None` before any read,
87/// so their invocations stay on the plain forwarding path.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// async function observeWorktreeRepository({ args, gitPath }): Promise<WorktreeCopyObservation | typeof WORKTREE_COPY_NOT_APPLICABLE>;
92/// ```
93pub fn observe_worktree_repository(
94 identity: &WorktreeIdentity,
95 effective_cwd: &Path,
96) -> Result<Option<WorktreeCopyObservation>, WorktreeCopyError> {
97 // `match` decides applicability from the identity alone; only the two repository kinds are observed.
98 match identity {
99 WorktreeIdentity::OutsideWorktree | WorktreeIdentity::MainWorktree { .. } => {
100 return Ok(None);
101 }
102 WorktreeIdentity::BareRepository { common_dir, .. } => {
103 return Ok(Some(observation(common_dir.as_path(), None, effective_cwd, CopySelection::Ignored)));
104 }
105 WorktreeIdentity::LinkedWorktree {
106 common_dir,
107 worktree_root,
108 ..
109 } => {
110 return Ok(Some(observation(
111 common_dir.as_path(),
112 Some(worktree_root.as_path()),
113 effective_cwd,
114 CopySelection::Ignored,
115 )));
116 }
117 }
118}
119
120/// What:
121/// The observation that also covers a main checkout, which takes part only when the caller's command creates a worktree.
122/// Why:
123/// A main checkout hands over its ignored state under its leave-out rule, so a worktree added from it works at once;
124/// the caller observes it only for a creating command, so the main checkout's other invocations cost no filesystem reads.
125///
126/// In TS you'd write (pseudocode):
127/// ```ts
128/// observeCopySource({ identity, effectiveCwd }): Promise<WorktreeCopyObservation | typeof WORKTREE_COPY_NOT_APPLICABLE>;
129/// ```
130pub fn observe_copy_source(
131 identity: &WorktreeIdentity,
132 effective_cwd: &Path,
133) -> Result<Option<WorktreeCopyObservation>, WorktreeCopyError> {
134 // `match`: a main checkout takes the main-checkout selection; every other shape keeps the full listing.
135 match identity {
136 WorktreeIdentity::MainWorktree {
137 common_dir,
138 worktree_root,
139 ..
140 } => {
141 return Ok(Some(observation(
142 common_dir.as_path(),
143 Some(worktree_root.as_path()),
144 effective_cwd,
145 CopySelection::MainCheckout,
146 )));
147 }
148 _ => return observe_worktree_repository(identity, effective_cwd),
149 }
150}
151
152/// What:
153/// The observation of one repository with its optional source worktree.
154/// Why:
155/// Both repository kinds share it; only the source differs.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// { commonDir, effectiveCwd, sourceRoot, selection }
160/// ```
161fn observation(
162 common_dir: &Path,
163 source_root: Option<&Path>,
164 effective_cwd: &Path,
165 selection: CopySelection,
166) -> WorktreeCopyObservation {
167 return WorktreeCopyObservation {
168 common_dir: common_dir.to_path_buf(),
169 effective_cwd: effective_cwd.to_path_buf(),
170 source_root: source_root.map(Path::to_path_buf),
171 selection,
172 };
173}
174
175/// What:
176/// Whether `destination` is a registered worktree of the repository right now.
177/// Why:
178/// It is asked before real Git runs: a destination already registered then is not this command's creation, whatever Git
179/// does with it. The answer follows the pointer in the destination's `.git` file and checks that the registration it names
180/// names the destination back, so no other worktree is read.
181///
182/// In TS you'd write (pseudocode):
183/// ```ts
184/// function destinationIsRegistered(commonDir: string, destination: string): boolean;
185/// ```
186pub fn destination_is_registered(common_dir: &Path, destination: &Path) -> bool {
187 return bind_registration(common_dir, destination).is_ok();
188}
189
190/// What:
191/// The canonical root of a Git-reported worktree path,
192/// or `None` when Git still lists a root that no longer exists.
193/// Why:
194/// Git keeps a registration after its directory is deleted;
195/// such a root cannot be copied into or excluded by identity, so it is skipped.
196///
197/// In TS you'd write (pseudocode):
198/// ```ts
199/// async function canonicalRegisteredRoot(root: string): Promise<string | typeof REGISTERED_ROOT_MISSING>;
200/// ```
201fn canonical_registered_root(root: &Path) -> Result<Option<PathBuf>, WorktreeCopyError> {
202 match std::fs::canonicalize(root) {
203 Ok(canonical) => return Ok(Some(canonical)),
204 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
205 Err(error) => return Err(raw_failure(&error)),
206 }
207}
208
209/// What:
210/// The roots of the worktrees Git lists right now, those that exist.
211/// Why:
212/// Git's porcelain listing with NUL separators is the one authority for registered roots,
213/// so the copy never guesses a root from the administrative directory name.
214///
215/// In TS you'd write (pseudocode):
216/// ```ts
217/// async function readRegisteredRoots({ observation, gitPath }): Promise<string[]>;
218/// ```
219fn read_registered_roots(
220 context: &GitContext,
221 observation: &WorktreeCopyObservation,
222) -> Result<Vec<PathBuf>, WorktreeCopyError> {
223 let request: GitRequest = GitRequest {
224 cwd: observation.effective_cwd.clone(),
225 arguments: vec![
226 "--git-dir".into(),
227 observation.common_dir.as_os_str().to_os_string(),
228 "worktree".into(),
229 "list".into(),
230 "--porcelain".into(),
231 "-z".into(),
232 ],
233 without_prefix: true,
234 ..GitRequest::default()
235 };
236 let output = run_git(context, &request).map_err(|error| return raw_failure(&error))?;
237 if !output.succeeded() {
238 return Err(raw_failure(&String::from_utf8_lossy(
239 output.stderr.as_slice(),
240 )));
241 }
242 let mut roots: Vec<PathBuf> = Vec::new();
243 // `filter_map` keeps only fields of the `worktree <path>` form.
244 for path in output
245 .stdout
246 .split(|byte| return *byte == 0)
247 .filter_map(|field| return field.strip_prefix(b"worktree "))
248 .filter_map(path_from_git_bytes)
249 {
250 if let Some(canonical) = canonical_registered_root(path.as_path())? {
251 roots.push(canonical);
252 }
253 }
254 return Ok(roots);
255}
256
257/// What:
258/// The worktree this command registered at `destination`, with every registered root.
259/// Why:
260/// Runs after real Git returned, whatever its status was, so a worktree Git registered before failing still counts.
261/// It is this command's when the destination was not registered before Git ran and is registered now: the destination is
262/// the one the command line named, so another add's worktree is never taken for it.
263///
264/// In TS you'd write (pseudocode):
265/// ```ts
266/// async function findCreatedWorktree({ observation, destination, registeredBefore, gitPath }): Promise<{ created; registeredRoots }>;
267/// ```
268pub fn find_created_worktree(
269 context: &GitContext,
270 observation: &WorktreeCopyObservation,
271 destination: &Path,
272 registered_before: bool,
273) -> Result<CreatedWorktrees, WorktreeCopyError> {
274 if registered_before || !destination_is_registered(observation.common_dir.as_path(), destination) {
275 return Ok(CreatedWorktrees {
276 created: Vec::new(),
277 registered_roots: Vec::new(),
278 });
279 }
280 return Ok(CreatedWorktrees {
281 created: vec![CreatedWorktree { root: destination.to_path_buf() }],
282 registered_roots: read_registered_roots(context, observation)?,
283 });
284}
285
286/// Registry controls stay out of the release executable.
287#[cfg(test)]
288#[path = "worktree_copy_registry_tests.rs"]
289mod tests;