1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! Process current working directory relative to a Git work tree.
//!
//! Git refuses to remove a path when that removal would delete the process's
//! original working directory (`unpack-trees.c` `ERROR_CWD_IN_THE_WAY`): the
//! removed index path must **equal** the cwd or be a **parent prefix** of cwd
//! (removing a directory subtree that still contains cwd).
//!
//! Parent directory cleanup after removing a file must not `rmdir` an ancestor
//! that still contains cwd (matches leaving `foo` when cwd is `foo`).
use std::path::Path;
const GRIT_INVOCATION_CWD_ENV: &str = "GRIT_INVOCATION_CWD";
/// Normalize a repository-relative path to a POSIX-style string for comparisons.
fn normalize_repo_rel(path: &str) -> String {
let mut s = path.trim().trim_start_matches('/').replace('\\', "/");
while s.ends_with('/') && s.len() > 1 {
s.pop();
}
s
}
/// Repository-relative path from `cwd` to `work_tree` root (POSIX, no leading `/`).
///
/// Tries canonical paths first, then a lexical `strip_prefix` when canonicalization
/// does not yield a prefix relationship (symlinks, `core.worktree` quirks).
#[must_use]
pub fn cwd_relative_under_work_tree(work_tree: &Path, cwd: &Path) -> Option<String> {
cwd_relative_to_work_tree(work_tree, cwd)
}
fn cwd_relative_to_work_tree(work_tree: &Path, cwd: &Path) -> Option<String> {
let rel = cwd
.canonicalize()
.ok()
.zip(work_tree.canonicalize().ok())
.and_then(|(c, w)| c.strip_prefix(&w).ok().map(|p| p.to_path_buf()))
.or_else(|| cwd.strip_prefix(work_tree).ok().map(|p| p.to_path_buf()))?;
let s = rel.to_string_lossy().replace('\\', "/");
let s = s.trim_start_matches('/').to_string();
if s.is_empty() {
None
} else {
Some(s)
}
}
/// Returns the repository-relative path of the current process directory under
/// `work_tree`, using canonical paths. Returns `None` when cwd is outside the
/// work tree, equals the work tree root, or cannot be resolved.
#[must_use]
pub fn process_cwd_repo_relative(work_tree: &Path) -> Option<String> {
if let Ok(prefix) = std::env::var("GIT_PREFIX") {
let p = normalize_repo_rel(&prefix);
if !p.is_empty() {
return Some(p);
}
}
if let Ok(inv) = std::env::var(GRIT_INVOCATION_CWD_ENV) {
let inv_path = Path::new(inv.trim());
if let Some(rel) = cwd_relative_to_work_tree(work_tree, inv_path) {
let n = normalize_repo_rel(&rel);
if !n.is_empty() {
return Some(n);
}
}
}
let cwd = std::env::current_dir().ok()?;
cwd_relative_to_work_tree(work_tree, &cwd)
}
/// Returns true if deleting the worktree entry at `repo_rel_path` (POSIX,
/// relative to the repository root) would delete the process cwd — i.e. the
/// path equals cwd or cwd lies under `repo_rel_path/`.
#[must_use]
pub fn cwd_would_be_removed_with_repo_path(work_tree: &Path, repo_rel_path: &str) -> bool {
let Some(cwd_rel) = process_cwd_repo_relative(work_tree) else {
return false;
};
removal_path_covers_cwd(&normalize_repo_rel(repo_rel_path), &cwd_rel)
}
/// True when `removed` is the cwd path or a parent directory of cwd.
fn removal_path_covers_cwd(removed: &str, cwd_rel: &str) -> bool {
if removed.is_empty() {
return false;
}
cwd_rel == removed || cwd_rel.starts_with(&format!("{removed}/"))
}
/// Returns true if removing the directory `dir_abs` during empty-parent cleanup
/// would delete cwd (cwd is `dir` or a descendant of `dir`).
#[must_use]
pub fn cwd_would_be_removed_with_dir(work_tree: &Path, dir_abs: &Path, cwd_rel: &str) -> bool {
let Ok(rel) = dir_abs.strip_prefix(work_tree) else {
return false;
};
let dir_rel = normalize_repo_rel(rel.to_string_lossy().as_ref());
if dir_rel.is_empty() {
return false;
}
removal_path_covers_cwd(&dir_rel, cwd_rel)
}