Browse Source

Adds support for blaming contents

main
Eric Amodio 7 years ago
parent
commit
eb3f570b66
4 changed files with 156 additions and 16 deletions
  1. +19
    -1
      src/git/git.ts
  2. +1
    -1
      src/git/models/logCommit.ts
  3. +129
    -11
      src/gitService.ts
  4. +7
    -3
      src/system/string.ts

+ 19
- 1
src/git/git.ts View File

@ -240,7 +240,6 @@ export class Git {
params.push(`-L ${options.startLine},${options.endLine}`);
}
// let stdin: Observable<string> | undefined;
let stdin: string | undefined;
if (sha) {
if (Git.isStagedUncommitted(sha)) {
@ -259,6 +258,25 @@ export class Git {
return gitCommand({ cwd: root, stdin: stdin }, ...params, `--`, file);
}
static async blame_contents(repoPath: string | undefined, fileName: string, contents: string, options: { ignoreWhitespace?: boolean, startLine?: number, endLine?: number } = {}) {
const [file, root] = Git.splitPath(fileName, repoPath);
const params = [...defaultBlameParams];
if (options.ignoreWhitespace) {
params.push('-w');
}
if (options.startLine != null && options.endLine != null) {
params.push(`-L ${options.startLine},${options.endLine}`);
}
// Pipe the blame contents to stdin
params.push(`--contents`);
params.push('-');
return gitCommand({ cwd: root, stdin: contents }, ...params, `--`, file);
}
static branch(repoPath: string, options: { all: boolean } = { all: false }) {
const params = [`branch`, `-vv`];
if (options.all) {

+ 1
- 1
src/git/models/logCommit.ts View File

@ -99,7 +99,7 @@ export class GitLogCommit extends GitCommit {
let gravatar = gravatarCache.get(key);
if (gravatar !== undefined) return gravatar;
gravatar = Uri.parse(`https://www.gravatar.com/avatar/${this.email ? Strings.md5(this.email) : '00000000000000000000000000000000'}.jpg?s=22&d=${fallback}`);
gravatar = Uri.parse(`https://www.gravatar.com/avatar/${this.email ? Strings.md5(this.email, 'hex') : '00000000000000000000000000000000'}.jpg?s=22&d=${fallback}`);
// HACK: Monkey patch Uri.toString to avoid the unwanted query string encoding
const originalToStringFn = gravatar.toString;

+ 129
- 11
src/gitService.ts View File

@ -1,5 +1,5 @@
'use strict';
import { Functions, Iterables, Objects, TernarySearchTree } from './system';
import { Functions, Iterables, Objects, Strings, TernarySearchTree } from './system';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
import { configuration, IConfig, IRemotesConfig } from './configuration';
import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
@ -536,7 +536,7 @@ export class GitService extends Disposable {
if (entry && entry.key) {
this._onDidBlameFail.fire(entry.key);
}
return await GitService.emptyPromise as GitBlame;
return GitService.emptyPromise as Promise<GitBlame>;
}
const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
@ -558,7 +558,82 @@ export class GitService extends Disposable {
} as CachedBlame);
this._onDidBlameFail.fire(entry.key);
return await GitService.emptyPromise as GitBlame;
return GitService.emptyPromise as Promise<GitBlame>;
}
return undefined;
}
}
async getBlameForFileContents(uri: GitUri, contents: string): Promise<GitBlame | undefined> {
const key = `blame:${Strings.sha1(contents)}`;
let entry: GitCacheEntry | undefined;
if (this.UseCaching) {
const cacheKey = this.getCacheEntryKey(uri);
entry = this._gitCache.get(cacheKey);
if (entry !== undefined) {
const cachedBlame = entry.get<CachedBlame>(key);
if (cachedBlame !== undefined) {
Logger.log(`getBlameForFileContents[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
return cachedBlame.item;
}
}
Logger.log(`getBlameForFileContents[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
if (entry === undefined) {
entry = new GitCacheEntry(cacheKey);
this._gitCache.set(entry.key, entry);
}
}
else {
Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
}
const promise = this.getBlameForFileContentsCore(uri, contents, entry, key);
if (entry) {
Logger.log(`Add blame cache for '${entry.key}:${key}'`);
entry.set<CachedBlame>(key, {
item: promise
} as CachedBlame);
}
return promise;
}
async getBlameForFileContentsCore(uri: GitUri, contents: string, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
if (!(await this.isTracked(uri))) {
Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
if (entry && entry.key) {
this._onDidBlameFail.fire(entry.key);
}
return GitService.emptyPromise as Promise<GitBlame>;
}
const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
try {
const data = await Git.blame_contents(root, file, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
const blame = GitBlameParser.parse(data, root, file);
return blame;
}
catch (ex) {
// Trap and cache expected blame errors
if (entry) {
const msg = ex && ex.toString();
Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
entry.set<CachedBlame>(key, {
item: GitService.emptyPromise,
errorMessage: msg
} as CachedBlame);
this._onDidBlameFail.fire(entry.key);
return GitService.emptyPromise as Promise<GitBlame>;
}
return undefined;
@ -582,16 +657,17 @@ export class GitService extends Disposable {
if (commit === undefined) return undefined;
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
commit: commit,
line: blameLine
} as GitBlameLine;
}
const lineToBlame = line + 1;
const fileName = uri.fsPath;
try {
const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: line + 1, endLine: line + 1 });
const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
if (blame === undefined) return undefined;
@ -601,7 +677,49 @@ export class GitService extends Disposable {
line: blame.lines[line]
} as GitBlameLine;
}
catch (ex) {
catch {
return undefined;
}
}
async getBlameForLineContents(uri: GitUri, line: number, contents: string): Promise<GitBlameLine | undefined> {
Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`);
if (this.UseCaching) {
const blame = await this.getBlameForFileContents(uri, contents);
if (blame === undefined) return undefined;
let blameLine = blame.lines[line];
if (blameLine === undefined) {
if (blame.lines.length !== line) return undefined;
blameLine = blame.lines[line - 1];
}
const commit = blame.commits.get(blameLine.sha);
if (commit === undefined) return undefined;
return {
author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
commit: commit,
line: blameLine
} as GitBlameLine;
}
const lineToBlame = line + 1;
const fileName = uri.fsPath;
try {
const data = await Git.blame_contents(uri.repoPath, fileName, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
if (blame === undefined) return undefined;
return {
author: Iterables.first(blame.authors.values()),
commit: Iterables.first(blame.commits.values()),
line: blame.lines[line]
} as GitBlameLine;
}
catch {
return undefined;
}
}
@ -618,10 +736,10 @@ export class GitService extends Disposable {
getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${range.end.line}])`);
if (blame.lines.length === 0) return Object.assign({ allLines: blame.lines }, blame);
if (blame.lines.length === 0) return { allLines: blame.lines, ...blame };
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
return Object.assign({ allLines: blame.lines }, blame);
return { allLines: blame.lines, ...blame };
}
const lines = blame.lines.slice(range.start.line, range.end.line + 1);
@ -778,7 +896,7 @@ export class GitService extends Disposable {
errorMessage: msg
} as CachedDiff);
return await GitService.emptyPromise as GitDiff;
return GitService.emptyPromise as Promise<GitDiff>;
}
return undefined;
@ -982,7 +1100,7 @@ export class GitService extends Disposable {
private async getLogForFileCore(repoPath: string | undefined, fileName: string, options: { maxCount?: number, range?: Range, ref?: string, reverse?: boolean, skipMerges?: boolean }, entry: GitCacheEntry | undefined, key: string): Promise<GitLog | undefined> {
if (!(await this.isTracked(fileName, repoPath, options.ref))) {
Logger.log(`Skipping log; '${fileName}' is not tracked`);
return await GitService.emptyPromise as GitLog;
return GitService.emptyPromise as Promise<GitLog>;
}
const [file, root] = Git.splitPath(fileName, repoPath, false);
@ -1015,7 +1133,7 @@ export class GitService extends Disposable {
errorMessage: msg
} as CachedLog);
return await GitService.emptyPromise as GitLog;
return GitService.emptyPromise as Promise<GitLog>;
}
return undefined;

+ 7
- 3
src/system/string.ts View File

@ -1,5 +1,5 @@
'use strict';
import * as crypto from 'crypto';
import { createHash, HexBase64Latin1Encoding } from 'crypto';
export namespace Strings {
const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g;
@ -53,8 +53,8 @@ export namespace Strings {
}
}
export function md5(s: string): string {
return crypto.createHash('md5').update(s).digest('hex');
export function md5(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
return createHash('md5').update(s).digest(encoding);
}
export function pad(s: string, before: number = 0, after: number = 0, padding: string = `\u00a0`) {
@ -105,6 +105,10 @@ export namespace Strings {
return s.replace(illegalCharsForFSRegEx, replacement);
}
export function sha1(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
return createHash('sha1').update(s).digest(encoding);
}
export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026') {
if (!s) return s;

Loading…
Cancel
Save