Skip to content

Commit b5dc2d5

Browse files
authored
Merge pull request #816 from fcollonval/fcollonval/issue308
Delete local branch
2 parents a08c424 + cabc052 commit b5dc2d5

File tree

11 files changed

+306
-169
lines changed

11 files changed

+306
-169
lines changed

jupyterlab_git/git.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,17 @@ async def branch(self, current_path):
497497
"current_branch": heads["current_branch"],
498498
}
499499

500+
async def branch_delete(self, current_path, branch):
501+
"""Execute 'git branch -D <branchname>'"""
502+
cmd = ["git", "branch", "-D", branch]
503+
code, _, error = await execute(
504+
cmd, cwd=os.path.join(self.root_dir, current_path)
505+
)
506+
if code != 0:
507+
return {"code": code, "command": " ".join(cmd), "message": error}
508+
else:
509+
return {"code": code}
510+
500511
async def branch_heads(self, current_path):
501512
"""
502513
Execute 'git for-each-ref' command on refs/heads & return the result.

jupyterlab_git/handlers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,31 @@ async def post(self):
234234
self.finish(json.dumps(result))
235235

236236

237+
class GitBranchDeleteHandler(GitHandler):
238+
"""
239+
Handler for 'git branch -D <branch>'
240+
"""
241+
242+
@web.authenticated
243+
async def post(self):
244+
"""
245+
POST request handler, delete branch in current repository.
246+
247+
Body: {
248+
"current_path": Git repository path relatively to the server root,
249+
"branch": Branch name to be deleted
250+
}
251+
"""
252+
data = self.get_json_body()
253+
result = await self.git.branch_delete(data["current_path"], data["branch"])
254+
255+
if result["code"] != 0:
256+
self.set_status(500)
257+
self.finish(json.dumps(result))
258+
else:
259+
self.set_status(204)
260+
261+
237262
class GitAddHandler(GitHandler):
238263
"""
239264
Handler for git add <filename>'.
@@ -747,6 +772,7 @@ def setup_handlers(web_app):
747772
("/git/add_all_untracked", GitAddAllUntrackedHandler),
748773
("/git/all_history", GitAllHistoryHandler),
749774
("/git/branch", GitBranchHandler),
775+
("/git/branch/delete", GitBranchDeleteHandler),
750776
("/git/changed_files", GitChangedFilesHandler),
751777
("/git/checkout", GitCheckoutHandler),
752778
("/git/clone", GitCloneHandler),

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"clean:slate": "jlpm clean:more && jlpm clean:labextension && rimraf node_modules",
2222
"contributors:generate": "jlpm run all-contributors generate",
2323
"lint": "eslint . --ext .ts,.tsx --fix",
24-
"test": "jest --no-cache",
24+
"test": "jest",
2525
"eslint-check": "eslint . --ext .ts,.tsx",
2626
"prepare": "jlpm run build",
2727
"watch": "tsc -w"

src/components/BranchMenu.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import * as React from 'react';
66
import { FixedSizeList, ListChildComponentProps } from 'react-window';
77
import { classes } from 'typestyle';
88
import { Logger } from '../logger';
9+
import { hiddenButtonStyle } from '../style/ActionButtonStyle';
910
import {
1011
activeListItemClass,
12+
nameClass,
1113
filterClass,
1214
filterClearClass,
1315
filterInputClass,
@@ -17,8 +19,9 @@ import {
1719
newBranchButtonClass,
1820
wrapperClass
1921
} from '../style/BranchMenu';
20-
import { branchIcon } from '../style/icons';
22+
import { branchIcon, trashIcon } from '../style/icons';
2123
import { Git, IGitExtension, Level } from '../tokens';
24+
import { ActionButton } from './ActionButton';
2225
import { NewBranchDialog } from './NewBranchDialog';
2326

2427
const CHANGES_ERR_MSG =
@@ -254,7 +257,18 @@ export class BranchMenu extends React.Component<
254257
style={style}
255258
>
256259
<branchIcon.react className={listItemIconClass} tag="span" />
257-
{branch.name}
260+
<span className={nameClass}>{branch.name}</span>
261+
{!branch.is_remote_branch && !isActive && (
262+
<ActionButton
263+
className={hiddenButtonStyle}
264+
icon={trashIcon}
265+
title={'Delete this branch locally'}
266+
onClick={(event: React.MouseEvent) => {
267+
event.stopPropagation();
268+
this._onDeleteBranch(branch.name);
269+
}}
270+
/>
271+
)}
258272
</ListItem>
259273
);
260274
};
@@ -297,6 +311,34 @@ export class BranchMenu extends React.Component<
297311
});
298312
};
299313

314+
/**
315+
* Callback on delete branch name button
316+
*
317+
* @param branchName Branch name
318+
*/
319+
private _onDeleteBranch = async (branchName: string): Promise<void> => {
320+
const acknowledgement = await showDialog<void>({
321+
title: 'Delete branch',
322+
body: (
323+
<p>
324+
Are you sure you want to permanently delete the branch{' '}
325+
<b>{branchName}</b>?
326+
<br />
327+
This action cannot be undone.
328+
</p>
329+
),
330+
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Delete' })]
331+
});
332+
if (acknowledgement.button.accept) {
333+
try {
334+
await this.props.model.deleteBranch(branchName);
335+
await this.props.model.refreshBranch();
336+
} catch (error) {
337+
console.error(`Failed to delete branch ${branchName}`, error);
338+
}
339+
}
340+
};
341+
300342
/**
301343
* Callback invoked upon clicking a button to create a new branch.
302344
*

src/components/TagMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as React from 'react';
66
import { FixedSizeList, ListChildComponentProps } from 'react-window';
77
import { Logger } from '../logger';
88
import {
9+
nameClass,
910
filterClass,
1011
filterClearClass,
1112
filterInputClass,
@@ -241,7 +242,7 @@ export class TagMenu extends React.Component<ITagMenuProps, ITagMenuState> {
241242
style={style}
242243
>
243244
<tagIcon.react className={listItemIconClass} tag="span" />
244-
{tag}
245+
<span className={nameClass}>{tag}</span>
245246
</ListItem>
246247
);
247248
};

src/model.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -486,15 +486,23 @@ export class GitExtension implements IGitExtension {
486486
}
487487

488488
/**
489-
* Dispose of model resources.
489+
* Delete a branch
490+
*
491+
* @param branchName Branch name
492+
* @returns promise which resolves when the branch has been deleted.
493+
*
494+
* @throws {Git.NotInRepository} If the current path is not a Git repository
495+
* @throws {Git.GitResponseError} If the server response is not ok
496+
* @throws {ServerConnection.NetworkError} If the request cannot be made
490497
*/
491-
dispose(): void {
492-
if (this.isDisposed) {
493-
return;
494-
}
495-
this._isDisposed = true;
496-
this._poll.dispose();
497-
Signal.clearData(this);
498+
async deleteBranch(branchName: string): Promise<void> {
499+
const path = await this._getPathRespository();
500+
await this._taskHandler.execute<void>('git:branch:delete', async () => {
501+
return await requestAPI<void>('branch/delete', 'POST', {
502+
current_path: path,
503+
branch: branchName
504+
});
505+
});
498506
}
499507

500508
/**
@@ -530,6 +538,18 @@ export class GitExtension implements IGitExtension {
530538
return data;
531539
}
532540

541+
/**
542+
* Dispose of model resources.
543+
*/
544+
dispose(): void {
545+
if (this.isDisposed) {
546+
return;
547+
}
548+
this._isDisposed = true;
549+
this._poll.dispose();
550+
Signal.clearData(this);
551+
}
552+
533553
/**
534554
* Ensure a .gitignore file exists
535555
*

src/style/BranchMenu.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { style } from 'typestyle';
2+
import { showButtonOnHover } from './ActionButtonStyle';
3+
4+
export const nameClass = style({
5+
flex: '1 1 auto',
6+
textOverflow: 'ellipsis',
7+
overflow: 'hidden',
8+
whiteSpace: 'nowrap'
9+
});
210

311
export const wrapperClass = style({
412
marginTop: '6px',
@@ -105,11 +113,12 @@ export const newBranchButtonClass = style({
105113
}
106114
});
107115

108-
export const listItemClass = style({
109-
paddingTop: '4px!important',
110-
paddingBottom: '4px!important',
111-
paddingLeft: '11px!important'
112-
});
116+
export const listItemClass = style(
117+
{
118+
padding: '4px 11px!important'
119+
},
120+
showButtonOnHover
121+
);
113122

114123
export const activeListItemClass = style({
115124
color: 'white!important',

src/style/icons.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import pushSvg from '../../style/icons/push.svg';
1616
import removeSvg from '../../style/icons/remove.svg';
1717
import rewindSvg from '../../style/icons/rewind.svg';
1818
import tagSvg from '../../style/icons/tag.svg';
19+
import trashSvg from '../../style/icons/trash.svg';
1920

2021
export const gitIcon = new LabIcon({ name: 'git', svgstr: gitSvg });
2122
export const addIcon = new LabIcon({
@@ -74,3 +75,7 @@ export const tagIcon = new LabIcon({
7475
name: 'git:tag',
7576
svgstr: tagSvg
7677
});
78+
export const trashIcon = new LabIcon({
79+
name: 'git:trash',
80+
svgstr: trashSvg
81+
});

src/tokens.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,18 @@ export interface IGitExtension extends IDisposable {
206206
*/
207207
config(options?: JSONObject): Promise<JSONObject | void>;
208208

209+
/**
210+
* Delete a branch
211+
*
212+
* @param branchName Branch name
213+
* @returns promise which resolves when the branch has been deleted.
214+
*
215+
* @throws {Git.NotInRepository} If the current path is not a Git repository
216+
* @throws {Git.GitResponseError} If the server response is not ok
217+
* @throws {ServerConnection.NetworkError} If the request cannot be made
218+
*/
219+
deleteBranch(branchName: string): Promise<void>;
220+
209221
/**
210222
* Fetch commit information.
211223
*

style/icons/trash.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)