Skip to content

Commit 5e228fd

Browse files
committed
Add specific context menu for linked worktrees in repo list
1 parent f2da58e commit 5e228fd

3 files changed

Lines changed: 180 additions & 21 deletions

File tree

app/src/ui/app.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3424,22 +3424,26 @@ export class App extends React.Component<IAppProps, IAppState> {
34243424
}
34253425
}
34263426

3427-
private openInShell = (repository: Repository | CloningRepository) => {
3427+
private openInShell = (
3428+
repository: Repository | CloningRepository,
3429+
path?: string
3430+
) => {
34283431
if (!(repository instanceof Repository)) {
34293432
return
34303433
}
34313434

3432-
this.props.dispatcher.openShell(repository.path)
3435+
this.props.dispatcher.openShell(path ?? repository.path)
34333436
}
34343437

34353438
private openRepositoryInNewWindow = (
3436-
repository: Repository | CloningRepository | null
3439+
repository: Repository | CloningRepository | null,
3440+
path?: string
34373441
) => {
34383442
if (!(repository instanceof Repository) || repository.missing) {
34393443
return
34403444
}
34413445

3442-
openRepositoryInNewWindow(repository.path)
3446+
openRepositoryInNewWindow(path ?? repository.path)
34433447
}
34443448

34453449
private showRepositoryPreferences = () => {
@@ -3465,13 +3469,17 @@ export class App extends React.Component<IAppProps, IAppState> {
34653469
}
34663470

34673471
private openInExternalEditor = (
3468-
repository: Repository | CloningRepository
3472+
repository: Repository | CloningRepository,
3473+
path?: string
34693474
) => {
34703475
if (!(repository instanceof Repository)) {
34713476
return
34723477
}
34733478

3474-
this.props.dispatcher.openInExternalEditor(repository, repository.path)
3479+
this.props.dispatcher.openInExternalEditor(
3480+
repository,
3481+
path ?? repository.path
3482+
)
34753483
}
34763484

34773485
private openRepositoryInSelectedEditor = async (
@@ -3502,12 +3510,15 @@ export class App extends React.Component<IAppProps, IAppState> {
35023510
this.props.dispatcher.openInExternalEditor(nonCloningRepository, fullPath)
35033511
}
35043512

3505-
private showRepository = (repository: Repository | CloningRepository) => {
3513+
private showRepository = (
3514+
repository: Repository | CloningRepository,
3515+
path?: string
3516+
) => {
35063517
if (!(repository instanceof Repository)) {
35073518
return
35083519
}
35093520

3510-
shell.showFolderContents(repository.path)
3521+
shell.showFolderContents(path ?? repository.path)
35113522
}
35123523

35133524
private onRepositoryDropdownStateChanged = (newState: DropdownState) => {

app/src/ui/repositories-list/repositories-list.tsx

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import { encodePathAsUrl } from '../../lib/path'
3131
import { TooltippedContent } from '../lib/tooltipped-content'
3232
import memoizeOne from 'memoize-one'
3333
import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut'
34-
import { generateRepositoryListContextMenu } from '../repositories-list/repository-list-item-context-menu'
34+
import {
35+
generateRepositoryListContextMenu,
36+
generateWorktreeListItemContextMenu,
37+
} from '../repositories-list/repository-list-item-context-menu'
38+
import { openWorktreeInNewWindow } from '../main-process-proxy'
3539
import { enableWorktreeSupport } from '../../lib/feature-flag'
3640
import { SectionFilterList } from '../lib/section-filter-list'
3741
import { assertNever } from '../../lib/fatal-error'
@@ -63,19 +67,22 @@ interface IRepositoriesListProps {
6367
readonly onRemoveRepository: (repository: Repositoryish) => void
6468

6569
/** Called when the repository should be shown in Finder/Explorer/File Manager. */
66-
readonly onShowRepository: (repository: Repositoryish) => void
70+
readonly onShowRepository: (repository: Repositoryish, path?: string) => void
6771

6872
/** Called when the repository should be opened in the default web browser. */
6973
readonly onViewOnGitHub: (repository: Repositoryish) => void
7074

7175
/** Called when the repository should be shown in the shell. */
72-
readonly onOpenInShell: (repository: Repositoryish) => void
76+
readonly onOpenInShell: (repository: Repositoryish, path?: string) => void
7377

7478
/** Called when the repository should be opened in a new window. */
75-
readonly onOpenInNewWindow: (repository: Repositoryish) => void
79+
readonly onOpenInNewWindow: (repository: Repositoryish, path?: string) => void
7680

7781
/** Called when the repository should be opened in an external editor */
78-
readonly onOpenInExternalEditor: (repository: Repositoryish) => void
82+
readonly onOpenInExternalEditor: (
83+
repository: Repositoryish,
84+
path?: string
85+
) => void
7986

8087
/** The current external editor selected by the user */
8188
readonly externalEditorLabel?: string
@@ -375,11 +382,38 @@ export class RepositoriesList extends React.Component<
375382
) => {
376383
event.preventDefault()
377384

385+
if (
386+
item.worktree !== null &&
387+
item.worktree.type === 'linked' &&
388+
item.repository instanceof Repository
389+
) {
390+
showContextualMenu(
391+
generateWorktreeListItemContextMenu({
392+
repository: item.repository,
393+
worktree: item.worktree,
394+
shellLabel: this.props.shellLabel,
395+
externalEditorLabel: this.getExternalEditorLabel(item.repository),
396+
onCreateWorktree: this.onCreateWorktree,
397+
onRenameWorktree: this.onRenameWorktree,
398+
onDeleteWorktree: this.onDeleteWorktree,
399+
onViewOnGitHub: this.props.onViewOnGitHub,
400+
onOpenWorktreeInNewWindow: this.onOpenWorktreeInNewWindow,
401+
onOpenInShell: this.props.onOpenInShell,
402+
onShowRepository: this.props.onShowRepository,
403+
onOpenInExternalEditor: this.props.onOpenInExternalEditor,
404+
onCopyWorktreePath: path =>
405+
this.props.dispatcher.copyPathToClipboard(path),
406+
})
407+
)
408+
return
409+
}
410+
378411
const isPinned =
379412
item.repository instanceof Repository &&
380413
this.state.pinnedRepositoriesIds.includes(item.repository.id)
381414

382415
const items = generateRepositoryListContextMenu({
416+
worktreePath: item.worktree?.path,
383417
onRemoveRepository: this.props.onRemoveRepository,
384418
onShowRepository: this.props.onShowRepository,
385419
onOpenInNewWindow: this.props.onOpenInNewWindow,
@@ -634,6 +668,25 @@ export class RepositoriesList extends React.Component<
634668
this.props.dispatcher.showWorktreesFoldout()
635669
}
636670

671+
private onRenameWorktree = (repository: Repository, worktreePath: string) => {
672+
this.props.dispatcher.showPopup({
673+
type: PopupType.RenameWorktree,
674+
repository,
675+
worktreePath,
676+
})
677+
}
678+
679+
private onDeleteWorktree = (repository: Repository, worktreePath: string) => {
680+
this.props.dispatcher.requestDeleteWorktree(repository, worktreePath)
681+
}
682+
683+
private onOpenWorktreeInNewWindow = (
684+
repository: Repository,
685+
worktreePath: string
686+
) => {
687+
openWorktreeInNewWindow(repository.id, worktreePath)
688+
}
689+
637690
private onChangeRepositoryGroupName = (repository: Repository) => {
638691
this.props.dispatcher.showPopup({
639692
type: PopupType.ChangeRepositoryGroupName,

app/src/ui/repositories-list/repository-list-item-context-menu.ts

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as Path from 'path'
2+
13
import {
24
isRepositoryWithGitHubRepository,
35
hasDefaultRemoteUrl,
@@ -6,6 +8,7 @@ import {
68
import { RepoType } from '../../models/github-repository'
79
import { IMenuItem } from '../../lib/menu-item'
810
import { Repositoryish } from './group-repositories'
11+
import { WorktreeEntry } from '../../models/worktree'
912
import { clipboard } from 'electron'
1013
import {
1114
RevealInFileManagerLabel,
@@ -19,10 +22,10 @@ interface IRepositoryListItemContextMenuConfig {
1922
externalEditorLabel: string | undefined
2023
askForConfirmationOnRemoveRepository: boolean
2124
onViewOnGitHub: (repository: Repositoryish) => void
22-
onOpenInNewWindow?: (repository: Repositoryish) => void
23-
onOpenInShell: (repository: Repositoryish) => void
24-
onShowRepository: (repository: Repositoryish) => void
25-
onOpenInExternalEditor: (repository: Repositoryish) => void
25+
onOpenInNewWindow?: (repository: Repositoryish, path?: string) => void
26+
onOpenInShell: (repository: Repositoryish, path?: string) => void
27+
onShowRepository: (repository: Repositoryish, path?: string) => void
28+
onOpenInExternalEditor: (repository: Repositoryish, path?: string) => void
2629
onRemoveRepository: (repository: Repositoryish) => void
2730
onChangeRepositoryAlias: (repository: Repository) => void
2831
onRemoveRepositoryAlias: (repository: Repository) => void
@@ -34,6 +37,7 @@ interface IRepositoryListItemContextMenuConfig {
3437
onUnpinRepository?: (repository: Repository) => void
3538
onCreateWorktree?: (repository: Repository) => void
3639
onShowWorktrees?: (repository: Repository) => void
40+
worktreePath?: string
3741
}
3842

3943
export const generateRepositoryListContextMenu = (
@@ -83,23 +87,25 @@ export const generateRepositoryListContextMenu = (
8387
label: __DARWIN__
8488
? 'Open Repository in New Window'
8589
: 'Open repository in new window',
86-
action: () => config.onOpenInNewWindow?.(repository),
90+
action: () =>
91+
config.onOpenInNewWindow?.(repository, config.worktreePath),
8792
},
8893
]
8994
: []),
9095
{
9196
label: openInShell,
92-
action: () => config.onOpenInShell(repository),
97+
action: () => config.onOpenInShell(repository, config.worktreePath),
9398
enabled: !missing,
9499
},
95100
{
96101
label: RevealInFileManagerLabel,
97-
action: () => config.onShowRepository(repository),
102+
action: () => config.onShowRepository(repository, config.worktreePath),
98103
enabled: !missing,
99104
},
100105
{
101106
label: openInExternalEditor,
102-
action: () => config.onOpenInExternalEditor(repository),
107+
action: () =>
108+
config.onOpenInExternalEditor(repository, config.worktreePath),
103109
enabled: !missing,
104110
},
105111
{ type: 'separator' },
@@ -112,6 +118,95 @@ export const generateRepositoryListContextMenu = (
112118
return items
113119
}
114120

121+
interface IWorktreeListItemContextMenuConfig {
122+
repository: Repository
123+
worktree: WorktreeEntry
124+
shellLabel: string | undefined
125+
externalEditorLabel: string | undefined
126+
onCreateWorktree: (repository: Repository) => void
127+
onRenameWorktree: (repository: Repository, worktreePath: string) => void
128+
onDeleteWorktree: (repository: Repository, worktreePath: string) => void
129+
onViewOnGitHub: (repository: Repositoryish) => void
130+
onOpenWorktreeInNewWindow: (
131+
repository: Repository,
132+
worktreePath: string
133+
) => void
134+
onOpenInShell: (repository: Repositoryish, path?: string) => void
135+
onShowRepository: (repository: Repositoryish, path?: string) => void
136+
onOpenInExternalEditor: (repository: Repositoryish, path?: string) => void
137+
onCopyWorktreePath: (path: string) => void
138+
}
139+
140+
export const generateWorktreeListItemContextMenu = (
141+
config: IWorktreeListItemContextMenuConfig
142+
): ReadonlyArray<IMenuItem> => {
143+
const { repository, worktree } = config
144+
const path = worktree.path
145+
const name = Path.basename(path)
146+
const isGitHub = isRepositoryWithGitHubRepository(repository)
147+
const hasOriginUrl = hasDefaultRemoteUrl(repository)
148+
const canModify = !worktree.isLocked
149+
const openInExternalEditor = config.externalEditorLabel
150+
? `Open in ${config.externalEditorLabel}`
151+
: DefaultEditorLabel
152+
const openInShell = config.shellLabel
153+
? `Open in ${config.shellLabel}`
154+
: DefaultShellLabel
155+
156+
return [
157+
{
158+
label: __DARWIN__ ? 'New Worktree…' : 'New worktree…',
159+
action: () => config.onCreateWorktree(repository),
160+
},
161+
{
162+
label: __DARWIN__ ? 'Rename Worktree…' : 'Rename worktree…',
163+
action: () => config.onRenameWorktree(repository, path),
164+
enabled: canModify,
165+
},
166+
{ type: 'separator' },
167+
{
168+
label: __DARWIN__ ? 'Copy Worktree Name' : 'Copy worktree name',
169+
action: () => clipboard.writeText(name),
170+
},
171+
{
172+
label: __DARWIN__ ? 'Copy Worktree Path' : 'Copy worktree path',
173+
action: () => config.onCopyWorktreePath(path),
174+
},
175+
{ type: 'separator' },
176+
{
177+
label: getViewOnBrowserLabel(
178+
isGitHub ? repository.gitHubRepository.type : null
179+
),
180+
action: () => config.onViewOnGitHub(repository),
181+
enabled: isGitHub || hasOriginUrl,
182+
},
183+
{
184+
label: __DARWIN__
185+
? 'Open Worktree in New Window'
186+
: 'Open worktree in new window',
187+
action: () => config.onOpenWorktreeInNewWindow(repository, path),
188+
},
189+
{
190+
label: openInShell,
191+
action: () => config.onOpenInShell(repository, path),
192+
},
193+
{
194+
label: RevealInFileManagerLabel,
195+
action: () => config.onShowRepository(repository, path),
196+
},
197+
{
198+
label: openInExternalEditor,
199+
action: () => config.onOpenInExternalEditor(repository, path),
200+
},
201+
{ type: 'separator' },
202+
{
203+
label: __DARWIN__ ? 'Delete Worktree…' : 'Delete worktree…',
204+
action: () => config.onDeleteWorktree(repository, path),
205+
enabled: canModify,
206+
},
207+
]
208+
}
209+
115210
function getViewOnBrowserLabel(repoType: RepoType | null) {
116211
switch (repoType) {
117212
case 'github':

0 commit comments

Comments
 (0)