Skip to content

Commit 8a87499

Browse files
committed
feat(sidebar): show worktrees under their repository
Add an optional sidebar mode that shows linked worktrees nested under their repository in the main repository list so repository switching can stay in the main sidebar instead of requiring the worktree dropdown. Changes: - add a secondary Appearance setting to show worktrees in the repository sidebar when worktree support is enabled - group linked worktrees under their main repository in the sidebar - synthesize child rows for linked worktrees discovered from `git worktree list` even when those worktrees were never added as repositories - support linked-only setups by synthesizing sibling worktree rows even when the stored entry is itself a linked worktree instead of the main worktree - use worktree folder names for child row labels while preserving existing alias styling for saved repository entries - use the same displayed-title logic for sorting and disambiguation so nested rows sort and label consistently with what the user sees - preload main-repository worktree state for the sidebar so nested rows and stored linked-worktree branch pills are available on initial render instead of only after opening the worktree dropdown or forcing another sidebar refresh - refresh parent sidebar rows when linked worktrees are selected so nested rows stay in sync with the active repository view - surface nested worktree rows from both saved worktree repositories and synthetic virtual rows without duplicating entries already stored in Desktop - avoid duplicate `Pull all` work for linked worktrees while still including orphan linked worktrees when the main repo is absent from the stored repository list - route virtual worktree open failures through the normal app error path instead of silently failing - open synthetic worktree rows transiently instead of persisting them as top-level repositories under `Other` - keep stored linked-worktree rows on the repository-management context menu while giving synthetic rows a worktree-specific context menu that uses `PopupType.DeleteWorktree` - persist sidebar worktree metadata in repository state and extract the sidebar-specific state shaping into a dedicated helper to keep `app-store.ts` smaller - throttle sidebar worktree refreshes during repository indicator updates to reduce repeated `git worktree list` churn - prune sidebar worktree refresh timestamps when repository lists change so stale cache entries do not accumulate - tighten TypeScript null/undefined handling in repository and sidebar list code so production webpack builds pass across the full CI matrix - add and extend unit coverage for grouped rows, synthetic rows, linked-only setups, stored linked-worktree branch labels, and repository-list context menu behavior Behavioral effect: Users can opt into seeing and switching linked worktrees directly from the main repository sidebar, including unstored Git worktrees, with branch labels, parent-child grouping, and worktree-aware context menu behavior available without forcing those virtual rows into the saved repository list. Testing: - yarn test:unit app/test/unit/repositories-list-grouping-test.ts - yarn test:unit app/test/unit/repository-list-item-context-menu-test.ts - yarn lint - yarn compile:dev - yarn compile:prod
1 parent bce9f0a commit 8a87499

17 files changed

Lines changed: 1497 additions & 101 deletions

app/src/lib/app-state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ export interface IAppState {
321321
/** Whether or not the worktrees dropdown should be shown in the toolbar */
322322
readonly showWorktrees: boolean
323323

324+
/** Whether linked worktrees should be shown under their repository in the sidebar */
325+
readonly showWorktreesInSidebar: boolean
326+
324327
/** Whether or not the Compare tab should be shown in the repository view */
325328
readonly showCompareTab: boolean
326329

app/src/lib/git/worktree.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import type { WorktreeEntry, WorktreeType } from '../../models/worktree'
55
import { git } from './core'
66
import { normalizePath } from '../helpers/path'
77

8+
function getDotGitPath(repositoryPath: string): string {
9+
return Path.join(repositoryPath, '.git')
10+
}
11+
12+
export interface IWorktreePathInfo {
13+
readonly isLinkedWorktree: boolean
14+
readonly mainWorktreePath: string | null
15+
}
16+
817
export function parseWorktreePorcelainOutput(
918
stdout: string
1019
): ReadonlyArray<WorktreeEntry> {
@@ -104,6 +113,10 @@ export async function removeWorktree(
104113
await git(args, repository.path, 'removeWorktree')
105114
}
106115

116+
export async function pruneWorktrees(repository: Repository): Promise<void> {
117+
await git(['worktree', 'prune'], repository.path, 'pruneWorktrees')
118+
}
119+
107120
export async function moveWorktree(
108121
repository: Repository,
109122
oldPath: string,
@@ -135,17 +148,48 @@ export async function getMainWorktreePath(
135148
return main?.path ?? null
136149
}
137150

138-
/**
139-
* Synchronously checks if a repository path is a linked worktree by examining
140-
* whether `.git` is a file (linked worktree) or directory (main worktree).
141-
*/
142-
export function isLinkedWorktreeSync(repositoryPath: string): boolean {
151+
export function getWorktreePathInfoSync(
152+
repositoryPath: string
153+
): IWorktreePathInfo | null {
143154
try {
144-
const dotGit = Path.join(repositoryPath, '.git')
155+
const dotGit = getDotGitPath(repositoryPath)
145156
// eslint-disable-next-line no-sync
146157
const stats = Fs.statSync(dotGit)
147-
return stats.isFile()
158+
159+
if (stats.isDirectory()) {
160+
return { isLinkedWorktree: false, mainWorktreePath: repositoryPath }
161+
}
162+
163+
if (!stats.isFile()) {
164+
return null
165+
}
166+
167+
// eslint-disable-next-line no-sync
168+
const contents = Fs.readFileSync(dotGit, 'utf8').trim()
169+
if (!contents.startsWith('gitdir: ')) {
170+
return null
171+
}
172+
173+
const gitDirPath = Path.resolve(
174+
repositoryPath,
175+
contents.substring('gitdir: '.length)
176+
)
177+
178+
// eslint-disable-next-line no-sync
179+
const commondir = Fs.readFileSync(
180+
Path.join(gitDirPath, 'commondir'),
181+
'utf8'
182+
).trim()
183+
if (commondir.length === 0) {
184+
return null
185+
}
186+
187+
const commonGitDir = Path.resolve(gitDirPath, commondir)
188+
return {
189+
isLinkedWorktree: true,
190+
mainWorktreePath: Path.dirname(commonGitDir),
191+
}
148192
} catch {
149-
return false
193+
return null
150194
}
151195
}

0 commit comments

Comments
 (0)