Skip to main content

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;