Skip to main content

git_policy_cli/
worktree_copy_lock.rs

1//! What:
2//!  The settlement lock that serializes worktree-copy installation and recovery per common Git directory,
3//!  its owner record, liveness-based retirement of dead owners, and the inherited-lease check nested Git runs use.
4//! Why:
5//!  Two copies into one repository must not interleave, but a commit in a linked worktree must never wait for them;
6//!  only an owner proven alive may be waited for, and only an owner proven dead may be replaced.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! await using lease = await acquireWorktreeCopyLock(commonDir);
11//! ```
12
13use super::index_lock_wait::{BackoffState, INITIAL_BACKOFF, jittered_wait, next_backoff};
14use super::json_record::quote;
15use super::owner_lock_record::{OwnerLockRecord, owner_lock_holder_is_alive};
16use super::process_identity::current_birth_identity;
17use super::random_id::random_uuid;
18use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
19use crate::worktree_copy_journal::ensure_journal_root;
20use std::io::Write;
21use std::path::{Path, PathBuf};
22use std::time::{Duration, Instant};
23
24/// What:
25///  The published name of the settlement lock inside the journal root.
26/// Why:
27///   Every process in one repository contends on this exact name.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// join(journalRoot, 'settlement.lock')
32/// ```
33pub const SETTLEMENT_LOCK_NAME: &str = "settlement.lock";
34
35/// What:
36///  The owner record's file name inside a published lock directory.
37/// Why:
38///   Readers find the owner by this name.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// const SETTLEMENT_OWNER_FILENAME = 'owner.json';
43/// ```
44pub const OWNER_RECORD_NAME: &str = "owner.json";
45
46/// What:
47///  How long an owner without evidence may hold the lock before acquisition gives up.
48/// Why:
49///   A malformed record must not block the repository forever, but a live owner is waited for without limit.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// export const UNPROVEN_OWNER_TIMEOUT_MS = 1_000;
54/// ```
55pub const UNPROVEN_OWNER_TIMEOUT_MS: u64 = 1_000;
56
57/// What:
58///  The delay between read-only checks while a live owner holds the lock.
59/// Why:
60///   Short enough to notice a release promptly, long enough not to spin.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// const LIVE_OWNER_POLL_MS = 20;
65/// ```
66const LIVE_OWNER_POLL: Duration = Duration::from_millis(20);
67
68/// What:
69///  The environment variable through which nested Git runs inherit a held lease.
70/// Why:
71///   A hook that runs the wrapper inside `git worktree add` must forward without waiting for its own parent.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// export const WORKTREE_COPY_LEASE_ENV = 'CLI_GIT_WORKTREE_COPY_LEASE';
76/// ```
77pub const WORKTREE_COPY_LEASE_ENV: &str = "CLI_GIT_WORKTREE_COPY_LEASE";
78
79/// What:
80///  The identity an owner record names: a unguessable token, a process, and that process's birth identity.
81/// Why:
82///   Liveness is judged on the process and its birth, so a reused PID is never taken for the owner.
83///
84/// In TS you'd write (pseudocode):
85/// ```ts
86/// type LockOwner = { leaseToken: string; ownerPid: number; ownerBirthIdentity: string; schemaVersion: 1 };
87/// ```
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct LockOwner {
90    /// Capability passed to real-Git descendants.
91    pub lease_token: String,
92    /// The owner's process identifier.
93    pub owner_pid: i64,
94    /// The owner's process birth identity.
95    pub owner_birth_identity: String,
96}
97
98/// What:
99///  What a published lock directory currently says.
100/// Why:
101///   Acquisition acts differently on a live owner, a dead one, a vanished lock, and an unproven record.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// readPublishedOwner(lockDirectory): Promise<LockOwner | UnprovenOwner | typeof LOCK_RETRY>;
106/// ```
107#[derive(Clone, Debug, Eq, PartialEq)]
108enum Published {
109    /// A parsed owner record.
110    Owner(LockOwner),
111    /// The lock vanished or changed while it was read.
112    Retry,
113    /// A record or file that proves nothing, with the evidence.
114    Unproven(String),
115}
116
117/// What:
118///  The text of one owner record, as the incumbent writes it.
119/// Why:
120///   Other wrappers read this record, so its key order and spelling are fixed.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// `${JSON.stringify({ leaseToken, ownerBirthIdentity, ownerPid, schemaVersion })}\n`
125/// ```
126pub fn encode_owner(owner: &LockOwner) -> String {
127    return format!(
128        "{{\"leaseToken\":{},\"ownerBirthIdentity\":{},\"ownerPid\":{},\"schemaVersion\":1}}\n",
129        quote(owner.lease_token.as_str()),
130        quote(owner.owner_birth_identity.as_str()),
131        owner.owner_pid
132    );
133}
134
135/// What:
136///  Parses one owner record, or names why it proves nothing.
137/// Why:
138///   Every field is checked before the owner's process is ever consulted.
139///
140/// In TS you'd write (pseudocode):
141/// ```ts
142/// function parseLockOwner(text: string): LockOwner | string;
143/// ```
144pub fn parse_owner(text: &str) -> Result<LockOwner, String> {
145    let value: serde_json::Value =
146        serde_json::from_str(text).map_err(|error| format!("not JSON ({error})"))?;
147    let missing: &str = "missing or mistyped owner fields";
148    let object = value
149        .as_object()
150        .ok_or_else(|| return String::from(missing))?;
151    let lease_token: &str = object
152        .get("leaseToken")
153        .and_then(serde_json::Value::as_str)
154        .filter(|token| return !token.is_empty())
155        .ok_or_else(|| return String::from(missing))?;
156    let owner_pid: i64 = object
157        .get("ownerPid")
158        .and_then(serde_json::Value::as_i64)
159        .filter(|pid| return *pid >= 1)
160        .ok_or_else(|| return String::from(missing))?;
161    let birth: &str = object
162        .get("ownerBirthIdentity")
163        .and_then(serde_json::Value::as_str)
164        .filter(|identity| return !identity.is_empty())
165        .ok_or_else(|| return String::from(missing))?;
166    if object
167        .get("schemaVersion")
168        .and_then(serde_json::Value::as_f64)
169        != Some(1.0)
170    {
171        return Err(String::from(missing));
172    }
173    return Ok(LockOwner {
174        lease_token: lease_token.to_string(),
175        owner_pid,
176        owner_birth_identity: birth.to_string(),
177    });
178}
179
180/// What:
181///  Reads the owner a published lock names.
182/// Why:
183///   A vanished lock asks for another attempt; an unreadable or invalid one is unproven evidence.
184///
185/// In TS you'd write (pseudocode):
186/// ```ts
187/// await readPublishedOwner(lockDirectory);
188/// ```
189fn read_published(lock: &Path) -> Result<Published, WorktreeCopyError> {
190    let owner_path: PathBuf = lock.join(OWNER_RECORD_NAME);
191    let text: String = match std::fs::read_to_string(owner_path.as_path()) {
192        Ok(text) => text,
193        Err(error) => {
194            // `match` on the operating-system code: vanished or non-directory means retry; unreadable means unproven.
195            return match error.raw_os_error() {
196                Some(code) if code == libc::ENOENT || code == libc::ENOTDIR => Ok(Published::Retry),
197                Some(code) if code == libc::EACCES || code == libc::EISDIR => {
198                    let name: &str = if code == libc::EACCES {
199                        "EACCES"
200                    } else {
201                        "EISDIR"
202                    };
203                    Ok(Published::Unproven(format!(
204                        "{} is unreadable ({name})",
205                        quote(owner_path.to_string_lossy().as_ref())
206                    )))
207                }
208                _ => Err(raw_failure(&error)),
209            };
210        }
211    };
212    return Ok(match parse_owner(text.as_str()) {
213        Ok(owner) => Published::Owner(owner),
214        Err(reason) => Published::Unproven(format!(
215            "{} holds an invalid owner record: {reason}",
216            quote(owner_path.to_string_lossy().as_ref())
217        )),
218    });
219}
220
221/// What:
222///  Whether a recorded owner is alive with the same process birth.
223/// Why:
224///   A reused PID or an exited but unreaped process is dead.
225///
226/// In TS you'd write (pseudocode):
227/// ```ts
228/// await lockOwnerIsAlive(owner);
229/// ```
230fn owner_alive(owner: &LockOwner) -> Result<bool, WorktreeCopyError> {
231    let record: OwnerLockRecord = OwnerLockRecord {
232        token: owner.lease_token.clone(),
233        owner_pid: owner.owner_pid,
234        owner_birth_identity: owner.owner_birth_identity.clone(),
235        transaction_id: None,
236    };
237    return owner_lock_holder_is_alive(&record).map_err(|error| return raw_failure(&error));
238}
239
240/// What:
241///  The disposable settlement lock: released explicitly, or removed when dropped if still held.
242/// Why:
243///   Release reports an ownership change; the drop fallback cannot report, so it only removes what it still owns.
244///
245/// In TS you'd write (pseudocode):
246/// ```ts
247/// type WorktreeCopyLease = AsyncDisposable & { leaseToken: string };
248/// ```
249#[derive(Debug)]
250pub struct WorktreeCopyLease {
251    /// The published lock directory.
252    lock_directory: PathBuf,
253    /// The owner identity this process published.
254    owner: LockOwner,
255    /// Whether release has already run.
256    released: bool,
257}
258
259impl WorktreeCopyLease {
260    /// The token a nested Git run inherits.
261    pub fn lease_token(&self) -> &str {
262        return self.owner.lease_token.as_str();
263    }
264
265    /// Removes the lock when this process still owns it, and fails with the ownership wording otherwise.
266    pub fn release(mut self) -> Result<(), WorktreeCopyError> {
267        self.released = true;
268        if !remove_lock_of(
269            self.lock_directory.as_path(),
270            self.owner.lease_token.as_str(),
271        )? {
272            return Err(failure_with_cause(
273                format!(
274                    "cli-git: worktree-copy lock ownership changed: {}.",
275                    quote(self.lock_directory.to_string_lossy().as_ref())
276                )
277                .as_str(),
278                None,
279            ));
280        }
281        return Ok(());
282    }
283}
284
285impl Drop for WorktreeCopyLease {
286    /// Removes a lease that was never released explicitly, ignoring a failure that nothing can report.
287    fn drop(&mut self) {
288        if !self.released {
289            let _ignored: Result<bool, WorktreeCopyError> = remove_lock_of(
290                self.lock_directory.as_path(),
291                self.owner.lease_token.as_str(),
292            );
293        }
294    }
295}
296
297/// What:
298///  Removes a published lock by renaming it away first, when it still names the expected owner.
299/// Why:
300///   A rename frees the published name atomically, so a concurrent publication is never emptied in place;
301///  a replacement moved by a race is renamed back.
302///
303/// In TS you'd write (pseudocode):
304/// ```ts
305/// await removeLockOf({ lockDirectory, leaseToken });
306/// ```
307fn remove_lock_of(lock: &Path, lease_token: &str) -> Result<bool, WorktreeCopyError> {
308    match read_published(lock)? {
309        Published::Owner(owner) if owner.lease_token == lease_token => {}
310        _ => return Ok(false),
311    }
312    let retired: PathBuf = PathBuf::from(format!(
313        "{}.{}.stale",
314        lock.to_string_lossy(),
315        random_uuid().map_err(|error| return raw_failure(&error.0))?
316    ));
317    // `match` on the rename: a vanished lock is simply not ours to remove; any other failure is fatal.
318    match std::fs::rename(lock, retired.as_path()) {
319        Ok(()) => {}
320        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
321        Err(error) => return Err(raw_failure(&error)),
322    }
323    let moved: Published = read_published(retired.as_path())?;
324    if !matches!(&moved, Published::Owner(owner) if owner.lease_token == lease_token) {
325        // A replacement moved by the race goes back; a rename-back collision leaves both in place for inspection.
326        let _ignored: std::io::Result<()> = std::fs::rename(retired.as_path(), lock);
327        return Ok(false);
328    }
329    std::fs::remove_dir_all(retired.as_path()).map_err(|error| return raw_failure(&error))?;
330    return Ok(true);
331}
332
333/// What:
334///  Publishes this process's owner as a complete candidate directory, then renames it to the lock name.
335/// Why:
336///   The rename is the single atomic step that makes the lock visible, so a reader never sees an owner-less lock.
337///
338/// In TS you'd write (pseudocode):
339/// ```ts
340/// await writeCandidateOwner({ candidateDirectory, owner }); await rename(candidateDirectory, lockDirectory);
341/// ```
342fn publish_candidate(
343    lock: &Path,
344    owner: &LockOwner,
345) -> Result<Option<WorktreeCopyLease>, WorktreeCopyError> {
346    let candidate: PathBuf = PathBuf::from(format!(
347        "{}.{}.pending",
348        lock.to_string_lossy(),
349        random_uuid().map_err(|error| return raw_failure(&error.0))?
350    ));
351    write_candidate(candidate.as_path(), owner)?;
352    match std::fs::rename(candidate.as_path(), lock) {
353        Ok(()) => {
354            return Ok(Some(WorktreeCopyLease {
355                lock_directory: lock.to_path_buf(),
356                owner: owner.clone(),
357                released: false,
358            }));
359        }
360        Err(error) => {
361            let _ignored: std::io::Result<()> = std::fs::remove_dir_all(candidate.as_path());
362            // `match` on the error: an occupied name means another owner holds the lock; anything else is fatal.
363            if matches!(error.kind(), std::io::ErrorKind::AlreadyExists)
364                || error.raw_os_error() == Some(libc::ENOTEMPTY)
365            {
366                return Ok(None);
367            }
368            return Err(raw_failure(&error));
369        }
370    }
371}
372
373/// What:
374///  Writes one candidate lock directory with its owner record, synced, and removes it on failure.
375/// Why:
376///   A candidate is never visible under the lock name until its owner record is complete.
377///
378/// In TS you'd write (pseudocode):
379/// ```ts
380/// async function writeCandidateOwner({ candidateDirectory, owner }): Promise<void>;
381/// ```
382fn write_candidate(candidate: &Path, owner: &LockOwner) -> Result<(), WorktreeCopyError> {
383    let written: std::io::Result<()> = (|| {
384        use std::os::unix::fs::DirBuilderExt;
385        std::fs::DirBuilder::new().mode(0o700).create(candidate)?;
386        crate::private_storage::write_exclusive_synced(
387            candidate.join(OWNER_RECORD_NAME).as_path(),
388            encode_owner(owner).as_bytes(),
389        )?;
390        return Ok(());
391    })();
392    if let Err(error) = written {
393        let _ignored: std::io::Result<()> = std::fs::remove_dir_all(candidate);
394        return Err(raw_failure(&error));
395    }
396    return Ok(());
397}
398
399/// What:
400///  The owner identity of this process with a fresh token.
401/// Why:
402///   Every acquisition publishes a token no other process can guess.
403///
404/// In TS you'd write (pseudocode):
405/// ```ts
406/// await currentLockOwner();
407/// ```
408fn current_owner() -> Result<LockOwner, WorktreeCopyError> {
409    let birth: String = current_birth_identity().map_err(|error| {
410        return failure_with_cause(
411            "cli-git: current worktree-copy lock owner identity is unavailable.",
412            Some(&error),
413        );
414    })?;
415    let token: String = random_uuid().map_err(|error| return raw_failure(&error.0))?;
416    return Ok(LockOwner {
417        lease_token: token,
418        owner_pid: i64::from(std::process::id()),
419        owner_birth_identity: birth,
420    });
421}
422
423/// What:
424///  One unit-interval random number from the operating system.
425/// Why:
426///   Git's backoff is jittered, so waiters do not move in lockstep.
427///
428/// In TS you'd write (pseudocode):
429/// ```ts
430/// Math.random()
431/// ```
432fn unit_random() -> Result<f64, WorktreeCopyError> {
433    let mut bytes: [u8; 8] = [0; 8];
434    getrandom::fill(&mut bytes).map_err(|error| return raw_failure(&error))?;
435    return Ok((u64::from_le_bytes(bytes) >> 11) as f64 / (1u64 << 53) as f64);
436}
437
438/// What:
439///  The lock as a waiter sees it at one attempt.
440/// Why:
441///   Each result tells the acquisition loop whether to take the lock, wait for a live owner, or back off.
442///
443/// In TS you'd write (pseudocode):
444/// ```ts
445/// type AttemptResult = { kind: 'acquired' } | { kind: 'live' } | { kind: 'retry' } | { kind: 'unproven' };
446/// ```
447enum Attempt {
448    /// The lock is ours.
449    Acquired(WorktreeCopyLease),
450    /// A live owner holds it.
451    Live(LockOwner),
452    /// The lock changed or a dead owner was retired; try again.
453    Retry,
454    /// An owner without evidence holds it.
455    Unproven(String),
456}
457
458/// What:
459///  One publication attempt and the classification of whatever blocks it.
460/// Why:
461///   A dead owner is retired on the spot, so one attempt can end the wait.
462///
463/// In TS you'd write (pseudocode):
464/// ```ts
465/// await attemptAcquire({ lockDirectory, owner });
466/// ```
467fn attempt(lock: &Path, owner: &LockOwner) -> Result<Attempt, WorktreeCopyError> {
468    if let Some(lease) = publish_candidate(lock, owner)? {
469        return Ok(Attempt::Acquired(lease));
470    }
471    return match read_published(lock)? {
472        Published::Retry => Ok(Attempt::Retry),
473        Published::Unproven(evidence) => Ok(Attempt::Unproven(evidence)),
474        Published::Owner(holder) => {
475            if owner_alive(&holder)? {
476                return Ok(Attempt::Live(holder));
477            }
478            let _retired: bool = remove_lock_of(lock, holder.lease_token.as_str())?;
479            Ok(Attempt::Retry)
480        }
481    };
482}
483
484/// What:
485///  Waits, reading only, while a live owner keeps the lock.
486/// Why:
487///   The wait ends when the owner releases, replaces the lock, or stops running.
488///
489/// In TS you'd write (pseudocode):
490/// ```ts
491/// await waitWhileOwnerLives({ lockDirectory, owner });
492/// ```
493fn wait_while_owner_lives(lock: &Path, owner: &LockOwner) -> Result<(), WorktreeCopyError> {
494    loop {
495        std::thread::sleep(LIVE_OWNER_POLL);
496        match read_published(lock)? {
497            Published::Owner(current) if current.lease_token == owner.lease_token => {
498                if !owner_alive(&current)? {
499                    return Ok(());
500                }
501            }
502            _ => return Ok(()),
503        }
504    }
505}
506
507/// What:
508///  The stderr line that names a live holder.
509/// Why:
510///   A waiting user learns which process to look at and that cli-git is still waiting on purpose.
511///
512/// In TS you'd write (pseudocode):
513/// ```ts
514/// liveHolderLine({ lockDirectory, owner });
515/// ```
516fn live_holder_line(lock: &Path, owner: &LockOwner) -> String {
517    return format!(
518        "cli-git: waiting for PID {}, which holds worktree-copy settlement {} (proven by its live process identity); cli-git waits until it releases the lock.\n",
519        owner.owner_pid,
520        quote(lock.to_string_lossy().as_ref())
521    );
522}
523
524/// What:
525///  Acquires the settlement lock: waits without limit for a proven-live owner, retires a dead one,
526///  and gives an owner without evidence the bounded wait before failing.
527/// Why:
528///   Installation and recovery of one repository must never interleave, and no lock is taken from an owner that might still run.
529///
530/// In TS you'd write (pseudocode):
531/// ```ts
532/// await using lease = await acquireWorktreeCopyLock(commonDir);
533/// ```
534pub fn acquire_worktree_copy_lock(
535    common_dir: &Path,
536    stderr: &mut dyn Write,
537) -> Result<WorktreeCopyLease, WorktreeCopyError> {
538    let lock: PathBuf = ensure_journal_root(common_dir)?.join(SETTLEMENT_LOCK_NAME);
539    let owner: LockOwner = current_owner()?;
540    let mut named: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
541    let mut unproven_since: Option<Instant> = None;
542    let mut backoff: BackoffState = INITIAL_BACKOFF;
543    loop {
544        // `match` on the attempt: a held lock is waited for, a retry backs off, and an acquisition ends the loop.
545        let unproven: Option<String> = match attempt(lock.as_path(), &owner)? {
546            Attempt::Acquired(lease) => return Ok(lease),
547            Attempt::Live(holder) => {
548                unproven_since = None;
549                if named.insert(holder.lease_token.clone()) {
550                    let line: String = live_holder_line(lock.as_path(), &holder);
551                    stderr
552                        .write_all(line.as_bytes())
553                        .map_err(|error| return raw_failure(&error))?;
554                }
555                wait_while_owner_lives(lock.as_path(), &holder)?;
556                continue;
557            }
558            Attempt::Retry => None,
559            Attempt::Unproven(evidence) => Some(evidence),
560        };
561        let started: Instant = *unproven_since.get_or_insert_with(Instant::now);
562        if let Some(evidence) = unproven.filter(|_| {
563            return started.elapsed() >= Duration::from_millis(UNPROVEN_OWNER_TIMEOUT_MS);
564        }) {
565            {
566                let message: String = format!(
567                    "cli-git: worktree-copy settlement lock {} stayed in place for {} ms and cli-git could not prove whether its owner is alive. Evidence: {}. cli-git left the lock in place; remove it only after confirming no cli-git process is using this repository.",
568                    quote(lock.to_string_lossy().as_ref()),
569                    UNPROVEN_OWNER_TIMEOUT_MS,
570                    evidence
571                );
572                return Err(failure_with_cause(message.as_str(), None));
573            }
574        }
575        let delay_ms: u64 = jittered_wait(backoff, unit_random()?);
576        backoff = next_backoff(backoff);
577        std::thread::sleep(Duration::from_millis(delay_ms));
578    }
579}
580
581/// What:
582///  Takes the settlement lock if it is free or its dead owner is retired; reports a held lock instead of waiting.
583/// Why:
584///   A command that neither creates nor moves worktrees must not wait behind a copy.
585///
586/// In TS you'd write (pseudocode):
587/// ```ts
588/// const lease = await tryAcquireWorktreeCopyLock(commonDir); // WORKTREE_COPY_LOCK_HELD when held
589/// ```
590pub fn try_acquire_worktree_copy_lock(
591    common_dir: &Path,
592) -> Result<Option<WorktreeCopyLease>, WorktreeCopyError> {
593    let lock: PathBuf = ensure_journal_root(common_dir)?.join(SETTLEMENT_LOCK_NAME);
594    let owner: LockOwner = current_owner()?;
595    // Ten attempts absorb a lock that keeps changing; a live or unproven holder ends the attempts at once.
596    for _attempt in 0..10 {
597        // `match` on the attempt: an acquisition returns the lease, a retry tries again, and any holder means held.
598        match attempt(lock.as_path(), &owner)? {
599            Attempt::Acquired(lease) => return Ok(Some(lease)),
600            Attempt::Retry => continue,
601            Attempt::Live(_) | Attempt::Unproven(_) => return Ok(None),
602        }
603    }
604    return Err(failure_with_cause(
605        format!(
606            "cli-git: worktree-copy settlement lock under {} kept changing while it was acquired.",
607            quote(common_dir.to_string_lossy().as_ref())
608        )
609        .as_str(),
610        None,
611    ));
612}
613
614/// What:
615///  Whether the environment's lease token names the live owner of the published lock.
616/// Why:
617///   A nested Git run that carries the outer run's token forwards without waiting for the lock it is already inside.
618///
619/// In TS you'd write (pseudocode):
620/// ```ts
621/// await validatesInheritedWorktreeCopyLease({ commonDir, leaseToken });
622/// ```
623pub fn validate_inherited_lease(
624    common_dir: &Path,
625    lease_token: &str,
626) -> Result<bool, WorktreeCopyError> {
627    if lease_token.is_empty() {
628        return Ok(false);
629    }
630    let lock: PathBuf = ensure_journal_root(common_dir)?.join(SETTLEMENT_LOCK_NAME);
631    return match read_published(lock.as_path())? {
632        Published::Owner(owner) if owner.lease_token == lease_token => owner_alive(&owner),
633        _ => Ok(false),
634    };
635}
636
637/// Lock controls stay out of the release executable.
638#[cfg(test)]
639#[path = "worktree_copy_lock_tests.rs"]
640mod tests;