Skip to main content

git_policy_cli/
effective_target.rs

1//! What:
2//!  Classify the worktree an invocation targets for worktree-enforcing policies.
3//! Why:
4//!  Linked-worktree safeguards discipline a person's repositories,
5//!  not the
6//!      disposable clones a tool keeps in its own cache.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // classifyEffectiveTarget(identity, allowedDirs) === 'linked-worktree'
11//! ```
12
13/// Import getenv-style lookup and the resolved repository identity.
14use super::child_environment::environment_value;
15use super::worktree_identity::WorktreeIdentity;
16/// What:
17///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
18///       must be UTF-8).
19/// Why:
20///   Cache directories come from environment values that need not be UTF-8.
21///
22/// In TS you'd write (pseudocode):
23/// ```ts
24/// // process.env values, but byte-preserving.
25/// ```
26use std::ffi::OsString;
27/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
28use std::path::{Path, PathBuf};
29
30/// What:
31///  The four targets worktree policies distinguish.
32///       `#[derive(...)]` generates copying,
33///  debug printing and `==`.
34/// Why:
35///   A bare repository or no repository has no worktree to protect;
36///  an
37///       allowlisted tool cache is exempt;
38///  main versus linked decides enforcement.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// type EffectiveTarget = 'outside-worktree' | 'main-worktree' | 'linked-worktree' | 'allowlisted';
43/// ```
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum EffectiveTarget {
46    /// No worktree:
47    ///  outside any repository,
48    ///  or a bare repository.
49    OutsideWorktree,
50    /// The repository's main worktree.
51    MainWorktree,
52    /// A linked worktree.
53    LinkedWorktree,
54    /// A repository under a tool-cache directory exempt from enforcement.
55    Allowlisted,
56}
57
58/// What:
59///  Resolve uv's cache directory the way uv does:
60///  `UV_CACHE_DIR`,
61///  then
62///       `XDG_CACHE_HOME/uv`,
63///  then `HOME/.cache/uv`;
64///  an empty value counts as unset.
65///       `Option<&Path>` is "a borrowed home directory or nothing";
66///       `Option<PathBuf>` is "an owned path or nothing".
67/// Why:
68///   uv runs destructive Git inside throwaway clones it owns there.
69///  The home
70///       directory is injected so tests never read the real one.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// function uvCacheDir(environment: [string, string][], home?: string): string | undefined;
75/// ```
76pub fn uv_cache_dir(environment: &[(OsString, OsString)], home: Option<&Path>) -> Option<PathBuf> {
77    // `if let Some(x) = ... && cond` runs only when the variable is set and the second
78    // condition also holds, like `x !== undefined && x !== ""`.
79    if let Some(explicit) = environment_value(environment, "UV_CACHE_DIR")
80        && !explicit.is_empty()
81    {
82        // `Some(...)` is the "present" variant.
83        return Some(PathBuf::from(explicit));
84    }
85    if let Some(cache_home) = environment_value(environment, "XDG_CACHE_HOME")
86        && !cache_home.is_empty()
87    {
88        // `.join` appends one path segment and returns an owned path.
89        return Some(PathBuf::from(cache_home).join("uv"));
90    }
91    // What: `let Some(x) = ... else { return None; };` unwraps the home or exits.
92    // Why:  Without any hint and without a home directory there is no cache to exempt.
93    //
94    // In TS you'd write (pseudocode):
95    // ```ts
96    // if (home === undefined) return undefined;
97    // ```
98    let Some(home_directory) = home else {
99        // `None` is the "absent" variant.
100        return None;
101    };
102    return Some(home_directory.join(".cache").join("uv"));
103}
104
105/// What:
106///  The tool-cache directories exempt from worktree enforcement on this machine.
107///       `Vec<PathBuf>` is an owned list of owned paths.
108/// Why:
109///   The set is a property of installed tooling,
110///  not of any repository,
111///  so it is
112///       compiled in rather than configured.
113///
114/// In TS you'd write (pseudocode):
115/// ```ts
116/// function defaultAllowedWorktreeDirs(environment, home): string[];
117/// ```
118pub fn default_allowed_worktree_dirs(
119    environment: &[(OsString, OsString)],
120    home: Option<&Path>,
121) -> Vec<PathBuf> {
122    // `Vec::<PathBuf>::new()` is an empty owned list; `mut` allows pushing.
123    let mut directories: Vec<PathBuf> = Vec::<PathBuf>::new();
124    if let Some(uv) = uv_cache_dir(environment, home) {
125        directories.push(uv);
126    }
127    return directories;
128}
129
130/// What:
131///  Report whether the canonical `candidate` lies inside any allowed directory.
132/// Why:
133///   Each allowed directory is resolved through symbolic links first,
134///  so a home
135///       reached through a link still matches;
136///  a directory that does not exist drops
137///       out.
138///  `Path::starts_with` compares whole segments,
139///  so `/a/b` does not contain
140///       `/a/bc`.
141///
142/// In TS you'd write (pseudocode):
143/// ```ts
144/// async function isAllowedWorktreeDir(candidate: string, allowedDirs: string[]): Promise<boolean>;
145/// ```
146pub fn is_allowed_worktree_dir(candidate: &Path, allowed_dirs: &[PathBuf]) -> bool {
147    // `for directory in allowed_dirs` borrows each entry in turn.
148    for directory in allowed_dirs {
149        // What: `if let Ok(resolved) = ... && cond` runs only when the directory could be
150        //       resolved and the candidate lies inside it.
151        // Why:  A tool that is not installed has no cache directory to compare with.
152        //
153        // In TS you'd write (pseudocode):
154        // ```ts
155        // let resolved; try { resolved = await realpath(directory); } catch { continue; }
156        // ```
157        if let Ok(resolved) = std::fs::canonicalize(directory)
158            && candidate.starts_with(resolved.as_path())
159        {
160            return true;
161        }
162    }
163    return false;
164}
165
166/// What:
167///  Map a repository identity to the target worktree policies act on.
168/// Why:
169///   Bare and absent repositories have no worktree;
170///  a repository whose Git
171///       directory sits under an allowed tool cache is exempt before main/linked matters.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// function classifyEffectiveTarget(identity: WorktreeIdentity, allowedDirs: string[]): EffectiveTarget;
176/// ```
177pub fn classify_effective_target(
178    identity: &WorktreeIdentity,
179    allowed_dirs: &[PathBuf],
180) -> EffectiveTarget {
181    // What: `match` on the borrowed identity; `{ git_dir, .. }` binds one field and
182    //       ignores the rest.
183    // Why:  Each shape maps to exactly one target, and the compiler checks none is missed.
184    //
185    // In TS you'd write (pseudocode):
186    // ```ts
187    // switch (identity.kind) { case 'outside-worktree': return 'outside-worktree'; ... }
188    // ```
189    match identity {
190        WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => {
191            return EffectiveTarget::OutsideWorktree;
192        }
193        WorktreeIdentity::MainWorktree { git_dir, .. } => {
194            if is_allowed_worktree_dir(git_dir.as_path(), allowed_dirs) {
195                return EffectiveTarget::Allowlisted;
196            }
197            return EffectiveTarget::MainWorktree;
198        }
199        WorktreeIdentity::LinkedWorktree { git_dir, .. } => {
200            if is_allowed_worktree_dir(git_dir.as_path(), allowed_dirs) {
201                return EffectiveTarget::Allowlisted;
202            }
203            return EffectiveTarget::LinkedWorktree;
204        }
205    }
206}
207
208/// Disposable-directory controls stay out of the release executable.
209#[cfg(test)]
210#[path = "effective_target_tests.rs"]
211mod tests;