Skip to content

Commit d46c8a8

Browse files
author
Eric Amodio
committed
Lots of timeline related changes, below
UI: Adds Refresh icon to view title Adds "Load more" entry at the end of the list for paging API: Restructures api around cursors Renames TimelineCursor to generic TimelineOptions for more flexibility Adds paging object to Timeline for clearer paging usage Changes cursors to be strings, and explicit before and after cursors Allows limit to take a cursor, so we can reload current data set Clarifies id and fallback to timestamp Adds reset flag to TimelineChangeEvent for providers to reset caching Git provider: Orders and returns commit date as the timestamp Supports limit of a cursor (using rev-list --count) Stops returning working/index changes when paging Forcably resets cached data when changes are detected (naive for now)
1 parent d226035 commit d46c8a8

11 files changed

Lines changed: 634 additions & 185 deletions

File tree

extensions/git/src/git.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,13 @@ interface MutableRemote extends Remote {
5050
* Log file options.
5151
*/
5252
export interface LogFileOptions {
53-
/** Max number of log entries to retrieve. If not specified, the default is 32. */
54-
readonly maxEntries?: number;
53+
/** Optional. The maximum number of log entries to retrieve. */
54+
readonly maxEntries?: number | string;
55+
/** Optional. The Git sha (hash) to start retrieving log entries from. */
56+
readonly hash?: string;
57+
/** Optional. Specifies whether to start retrieving log entries in reverse order. */
58+
readonly reverse?: boolean;
59+
readonly sortByAuthorDate?: boolean;
5560
}
5661

5762
function parseVersion(raw: string): string {
@@ -817,8 +822,26 @@ export class Repository {
817822
}
818823

819824
async logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
820-
const maxEntries = options?.maxEntries ?? 32;
821-
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath];
825+
const args = ['log', `--format=${COMMIT_FORMAT}`, '-z'];
826+
827+
if (options?.maxEntries && !options?.reverse) {
828+
args.push(`-n${options.maxEntries}`);
829+
}
830+
831+
if (options?.hash) {
832+
// If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking
833+
if (options?.reverse) {
834+
args.push('--reverse', '--ancestry-path', `${options.hash}..HEAD`);
835+
} else {
836+
args.push(options.hash);
837+
}
838+
}
839+
840+
if (options?.sortByAuthorDate) {
841+
args.push('--author-date-order');
842+
}
843+
844+
args.push('--', uri.fsPath);
822845

823846
const result = await this.run(args);
824847
if (result.exitCode) {

extensions/git/src/timelineProvider.ts

Lines changed: 129 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as dayjs from 'dayjs';
77
import * as advancedFormat from 'dayjs/plugin/advancedFormat';
8-
import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineCursor, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode';
8+
import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
99
import { Model } from './model';
1010
import { Repository } from './repository';
1111
import { debounce } from './decorators';
@@ -87,7 +87,7 @@ export class GitTimelineProvider implements TimelineProvider {
8787
this._disposable.dispose();
8888
}
8989

90-
async provideTimeline(uri: Uri, _cursor: TimelineCursor, _token: CancellationToken): Promise<Timeline> {
90+
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
9191
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
9292

9393
const repo = this._model.getRepository(uri);
@@ -112,109 +112,152 @@ export class GitTimelineProvider implements TimelineProvider {
112112

113113
// TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo?
114114

115-
const commits = await repo.logFile(uri);
115+
let limit: number | undefined;
116+
if (typeof options.limit === 'string') {
117+
try {
118+
const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit}..`, '--', uri.fsPath]);
119+
if (!result.exitCode) {
120+
// Ask for 1 more than so we can determine if there are more commits
121+
limit = Number(result.stdout) + 1;
122+
}
123+
}
124+
catch {
125+
limit = undefined;
126+
}
127+
} else {
128+
// If we are not getting everything, ask for 1 more than so we can determine if there are more commits
129+
limit = options.limit === undefined ? undefined : options.limit + 1;
130+
}
131+
132+
133+
const commits = await repo.logFile(uri, {
134+
maxEntries: limit,
135+
hash: options.cursor,
136+
reverse: options.before,
137+
// sortByAuthorDate: true
138+
});
139+
140+
const more = limit === undefined || options.before ? false : commits.length >= limit;
141+
const paging = commits.length ? {
142+
more: more,
143+
cursors: {
144+
before: commits[0]?.hash,
145+
after: commits[commits.length - (more ? 1 : 2)]?.hash
146+
}
147+
} : undefined;
148+
149+
// If we asked for an extra commit, strip it off
150+
if (limit !== undefined && commits.length >= limit) {
151+
commits.splice(commits.length - 1, 1);
152+
}
116153

117154
let dateFormatter: dayjs.Dayjs;
118155
const items = commits.map<GitTimelineItem>(c => {
119-
dateFormatter = dayjs(c.authorDate);
156+
const date = c.commitDate; // c.authorDate
157+
158+
dateFormatter = dayjs(date);
120159

121-
const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, c.authorDate?.getTime() ?? 0, c.hash, 'git:file:commit');
160+
const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
122161
item.iconPath = new (ThemeIcon as any)('git-commit');
123162
item.description = c.authorName;
124163
item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`;
125164
item.command = {
126165
title: 'Open Comparison',
127166
command: 'git.timeline.openDiff',
128-
arguments: [uri, this.id, item]
167+
arguments: [item, uri, this.id]
129168
};
130169

131170
return item;
132171
});
133172

134-
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
135-
if (index) {
136-
const date = this._repoStatusDate ?? new Date();
137-
dateFormatter = dayjs(date);
138-
139-
let status;
140-
switch (index.type) {
141-
case Status.INDEX_MODIFIED:
142-
status = 'Modified';
143-
break;
144-
case Status.INDEX_ADDED:
145-
status = 'Added';
146-
break;
147-
case Status.INDEX_DELETED:
148-
status = 'Deleted';
149-
break;
150-
case Status.INDEX_RENAMED:
151-
status = 'Renamed';
152-
break;
153-
case Status.INDEX_COPIED:
154-
status = 'Copied';
155-
break;
156-
default:
157-
status = '';
158-
break;
173+
if (options.cursor === undefined || options.before) {
174+
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
175+
if (index) {
176+
const date = this._repoStatusDate ?? new Date();
177+
dateFormatter = dayjs(date);
178+
179+
let status;
180+
switch (index.type) {
181+
case Status.INDEX_MODIFIED:
182+
status = 'Modified';
183+
break;
184+
case Status.INDEX_ADDED:
185+
status = 'Added';
186+
break;
187+
case Status.INDEX_DELETED:
188+
status = 'Deleted';
189+
break;
190+
case Status.INDEX_RENAMED:
191+
status = 'Renamed';
192+
break;
193+
case Status.INDEX_COPIED:
194+
status = 'Copied';
195+
break;
196+
default:
197+
status = '';
198+
break;
199+
}
200+
201+
const item = new GitTimelineItem('~', 'HEAD', 'Staged Changes', date.getTime(), 'index', 'git:file:index');
202+
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
203+
item.iconPath = new (ThemeIcon as any)('git-commit');
204+
item.description = 'You';
205+
item.detail = `You \u2014 Index\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
206+
item.command = {
207+
title: 'Open Comparison',
208+
command: 'git.timeline.openDiff',
209+
arguments: [item, uri, this.id]
210+
};
211+
212+
items.splice(0, 0, item);
159213
}
160214

161-
const item = new GitTimelineItem('~', 'HEAD', 'Staged Changes', date.getTime(), 'index', 'git:file:index');
162-
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
163-
item.iconPath = new (ThemeIcon as any)('git-commit');
164-
item.description = 'You';
165-
item.detail = `You \u2014 Index\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
166-
item.command = {
167-
title: 'Open Comparison',
168-
command: 'git.timeline.openDiff',
169-
arguments: [uri, this.id, item]
170-
};
171-
172-
items.push(item);
173-
}
174-
175-
176-
const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
177-
if (working) {
178-
const date = new Date();
179-
dateFormatter = dayjs(date);
180-
181-
let status;
182-
switch (working.type) {
183-
case Status.INDEX_MODIFIED:
184-
status = 'Modified';
185-
break;
186-
case Status.INDEX_ADDED:
187-
status = 'Added';
188-
break;
189-
case Status.INDEX_DELETED:
190-
status = 'Deleted';
191-
break;
192-
case Status.INDEX_RENAMED:
193-
status = 'Renamed';
194-
break;
195-
case Status.INDEX_COPIED:
196-
status = 'Copied';
197-
break;
198-
default:
199-
status = '';
200-
break;
215+
const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
216+
if (working) {
217+
const date = new Date();
218+
dateFormatter = dayjs(date);
219+
220+
let status;
221+
switch (working.type) {
222+
case Status.INDEX_MODIFIED:
223+
status = 'Modified';
224+
break;
225+
case Status.INDEX_ADDED:
226+
status = 'Added';
227+
break;
228+
case Status.INDEX_DELETED:
229+
status = 'Deleted';
230+
break;
231+
case Status.INDEX_RENAMED:
232+
status = 'Renamed';
233+
break;
234+
case Status.INDEX_COPIED:
235+
status = 'Copied';
236+
break;
237+
default:
238+
status = '';
239+
break;
240+
}
241+
242+
const item = new GitTimelineItem('', index ? '~' : 'HEAD', 'Uncommited Changes', date.getTime(), 'working', 'git:file:working');
243+
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
244+
item.iconPath = new (ThemeIcon as any)('git-commit');
245+
item.description = 'You';
246+
item.detail = `You \u2014 Working Tree\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
247+
item.command = {
248+
title: 'Open Comparison',
249+
command: 'git.timeline.openDiff',
250+
arguments: [item, uri, this.id]
251+
};
252+
253+
items.splice(0, 0, item);
201254
}
202-
203-
const item = new GitTimelineItem('', index ? '~' : 'HEAD', 'Uncommited Changes', date.getTime(), 'working', 'git:file:working');
204-
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
205-
item.iconPath = new (ThemeIcon as any)('git-commit');
206-
item.description = 'You';
207-
item.detail = `You \u2014 Working Tree\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
208-
item.command = {
209-
title: 'Open Comparison',
210-
command: 'git.timeline.openDiff',
211-
arguments: [uri, this.id, item]
212-
};
213-
214-
items.push(item);
215255
}
216256

217-
return { items: items };
257+
return {
258+
items: items,
259+
paging: paging
260+
};
218261
}
219262

220263
private onRepositoriesChanged(_repo: Repository) {
@@ -241,6 +284,6 @@ export class GitTimelineProvider implements TimelineProvider {
241284

242285
@debounce(500)
243286
private fireChanged() {
244-
this._onDidChange.fire({});
287+
this._onDidChange.fire({ reset: true });
245288
}
246289
}

src/vs/base/common/iterator.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ export module Iterator {
125125
};
126126
}
127127

128+
export function some<T>(iterator: Iterator<T> | NativeIterator<T>, fn: (t: T) => boolean): boolean {
129+
while (true) {
130+
const element = iterator.next();
131+
if (element.done) {
132+
return false;
133+
}
134+
135+
if (fn(element.value)) {
136+
return true;
137+
}
138+
}
139+
}
140+
128141
export function forEach<T>(iterator: Iterator<T>, fn: (t: T) => void): void {
129142
for (let next = iterator.next(); !next.done; next = iterator.next()) {
130143
fn(next.value);

0 commit comments

Comments
 (0)