From 12d0463509b72d62d3e298d3b1cd54ac9fd366ec Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 22:20:32 +0000 Subject: [PATCH] fix: keep co-author trailers in one trailer block gstack-coauthor-trailers --message appended the new Co-authored-by lines as a separate paragraph after existing trailers, so git interpret-trailers (and GitHub attribution) read only the last paragraph and the existing co-author lost credit. The new lines now join the existing trailer block; the test checks the result with git interpret-trailers --parse. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-coauthor-trailers | 6 +++++- test/gstack-coauthor-trailers.test.ts | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bin/gstack-coauthor-trailers b/bin/gstack-coauthor-trailers index a5f3e0665..cd61abfcd 100755 --- a/bin/gstack-coauthor-trailers +++ b/bin/gstack-coauthor-trailers @@ -77,8 +77,12 @@ function main(argv: string[]): number { const present = new Set([...text.matchAll(/^co-authored-by:.*<([^>]+)>\s*$/gim)].map(m => m[1]!.toLowerCase())); appended = [...authors.entries()].filter(([key]) => !present.has(key)).map(([, a]) => `Co-authored-by: ${a.name} <${a.email}>`); if (appended.length) { + // Trailers are one paragraph: a blank line before the new ones would make git (and GitHub's + // attribution) read only the last paragraph as the trailer block and drop the existing co-authors. const body = text.replace(/\s+$/, ''); - fs.writeFileSync(message, (body ? body + '\n\n' : '') + appended.join('\n') + '\n'); + const cut = body.lastIndexOf('\n\n'); + const endsInTrailer = cut >= 0 && body.slice(cut + 2).split('\n').every(l => /^[A-Za-z][\w-]*: \S/.test(l)); + fs.writeFileSync(message, (body ? body + (endsInTrailer ? '\n' : '\n\n') : '') + appended.join('\n') + '\n'); } } if (argv.includes('--json')) process.stdout.write(JSON.stringify({ range, authors: [...authors.values()], trailers, appended }) + '\n'); diff --git a/test/gstack-coauthor-trailers.test.ts b/test/gstack-coauthor-trailers.test.ts index 419b9e71b..2c4bf1664 100644 --- a/test/gstack-coauthor-trailers.test.ts +++ b/test/gstack-coauthor-trailers.test.ts @@ -72,8 +72,13 @@ test('--message appends only the missing trailers and is idempotent; a bad range const file = path.join(dir, 'MSG'); fs.writeFileSync(file, 'port: contributor fix\n\nCo-authored-by: ada contributor \n'); expect(run(dir, ['--message', file]).status).toBe(EXIT.ok); - const expected = 'port: contributor fix\n\nCo-authored-by: ada contributor \n\nCo-authored-by: Bo Helper \n'; + const expected = 'port: contributor fix\n\nCo-authored-by: ada contributor \nCo-authored-by: Bo Helper \n'; expect(fs.readFileSync(file, 'utf8')).toBe(expected); + expect(spawnSync('git', ['interpret-trailers', '--parse'], { cwd: dir, input: expected, encoding: 'utf8' }).stdout.trim().split('\n')).toHaveLength(2); + const plain = path.join(dir, 'PLAIN'); + fs.writeFileSync(plain, 'port: contributor fix\n'); + expect(run(dir, ['--message', plain]).status).toBe(EXIT.ok); + expect(fs.readFileSync(plain, 'utf8')).toBe('port: contributor fix\n\nCo-authored-by: Ada Contributor \nCo-authored-by: Bo Helper \n'); const again = run(dir, ['--message', file, '--json']); expect(JSON.parse(again.stdout).appended).toEqual([]); expect(fs.readFileSync(file, 'utf8')).toBe(expected);