Skip to content

Commit b451b05

Browse files
committed
feat(sidebar): show worktrees under their repository
Add an opt-in sidebar mode that lists linked worktrees beneath their main repository 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 and synthesize sidebar rows for worktrees that are not already stored as repositories - render nested rows with the worktree folder name, keep alias styling behavior unchanged, and avoid duplicate pull-all work for linked worktrees - persist sidebar worktree metadata in repository state and expose main worktree path helpers needed for grouping - preload main-repository worktree state for the sidebar, show a loading hint while discovery is in flight, and refresh the parent row when a linked worktree is selected - use preloaded parent worktree metadata so stored linked worktree rows show their branch pill on first render instead of waiting for an explicit refresh - add unit coverage for grouped, synthetic, loading, and stored-linked branch-label cases and throttle sidebar worktree refreshes to reduce repeated git worktree list churn during indicator updates Testing: - yarn test:unit app/test/unit/repositories-list-grouping-test.ts - yarn eslint app/src/lib/stores/app-store.ts app/src/models/repository.ts app/src/ui/repositories-list/group-repositories.ts app/src/ui/repositories-list/repositories-list.tsx app/src/ui/repositories-list/repository-list-item.tsx app/test/unit/repositories-list-grouping-test.ts - yarn compile:dev
1 parent bce9f0a commit b451b05

15 files changed

Lines changed: 1021 additions & 69 deletions

File tree

.github/actions/setup-ci-environment/action.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,22 @@ runs:
3333

3434
- name: Install and build dependencies
3535
shell: bash
36-
run: yarn
36+
run: |
37+
max_attempts=3
38+
39+
for attempt in $(seq 1 "$max_attempts"); do
40+
if yarn; then
41+
exit 0
42+
fi
43+
44+
if [ "$attempt" -eq "$max_attempts" ]; then
45+
exit 1
46+
fi
47+
48+
sleep_seconds=$((attempt * 10))
49+
echo "yarn install failed on attempt $attempt/$max_attempts, retrying in ${sleep_seconds}s..."
50+
sleep "$sleep_seconds"
51+
done
3752
env:
3853
npm_config_arch: ${{ inputs.arch }}
3954
TARGET_ARCH: ${{ inputs.arch }}

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: 56 additions & 3 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> {
@@ -140,12 +149,56 @@ export async function getMainWorktreePath(
140149
* whether `.git` is a file (linked worktree) or directory (main worktree).
141150
*/
142151
export function isLinkedWorktreeSync(repositoryPath: string): boolean {
152+
const worktreeInfo = getWorktreePathInfoSync(repositoryPath)
153+
return worktreeInfo?.isLinkedWorktree ?? false
154+
}
155+
156+
export function getWorktreePathInfoSync(
157+
repositoryPath: string
158+
): IWorktreePathInfo | null {
143159
try {
144-
const dotGit = Path.join(repositoryPath, '.git')
160+
const dotGit = getDotGitPath(repositoryPath)
145161
// eslint-disable-next-line no-sync
146162
const stats = Fs.statSync(dotGit)
147-
return stats.isFile()
163+
164+
if (stats.isDirectory()) {
165+
return { isLinkedWorktree: false, mainWorktreePath: repositoryPath }
166+
}
167+
168+
if (!stats.isFile()) {
169+
return null
170+
}
171+
172+
// eslint-disable-next-line no-sync
173+
const contents = Fs.readFileSync(dotGit, 'utf8').trim()
174+
if (!contents.startsWith('gitdir: ')) {
175+
return null
176+
}
177+
178+
const gitDirPath = Path.resolve(
179+
repositoryPath,
180+
contents.substring('gitdir: '.length)
181+
)
182+
183+
// eslint-disable-next-line no-sync
184+
const commondir = Fs.readFileSync(
185+
Path.join(gitDirPath, 'commondir'),
186+
'utf8'
187+
).trim()
188+
if (commondir.length === 0) {
189+
return null
190+
}
191+
192+
const commonGitDir = Path.resolve(gitDirPath, commondir)
193+
return {
194+
isLinkedWorktree: true,
195+
mainWorktreePath: Path.dirname(commonGitDir),
196+
}
148197
} catch {
149-
return false
198+
return null
150199
}
151200
}
201+
202+
export function getMainWorktreePathSync(repositoryPath: string): string | null {
203+
return getWorktreePathInfoSync(repositoryPath)?.mainWorktreePath ?? null
204+
}

0 commit comments

Comments
 (0)