Repository navigation
fix: handle multi-line commit bodies in git log parsing - #1553
Conversation
Use a null byte (%x00) record separator in git log format to correctly delimit commits, preventing multi-line bodies (e.g. merge commits with Reviewed-on/Reviewed-by lines) from being split into bogus commit entries with undefined fields. Fixes #1552 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||||||||||||
There was a problem hiding this comment.
Pull request overview
Fixes getCommitsBetween parsing of git log output so merge commits with multi-line bodies (e.g., Reviewed-on: / Reviewed-by: lines) don’t create bogus “commit” entries and crash downstream processing.
Changes:
- Updates
git logpretty format to prefix each commit with a NUL (%x00) record separator. - Reworks parsing to split by NUL records and reassemble multi-line bodies (including
|in bodies). - Adds a new Vitest suite covering multi-line bodies,
|preservation, and empty output.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/pr-collector/gitHelper.ts |
Uses NUL-delimited records and updated parsing to correctly handle multi-line commit bodies |
__tests__/gitHelper.test.ts |
Adds regression tests reproducing #1552 and verifying the new parsing behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'log', | ||
| '--pretty=format:%H|%an|%ae|%aI|%s|%b', | ||
| '--pretty=format:%x00%H|%an|%ae|%aI|%s|%b', | ||
| `${base}..${head}` | ||
| ] | ||
| if (includeOnlyPaths) { | ||
| logArgs.push('--', ...includeOnlyPaths) | ||
| } | ||
|
|
||
| const logOutput = await this.execGit(logArgs) | ||
| const lines = logOutput.stdout.trim().split('\n').filter(line => line.trim() !== '') | ||
| const commits = lines.map(line => { | ||
| const [sha, authorName, authorEmail, authorDate, subject, body] = line.split('|') | ||
| // Split on null byte record separator to correctly handle multi-line commit bodies | ||
| const records = logOutput.stdout.split('\x00').filter(record => record.trim() !== '') | ||
| const commits = records.map(record => { | ||
| // The first line contains the pipe-delimited fields; remaining lines are part of the body | ||
| const [firstLine, ...remainingLines] = record.split('\n') | ||
| const [sha, authorName, authorEmail, authorDate, subject, ...bodyParts] = firstLine.split('|') | ||
| const firstBodyLine = bodyParts.join('|') |
There was a problem hiding this comment.
Parsing still relies on | as the field separator (firstLine.split('|')). While you re-join the body to preserve | in the body, any | in earlier fields (notably %s subject or %an author name) will still be mis-parsed and leak into the body. Consider switching the git pretty format to use a non-printable field separator (e.g., %x1f) in addition to the %x00 record separator, and split on that character for robust parsing of all fields.
| expect(bogusCommit.sha).toBe('Reviewed-by: chosen <chosen@example.com>') | ||
| expect(bogusCommit.subject).toBeUndefined() | ||
|
|
||
| // This is the exact crash from the issue: commit.subject.split('\n') throws TypeError |
There was a problem hiding this comment.
This test intentionally calls .split() on an undefined value to reproduce the historical crash, which can trigger TypeScript/IDE warnings. Consider making the intent explicit with a // @ts-expect-error (or casting bogusCommit.subject as any) on this line so future refactors don’t “fix” it accidentally.
| // This is the exact crash from the issue: commit.subject.split('\n') throws TypeError | |
| // This is the exact crash from the issue: commit.subject.split('\n') throws TypeError | |
| // @ts-expect-error Intentionally calling .split() on undefined to reproduce the historical crash |
Address review feedback: - Use %x1f (unit separator) as field delimiter instead of |, so pipe characters in author names or subjects don't corrupt parsing - Use trimEnd() instead of trim() to preserve leading whitespace in commit bodies - Recompile dist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js child_process.spawn() rejects arguments containing null bytes. Use git's own %x00 and %x1f pretty-format placeholders in the command argument, which git interprets at output time, producing the expected separator bytes in stdout without embedding them in the command args. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 4 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| test('fixed parsing handles multi-line commit bodies and preserves their content', async () => { | ||
| const gitHelper = await createCommandManager('.') | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| vi.spyOn(gitHelper as any, 'execGit').mockResolvedValue({ | ||
| stdout: buildFixedGitLog(), | ||
| exitCode: 0 | ||
| }) |
There was a problem hiding this comment.
The test constructs a real GitCommandManager via createCommandManager('.'), which calls io.which('git', true) during initialization. This makes the unit tests depend on git being present in PATH even though execGit is mocked. Consider mocking @actions/io's which (or exposing a lightweight constructor/factory for tests) so the tests stay hermetic and don't fail in minimal CI/dev environments.
| const FIELD_SEP = '\x1f' // unit separator — used for splitting output | ||
| const RECORD_SEP = '\x00' // null byte — used for splitting output | ||
| const logArgs = [ |
There was a problem hiding this comment.
FIELD_SEP/RECORD_SEP are created on every getCommitsBetween call; since they are constants, consider hoisting them to module scope (or static readonly) to avoid repetition and ensure the separators stay consistent if this format string changes elsewhere.
Summary
Reviewed-on:/Reviewed-by:lines from Gitea) that causedTypeError: Cannot read properties of undefined (reading 'split')%x00) record separator in the git log format string to correctly delimit commits, so newlines within the body don't split into bogus entries|) in commit bodies being truncated via rest destructuringTest plan
__tests__/gitHelper.test.tswith 4 tests:subjectfields from multi-line bodiesFixes #1552
🤖 Generated with Claude Code