Skip to content

fix: handle multi-line commit bodies in git log parsing - #1553

Merged
mikepenz merged 3 commits into
developfrom
fix/multiline-commit-body-parsing
Apr 10, 2026
Merged

mikepenz merged 3 commits into
developfrom
fix/multiline-commit-body-parsing

Conversation

@mikepenz

Copy link
Copy Markdown
Owner

Summary

  • Fixes crash when merge commits have multi-line bodies (e.g. Reviewed-on: / Reviewed-by: lines from Gitea) that caused TypeError: Cannot read properties of undefined (reading 'split')
  • Uses a null byte (%x00) record separator in the git log format string to correctly delimit commits, so newlines within the body don't split into bogus entries
  • Also fixes pipe characters (|) in commit bodies being truncated via rest destructuring

Test plan

  • Added __tests__/gitHelper.test.ts with 4 tests:
    • Reproduces Failed to retrieve due to type error #1552 by proving the legacy parsing creates undefined subject fields from multi-line bodies
    • Verifies the fix produces exactly 4 commits with all fields defined and multi-line body content preserved
    • Verifies pipe characters in commit bodies are preserved
    • Verifies empty git log output is handled gracefully
  • All existing tests pass

Fixes #1552

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings April 10, 2026 10:09
@github-actions

github-actions Bot commented Apr 10, 2026 •

Copy link
Copy Markdown
TestsPassed ✅SkippedFailed
JUnit Test Report71 ran71 passed0 skipped0 failed
TestResult
No test annotations available

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 log pretty 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.

Comment thread src/pr-collector/gitHelper.ts Outdated
Comment thread src/pr-collector/gitHelper.ts Outdated
Comment on lines +91 to +106
'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('|')

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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

Copilot uses AI. Check for mistakes.
Mike Penz and others added 2 commits April 10, 2026 12:20
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>
Copilot AI review requested due to automatic review settings April 10, 2026 10:24
@mikepenz mikepenz added the fix label Apr 10, 2026
@mikepenz
mikepenz merged commit 5795a33 into develop Apr 10, 2026
10 checks passed
@mikepenz
mikepenz deleted the fix/multiline-commit-body-parsing branch April 10, 2026 10:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +65 to +72
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
})

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to 92
const FIELD_SEP = '\x1f' // unit separator — used for splitting output
const RECORD_SEP = '\x00' // null byte — used for splitting output
const logArgs = [

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed to retrieve due to type error

2 participants