git_policy_cli/worktree/
worktree_remove_registry.rs1use std::ffi::{OsStr, OsString};
18use std::os::unix::ffi::{OsStrExt, OsStringExt};
19use std::path::{Path, PathBuf};
20
21#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct Registration {
32 pub id: OsString,
34 pub admin: PathBuf,
36 pub worktrees: PathBuf,
38 pub gitdir_text: Vec<u8>,
40}
41
42fn without_newline(text: &[u8]) -> &[u8] {
44 return text.strip_suffix(b"\n").unwrap_or(text);
45}
46
47fn ends_with_name(candidate: &[u8], operand: &[u8]) -> bool {
58 if operand.is_empty() || operand.len() > candidate.len() {
59 return false;
60 }
61 let start: usize = candidate.len() - operand.len();
62 let at_boundary: bool = start == 0 || candidate[start - 1] == b'/';
63 return at_boundary && candidate[start..].eq_ignore_ascii_case(operand);
64}
65
66fn registered_paths(common_dir: &Path, worktrees: &Path) -> Result<Vec<Vec<u8>>, String> {
77 let mut paths: Vec<Vec<u8>> = Vec::new();
78 let main: &[u8] = common_dir.as_os_str().as_bytes();
79 paths.push(main.strip_suffix(b"/.git").unwrap_or(main).to_vec());
80 let listing = match std::fs::read_dir(worktrees) {
81 Ok(listing) => listing,
82 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(paths),
83 Err(error) => return Err(format!("the registrations in {} cannot be listed ({error})", worktrees.display())),
84 };
85 for found in listing {
86 let entry: std::fs::DirEntry = found.map_err(|error: std::io::Error| { return format!("the registrations in {} cannot be listed ({error})", worktrees.display()); })?;
87 let Ok(text) = std::fs::read(entry.path().join("gitdir")) else { continue };
88 let trimmed: &[u8] = text.trim_ascii_end();
89 if trimmed.is_empty() {
90 continue;
91 }
92 let path: &[u8] = trimmed.strip_suffix(b"/.git").unwrap_or(trimmed);
93 if path.starts_with(b"/") {
94 paths.push(path.to_vec());
95 } else {
96 let joined: PathBuf = entry.path().join(OsStr::from_bytes(path));
98 let resolved: PathBuf = std::fs::canonicalize(joined.as_path()).unwrap_or(joined);
99 paths.push(resolved.into_os_string().into_vec());
100 }
101 }
102 return Ok(paths);
103}
104
105pub fn registration_of(common_dir: &Path, target: &Path, operand: &OsStr) -> Result<Registration, String> {
116 let pointer_path: PathBuf = target.join(".git");
117 let pointer: Vec<u8> = std::fs::read(pointer_path.as_path()).map_err(|error: std::io::Error| { return format!("{} cannot be read as a file ({error}), so it is not a linked worktree", pointer_path.display()); })?;
118 let line: &[u8] = without_newline(pointer.as_slice());
119 let named: &[u8] = line.strip_prefix(b"gitdir: ").ok_or_else(|| { return format!("{} does not start with `gitdir: `", pointer_path.display()); })?;
120 let admin: PathBuf = std::fs::canonicalize(target.join(OsStr::from_bytes(named)))
121 .map_err(|error: std::io::Error| { return format!("the administrative directory {} that {} names cannot be resolved ({error})", String::from_utf8_lossy(named), pointer_path.display()); })?;
122 let worktrees: PathBuf = std::fs::canonicalize(common_dir.join("worktrees"))
123 .map_err(|error: std::io::Error| { return format!("{} cannot be resolved ({error}), so this repository has no linked worktree", common_dir.join("worktrees").display()); })?;
124 if admin.parent() != Some(worktrees.as_path()) {
125 return Err(format!(
126 "the administrative directory {} of {} is not one of the registrations in {}, the repository this command runs in",
127 admin.display(),
128 target.display(),
129 worktrees.display()
130 ));
131 }
132 let id: OsString = admin.file_name().ok_or_else(|| { return format!("{} has no name", admin.display()); })?.to_os_string();
133 let gitdir_text: Vec<u8> = std::fs::read(admin.join("gitdir")).map_err(|error: std::io::Error| { return format!("{} cannot be read ({error})", admin.join("gitdir").display()); })?;
134 let recorded: PathBuf = PathBuf::from(OsStr::from_bytes(without_newline(gitdir_text.as_slice())));
135 if recorded != pointer_path {
136 return Err(format!("{} records {}, not {}", admin.join("gitdir").display(), recorded.display(), pointer_path.display()));
137 }
138 let registered: Vec<Vec<u8>> = registered_paths(common_dir, worktrees.as_path())?;
139 for path in registered {
140 if ends_with_name(path.as_slice(), operand.as_bytes()) && path.as_slice() != target.as_os_str().as_bytes() {
141 return Err(format!(
142 "Git matches the name {} to the registered worktree {} by its last components, not to {}",
143 operand.to_string_lossy(),
144 String::from_utf8_lossy(path.as_slice()),
145 target.display()
146 ));
147 }
148 }
149 return Ok(Registration { id, admin, worktrees, gitdir_text });
150}
151
152#[cfg(test)]
154#[path = "worktree_remove_registry_tests.rs"]
155mod tests;