Skip to content

Commit 363a0e9

Browse files
committed
Fixes #255 - Messages with empty lines are truncated
Rewrite of the log and stash parser -- more robust and perf
1 parent ab9d0eb commit 363a0e9

File tree

3 files changed

+174
-229
lines changed

3 files changed

+174
-229
lines changed

src/git/git.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export * from './remotes/provider';
2323
let git: IGit;
2424

2525
const defaultBlameParams = ['blame', '--root', '--incremental'];
26-
const defaultLogParams = ['log', '--name-status', '--full-history', '-M', '--format=%H -%nauthor %an%nauthor-mail %ae%nauthor-date %at%nparents %P%nsummary %B%nfilename ?'];
27-
const defaultStashParams = ['stash', 'list', '--name-status', '--full-history', '-M', '--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?'];
26+
// Using %x00 codes because some shells seem to try to expand things if not
27+
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'];
28+
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'];
2829

2930
const GitWarnings = [
3031
/Not a git repository/,

src/git/parsers/logParser.ts

Lines changed: 87 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatu
66
import * as path from 'path';
77

88
interface LogEntry {
9-
sha: string;
9+
ref?: string;
1010

11-
author: string;
12-
authorDate?: string;
13-
authorEmail?: string;
11+
author?: string;
12+
date?: string;
13+
email?: string;
1414

1515
parentShas?: string[];
1616

@@ -24,142 +24,105 @@ interface LogEntry {
2424
}
2525

2626
const diffRegex = /diff --git a\/(.*) b\/(.*)/;
27+
const emptyEntry: LogEntry = {};
2728

2829
export class GitLogParser {
2930

3031
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 {
3132
if (!data) return undefined;
3233

33-
const authors: Map<string, GitAuthor> = new Map();
34-
const commits: Map<string, GitLogCommit> = new Map();
35-
3634
let relativeFileName: string;
3735
let recentCommit: GitLogCommit | undefined = undefined;
3836

39-
if (repoPath !== undefined) {
40-
repoPath = Strings.normalizePath(repoPath);
41-
}
42-
43-
let entry: LogEntry | undefined = undefined;
37+
let entry: LogEntry = emptyEntry;
4438
let line: string | undefined = undefined;
45-
let lineParts: string[];
46-
let next: IteratorResult<string> | undefined = undefined;
39+
let token: number;
4740

4841
let i = 0;
4942
let first = true;
50-
let skip = false;
5143

52-
const lines = Strings.lines(data);
44+
const lines = Strings.lines(data + '\n</f>');
45+
// Skip the first line since it will always be </f>
46+
let next = lines.next();
47+
if (next.done) return undefined;
48+
49+
if (repoPath !== undefined) {
50+
repoPath = Strings.normalizePath(repoPath);
51+
}
52+
53+
const authors: Map<string, GitAuthor> = new Map();
54+
const commits: Map<string, GitLogCommit> = new Map();
55+
5356
while (true) {
54-
if (!skip) {
55-
next = lines.next();
56-
if (next.done) break;
57+
next = lines.next();
58+
if (next.done) break;
5759

58-
line = next.value;
59-
}
60-
else {
61-
skip = false;
62-
}
60+
line = next.value;
6361

6462
// Since log --reverse doesn't properly honor a max count -- enforce it here
6563
if (reverse && maxCount && (i >= maxCount)) break;
6664

67-
lineParts = line!.split(' ');
68-
if (lineParts.length < 2) continue;
65+
// <<1-char token>> <data>
66+
// e.g. <r> bd1452a2dc
67+
token = line.charCodeAt(1);
6968

70-
if (entry === undefined) {
71-
if (!Git.shaRegex.test(lineParts[0])) continue;
72-
73-
entry = {
74-
sha: lineParts[0]
75-
} as LogEntry;
76-
77-
continue;
78-
}
69+
switch (token) {
70+
case 114: // 'r': // ref
71+
entry = {
72+
ref: line.substring(4)
73+
};
74+
break;
7975

80-
switch (lineParts[0]) {
81-
case 'author':
82-
entry.author = Git.isUncommitted(entry.sha)
76+
case 97: // 'a': // author
77+
entry.author = Git.isUncommitted(entry.ref)
8378
? 'You'
84-
: lineParts.slice(1).join(' ').trim();
79+
: line.substring(4);
8580
break;
8681

87-
case 'author-mail':
88-
entry.authorEmail = lineParts.slice(1).join(' ').trim();
82+
case 101: // 'e': // author-mail
83+
entry.email = line.substring(4);
8984
break;
9085

91-
case 'author-date':
92-
entry.authorDate = lineParts[1];
86+
case 100: // 'd': // author-date
87+
entry.date = line.substring(4);
9388
break;
9489

95-
case 'parents':
96-
entry.parentShas = lineParts.slice(1);
90+
case 112: // 'p': // parents
91+
entry.parentShas = line.substring(4).split(' ');
9792
break;
9893

99-
case 'summary':
100-
entry.summary = lineParts.slice(1).join(' ').trim();
94+
case 115: // 's': // summary
10195
while (true) {
10296
next = lines.next();
10397
if (next.done) break;
10498

10599
line = next.value;
106-
if (!line) break;
100+
if (line === '</s>') break;
107101

108-
if (line === 'filename ?') {
109-
skip = true;
110-
break;
102+
if (entry.summary === undefined) {
103+
entry.summary = line;
104+
}
105+
else {
106+
entry.summary += `\n${line}`;
111107
}
112-
113-
entry.summary += `\n${line}`;
114108
}
115109
break;
116110

117-
case 'filename':
118-
if (type === GitCommitType.Branch) {
111+
case 102: // 'f': // files
112+
// Skip the blank line git adds before the files
113+
next = lines.next();
114+
if (next.done || next.value === '</f>') break;
115+
116+
while (true) {
119117
next = lines.next();
120118
if (next.done) break;
121119

122120
line = next.value;
121+
if (line === '</f>') break;
123122

124-
// If the next line isn't blank, make sure it isn't starting a new commit or s git warning
125-
if (line && (Git.shaRegex.test(line) || line.startsWith('warning:'))) {
126-
skip = true;
127-
continue;
128-
}
129-
130-
let diff = false;
131-
while (true) {
132-
next = lines.next();
133-
if (next.done) break;
134-
135-
line = next.value;
136-
lineParts = line.split(' ');
137-
138-
// make sure the line isn't starting a new commit or s git warning
139-
if (Git.shaRegex.test(lineParts[0]) || line.startsWith('warning:')) {
140-
skip = true;
141-
break;
142-
}
143-
144-
if (diff) continue;
145-
146-
if (lineParts[0] === 'diff') {
147-
diff = true;
148-
const matches = diffRegex.exec(line);
149-
if (matches != null) {
150-
entry.fileName = matches[1];
151-
const originalFileName = matches[2];
152-
if (entry.fileName !== originalFileName) {
153-
entry.originalFileName = originalFileName;
154-
}
155-
}
156-
continue;
157-
}
158-
159-
if (entry.fileStatuses == null) {
160-
entry.fileStatuses = [];
161-
}
123+
if (line.startsWith('warning:')) continue;
162124

125+
if (type === GitCommitType.Branch) {
163126
const status = {
164127
status: line[0] as GitStatusFileStatus,
165128
fileName: line.substring(1),
@@ -168,28 +131,41 @@ export class GitLogParser {
168131
this.parseFileName(status);
169132

170133
if (status.fileName) {
134+
if (entry.fileStatuses === undefined) {
135+
entry.fileStatuses = [];
136+
}
171137
entry.fileStatuses.push(status);
172138
}
173139
}
140+
else if (line.startsWith('diff')) {
141+
const matches = diffRegex.exec(line);
142+
if (matches != null) {
143+
entry.fileName = matches[1];
144+
const originalFileName = matches[2];
145+
if (entry.fileName !== originalFileName) {
146+
entry.originalFileName = originalFileName;
147+
}
148+
entry.status = entry.fileName !== entry.originalFileName ? 'R' : 'M';
149+
}
174150

175-
if (entry.fileStatuses) {
176-
entry.fileName = Arrays.filterMap(entry.fileStatuses,
177-
f => !!f.fileName ? f.fileName : undefined).join(', ');
151+
while (true) {
152+
next = lines.next();
153+
if (next.done || next.value === '</f>') break;
154+
}
155+
break;
178156
}
179-
}
180-
else {
181-
lines.next();
182-
next = lines.next();
183-
184-
line = next.value;
185-
186-
if (line !== undefined && !line.startsWith('warning:')) {
157+
else {
187158
entry.status = line[0] as GitStatusFileStatus;
188159
entry.fileName = line.substring(1);
189160
this.parseFileName(entry);
190161
}
191162
}
192163

164+
if (entry.fileStatuses !== undefined) {
165+
entry.fileName = Arrays.filterMap(entry.fileStatuses,
166+
f => !!f.fileName ? f.fileName : undefined).join(', ');
167+
}
168+
193169
if (first && repoPath === undefined && type === GitCommitType.File && fileName !== undefined) {
194170
// Try to get the repoPath from the most recent commit
195171
repoPath = Strings.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
@@ -200,18 +176,14 @@ export class GitLogParser {
200176
}
201177
first = false;
202178

203-
const commit = commits.get(entry.sha);
179+
const commit = commits.get(entry.ref!);
204180
if (commit === undefined) {
205181
i++;
206182
}
207183
recentCommit = GitLogParser.parseEntry(entry, commit, type, repoPath, relativeFileName, commits, authors, recentCommit);
208184

209-
entry = undefined;
210185
break;
211186
}
212-
213-
if (next!.done) break;
214-
215187
}
216188

217189
return {
@@ -247,10 +219,10 @@ export class GitLogParser {
247219
commit = new GitLogCommit(
248220
type,
249221
repoPath!,
250-
entry.sha,
251-
entry.author,
252-
entry.authorEmail,
253-
new Date(entry.authorDate! as any * 1000),
222+
entry.ref!,
223+
entry.author!,
224+
entry.email,
225+
new Date(entry.date! as any * 1000),
254226
entry.summary!,
255227
relativeFileName,
256228
entry.fileStatuses || [],
@@ -261,7 +233,7 @@ export class GitLogParser {
261233
entry.parentShas!
262234
);
263235

264-
commits.set(entry.sha, commit);
236+
commits.set(entry.ref!, commit);
265237
}
266238
// else {
267239
// Logger.log(`merge commit? ${entry.sha}`);
@@ -282,7 +254,7 @@ export class GitLogParser {
282254
return commit;
283255
}
284256

285-
private static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
257+
static parseFileName(entry: { fileName?: string, originalFileName?: string }) {
286258
if (entry.fileName === undefined) return;
287259

288260
const index = entry.fileName.indexOf('\t') + 1;

0 commit comments

Comments
 (0)