@theia/scm 1.75.0-next.3 → 1.75.0-next.34
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/decorations/scm-navigator-decorator.d.ts.map +1 -1
- package/lib/browser/decorations/scm-navigator-decorator.js +1 -0
- package/lib/browser/decorations/scm-navigator-decorator.js.map +1 -1
- package/lib/browser/scm-context-key-service.d.ts +2 -0
- package/lib/browser/scm-context-key-service.d.ts.map +1 -1
- package/lib/browser/scm-context-key-service.js +4 -0
- package/lib/browser/scm-context-key-service.js.map +1 -1
- package/lib/browser/scm-context-key-service.spec.js +7 -0
- package/lib/browser/scm-context-key-service.spec.js.map +1 -1
- 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 +44 -0
- package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-contribution.js +226 -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 +34 -6
- package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-helpers.js +71 -5
- package/lib/browser/scm-history-graph-helpers.js.map +1 -1
- package/lib/browser/scm-history-graph-helpers.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-helpers.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-helpers.spec.js +96 -0
- package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-lanes.d.ts +19 -5
- package/lib/browser/scm-history-graph-lanes.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-lanes.js +25 -9
- package/lib/browser/scm-history-graph-lanes.js.map +1 -1
- package/lib/browser/scm-history-graph-lanes.spec.js +62 -25
- package/lib/browser/scm-history-graph-lanes.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-model.d.ts +52 -5
- package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-model.js +108 -10
- package/lib/browser/scm-history-graph-model.js.map +1 -1
- package/lib/browser/scm-history-graph-model.spec.js +197 -4
- package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.d.ts +27 -2
- package/lib/browser/scm-history-graph-tooltip.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.js +113 -22
- package/lib/browser/scm-history-graph-tooltip.js.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-tooltip.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-tooltip.spec.js +213 -0
- package/lib/browser/scm-history-graph-tooltip.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-widget.d.ts +23 -1
- package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-widget.js +93 -24
- package/lib/browser/scm-history-graph-widget.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-widget.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-widget.spec.js +100 -0
- package/lib/browser/scm-history-graph-widget.spec.js.map +1 -0
- package/lib/browser/scm-provider.d.ts +9 -1
- package/lib/browser/scm-provider.d.ts.map +1 -1
- package/lib/browser/scm-provider.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/decorations/scm-navigator-decorator.ts +3 -2
- package/src/browser/scm-context-key-service.spec.ts +9 -0
- package/src/browser/scm-context-key-service.ts +6 -0
- 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 +222 -0
- package/src/browser/scm-history-graph-helpers.spec.ts +118 -0
- package/src/browser/scm-history-graph-helpers.ts +81 -6
- package/src/browser/scm-history-graph-lanes.spec.ts +70 -25
- package/src/browser/scm-history-graph-lanes.ts +48 -11
- package/src/browser/scm-history-graph-model.spec.ts +247 -12
- package/src/browser/scm-history-graph-model.ts +125 -12
- package/src/browser/scm-history-graph-tooltip.spec.ts +254 -0
- package/src/browser/scm-history-graph-tooltip.ts +144 -24
- package/src/browser/scm-history-graph-widget.spec.ts +112 -0
- package/src/browser/scm-history-graph-widget.tsx +104 -35
- package/src/browser/scm-provider.ts +9 -1
- package/src/browser/style/scm-history-graph.css +43 -2
- package/src/common/scm-preferences.ts +27 -0
|
@@ -47,8 +47,14 @@ export interface GraphEdge {
|
|
|
47
47
|
export interface GraphRow {
|
|
48
48
|
/** Lane index where the commit node is rendered. */
|
|
49
49
|
readonly lane: number;
|
|
50
|
-
/** Color index for the commit node dot. */
|
|
50
|
+
/** Color index for the commit node dot and the lane segment below it. */
|
|
51
51
|
readonly color: number;
|
|
52
|
+
/**
|
|
53
|
+
* Color index for the lane segment above the commit node. Differs from
|
|
54
|
+
* `color` when this commit's ref color overrides the color inherited from
|
|
55
|
+
* the chain above (e.g. the current branch tip below remote-only commits).
|
|
56
|
+
*/
|
|
57
|
+
readonly topColor: number;
|
|
52
58
|
/** Edges crossing or originating on this row. */
|
|
53
59
|
readonly edges: readonly GraphEdge[];
|
|
54
60
|
/**
|
|
@@ -71,11 +77,29 @@ export interface GraphRow {
|
|
|
71
77
|
interface MutableGraphRow {
|
|
72
78
|
lane: number;
|
|
73
79
|
color: number;
|
|
80
|
+
topColor: number;
|
|
74
81
|
edges: GraphEdge[];
|
|
75
82
|
hasContinuation: boolean;
|
|
76
83
|
hasTopLine: boolean;
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
export interface GraphCommitInput {
|
|
87
|
+
id: string;
|
|
88
|
+
parentIds?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Ref-based color index resolved from the commit's references
|
|
91
|
+
* (0 = current ref, 1 = remote ref, 2 = base ref). When set, it colors
|
|
92
|
+
* this row and propagates down the first-parent chain, mirroring
|
|
93
|
+
* VS Code's scm graph color map.
|
|
94
|
+
*/
|
|
95
|
+
colorIndex?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** First color index of the default rotation (`scmGraph.foreground1`). */
|
|
99
|
+
const DEFAULT_COLOR_FIRST = 3;
|
|
100
|
+
/** Number of default rotation colors (`scmGraph.foreground1-5`). */
|
|
101
|
+
const DEFAULT_COLOR_COUNT = 5;
|
|
102
|
+
|
|
79
103
|
/**
|
|
80
104
|
* Compute graph rows for an ordered list of commits (topological order,
|
|
81
105
|
* newest first). Each commit must supply its own `id` and `parentIds`.
|
|
@@ -84,14 +108,22 @@ interface MutableGraphRow {
|
|
|
84
108
|
* @returns One `GraphRow` per commit in the same order.
|
|
85
109
|
*/
|
|
86
110
|
export function computeGraphRows(
|
|
87
|
-
commits: ReadonlyArray<
|
|
111
|
+
commits: ReadonlyArray<GraphCommitInput>
|
|
88
112
|
): GraphRow[] {
|
|
89
113
|
// lanes[i] = id of commit that "owns" lane i (i.e. we are waiting for this
|
|
90
114
|
// commit to appear in the list so we can close the lane).
|
|
91
115
|
const lanes: (string | undefined)[] = [];
|
|
92
|
-
// laneColors[i] = the color index
|
|
116
|
+
// laneColors[i] = the color index currently assigned to lane i
|
|
93
117
|
const laneColors: (number | undefined)[] = [];
|
|
94
118
|
|
|
119
|
+
// Default lane colors rotate through foreground1-5, like VS Code's
|
|
120
|
+
// color registry; indices 0-2 are reserved for ref-based colors.
|
|
121
|
+
let rotation = -1;
|
|
122
|
+
const nextDefaultColor = (): number => {
|
|
123
|
+
rotation = (rotation + 1) % DEFAULT_COLOR_COUNT;
|
|
124
|
+
return DEFAULT_COLOR_FIRST + rotation;
|
|
125
|
+
};
|
|
126
|
+
|
|
95
127
|
const rows: MutableGraphRow[] = [];
|
|
96
128
|
|
|
97
129
|
for (const commit of commits) {
|
|
@@ -106,10 +138,15 @@ export function computeGraphRows(
|
|
|
106
138
|
// Not yet tracked → open a new lane
|
|
107
139
|
myLane = firstFreeLane(lanes);
|
|
108
140
|
lanes[myLane] = commit.id;
|
|
109
|
-
laneColors[myLane] =
|
|
141
|
+
laneColors[myLane] = commit.colorIndex ?? nextDefaultColor();
|
|
110
142
|
}
|
|
111
143
|
|
|
112
|
-
|
|
144
|
+
// The segment above the commit keeps the color inherited from the
|
|
145
|
+
// chain above; the commit's own ref color (if any) takes over from
|
|
146
|
+
// this row downward.
|
|
147
|
+
const topColor = laneColors[myLane] ?? DEFAULT_COLOR_FIRST;
|
|
148
|
+
const myColor = commit.colorIndex ?? topColor;
|
|
149
|
+
laneColors[myLane] = myColor;
|
|
113
150
|
|
|
114
151
|
// Collect any duplicate lane occupants: other lanes that were kept
|
|
115
152
|
// alive pointing at this same commit (sibling branch tips whose lane
|
|
@@ -120,7 +157,7 @@ export function computeGraphRows(
|
|
|
120
157
|
if (hasTopLine) {
|
|
121
158
|
for (let li = 0; li < lanes.length; li++) {
|
|
122
159
|
if (lanes[li] === commit.id && li !== myLane) {
|
|
123
|
-
duplicateLanes.push({ lane: li, color: laneColors[li] ??
|
|
160
|
+
duplicateLanes.push({ lane: li, color: laneColors[li] ?? DEFAULT_COLOR_FIRST });
|
|
124
161
|
lanes[li] = undefined;
|
|
125
162
|
laneColors[li] = undefined;
|
|
126
163
|
}
|
|
@@ -170,7 +207,7 @@ export function computeGraphRows(
|
|
|
170
207
|
// Additional parents get new lanes — branch-out
|
|
171
208
|
const newLane = firstFreeLane(lanes);
|
|
172
209
|
lanes[newLane] = pid;
|
|
173
|
-
laneColors[newLane] =
|
|
210
|
+
laneColors[newLane] = nextDefaultColor();
|
|
174
211
|
parentLanes.push(newLane);
|
|
175
212
|
parentIsExisting.push(false);
|
|
176
213
|
}
|
|
@@ -206,7 +243,7 @@ export function computeGraphRows(
|
|
|
206
243
|
if (!occupant || occupant === commit.id) {
|
|
207
244
|
continue;
|
|
208
245
|
}
|
|
209
|
-
const color = laneColors[li] ??
|
|
246
|
+
const color = laneColors[li] ?? DEFAULT_COLOR_FIRST;
|
|
210
247
|
edges.push({ fromLane: li, toLane: li, color, type: 'pass-through' });
|
|
211
248
|
}
|
|
212
249
|
|
|
@@ -225,11 +262,11 @@ export function computeGraphRows(
|
|
|
225
262
|
if (isExisting) {
|
|
226
263
|
// Merge-in: an existing lane at the top of the row curves into
|
|
227
264
|
// the commit position at mid-row.
|
|
228
|
-
const color = laneColors[toLane] ??
|
|
265
|
+
const color = laneColors[toLane] ?? DEFAULT_COLOR_FIRST;
|
|
229
266
|
edges.push({ fromLane: toLane, toLane: myLane, color, type: 'merge-in' });
|
|
230
267
|
} else {
|
|
231
268
|
// Branch-out: the commit spawns a new lane below mid-row.
|
|
232
|
-
const color = laneColors[toLane] ??
|
|
269
|
+
const color = laneColors[toLane] ?? DEFAULT_COLOR_FIRST;
|
|
233
270
|
edges.push({ fromLane: myLane, toLane, color, type: 'branch-out' });
|
|
234
271
|
}
|
|
235
272
|
}
|
|
@@ -245,7 +282,7 @@ export function computeGraphRows(
|
|
|
245
282
|
// at the parent) so it persists as a pass-through to the parent row.
|
|
246
283
|
const hasContinuation = parentIds.length > 0 && (parentLanes[0] === myLane || lanes[myLane] === parentIds[0]);
|
|
247
284
|
|
|
248
|
-
rows.push({ lane: myLane, color: myColor, edges, hasContinuation, hasTopLine });
|
|
285
|
+
rows.push({ lane: myLane, color: myColor, topColor, edges, hasContinuation, hasTopLine });
|
|
249
286
|
}
|
|
250
287
|
|
|
251
288
|
return rows;
|
|
@@ -17,20 +17,21 @@
|
|
|
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 {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
currentHistoryItemRef: ScmHistoryItemRef | undefined;
|
|
25
|
+
currentHistoryItemRemoteRef: ScmHistoryItemRef | undefined;
|
|
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 () => {
|
|
@@ -169,3 +186,221 @@ describe('ScmHistoryGraphModel - pagination', () => {
|
|
|
169
186
|
expect(model.entries).to.have.length(PAGE_SIZE, 'should not grow when all items are duplicates');
|
|
170
187
|
});
|
|
171
188
|
});
|
|
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
|
+
|
|
348
|
+
describe('ScmHistoryGraphModel - ref colors and current item', () => {
|
|
349
|
+
|
|
350
|
+
const mainRef: ScmHistoryItemRef = { id: 'refs/heads/main', name: 'main', revision: 'c2' };
|
|
351
|
+
const originMainRef: ScmHistoryItemRef = { id: 'refs/remotes/origin/main', name: 'origin/main', revision: 'c1' };
|
|
352
|
+
|
|
353
|
+
/** Remote is one commit ahead: c1 (origin/main) → c2 (main = HEAD) → c3. */
|
|
354
|
+
function createBehindProvider(): StubHistoryProvider {
|
|
355
|
+
const provider = new StubHistoryProvider();
|
|
356
|
+
provider.currentHistoryItemRef = mainRef;
|
|
357
|
+
provider.currentHistoryItemRemoteRef = originMainRef;
|
|
358
|
+
provider.pages = [[
|
|
359
|
+
{ id: 'c1', subject: 'remote only', parentIds: ['c2'], references: [originMainRef] },
|
|
360
|
+
{ id: 'c2', subject: 'local head', parentIds: ['c3'], references: [mainRef] },
|
|
361
|
+
{ id: 'c3', subject: 'base', parentIds: [] },
|
|
362
|
+
]];
|
|
363
|
+
return provider;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
it('colors the remote ref tip with the remote ref color', async () => {
|
|
367
|
+
const { model, loadPage } = createModel(createBehindProvider());
|
|
368
|
+
await loadPage();
|
|
369
|
+
expect(model.entries[0].graphRow.color).to.equal(1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('colors the current ref chain with the current ref color', async () => {
|
|
373
|
+
const { model, loadPage } = createModel(createBehindProvider());
|
|
374
|
+
await loadPage();
|
|
375
|
+
expect(model.entries[1].graphRow.color).to.equal(0);
|
|
376
|
+
expect(model.entries[2].graphRow.color).to.equal(0);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('prefers the current ref color when a commit carries both local and remote refs', async () => {
|
|
380
|
+
const provider = new StubHistoryProvider();
|
|
381
|
+
const syncedMain: ScmHistoryItemRef = { ...mainRef, revision: 'c1' };
|
|
382
|
+
const syncedRemote: ScmHistoryItemRef = { ...originMainRef, revision: 'c1' };
|
|
383
|
+
provider.currentHistoryItemRef = syncedMain;
|
|
384
|
+
provider.currentHistoryItemRemoteRef = syncedRemote;
|
|
385
|
+
provider.pages = [[
|
|
386
|
+
{ id: 'c1', subject: 'in sync', parentIds: [], references: [syncedRemote, syncedMain] },
|
|
387
|
+
]];
|
|
388
|
+
const { model, loadPage } = createModel(provider);
|
|
389
|
+
await loadPage();
|
|
390
|
+
expect(model.entries[0].graphRow.color).to.equal(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('marks only the entry at the current ref revision as current', async () => {
|
|
394
|
+
const { model, loadPage } = createModel(createBehindProvider());
|
|
395
|
+
await loadPage();
|
|
396
|
+
expect(model.entries.map(e => e.isCurrent)).to.deep.equal([false, true, false]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('marks no entry as current when the provider has no current ref', async () => {
|
|
400
|
+
const provider = new StubHistoryProvider();
|
|
401
|
+
provider.pages = [makeItems(1, 3)];
|
|
402
|
+
const { model, loadPage } = createModel(provider);
|
|
403
|
+
await loadPage();
|
|
404
|
+
expect(model.entries.every(e => !e.isCurrent)).to.be.true;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -14,25 +14,44 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
17
|
+
import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
|
|
18
18
|
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
|
19
19
|
import { Emitter } from '@theia/core/lib/common/event';
|
|
20
20
|
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
|
21
21
|
import { ScmService } from './scm-service';
|
|
22
|
-
import { ScmHistoryItem, ScmHistoryProvider, ScmHistoryOptions } from './scm-provider';
|
|
22
|
+
import { ScmHistoryItem, ScmHistoryItemRef, ScmHistoryProvider, ScmHistoryOptions } from './scm-provider';
|
|
23
23
|
import { computeGraphRows, GraphRow } from './scm-history-graph-lanes';
|
|
24
|
+
import { ILogger } from '@theia/core';
|
|
25
|
+
import { getRefColorIndex } from './scm-history-graph-helpers';
|
|
26
|
+
import { ScmPreferences } from '../common/scm-preferences';
|
|
24
27
|
|
|
25
28
|
export const PAGE_SIZE = 50;
|
|
26
29
|
|
|
30
|
+
export const ScmHistoryGraphModelProvider = Symbol('ScmHistoryGraphModelProvider');
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the {@link ScmHistoryGraphModel} singleton on first use. The model
|
|
33
|
+
* starts loading history and subscribing to provider events as soon as it is
|
|
34
|
+
* instantiated, so clients that may not need it (e.g. command contributions
|
|
35
|
+
* instantiated at application start) must inject this provider instead of the
|
|
36
|
+
* model itself.
|
|
37
|
+
*/
|
|
38
|
+
export type ScmHistoryGraphModelProvider = () => ScmHistoryGraphModel;
|
|
39
|
+
|
|
27
40
|
export interface HistoryGraphEntry {
|
|
28
41
|
readonly item: ScmHistoryItem;
|
|
29
42
|
readonly graphRow: GraphRow;
|
|
43
|
+
/** True when this item is the commit the current history item ref points at (HEAD). */
|
|
44
|
+
readonly isCurrent: boolean;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
@injectable()
|
|
33
48
|
export class ScmHistoryGraphModel {
|
|
34
49
|
|
|
35
50
|
@inject(ScmService) protected readonly scmService: ScmService;
|
|
51
|
+
@inject(ScmPreferences) protected readonly scmPreferences: ScmPreferences;
|
|
52
|
+
|
|
53
|
+
@inject(ILogger) @named('scm:ScmHistoryGraphModel')
|
|
54
|
+
protected readonly logger: ILogger;
|
|
36
55
|
|
|
37
56
|
protected readonly toDispose = new DisposableCollection();
|
|
38
57
|
protected readonly toDisposeOnProviderChange = new DisposableCollection();
|
|
@@ -42,6 +61,8 @@ export class ScmHistoryGraphModel {
|
|
|
42
61
|
protected _loading = false;
|
|
43
62
|
protected _hasAttemptedLoad = false;
|
|
44
63
|
protected _provider: ScmHistoryProvider | undefined;
|
|
64
|
+
/** Explicitly picked ref ids to filter the graph by; `undefined` = auto (current/remote/base). */
|
|
65
|
+
protected _historyItemRefFilter: string[] | undefined;
|
|
45
66
|
|
|
46
67
|
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
47
68
|
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
@@ -54,6 +75,11 @@ export class ScmHistoryGraphModel {
|
|
|
54
75
|
Disposable.create(() => this.toDisposeOnProviderChange.dispose()),
|
|
55
76
|
this.onDidChangeEmitter,
|
|
56
77
|
this.scmService.onDidChangeSelectedRepository(() => this.refresh()),
|
|
78
|
+
this.scmPreferences.onPreferenceChanged(e => {
|
|
79
|
+
if (e.preferenceName === 'scm.graph.pageSize') {
|
|
80
|
+
this.reload();
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
57
83
|
]);
|
|
58
84
|
this.refresh();
|
|
59
85
|
}
|
|
@@ -67,6 +93,32 @@ export class ScmHistoryGraphModel {
|
|
|
67
93
|
return this._provider;
|
|
68
94
|
}
|
|
69
95
|
|
|
96
|
+
/** The explicitly picked ref ids filtering the graph, or `undefined` in auto mode. */
|
|
97
|
+
get historyItemRefFilter(): readonly string[] | undefined {
|
|
98
|
+
return this._historyItemRefFilter;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sets the ref ids to filter the graph by (`undefined` returns to auto
|
|
103
|
+
* mode) and reloads the graph.
|
|
104
|
+
*/
|
|
105
|
+
setHistoryItemRefFilter(refIds: readonly string[] | undefined): void {
|
|
106
|
+
this._historyItemRefFilter = refIds && refIds.length > 0 ? [...refIds] : undefined;
|
|
107
|
+
this.reload();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether the current history item ref is part of the graph's filter.
|
|
112
|
+
* Always true in auto mode, which includes the current ref.
|
|
113
|
+
*/
|
|
114
|
+
isCurrentRefInFilter(): boolean {
|
|
115
|
+
if (!this._historyItemRefFilter) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const currentId = this._provider?.currentHistoryItemRef?.id;
|
|
119
|
+
return currentId !== undefined && this._historyItemRefFilter.includes(currentId);
|
|
120
|
+
}
|
|
121
|
+
|
|
70
122
|
get entries(): readonly HistoryGraphEntry[] {
|
|
71
123
|
return this._entries;
|
|
72
124
|
}
|
|
@@ -89,13 +141,14 @@ export class ScmHistoryGraphModel {
|
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
refresh(): void {
|
|
92
|
-
this.cancelSource.cancel();
|
|
93
|
-
this.cancelSource = new CancellationTokenSource();
|
|
94
|
-
|
|
95
144
|
this.toDisposeOnProviderChange.dispose();
|
|
96
145
|
|
|
97
146
|
const repo = this.scmService.selectedRepository;
|
|
98
147
|
const hp = repo?.provider.historyProvider;
|
|
148
|
+
if (hp !== this._provider) {
|
|
149
|
+
// The repository changed — a filter picked for the old provider does not apply.
|
|
150
|
+
this._historyItemRefFilter = undefined;
|
|
151
|
+
}
|
|
99
152
|
this._provider = hp;
|
|
100
153
|
|
|
101
154
|
if (this._provider) {
|
|
@@ -103,7 +156,10 @@ export class ScmHistoryGraphModel {
|
|
|
103
156
|
this._provider.onDidChangeCurrentHistoryItemRefs(() => this.refresh())
|
|
104
157
|
);
|
|
105
158
|
this.toDisposeOnProviderChange.push(
|
|
106
|
-
this._provider.onDidChangeHistoryItemRefs(
|
|
159
|
+
this._provider.onDidChangeHistoryItemRefs(e => {
|
|
160
|
+
this.pruneHistoryItemRefFilter(e.removed);
|
|
161
|
+
this.refresh();
|
|
162
|
+
})
|
|
107
163
|
);
|
|
108
164
|
} else if (repo) {
|
|
109
165
|
// historyProvider is not yet available; listen for provider changes
|
|
@@ -113,6 +169,31 @@ export class ScmHistoryGraphModel {
|
|
|
113
169
|
);
|
|
114
170
|
}
|
|
115
171
|
|
|
172
|
+
this.reload();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Drops refs that no longer exist from the explicit filter, so that
|
|
177
|
+
* deleting or renaming a filtered ref does not leave the graph stuck
|
|
178
|
+
* requesting history for it. When the last filtered ref is removed,
|
|
179
|
+
* the filter falls back to auto mode.
|
|
180
|
+
*/
|
|
181
|
+
protected pruneHistoryItemRefFilter(removed: readonly ScmHistoryItemRef[]): void {
|
|
182
|
+
if (!this._historyItemRefFilter || removed.length === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const removedIds = new Set(removed.map(ref => ref.id));
|
|
186
|
+
const remaining = this._historyItemRefFilter.filter(id => !removedIds.has(id));
|
|
187
|
+
if (remaining.length !== this._historyItemRefFilter.length) {
|
|
188
|
+
this._historyItemRefFilter = remaining.length > 0 ? remaining : undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Clears the loaded entries and loads the first page again from the current provider. */
|
|
193
|
+
protected reload(): void {
|
|
194
|
+
this.cancelSource.cancel();
|
|
195
|
+
this.cancelSource = new CancellationTokenSource();
|
|
196
|
+
|
|
116
197
|
this._entries = [];
|
|
117
198
|
this._hasMore = false;
|
|
118
199
|
|
|
@@ -141,10 +222,11 @@ export class ScmHistoryGraphModel {
|
|
|
141
222
|
|
|
142
223
|
const token = this.cancelSource.token;
|
|
143
224
|
try {
|
|
225
|
+
const pageSize = this.pageSize;
|
|
144
226
|
const historyItemRefs = this.getCurrentHistoryItemRefs();
|
|
145
227
|
const options: ScmHistoryOptions = {
|
|
146
228
|
skip: this._entries.length,
|
|
147
|
-
limit:
|
|
229
|
+
limit: pageSize,
|
|
148
230
|
historyItemRefs: historyItemRefs.length > 0 ? historyItemRefs : undefined,
|
|
149
231
|
};
|
|
150
232
|
const items = await this._provider.provideHistoryItems(options, token);
|
|
@@ -154,7 +236,7 @@ export class ScmHistoryGraphModel {
|
|
|
154
236
|
}
|
|
155
237
|
|
|
156
238
|
const fetchedItems: ScmHistoryItem[] = items ?? [];
|
|
157
|
-
this._hasMore = fetchedItems.length >=
|
|
239
|
+
this._hasMore = fetchedItems.length >= pageSize;
|
|
158
240
|
|
|
159
241
|
// Filter out any items already loaded so the graph does not show duplicates.
|
|
160
242
|
const existingIds = new Set(this._entries.map(e => e.item.id));
|
|
@@ -164,15 +246,18 @@ export class ScmHistoryGraphModel {
|
|
|
164
246
|
const graphRows = computeGraphRows(allItems.map(i => ({
|
|
165
247
|
id: i.id,
|
|
166
248
|
parentIds: i.parentIds,
|
|
249
|
+
colorIndex: this.resolveColorIndex(i),
|
|
167
250
|
})));
|
|
168
251
|
|
|
252
|
+
const currentRevision = this._provider.currentHistoryItemRef?.revision;
|
|
169
253
|
this._entries = allItems.map((item, idx) => ({
|
|
170
254
|
item,
|
|
171
255
|
graphRow: graphRows[idx],
|
|
256
|
+
isCurrent: currentRevision !== undefined && item.id === currentRevision,
|
|
172
257
|
}));
|
|
173
258
|
} catch (err) {
|
|
174
259
|
if (!token.isCancellationRequested) {
|
|
175
|
-
|
|
260
|
+
this.logger.error('Failed to load history', err);
|
|
176
261
|
}
|
|
177
262
|
} finally {
|
|
178
263
|
if (!token.isCancellationRequested) {
|
|
@@ -183,12 +268,40 @@ export class ScmHistoryGraphModel {
|
|
|
183
268
|
}
|
|
184
269
|
}
|
|
185
270
|
|
|
271
|
+
/** The configured page size (`scm.graph.pageSize`). */
|
|
272
|
+
protected get pageSize(): number {
|
|
273
|
+
return this.scmPreferences['scm.graph.pageSize'] ?? PAGE_SIZE;
|
|
274
|
+
}
|
|
275
|
+
|
|
186
276
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
277
|
+
* Resolves the ref-based color index of an item from its references,
|
|
278
|
+
* preferring current (0) over remote (1) over base (2). Refs excluded by
|
|
279
|
+
* an explicit filter get no role color, mirroring VS Code's color map.
|
|
280
|
+
*/
|
|
281
|
+
protected resolveColorIndex(item: ScmHistoryItem): number | undefined {
|
|
282
|
+
let result: number | undefined;
|
|
283
|
+
for (const ref of item.references ?? []) {
|
|
284
|
+
if (this._historyItemRefFilter && !this._historyItemRefFilter.includes(ref.id)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const index = getRefColorIndex(ref, this._provider);
|
|
288
|
+
if (index !== undefined && (result === undefined || index < result)) {
|
|
289
|
+
result = index;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Returns the refs to pass to `provideHistoryItems`: the explicitly picked
|
|
297
|
+
* ref ids when a filter is active, otherwise (auto mode) the revisions of
|
|
298
|
+
* the current branch ref, its remote tracking ref, and the merge-base ref.
|
|
299
|
+
* Providers walk history starting from these refs.
|
|
190
300
|
*/
|
|
191
301
|
protected getCurrentHistoryItemRefs(): string[] {
|
|
302
|
+
if (this._historyItemRefFilter) {
|
|
303
|
+
return [...this._historyItemRefFilter];
|
|
304
|
+
}
|
|
192
305
|
if (!this._provider) {
|
|
193
306
|
return [];
|
|
194
307
|
}
|