Browse Source

Fixes #255 - Messages with empty lines are truncated

Rewrite of the log and stash parser -- more robust and perf
main
Eric Amodio 6 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; let git: IGit;
const defaultBlameParams = ['blame', '--root', '--incremental']; 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 = [ const GitWarnings = [
/Not a git repository/, /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'; import * as path from 'path';
interface LogEntry { interface LogEntry {
sha: string;
ref?: string;
author: string;
authorDate?: string;
authorEmail?: string;
author?: string;
date?: string;
email?: string;
parentShas?: string[]; parentShas?: string[];
@ -24,142 +24,105 @@ interface LogEntry {
} }
const diffRegex = /diff --git a\/(.*) b\/(.*)/; const diffRegex = /diff --git a\/(.*) b\/(.*)/;
const emptyEntry: LogEntry = {};
export class GitLogParser { 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 { 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; if (!data) return undefined;
const authors: Map<string, GitAuthor> = new Map();
const commits: Map<string, GitLogCommit> = new Map();
let relativeFileName: string; let relativeFileName: string;
let recentCommit: GitLogCommit | undefined = undefined; 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 line: string | undefined = undefined;
let lineParts: string[];
let next: IteratorResult<string> | undefined = undefined;
let token: number;
let i = 0; let i = 0;
let first = true; 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) { 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 // Since log --reverse doesn't properly honor a max count -- enforce it here
if (reverse && maxCount && (i >= maxCount)) break; 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' ? 'You'
: lineParts.slice(1).join(' ').trim();
: line.substring(4);
break; break;
case 'author-mail':
entry.authorEmail = lineParts.slice(1).join(' ').trim();
case 101: // 'e': // author-mail
entry.email = line.substring(4);
break; break;
case 'author-date':
entry.authorDate = lineParts[1];
case 100: // 'd': // author-date
entry.date = line.substring(4);
break; break;
case 'parents':
entry.parentShas = lineParts.slice(1);
case 112: // 'p': // parents
entry.parentShas = line.substring(4).split(' ');
break; break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
case 115: // 's': // summary
while (true) { while (true) {
next = lines.next(); next = lines.next();
if (next.done) break; if (next.done) break;
line = next.value; 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; 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(); next = lines.next();
if (next.done) break; if (next.done) break;
line = next.value; 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 = { const status = {
status: line[0] as GitStatusFileStatus, status: line[0] as GitStatusFileStatus,
fileName: line.substring(1), fileName: line.substring(1),
@ -168,28 +131,41 @@ export class GitLogParser {
this.parseFileName(status); this.parseFileName(status);
if (status.fileName) { if (status.fileName) {
if (entry.fileStatuses === undefined) {
entry.fileStatuses = [];
}
entry.fileStatuses.push(status); 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.status = line[0] as GitStatusFileStatus;
entry.fileName = line.substring(1); entry.fileName = line.substring(1);
this.parseFileName(entry); 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) { if (first && repoPath === undefined && type === GitCommitType.File && fileName !== undefined) {
// Try to get the repoPath from the most recent commit // Try to get the repoPath from the most recent commit
repoPath = Strings.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, '')); repoPath = Strings.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
@ -200,18 +176,14 @@ export class GitLogParser {
} }
first = false; first = false;
const commit = commits.get(entry.sha);
const commit = commits.get(entry.ref!);
if (commit === undefined) { if (commit === undefined) {
i++; i++;
} }
recentCommit = GitLogParser.parseEntry(entry, commit, type, repoPath, relativeFileName, commits, authors, recentCommit); recentCommit = GitLogParser.parseEntry(entry, commit, type, repoPath, relativeFileName, commits, authors, recentCommit);
entry = undefined;
break; break;
} }
if (next!.done) break;
} }
return { return {
@ -247,10 +219,10 @@ export class GitLogParser {
commit = new GitLogCommit( commit = new GitLogCommit(
type, type,
repoPath!, 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!, entry.summary!,
relativeFileName, relativeFileName,
entry.fileStatuses || [], entry.fileStatuses || [],
@ -261,7 +233,7 @@ export class GitLogParser {
entry.parentShas! entry.parentShas!
); );
commits.set(entry.sha, commit);
commits.set(entry.ref!, commit);
} }
// else { // else {
// Logger.log(`merge commit? ${entry.sha}`); // Logger.log(`merge commit? ${entry.sha}`);
@ -282,7 +254,7 @@ export class GitLogParser {
return commit; return commit;
} }
private static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
if (entry.fileName === undefined) return; if (entry.fileName === undefined) return;
const index = entry.fileName.indexOf('\t') + 1; const index = entry.fileName.indexOf('\t') + 1;

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

@ -1,167 +1,139 @@
'use strict'; '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'; // import { Logger } from '../../logger';
interface StashEntry { interface StashEntry {
sha: string;
ref?: string;
date?: string; date?: string;
fileNames: string;
fileNames?: string;
fileStatuses?: IGitStatusFile[]; fileStatuses?: IGitStatusFile[];
summary: string;
stashName: string;
summary?: string;
stashName?: string;
} }
const emptyEntry: StashEntry = {};
export class GitStashParser { export class GitStashParser {
static parse(data: string, repoPath: string): GitStash | undefined { 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; 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; break;
case 'reflog-selector':
entry.stashName = lineParts.slice(1).join(' ').trim();
case 108: // 'l': // reflog-selector
entry.stashName = line.substring(4);
break; 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 = { const status = {
status: line[0] as GitStatusFileStatus, status: line[0] as GitStatusFileStatus,
fileName: line.substring(1), fileName: line.substring(1),
originalFileName: undefined originalFileName: undefined
} as IGitStatusFile; } as IGitStatusFile;
this.parseFileName(status);
GitLogParser.parseFileName(status);
if (status.fileName) { if (status.fileName) {
if (entry.fileStatuses === undefined) {
entry.fileStatuses = [];
}
entry.fileStatuses.push(status); entry.fileStatuses.push(status);
} }
} }
if (entry.fileStatuses) {
if (entry.fileStatuses !== undefined) {
entry.fileNames = Arrays.filterMap(entry.fileStatuses, entry.fileNames = Arrays.filterMap(entry.fileStatuses,
f => !!f.fileName ? f.fileName : undefined).join(', '); 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