workon/worktree_name.rs
1//! Encodes a root-relative worktree path into a worktree admin (metadata directory) name.
2//!
3//! See [ADR-027](../../../docs/adr/027-path-encoded-worktree-names.md). Git discovers
4//! worktrees by listing `.bare/worktrees/` exactly one level deep, so the admin name can
5//! never contain `/`. Deriving it as a plain basename lets two worktrees whose paths end
6//! in the same component collide (`ee/feature-name` and `archive/feature-name` both reduce
7//! to `feature-name`). Encoding the full path avoids that: every `/` becomes `~`, a
8//! separator `git check-ref-format` rejects, so an encoded name can never alias a real
9//! branch and no branch can ever produce one by accident.
10
11use std::path::Path;
12
13use git2::Repository;
14
15use crate::workon_root;
16
17/// Encode a root-relative worktree path into a worktree admin name.
18///
19/// Replaces every `/` with `~`. Called at the two sites that compute an admin name from a
20/// path — [`add_worktree`](crate::add_worktree) and
21/// [`move_worktree`](crate::move_worktree) — never anywhere else. Nothing decodes the
22/// result back into a path; a stored name is a label, not a key.
23pub fn encode_worktree_name(relative_path: &str) -> String {
24 relative_path.replace('/', "~")
25}
26
27/// Compute `path`'s root-relative path, robust to symlinks on either side.
28///
29/// `workon_root()` and a worktree's `path()` can disagree on symlink resolution — git
30/// canonicalizes the paths it writes into `gitdir`/`commondir`, while a path computed
31/// from `workon_root()` may not be (e.g. macOS routes both `/tmp` and `/var/folders`
32/// through symlinks). A plain `strip_prefix` fails silently in that case, which would
33/// make every path-based lookup miss every worktree under a symlinked root. Canonicalize
34/// both sides first, falling back to the original when `canonicalize` fails (e.g. a
35/// worktree directory that no longer exists).
36///
37/// Returns `None` if `path` isn't inside `workon_root()`. The result always uses `/`
38/// separators, matching the format `encode_worktree_name` expects.
39pub fn relative_worktree_path(repo: &Repository, path: &Path) -> Option<String> {
40 let root = workon_root(repo).ok()?;
41 let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
42 let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
43 let relative = canonical_path.strip_prefix(&canonical_root).ok()?;
44 let relative = relative.to_str()?;
45 Some(if std::path::MAIN_SEPARATOR == '/' {
46 relative.to_string()
47 } else {
48 relative.replace(std::path::MAIN_SEPARATOR, "/")
49 })
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn encodes_namespaced_path() {
58 assert_eq!(encode_worktree_name("ee/feature-name"), "ee~feature-name");
59 }
60
61 #[test]
62 fn leaves_top_level_name_unchanged() {
63 assert_eq!(encode_worktree_name("feature-name"), "feature-name");
64 }
65
66 #[test]
67 fn encodes_every_separator_in_a_nested_path() {
68 assert_eq!(encode_worktree_name("a/b/c"), "a~b~c");
69 }
70}