Skip to main content

lds_git/
session.rs

1//! Session helpers: adoption of orphan worktrees / branches that a previous
2//! session owned but never released.
3//!
4//! `session_release` is the counterpart to `worktree_add` — when an MCP
5//! client crashes and leaves `.worktrees/foo` on disk, the next session can
6//! adopt it (and the branch checked out inside it) so that the normal
7//! `worktree_remove` / `branch_delete` ownership checks pass.
8
9use std::path::PathBuf;
10
11use anyhow::Result;
12
13use crate::output::SessionReleaseOutput;
14use crate::{GitModule, git_cmd};
15
16impl GitModule {
17    /// Adopt orphan worktrees under `.worktrees/` (those known to `git
18    /// worktree list` but not yet owned by this session). Any branches
19    /// currently checked out inside them are adopted at the same time so
20    /// the normal `branch_delete` ownership check will pass.
21    ///
22    /// Returns the set of worktrees / branches that were newly adopted.
23    /// Already-owned entries are skipped silently — calling this repeatedly
24    /// is idempotent.
25    pub fn session_release(&mut self) -> Result<SessionReleaseOutput> {
26        let raw = git_cmd(self.session().root(), &["worktree", "list", "--porcelain"])?;
27
28        let mut adopted_worktrees: Vec<PathBuf> = Vec::new();
29        let mut adopted_branches: Vec<String> = Vec::new();
30
31        let worktrees_dir = self.worktrees_dir();
32        let root = self.session().root().to_path_buf();
33
34        // Canonicalise once up-front so symlink-resolved tempdirs (e.g. macOS
35        // `/var/...` vs `/private/var/...`) don't make `starts_with` lie.
36        let canonical_worktrees_dir = worktrees_dir
37            .canonicalize()
38            .unwrap_or_else(|_| worktrees_dir.clone());
39        let canonical_root = root.canonicalize().unwrap_or_else(|_| root.clone());
40
41        let mut current_path: Option<PathBuf> = None;
42        let mut current_branch: Option<String> = None;
43
44        let entries: Vec<(PathBuf, Option<String>)> = {
45            let mut acc = Vec::new();
46            for line in raw.lines() {
47                if line.is_empty() {
48                    if let Some(p) = current_path.take() {
49                        acc.push((p, current_branch.take()));
50                    }
51                    continue;
52                }
53                if let Some(p) = line.strip_prefix("worktree ") {
54                    current_path = Some(PathBuf::from(p));
55                } else if let Some(b) = line.strip_prefix("branch ") {
56                    current_branch = Some(b.trim_start_matches("refs/heads/").to_string());
57                }
58            }
59            if let Some(p) = current_path {
60                acc.push((p, current_branch));
61            }
62            acc
63        };
64
65        for (path, branch) in entries {
66            let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
67
68            // Never adopt the main worktree — that's the session root, which
69            // is implicitly addressable but not ownership-tracked.
70            if canonical_path == canonical_root {
71                continue;
72            }
73            // Only adopt worktrees under `.worktrees/` — anything outside is
74            // probably user-managed and shouldn't be silently claimed.
75            if !canonical_path.starts_with(&canonical_worktrees_dir) {
76                continue;
77            }
78            if self.is_owned(&canonical_path) || self.is_owned(&path) {
79                continue;
80            }
81            self.register_worktree(canonical_path.clone());
82            adopted_worktrees.push(path.clone());
83            if let Some(b) = branch
84                && !self.owned_branches.contains(&b)
85            {
86                self.register_branch(b.clone());
87                adopted_branches.push(b);
88            }
89        }
90
91        Ok(SessionReleaseOutput {
92            adopted_worktrees,
93            adopted_branches,
94        })
95    }
96}