Browse Source

Fixes #255 - Messages with empty lines are truncated

Rewrite of the log and stash parser -- more robust and perf
main
Eric Amodio 7 years ago
parent
commit
363a0e93dc
3 changed files with 174 additions and 229 deletions
  1. +3
    -2
      src/git/git.ts
  2. +87
    -115
      src/git/parsers/logParser.ts
  3. +84
    -112
      src/git/parsers/stashParser.ts

+ 3
- 2
src/git/git.ts View File

@ -23,8 +23,9 @@ export * from './remotes/provider';
let git: IGit;
const defaultBlameParams = ['blame', '--root', '--incremental'];
const defaultLogParams = ['log', '--name-status', '--full-history', '-M', '--format=%H -%nauthor %an%nauthor-mail %ae%nauthor-date %at%nparents %P%nsummary %B%nfilename ?'];
const defaultStashParams = ['stash', 'list', '--name-status', '--full-history', '-M', '--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?'];
// Using %x00 codes because some shells seem to try to expand things if not
const defaultLogParams = ['log', '--name-status', '--full-history', '-M', '--format=%x3c%x2ff%x3e%n%x3cr%x3e %H%n%x3ca%x3e %an%n%x3ce%x3e %ae%n%x3cd%x3e %at%n%x3cp%x3e %P%n%x3cs%x3e%n%B%x3c%x2fs%x3e%n%x3cf%x3e'];
const defaultStashParams = ['stash', 'list', '--name-status', '--full-history', '-M', '--format=%x3c%x2ff%x3e%n%x3cr%x3e %H%n%x3cd%x3e %at%n%x3cl%x3e %gd%n%x3cs%x3e%n%B%x3c%x2fs%x3e%n%x3cf%x3e'];
const GitWarnings = [
/Not a git repository/,

+ 87
- 115
src/git/parsers/logParser.ts View File

@ -6,11 +6,11 @@ import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatu
import * as path from 'path';
interface LogEntry {
sha: string;
ref?: string;
author: string;
authorDate?: string;
authorEmail?: string;
author?: string;
date?: string;
email?: string;
parentShas?: string[];
@ -24,142 +24,105 @@ interface LogEntry {
}
const diffRegex = /diff --git a\/(.*) b\/(.*)/;
const emptyEntry: LogEntry = {};
export class GitLogParser {
static parse(data: string, type: GitCommitType, repoPath: string | undefined, fileName: string | undefined, sha: string | undefined, maxCount: number | undefined, reverse: boolean, range: Range | undefined): GitLog | undefined {
if (!data) return undefined;
const authors: Map<string, GitAuthor> = new Map();
const commits: Map<string, GitLogCommit> = new Map();
let relativeFileName: string;
let recentCommit: GitLogCommit | undefined = undefined;
if (repoPath !== undefined) {
repoPath = Strings.normalizePath(repoPath);
}
let entry: LogEntry | undefined = undefined;
let entry: LogEntry = emptyEntry;
let line: string | undefined = undefined;
let lineParts: string[];
let next: IteratorResult<string> | undefined = undefined;
let token: number;
let i = 0;
let first = true;
let skip = false;
const lines = Strings.lines(data);
const lines = Strings.lines(data + '\n</f>');
// Skip the first line since it will always be </f>
let next = lines.next();
if (next.done) return undefined;
if (repoPath !== undefined) {
repoPath = Strings.normalizePath(repoPath);
}
const authors: Map<string, GitAuthor> = new Map();
const commits: Map<string, GitLogCommit> = new Map();
while (true) {
if (!skip) {
next = lines.next();
if (next.done) break;
next = lines.next();
if (next.done) break;
line = next.value;
}
else {
skip = false;
}
line = next.value;
// Since log --reverse doesn't properly honor a max count -- enforce it here
if (reverse && maxCount && (i >= maxCount)) break;
lineParts = line!.split(' ');
if (lineParts.length < 2) continue;
// <<1-char token>> <data>
// e.g. <r> bd1452a2dc
token = line.charCodeAt(1);
if (entry === undefined) {
if (!Git.shaRegex.test(lineParts[0])) continue;
entry = {
sha: lineParts[0]
} as LogEntry;
continue;
}
switch (token) {
case 114: // 'r': // ref
entry = {
ref: line.substring(4)
};
break;
switch (lineParts[0]) {
case 'author':
entry.author = Git.isUncommitted(entry.sha)
case 97: // 'a': // author
entry.author = Git.isUncommitted(entry.ref)
? 'You'
: lineParts.slice(1).join(' ').trim();
: line.substring(4);
break;
case 'author-mail':
entry.authorEmail = lineParts.slice(1).join(' ').trim();
case 101: // 'e': // author-mail
entry.email = line.substring(4);
break;
case 'author-date':
entry.authorDate = lineParts[1];
case 100: // 'd': // author-date
entry.date = line.substring(4);
break;
case 'parents':
entry.parentShas = lineParts.slice(1);
case 112: // 'p': // parents
entry.parentShas = line.substring(4).split(' ');
break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
case 115: // 's': // summary
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
if (!line) break;
if (line === '</s>') break;
if (line === 'filename ?') {
skip = true;
break;
if (entry.summary === undefined) {
entry.summary = line;
}
else {
entry.summary += `\n${line}`;
}
entry.summary += `\n${line}`;
}
break;
case 'filename':
if (type === GitCommitType.Branch) {
case 102: // 'f': // files
// Skip the blank line git adds before the files
next = lines.next();
if (next.done || next.value === '</f>') break;
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
if (line === '</f>') break;
// If the next line isn't blank, make sure it isn't starting a new commit or s git warning
if (line && (Git.shaRegex.test(line) || line.startsWith('warning:'))) {
skip = true;
continue;
}
let diff = false;
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
lineParts = line.split(' ');
// make sure the line isn't starting a new commit or s git warning
if (Git.shaRegex.test(lineParts[0]) || line.startsWith('warning:')) {
skip = true;
break;
}
if (diff) continue;
if (lineParts[0] === 'diff') {
diff = true;
const matches = diffRegex.exec(line);
if (matches != null) {
entry.fileName = matches[1];
const originalFileName = matches[2];
if (entry.fileName !== originalFileName) {
entry.originalFileName = originalFileName;
}
}
continue;
}
if (entry.fileStatuses == null) {
entry.fileStatuses = [];
}
if (line.startsWith('warning:')) continue;
if (type === GitCommitType.Branch) {
const status = {
status: line[0] as GitStatusFileStatus,
fileName: line.substring(1),
@ -168,28 +131,41 @@ export class GitLogParser {
this.parseFileName(status);
if (status.fileName) {
if (entry.fileStatuses === undefined) {
entry.fileStatuses = [];
}
entry.fileStatuses.push(status);
}
}
else if (line.startsWith('diff')) {
const matches = diffRegex.exec(line);
if (matches != null) {
entry.fileName = matches[1];
const originalFileName = matches[2];
if (entry.fileName !== originalFileName) {
entry.originalFileName = originalFileName;
}
entry.status = entry.fileName !== entry.originalFileName ? 'R' : 'M';
}
if (entry.fileStatuses) {
entry.fileName = Arrays.filterMap(entry.fileStatuses,
f => !!f.fileName ? f.fileName : undefined).join(', ');
while (true) {
next = lines.next();
if (next.done || next.value === '</f>') break;
}
break;
}
}
else {
lines.next();
next = lines.next();
line = next.value;
if (line !== undefined && !line.startsWith('warning:')) {
else {
entry.status = line[0] as GitStatusFileStatus;
entry.fileName = line.substring(1);
this.parseFileName(entry);
}
}
if (entry.fileStatuses !== undefined) {
entry.fileName = Arrays.filterMap(entry.fileStatuses,
f => !!f.fileName ? f.fileName : undefined).join(', ');
}
if (first && repoPath === undefined && type === GitCommitType.File && fileName !== undefined) {
// Try to get the repoPath from the most recent commit
repoPath = Strings.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
@ -200,18 +176,14 @@ export class GitLogParser {
}
first = false;
const commit = commits.get(entry.sha);
const commit = commits.get(entry.ref!);
if (commit === undefined) {
i++;
}
recentCommit = GitLogParser.parseEntry(entry, commit, type, repoPath, relativeFileName, commits, authors, recentCommit);
entry = undefined;
break;
}
if (next!.done) break;
}
return {
@ -247,10 +219,10 @@ export class GitLogParser {
commit = new GitLogCommit(
type,
repoPath!,
entry.sha,
entry.author,
entry.authorEmail,
new Date(entry.authorDate! as any * 1000),
entry.ref!,
entry.author!,
entry.email,
new Date(entry.date! as any * 1000),
entry.summary!,
relativeFileName,
entry.fileStatuses || [],
@ -261,7 +233,7 @@ export class GitLogParser {
entry.parentShas!
);
commits.set(entry.sha, commit);
commits.set(entry.ref!, commit);
}
// else {
// Logger.log(`merge commit? ${entry.sha}`);
@ -282,7 +254,7 @@ export class GitLogParser {
return commit;
}
private static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
if (entry.fileName === undefined) return;
const index = entry.fileName.indexOf('\t') + 1;

+ 84
- 112
src/git/parsers/stashParser.ts View File

@ -1,167 +1,139 @@
'use strict';
import { Arrays } from '../../system';
import { Git, GitCommitType, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
import { Arrays, Strings } from '../../system';
import { GitCommitType, GitLogParser, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger';
interface StashEntry {
sha: string;
ref?: string;
date?: string;
fileNames: string;
fileNames?: string;
fileStatuses?: IGitStatusFile[];
summary: string;
stashName: string;
summary?: string;
stashName?: string;
}
const emptyEntry: StashEntry = {};
export class GitStashParser {
static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this.parseEntries(data);
if (entries === undefined) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
const lines = Strings.lines(data + '\n</f>');
// Skip the first line since it will always be </f>
let next = lines.next();
if (next.done) return undefined;
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (commit === undefined) {
commit = new GitStashCommit(
GitCommitType.Stash,
entry.stashName,
repoPath,
entry.sha,
new Date(entry.date! as any * 1000),
entry.summary,
entry.fileNames,
entry.fileStatuses || []
);
commits.set(entry.sha, commit);
}
if (repoPath !== undefined) {
repoPath = Strings.normalizePath(repoPath);
}
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static parseEntries(data: string): StashEntry[] | undefined {
if (!data) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
const lines = data.split('\n');
if (lines.length === 0) return undefined;
let entry: StashEntry = emptyEntry;
let line: string | undefined = undefined;
let token: number;
const entries: StashEntry[] = [];
while (true) {
next = lines.next();
if (next.done) break;
let entry: StashEntry | undefined = undefined;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(' ');
if (lineParts.length < 2) {
continue;
}
line = next.value;
if (entry === undefined) {
if (!Git.shaRegex.test(lineParts[0])) continue;
// <<1-char token>> <data>
// e.g. <r> bd1452a2dc
token = line.charCodeAt(1);
entry = {
sha: lineParts[0]
} as StashEntry;
continue;
}
switch (lineParts[0]) {
case 'author-date':
entry.date = lineParts[1];
switch (token) {
case 114: // 'r': // ref
entry = {
ref: line.substring(4)
};
break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
while (++position < lines.length) {
const next = lines[position];
if (!next) break;
if (next === 'filename ?') {
position--;
break;
}
entry.summary += `\n${lines[position]}`;
}
case 100: // 'd': // author-date
entry.date = line.substring(4);
break;
case 'reflog-selector':
entry.stashName = lineParts.slice(1).join(' ').trim();
case 108: // 'l': // reflog-selector
entry.stashName = line.substring(4);
break;
case 'filename':
const nextLine = lines[position + 1];
// If the next line isn't blank, make sure it isn't starting a new commit
if (nextLine && Git.shaRegex.test(nextLine)) {
entries.push(entry);
entry = undefined;
case 115: // 's': // summary
while (true) {
next = lines.next();
if (next.done) break;
continue;
line = next.value;
if (line === '</s>') break;
if (entry.summary === undefined) {
entry.summary = line;
}
else {
entry.summary += `\n${line}`;
}
}
break;
position++;
case 102: // 'f': // files
// Skip the blank line git adds before the files
next = lines.next();
if (next.done || next.value === '</f>') break;
while (++position < lines.length) {
const line = lines[position];
lineParts = line.split(' ');
while (true) {
next = lines.next();
if (next.done) break;
if (Git.shaRegex.test(lineParts[0])) {
position--;
break;
}
line = next.value;
if (line === '</f>') break;
if (entry.fileStatuses == null) {
entry.fileStatuses = [];
}
if (line.startsWith('warning:')) continue;
const status = {
status: line[0] as GitStatusFileStatus,
fileName: line.substring(1),
originalFileName: undefined
} as IGitStatusFile;
this.parseFileName(status);
GitLogParser.parseFileName(status);
if (status.fileName) {
if (entry.fileStatuses === undefined) {
entry.fileStatuses = [];
}
entry.fileStatuses.push(status);
}
}
if (entry.fileStatuses) {
if (entry.fileStatuses !== undefined) {
entry.fileNames = Arrays.filterMap(entry.fileStatuses,
f => !!f.fileName ? f.fileName : undefined).join(', ');
}
entries.push(entry);
entry = undefined;
break;
default:
break;
let commit = commits.get(entry.ref!);
commit = GitStashParser.parseEntry(entry, commit, repoPath, commits);
}
}
return entries;
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
if (entry.fileName === undefined) return;
const index = entry.fileName.indexOf('\t') + 1;
if (index > 0) {
const next = entry.fileName.indexOf('\t', index) + 1;
if (next > 0) {
entry.originalFileName = entry.fileName.substring(index, next - 1);
entry.fileName = entry.fileName.substring(next);
}
else {
entry.fileName = entry.fileName.substring(index);
}
private static parseEntry(entry: StashEntry, commit: GitStashCommit | undefined, repoPath: string, commits: Map<string, GitStashCommit>): GitStashCommit | undefined {
if (commit === undefined) {
commit = new GitStashCommit(
GitCommitType.Stash,
entry.stashName!,
repoPath,
entry.ref!,
new Date(entry.date! as any * 1000),
entry.summary!,
entry.fileNames!,
entry.fileStatuses || []
);
}
commits.set(entry.ref!, commit);
return commit;
}
}

Loading…
Cancel
Save