Skip to content

Commit eb3f570

Browse files
committed
Adds support for blaming contents
1 parent 9dfed28 commit eb3f570

File tree

4 files changed

+156
-16
lines changed

4 files changed

+156
-16
lines changed

src/git/git.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ export class Git {
240240
params.push(`-L ${options.startLine},${options.endLine}`);
241241
}
242242

243-
// let stdin: Observable<string> | undefined;
244243
let stdin: string | undefined;
245244
if (sha) {
246245
if (Git.isStagedUncommitted(sha)) {
@@ -259,6 +258,25 @@ export class Git {
259258
return gitCommand({ cwd: root, stdin: stdin }, ...params, `--`, file);
260259
}
261260

261+
static async blame_contents(repoPath: string | undefined, fileName: string, contents: string, options: { ignoreWhitespace?: boolean, startLine?: number, endLine?: number } = {}) {
262+
const [file, root] = Git.splitPath(fileName, repoPath);
263+
264+
const params = [...defaultBlameParams];
265+
266+
if (options.ignoreWhitespace) {
267+
params.push('-w');
268+
}
269+
if (options.startLine != null && options.endLine != null) {
270+
params.push(`-L ${options.startLine},${options.endLine}`);
271+
}
272+
273+
// Pipe the blame contents to stdin
274+
params.push(`--contents`);
275+
params.push('-');
276+
277+
return gitCommand({ cwd: root, stdin: contents }, ...params, `--`, file);
278+
}
279+
262280
static branch(repoPath: string, options: { all: boolean } = { all: false }) {
263281
const params = [`branch`, `-vv`];
264282
if (options.all) {

src/git/models/logCommit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class GitLogCommit extends GitCommit {
9999
let gravatar = gravatarCache.get(key);
100100
if (gravatar !== undefined) return gravatar;
101101

102-
gravatar = Uri.parse(`https://www.gravatar.com/avatar/${this.email ? Strings.md5(this.email) : '00000000000000000000000000000000'}.jpg?s=22&d=${fallback}`);
102+
gravatar = Uri.parse(`https://www.gravatar.com/avatar/${this.email ? Strings.md5(this.email, 'hex') : '00000000000000000000000000000000'}.jpg?s=22&d=${fallback}`);
103103

104104
// HACK: Monkey patch Uri.toString to avoid the unwanted query string encoding
105105
const originalToStringFn = gravatar.toString;

src/gitService.ts

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
import { Functions, Iterables, Objects, TernarySearchTree } from './system';
2+
import { Functions, Iterables, Objects, Strings, TernarySearchTree } from './system';
33
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
44
import { configuration, IConfig, IRemotesConfig } from './configuration';
55
import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
@@ -536,7 +536,7 @@ export class GitService extends Disposable {
536536
if (entry && entry.key) {
537537
this._onDidBlameFail.fire(entry.key);
538538
}
539-
return await GitService.emptyPromise as GitBlame;
539+
return GitService.emptyPromise as Promise<GitBlame>;
540540
}
541541

542542
const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
@@ -558,7 +558,82 @@ export class GitService extends Disposable {
558558
} as CachedBlame);
559559

560560
this._onDidBlameFail.fire(entry.key);
561-
return await GitService.emptyPromise as GitBlame;
561+
return GitService.emptyPromise as Promise<GitBlame>;
562+
}
563+
564+
return undefined;
565+
}
566+
}
567+
568+
async getBlameForFileContents(uri: GitUri, contents: string): Promise<GitBlame | undefined> {
569+
const key = `blame:${Strings.sha1(contents)}`;
570+
571+
let entry: GitCacheEntry | undefined;
572+
if (this.UseCaching) {
573+
const cacheKey = this.getCacheEntryKey(uri);
574+
entry = this._gitCache.get(cacheKey);
575+
576+
if (entry !== undefined) {
577+
const cachedBlame = entry.get<CachedBlame>(key);
578+
if (cachedBlame !== undefined) {
579+
Logger.log(`getBlameForFileContents[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
580+
return cachedBlame.item;
581+
}
582+
}
583+
584+
Logger.log(`getBlameForFileContents[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
585+
586+
if (entry === undefined) {
587+
entry = new GitCacheEntry(cacheKey);
588+
this._gitCache.set(entry.key, entry);
589+
}
590+
}
591+
else {
592+
Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
593+
}
594+
595+
const promise = this.getBlameForFileContentsCore(uri, contents, entry, key);
596+
597+
if (entry) {
598+
Logger.log(`Add blame cache for '${entry.key}:${key}'`);
599+
600+
entry.set<CachedBlame>(key, {
601+
item: promise
602+
} as CachedBlame);
603+
}
604+
605+
return promise;
606+
}
607+
608+
async getBlameForFileContentsCore(uri: GitUri, contents: string, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
609+
if (!(await this.isTracked(uri))) {
610+
Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
611+
if (entry && entry.key) {
612+
this._onDidBlameFail.fire(entry.key);
613+
}
614+
return GitService.emptyPromise as Promise<GitBlame>;
615+
}
616+
617+
const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
618+
619+
try {
620+
const data = await Git.blame_contents(root, file, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
621+
const blame = GitBlameParser.parse(data, root, file);
622+
return blame;
623+
}
624+
catch (ex) {
625+
// Trap and cache expected blame errors
626+
if (entry) {
627+
const msg = ex && ex.toString();
628+
Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
629+
630+
entry.set<CachedBlame>(key, {
631+
item: GitService.emptyPromise,
632+
errorMessage: msg
633+
} as CachedBlame);
634+
635+
this._onDidBlameFail.fire(entry.key);
636+
return GitService.emptyPromise as Promise<GitBlame>;
562637
}
563638

564639
return undefined;
@@ -582,16 +657,17 @@ export class GitService extends Disposable {
582657
if (commit === undefined) return undefined;
583658

584659
return {
585-
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
660+
author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
586661
commit: commit,
587662
line: blameLine
588663
} as GitBlameLine;
589664
}
590665

666+
const lineToBlame = line + 1;
591667
const fileName = uri.fsPath;
592668

593669
try {
594-
const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: line + 1, endLine: line + 1 });
670+
const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
595671
const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
596672
if (blame === undefined) return undefined;
597673

@@ -601,7 +677,49 @@ export class GitService extends Disposable {
601677
line: blame.lines[line]
602678
} as GitBlameLine;
603679
}
604-
catch (ex) {
680+
catch {
681+
return undefined;
682+
}
683+
}
684+
685+
async getBlameForLineContents(uri: GitUri, line: number, contents: string): Promise<GitBlameLine | undefined> {
686+
Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`);
687+
688+
if (this.UseCaching) {
689+
const blame = await this.getBlameForFileContents(uri, contents);
690+
if (blame === undefined) return undefined;
691+
692+
let blameLine = blame.lines[line];
693+
if (blameLine === undefined) {
694+
if (blame.lines.length !== line) return undefined;
695+
blameLine = blame.lines[line - 1];
696+
}
697+
698+
const commit = blame.commits.get(blameLine.sha);
699+
if (commit === undefined) return undefined;
700+
701+
return {
702+
author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
703+
commit: commit,
704+
line: blameLine
705+
} as GitBlameLine;
706+
}
707+
708+
const lineToBlame = line + 1;
709+
const fileName = uri.fsPath;
710+
711+
try {
712+
const data = await Git.blame_contents(uri.repoPath, fileName, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
713+
const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
714+
if (blame === undefined) return undefined;
715+
716+
return {
717+
author: Iterables.first(blame.authors.values()),
718+
commit: Iterables.first(blame.commits.values()),
719+
line: blame.lines[line]
720+
} as GitBlameLine;
721+
}
722+
catch {
605723
return undefined;
606724
}
607725
}
@@ -618,10 +736,10 @@ export class GitService extends Disposable {
618736
getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
619737
Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${range.end.line}])`);
620738

621-
if (blame.lines.length === 0) return Object.assign({ allLines: blame.lines }, blame);
739+
if (blame.lines.length === 0) return { allLines: blame.lines, ...blame };
622740

623741
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
624-
return Object.assign({ allLines: blame.lines }, blame);
742+
return { allLines: blame.lines, ...blame };
625743
}
626744

627745
const lines = blame.lines.slice(range.start.line, range.end.line + 1);
@@ -778,7 +896,7 @@ export class GitService extends Disposable {
778896
errorMessage: msg
779897
} as CachedDiff);
780898

781-
return await GitService.emptyPromise as GitDiff;
899+
return GitService.emptyPromise as Promise<GitDiff>;
782900
}
783901

784902
return undefined;
@@ -982,7 +1100,7 @@ export class GitService extends Disposable {
9821100
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> {
9831101
if (!(await this.isTracked(fileName, repoPath, options.ref))) {
9841102
Logger.log(`Skipping log; '${fileName}' is not tracked`);
985-
return await GitService.emptyPromise as GitLog;
1103+
return GitService.emptyPromise as Promise<GitLog>;
9861104
}
9871105

9881106
const [file, root] = Git.splitPath(fileName, repoPath, false);
@@ -1015,7 +1133,7 @@ export class GitService extends Disposable {
10151133
errorMessage: msg
10161134
} as CachedLog);
10171135

1018-
return await GitService.emptyPromise as GitLog;
1136+
return GitService.emptyPromise as Promise<GitLog>;
10191137
}
10201138

10211139
return undefined;

src/system/string.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
import * as crypto from 'crypto';
2+
import { createHash, HexBase64Latin1Encoding } from 'crypto';
33

44
export namespace Strings {
55
const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g;
@@ -53,8 +53,8 @@ export namespace Strings {
5353
}
5454
}
5555

56-
export function md5(s: string): string {
57-
return crypto.createHash('md5').update(s).digest('hex');
56+
export function md5(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
57+
return createHash('md5').update(s).digest(encoding);
5858
}
5959

6060
export function pad(s: string, before: number = 0, after: number = 0, padding: string = `\u00a0`) {
@@ -105,6 +105,10 @@ export namespace Strings {
105105
return s.replace(illegalCharsForFSRegEx, replacement);
106106
}
107107

108+
export function sha1(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
109+
return createHash('sha1').update(s).digest(encoding);
110+
}
111+
108112
export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026') {
109113
if (!s) return s;
110114

0 commit comments

Comments
 (0)