Skip to main content

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}