Skip to content

Commit c06a5b9

Browse files
committed
Implement "Delete all local branches"
1 parent 74bdc3f commit c06a5b9

13 files changed

Lines changed: 218 additions & 2 deletions

app/src/lib/stores/app-store.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5535,6 +5535,24 @@ export class AppStore extends TypedBaseStore<IAppState> {
55355535
})
55365536
}
55375537

5538+
public async _deleteLocalBranches(
5539+
repository: Repository,
5540+
branches: ReadonlyArray<Branch>
5541+
): Promise<void> {
5542+
return this.withRefreshedGitHubRepository(repository, async repository => {
5543+
const gitStore = this.gitStoreCache.get(repository)
5544+
5545+
// Deleting a local branch is fast, so we can do it sequentially
5546+
for (const branch of branches) {
5547+
await gitStore.performFailableOperation(() =>
5548+
deleteLocalBranch(repository, branch.name)
5549+
)
5550+
}
5551+
5552+
return this._refreshRepository(repository)
5553+
})
5554+
}
5555+
55385556
/**
55395557
* Deletes the local branch. If the parameter `includeUpstream` is true, the
55405558
* upstream branch will be deleted also.

app/src/models/popup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { WorktreeEntry } from './worktree'
3232
export enum PopupType {
3333
RenameBranch = 'RenameBranch',
3434
DeleteBranch = 'DeleteBranch',
35+
DeleteAllLocalBranches = 'DeleteAllLocalBranches',
3536
DeleteRemoteBranch = 'DeleteRemoteBranch',
3637
ConfirmDiscardChanges = 'ConfirmDiscardChanges',
3738
Preferences = 'Preferences',
@@ -158,6 +159,11 @@ export type PopupDetail =
158159
branch: Branch
159160
existsOnRemote: boolean
160161
}
162+
| {
163+
type: PopupType.DeleteAllLocalBranches
164+
repository: Repository
165+
branches: ReadonlyArray<Branch>
166+
}
161167
| {
162168
type: PopupType.DeleteRemoteBranch
163169
repository: Repository

app/src/ui/app.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
CantDeleteCurrentBranch,
5252
CantDeleteCurrentBranchUncommittedChanges,
5353
DeleteBranch,
54+
DeleteAllLocalBranches,
5455
DeleteRemoteBranch,
5556
} from './delete-branch'
5657
import { CantDeleteMainBranch } from './delete-branch/cant-delete-main-branch'
@@ -1741,6 +1742,17 @@ export class App extends React.Component<IAppProps, IAppState> {
17411742
onDeleted={this.onBranchDeleted}
17421743
/>
17431744
)
1745+
case PopupType.DeleteAllLocalBranches:
1746+
return (
1747+
<DeleteAllLocalBranches
1748+
key="delete-all-local-branches"
1749+
dispatcher={this.props.dispatcher}
1750+
repository={popup.repository}
1751+
branches={popup.branches}
1752+
onDismissed={onPopupDismissedFn}
1753+
onDeleted={this.onBranchDeleted}
1754+
/>
1755+
)
17441756
case PopupType.DeleteRemoteBranch:
17451757
return (
17461758
<DeleteRemoteBranch

app/src/ui/branches/branch-list-item-context-menu.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface IBranchContextMenuConfig {
1212
onViewPullRequestOnGitHub?: () => void
1313
onSetAsDefaultBranch?: (branchName: string) => void
1414
onDeleteBranch?: (branchName: string) => void
15+
onDeleteAllLocalBranches?: () => void
1516
onPullSingleBranch?: (branchName: string) => void
1617
onCheckoutInNewWorktree?: (branch: Branch) => void
1718
}
@@ -27,6 +28,7 @@ export function generateBranchContextMenuItems(
2728
onViewPullRequestOnGitHub,
2829
onSetAsDefaultBranch,
2930
onDeleteBranch,
31+
onDeleteAllLocalBranches,
3032
onPullSingleBranch,
3133
onCheckoutInNewWorktree,
3234
} = config
@@ -92,6 +94,15 @@ export function generateBranchContextMenuItems(
9294
})
9395
}
9496

97+
if (onDeleteAllLocalBranches !== undefined) {
98+
items.push({
99+
label: __DARWIN__
100+
? 'Delete All Local Branches…'
101+
: 'Delete all local branches…',
102+
action: () => onDeleteAllLocalBranches(),
103+
})
104+
}
105+
95106
return items
96107
}
97108

app/src/ui/branches/branch-list.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TextBox } from '../lib/text-box'
1313

1414
import {
1515
groupBranches,
16+
isLocalOnlyBranch,
1617
IBranchListItem,
1718
BranchGroupIdentifier,
1819
} from './group-branches'
@@ -145,6 +146,13 @@ interface IBranchListProps {
145146
/** Optional: Callback for if delete context menu should exist */
146147
readonly onDeleteBranch?: (branchName: string) => void
147148

149+
/**
150+
* Optional: Callback for if the "delete all local branches" context menu
151+
* should exist. It is only shown when the right-clicked branch is local-only
152+
* and the repository has at least three local-only branches.
153+
*/
154+
readonly onDeleteAllLocalBranches?: () => void
155+
148156
/** Optional: Callback to checkout a branch in a new worktree */
149157
readonly onCheckoutInNewWorktree?: (branch: Branch) => void
150158

@@ -243,6 +251,7 @@ export class BranchList extends React.Component<IBranchListProps> {
243251
const {
244252
onRenameBranch,
245253
onDeleteBranch,
254+
onDeleteAllLocalBranches,
246255
onCheckoutInNewWorktree,
247256
onSetAsDefaultBranch,
248257
onPullSingleBranch,
@@ -259,6 +268,15 @@ export class BranchList extends React.Component<IBranchListProps> {
259268

260269
const { branch } = item
261270

271+
// Only offer "delete all local branches" when right-clicking a local-only
272+
// branch and the repository has at least three local-only branches.
273+
const localOnlyBranchCount =
274+
this.props.allBranches.filter(isLocalOnlyBranch).length
275+
const canDeleteAllLocalBranches =
276+
onDeleteAllLocalBranches !== undefined &&
277+
isLocalOnlyBranch(branch) &&
278+
localOnlyBranchCount >= 3
279+
262280
const items = generateBranchContextMenuItems({
263281
branch,
264282
repoType: this.props.repository.gitHubRepository?.type,
@@ -268,6 +286,9 @@ export class BranchList extends React.Component<IBranchListProps> {
268286
? undefined
269287
: onSetAsDefaultBranch,
270288
onDeleteBranch,
289+
onDeleteAllLocalBranches: canDeleteAllLocalBranches
290+
? onDeleteAllLocalBranches
291+
: undefined,
271292
onPullSingleBranch,
272293
onCheckoutInNewWorktree,
273294
})

app/src/ui/branches/branches-container.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface IBranchesContainerProps {
5454
readonly onRenameBranch: (branchName: string) => void
5555
readonly onSetAsDefaultBranch: (branchName: string) => void
5656
readonly onDeleteBranch: (branchName: string) => void
57+
readonly onDeleteAllLocalBranches: () => void
5758
readonly onCheckoutInNewWorktree?: (branch: Branch) => void
5859

5960
/** Optional callback to checkout a PR in a new worktree */
@@ -298,6 +299,7 @@ export class BranchesContainer extends React.Component<
298299
onRenameBranch={this.props.onRenameBranch}
299300
onSetAsDefaultBranch={this.props.onSetAsDefaultBranch}
300301
onDeleteBranch={this.props.onDeleteBranch}
302+
onDeleteAllLocalBranches={this.props.onDeleteAllLocalBranches}
301303
onPullSingleBranch={this.props.onPullSingleBranch}
302304
onCheckoutInNewWorktree={
303305
enableWorktreeSupport()

app/src/ui/branches/group-branches.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Branch } from '../../models/branch'
1+
import { Branch, BranchType } from '../../models/branch'
22
import { BranchSortOrder } from '../../models/branch-sort-order'
33
import { WorktreeEntry } from '../../models/worktree'
44
import { IFilterListGroup, IFilterListItem } from '../lib/filter-list'
@@ -13,11 +13,19 @@ export interface IBranchListItem extends IFilterListItem {
1313
readonly worktreeInUse: WorktreeEntry | null
1414
}
1515

16+
/**
17+
* Whether a branch is local-only, i.e. a local branch that has either never
18+
* been published to a remote or whose upstream has since been deleted.
19+
*/
20+
export function isLocalOnlyBranch(branch: Branch): boolean {
21+
return branch.type === BranchType.Local && (!branch.upstream || branch.isGone)
22+
}
23+
1624
/**
1725
* Finds the worktree where a given branch is currently checked out.
1826
* Returns null if the branch is not checked out in any worktree.
1927
*/
20-
function findWorktreeForBranch(
28+
export function findWorktreeForBranch(
2129
branchName: string,
2230
worktrees: ReadonlyArray<WorktreeEntry>
2331
): WorktreeEntry | null {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as React from 'react'
2+
3+
import { Dispatcher } from '../dispatcher'
4+
import { Repository } from '../../models/repository'
5+
import { Branch } from '../../models/branch'
6+
import { Dialog, DialogContent, DialogFooter } from '../dialog'
7+
import { Ref } from '../lib/ref'
8+
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
9+
10+
interface IDeleteAllLocalBranchesProps {
11+
readonly dispatcher: Dispatcher
12+
readonly repository: Repository
13+
readonly branches: ReadonlyArray<Branch>
14+
readonly onDismissed: () => void
15+
readonly onDeleted: (repository: Repository) => void
16+
}
17+
18+
interface IDeleteAllLocalBranchesState {
19+
readonly isDeleting: boolean
20+
}
21+
22+
export class DeleteAllLocalBranches extends React.Component<
23+
IDeleteAllLocalBranchesProps,
24+
IDeleteAllLocalBranchesState
25+
> {
26+
public constructor(props: IDeleteAllLocalBranchesProps) {
27+
super(props)
28+
29+
this.state = {
30+
isDeleting: false,
31+
}
32+
}
33+
34+
public render() {
35+
const count = this.props.branches.length
36+
37+
return (
38+
<Dialog
39+
id="delete-all-local-branches"
40+
title={
41+
__DARWIN__ ? 'Delete All Local Branches' : 'Delete all local branches'
42+
}
43+
type="warning"
44+
onSubmit={this.deleteBranches}
45+
onDismissed={this.props.onDismissed}
46+
disabled={this.state.isDeleting}
47+
loading={this.state.isDeleting}
48+
role="alertdialog"
49+
ariaDescribedBy="delete-all-local-branches-message"
50+
>
51+
<DialogContent>
52+
<div id="delete-all-local-branches-message">
53+
<p>
54+
Delete the following {count}{' '}
55+
{count === 1 ? 'local branch' : 'local branches'}?
56+
</p>
57+
<ul className="delete-all-local-branches-list">
58+
{this.props.branches.map(branch => (
59+
<li key={branch.name}>
60+
<Ref>{branch.name}</Ref>
61+
</li>
62+
))}
63+
</ul>
64+
<p>This action cannot be undone.</p>
65+
</div>
66+
</DialogContent>
67+
<DialogFooter>
68+
<OkCancelButtonGroup destructive={true} okButtonText="Delete" />
69+
</DialogFooter>
70+
</Dialog>
71+
)
72+
}
73+
74+
private deleteBranches = async () => {
75+
const { dispatcher, repository, branches } = this.props
76+
77+
this.setState({ isDeleting: true })
78+
79+
await dispatcher.deleteLocalBranches(repository, branches)
80+
this.props.onDeleted(repository)
81+
82+
this.props.onDismissed()
83+
}
84+
}

app/src/ui/delete-branch/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { DeleteBranch } from './delete-branch-dialog'
2+
export { DeleteAllLocalBranches } from './delete-all-local-branches-dialog'
23
export { DeleteRemoteBranch } from './delete-remote-branch-dialog'
34
export { CantDeleteCurrentBranch } from './cant-delete-current-branch-dialog'
45
export { CantDeleteCurrentBranchUncommittedChanges } from './cant-delete-current-branch-uncommitted-changes-dialog'

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,17 @@ export class Dispatcher {
10151015
return this.appStore._deleteBranch(repository, branch, includeUpstream)
10161016
}
10171017

1018+
/**
1019+
* Delete several local branches in one operation. None of the branches may be
1020+
* currently checked out (in this or any other worktree).
1021+
*/
1022+
public deleteLocalBranches(
1023+
repository: Repository,
1024+
branches: ReadonlyArray<Branch>
1025+
): Promise<void> {
1026+
return this.appStore._deleteLocalBranches(repository, branches)
1027+
}
1028+
10181029
/**
10191030
* Delete the remote branch.
10201031
*/

0 commit comments

Comments
 (0)