Skip to content

Commit c1e45f8

Browse files
committed
Adds folder history commands to SCM
1 parent d79c215 commit c1e45f8

File tree

9 files changed

+161
-52
lines changed

9 files changed

+161
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
2020
- Improves hover content and interations
2121
- Adds explicit zoom in/out buttons and changes mouse zoom to use the mouse wheel — when zoomed, drag to scrub through the history
2222
- Optimizes chart resizing and axis label rendering with author indicators, and re-adds the legend to the view
23-
- Adds a new _Folder History_ > _Open Visual Folder History_ command to folders in the _Explorer_ view, and other GitLens views
23+
- Adds a new _Folder History_ > _Open Visual Folder History_ command to folders in the _Explorer_ view, _Source Control_ view, and other GitLens views
2424
- Adds new ability to see and act upon a "paused" Git operation, e.g. merge, rebase, cherry-pick, revert, across the _Commits_, _Commit Graph_, and _Home_ views — closes [#3913](https://github.com/gitkraken/vscode-gitlens/issues/3913)
2525
- Adds a new banner on the _Commit Graph_ and updates the banner on _Home_ with actions to continue, skip, or abort the operation
2626
- Adds _Continue_, _Skip_, and _Abort_ actions to the _Commits_ view

contributions.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3199,6 +3199,12 @@
31993199
"group": "1_gitlens",
32003200
"order": 1
32013201
}
3202+
],
3203+
"gitlens/scm/resourceFolder/history": [
3204+
{
3205+
"group": "1_gitlens",
3206+
"order": 2
3207+
}
32023208
]
32033209
}
32043210
},
@@ -3723,6 +3729,12 @@
37233729
"group": "1_gitlens",
37243730
"order": 2
37253731
}
3732+
],
3733+
"gitlens/scm/resourceFolder/history": [
3734+
{
3735+
"group": "1_gitlens",
3736+
"order": 2
3737+
}
37263738
]
37273739
}
37283740
},
@@ -12116,6 +12128,18 @@
1211612128
]
1211712129
}
1211812130
},
12131+
"gitlens/scm/resourceFolder/history": {
12132+
"label": "Folder History",
12133+
"menus": {
12134+
"scm/resourceFolder/context": [
12135+
{
12136+
"when": "scmResourceGroup =~ /^(workingTree|index|merge)$/ && scmProvider == git && gitlens:enabled && config.gitlens.menus.scmItem.history",
12137+
"group": "2_gitlens_1",
12138+
"order": 1
12139+
}
12140+
]
12141+
}
12142+
},
1211912143
"gitlens/scm/resourceGroup/changes": {
1212012144
"label": "Open Changes",
1212112145
"menus": {

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13904,6 +13904,16 @@
1390413904
"group": "1_gitlens@2"
1390513905
}
1390613906
],
13907+
"gitlens/scm/resourceFolder/history": [
13908+
{
13909+
"command": "gitlens.openFolderHistory",
13910+
"group": "1_gitlens@2"
13911+
},
13912+
{
13913+
"command": "gitlens.showFolderInTimeline",
13914+
"group": "1_gitlens@2"
13915+
}
13916+
],
1390713917
"gitlens/scm/resourceGroup/changes": [
1390813918
{
1390913919
"command": "gitlens.externalDiffAll",
@@ -15050,6 +15060,11 @@
1505015060
"when": "scmResourceGroup =~ /^(workingTree|index|merge)$/ && scmProvider == git && gitlens:enabled && config.gitlens.menus.scmItem.compare",
1505115061
"group": "2_gitlens@1"
1505215062
},
15063+
{
15064+
"submenu": "gitlens/scm/resourceFolder/history",
15065+
"when": "scmResourceGroup =~ /^(workingTree|index|merge)$/ && scmProvider == git && gitlens:enabled && config.gitlens.menus.scmItem.history",
15066+
"group": "2_gitlens_1@1"
15067+
},
1505315068
{
1505415069
"command": "gitlens.copyPatchToClipboard",
1505515070
"when": "scmResourceGroup =~ /^(workingTree|index)$/ && scmProvider == git && gitlens:enabled && config.gitlens.menus.scmGroup.patch",
@@ -18936,6 +18951,10 @@
1893618951
"id": "gitlens/scm/resourceFolder/changes",
1893718952
"label": "Open Changes with"
1893818953
},
18954+
{
18955+
"id": "gitlens/scm/resourceFolder/history",
18956+
"label": "Folder History"
18957+
},
1893918958
{
1894018959
"id": "gitlens/scm/resourceGroup/changes",
1894118960
"label": "Open Changes"

src/commands/base.ts

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { isTag } from '../git/models/tag';
2626
import { CloudWorkspace, LocalWorkspace } from '../plus/workspaces/models';
2727
import { sequentialize } from '../system/function';
2828
import { registerCommand } from '../system/vscode/command';
29+
import { isScm, isScmResourceGroup, isScmResourceState } from '../system/vscode/scm';
2930
import { ViewNode } from '../views/nodes/abstract/viewNode';
3031
import { ViewRefFileNode, ViewRefNode } from '../views/nodes/abstract/viewRefNode';
3132

@@ -42,6 +43,8 @@ export interface CommandBaseContext {
4243
command: string;
4344
editor?: TextEditor;
4445
uri?: Uri;
46+
47+
readonly args: unknown[];
4548
}
4649

4750
export interface CommandEditorLineContext extends CommandBaseContext {
@@ -237,34 +240,6 @@ export type CommandContext =
237240
| CommandViewNodeContext
238241
| CommandViewNodesContext;
239242

240-
export function isScm(scm: any): scm is SourceControl {
241-
if (scm == null) return false;
242-
243-
return (
244-
(scm as SourceControl).id != null &&
245-
(scm as SourceControl).rootUri != null &&
246-
(scm as SourceControl).inputBox != null &&
247-
(scm as SourceControl).statusBarCommands != null
248-
);
249-
}
250-
251-
function isScmResourceGroup(group: any): group is SourceControlResourceGroup {
252-
if (group == null) return false;
253-
254-
return (
255-
(group as SourceControlResourceGroup).id != null &&
256-
(group as SourceControlResourceGroup).label != null &&
257-
(group as SourceControlResourceGroup).resourceStates != null &&
258-
Array.isArray((group as SourceControlResourceGroup).resourceStates)
259-
);
260-
}
261-
262-
function isScmResourceState(resource: any): resource is SourceControlResourceState {
263-
if (resource == null) return false;
264-
265-
return (resource as SourceControlResourceState).resourceUri != null;
266-
}
267-
268243
function isTimelineItem(item: any): item is TimelineItem {
269244
if (item == null) return false;
270245

@@ -334,6 +309,7 @@ export function parseCommandContext(
334309
): [CommandContext | CommandContext[], any[]] {
335310
let editor: TextEditor | undefined = undefined;
336311

312+
const originalArgs = [...args];
337313
let firstArg = args[0];
338314

339315
if (options?.expectsEditor) {
@@ -357,9 +333,12 @@ export function parseCommandContext(
357333

358334
const uris = rest[0];
359335
if (uris != null && Array.isArray(uris) && uris.length !== 0 && uris[0] instanceof Uri) {
360-
return [{ command: command, type: 'uris', editor: editor, uri: uri, uris: uris }, rest.slice(1)];
336+
return [
337+
{ command: command, type: 'uris', args: originalArgs, editor: editor, uri: uri, uris: uris },
338+
rest.slice(1),
339+
];
361340
}
362-
return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest];
341+
return [{ command: command, type: 'uri', args: originalArgs, editor: editor, uri: uri }, rest];
363342
}
364343

365344
args = args.slice(1);
@@ -370,6 +349,7 @@ export function parseCommandContext(
370349
{
371350
command: command,
372351
type: 'editorLine',
352+
args: originalArgs,
373353
editor: undefined,
374354
line: firstArg.lineNumber - 1, // convert to zero-based
375355
uri: firstArg.uri,
@@ -395,14 +375,14 @@ export function parseCommandContext(
395375
const contexts: CommandContext[] = [];
396376
for (const n of nodes) {
397377
if (n?.constructor === node.constructor) {
398-
contexts.push({ command: command, type: 'viewItem', node: n, uri: n.uri });
378+
contexts.push({ command: command, type: 'viewItem', args: originalArgs, node: n, uri: n.uri });
399379
}
400380
}
401381

402382
return [contexts, rest];
403383
}
404384

405-
return [{ command: command, type: 'viewItem', node: node, uri: node.uri }, rest];
385+
return [{ command: command, type: 'viewItem', args: originalArgs, node: node, uri: node.uri }, rest];
406386
}
407387

408388
if (isScmResourceState(firstArg)) {
@@ -416,7 +396,13 @@ export function parseCommandContext(
416396
}
417397

418398
return [
419-
{ command: command, type: 'scm-states', scmResourceStates: states, uri: states[0].resourceUri },
399+
{
400+
command: command,
401+
type: 'scm-states',
402+
args: originalArgs,
403+
scmResourceStates: states,
404+
uri: states[0].resourceUri,
405+
},
420406
args.slice(count),
421407
];
422408
}
@@ -431,20 +417,23 @@ export function parseCommandContext(
431417
groups.push(arg);
432418
}
433419

434-
return [{ command: command, type: 'scm-groups', scmResourceGroups: groups }, args.slice(count)];
420+
return [
421+
{ command: command, type: 'scm-groups', args: originalArgs, scmResourceGroups: groups },
422+
args.slice(count),
423+
];
435424
}
436425

437426
if (isGitTimelineItem(firstArg)) {
438427
const [item, uri, ...rest] = args as [GitTimelineItem, Uri, any];
439-
return [{ command: command, type: 'timeline-item:git', item: item, uri: uri }, rest];
428+
return [{ command: command, type: 'timeline-item:git', args: originalArgs, item: item, uri: uri }, rest];
440429
}
441430

442431
if (isScm(firstArg)) {
443432
const [scm, ...rest] = args as [SourceControl, any];
444-
return [{ command: command, type: 'scm', scm: scm }, rest];
433+
return [{ command: command, type: 'scm', args: originalArgs, scm: scm }, rest];
445434
}
446435

447-
return [{ command: command, type: 'unknown', editor: editor, uri: editor?.document.uri }, args];
436+
return [{ command: command, type: 'unknown', args: originalArgs, editor: editor, uri: editor?.document.uri }, args];
448437
}
449438

450439
export abstract class ActiveEditorCommand extends GlCommandBase {

src/commands/showQuickFileHistory.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { GitReference } from '../git/models/reference';
99
import type { GitTag } from '../git/models/tag';
1010
import type { CommandQuickPickItem } from '../quickpicks/items/common';
1111
import { command } from '../system/vscode/command';
12+
import { getScmResourceFolderUri } from '../system/vscode/scm';
1213
import type { CommandContext } from './base';
1314
import { ActiveEditorCachedCommand, getCommandUri } from './base';
1415

@@ -36,16 +37,22 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand {
3637
}
3738

3839
protected override preExecute(context: CommandContext, args?: ShowQuickFileHistoryCommandArgs) {
40+
let uri = context.uri;
3941
if (
4042
context.command === GlCommand.OpenFileHistory ||
41-
context.command === GlCommand.OpenFolderHistory ||
4243
context.command === GlCommand.Deprecated_ShowFileHistoryInView
4344
) {
4445
args = { ...args };
4546
args.showInSideBar = true;
47+
} else if (context.command === GlCommand.OpenFolderHistory) {
48+
args = { ...args };
49+
args.showInSideBar = true;
50+
if (context.type === 'scm-states') {
51+
uri = getScmResourceFolderUri(context.args) ?? context.uri;
52+
}
4653
}
4754

48-
return this.execute(context.editor, context.uri, args);
55+
return this.execute(context.editor, uri, args);
4956
}
5057

5158
async execute(editor?: TextEditor, uri?: Uri, args?: ShowQuickFileHistoryCommandArgs) {

src/system/path.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/;
99
const hasSchemeRegex = /^([a-zA-Z][\w+.-]+):/;
1010
const pathNormalizeRegex = /\\/g;
1111

12-
export function commonBase(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): string | undefined {
13-
const index = commonBaseIndex(s1, s2, delimiter, ignoreCase);
14-
return index > 0 ? s1.substring(0, index + 1) : undefined;
12+
export function commonBase(s: string[], delimiter: string, ignoreCase?: boolean): string | undefined {
13+
if (s.length === 0) return undefined;
14+
15+
let common = s[0];
16+
for (let i = 1; i < s.length; i++) {
17+
const index = commonBaseIndex(common, s[i], delimiter, ignoreCase);
18+
if (index === 0) return undefined;
19+
common = common.substring(0, index + 1);
20+
}
21+
22+
return common;
1523
}
1624

1725
export function commonBaseIndex(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): number {

src/system/vscode/scm.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { SourceControl, SourceControlResourceGroup, SourceControlResourceState } from 'vscode';
2+
import { Uri } from 'vscode';
3+
import { commonBase } from '../path';
4+
5+
// Since `scm/resourceFolder/context` commands use the URIs of the files, we have to find the common parent
6+
export function getScmResourceFolderUri(args: unknown[]): Uri | undefined {
7+
const uris = args
8+
.map(a => (isScmResourceState(a) ? a.resourceUri : undefined))
9+
.filter(<T>(u?: T): u is T => Boolean(u));
10+
if (!uris.length) return undefined;
11+
12+
const [uri] = uris;
13+
if (uris.length === 1) {
14+
// Strip off the filename
15+
return Uri.joinPath(uri, '..');
16+
}
17+
18+
const common = commonBase(
19+
uris.map(u => u.path),
20+
'/',
21+
);
22+
return Uri.from({
23+
scheme: uri.scheme,
24+
authority: uri.authority,
25+
path: common,
26+
});
27+
}
28+
29+
export function isScm(scm: unknown): scm is SourceControl {
30+
if (scm == null) return false;
31+
32+
return (
33+
(scm as SourceControl).id != null &&
34+
(scm as SourceControl).rootUri != null &&
35+
(scm as SourceControl).inputBox != null &&
36+
(scm as SourceControl).statusBarCommands != null
37+
);
38+
}
39+
40+
export function isScmResourceGroup(group: unknown): group is SourceControlResourceGroup {
41+
if (group == null) return false;
42+
43+
return (
44+
(group as SourceControlResourceGroup).id != null &&
45+
(group as SourceControlResourceGroup).label != null &&
46+
(group as SourceControlResourceGroup).resourceStates != null &&
47+
Array.isArray((group as SourceControlResourceGroup).resourceStates)
48+
);
49+
}
50+
51+
export function isScmResourceState(resource: unknown): resource is SourceControlResourceState {
52+
if (resource == null) return false;
53+
54+
return (resource as SourceControlResourceState).resourceUri != null;
55+
}

src/webviews/plus/graph/registration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Disposable, ViewColumn } from 'vscode';
2-
import { isScm } from '../../../commands/base';
32
import { GlCommand } from '../../../constants.commands';
43
import type { Container } from '../../../container';
54
import type { GitReference } from '../../../git/models/reference';
65
import type { Repository } from '../../../git/models/repository';
76
import { executeCommand, executeCoreCommand, registerCommand } from '../../../system/vscode/command';
87
import { configuration } from '../../../system/vscode/configuration';
98
import { getContext } from '../../../system/vscode/context';
9+
import { isScm } from '../../../system/vscode/scm';
1010
import { ViewNode } from '../../../views/nodes/abstract/viewNode';
1111
import type { BranchNode } from '../../../views/nodes/branchNode';
1212
import type { CommitFileNode } from '../../../views/nodes/commitFileNode';

src/webviews/plus/timeline/registration.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Disposable, ViewColumn } from 'vscode';
33
import { GlCommand } from '../../../constants.commands';
44
import { registerCommand } from '../../../system/vscode/command';
55
import { configuration } from '../../../system/vscode/configuration';
6+
import { getScmResourceFolderUri, isScmResourceState } from '../../../system/vscode/scm';
67
import type { ViewFileNode } from '../../../views/nodes/abstract/viewFileNode';
78
import type { WebviewPanelsProxy, WebviewsController } from '../../webviewsController';
89
import type { State } from './protocol';
@@ -64,14 +65,20 @@ export function registerTimelineWebviewCommands<T>(
6465
panels: WebviewPanelsProxy<'gitlens.timeline', TimelineWebviewShowingArgs, T>,
6566
) {
6667
return Disposable.from(
67-
registerCommand(
68-
GlCommand.ShowFileInTimeline,
69-
(...args: TimelineWebviewShowingArgs) => void panels.show(undefined, ...args),
70-
),
71-
registerCommand(
72-
GlCommand.ShowFolderInTimeline,
73-
(...args: TimelineWebviewShowingArgs) => void panels.show(undefined, ...args),
74-
),
68+
registerCommand(GlCommand.ShowFileInTimeline, (...args: TimelineWebviewShowingArgs) => {
69+
const [arg] = args;
70+
if (isScmResourceState(arg)) {
71+
args = [arg.resourceUri];
72+
}
73+
return void panels.show(undefined, ...args);
74+
}),
75+
registerCommand(GlCommand.ShowFolderInTimeline, (...args: TimelineWebviewShowingArgs) => {
76+
const uri = getScmResourceFolderUri(args);
77+
if (uri != null) {
78+
args = [uri];
79+
}
80+
void panels.show(undefined, ...args);
81+
}),
7582

7683
registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)),
7784
registerCommand(

0 commit comments

Comments
 (0)