diff --git a/src/git/git.ts b/src/git/git.ts index 3983e4a..8ab5796 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -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/, diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index d2e9d6d..2329074 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -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 = new Map(); - const commits: Map = 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 | 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'); + // Skip the first line since it will always be + let next = lines.next(); + if (next.done) return undefined; + + if (repoPath !== undefined) { + repoPath = Strings.normalizePath(repoPath); + } + + const authors: Map = new Map(); + const commits: Map = 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>> + // e.g. 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 === '') 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 === '') break; + + while (true) { next = lines.next(); if (next.done) break; line = next.value; + if (line === '') 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 === '') 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; diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index 192c72d..ff1fb2d 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -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 = new Map(); + const lines = Strings.lines(data + '\n'); + // Skip the first line since it will always be + 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 = 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>> + // e.g. 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 === '') 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 === '') 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 === '') 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): 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; } } \ No newline at end of file