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 uncommitted changes to tracked files.
142    ///
143    /// Unlike `is_dirty()`, this excludes untracked files. Use this when
144    /// untracked files should not block an operation (e.g. pruning a worktree
145    /// whose remote branch is gone).
146    pub fn has_tracked_changes(&self) -> Result<bool> {
147        let repo = Repository::open(self.path())?;
148        let mut opts = git2::StatusOptions::new();
149        opts.include_untracked(false);
150        let statuses = repo.statuses(Some(&mut opts))?;
151        Ok(!statuses.is_empty())
152    }
153
154    /// Returns true if the worktree's branch has unpushed commits (ahead of upstream).
155    ///
156    /// Returns false if:
157    /// - The worktree is detached (no branch)
158    /// - The branch has no upstream configured
159    /// - The branch is up to date with upstream
160    ///
161    /// Returns true if:
162    /// - The branch has commits ahead of its upstream
163    /// - The upstream is configured but the remote reference is gone (conservative)
164    pub fn has_unpushed_commits(&self) -> Result<bool> {
165        // Get the branch name - return false if detached
166        let branch_name = match self.branch()? {
167            Some(name) => name,
168            None => return Ok(false), // Detached HEAD, no branch to check
169        };
170
171        // Open the repository (use the bare repo, not the worktree)
172        let repo = Repository::open(self.path())?;
173
174        // Find the local branch
175        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
176            Ok(b) => b,
177            Err(_) => return Ok(false), // Branch doesn't exist
178        };
179
180        // Check if upstream is configured via git config
181        let config = repo.config()?;
182        let remote_key = format!("branch.{}.remote", branch_name);
183
184        // If no upstream is configured, there can't be unpushed commits
185        let _remote = match config.get_string(&remote_key) {
186            Ok(r) => r,
187            Err(_) => return Ok(false), // No remote configured
188        };
189
190        // Get the upstream branch
191        let upstream = match branch.upstream() {
192            Ok(u) => u,
193            Err(_) => {
194                // Upstream is configured but ref is gone - conservatively assume unpushed
195                return Ok(true);
196            }
197        };
198
199        // Get the local and upstream commit OIDs
200        let local_oid = branch
201            .get()
202            .target()
203            .ok_or(WorktreeError::NoLocalBranchTarget)?;
204        let upstream_oid = upstream
205            .get()
206            .target()
207            .ok_or(WorktreeError::NoBranchTarget)?;
208
209        // Check if local is ahead of upstream
210        let (ahead, _behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
211
212        Ok(ahead > 0)
213    }
214
215    /// Returns true if the worktree's branch is behind its upstream.
216    ///
217    /// Returns false if:
218    /// - The worktree is detached (no branch)
219    /// - The branch has no upstream configured
220    /// - The branch is up to date with upstream
221    /// - The upstream is configured but the remote reference is gone
222    ///
223    /// Returns true if:
224    /// - The branch has commits behind its upstream
225    pub fn is_behind_upstream(&self) -> Result<bool> {
226        // Get the branch name - return false if detached
227        let branch_name = match self.branch()? {
228            Some(name) => name,
229            None => return Ok(false), // Detached HEAD, no branch to check
230        };
231
232        // Open the repository
233        let repo = Repository::open(self.path())?;
234
235        // Find the local branch
236        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
237            Ok(b) => b,
238            Err(_) => return Ok(false), // Branch doesn't exist
239        };
240
241        // Check if upstream is configured via git config
242        let config = repo.config()?;
243        let remote_key = format!("branch.{}.remote", branch_name);
244
245        // If no upstream is configured, can't be behind
246        let _remote = match config.get_string(&remote_key) {
247            Ok(r) => r,
248            Err(_) => return Ok(false), // No remote configured
249        };
250
251        // Get the upstream branch
252        let upstream = match branch.upstream() {
253            Ok(u) => u,
254            Err(_) => {
255                // Upstream is configured but ref is gone - can't be behind non-existent branch
256                return Ok(false);
257            }
258        };
259
260        // Get the local and upstream commit OIDs
261        let local_oid = branch
262            .get()
263            .target()
264            .ok_or(WorktreeError::NoLocalBranchTarget)?;
265        let upstream_oid = upstream
266            .get()
267            .target()
268            .ok_or(WorktreeError::NoBranchTarget)?;
269
270        // Check if local is behind upstream
271        let (_ahead, behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
272
273        Ok(behind > 0)
274    }
275
276    /// Returns true if the worktree's upstream branch reference is gone (deleted on remote).
277    ///
278    /// Returns false if:
279    /// - The worktree is detached (no branch)
280    /// - The branch has no upstream configured
281    /// - The upstream branch reference exists
282    ///
283    /// Returns true if:
284    /// - Upstream is configured (branch.{name}.remote exists in config)
285    /// - But the upstream branch reference cannot be found
286    pub fn has_gone_upstream(&self) -> Result<bool> {
287        // Get the branch name - return false if detached
288        let branch_name = match self.branch()? {
289            Some(name) => name,
290            None => return Ok(false), // Detached HEAD, no branch to check
291        };
292
293        // Open the repository
294        let repo = Repository::open(self.path())?;
295
296        // Find the local branch
297        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
298            Ok(b) => b,
299            Err(_) => return Ok(false), // Branch doesn't exist
300        };
301
302        // Check if upstream is configured via git config
303        let config = repo.config()?;
304        let remote_key = format!("branch.{}.remote", branch_name);
305
306        // If no upstream is configured, it's not "gone"
307        match config.get_string(&remote_key) {
308            Ok(_) => {
309                // Upstream is configured - check if the reference exists
310                match branch.upstream() {
311                    Ok(_) => Ok(false), // Upstream exists
312                    Err(_) => Ok(true), // Upstream configured but ref is gone
313                }
314            }
315            Err(_) => Ok(false), // No upstream configured
316        }
317    }
318
319    /// Returns true if the worktree's branch has been merged into the target branch.
320    ///
321    /// A branch is considered merged if its HEAD commit is reachable from the target branch,
322    /// meaning all commits in this branch exist in the target branch's history.
323    ///
324    /// Returns false if:
325    /// - The worktree is detached (no branch)
326    /// - The target branch doesn't exist
327    /// - The branch has commits not in the target branch
328    ///
329    /// Returns true if:
330    /// - All commits in this branch are reachable from the target branch
331    pub fn is_merged_into(&self, target_branch: &str) -> Result<bool> {
332        // Get the branch name - return false if detached
333        let branch_name = match self.branch()? {
334            Some(name) => name,
335            None => return Ok(false), // Detached HEAD, no branch to check
336        };
337
338        // Don't consider the target branch as merged into itself
339        if branch_name == target_branch {
340            return Ok(false);
341        }
342
343        // Open the bare repository (not the worktree) to check actual branch states
344        // The worktree's .git points to the commondir (bare repo)
345        let worktree_repo = Repository::open(self.path())?;
346        let commondir = worktree_repo.commondir();
347        let repo = Repository::open(commondir)?;
348
349        // Find the current branch
350        let current_branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
351            Ok(b) => b,
352            Err(_) => return Ok(false), // Branch doesn't exist
353        };
354
355        // Find the target branch
356        let target = match repo.find_branch(target_branch, git2::BranchType::Local) {
357            Ok(b) => b,
358            Err(_) => return Ok(false), // Target branch doesn't exist
359        };
360
361        // Get commit OIDs
362        let current_oid = current_branch
363            .get()
364            .target()
365            .ok_or(WorktreeError::NoCurrentBranchTarget)?;
366        let target_oid = target.get().target().ok_or(WorktreeError::NoBranchTarget)?;
367
368        // If they point to the same commit, the branch is merged
369        if current_oid == target_oid {
370            return Ok(true);
371        }
372
373        // Check if current branch's commit is reachable from target
374        // This means target is a descendant of (or equal to) current
375        Ok(repo.graph_descendant_of(target_oid, current_oid)?)
376    }
377
378    /// Returns the commit hash (SHA) of the worktree's current HEAD.
379    ///
380    /// Returns None if HEAD cannot be resolved (e.g., empty repository).
381    pub fn head_commit(&self) -> Result<Option<String>> {
382        let repo = Repository::open(self.path())?;
383
384        // Try to resolve HEAD to a commit and extract the OID immediately
385        let commit_oid = match repo.head() {
386            Ok(head) => match head.peel_to_commit() {
387                Ok(commit) => Some(commit.id()),
388                Err(_) => return Ok(None), // HEAD exists but can't resolve to commit
389            },
390            Err(_) => return Ok(None), // No HEAD (unborn branch)
391        };
392
393        Ok(commit_oid.map(|oid| oid.to_string()))
394    }
395
396    /// Returns the timestamp of the HEAD commit as the last activity time.
397    ///
398    /// Returns None if:
399    /// - HEAD cannot be resolved (empty/unborn repository)
400    /// - HEAD cannot be peeled to a commit
401    pub fn last_activity(&self) -> Result<Option<i64>> {
402        let repo = Repository::open(self.path())?;
403        let seconds = match repo.head() {
404            Ok(head) => match head.peel_to_commit() {
405                Ok(commit) => Some(commit.time().seconds()),
406                Err(_) => None,
407            },
408            Err(_) => None,
409        };
410        Ok(seconds)
411    }
412
413    /// Returns true if the worktree's last activity is older than `days` days.
414    ///
415    /// Returns false if:
416    /// - Last activity cannot be determined
417    /// - The worktree has recent activity within the threshold
418    pub fn is_stale(&self, days: u32) -> Result<bool> {
419        let last = match self.last_activity()? {
420            Some(ts) => ts,
421            None => return Ok(false),
422        };
423        let now = std::time::SystemTime::now()
424            .duration_since(std::time::UNIX_EPOCH)
425            .map_err(std::io::Error::other)?
426            .as_secs() as i64;
427        let threshold = i64::from(days) * 86400;
428        Ok((now - last) > threshold)
429    }
430
431    /// Returns the name of the remote that the worktree's branch tracks (e.g., "origin").
432    ///
433    /// Returns None if:
434    /// - The worktree is detached (no branch)
435    /// - The branch has no upstream configured
436    pub fn remote(&self) -> Result<Option<String>> {
437        // Get the branch name - return None if detached
438        let branch_name = match self.branch()? {
439            Some(name) => name,
440            None => return Ok(None), // Detached HEAD, no branch to check
441        };
442
443        let repo = Repository::open(self.path())?;
444        let config = repo.config()?;
445
446        // Check for branch.<name>.remote in git config
447        let remote_key = format!("branch.{}.remote", branch_name);
448        match config.get_string(&remote_key) {
449            Ok(remote) => Ok(Some(remote)),
450            Err(_) => Ok(None), // No remote configured
451        }
452    }
453
454    /// Returns the full name of the upstream remote branch (e.g., "refs/remotes/origin/main").
455    ///
456    /// Returns None if:
457    /// - The worktree is detached (no branch)
458    /// - The branch has no upstream configured
459    pub fn remote_branch(&self) -> Result<Option<String>> {
460        // Get the branch name - return None if detached
461        let branch_name = match self.branch()? {
462            Some(name) => name,
463            None => return Ok(None), // Detached HEAD, no branch to check
464        };
465
466        let repo = Repository::open(self.path())?;
467
468        // Find the local branch and get its upstream, extracting the name immediately
469        let branch = match repo.find_branch(&branch_name, git2::BranchType::Local) {
470            Ok(b) => b,
471            Err(_) => return Ok(None), // Branch doesn't exist
472        };
473
474        let upstream_name = match branch.upstream() {
475            Ok(upstream) => match upstream.name() {
476                Ok(Some(name)) => Some(name.to_string()),
477                _ => None,
478            },
479            Err(_) => return Ok(None), // No upstream configured
480        };
481
482        Ok(upstream_name)
483    }
484
485    /// Returns the default URL for the remote (usually the fetch URL).
486    ///
487    /// Returns None if:
488    /// - The worktree is detached (no branch)
489    /// - The branch has no upstream configured
490    /// - The remote has no URL configured
491    pub fn remote_url(&self) -> Result<Option<String>> {
492        // Get the remote name
493        let remote_name = match self.remote()? {
494            Some(name) => name,
495            None => return Ok(None),
496        };
497
498        let repo = Repository::open(self.path())?;
499
500        // Find the remote and extract the URL immediately
501        let url = match repo.find_remote(&remote_name) {
502            Ok(remote) => remote.url().map(|s| s.to_string()),
503            Err(_) => return Ok(None), // Remote doesn't exist
504        };
505
506        Ok(url)
507    }
508
509    /// Returns the fetch URL for the remote.
510    ///
511    /// Returns None if:
512    /// - The worktree is detached (no branch)
513    /// - The branch has no upstream configured
514    /// - The remote has no fetch URL configured
515    pub fn remote_fetch_url(&self) -> Result<Option<String>> {
516        // Get the remote name
517        let remote_name = match self.remote()? {
518            Some(name) => name,
519            None => return Ok(None),
520        };
521
522        let repo = Repository::open(self.path())?;
523
524        // Find the remote and extract the fetch URL immediately
525        let url = match repo.find_remote(&remote_name) {
526            Ok(remote) => remote.url().map(|s| s.to_string()),
527            Err(_) => return Ok(None), // Remote doesn't exist
528        };
529
530        Ok(url)
531    }
532
533    /// Returns the push URL for the remote.
534    ///
535    /// Returns None if:
536    /// - The worktree is detached (no branch)
537    /// - The branch has no upstream configured
538    /// - The remote has no push URL configured (falls back to fetch URL)
539    pub fn remote_push_url(&self) -> Result<Option<String>> {
540        // Get the remote name
541        let remote_name = match self.remote()? {
542            Some(name) => name,
543            None => return Ok(None),
544        };
545
546        let repo = Repository::open(self.path())?;
547
548        // Find the remote and extract the push URL (or fallback to fetch URL) immediately
549        let url = match repo.find_remote(&remote_name) {
550            Ok(remote) => remote
551                .pushurl()
552                .or_else(|| remote.url())
553                .map(|s| s.to_string()),
554            Err(_) => return Ok(None), // Remote doesn't exist
555        };
556
557        Ok(url)
558    }
559}
560
561impl fmt::Debug for WorktreeDescriptor {
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        write!(f, "WorktreeDescriptor({:?})", self.worktree.path())
564    }
565}
566
567impl fmt::Display for WorktreeDescriptor {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        write!(f, "{}", self.worktree.path().display())
570    }
571}
572
573/// Return all worktrees registered with the repository.
574pub fn get_worktrees(repo: &Repository) -> Result<Vec<WorktreeDescriptor>> {
575    repo.worktrees()?
576        .into_iter()
577        .map(|name| {
578            let name = name.ok_or(WorktreeError::InvalidName)?;
579            WorktreeDescriptor::new(repo, name)
580        })
581        .collect()
582}
583
584/// Return the worktree that contains the current working directory.
585///
586/// Returns [`WorktreeError::NotInWorktree`] if the current directory is not
587/// inside any registered worktree.
588pub fn current_worktree(repo: &Repository) -> Result<WorktreeDescriptor> {
589    let current_dir = std::env::current_dir().map_err(std::io::Error::other)?;
590
591    let worktrees = get_worktrees(repo)?;
592    worktrees
593        .into_iter()
594        .find(|wt| current_dir.starts_with(wt.path()))
595        .ok_or_else(|| WorktreeError::NotInWorktree.into())
596}
597
598/// Find a worktree by its name or by its branch name.
599///
600/// Returns [`WorktreeError::NotFound`] if no matching worktree exists.
601pub fn find_worktree(repo: &Repository, name: &str) -> Result<WorktreeDescriptor> {
602    let worktrees = get_worktrees(repo)?;
603    worktrees
604        .into_iter()
605        .find(|wt| {
606            // Match by worktree name or branch name
607            wt.name() == Some(name) || wt.branch().ok().flatten().as_deref() == Some(name)
608        })
609        .ok_or_else(|| WorktreeError::NotFound(name.to_string()).into())
610}
611
612/// Create a new worktree for the given branch.
613///
614/// The worktree directory is placed under the workon root (see [`workon_root`]).
615/// Branch names containing `/` are supported; parent directories are created
616/// automatically and the worktree is named after the final path component.
617///
618/// # Branch types
619///
620/// - [`BranchType::Normal`] — uses an existing local/remote branch, or creates one from
621///   `base_branch` (or HEAD if `base_branch` is `None`).
622/// - [`BranchType::Orphan`] — creates an independent branch with no shared history,
623///   seeded with an empty initial commit.
624/// - [`BranchType::Detached`] — creates a worktree with a detached HEAD pointing to
625///   the current HEAD commit.
626pub fn add_worktree(
627    repo: &Repository,
628    branch_name: &str,
629    branch_type: BranchType,
630    base_branch: Option<&str>,
631) -> Result<WorktreeDescriptor> {
632    // git worktree add <branch>
633    debug!(
634        "adding worktree for branch {:?} with type: {:?}",
635        branch_name, branch_type
636    );
637
638    let reference = match branch_type {
639        BranchType::Orphan => {
640            debug!("creating orphan branch {:?}", branch_name);
641            // For orphan branches, we'll create the branch after the worktree
642            None
643        }
644        BranchType::Detached => {
645            debug!("creating detached HEAD worktree at {:?}", branch_name);
646            // For detached worktrees, we don't create or use a branch reference
647            None
648        }
649        BranchType::Normal => {
650            let branch = match repo.find_branch(branch_name, git2::BranchType::Local) {
651                Ok(b) => b,
652                Err(e) => {
653                    debug!("local branch not found: {:?}", e);
654                    debug!("looking for remote branch {:?}", branch_name);
655                    match repo.find_branch(branch_name, git2::BranchType::Remote) {
656                        Ok(b) => b,
657                        Err(e) => {
658                            debug!("remote branch not found: {:?}", e);
659                            debug!("creating new local branch {:?}", branch_name);
660
661                            // Determine which commit to branch from
662                            let base_commit = if let Some(base) = base_branch {
663                                // Branch from specified base branch
664                                debug!("branching from base branch {:?}", base);
665                                // Try local branch first, then remote branch
666                                let base_branch =
667                                    match repo.find_branch(base, git2::BranchType::Local) {
668                                        Ok(b) => b,
669                                        Err(_) => {
670                                            debug!("base branch not found as local, trying remote");
671                                            repo.find_branch(base, git2::BranchType::Remote)?
672                                        }
673                                    };
674                                base_branch.into_reference().peel_to_commit()?
675                            } else {
676                                // Default: branch from HEAD
677                                repo.head()?.peel_to_commit()?
678                            };
679
680                            repo.branch(branch_name, &base_commit, false)?
681                        }
682                    }
683                }
684            };
685
686            Some(branch.into_reference())
687        }
688    };
689
690    let root = workon_root(repo)?;
691
692    // Git does not support worktree names with slashes in them,
693    // so take the base of the branch name as the worktree name.
694    let worktree_name = match Path::new(&branch_name).file_name() {
695        Some(basename) => basename.to_str().ok_or(WorktreeError::InvalidName)?,
696        None => branch_name,
697    };
698
699    let worktree_path = root.join(branch_name);
700
701    // Create parent directories if the branch name contains slashes
702    if let Some(parent) = worktree_path.parent() {
703        create_dir_all(parent)?;
704    }
705
706    let mut opts = WorktreeAddOptions::new();
707    if let Some(ref r) = reference {
708        opts.reference(Some(r));
709    }
710
711    debug!(
712        "adding worktree {} at {}",
713        worktree_name,
714        worktree_path.display()
715    );
716
717    let worktree = repo.worktree(worktree_name, worktree_path.as_path(), Some(&opts))?;
718
719    // For detached worktrees, set HEAD to point directly to a commit SHA
720    if branch_type == BranchType::Detached {
721        debug!("setting up detached HEAD for worktree {:?}", branch_name);
722
723        use std::fs;
724
725        // Get the current HEAD commit SHA
726        let head_commit = repo.head()?.peel_to_commit()?;
727        let commit_sha = head_commit.id().to_string();
728
729        // Write the commit SHA directly to the worktree's HEAD file
730        let git_dir = repo.path().join("worktrees").join(worktree_name);
731        let head_path = git_dir.join("HEAD");
732        fs::write(&head_path, format!("{}\n", commit_sha).as_bytes())?;
733
734        debug!(
735            "detached HEAD setup complete for worktree {:?} at {}",
736            branch_name, commit_sha
737        );
738    }
739
740    // For orphan branches, create an initial empty commit with no parent
741    if branch_type == BranchType::Orphan {
742        debug!(
743            "setting up orphan branch {:?} with initial empty commit",
744            branch_name
745        );
746
747        use std::fs;
748
749        // Get the common directory (bare repo path) - important when running from a worktree
750        let common_dir = repo.commondir();
751
752        // First, manually set HEAD to point to the new branch as a symbolic reference
753        // This ensures we're not trying to update an existing branch
754        let git_dir = common_dir.join("worktrees").join(worktree_name);
755        let head_path = git_dir.join("HEAD");
756        let branch_ref = format!("ref: refs/heads/{}\n", branch_name);
757        fs::write(&head_path, branch_ref.as_bytes())?;
758
759        // Remove any existing branch ref that libgit2 may have created
760        let branch_ref_path = common_dir.join("refs/heads").join(branch_name);
761        let _ = fs::remove_file(&branch_ref_path);
762
763        // Open the worktree repository
764        let worktree_repo = Repository::open(&worktree_path)?;
765
766        // Remove all files from the working directory (but keep .git)
767        for entry in fs::read_dir(&worktree_path)? {
768            let entry = entry?;
769            let path = entry.path();
770            if path.file_name() != Some(std::ffi::OsStr::new(".git")) {
771                if path.is_dir() {
772                    fs::remove_dir_all(&path)?;
773                } else {
774                    fs::remove_file(&path)?;
775                }
776            }
777        }
778
779        // Clear the index to start fresh
780        let mut index = worktree_repo.index()?;
781        index.clear()?;
782        index.write()?;
783
784        // Create an empty tree for the initial commit
785        let tree_id = index.write_tree()?;
786        let tree = worktree_repo.find_tree(tree_id)?;
787
788        // Create signature for the commit
789        let config = worktree_repo.config()?;
790        let sig = worktree_repo.signature().or_else(|_| {
791            // Fallback if no git config is set
792            git2::Signature::now(
793                config
794                    .get_string("user.name")
795                    .unwrap_or_else(|_| "git-workon".to_string())
796                    .as_str(),
797                config
798                    .get_string("user.email")
799                    .unwrap_or_else(|_| "git-workon@localhost".to_string())
800                    .as_str(),
801            )
802        })?;
803
804        // Create initial commit with no parents (orphan)
805        worktree_repo.commit(
806            Some("HEAD"),
807            &sig,
808            &sig,
809            "Initial commit",
810            &tree,
811            &[], // No parents - this makes it an orphan
812        )?;
813
814        debug!("orphan branch setup complete for {:?}", branch_name);
815    }
816
817    Ok(WorktreeDescriptor::of(worktree))
818}
819
820/// Set upstream tracking for a worktree branch
821///
822/// Configures the branch in the worktree to track a remote branch by setting
823/// `branch.*.remote` and `branch.*.merge` configuration entries.
824///
825/// This is particularly important for PR worktrees to ensure they properly track
826/// the PR's remote branch.
827pub fn set_upstream_tracking(
828    worktree: &WorktreeDescriptor,
829    remote: &str,
830    remote_ref: &str,
831) -> Result<()> {
832    let repo = Repository::open(worktree.path())?;
833    let mut config = repo.config()?;
834
835    let head = repo.head()?;
836    let branch_name = head
837        .shorthand()
838        .ok_or(WorktreeError::NoCurrentBranchTarget)?;
839
840    // Set branch.*.remote
841    let remote_key = format!("branch.{}.remote", branch_name);
842    config.set_str(&remote_key, remote)?;
843
844    // Set branch.*.merge
845    let merge_key = format!("branch.{}.merge", branch_name);
846    config.set_str(&merge_key, remote_ref)?;
847
848    debug!(
849        "Set upstream tracking: {} -> {}/{}",
850        branch_name, remote, remote_ref
851    );
852    Ok(())
853}