@theia/scm 1.75.0-next.18 → 1.75.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/scm-frontend-module.d.ts.map +1 -1
- package/lib/browser/scm-frontend-module.js +6 -0
- package/lib/browser/scm-frontend-module.js.map +1 -1
- package/lib/browser/scm-history-graph-contribution.d.ts +42 -0
- package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-contribution.js +220 -0
- package/lib/browser/scm-history-graph-contribution.js.map +1 -0
- package/lib/browser/scm-history-graph-contribution.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-contribution.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-contribution.spec.js +66 -0
- package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-helpers.d.ts +8 -0
- package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-helpers.js +20 -0
- package/lib/browser/scm-history-graph-helpers.js.map +1 -1
- package/lib/browser/scm-history-graph-helpers.spec.js +25 -0
- package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-model.d.ts +43 -5
- package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-model.js +84 -10
- package/lib/browser/scm-history-graph-model.js.map +1 -1
- package/lib/browser/scm-history-graph-model.spec.js +145 -4
- package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.d.ts +8 -1
- package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-widget.js +23 -4
- package/lib/browser/scm-history-graph-widget.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.spec.js +11 -1
- package/lib/browser/scm-history-graph-widget.spec.js.map +1 -1
- package/lib/common/scm-preferences.d.ts +3 -0
- package/lib/common/scm-preferences.d.ts.map +1 -1
- package/lib/common/scm-preferences.js +24 -0
- package/lib/common/scm-preferences.js.map +1 -1
- package/package.json +6 -6
- package/src/browser/scm-frontend-module.ts +7 -1
- package/src/browser/scm-history-graph-contribution.spec.ts +83 -0
- package/src/browser/scm-history-graph-contribution.ts +220 -0
- package/src/browser/scm-history-graph-helpers.spec.ts +34 -1
- package/src/browser/scm-history-graph-helpers.ts +25 -0
- package/src/browser/scm-history-graph-model.spec.ts +184 -9
- package/src/browser/scm-history-graph-model.ts +99 -11
- package/src/browser/scm-history-graph-widget.spec.ts +12 -1
- package/src/browser/scm-history-graph-widget.tsx +26 -6
- package/src/common/scm-preferences.ts +27 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { injectable, inject, optional } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
|
19
|
+
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu';
|
|
20
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
21
|
+
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
|
22
|
+
import { QuickInputService } from '@theia/core/lib/browser';
|
|
23
|
+
import { QuickPickItem, QuickPickSeparator } from '@theia/core/lib/common/quick-pick-service';
|
|
24
|
+
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
|
25
|
+
import { ScmHistoryGraphModel, ScmHistoryGraphModelProvider } from './scm-history-graph-model';
|
|
26
|
+
import { ScmService } from './scm-service';
|
|
27
|
+
import { ScmHistoryItemRef, ScmHistoryProvider } from './scm-provider';
|
|
28
|
+
import { SCM_HISTORY_TITLE_MENU } from './scm-history-graph-widget';
|
|
29
|
+
import { getRefBadgeClass } from './scm-history-graph-helpers';
|
|
30
|
+
|
|
31
|
+
export namespace ScmHistoryGraphCommands {
|
|
32
|
+
export const REFRESH = Command.toDefaultLocalizedCommand({
|
|
33
|
+
id: 'scmHistoryGraph.refresh',
|
|
34
|
+
category: 'Source Control',
|
|
35
|
+
label: 'Refresh',
|
|
36
|
+
iconClass: codicon('refresh')
|
|
37
|
+
});
|
|
38
|
+
export const PICK_HISTORY_ITEM_REFS = Command.toLocalizedCommand({
|
|
39
|
+
id: 'scmHistoryGraph.pickHistoryItemRefs',
|
|
40
|
+
category: 'Source Control',
|
|
41
|
+
label: 'Pick History Item Reference...',
|
|
42
|
+
iconClass: codicon('git-branch')
|
|
43
|
+
}, 'theia/scm/pickHistoryItemRefs');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type RefPickItem = QuickPickItem & { refId?: string; auto?: boolean };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Contributes the native toolbar actions of the SCM history graph: the
|
|
50
|
+
* history item reference filter picker (VS Code's "Auto" dropdown) and the
|
|
51
|
+
* refresh action. Extension actions (e.g. fetch/pull/push from vscode.git)
|
|
52
|
+
* are contributed separately via the `scm/history/title` menu.
|
|
53
|
+
*/
|
|
54
|
+
@injectable()
|
|
55
|
+
export class ScmHistoryGraphContribution implements CommandContribution, MenuContribution {
|
|
56
|
+
|
|
57
|
+
@inject(ScmService) protected readonly scmService: ScmService;
|
|
58
|
+
@inject(ScmHistoryGraphModelProvider) protected readonly modelProvider: ScmHistoryGraphModelProvider;
|
|
59
|
+
@inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The graph model, resolved lazily: instantiating it starts loading
|
|
63
|
+
* history, so it must only be resolved when a graph command actually
|
|
64
|
+
* runs or the graph toolbar renders (the widget has created the model
|
|
65
|
+
* by then). In particular, `isEnabled`/`isVisible` are also queried by
|
|
66
|
+
* the command palette and therefore consult the `ScmService` instead.
|
|
67
|
+
*/
|
|
68
|
+
protected get model(): ScmHistoryGraphModel {
|
|
69
|
+
return this.modelProvider();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected get historyProvider(): ScmHistoryProvider | undefined {
|
|
73
|
+
return this.scmService.selectedRepository?.provider.historyProvider;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
registerCommands(commands: CommandRegistry): void {
|
|
77
|
+
commands.registerCommand(ScmHistoryGraphCommands.REFRESH, {
|
|
78
|
+
isEnabled: () => !!this.historyProvider,
|
|
79
|
+
isVisible: () => !!this.historyProvider,
|
|
80
|
+
execute: () => this.model.refresh()
|
|
81
|
+
});
|
|
82
|
+
commands.registerCommand(ScmHistoryGraphCommands.PICK_HISTORY_ITEM_REFS, {
|
|
83
|
+
isEnabled: () => !!this.historyProvider && !!this.quickInputService,
|
|
84
|
+
isVisible: () => !!this.historyProvider,
|
|
85
|
+
isToggled: () => !!this.model.historyItemRefFilter,
|
|
86
|
+
execute: () => this.pickHistoryItemRefs()
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
registerMenus(menus: MenuModelRegistry): void {
|
|
91
|
+
// Extension actions from vscode.git sit at order 900-902; the picker
|
|
92
|
+
// goes before them and refresh last, matching VS Code's graph toolbar.
|
|
93
|
+
menus.registerMenuAction([...SCM_HISTORY_TITLE_MENU, 'navigation'], {
|
|
94
|
+
commandId: ScmHistoryGraphCommands.PICK_HISTORY_ITEM_REFS.id,
|
|
95
|
+
order: '100'
|
|
96
|
+
});
|
|
97
|
+
menus.registerMenuAction([...SCM_HISTORY_TITLE_MENU, 'navigation'], {
|
|
98
|
+
commandId: ScmHistoryGraphCommands.REFRESH.id,
|
|
99
|
+
order: '999'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected async pickHistoryItemRefs(): Promise<void> {
|
|
104
|
+
if (!this.quickInputService) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const model = this.model;
|
|
108
|
+
const provider = model.provider;
|
|
109
|
+
if (!provider) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const picker = this.quickInputService.createQuickPick<RefPickItem>();
|
|
113
|
+
const cts = new CancellationTokenSource();
|
|
114
|
+
try {
|
|
115
|
+
picker.placeholder = nls.localize('theia/scm/pickHistoryItemRefsPlaceholder', 'Select one or more history item references');
|
|
116
|
+
picker.canSelectMany = true;
|
|
117
|
+
picker.matchOnDescription = true;
|
|
118
|
+
// Show the picker right away and populate it once the refs arrive,
|
|
119
|
+
// so that a slow ref lookup surfaces as a busy picker instead of a
|
|
120
|
+
// command that appears to do nothing.
|
|
121
|
+
picker.busy = true;
|
|
122
|
+
|
|
123
|
+
const selection = await new Promise<readonly RefPickItem[] | undefined>(resolve => {
|
|
124
|
+
picker.onDidAccept(() => {
|
|
125
|
+
resolve(picker.selectedItems);
|
|
126
|
+
picker.hide();
|
|
127
|
+
});
|
|
128
|
+
// Also cancels a still-running ref lookup when the picker is dismissed.
|
|
129
|
+
picker.onDidHide(() => {
|
|
130
|
+
cts.cancel();
|
|
131
|
+
resolve(undefined);
|
|
132
|
+
});
|
|
133
|
+
picker.show();
|
|
134
|
+
|
|
135
|
+
provider.provideHistoryItemRefs(undefined, cts.token).then(refs => {
|
|
136
|
+
if (cts.token.isCancellationRequested) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const currentFilter = model.historyItemRefFilter;
|
|
140
|
+
const items = this.createRefPickItems(refs ?? [], provider);
|
|
141
|
+
picker.items = items;
|
|
142
|
+
picker.selectedItems = items.filter((item): item is RefPickItem => !QuickPickSeparator.is(item)
|
|
143
|
+
&& (currentFilter ? !!item.refId && currentFilter.includes(item.refId) : item.auto === true));
|
|
144
|
+
|
|
145
|
+
// "Auto" is exclusive: checking it unchecks everything else and vice versa
|
|
146
|
+
let lastSelection: readonly RefPickItem[] = picker.selectedItems;
|
|
147
|
+
picker.onDidChangeSelection(newSelection => {
|
|
148
|
+
const autoNow = newSelection.some(item => item.auto);
|
|
149
|
+
if (autoNow && newSelection.length > 1) {
|
|
150
|
+
const autoBefore = lastSelection.some(item => item.auto);
|
|
151
|
+
picker.selectedItems = autoBefore ? newSelection.filter(item => !item.auto) : newSelection.filter(item => item.auto);
|
|
152
|
+
}
|
|
153
|
+
lastSelection = picker.selectedItems;
|
|
154
|
+
});
|
|
155
|
+
picker.busy = false;
|
|
156
|
+
}, err => {
|
|
157
|
+
if (!cts.token.isCancellationRequested) {
|
|
158
|
+
console.error('ScmHistoryGraphContribution: failed to load history item refs', err);
|
|
159
|
+
}
|
|
160
|
+
picker.hide();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!selection) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// The selected repository may have changed while the picker was open;
|
|
168
|
+
// the picked refs belong to the old provider and must not be applied
|
|
169
|
+
// to the new one.
|
|
170
|
+
if (model.provider !== provider) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const refIds = selection.filter(item => item.refId !== undefined).map(item => item.refId!);
|
|
174
|
+
const auto = selection.some(item => item.auto) || refIds.length === 0;
|
|
175
|
+
model.setHistoryItemRefFilter(auto ? undefined : refIds);
|
|
176
|
+
} finally {
|
|
177
|
+
picker.dispose();
|
|
178
|
+
cts.dispose();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected createRefPickItems(refs: readonly ScmHistoryItemRef[], provider: ScmHistoryProvider): (RefPickItem | QuickPickSeparator)[] {
|
|
183
|
+
const autoRefs = [provider.currentHistoryItemRef, provider.currentHistoryItemRemoteRef, provider.currentHistoryItemBaseRef]
|
|
184
|
+
.filter((ref): ref is ScmHistoryItemRef => !!ref);
|
|
185
|
+
const items: (RefPickItem | QuickPickSeparator)[] = [{
|
|
186
|
+
label: nls.localizeByDefault('Auto'),
|
|
187
|
+
description: autoRefs.map(ref => ref.name).join(', '),
|
|
188
|
+
auto: true
|
|
189
|
+
}];
|
|
190
|
+
|
|
191
|
+
const groups: { label: string; badgeClass: string }[] = [
|
|
192
|
+
{ label: nls.localize('theia/scm/refPickerBranches', 'branches'), badgeClass: 'head' },
|
|
193
|
+
{ label: nls.localize('theia/scm/refPickerRemoteBranches', 'remote branches'), badgeClass: 'remote' },
|
|
194
|
+
{ label: nls.localize('theia/scm/refPickerTags', 'tags'), badgeClass: 'tag' }
|
|
195
|
+
];
|
|
196
|
+
const grouped = new Set<ScmHistoryItemRef>();
|
|
197
|
+
for (const group of groups) {
|
|
198
|
+
const groupRefs = refs.filter(ref => getRefBadgeClass(ref) === group.badgeClass);
|
|
199
|
+
if (groupRefs.length > 0) {
|
|
200
|
+
groupRefs.forEach(ref => grouped.add(ref));
|
|
201
|
+
items.push({ type: 'separator', label: group.label });
|
|
202
|
+
items.push(...groupRefs.map(ref => this.toRefPickItem(ref)));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const remaining = refs.filter(ref => !grouped.has(ref));
|
|
206
|
+
if (remaining.length > 0) {
|
|
207
|
+
items.push({ type: 'separator' });
|
|
208
|
+
items.push(...remaining.map(ref => this.toRefPickItem(ref)));
|
|
209
|
+
}
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected toRefPickItem(ref: ScmHistoryItemRef): RefPickItem {
|
|
214
|
+
return {
|
|
215
|
+
label: ref.name,
|
|
216
|
+
description: ref.description,
|
|
217
|
+
refId: ref.id
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
-
import { getRefBadgePresentation, getRefColorIndex } from './scm-history-graph-helpers';
|
|
18
|
+
import { filterRefsForBadges, getRefBadgePresentation, getRefColorIndex } from './scm-history-graph-helpers';
|
|
19
19
|
import { ScmHistoryItemRef, ScmHistoryProvider } from './scm-provider';
|
|
20
20
|
|
|
21
21
|
describe('getRefColorIndex', () => {
|
|
@@ -83,3 +83,36 @@ describe('getRefBadgePresentation', () => {
|
|
|
83
83
|
expect(getRefBadgePresentation({ id: 'refs/heads/feature', name: 'feature' }, provider).colorIndex).to.be.undefined;
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('filterRefsForBadges', () => {
|
|
88
|
+
|
|
89
|
+
const mainRef: ScmHistoryItemRef = { id: 'refs/heads/main', name: 'main' };
|
|
90
|
+
const remoteRef: ScmHistoryItemRef = { id: 'refs/remotes/origin/main', name: 'origin/main' };
|
|
91
|
+
const featureRef: ScmHistoryItemRef = { id: 'refs/heads/feature', name: 'feature' };
|
|
92
|
+
const refs = [mainRef, remoteRef, featureRef];
|
|
93
|
+
|
|
94
|
+
const provider = {
|
|
95
|
+
currentHistoryItemRef: mainRef,
|
|
96
|
+
currentHistoryItemRemoteRef: remoteRef,
|
|
97
|
+
} as ScmHistoryProvider;
|
|
98
|
+
|
|
99
|
+
it("returns all refs in 'all' mode", () => {
|
|
100
|
+
expect(filterRefsForBadges(refs, provider, 'all')).to.deep.equal(refs);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("keeps only the refs used as a filter in 'filter' mode", () => {
|
|
104
|
+
expect(filterRefsForBadges(refs, provider, 'filter')).to.deep.equal([mainRef, remoteRef]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns all refs in 'filter' mode when no provider is available", () => {
|
|
108
|
+
expect(filterRefsForBadges(refs, undefined, 'filter')).to.deep.equal(refs);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps only the explicitly picked refs in 'filter' mode with an explicit filter", () => {
|
|
112
|
+
expect(filterRefsForBadges(refs, provider, 'filter', ['refs/heads/feature'])).to.deep.equal([featureRef]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("ignores an explicit filter in 'all' mode", () => {
|
|
116
|
+
expect(filterRefsForBadges(refs, provider, 'all', ['refs/heads/feature'])).to.deep.equal(refs);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -105,6 +105,31 @@ export function getRefColorIndex(ref: ScmHistoryItemRef, provider: ScmHistoryPro
|
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Filters which refs get badges in the graph, following VS Code's
|
|
110
|
+
* `scm.graph.badges` setting: `'all'` shows every ref, `'filter'` shows only
|
|
111
|
+
* the refs used as the graph's filter — the explicitly picked ref ids, or,
|
|
112
|
+
* without an explicit filter (auto mode), the current, remote, and base refs.
|
|
113
|
+
* Without provider information all refs are kept.
|
|
114
|
+
*/
|
|
115
|
+
export function filterRefsForBadges(
|
|
116
|
+
refs: readonly ScmHistoryItemRef[],
|
|
117
|
+
provider: ScmHistoryProvider | undefined,
|
|
118
|
+
badges: 'all' | 'filter',
|
|
119
|
+
filter?: readonly string[]
|
|
120
|
+
): readonly ScmHistoryItemRef[] {
|
|
121
|
+
if (badges === 'all') {
|
|
122
|
+
return refs;
|
|
123
|
+
}
|
|
124
|
+
if (filter) {
|
|
125
|
+
return refs.filter(ref => filter.includes(ref.id));
|
|
126
|
+
}
|
|
127
|
+
if (!provider) {
|
|
128
|
+
return refs;
|
|
129
|
+
}
|
|
130
|
+
return refs.filter(ref => getRefColorIndex(ref, provider) !== undefined);
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
export interface RefBadgePresentation {
|
|
109
134
|
/** Icon class for the badge icon, e.g. 'codicon-git-branch'. */
|
|
110
135
|
iconClass: string;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
18
|
import { Emitter } from '@theia/core/lib/common/event';
|
|
19
19
|
import { ScmHistoryGraphModel, PAGE_SIZE } from './scm-history-graph-model';
|
|
20
|
-
import { ScmHistoryItem, ScmHistoryItemRef, ScmHistoryItemChange, ScmHistoryOptions, ScmHistoryProvider } from './scm-provider';
|
|
20
|
+
import { ScmHistoryItem, ScmHistoryItemRef, ScmHistoryItemRefsChangeEvent, ScmHistoryItemChange, ScmHistoryOptions, ScmHistoryProvider } from './scm-provider';
|
|
21
21
|
import { ScmRepository } from './scm-repository';
|
|
22
22
|
|
|
23
23
|
class StubHistoryProvider implements ScmHistoryProvider {
|
|
@@ -26,11 +26,12 @@ class StubHistoryProvider implements ScmHistoryProvider {
|
|
|
26
26
|
currentHistoryItemBaseRef: ScmHistoryItemRef | undefined;
|
|
27
27
|
|
|
28
28
|
readonly onDidChangeCurrentHistoryItemRefs = new Emitter<void>().event;
|
|
29
|
-
readonly
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
protected readonly onDidChangeHistoryItemRefsEmitter = new Emitter<ScmHistoryItemRefsChangeEvent>();
|
|
30
|
+
readonly onDidChangeHistoryItemRefs = this.onDidChangeHistoryItemRefsEmitter.event;
|
|
31
|
+
|
|
32
|
+
fireHistoryItemRefsChanged(event: ScmHistoryItemRefsChangeEvent): void {
|
|
33
|
+
this.onDidChangeHistoryItemRefsEmitter.fire(event);
|
|
34
|
+
}
|
|
34
35
|
|
|
35
36
|
/** Pages this provider will return on consecutive calls (FIFO). */
|
|
36
37
|
pages: ScmHistoryItem[][] = [];
|
|
@@ -75,30 +76,46 @@ function makeItems(start: number, count: number): ScmHistoryItem[] {
|
|
|
75
76
|
/**
|
|
76
77
|
* Instantiates the model with a stub provider, bypassing inversify and the
|
|
77
78
|
* SCM service so tests can drive pagination directly via loadPage().
|
|
79
|
+
* When `wireRepository` is set, the provider is instead wired through a stub
|
|
80
|
+
* selected repository and refresh(), so provider event subscriptions are live.
|
|
78
81
|
*/
|
|
79
|
-
function createModel(
|
|
82
|
+
function createModel(
|
|
83
|
+
provider: ScmHistoryProvider,
|
|
84
|
+
preferences: Record<string, unknown> = {},
|
|
85
|
+
wireRepository = false
|
|
86
|
+
): { model: ScmHistoryGraphModel; loadPage(): Promise<void>; firePreferenceChanged(preferenceName: string): void } {
|
|
80
87
|
const model = new ScmHistoryGraphModel();
|
|
81
88
|
const internals = model as unknown as {
|
|
82
89
|
scmService: {
|
|
83
90
|
onDidChangeSelectedRepository: Emitter<ScmRepository | undefined>['event'];
|
|
84
91
|
selectedRepository: ScmRepository | undefined;
|
|
85
92
|
};
|
|
93
|
+
scmPreferences: Record<string, unknown>;
|
|
86
94
|
_provider: ScmHistoryProvider | undefined;
|
|
87
95
|
init(): void;
|
|
88
96
|
loadPage(): Promise<void>;
|
|
89
97
|
};
|
|
98
|
+
const preferenceEmitter = new Emitter<{ preferenceName: string }>();
|
|
90
99
|
internals.scmService = {
|
|
91
100
|
onDidChangeSelectedRepository: new Emitter<ScmRepository | undefined>().event,
|
|
92
|
-
selectedRepository: undefined,
|
|
101
|
+
selectedRepository: wireRepository ? { provider: { historyProvider: provider } } as unknown as ScmRepository : undefined,
|
|
93
102
|
};
|
|
103
|
+
internals.scmPreferences = Object.assign(preferences, { onPreferenceChanged: preferenceEmitter.event });
|
|
94
104
|
internals.init();
|
|
95
|
-
|
|
105
|
+
if (!wireRepository) {
|
|
106
|
+
internals._provider = provider;
|
|
107
|
+
}
|
|
96
108
|
return {
|
|
97
109
|
model,
|
|
98
110
|
loadPage: () => internals.loadPage(),
|
|
111
|
+
firePreferenceChanged: preferenceName => preferenceEmitter.fire({ preferenceName }),
|
|
99
112
|
};
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
async function nextTick(): Promise<void> {
|
|
116
|
+
return new Promise(resolve => setTimeout(resolve, 0));
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
describe('ScmHistoryGraphModel - pagination', () => {
|
|
103
120
|
|
|
104
121
|
it('hasMore is true when a full page returns', async () => {
|
|
@@ -170,6 +187,164 @@ describe('ScmHistoryGraphModel - pagination', () => {
|
|
|
170
187
|
});
|
|
171
188
|
});
|
|
172
189
|
|
|
190
|
+
describe('ScmHistoryGraphModel - page size preference', () => {
|
|
191
|
+
|
|
192
|
+
it('requests the configured page size and paginates by it', async () => {
|
|
193
|
+
const provider = new StubHistoryProvider();
|
|
194
|
+
provider.pages = [makeItems(1, 10)];
|
|
195
|
+
const { model, loadPage } = createModel(provider, { 'scm.graph.pageSize': 10 });
|
|
196
|
+
|
|
197
|
+
await loadPage();
|
|
198
|
+
|
|
199
|
+
expect(provider.receivedOptions[0].limit).to.equal(10);
|
|
200
|
+
expect(model.entries).to.have.length(10);
|
|
201
|
+
expect(model.hasMore).to.be.true;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('falls back to the default page size without a preference value', async () => {
|
|
205
|
+
const provider = new StubHistoryProvider();
|
|
206
|
+
provider.pages = [makeItems(1, 10)];
|
|
207
|
+
const { loadPage } = createModel(provider);
|
|
208
|
+
|
|
209
|
+
await loadPage();
|
|
210
|
+
|
|
211
|
+
expect(provider.receivedOptions[0].limit).to.equal(PAGE_SIZE);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('reloads from the start when scm.graph.pageSize changes', async () => {
|
|
215
|
+
const provider = new StubHistoryProvider();
|
|
216
|
+
provider.pages = [makeItems(1, 10), makeItems(1, 5)];
|
|
217
|
+
const preferences: Record<string, unknown> = { 'scm.graph.pageSize': 10 };
|
|
218
|
+
const { loadPage, firePreferenceChanged } = createModel(provider, preferences);
|
|
219
|
+
|
|
220
|
+
await loadPage();
|
|
221
|
+
expect(provider.receivedOptions[0].limit).to.equal(10);
|
|
222
|
+
|
|
223
|
+
preferences['scm.graph.pageSize'] = 5;
|
|
224
|
+
firePreferenceChanged('scm.graph.pageSize');
|
|
225
|
+
await nextTick();
|
|
226
|
+
|
|
227
|
+
expect(provider.receivedOptions[1].limit).to.equal(5);
|
|
228
|
+
expect(provider.receivedOptions[1].skip).to.equal(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('ScmHistoryGraphModel - history item ref filter', () => {
|
|
233
|
+
|
|
234
|
+
const mainRef: ScmHistoryItemRef = { id: 'refs/heads/main', name: 'main', revision: 'c1' };
|
|
235
|
+
|
|
236
|
+
function createFilterProvider(): StubHistoryProvider {
|
|
237
|
+
const provider = new StubHistoryProvider();
|
|
238
|
+
provider.currentHistoryItemRef = mainRef;
|
|
239
|
+
provider.pages = [makeItems(1, 3), makeItems(1, 3), makeItems(1, 3)];
|
|
240
|
+
return provider;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it('is in auto mode by default: current ref revisions are requested and the current ref is in the filter', async () => {
|
|
244
|
+
const provider = createFilterProvider();
|
|
245
|
+
const { model, loadPage } = createModel(provider);
|
|
246
|
+
await loadPage();
|
|
247
|
+
expect(provider.receivedOptions[0].historyItemRefs).to.deep.equal(['c1']);
|
|
248
|
+
expect(model.historyItemRefFilter).to.be.undefined;
|
|
249
|
+
expect(model.isCurrentRefInFilter()).to.be.true;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('passes the picked ref ids to the provider and reloads', async () => {
|
|
253
|
+
const provider = createFilterProvider();
|
|
254
|
+
const { model } = createModel(provider);
|
|
255
|
+
model.setHistoryItemRefFilter(['refs/heads/feature']);
|
|
256
|
+
await nextTick();
|
|
257
|
+
expect(provider.receivedOptions[0].historyItemRefs).to.deep.equal(['refs/heads/feature']);
|
|
258
|
+
expect(model.historyItemRefFilter).to.deep.equal(['refs/heads/feature']);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('reports whether the current ref is part of the filter', async () => {
|
|
262
|
+
const provider = createFilterProvider();
|
|
263
|
+
const { model } = createModel(provider);
|
|
264
|
+
model.setHistoryItemRefFilter(['refs/heads/feature']);
|
|
265
|
+
await nextTick();
|
|
266
|
+
expect(model.isCurrentRefInFilter()).to.be.false;
|
|
267
|
+
model.setHistoryItemRefFilter(['refs/heads/feature', 'refs/heads/main']);
|
|
268
|
+
await nextTick();
|
|
269
|
+
expect(model.isCurrentRefInFilter()).to.be.true;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('returns to auto mode when the filter is cleared', async () => {
|
|
273
|
+
const provider = createFilterProvider();
|
|
274
|
+
const { model } = createModel(provider);
|
|
275
|
+
model.setHistoryItemRefFilter(['refs/heads/feature']);
|
|
276
|
+
await nextTick();
|
|
277
|
+
model.setHistoryItemRefFilter(undefined);
|
|
278
|
+
await nextTick();
|
|
279
|
+
expect(model.historyItemRefFilter).to.be.undefined;
|
|
280
|
+
expect(provider.receivedOptions[1].historyItemRefs).to.deep.equal(['c1']);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('does not apply ref role colors to refs excluded by the filter', async () => {
|
|
284
|
+
const provider = createFilterProvider();
|
|
285
|
+
provider.pages = [[
|
|
286
|
+
{ id: 'c1', subject: 'head', parentIds: ['c2'], references: [mainRef] },
|
|
287
|
+
{ id: 'c2', subject: 'base', parentIds: [] },
|
|
288
|
+
]];
|
|
289
|
+
const { model } = createModel(provider);
|
|
290
|
+
model.setHistoryItemRefFilter(['refs/heads/feature']);
|
|
291
|
+
await nextTick();
|
|
292
|
+
expect(model.entries[0].graphRow.color).to.not.equal(0);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('ScmHistoryGraphModel - ref filter pruning', () => {
|
|
297
|
+
|
|
298
|
+
const mainRef: ScmHistoryItemRef = { id: 'refs/heads/main', name: 'main', revision: 'c1' };
|
|
299
|
+
const featureRef: ScmHistoryItemRef = { id: 'refs/heads/feature', name: 'feature', revision: 'c5' };
|
|
300
|
+
|
|
301
|
+
function createWiredProvider(): StubHistoryProvider {
|
|
302
|
+
const provider = new StubHistoryProvider();
|
|
303
|
+
provider.currentHistoryItemRef = mainRef;
|
|
304
|
+
return provider;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
it('drops a removed ref from the filter and keeps the remaining ones', async () => {
|
|
308
|
+
const provider = createWiredProvider();
|
|
309
|
+
const { model } = createModel(provider, {}, true);
|
|
310
|
+
model.setHistoryItemRefFilter([featureRef.id, mainRef.id]);
|
|
311
|
+
await nextTick();
|
|
312
|
+
|
|
313
|
+
provider.fireHistoryItemRefsChanged({ added: [], removed: [featureRef], modified: [] });
|
|
314
|
+
await nextTick();
|
|
315
|
+
|
|
316
|
+
expect(model.historyItemRefFilter).to.deep.equal([mainRef.id]);
|
|
317
|
+
const last = provider.receivedOptions[provider.receivedOptions.length - 1];
|
|
318
|
+
expect(last.historyItemRefs).to.deep.equal([mainRef.id]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('falls back to auto mode when all filtered refs are removed', async () => {
|
|
322
|
+
const provider = createWiredProvider();
|
|
323
|
+
const { model } = createModel(provider, {}, true);
|
|
324
|
+
model.setHistoryItemRefFilter([featureRef.id]);
|
|
325
|
+
await nextTick();
|
|
326
|
+
|
|
327
|
+
provider.fireHistoryItemRefsChanged({ added: [], removed: [featureRef], modified: [] });
|
|
328
|
+
await nextTick();
|
|
329
|
+
|
|
330
|
+
expect(model.historyItemRefFilter).to.be.undefined;
|
|
331
|
+
const last = provider.receivedOptions[provider.receivedOptions.length - 1];
|
|
332
|
+
expect(last.historyItemRefs).to.deep.equal(['c1']);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('keeps the filter when unrelated refs are removed', async () => {
|
|
336
|
+
const provider = createWiredProvider();
|
|
337
|
+
const { model } = createModel(provider, {}, true);
|
|
338
|
+
model.setHistoryItemRefFilter([mainRef.id]);
|
|
339
|
+
await nextTick();
|
|
340
|
+
|
|
341
|
+
provider.fireHistoryItemRefsChanged({ added: [], removed: [featureRef], modified: [] });
|
|
342
|
+
await nextTick();
|
|
343
|
+
|
|
344
|
+
expect(model.historyItemRefFilter).to.deep.equal([mainRef.id]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
173
348
|
describe('ScmHistoryGraphModel - ref colors and current item', () => {
|
|
174
349
|
|
|
175
350
|
const mainRef: ScmHistoryItemRef = { id: 'refs/heads/main', name: 'main', revision: 'c2' };
|