mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 10:38:59 +01:00
Adds timeline diff on click and icon support
This commit is contained in:
12
extensions/git/src/api/git.d.ts
vendored
12
extensions/git/src/api/git.d.ts
vendored
@@ -41,7 +41,9 @@ export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorEmail?: string | undefined;
|
||||
readonly authorDate?: Date;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
@@ -119,6 +121,14 @@ export interface LogOptions {
|
||||
readonly maxEntries?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log file options.
|
||||
*/
|
||||
export interface LogFileOptions {
|
||||
/** Max number of log entries to retrieve. If not specified, the default is 32. */
|
||||
readonly maxEntries?: number;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
|
||||
readonly rootUri: Uri;
|
||||
|
||||
@@ -2332,6 +2332,11 @@ export class CommandCenter {
|
||||
return result && result.stash;
|
||||
}
|
||||
|
||||
@command('git.openDiff', { repository: false })
|
||||
async openDiff(uri: Uri, hash: string) {
|
||||
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `${hash}^`));
|
||||
}
|
||||
|
||||
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
|
||||
const result = (...args: any[]) => {
|
||||
let result: Promise<any>;
|
||||
|
||||
@@ -12,10 +12,10 @@ import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
|
||||
import { CancellationToken, Progress } from 'vscode';
|
||||
import { CancellationToken, Progress, Uri } from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, LogFileOptions } from './api/git';
|
||||
import * as byline from 'byline';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
|
||||
@@ -318,7 +318,7 @@ function getGitErrorCode(stderr: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
|
||||
const COMMIT_FORMAT = '%H\n%aN\n%aE\n%at\n%P\n%B';
|
||||
|
||||
export class Git {
|
||||
|
||||
@@ -503,7 +503,9 @@ export interface Commit {
|
||||
hash: string;
|
||||
message: string;
|
||||
parents: string[];
|
||||
authorEmail?: string | undefined;
|
||||
authorDate?: Date;
|
||||
authorName?: string;
|
||||
authorEmail?: string;
|
||||
}
|
||||
|
||||
export class GitStatusParser {
|
||||
@@ -634,14 +636,43 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseGitCommit(raw: string): Commit | null {
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
|
||||
|
||||
const parents = match[3] ? match[3].split(' ') : [];
|
||||
return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
|
||||
export function parseGitCommits(data: string): Commit[] {
|
||||
let commits: Commit[] = [];
|
||||
|
||||
let ref;
|
||||
let name;
|
||||
let email;
|
||||
let date;
|
||||
let parents;
|
||||
let message;
|
||||
let match;
|
||||
|
||||
do {
|
||||
match = commitRegex.exec(data);
|
||||
if (match === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
[, ref, name, email, date, parents, message] = match;
|
||||
|
||||
if (message[message.length - 1] === '\n') {
|
||||
message = message.substr(0, message.length - 1);
|
||||
}
|
||||
|
||||
// Stop excessive memory usage by using substr -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
||||
commits.push({
|
||||
hash: ` ${ref}`.substr(1),
|
||||
message: ` ${message}`.substr(1),
|
||||
parents: parents ? parents.split(' ') : [],
|
||||
authorDate: new Date(Number(date) * 1000),
|
||||
authorName: ` ${name}`.substr(1),
|
||||
authorEmail: ` ${email}`.substr(1)
|
||||
});
|
||||
} while (true);
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
interface LsTreeElement {
|
||||
@@ -760,38 +791,28 @@ export class Repository {
|
||||
|
||||
async log(options?: LogOptions): Promise<Commit[]> {
|
||||
const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
|
||||
const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
|
||||
const args = ['log', '-' + maxEntries, `--format:${COMMIT_FORMAT}`, '-z'];
|
||||
|
||||
const gitResult = await this.run(args);
|
||||
if (gitResult.exitCode) {
|
||||
const result = await this.run(args);
|
||||
if (result.exitCode) {
|
||||
// An empty repo
|
||||
return [];
|
||||
}
|
||||
|
||||
const s = gitResult.stdout;
|
||||
const result: Commit[] = [];
|
||||
let index = 0;
|
||||
while (index < s.length) {
|
||||
let nextIndex = s.indexOf('\x00\x00', index);
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = s.length;
|
||||
}
|
||||
return parseGitCommits(result.stdout);
|
||||
}
|
||||
|
||||
let entry = s.substr(index, nextIndex - index);
|
||||
if (entry.startsWith('\n')) {
|
||||
entry = entry.substring(1);
|
||||
}
|
||||
async logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
|
||||
const maxEntries = options?.maxEntries ?? 32;
|
||||
const args = ['log', `-${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath];
|
||||
|
||||
const commit = parseGitCommit(entry);
|
||||
if (!commit) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.push(commit);
|
||||
index = nextIndex + 2;
|
||||
const result = await this.run(args);
|
||||
if (result.exitCode) {
|
||||
// No file history, e.g. a new file or untracked
|
||||
return [];
|
||||
}
|
||||
|
||||
return result;
|
||||
return parseGitCommits(result.stdout);
|
||||
}
|
||||
|
||||
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
|
||||
@@ -1853,8 +1874,12 @@ export class Repository {
|
||||
}
|
||||
|
||||
async getCommit(ref: string): Promise<Commit> {
|
||||
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
|
||||
return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
|
||||
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]);
|
||||
const commits = parseGitCommits(result.stdout);
|
||||
if (commits.length === 0) {
|
||||
return Promise.reject<Commit>('bad commit format');
|
||||
}
|
||||
return commits[0];
|
||||
}
|
||||
|
||||
async updateSubmodules(paths: string[]): Promise<void> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { GitExtensionImpl } from './api/extension';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
|
||||
import { GitTimelineProvider } from './timelineProvider';
|
||||
|
||||
const deactivateTasks: { (): Promise<any>; }[] = [];
|
||||
|
||||
@@ -82,7 +83,8 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
|
||||
new GitContentProvider(model),
|
||||
new GitFileSystemProvider(model),
|
||||
new GitDecorations(model),
|
||||
new GitProtocolHandler()
|
||||
new GitProtocolHandler(),
|
||||
new GitTimelineProvider(model)
|
||||
);
|
||||
|
||||
await checkGitVersion(info);
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
|
||||
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, LogFileOptions } from './api/git';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
import { debounce, memoize, throttle } from './decorators';
|
||||
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git';
|
||||
@@ -304,6 +304,7 @@ export const enum Operation {
|
||||
Apply = 'Apply',
|
||||
Blame = 'Blame',
|
||||
Log = 'Log',
|
||||
LogFile = 'LogFile',
|
||||
}
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
@@ -868,6 +869,11 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Log, () => this.repository.log(options));
|
||||
}
|
||||
|
||||
logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
|
||||
// TODO: This probably needs per-uri granularity
|
||||
return this.run(Operation.LogFile, () => this.repository.logFile(uri, options));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async status(): Promise<void> {
|
||||
await this.run(Operation.Status);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
|
||||
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
|
||||
import * as assert from 'assert';
|
||||
import { splitInChunks } from '../util';
|
||||
|
||||
@@ -191,42 +191,42 @@ suite('git', () => {
|
||||
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
john.doe@mail.com
|
||||
8e5a374372b8393906c7e380dbb09349c5385554
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
|
||||
test('multiple parent commits', function () {
|
||||
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
john.doe@mail.com
|
||||
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
|
||||
test('no parent commits', function () {
|
||||
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
john.doe@mail.com
|
||||
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: [],
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
53
extensions/git/src/timelineProvider.ts
Normal file
53
extensions/git/src/timelineProvider.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, Disposable, TimelineItem, TimelineProvider, Uri, workspace, ThemeIcon } from 'vscode';
|
||||
import { Model } from './model';
|
||||
|
||||
export class GitTimelineProvider implements TimelineProvider {
|
||||
readonly source = 'git-history';
|
||||
readonly sourceDescription = 'Git History';
|
||||
|
||||
private _disposable: Disposable;
|
||||
|
||||
constructor(private readonly _model: Model) {
|
||||
this._disposable = workspace.registerTimelineProvider('*', this);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
async provideTimeline(uri: Uri, _since: number, _token: CancellationToken): Promise<TimelineItem[]> {
|
||||
const repo = this._model.getRepository(uri);
|
||||
if (!repo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const commits = await repo.logFile(uri, { maxEntries: 10 });
|
||||
return commits.map<TimelineItem>(c => {
|
||||
let message = c.message;
|
||||
|
||||
const index = message.indexOf('\n');
|
||||
if (index !== -1) {
|
||||
message = `${message.substring(0, index)} \u2026`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: c.hash,
|
||||
date: c.authorDate?.getTime() ?? 0,
|
||||
iconPath: new ThemeIcon('git-commit'),
|
||||
label: message,
|
||||
description: `${c.authorName} (${c.authorEmail}) \u2022 ${c.hash.substr(0, 8)}`,
|
||||
detail: `${c.authorName} (${c.authorEmail})\n${c.authorDate}\n\n${c.message}`,
|
||||
command: {
|
||||
title: 'Open Diff',
|
||||
command: 'git.openDiff',
|
||||
arguments: [uri, c.hash]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user