Skip to content

Commit 6f7328c

Browse files
authored
Git commit amend (#973)
* Add commit amend tokens to file list * Add amend flag to commit model * Add commit amend option to right context menu * Add git amend handler and command Note that this is using amend without editing the commit message. * Add push force option * Formatting * Update model for amend commits and force push * Add amend checkbox * Fix tests * Changes from code review * Remove extra command, replace with argument * Use force-with-lease instead of force * Minor fixes * Add test for commitbox
1 parent a915184 commit 6f7328c

File tree

12 files changed

+198
-41
lines changed

12 files changed

+198
-41
lines changed

jupyterlab_git/git.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -916,11 +916,18 @@ async def checkout_all(self, path):
916916
return {"code": code, "command": " ".join(cmd), "message": error}
917917
return {"code": code}
918918

919-
async def commit(self, commit_msg, path):
919+
async def commit(self, commit_msg, amend, path):
920920
"""
921921
Execute git commit <filename> command & return the result.
922+
923+
If the amend argument is true, amend the commit instead of creating a new one.
922924
"""
923-
cmd = ["git", "commit", "-m", commit_msg]
925+
cmd = ["git", "commit"]
926+
if amend:
927+
cmd.extend(["--amend", "--no-edit"])
928+
else:
929+
cmd.extend(["--m", commit_msg])
930+
924931
code, _, error = await execute(cmd, cwd=path)
925932

926933
if code != 0:
@@ -976,11 +983,15 @@ async def pull(self, path, auth=None, cancel_on_conflict=False):
976983

977984
return response
978985

979-
async def push(self, remote, branch, path, auth=None, set_upstream=False):
986+
async def push(
987+
self, remote, branch, path, auth=None, set_upstream=False, force=False
988+
):
980989
"""
981990
Execute `git push $UPSTREAM $BRANCH`. The choice of upstream and branch is up to the caller.
982991
"""
983992
command = ["git", "push"]
993+
if force:
994+
command.append("--force-with-lease")
984995
if set_upstream:
985996
command.append("--set-upstream")
986997
command.extend([remote, branch])

jupyterlab_git/handlers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ async def post(self, path: str = ""):
463463

464464
class GitCommitHandler(GitHandler):
465465
"""
466-
Handler for 'git commit -m <message>'. Commits files.
466+
Handler for 'git commit -m <message>' and 'git commit --amend'. Commits files.
467467
"""
468468

469469
@tornado.web.authenticated
@@ -473,7 +473,8 @@ async def post(self, path: str = ""):
473473
"""
474474
data = self.get_json_body()
475475
commit_msg = data["commit_msg"]
476-
body = await self.git.commit(commit_msg, self.url2localpath(path))
476+
amend = data.get("amend", False)
477+
body = await self.git.commit(commit_msg, amend, self.url2localpath(path))
477478

478479
if body["code"] != 0:
479480
self.set_status(500)
@@ -533,11 +534,13 @@ async def post(self, path: str = ""):
533534
Request body:
534535
{
535536
remote?: string # Remote to push to; i.e. <remote_name> or <remote_name>/<branch>
537+
force: boolean # Whether or not to force the push
536538
}
537539
"""
538540
local_path = self.url2localpath(path)
539541
data = self.get_json_body()
540542
known_remote = data.get("remote")
543+
force = data.get("force", False)
541544

542545
current_local_branch = await self.git.get_current_branch(local_path)
543546

@@ -565,6 +568,7 @@ async def post(self, path: str = ""):
565568
local_path,
566569
data.get("auth", None),
567570
set_upstream,
571+
force,
568572
)
569573

570574
else:
@@ -590,6 +594,7 @@ async def post(self, path: str = ""):
590594
local_path,
591595
data.get("auth", None),
592596
set_upstream=True,
597+
force=force,
593598
)
594599
else:
595600
response = {

jupyterlab_git/tests/test_handlers.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ async def test_push_handler_localbranch(mock_git, jp_fetch, jp_root_dir):
327327
mock_git.get_current_branch.assert_called_with(str(local_path))
328328
mock_git.get_upstream_branch.assert_called_with(str(local_path), "localbranch")
329329
mock_git.push.assert_called_with(
330-
".", "HEAD:localbranch", str(local_path), None, False
330+
".", "HEAD:localbranch", str(local_path), None, False, False
331331
)
332332

333333
assert response.code == 200
@@ -357,7 +357,12 @@ async def test_push_handler_remotebranch(mock_git, jp_fetch, jp_root_dir):
357357
mock_git.get_current_branch.assert_called_with(str(local_path))
358358
mock_git.get_upstream_branch.assert_called_with(str(local_path), "foo/bar")
359359
mock_git.push.assert_called_with(
360-
"origin/something", "HEAD:remote-branch-name", str(local_path), None, False
360+
"origin/something",
361+
"HEAD:remote-branch-name",
362+
str(local_path),
363+
None,
364+
False,
365+
False,
361366
)
362367

363368
assert response.code == 200
@@ -458,7 +463,7 @@ async def test_push_handler_noupstream_unique_remote(mock_git, jp_fetch, jp_root
458463
mock_git.config.assert_called_with(str(local_path))
459464
mock_git.remote_show.assert_called_with(str(local_path))
460465
mock_git.push.assert_called_with(
461-
remote, "foo", str(local_path), None, set_upstream=True
466+
remote, "foo", str(local_path), None, set_upstream=True, force=False
462467
)
463468

464469
assert response.code == 200
@@ -491,7 +496,7 @@ async def test_push_handler_noupstream_pushdefault(mock_git, jp_fetch, jp_root_d
491496
mock_git.config.assert_called_with(str(local_path))
492497
mock_git.remote_show.assert_called_with(str(local_path))
493498
mock_git.push.assert_called_with(
494-
remote, "foo", str(local_path), None, set_upstream=True
499+
remote, "foo", str(local_path), None, set_upstream=True, force=False
495500
)
496501

497502
assert response.code == 200
@@ -525,7 +530,9 @@ async def test_push_handler_noupstream_pass_remote_nobranch(
525530
mock_git.get_upstream_branch.assert_called_with(str(local_path), "foo")
526531
mock_git.config.assert_not_called()
527532
mock_git.remote_show.assert_not_called()
528-
mock_git.push.assert_called_with(remote, "HEAD:foo", str(local_path), None, True)
533+
mock_git.push.assert_called_with(
534+
remote, "HEAD:foo", str(local_path), None, True, False
535+
)
529536

530537
assert response.code == 200
531538
payload = json.loads(response.body)
@@ -560,7 +567,7 @@ async def test_push_handler_noupstream_pass_remote_branch(
560567
mock_git.config.assert_not_called()
561568
mock_git.remote_show.assert_not_called()
562569
mock_git.push.assert_called_with(
563-
remote, "HEAD:" + remote_branch, str(local_path), None, True
570+
remote, "HEAD:" + remote_branch, str(local_path), None, True, False
564571
)
565572

566573
assert response.code == 200

src/commandsAndMenu.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ interface IGitCloneArgs {
6363
enum Operation {
6464
Clone = 'Clone',
6565
Pull = 'Pull',
66-
Push = 'Push'
66+
Push = 'Push',
67+
ForcePush = 'ForcePush'
6768
}
6869

6970
interface IFileDiffArgument {
@@ -344,18 +345,21 @@ export function addCommands(
344345

345346
/** Add git push command */
346347
commands.addCommand(CommandIDs.gitPush, {
347-
label: trans.__('Push to Remote'),
348+
label: args =>
349+
args.force
350+
? trans.__('Push to Remote (Force)')
351+
: trans.__('Push to Remote'),
348352
caption: trans.__('Push code to remote repository'),
349353
isEnabled: () => gitModel.pathRepository !== null,
350-
execute: async () => {
354+
execute: async args => {
351355
logger.log({
352356
level: Level.RUNNING,
353357
message: trans.__('Pushing...')
354358
});
355359
try {
356360
const details = await Private.showGitOperationDialog(
357361
gitModel,
358-
Operation.Push,
362+
args.force ? Operation.ForcePush : Operation.Push,
359363
trans
360364
);
361365
logger.log({
@@ -932,6 +936,9 @@ export function createGitMenu(
932936
CommandIDs.gitAddRemote,
933937
CommandIDs.gitTerminalCommand
934938
].forEach(command => {
939+
if (command === CommandIDs.gitPush) {
940+
menu.addItem({ command, args: { force: true } });
941+
}
935942
menu.addItem({ command });
936943
});
937944

@@ -1163,6 +1170,9 @@ namespace Private {
11631170
case Operation.Push:
11641171
result = await model.push(authentication);
11651172
break;
1173+
case Operation.ForcePush:
1174+
result = await model.push(authentication, true);
1175+
break;
11661176
default:
11671177
result = { code: -1, message: 'Unknown git command' };
11681178
break;

src/components/CommitBox.tsx

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
commitButtonClass,
77
commitDescriptionClass,
88
commitFormClass,
9-
commitSummaryClass
9+
commitSummaryClass,
10+
commitInputWrapperClass
1011
} from '../style/CommitBox';
1112
import { CommandIDs } from '../tokens';
1213

@@ -44,6 +45,11 @@ export interface ICommitBoxProps {
4445
*/
4546
description: string;
4647

48+
/**
49+
* Whether the "amend" checkbox is checked
50+
*/
51+
amend: boolean;
52+
4753
/**
4854
* Updates the commit message summary.
4955
*
@@ -58,6 +64,13 @@ export interface ICommitBoxProps {
5864
*/
5965
setDescription: (description: string) => void;
6066

67+
/**
68+
* Updates the amend checkbox state
69+
*
70+
* @param amend - amend toggle on/off
71+
*/
72+
setAmend: (amend: boolean) => void;
73+
6174
/**
6275
* Callback to invoke in order to commit changes.
6376
*
@@ -114,29 +127,52 @@ export class CommitBox extends React.Component<ICommitBoxProps> {
114127
className={commitSummaryClass}
115128
type="text"
116129
placeholder={summaryPlaceholder}
117-
title={this.props.trans.__(
118-
'Enter a commit message summary (a single line, preferably less than 50 characters)'
119-
)}
130+
title={
131+
this.props.amend
132+
? this.props.trans.__(
133+
'Amending the commit will re-use the previous commit summary'
134+
)
135+
: this.props.trans.__(
136+
'Enter a commit message summary (a single line, preferably less than 50 characters)'
137+
)
138+
}
120139
value={this.props.summary}
121140
onChange={this._onSummaryChange}
122141
onKeyPress={this._onSummaryKeyPress}
142+
disabled={this.props.amend}
123143
/>
124144
<TextareaAutosize
125145
className={commitDescriptionClass}
126146
minRows={5}
127147
placeholder={this.props.trans.__('Description (optional)')}
128-
title={this.props.trans.__('Enter a commit message description')}
148+
title={
149+
this.props.amend
150+
? this.props.trans.__(
151+
'Amending the commit will re-use the previous commit summary'
152+
)
153+
: this.props.trans.__('Enter a commit message description')
154+
}
129155
value={this.props.description}
130156
onChange={this._onDescriptionChange}
157+
disabled={this.props.amend}
131158
/>
132-
<input
133-
className={commitButtonClass}
134-
type="button"
135-
title={title}
136-
value={this.props.label}
137-
disabled={disabled}
138-
onClick={this.props.onCommit}
139-
/>
159+
<div className={commitInputWrapperClass}>
160+
<input
161+
className={commitButtonClass}
162+
type="button"
163+
title={title}
164+
value={this.props.label}
165+
disabled={disabled}
166+
onClick={this.props.onCommit}
167+
/>
168+
<input
169+
type="checkbox"
170+
id="commit-amend"
171+
onChange={this._onAmendChange}
172+
checked={this.props.amend}
173+
/>
174+
<label htmlFor="commit-amend">Amend</label>
175+
</div>
140176
</form>
141177
);
142178
}
@@ -145,6 +181,9 @@ export class CommitBox extends React.Component<ICommitBoxProps> {
145181
* Whether a commit can be performed (files are staged and summary is not empty).
146182
*/
147183
private _canCommit(): boolean {
184+
if (this.props.amend) {
185+
return this.props.hasFiles;
186+
}
148187
return !!(this.props.hasFiles && this.props.summary);
149188
}
150189

@@ -176,6 +215,15 @@ export class CommitBox extends React.Component<ICommitBoxProps> {
176215
this.props.setSummary(event.target.value);
177216
};
178217

218+
/**
219+
* Callback invoked when the amend checkbox is toggled
220+
*
221+
* @param event - event object
222+
*/
223+
private _onAmendChange = (event: any): void => {
224+
this.props.setAmend(event.target.checked);
225+
};
226+
179227
/**
180228
* Callback invoked upon a `'keypress'` event when entering a commit message summary.
181229
*

src/components/FileList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export const CONTEXT_COMMANDS: ContextCommands = {
7373
staged: [
7474
ContextCommandIDs.gitFileOpen,
7575
ContextCommandIDs.gitFileUnstage,
76-
ContextCommandIDs.gitFileDiff
76+
ContextCommandIDs.gitFileDiff,
77+
ContextCommandIDs.gitCommitAmendStaged
7778
]
7879
};
7980

0 commit comments

Comments
 (0)