Fixes #1 Support blame on files outside repo

Replaces blame regex parsing with more robust parser (also use s--incremental instead of --porcelain)
Stops throwing on git blame errors (too many are common)
Fixes issues with Diff with Previous command
Fixes issues with blame explorer code lens -- with previous commits
Fixes issues with compact blame annotations -- skips blank lines
This commit is contained in:
Eric Amodio
2016-09-19 04:11:46 -04:00
parent c69a160ea5
commit ff01054f90
8 changed files with 368 additions and 234 deletions

View File

@@ -0,0 +1,202 @@
'use strict'
import {GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommit, IGitCommitLine, IGitEnricher} from './../git';
import * as moment from 'moment';
import * as path from 'path';
interface IBlameEntry {
sha: string;
line: number;
originalLine: number;
lineCount: number;
author?: string;
authorEmail?: string;
authorDate?: string;
authorTimeZone?: string;
committer?: string;
committerEmail?: string;
committerDate?: string;
committerTimeZone?: string;
previousSha?: string;
previousFileName?: string;
fileName?: string;
summary?: string;
}
export class GitBlameParserEnricher implements IGitEnricher<IGitBlame> {
constructor(public format: GitBlameFormat) {
if (format !== GitBlameFormat.incremental) {
throw new Error(`Invalid blame format=${format}`);
}
}
private _parseEntries(data: string): IBlameEntry[] {
if (!data) return null;
const lines = data.split('\n');
if (!lines.length) return null;
const entries: IBlameEntry[] = [];
let entry: IBlameEntry;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(" ");
if (lineParts.length < 2) {
continue;
}
if (!entry) {
entry = {
sha: lineParts[0].substring(0, 8),
originalLine: parseInt(lineParts[1], 10) - 1,
line: parseInt(lineParts[2], 10) - 1,
lineCount: parseInt(lineParts[3], 10)
};
continue;
}
switch (lineParts[0]) {
case "author":
entry.author = lineParts.slice(1).join(" ").trim();
break;
// case "author-mail":
// entry.authorEmail = lineParts[1].trim();
// break;
case "author-time":
entry.authorDate = lineParts[1];
break;
case "author-tz":
entry.authorTimeZone = lineParts[1];
break;
// case "committer":
// entry.committer = lineParts.slice(1).join(" ").trim();
// break;
// case "committer-mail":
// entry.committerEmail = lineParts[1].trim();
// break;
// case "committer-time":
// entry.committerDate = lineParts[1];
// break;
// case "committer-tz":
// entry.committerTimeZone = lineParts[1];
// break;
case "summary":
entry.summary = lineParts.slice(1).join(" ").trim();
break;
case "previous":
entry.previousSha = lineParts[1].substring(0, 8);
entry.previousFileName = lineParts.slice(2).join(" ");
break;
case "filename":
entry.fileName = lineParts.slice(1).join(" ");
entries.push(entry);
entry = null;
break;
default:
break;
}
}
return entries;
}
enrich(data: string, fileName: string): IGitBlame {
const entries = this._parseEntries(data);
if (!entries) return null;
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, IGitCommit> = new Map();
const lines: Array<IGitCommitLine> = [];
let repoPath: string;
let relativeFileName: string;
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
if (i === 0) {
// Try to get the repoPath from the most recent commit
repoPath = fileName.replace(`/${entry.fileName}`, '');
relativeFileName = path.relative(repoPath, fileName).replace(/\\/g, '/');
}
let commit = commits.get(entry.sha);
if (!commit) {
let author = authors.get(entry.author);
if (!author) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
commit = new GitCommit(repoPath, entry.sha, relativeFileName, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X Z').toDate(), entry.summary);
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
if (entry.previousSha) {
commit.previousSha = entry.previousSha;
commit.previousFileName = entry.previousFileName;
}
commits.set(entry.sha, commit);
}
for (let j = 0, len = entry.lineCount; j < len; j++) {
const line: IGitCommitLine = {
sha: entry.sha,
line: entry.line + j,
originalLine: entry.originalLine + j
}
if (commit.previousSha) {
line.previousSha = commit.previousSha;
}
commit.lines.push(line);
lines[line.line] = line;
}
}
commits.forEach(c => authors.get(c.author).lineCount += c.lines.length);
const sortedAuthors: Map<string, IGitAuthor> = new Map();
const values = Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
// const sortedCommits: Map<string, IGitCommit> = new Map();
// Array.from(commits.values())
// .sort((a, b) => b.date.getTime() - a.date.getTime())
// .forEach(c => sortedCommits.set(c.sha, c));
return <IGitBlame>{
repoPath: repoPath,
authors: sortedAuthors,
// commits: sortedCommits,
commits: commits,
lines: lines
};
}
}

View File

@@ -0,0 +1,92 @@
'use strict'
import {GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommit, IGitCommitLine, IGitEnricher} from './../git';
import * as moment from 'moment';
const blamePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n(?:^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n)?^(.*)$/gm;
const blameLinePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n^(.*)$/gm;
export class GitBlameRegExpEnricher implements IGitEnricher<IGitBlame> {
private _matcher: RegExp;
constructor(public format: GitBlameFormat, private repoPath: string) {
if (format === GitBlameFormat.porcelain) {
this._matcher = blamePorcelainMatcher;
} else if (format === GitBlameFormat.linePorcelain) {
this._matcher = blamePorcelainMatcher;
} else {
throw new Error(`Invalid blame format=${format}`);
}
}
enrich(data: string, fileName: string): IGitBlame {
if (!data) return null;
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, IGitCommit> = new Map();
const lines: Array<IGitCommitLine> = [];
let m: Array<string>;
while ((m = this._matcher.exec(data)) != null) {
const sha = m[1].substring(0, 8);
const previousSha = m[14];
let commit = commits.get(sha);
if (!commit) {
const authorName = m[5].trim();
let author = authors.get(authorName);
if (!author) {
author = {
name: authorName,
lineCount: 0
};
authors.set(authorName, author);
}
commit = new GitCommit(this.repoPath, sha, fileName, authorName, moment(`${m[7]} ${m[8]}`, 'X Z').toDate(), m[13]);
const originalFileName = m[16];
if (!fileName.toLowerCase().endsWith(originalFileName.toLowerCase())) {
commit.originalFileName = originalFileName;
}
if (previousSha) {
commit.previousSha = previousSha.substring(0, 8);
commit.previousFileName = m[15];
}
commits.set(sha, commit);
}
const line: IGitCommitLine = {
sha,
line: parseInt(m[3], 10) - 1,
originalLine: parseInt(m[2], 10) - 1
//code: m[17]
}
if (previousSha) {
line.previousSha = previousSha.substring(0, 8);
}
commit.lines.push(line);
lines.push(line);
}
commits.forEach(c => authors.get(c.author).lineCount += c.lines.length);
const sortedAuthors: Map<string, IGitAuthor> = new Map();
const values = Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
const sortedCommits: Map<string, IGitCommit> = new Map();
Array.from(commits.values())
.sort((a, b) => b.date.getTime() - a.date.getTime())
.forEach(c => sortedCommits.set(c.sha, c));
return <IGitBlame>{
authors: sortedAuthors,
commits: sortedCommits,
lines: lines
};
}
}