mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 04:23:32 +01:00
Add Git log, globalConfig, and tree diff API
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, GitLogOptions } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode';
|
||||
import { mapEvent } from '../util';
|
||||
|
||||
@@ -76,6 +76,10 @@ export class ApiRepository implements Repository {
|
||||
return this._repository.setConfig(key, value);
|
||||
}
|
||||
|
||||
getGlobalConfig(key: string): Promise<string> {
|
||||
return this._repository.getGlobalConfig(key);
|
||||
}
|
||||
|
||||
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number; }> {
|
||||
return this._repository.getObjectDetails(treeish, path);
|
||||
}
|
||||
@@ -104,28 +108,38 @@ export class ApiRepository implements Repository {
|
||||
return this._repository.diff(cached);
|
||||
}
|
||||
|
||||
diffWithHEAD(path: string): Promise<string> {
|
||||
return this._repository.diffWithHEAD(path);
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
return path ? this._repository.diffWithHEAD(path) : this._repository.diffWithHEAD();
|
||||
}
|
||||
|
||||
diffWith(ref: string, path: string): Promise<string> {
|
||||
return this._repository.diffWith(ref, path);
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
return path ? this._repository.diffWith(ref, path) : this._repository.diffWith(ref);
|
||||
}
|
||||
|
||||
diffIndexWithHEAD(path: string): Promise<string> {
|
||||
return this._repository.diffIndexWithHEAD(path);
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
return path ? this._repository.diffIndexWithHEAD(path) : this._repository.diffIndexWithHEAD();
|
||||
}
|
||||
|
||||
diffIndexWith(ref: string, path: string): Promise<string> {
|
||||
return this._repository.diffIndexWith(ref, path);
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
return path ? this._repository.diffIndexWith(ref, path) : this._repository.diffIndexWith(ref);
|
||||
}
|
||||
|
||||
diffBlobs(object1: string, object2: string): Promise<string> {
|
||||
return this._repository.diffBlobs(object1, object2);
|
||||
}
|
||||
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
|
||||
return this._repository.diffBetween(ref1, ref2, path);
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
|
||||
return path ? this._repository.diffBetween(ref1, ref2, path) : this._repository.diffBetween(ref1, ref2);
|
||||
}
|
||||
|
||||
hashObject(data: string): Promise<string> {
|
||||
@@ -179,6 +193,10 @@ export class ApiRepository implements Repository {
|
||||
push(remoteName?: string, branchName?: string, setUpstream: boolean = false): Promise<void> {
|
||||
return this._repository.pushTo(remoteName, branchName, setUpstream);
|
||||
}
|
||||
|
||||
getLog(options?: GitLogOptions): Promise<Commit[]> {
|
||||
return this._repository.getLog(options);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiGit implements Git {
|
||||
|
||||
13
extensions/git/src/api/git.d.ts
vendored
13
extensions/git/src/api/git.d.ts
vendored
@@ -41,6 +41,7 @@ export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorEmail?: string | undefined;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
@@ -109,6 +110,10 @@ export interface RepositoryUIState {
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
export interface GitLogOptions {
|
||||
readonly maxEntries?: number;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
|
||||
readonly rootUri: Uri;
|
||||
@@ -119,6 +124,7 @@ export interface Repository {
|
||||
getConfigs(): Promise<{ key: string; value: string; }[]>;
|
||||
getConfig(key: string): Promise<string>;
|
||||
setConfig(key: string, value: string): Promise<string>;
|
||||
getGlobalConfig(key: string): Promise<string>;
|
||||
|
||||
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
|
||||
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
|
||||
@@ -130,11 +136,16 @@ export interface Repository {
|
||||
|
||||
apply(patch: string, reverse?: boolean): Promise<void>;
|
||||
diff(cached?: boolean): Promise<string>;
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffBlobs(object1: string, object2: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
|
||||
hashObject(data: string): Promise<string>;
|
||||
@@ -155,6 +166,8 @@ export interface Repository {
|
||||
fetch(remote?: string, ref?: string): Promise<void>;
|
||||
pull(): Promise<void>;
|
||||
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>;
|
||||
|
||||
getLog(options?: GitLogOptions): Promise<Commit[]>;
|
||||
}
|
||||
|
||||
export interface API {
|
||||
|
||||
@@ -12,9 +12,9 @@ import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util';
|
||||
import { CancellationToken } from 'vscode';
|
||||
import { CancellationToken, Uri } from 'vscode';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes } from './api/git';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, GitLogOptions, Change, Status } from './api/git';
|
||||
|
||||
const readfile = denodeify<string, string | null, string>(fs.readFile);
|
||||
|
||||
@@ -311,6 +311,8 @@ function getGitErrorCode(stderr: string): string | undefined {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
|
||||
|
||||
export class Git {
|
||||
|
||||
readonly path: string;
|
||||
@@ -450,6 +452,7 @@ export interface Commit {
|
||||
hash: string;
|
||||
message: string;
|
||||
parents: string[];
|
||||
authorEmail?: string | undefined;
|
||||
}
|
||||
|
||||
export class GitStatusParser {
|
||||
@@ -581,13 +584,13 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
}
|
||||
|
||||
export function parseGitCommit(raw: string): Commit | null {
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n([^]*)$/m.exec(raw.trim());
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n(.*)\n([^]*)$/m.exec(raw.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parents = match[2] ? match[2].split(' ') : [];
|
||||
return { hash: match[1], message: match[3], parents };
|
||||
const parents = match[3] ? match[3].split(' ') : [];
|
||||
return { hash: match[1], message: match[4], parents, authorEmail: match[2] };
|
||||
}
|
||||
|
||||
interface LsTreeElement {
|
||||
@@ -697,6 +700,38 @@ export class Repository {
|
||||
});
|
||||
}
|
||||
|
||||
async getLog(options?: GitLogOptions): Promise<Commit[]> {
|
||||
const args = ['log'];
|
||||
if (options) {
|
||||
if (typeof options.maxEntries === 'number' && options.maxEntries > 0) {
|
||||
args.push('-' + options.maxEntries);
|
||||
}
|
||||
}
|
||||
|
||||
args.push(`--pretty=format:${COMMIT_FORMAT}%x00%x00`);
|
||||
const gitResult = await this.run(args);
|
||||
if (gitResult.exitCode) {
|
||||
// An empty repo.
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = gitResult.stdout.split('\x00\x00');
|
||||
const result: Commit[] = [];
|
||||
for (let entry of entries) {
|
||||
if (entry.startsWith('\n')) {
|
||||
entry = entry.substring(1);
|
||||
}
|
||||
const commit = parseGitCommit(entry);
|
||||
if (!commit) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.push(commit);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
|
||||
const stdout = await this.buffer(object);
|
||||
|
||||
@@ -851,25 +886,41 @@ export class Repository {
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async diffWithHEAD(path: string): Promise<string> {
|
||||
async diffWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
if (!path) {
|
||||
return await this.diffFiles(false);
|
||||
}
|
||||
|
||||
const args = ['diff', '--', path];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async diffWith(ref: string, path: string): Promise<string> {
|
||||
async diffWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
if (!path) {
|
||||
return await this.diffFiles(false, ref);
|
||||
}
|
||||
|
||||
const args = ['diff', ref, '--', path];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async diffIndexWithHEAD(path: string): Promise<string> {
|
||||
async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
if (!path) {
|
||||
return await this.diffFiles(true);
|
||||
}
|
||||
|
||||
const args = ['diff', '--cached', '--', path];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async diffIndexWith(ref: string, path: string): Promise<string> {
|
||||
async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
if (!path) {
|
||||
return await this.diffFiles(true, ref);
|
||||
}
|
||||
|
||||
const args = ['diff', '--cached', ref, '--', path];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
@@ -881,13 +932,99 @@ export class Repository {
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
|
||||
const args = ['diff', `${ref1}...${ref2}`, '--', path];
|
||||
async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
|
||||
const range = `${ref1}...${ref2}`;
|
||||
if (!path) {
|
||||
return await this.diffFiles(false, range);
|
||||
}
|
||||
|
||||
const args = ['diff', range, '--', path];
|
||||
const result = await this.run(args);
|
||||
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
|
||||
const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
|
||||
if (cached) {
|
||||
args.push('--cached');
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
args.push(ref);
|
||||
}
|
||||
|
||||
const gitResult = await this.run(args);
|
||||
if (gitResult.exitCode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = gitResult.stdout.split('\x00');
|
||||
let index = 0;
|
||||
const result: Change[] = [];
|
||||
|
||||
entriesLoop:
|
||||
while (index < entries.length - 1) {
|
||||
const change = entries[index++];
|
||||
const resourcePath = entries[index++];
|
||||
if (!change || !resourcePath) {
|
||||
break;
|
||||
}
|
||||
|
||||
const originalUri = Uri.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
|
||||
let status: Status = Status.UNTRACKED;
|
||||
|
||||
// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
|
||||
switch (change[0]) {
|
||||
case 'M':
|
||||
status = Status.MODIFIED;
|
||||
break;
|
||||
|
||||
case 'A':
|
||||
status = Status.INDEX_ADDED;
|
||||
break;
|
||||
|
||||
case 'D':
|
||||
status = Status.DELETED;
|
||||
break;
|
||||
|
||||
// Rename contains two paths, the second one is what the file is renamed/copied to.
|
||||
case 'R':
|
||||
if (index >= entries.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const newPath = entries[index++];
|
||||
if (!newPath) {
|
||||
break;
|
||||
}
|
||||
|
||||
const uri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
|
||||
result.push({
|
||||
uri,
|
||||
renameUri: uri,
|
||||
originalUri,
|
||||
status: Status.INDEX_RENAMED
|
||||
});
|
||||
|
||||
continue;
|
||||
|
||||
default:
|
||||
// Unknown status
|
||||
break entriesLoop;
|
||||
}
|
||||
|
||||
result.push({
|
||||
status,
|
||||
originalUri,
|
||||
uri: originalUri,
|
||||
renameUri: originalUri,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getMergeBase(ref1: string, ref2: string): Promise<string> {
|
||||
const args = ['merge-base', ref1, ref2];
|
||||
const result = await this.run(args);
|
||||
@@ -1529,7 +1666,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
async getCommit(ref: string): Promise<Commit> {
|
||||
const result = await this.run(['show', '-s', '--format=%H\n%P\n%B', ref]);
|
||||
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
|
||||
return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as path from 'path';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as fs from 'fs';
|
||||
import { StatusBarCommands } from './statusbar';
|
||||
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status } from './api/git';
|
||||
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status, GitLogOptions, Change } from './api/git';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
@@ -295,7 +295,8 @@ export const enum Operation {
|
||||
GetObjectDetails = 'GetObjectDetails',
|
||||
SubmoduleUpdate = 'SubmoduleUpdate',
|
||||
RebaseContinue = 'RebaseContinue',
|
||||
Apply = 'Apply'
|
||||
Apply = 'Apply',
|
||||
Log = 'Log',
|
||||
}
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
@@ -697,10 +698,18 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Config, () => this.repository.config('local', key));
|
||||
}
|
||||
|
||||
getGlobalConfig(key: string): Promise<string> {
|
||||
return this.run(Operation.Config, () => this.repository.config('global', key));
|
||||
}
|
||||
|
||||
setConfig(key: string, value: string): Promise<string> {
|
||||
return this.run(Operation.Config, () => this.repository.config('local', key, value));
|
||||
}
|
||||
|
||||
getLog(options?: GitLogOptions): Promise<Commit[]> {
|
||||
return this.run(Operation.Log, () => this.repository.getLog(options));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async status(): Promise<void> {
|
||||
await this.run(Operation.Status);
|
||||
@@ -710,19 +719,27 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Diff, () => this.repository.diff(cached));
|
||||
}
|
||||
|
||||
diffWithHEAD(path: string): Promise<string> {
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
|
||||
}
|
||||
|
||||
diffWith(ref: string, path: string): Promise<string> {
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffWith(ref, path));
|
||||
}
|
||||
|
||||
diffIndexWithHEAD(path: string): Promise<string> {
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path));
|
||||
}
|
||||
|
||||
diffIndexWith(ref: string, path: string): Promise<string> {
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path));
|
||||
}
|
||||
|
||||
@@ -730,7 +747,9 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2));
|
||||
}
|
||||
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
|
||||
}
|
||||
|
||||
|
||||
@@ -177,37 +177,43 @@ suite('git', () => {
|
||||
suite('parseGitCommit', () => {
|
||||
test('single parent commit', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
john.doe@mail.com
|
||||
8e5a374372b8393906c7e380dbb09349c5385554
|
||||
This is a commit message.`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), {
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554']
|
||||
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.`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), {
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217']
|
||||
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.`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), {
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: []
|
||||
parents: [],
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user