Skip to content

Commit a50bafd

Browse files
committed
#516 New "Reset File to this Revision..." action on the File Context Menu in the Commit Details View.
1 parent b4f9f77 commit a50bafd

7 files changed

Lines changed: 112 additions & 3 deletions

File tree

src/dataSource.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,20 @@ export class DataSource extends Disposable {
11661166
}
11671167

11681168

1169+
/* Git Action Methods - File */
1170+
1171+
/**
1172+
* Reset a file to the specified revision.
1173+
* @param repo The path of the repository.
1174+
* @param commitHash The commit to reset the file to.
1175+
* @param filePath The file to reset.
1176+
* @returns The ErrorInfo from the executed command.
1177+
*/
1178+
public resetFileToRevision(repo: string, commitHash: string, filePath: string) {
1179+
return this.runGitCommand(['checkout', commitHash, '--', filePath], repo);
1180+
}
1181+
1182+
11691183
/* Git Action Methods - Stash */
11701184

11711185
/**

src/gitGraphView.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,12 @@ export class GitGraphView extends Disposable {
542542
showErrorMessage('No Git repositories were found in the current workspace.');
543543
}
544544
break;
545+
case 'resetFileToRevision':
546+
this.sendMessage({
547+
command: 'resetFileToRevision',
548+
error: await this.dataSource.resetFileToRevision(msg.repo, msg.commitHash, msg.filePath)
549+
});
550+
break;
545551
case 'resetToCommit':
546552
this.sendMessage({
547553
command: 'resetToCommit',

src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,15 @@ export interface RequestRescanForRepos extends BaseMessage {
11081108
readonly command: 'rescanForRepos';
11091109
}
11101110

1111+
export interface RequestResetFileToRevision extends RepoRequest {
1112+
readonly command: 'resetFileToRevision';
1113+
readonly commitHash: string;
1114+
readonly filePath: string;
1115+
}
1116+
export interface ResponseResetFileToRevision extends ResponseWithErrorInfo {
1117+
readonly command: 'resetFileToRevision';
1118+
}
1119+
11111120
export interface RequestResetToCommit extends RepoRequest {
11121121
readonly command: 'resetToCommit';
11131122
readonly commit: string;
@@ -1276,6 +1285,7 @@ export type RequestMessage =
12761285
| RequestRebase
12771286
| RequestRenameBranch
12781287
| RequestRescanForRepos
1288+
| RequestResetFileToRevision
12791289
| RequestResetToCommit
12801290
| RequestRevertCommit
12811291
| RequestSetGlobalViewState
@@ -1338,6 +1348,7 @@ export type ResponseMessage =
13381348
| ResponseRebase
13391349
| ResponseRefresh
13401350
| ResponseRenameBranch
1351+
| ResponseResetFileToRevision
13411352
| ResponseResetToCommit
13421353
| ResponseRevertCommit
13431354
| ResponseSetGlobalViewState

tests/dataSource.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6405,6 +6405,31 @@ describe('DataSource', () => {
64056405
});
64066406
});
64076407

6408+
describe('resetFileToRevision', () => {
6409+
it('Should reset file to revision', async () => {
6410+
// Setup
6411+
mockGitSuccessOnce();
6412+
6413+
// Run
6414+
const result = await dataSource.resetFileToRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'path/to/file');
6415+
6416+
// Assert
6417+
expect(result).toBe(null);
6418+
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['checkout', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--', 'path/to/file'], expect.objectContaining({ cwd: '/path/to/repo' }));
6419+
});
6420+
6421+
it('Should return an error message thrown by git', async () => {
6422+
// Setup
6423+
mockGitThrowingErrorOnce();
6424+
6425+
// Run
6426+
const result = await dataSource.resetFileToRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'path/to/file');
6427+
6428+
// Assert
6429+
expect(result).toBe('error message');
6430+
});
6431+
});
6432+
64086433
describe('applyStash', () => {
64096434
it('Should apply a stash', async () => {
64106435
// Setup

tests/gitGraphView.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3117,6 +3117,34 @@ describe('GitGraphView', () => {
31173117
});
31183118
});
31193119

3120+
describe('resetFileToRevision', () => {
3121+
it('Should reset the file to a revision', async () => {
3122+
// Setup
3123+
const resetFileToRevisionResolvedValue = null;
3124+
const spyOnResetFileToRevision = jest.spyOn(dataSource, 'resetFileToRevision');
3125+
spyOnResetFileToRevision.mockResolvedValueOnce(resetFileToRevisionResolvedValue);
3126+
3127+
// Run
3128+
onDidReceiveMessage({
3129+
command: 'resetFileToRevision',
3130+
repo: '/path/to/repo',
3131+
commitHash: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b',
3132+
filePath: 'path/to/file'
3133+
});
3134+
3135+
// Assert
3136+
await waitForExpect(() => {
3137+
expect(spyOnResetFileToRevision).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'path/to/file');
3138+
expect(messages).toStrictEqual([
3139+
{
3140+
command: 'resetFileToRevision',
3141+
error: resetFileToRevisionResolvedValue
3142+
}
3143+
]);
3144+
});
3145+
});
3146+
});
3147+
31203148
describe('resetToCommit', () => {
31213149
it('Should reset the current branch to a commit', async () => {
31223150
// Setup

web/main.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,20 @@ class GitGraphView {
29272927
sendMessage({ command: 'copyFilePath', repo: this.currentRepo, filePath: file.newFilePath, absolute: absolute });
29282928
};
29292929

2930+
const triggerResetFileToRevision = (file: GG.GitFileChange, fileElem: HTMLElement) => {
2931+
const expandedCommit = this.expandedCommit;
2932+
if (expandedCommit === null) return;
2933+
2934+
const commitHash = getCommitHashForFile(file, expandedCommit);
2935+
dialog.showConfirmation('Are you sure you want to reset <b><i>' + escapeHtml(file.newFilePath) + '</i></b> to it\'s state at commit <b><i>' + abbrevCommit(commitHash) + '</i></b>? Any uncommitted changes made to this file will be overwritten.', 'Yes, reset file', () => {
2936+
runAction({ command: 'resetFileToRevision', repo: this.currentRepo, commitHash: commitHash, filePath: file.newFilePath }, 'Resetting file');
2937+
}, {
2938+
type: TargetType.CommitDetailsView,
2939+
hash: commitHash,
2940+
elem: fileElem
2941+
});
2942+
};
2943+
29302944
const triggerViewFileAtRevision = (file: GG.GitFileChange, fileElem: HTMLElement) => {
29312945
const expandedCommit = this.expandedCommit;
29322946
if (expandedCommit === null) return;
@@ -3024,7 +3038,8 @@ class GitGraphView {
30243038
elem: fileElem
30253039
};
30263040
const diffPossible = file.type === GG.GitFileStatus.Untracked || (file.additions !== null && file.deletions !== null);
3027-
const fileExistsAtThisRevisionAndDiffPossible = file.type !== GG.GitFileStatus.Deleted && diffPossible && !isUncommitted;
3041+
const fileExistsAtThisRevision = file.type !== GG.GitFileStatus.Deleted && !isUncommitted;
3042+
const fileExistsAtThisRevisionAndDiffPossible = fileExistsAtThisRevision && diffPossible;
30283043
const codeReviewInProgressAndNotReviewed = expandedCommit.codeReview !== null && expandedCommit.codeReview.remainingFiles.includes(file.newFilePath);
30293044

30303045
contextMenu.show([
@@ -3062,6 +3077,13 @@ class GitGraphView {
30623077
onClick: () => this.cdvUpdateFileState(file, fileElem, false, false)
30633078
}
30643079
],
3080+
[
3081+
{
3082+
title: 'Reset File to this Revision' + ELLIPSIS,
3083+
visible: fileExistsAtThisRevision && expandedCommit.compareWithHash === null,
3084+
onClick: () => triggerResetFileToRevision(file, fileElem)
3085+
}
3086+
],
30653087
[
30663088
{
30673089
title: 'Copy Absolute File Path to Clipboard',
@@ -3343,6 +3365,9 @@ window.addEventListener('load', () => {
33433365
case 'renameBranch':
33443366
refreshOrDisplayError(msg.error, 'Unable to Rename Branch');
33453367
break;
3368+
case 'resetFileToRevision':
3369+
refreshOrDisplayError(msg.error, 'Unable to Reset File to Revision');
3370+
break;
33463371
case 'resetToCommit':
33473372
refreshOrDisplayError(msg.error, 'Unable to Reset to Commit');
33483373
break;

web/styles/main.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ code{
566566
.fileTreeFileAction.viewGitFileAtRevision, .fileTreeFileAction.openGitFile{
567567
margin-left:6px;
568568
}
569-
.fileTreeFileRecord:hover .fileTreeFileAction, .fileTreeFileRecord.contextMenuActive .fileTreeFileAction{
569+
.fileTreeFileRecord:hover .fileTreeFileAction, .fileTreeFileRecord.contextMenuActive .fileTreeFileAction, .fileTreeFileRecord.dialogActive .fileTreeFileAction{
570570
cursor:pointer;
571571
opacity:1;
572572
}
@@ -583,7 +583,7 @@ svg.openFolderIcon, svg.closedFolderIcon, svg.fileIcon, .fileTreeFileAction svg,
583583
.fileTreeFileAction.viewGitFileAtRevision svg, .fileTreeFileAction.openGitFile svg{
584584
top:4px;
585585
}
586-
.fileTreeFolder:hover svg.openFolderIcon, .fileTreeFolder:hover svg.closedFolderIcon, .fileTreeFileRecord:hover svg.fileIcon, .fileTreeFileRecord.contextMenuActive svg.fileIcon, .fileTreeFileRecord:hover #cdvLastFileViewed svg, .fileTreeFileRecord.contextMenuActive #cdvLastFileViewed svg, .fileTreeRepo:hover svg, .fileTreeFileAction:hover svg{
586+
.fileTreeFolder:hover svg.openFolderIcon, .fileTreeFolder:hover svg.closedFolderIcon, .fileTreeFileRecord:hover svg.fileIcon, .fileTreeFileRecord.contextMenuActive svg.fileIcon, .fileTreeFileRecord.dialogActive svg.fileIcon, .fileTreeFileRecord:hover #cdvLastFileViewed svg, .fileTreeFileRecord.contextMenuActive #cdvLastFileViewed svg, .fileTreeFileRecord.dialogActive #cdvLastFileViewed svg, .fileTreeRepo:hover svg, .fileTreeFileAction:hover svg{
587587
fill-opacity:0.8;
588588
}
589589
svg.openFolderIcon, svg.closedFolderIcon, svg.fileIcon{

0 commit comments

Comments
 (0)