@@ -2,12 +2,63 @@ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
22import * as path from "path" ;
33import { execSync } from "child_process" ;
44
5+ /**
6+ * Resolves the actual git directory path.
7+ *
8+ * In a normal repo, `.git` is a directory containing HEAD, refs, etc.
9+ * In a worktree, `.git` is a file containing `gitdir: /path/to/actual/git/dir`.
10+ *
11+ * @returns The resolved git directory path, or null if not a git repo
12+ */
13+ export function resolveGitDir ( repoRoot : string ) : string | null {
14+ const gitPath = path . join ( repoRoot , ".git" ) ;
15+
16+ if ( ! existsSync ( gitPath ) ) {
17+ return null ;
18+ }
19+
20+ try {
21+ const stat = statSync ( gitPath ) ;
22+
23+ if ( stat . isDirectory ( ) ) {
24+ // Normal repo: .git is a directory
25+ return gitPath ;
26+ }
27+
28+ if ( stat . isFile ( ) ) {
29+ // Worktree: .git is a file with gitdir pointer
30+ const content = readFileSync ( gitPath , "utf-8" ) . trim ( ) ;
31+ const match = content . match ( / ^ g i t d i r : \s * ( .+ ) $ / ) ;
32+ if ( match ) {
33+ const gitdir = match [ 1 ] ;
34+ // Handle relative paths
35+ const resolvedPath = path . isAbsolute ( gitdir )
36+ ? gitdir
37+ : path . resolve ( repoRoot , gitdir ) ;
38+
39+ if ( existsSync ( resolvedPath ) ) {
40+ return resolvedPath ;
41+ }
42+ }
43+ }
44+ } catch {
45+ // Ignore errors (permission issues, etc.)
46+ }
47+
48+ return null ;
49+ }
50+
551export function isGitRepo ( dir : string ) : boolean {
6- return existsSync ( path . join ( dir , ".git" ) ) ;
52+ return resolveGitDir ( dir ) !== null ;
753}
854
955export function getCurrentBranch ( repoRoot : string ) : string | null {
10- const headPath = path . join ( repoRoot , ".git" , "HEAD" ) ;
56+ const gitDir = resolveGitDir ( repoRoot ) ;
57+ if ( ! gitDir ) {
58+ return null ;
59+ }
60+
61+ const headPath = path . join ( gitDir , "HEAD" ) ;
1162
1263 if ( ! existsSync ( headPath ) ) {
1364 return null ;
@@ -16,13 +67,11 @@ export function getCurrentBranch(repoRoot: string): string | null {
1667 try {
1768 const headContent = readFileSync ( headPath , "utf-8" ) . trim ( ) ;
1869
19- // Check if it's a symbolic reference (normal branch)
2070 const match = headContent . match ( / ^ r e f : r e f s \/ h e a d s \/ ( .+ ) $ / ) ;
2171 if ( match ) {
2272 return match [ 1 ] ;
2373 }
2474
25- // Detached HEAD - return short commit hash
2675 if ( / ^ [ 0 - 9 a - f ] { 40 } $ / i. test ( headContent ) ) {
2776 return headContent . slice ( 0 , 7 ) ;
2877 }
@@ -47,30 +96,30 @@ export function getCurrentCommit(repoRoot: string): string | null {
4796}
4897
4998export function getBaseBranch ( repoRoot : string ) : string {
50- // Try to detect the default branch
99+ const gitDir = resolveGitDir ( repoRoot ) ;
51100 const candidates = [ "main" , "master" , "develop" , "trunk" ] ;
52101
53- for ( const candidate of candidates ) {
54- const refPath = path . join ( repoRoot , ".git" , "refs" , "heads" , candidate ) ;
55- if ( existsSync ( refPath ) ) {
56- return candidate ;
57- }
58-
59- // Also check packed-refs
60- const packedRefsPath = path . join ( repoRoot , ".git" , "packed-refs" ) ;
61- if ( existsSync ( packedRefsPath ) ) {
62- try {
63- const content = readFileSync ( packedRefsPath , "utf-8" ) ;
64- if ( content . includes ( `refs/heads/${ candidate } ` ) ) {
65- return candidate ;
102+ if ( gitDir ) {
103+ for ( const candidate of candidates ) {
104+ const refPath = path . join ( gitDir , "refs" , "heads" , candidate ) ;
105+ if ( existsSync ( refPath ) ) {
106+ return candidate ;
107+ }
108+
109+ const packedRefsPath = path . join ( gitDir , "packed-refs" ) ;
110+ if ( existsSync ( packedRefsPath ) ) {
111+ try {
112+ const content = readFileSync ( packedRefsPath , "utf-8" ) ;
113+ if ( content . includes ( `refs/heads/${ candidate } ` ) ) {
114+ return candidate ;
115+ }
116+ } catch {
117+ // Ignore
66118 }
67- } catch {
68- // Ignore
69119 }
70120 }
71121 }
72122
73- // Try git remote show origin
74123 try {
75124 const result = execSync ( "git remote show origin" , {
76125 cwd : repoRoot ,
@@ -85,13 +134,18 @@ export function getBaseBranch(repoRoot: string): string {
85134 // Ignore - remote might not exist
86135 }
87136
88- // Fallback to current branch or "main"
89137 return getCurrentBranch ( repoRoot ) ?? "main" ;
90138}
91139
92140export function getAllBranches ( repoRoot : string ) : string [ ] {
93141 const branches : string [ ] = [ ] ;
94- const refsPath = path . join ( repoRoot , ".git" , "refs" , "heads" ) ;
142+ const gitDir = resolveGitDir ( repoRoot ) ;
143+
144+ if ( ! gitDir ) {
145+ return branches ;
146+ }
147+
148+ const refsPath = path . join ( gitDir , "refs" , "heads" ) ;
95149
96150 if ( ! existsSync ( refsPath ) ) {
97151 return branches ;
@@ -111,7 +165,6 @@ export function getAllBranches(repoRoot: string): string[] {
111165 }
112166 }
113167 } catch {
114- // Fallback: read refs directory
115168 try {
116169 const entries = readdirSync ( refsPath ) ;
117170 for ( const entry of entries ) {
@@ -158,5 +211,9 @@ export function getBranchOrDefault(repoRoot: string): string {
158211}
159212
160213export function getHeadPath ( repoRoot : string ) : string {
214+ const gitDir = resolveGitDir ( repoRoot ) ;
215+ if ( gitDir ) {
216+ return path . join ( gitDir , "HEAD" ) ;
217+ }
161218 return path . join ( repoRoot , ".git" , "HEAD" ) ;
162219}
0 commit comments