Skip to content

Commit

Permalink
Add Git log, globalConfig, and tree diff API
Browse files Browse the repository at this point in the history
  • Loading branch information
Ilya Biryukov committed Dec 11, 2018
1 parent 76fbb84 commit 01292b4
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 33 deletions.
40 changes: 29 additions & 11 deletions extensions/git/src/api/api1.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions extensions/git/src/api/git.d.ts
Expand Up @@ -41,6 +41,7 @@ export interface Commit {
readonly hash: string;
readonly message: string;
readonly parents: string[];
readonly authorEmail?: string | undefined;
}

export interface Submodule {
Expand Down Expand Up @@ -109,6 +110,10 @@ export interface RepositoryUIState {
readonly onDidChange: Event<void>;
}

export interface GitLogOptions {
readonly maxEntries?: number;
}

export interface Repository {

readonly rootUri: Uri;
Expand All @@ -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 }>;
Expand All @@ -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>;
Expand All @@ -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 {
Expand Down
161 changes: 149 additions & 12 deletions extensions/git/src/git.ts
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -450,6 +452,7 @@ export interface Commit {
hash: string;
message: string;
parents: string[];
authorEmail?: string | undefined;
}

export class GitStatusParser {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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');
}

Expand Down

0 comments on commit 01292b4

Please sign in to comment.