Tucker McKnight <tucker@pangolin.lan> | Sun May 24 2026
Use git log --format=raw for getting info about commit parents Go back to having each commit know what its own parent is; that is how git actually works. Branches do not need their own lists of commits. Instead of figuring out what the parent of a commit is by checking which one comes next in the `git log`, use `git log --format=raw`. That will add a `parent` line for each commit, letting you know exactly what the parent of that commit is. The preceeding commit in the log is not always the parent commit. git log will show them in chronological order based on when the commit was created. Therefore, commits from different branches are mixed in together in the chronological git log. Rely on the `parent` field from `git log --format=raw` instead.
13 14 15 16 17
message: string,
isMerge: boolean,
author: string,
date: Date,
diffs: Array<{
fileName: string,13 14 15 16 17 18
message: string,
isMerge: boolean,
author: string,
parent: string | null,
date: Date,
diffs: Array<{
fileName: string,46 47 48 49 50 51
// to query for the file contents or to search around the existing file list
// to see if a copy is already available.
fileList: Map<string, {fileInfo: FileInfo}>,
commits: Array<Commit>,
}>,
tags?: Array<{
name: string,46 47 48 49 50
// to query for the file contents or to search around the existing file list
// to see if a copy is already available.
fileList: Map<string, {fileInfo: FileInfo}>,
}>,
tags?: Array<{
name: string,50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
export const addBranchToCommitsMap = async(branchName: string, repoLocation: string, commits: Repository['commits']): Promise<void> => {
const totalPatchesCountRes = await exec(`git -C ${repoLocation} rev-list --count ${branchName}`)
const totalPatchesCount = parseInt(totalPatchesCountRes.stdout)
let previousHash: null | string = null
let gitLogSubsetCmd = null
let gitLogSubset = []
return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount)
}
export const addBranchToCommitsMapWithSkip = async(branchName: string, repoLocation: string, commits: Repository['commits'], totalPatchesCount: number, skip = 0, previousHash = null): Promise<void> => {
console.log(`addBranchToCommitsMap, skip: ${skip}, total: ${totalPatchesCount}`)
return new Promise((resolve) => {
if (skip >= totalPatchesCount) {
return resolve()
}
// const gitLogSubsetRes = await exec(`git -C ${repoLocation} log ${branchName} -p -n 10 --skip ${i}`)
const gitLogSubsetCmd = childProcess.spawn('git', ['-C', repoLocation, 'log', branchName, '-p', '-n', '10', '--skip', skip.toString()], {
stdio: [0, "pipe", "inherit"],
})
// let gitLogSubset = gitLogSubsetRes.stdout.split("\n")
let gitLogSubset = []
gitLogSubsetCmd.stdout.on('data', (data) => {
gitLogSubset.push(data)
})
gitLogSubsetCmd.on('close', () => {
previousHash = addGitLogSubsetToMap(gitLogSubset.join('').split('\n'), commits, previousHash)
return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount, skip + 10, previousHash).then(() => {
return resolve()
})
})
})
}
const addGitLogSubsetToMap = (gitLogSubset: string[], commits: Repository['commits'], previousHash: string | null) => {
do {
const nextPatchStart = gitLogSubset.findIndex((line, index) => {
return (index > 0 && line.startsWith("commit ")) || index === gitLogSubset.length - 150 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
export const addBranchToCommitsMap = async(branchName: string, repoLocation: string, commits: Repository['commits']): Promise<void> => {
const totalPatchesCountRes = await exec(`git -C ${repoLocation} rev-list --count ${branchName}`)
const totalPatchesCount = parseInt(totalPatchesCountRes.stdout)
let gitLogSubsetCmd = null
let gitLogSubset = []
return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount)
}
export const addBranchToCommitsMapWithSkip = async(branchName: string, repoLocation: string, commits: Repository['commits'], totalPatchesCount: number, skip = 0): Promise<void> => {
return new Promise((resolve) => {
if (skip >= totalPatchesCount) {
return resolve()
}
const gitLogSubsetCmd = childProcess.spawn('git', ['-C', repoLocation, 'log', branchName, '--format=raw', '-p', '-n', '10', '--skip', skip.toString()], {
stdio: [0, "pipe", "inherit"],
})
let gitLogSubset = []
gitLogSubsetCmd.stdout.on('data', (data) => {
gitLogSubset.push(data)
})
gitLogSubsetCmd.on('close', () => {
addGitLogSubsetToMap(gitLogSubset.join('').split('\n'), commits)
return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount, skip + 10).then(() => {
return resolve()
})
})
})
}
const addGitLogSubsetToMap = (gitLogSubset: string[], commits: Repository['commits']) => {
do {
const nextPatchStart = gitLogSubset.findIndex((line, index) => {
return (index > 0 && line.startsWith("commit ")) || index === gitLogSubset.length - 192 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
gitLogSubset = gitLogSubset.slice(nextPatchStart)
const hash = currentPatch[0].replace("commit ", "").trim()
console.log(`doing ${hash}`)
// set the parent hash of the previous commit, unless we're on the first commit
if (previousHash !== null) {
commits.get(previousHash)['parent'] = hash
}
previousHash = hash
// exit early if the commits map already includes this hash
if (commits.has(hash)) {
return
}
let author: string, date: string, isMerge: boolean
[1, 2, 3].forEach((lineNumber) => {
if (currentPatch[lineNumber].startsWith("Author")) {
author = currentPatch[lineNumber].replace("Author: ", "").trim()
}
else if (currentPatch[lineNumber].startsWith("Date")) {
date = currentPatch[lineNumber].replace("Date: ", "").trim()
}
else if (currentPatch[lineNumber].startsWith("Merge")) {
isMerge = true
}
})
let diffStart = currentPatch.findIndex((line) => {
return line.startsWith("diff ")
})
// If no line starts with "diff", this
// is probably a mege commit. Use the last
// line of the patch + 1, in that case, to just get the full
// text of the commit
let messageStart = 4
if (diffStart === -1) {
messageStart = 5
diffStart = currentPatch.length
}
// Git log is indent four spaces by default -- remove those.
const commitMessage = currentPatch.slice(messageStart, diffStart).map(str => str.replace(" ", "")).filter((line) => {
// git log --porcelain output adds these "Ignore-this:" lines and I'm not sure what they are
return !line.startsWith('Ignore-this: ')
}).join("\n").trim()92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
gitLogSubset = gitLogSubset.slice(nextPatchStart)
const hash = currentPatch[0].replace("commit ", "").trim()
// exit early if the commits map already includes this hash
if (commits.has(hash)) {
return
}
const nextBlankLine = currentPatch.findIndex(line => line === "") || currentPatch.length
const commitInfoSection = currentPatch.slice(0, nextBlankLine)
let author: string, date: string
let isMerge = false
let parent: string | null = null
commitInfoSection.forEach((commitInfoLine) => {
if (commitInfoLine.startsWith("author")) {
const lineWords = commitInfoLine.replace("author ", "").trim().split(' ')
author = lineWords.slice(0, lineWords.length - 2).join(' ')
date = lineWords[lineWords.length - 2]
}
if (commitInfoLine.startsWith("parent")) {
parent = commitInfoLine.replace("parent ", "").trim()
}
})
// Mark as a merge if more than one line starts with "parent"
if (commitInfoSection.filter(line => line.startsWith("parent")).length > 1) {
isMerge = true
}
let diffStart = currentPatch.findIndex((line) => {
return line.startsWith("diff ")
})
if (diffStart === -1) {
diffStart = currentPatch.length
}
// Git log is indent four spaces by default -- remove those.
const commitMessage = currentPatch.slice(nextBlankLine + 1, diffStart).map(str => str.replace(" ", "")).filter((line) => {
// git log --porcelain output adds these "Ignore-this:" lines and I'm not sure what they are
return !line.startsWith('Ignore-this: ')
}).join("\n").trim()138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
commits.set(hash, {
hash,
message: commitMessage,
isMerge: isMerge || false,
author,
date: new Date(date),
diffs,
parent: null,
cachedFiles: new Map(),
})
} while (gitLogSubset.length > 1)
return previousHash
}
export type BlameInfo = {138 139 140 141 142 143 144 145 146 147 148 149 150 151
commits.set(hash, {
hash,
message: commitMessage,
isMerge: isMerge,
author,
date: new Date(parseInt(date) * 1000), // git timestamp is number of seconds,
// js Date() exects milliseconds
diffs,
parent,
cachedFiles: new Map(),
})
} while (gitLogSubset.length > 1)
}
export type BlameInfo = {