Skip to content

Commit dea5488

Browse files
committed
Fixes #708 - Prompt for file path when missing from revision
Improves the `openFileAtRevision()` function by: - Asking user to enter another path when the requested file doesn't exist in the selected revision. - Showing an error message on subsequent failure (or if an error is thrown). The above function is used by a number of commands: - `gitlens.openFileRevision` - `gitlens.openFileRevisionFrom` - `gitlens.openRevisionFile`
1 parent 7981bd5 commit dea5488

File tree

3 files changed

+89
-7
lines changed

3 files changed

+89
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Adds support for opening renamed/deleted files using the _Open File at Revision..._ & _Open File at Revision from..._ commands by showing a quick pick menu if the requested file doesn't exist in the selected revision — closes [#708](https://github.com/gitkraken/vscode-gitlens/issues/708) thanks to [PR #2825](https://github.com/gitkraken/vscode-gitlens/pull/2825) by Victor Hallberg ([@mogelbrod](https://github.com/mogelbrod))
12+
913
### Changed
1014

1115
- Changes blame to show the last modified time of the file for uncommitted changes

src/git/actions/commit.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TextDocumentShowOptions } from 'vscode';
1+
import type { TextDocumentShowOptions, TextEditor } from 'vscode';
22
import { env, Range, Uri, window, workspace } from 'vscode';
33
import type { DiffWithCommandArgs } from '../../commands/diffWith';
44
import type { DiffWithPreviousCommandArgs } from '../../commands/diffWithPrevious';
@@ -12,6 +12,7 @@ import type { FileAnnotationType } from '../../config';
1212
import { Commands, GlyphChars } from '../../constants';
1313
import { Container } from '../../container';
1414
import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/protocol';
15+
import { showRevisionPicker } from '../../quickpicks/revisionPicker';
1516
import { executeCommand, executeEditorCommand } from '../../system/command';
1617
import { configuration } from '../../system/configuration';
1718
import { findOrOpenEditor, findOrOpenEditors, openChangesEditor } from '../../system/utils';
@@ -463,7 +464,7 @@ export async function openFileAtRevision(
463464
commitOrOptions?: GitCommit | TextDocumentShowOptions,
464465
options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
465466
): Promise<void> {
466-
let uri;
467+
let uri: Uri;
467468
if (fileOrRevisionUri instanceof Uri) {
468469
if (isCommit(commitOrOptions)) throw new Error('Invalid arguments');
469470

@@ -501,11 +502,30 @@ export async function openFileAtRevision(
501502
opts.selection = new Range(line, 0, line, 0);
502503
}
503504

504-
const editor = await findOrOpenEditor(uri, opts);
505-
if (annotationType != null && editor != null) {
506-
void (await Container.instance.fileAnnotations.show(editor, annotationType, {
507-
selection: { line: line },
508-
}));
505+
const gitUri = await GitUri.fromUri(uri);
506+
507+
let editor: TextEditor | undefined;
508+
try {
509+
editor = await findOrOpenEditor(uri, { throwOnError: true, ...opts }).catch(error => {
510+
if (error?.message?.includes('Unable to resolve nonexistent file')) {
511+
return showRevisionPicker(gitUri, {
512+
title: 'File not found in revision - pick another file to open instead',
513+
}).then(pickedUri => {
514+
return pickedUri ? findOrOpenEditor(pickedUri, opts) : undefined;
515+
});
516+
}
517+
throw error;
518+
});
519+
520+
if (annotationType != null && editor != null) {
521+
void (await Container.instance.fileAnnotations.show(editor, annotationType, {
522+
selection: { line: line },
523+
}));
524+
}
525+
} catch (error) {
526+
await window.showErrorMessage(
527+
`Unable to open '${gitUri.relativePath}' - file doesn't exist in selected revision`,
528+
);
509529
}
510530
}
511531

src/quickpicks/revisionPicker.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// import path from "path";
2+
import type { Disposable, Uri } from "vscode";
3+
import { window } from "vscode";
4+
import { Container } from "../container";
5+
import type { GitUri } from "../git/gitUri";
6+
import { filterMap } from "../system/iterable";
7+
import { getQuickPickIgnoreFocusOut } from "../system/utils";
8+
9+
export async function showRevisionPicker(
10+
uri: GitUri,
11+
options: {
12+
title: string;
13+
initialPath?: string;
14+
},
15+
): Promise<Uri | undefined> {
16+
const disposables: Disposable[] = [];
17+
try {
18+
const picker = window.createQuickPick();
19+
picker.title = options.title;
20+
picker.value = options.initialPath ?? uri.relativePath;
21+
picker.placeholder = 'Enter path to file...';
22+
picker.matchOnDescription = true;
23+
picker.busy = true;
24+
picker.ignoreFocusOut = getQuickPickIgnoreFocusOut();
25+
26+
picker.show();
27+
28+
const tree = await Container.instance.git.getTreeForRevision(uri.repoPath, uri.sha!);
29+
picker.items = Array.from(filterMap(tree, file => {
30+
// Exclude directories
31+
if (file.type !== 'blob') { return null }
32+
return { label: file.path }
33+
// FIXME: Remove this unless we opt to show the directory in the description
34+
// const parsed = path.parse(file.path)
35+
// return { label: parsed.base, description: parsed.dir }
36+
}))
37+
picker.busy = false;
38+
39+
const pick = await new Promise<string | undefined>(resolve => {
40+
disposables.push(
41+
picker,
42+
picker.onDidHide(() => resolve(undefined)),
43+
picker.onDidAccept(() => {
44+
if (picker.activeItems.length === 0) return;
45+
resolve(picker.activeItems[0].label);
46+
}),
47+
);
48+
});
49+
50+
return pick
51+
? Container.instance.git.getRevisionUri(uri.sha!, `${uri.repoPath}/${pick}`, uri.repoPath!)
52+
: undefined;
53+
} finally {
54+
disposables.forEach(d => {
55+
d.dispose();
56+
});
57+
}
58+
}

0 commit comments

Comments
 (0)