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;