Skip to main content

workon/
worktree.rs

1//! Worktree descriptor and metadata access.
2//!
3//! This module provides the core `WorktreeDescriptor` type that wraps git2's `Worktree`
4//! and exposes rich metadata about worktree state.
5//!
6//! ## Completed Metadata Methods
7//!
8//! The following metadata is fully implemented and working:
9//! - **Basic info**: `name()`, `path()`, `branch()`
10//! - **State detection**: `is_detached()`, `is_dirty()`, `is_valid()`, `is_locked()`
11//! - **Remote tracking**: `remote()`, `remote_branch()`, `remote_url()`, `remote_fetch_url()`, `remote_push_url()`
12//! - **Commit info**: `head_commit()`
13//! - **Status checks**: `has_unpushed_commits()`, `is_behind_upstream()`, `has_gone_upstream()`, `is_merged_into()`
14//!
15//! These methods enable status filtering (`--dirty`, `--ahead`, `--behind`, `--gone`) and
16//! interactive display with status indicators.
17//!
18//! ## Branch Types
19//!
20//! Supports three branch types for worktree creation:
21//! - **Normal**: Standard branch, tracks existing or creates from HEAD
22//! - **Orphan**: Independent history with initial empty commit (for documentation, gh-pages, etc.)
23//! - **Detached**: Detached HEAD state (for exploring specific commits)
24//!
25//! - **Activity tracking**: `last_activity()`, `is_stale()`
26//!
27//! ## Future Extensions
28//!
29//! Planned metadata methods for smart worktree management:
30//!
31//! TODO: Add worktree notes/descriptions support
32//! - Store user-provided notes/context for worktrees
33//! - Help remember why a worktree was created
34//! - Storage strategy TBD (git notes, config, or metadata file)
35
36use std::{fmt, fs::create_dir_all, path::Path};
37
38use git2::WorktreeAddOptions;
39use git2::{Repository, Worktree};
40use log::debug;
41
42use crate::error::{Result, WorktreeError};
43use crate::workon_root;
44
45/// Type of branch to create for a new worktree
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum BranchType {
48    /// Normal branch - track existing or create from HEAD
49    #[default]
50    Normal,
51    /// Orphan branch - independent history with initial empty commit
52    Orphan,
53    /// Detached HEAD
54    Detached,
55}
56
57/// A handle to a git worktree with rich metadata access.
58///
59/// Wraps a [`git2::Worktree`] and exposes branch state, remote tracking info,
60/// commit history, and status checks used by the CLI commands.
61pub struct WorktreeDescriptor {
62    worktree: Worktree,
63}
64
65impl WorktreeDescriptor {
66    /// Open a worktree by name within the given repository.
67    pub fn new(repo: &Repository, name: &str) -> Result<Self> {
68        Ok(Self {
69            worktree: repo.find_worktree(name)?,
70        })
71    }
72
73    /// Wrap an existing [`git2::Worktree`] without a repository lookup.
74    pub fn of(worktree: Worktree) -> Self {
75        Self { worktree }
76    }
77
78    /// Returns the name of the worktree, or `None` if the name is invalid UTF-8.
79    pub fn name(&self) -> Option<&str> {
80        self.worktree.name()
81    }
82
83    /// Returns the filesystem path to the worktree's working directory.
84    pub fn path(&self) -> &Path {
85        self.worktree.path()
86    }
87
88    /// Returns the branch name if the worktree is on a branch, or None if detached.
89    ///
90    /// This reads the HEAD file from the worktree's git directory to determine
91    /// if HEAD points to a branch reference or directly to a commit SHA.
92    pub fn branch(&self) -> Result<Option<String>> {
93        use std::fs;
94
95        // Get the path to the worktree's HEAD file
96        let git_dir = self.worktree.path().join(".git");
97        let head_path = if git_dir.is_file() {
98            // Linked worktree - read .git file to find actual git directory
99            let git_file_content = fs::read_to_string(&git_dir)?;
100            let git_dir_path = git_file_content
101                .strip_prefix("gitdir: ")
102                .and_then(|s| s.trim().strip_suffix('\n').or(Some(s.trim())))
103                .ok_or(WorktreeError::InvalidGitFile)?;
104            Path::new(git_dir_path).join("HEAD")
105        } else {
106            // Main worktree
107            git_dir.join("HEAD")
108        };
109
110        let head_content = fs::read_to_string(&head_path)?;
111
112        // HEAD file contains either:
113        // - "ref: refs/heads/branch-name" for a branch
114        // - A direct SHA for detached HEAD
115        if let Some(ref_line) = head_content.strip_prefix("ref: ") {
116            let ref_name = ref_line.trim();
117            Ok(ref_name.strip_prefix("refs/heads/").map(|s| s.to_string()))
118        } else {
119            // Direct SHA - detached HEAD
120            Ok(None)
121        }
122    }
123
124    /// Returns true if the worktree has a detached HEAD (not on a branch).
125    pub fn is_detached(&self) -> Result<bool> {
126        Ok(self.branch()?.is_none())
127    }
128
129    /// Returns true if the worktree has uncommitted changes (dirty working tree).
130    ///
131    /// This includes:
132    /// - Modified files (staged or unstaged)
133    /// - New untracked files
134    /// - Deleted files
135    pub fn is_dirty(&self) -> Result<bool> {
136        let repo = Repository::open(self.path())?;
137        let statuses = repo.statuses(None)?;
138        Ok(!statuses.is_empty())
139    }
140
141    /// Returns true if the worktree has a lock file.
142    ///
143    /// Locked worktrees are protected from pruning unless `--include-locked`
144    /// or `--force` is used.
145    pub fn is_locked(&self) -> Result<bool> {
146        Ok(!matches!(
147            self.worktree.is_locked()?,
148            git2::WorktreeLockStatus::Unlocked
149        ))
150    }
151
152    /// Returns true if the worktree's path and git metadata are intact.
153    ///
154    /// A worktree is invalid if its directory is missing or its git
155    /// metadata is broken.
156    pub fn is_valid(&self) -> bool {
157        self.worktree.validate().is_ok()
158    }
159
160    /// Returns true if the worktree has uncommitted changes to tracked files.
161    ///
162    /// Unlike `is_dirty()`, this excludes untracked files. Use this when
163    /// untracked files should not block an operation (e.g. pruning a worktree
164    /// whose remote branch is gone).
165    pub fn has_tracked_changes(&self) -> Result<bool> {
166        let repo = Repository::open(self.path())?;
167        let mut opts = git2::StatusOptions::new();
168        opts.include_untracked(false);
169        let statuses = repo.statuses(Some(&mut opts))?;
170        Ok(!statuses.is_empty())
171    }
172
173    /// Returns true if the worktree's branch has unpushed commits (ahead of upstream).
174    ///
175    /// Returns false if:
176    /// - The worktree is detached (no branch)
177    /// - The branch has no upstream configured
178    /// - The branch is up to date with upstream
179    ///
180    /// Returns true if:
181    /// - The branch has commits ahead of its upstream
182    /// - The upstream is configured but the remote reference is gone (conservative)
183    pub fn has_unpushed_commits(&self) -> Result<bool> {
184        // Get the branch name - return false if detached
185        let branch_name = match self.branch()? {
186            Some(name) => name,
187            None => return Ok(false), // Detached HEAD, no branch to check
188        };
189
190        // Open the repository (use the bare repo, not the worktree)
191        let repo = Repository::open(self.path())?;
192
193        // Find the local branch
194        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
195            Ok(b) => b,
196            Err(_) => return Ok(false), // Branch doesn't exist
197        };
198
199        // Check if upstream is configured via git config
200        let config = repo.config()?;
201        let remote_key = format!("branch.{}.remote", branch_name);
202
203        // If no upstream is configured, there can't be unpushed commits
204        let _remote = match config.get_string(&remote_key) {
205            Ok(r) => r,
206            Err(_) => return Ok(false), // No remote configured
207        };
208
209        // Get the upstream branch
210        let upstream = match branch.upstream() {
211            Ok(u) => u,
212            Err(_) => {
213                // Upstream is configured but ref is gone - conservatively assume unpushed
214                return Ok(true);
215            }
216        };
217
218        // Get the local and upstream commit OIDs
219        let local_oid = branch
220            .get()
221            .target()
222            .ok_or(WorktreeError::NoLocalBranchTarget)?;
223        let upstream_oid = upstream
224            .get()
225            .target()
226            .ok_or(WorktreeError::NoBranchTarget)?;
227
228        // Check if local is ahead of upstream
229        let (ahead, _behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
230
231        Ok(ahead > 0)
232    }
233
234    /// Returns true if the worktree's branch is behind its upstream.
235    ///
236    /// Returns false if:
237    /// - The worktree is detached (no branch)
238    /// - The branch has no upstream configured
239    /// - The branch is up to date with upstream
240    /// - The upstream is configured but the remote reference is gone
241    ///
242    /// Returns true if:
243    /// - The branch has commits behind its upstream
244    pub fn is_behind_upstream(&self) -> Result<bool> {
245        // Get the branch name - return false if detached
246        let branch_name = match self.branch()? {
247            Some(name) => name,
248            None => return Ok(false), // Detached HEAD, no branch to check
249        };
250
251        // Open the repository
252        let repo = Repository::open(self.path())?;
253
254        // Find the local branch
255        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
256            Ok(b) => b,
257            Err(_) => return Ok(false), // Branch doesn't exist
258        };
259
260        // Check if upstream is configured via git config
261        let config = repo.config()?;
262        let remote_key = format!("branch.{}.remote", branch_name);
263
264        // If no upstream is configured, can't be behind
265        let _remote = match config.get_string(&remote_key) {
266            Ok(r) => r,
267            Err(_) => return Ok(false), // No remote configured
268        };
269
270        // Get the upstream branch
271        let upstream = match branch.upstream() {
272            Ok(u) => u,
273            Err(_) => {
274                // Upstream is configured but ref is gone - can't be behind non-existent branch
275                return Ok(false);
276            }
277        };
278
279        // Get the local and upstream commit OIDs
280        let local_oid = branch
281            .get()
282            .target()
283            .ok_or(WorktreeError::NoLocalBranchTarget)?;
284        let upstream_oid = upstream
285            .get()
286            .target()
287            .ok_or(WorktreeError::NoBranchTarget)?;
288
289        // Check if local is behind upstream
290        let (_ahead, behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
291
292        Ok(behind > 0)
293    }
294
295    /// Returns true if the worktree's upstream branch reference is gone (deleted on remote).
296    ///
297    /// Returns false if:
298    /// - The worktree is detached (no branch)
299    /// - The branch has no upstream configured
300    /// - The upstream branch reference exists
301    ///
302    /// Returns true if:
303    /// - Upstream is configured (branch.{name}.remote exists in config)
304    /// - But the upstream branch reference cannot be found
305    pub fn has_gone_upstream(&self) -> Result<bool> {
306        // Get the branch name - return false if detached
307        let branch_name = match self.branch()? {
308            Some(name) => name,
309            None => return Ok(false), // Detached HEAD, no branch to check
310        };
311
312        // Open the repository
313        let repo = Repository::open(self.path())?;
314
315        // Find the local branch
316        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
317            Ok(b) => b,
318            Err(_) => return Ok(false), // Branch doesn't exist
319        };
320
321        // Check if upstream is configured via git config
322        let config = repo.config()?;
323        let remote_key = format!("branch.{}.remote", branch_name);
324
325        // If no upstream is configured, it's not "gone"
326        match config.get_string(&remote_key) {
327            Ok(_) => {
328                // Upstream is configured - check if the reference exists
329                match branch.upstream() {
330                    Ok(_) => Ok(false), // Upstream exists
331                    Err(_) => Ok(true), // Upstream configured but ref is gone
332                }
333            }
334            Err(_) => Ok(false), // No upstream configured
335        }
336    }
337
338    /// Returns true if the worktree's branch has been merged into the target branch.
339    ///
340    /// A branch is considered merged if its HEAD commit is reachable from the target branch,
341    /// meaning all commits in this branch exist in the target branch's history.
342    ///
343    /// Returns false if:
344    /// - The worktree is detached (no branch)
345    /// - The target branch doesn't exist
346    /// - The branch has commits not in the target branch
347    ///
348    /// Returns true if:
349    /// - All commits in this branch are reachable from the target branch
350    pub fn is_merged_into(&self, target_branch: &str) -> Result<bool> {
351        // Get the branch name - return false if detached
352        let branch_name = match self.branch()? {
353            Some(name) => name,
354            None => return Ok(false), // Detached HEAD, no branch to check
355        };
356
357        // Don't consider the target branch as merged into itself
358        if branch_name == target_branch {
359            return Ok(false);
360        }
361
362        // Open the bare repository (not the worktree) to check actual branch states
363        // The worktree's .git points to the commondir (bare repo)
364        let worktree_repo = Repository::open(self.path())?;
365        let commondir = worktree_repo.commondir();
366        let repo = Repository::open(commondir)?;
367
368        // Find the current branch
369        let current_branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
370            Ok(b) => b,
371            Err(_) => return Ok(false), // Branch doesn't exist
372        };
373
374        // Find the target branch
375        let target = match repo.find_branch(target_branch, git2::BranchType::Local) {
376            Ok(b) => b,
377            Err(_) => return Ok(false), // Target branch doesn't exist
378        };
379
380        // Get commit OIDs
381        let current_oid = current_branch
382            .get()
383            .target()
384            .ok_or(WorktreeError::NoCurrentBranchTarget)?;
385        let target_oid = target.get().target().ok_or(WorktreeError::NoBranchTarget)?;
386
387        // If they point to the same commit, the branch is merged
388        if current_oid == target_oid {
389            return Ok(true);
390        }
391
392        // Check if current branch's commit is reachable from target
393        // This means target is a descendant of (or equal to) current
394        Ok(repo.graph_descendant_of(target_oid, current_oid)?)
395    }
396
397    /// Returns the commit hash (SHA) of the worktree's current HEAD.
398    ///
399    /// Returns None if HEAD cannot be resolved (e.g., empty repository).
400    pub fn head_commit(&self) -> Result<Option<String>> {
401        let repo = Repository::open(self.path())?;
402
403        // Try to resolve HEAD to a commit and extract the OID immediately
404        let commit_oid = match repo.head() {
405            Ok(head) => match head.peel_to_commit() {
406                Ok(commit) => Some(commit.id()),
407                Err(_) => return Ok(None), // HEAD exists but can't resolve to commit
408            },
409            Err(_) => return Ok(None), // No HEAD (unborn branch)
410        };
411
412        Ok(commit_oid.map(|oid| oid.to_string()))
413    }
414
415    /// Returns the timestamp of the HEAD commit as the last activity time.
416    ///
417    /// Returns None if:
418    /// - HEAD cannot be resolved (empty/unborn repository)
419    /// - HEAD cannot be peeled to a commit
420    pub fn last_activity(&self) -> Result<Option<i64>> {
421        let repo = Repository::open(self.path())?;
422        let seconds = match repo.head() {
423            Ok(head) => match head.peel_to_commit() {
424                Ok(commit) => Some(commit.time().seconds()),
425                Err(_) => None,
426            },
427            Err(_) => None,
428        };
429        Ok(seconds)
430    }
431
432    /// Returns true if the worktree's last activity is older than `days` days.
433    ///
434    /// Returns false if:
435    /// - Last activity cannot be determined
436    /// - The worktree has recent activity within the threshold
437    pub fn is_stale(&self, days: u32) -> Result<bool> {
438        let last = match self.last_activity()? {
439            Some(ts) => ts,
440            None => return Ok(false),
441        };
442        let now = std::time::SystemTime::now()
443            .duration_since(std::time::UNIX_EPOCH)
444            .map_err(std::io::Error::other)?
445            .as_secs() as i64;
446        let threshold = i64::from(days) * 86400;
447        Ok((now - last) > threshold)
448    }
449
450    /// Returns the name of the remote that the worktree's branch tracks (e.g., "origin").
451    ///
452    /// Returns None if:
453    /// - The worktree is detached (no branch)
454    /// - The branch has no upstream configured
455    pub fn remote(&self) -> Result<Option<String>> {
456        // Get the branch name - return None if detached
457        let branch_name = match self.branch()? {
458            Some(name) => name,
459            None => return Ok(None), // Detached HEAD, no branch to check
460        };
461
462        let repo = Repository::open(self.path())?;
463        let config = repo.config()?;
464
465        // Check for branch.<name>.remote in git config
466        let remote_key = format!("branch.{}.remote", branch_name);
467        match config.get_string(&remote_key) {
468            Ok(remote) => Ok(Some(remote)),
469            Err(_) => Ok(None), // No remote configured
470        }
471    }
472
473    /// Returns the full name of the upstream remote branch (e.g., "refs/remotes/origin/main").
474    ///
475    /// Returns None if:
476    /// - The worktree is detached (no branch)
477    /// - The branch has no upstream configured
478    pub fn remote_branch(&self) -> Result<Option<String>> {
479        // Get the branch name - return None if detached
480        let branch_name = match self.branch()? {
481            Some(name) => name,
482            None => return Ok(None), // Detached HEAD, no branch to check
483        };
484
485        let repo = Repository::open(self.path())?;
486
487        // Find the local branch and get its upstream, extracting the name immediately
488        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
489            Ok(b) => b,
490            Err(_) => return Ok(None), // Branch doesn't exist
491        };
492
493        let upstream_name = match branch.upstream() {
494            Ok(upstream) => match upstream.name() {
495                Ok(Some(name)) => Some(name.to_string()),
496                _ => None,
497            },
498            Err(_) => return Ok(None), // No upstream configured
499        };
500
501        Ok(upstream_name)
502    }
503
504    /// Returns the default URL for the remote (usually the fetch URL).
505    ///
506    /// Returns None if:
507    /// - The worktree is detached (no branch)
508    /// - The branch has no upstream configured
509    /// - The remote has no URL configured
510    pub fn remote_url(&self) -> Result<Option<String>> {
511        // Get the remote name
512        let remote_name = match self.remote()? {
513            Some(name) => name,
514            None => return Ok(None),
515        };
516
517        let repo = Repository::open(self.path())?;
518
519        // Find the remote and extract the URL immediately
520        let url = match repo.find_remote(&remote_name) {
521            Ok(remote) => remote.url().map(|s| s.to_string()),
522            Err(_) => return Ok(None), // Remote doesn't exist
523        };
524
525        Ok(url)
526    }
527
528    /// Returns the fetch URL for the remote.
529    ///
530    /// Returns None if:
531    /// - The worktree is detached (no branch)
532    /// - The branch has no upstream configured
533    /// - The remote has no fetch URL configured
534    pub fn remote_fetch_url(&self) -> Result<Option<String>> {
535        // Get the remote name
536        let remote_name = match self.remote()? {
537            Some(name) => name,
538            None => return Ok(None),
539        };
540
541        let repo = Repository::open(self.path())?;
542
543        // Find the remote and extract the fetch URL immediately
544        let url = match repo.find_remote(&remote_name) {
545            Ok(remote) => remote.url().map(|s| s.to_string()),
546            Err(_) => return Ok(None), // Remote doesn't exist
547        };
548
549        Ok(url)
550    }
551
552    /// Returns the push URL for the remote.
553    ///
554    /// Returns None if:
555    /// - The worktree is detached (no branch)
556    /// - The branch has no upstream configured
557    /// - The remote has no push URL configured (falls back to fetch URL)
558    pub fn remote_push_url(&self) -> Result<Option<String>> {
559        // Get the remote name
560        let remote_name = match self.remote()? {
561            Some(name) => name,
562            None => return Ok(None),
563        };
564
565        let repo = Repository::open(self.path())?;
566
567        // Find the remote and extract the push URL (or fallback to fetch URL) immediately
568        let url = match repo.find_remote(&remote_name) {
569            Ok(remote) => remote
570                .pushurl()
571                .or_else(|| remote.url())
572                .map(|s| s.to_string()),
573            Err(_) => return Ok(None), // Remote doesn't exist
574        };
575
576        Ok(url)
577    }
578}
579
580impl fmt::Debug for WorktreeDescriptor {
581    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582        write!(f, "WorktreeDescriptor({:?})", self.worktree.path())
583    }
584}
585
586impl fmt::Display for WorktreeDescriptor {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        write!(f, "{}", self.worktree.path().display())
589    }
590}
591
592/// Return all worktrees registered with the repository.
593pub fn get_worktrees(repo: &Repository) -> Result<Vec<WorktreeDescriptor>> {
594    repo.worktrees()?
595        .into_iter()
596        .map(|name| {
597            let name = name.ok_or(WorktreeError::InvalidName)?;
598            WorktreeDescriptor::new(repo, name)
599        })
600        .collect()
601}
602
603/// Return the worktree that contains the current working directory.
604///
605/// Returns [`WorktreeError::NotInWorktree`] if the current directory is not
606/// inside any registered worktree.
607pub fn current_worktree(repo: &Repository) -> Result<WorktreeDescriptor> {
608    let current_dir = std::env::current_dir().map_err(std::io::Error::other)?;
609
610    let worktrees = get_worktrees(repo)?;
611    worktrees
612        .into_iter()
613        .find(|wt| current_dir.starts_with(wt.path()))
614        .ok_or_else(|| WorktreeError::NotInWorktree.into())
615}
616
617/// Find a worktree by its name or by its branch name.
618///
619/// Returns [`WorktreeError::NotFound`] if no matching worktree exists.
620pub fn find_worktree(repo: &Repository, name: &str) -> Result<WorktreeDescriptor> {
621    let worktrees = get_worktrees(repo)?;
622    worktrees
623        .into_iter()
624        .find(|wt| {
625            // Match by worktree name or branch name
626            wt.name() == Some(name) || wt.branch().ok().flatten().as_deref() == Some(name)
627        })
628        .ok_or_else(|| WorktreeError::NotFound(name.to_string()).into())
629}
630
631/// Create a new worktree for the given branch.
632///
633/// The worktree directory is placed under the workon root (see [`workon_root`]).
634/// Branch names containing `/` are supported; parent directories are created
635/// automatically and the worktree is named after the final path component.
636///
637/// # Branch types
638///
639/// - [`BranchType::Normal`] — uses an existing local/remote branch, or creates one from
640///   `base_branch` (or HEAD if `base_branch` is `None`).
641/// - [`BranchType::Orphan`] — creates an independent branch with no shared history,
642///   seeded with an empty initial commit.
643/// - [`BranchType::Detached`] — creates a worktree with a detached HEAD pointing to
644///   the current HEAD commit.
645pub fn add_worktree(
646    repo: &Repository,
647    branch_name: &str,
648    branch_type: BranchType,
649    base_branch: Option<&str>,
650    lock: bool,
651) -> Result<WorktreeDescriptor> {
652    // git worktree add <branch>
653    debug!(
654        "adding worktree for branch {:?} with type: {:?}",
655        branch_name, branch_type
656    );
657
658    let reference = match branch_type {
659        BranchType::Orphan => {
660            debug!("creating orphan branch {:?}", branch_name);
661            // For orphan branches, we'll create the branch after the worktree
662            None
663        }
664        BranchType::Detached => {
665            debug!("creating detached HEAD worktree at {:?}", branch_name);
666            // For detached worktrees, we don't create or use a branch reference
667            None
668        }
669        BranchType::Normal => {
670            let branch = match repo.find_branch(branch_name, git2::BranchType::Local) {
671                Ok(b) => b,
672                Err(e) => {
673                    debug!("local branch not found: {:?}", e);
674                    debug!("looking for remote branch {:?}", branch_name);
675                    match repo.find_branch(branch_name, git2::BranchType::Remote) {
676                        Ok(b) => b,
677                        Err(e) => {
678                            debug!("remote branch not found: {:?}", e);
679                            debug!("creating new local branch {:?}", branch_name);
680
681                            // Determine which commit to branch from
682                            let base_commit = if let Some(base) = base_branch {
683                                // Branch from specified base branch
684                                debug!("branching from base branch {:?}", base);
685                                // Try local branch first, then remote branch
686                                let base_branch =
687                                    match repo.find_branch(base, git2::BranchType::Local) {
688                                        Ok(b) => b,
689                                        Err(_) => {
690                                            debug!("base branch not found as local, trying remote");
691                                            repo.find_branch(base, git2::BranchType::Remote)?
692                                        }
693                                    };
694                                base_branch.into_reference().peel_to_commit()?
695                            } else {
696                                // Default: branch from HEAD
697                                repo.head()?.peel_to_commit()?
698                            };
699
700                            repo.branch(branch_name, &base_commit, false)?
701                        }
702                    }
703                }
704            };
705
706            Some(branch.into_reference())
707        }
708    };
709
710    let root = workon_root(repo)?;
711
712    // Git does not support worktree names with slashes in them,
713    // so take the base of the branch name as the worktree name.
714    let worktree_name = match Path::new(&branch_name).file_name() {
715        Some(basename) => basename.to_str().ok_or(WorktreeError::InvalidName)?,
716        None => branch_name,
717    };
718
719    let worktree_path = root.join(branch_name);
720
721    // Create parent directories if the branch name contains slashes
722    if let Some(parent) = worktree_path.parent() {
723        create_dir_all(parent)?;
724    }
725
726    let mut opts = WorktreeAddOptions::new();
727    if let Some(ref r) = reference {
728        opts.reference(Some(r));
729    }
730    if lock {
731        opts.lock(true);
732    }
733
734    debug!(
735        "adding worktree {} at {}",
736        worktree_name,
737        worktree_path.display()
738    );
739
740    let worktree = repo.worktree(worktree_name, worktree_path.as_path(), Some(&opts))?;
741
742    // For detached worktrees, set HEAD to point directly to a commit SHA
743    if branch_type == BranchType::Detached {
744        debug!("setting up detached HEAD for worktree {:?}", branch_name);
745
746        use std::fs;
747
748        // Get the current HEAD commit SHA
749        let head_commit = repo.head()?.peel_to_commit()?;
750        let commit_sha = head_commit.id().to_string();
751
752        // Write the commit SHA directly to the worktree's HEAD file
753        let git_dir = repo.path().join("worktrees").join(worktree_name);
754        let head_path = git_dir.join("HEAD");
755        fs::write(&head_path, format!("{}\n", commit_sha).as_bytes())?;
756
757        debug!(
758            "detached HEAD setup complete for worktree {:?} at {}",
759            branch_name, commit_sha
760        );
761    }
762
763    // For orphan branches, create an initial empty commit with no parent
764    if branch_type == BranchType::Orphan {
765        debug!(
766            "setting up orphan branch {:?} with initial empty commit",
767            branch_name
768        );
769
770        use std::fs;
771
772        // Get the common directory (bare repo path) - important when running from a worktree
773        let common_dir = repo.commondir();
774
775        // First, manually set HEAD to point to the new branch as a symbolic reference
776        // This ensures we're not trying to update an existing branch
777        let git_dir = common_dir.join("worktrees").join(worktree_name);
778        let head_path = git_dir.join("HEAD");
779        let branch_ref = format!("ref: refs/heads/{}\n", branch_name);
780        fs::write(&head_path, branch_ref.as_bytes())?;
781
782        // Remove any existing branch ref that libgit2 may have created
783        let branch_ref_path = common_dir.join("refs/heads").join(branch_name);
784        let _ = fs::remove_file(&branch_ref_path);
785
786        // Open the worktree repository
787        let worktree_repo = Repository::open(&worktree_path)?;
788
789        // Remove all files from the working directory (but keep .git)
790        for entry in fs::read_dir(&worktree_path)? {
791            let entry = entry?;
792            let path = entry.path();
793            if path.file_name() != Some(std::ffi::OsStr::new(".git")) {
794                if path.is_dir() {
795                    fs::remove_dir_all(&path)?;
796                } else {
797                    fs::remove_file(&path)?;
798                }
799            }
800        }
801
802        // Clear the index to start fresh
803        let mut index = worktree_repo.index()?;
804        index.clear()?;
805        index.write()?;
806
807        // Create an empty tree for the initial commit
808        let tree_id = index.write_tree()?;
809        let tree = worktree_repo.find_tree(tree_id)?;
810
811        // Create signature for the commit
812        let config = worktree_repo.config()?;
813        let sig = worktree_repo.signature().or_else(|_| {
814            // Fallback if no git config is set
815            git2::Signature::now(
816                config
817                    .get_string("user.name")
818                    .unwrap_or_else(|_| "git-workon".to_string())
819                    .as_str(),
820                config
821                    .get_string("user.email")
822                    .unwrap_or_else(|_| "git-workon@localhost".to_string())
823                    .as_str(),
824            )
825        })?;
826
827        // Create initial commit with no parents (orphan)
828        worktree_repo.commit(
829            Some("HEAD"),
830            &sig,
831            &sig,
832            "Initial commit",
833            &tree,
834            &[], // No parents - this makes it an orphan
835        )?;
836
837        debug!("orphan branch setup complete for {:?}", branch_name);
838    }
839
840    Ok(WorktreeDescriptor::of(worktree))
841}
842
843/// Set upstream tracking for a worktree branch
844///
845/// Configures the branch in the worktree to track a remote branch by setting
846/// `branch.*.remote` and `branch.*.merge` configuration entries.
847///
848/// This is particularly important for PR worktrees to ensure they properly track
849/// the PR's remote branch.
850pub fn set_upstream_tracking(
851    worktree: &WorktreeDescriptor,
852    remote: &str,
853    remote_ref: &str,
854) -> Result<()> {
855    let repo = Repository::open(worktree.path())?;
856    let mut config = repo.config()?;
857
858    let head = repo.head()?;
859    let branch_name = head
860        .shorthand()
861        .ok_or(WorktreeError::NoCurrentBranchTarget)?;
862
863    // Set branch.*.remote
864    let remote_key = format!("branch.{}.remote", branch_name);
865    config.set_str(&remote_key, remote)?;
866
867    // Set branch.*.merge
868    let merge_key = format!("branch.{}.merge", branch_name);
869    config.set_str(&merge_key, remote_ref)?;
870
871    debug!(
872        "Set upstream tracking: {} -> {}/{}",
873        branch_name, remote, remote_ref
874    );
875    Ok(())
876}