diff --git a/lib/landing_session.js b/lib/landing_session.js index 89d6e400..1e56d835 100644 --- a/lib/landing_session.js +++ b/lib/landing_session.js @@ -316,35 +316,43 @@ export default class LandingSession extends Session { // git has very specific rules about what is a trailer and what is not. // Instead of trying to implement those ourselves, let git parse the - // original commit message and see if it outputs any trailers. - const originalTrailers = runSync('git', [ + // commit message and see if it outputs any trailers. + const interpretTrailers = commitMessage => runSync('git', [ 'interpret-trailers', '--parse', '--no-divider' ], { - input: `${original}\n` - }).split('\n').map(trailer => { - const separatorIndex = trailer.indexOf(':'); - return [trailer.slice(0, separatorIndex), trailer.slice(separatorIndex + 2)]; - }).slice(0, -1); // Remove the last line return entry + input: `${commitMessage}\n` + }); + const originalTrailers = interpretTrailers(original); const containCVETrailer = CVE_RE.test(originalTrailers); const filterTrailer = (line) => ([key]) => line.startsWith(key) && new RegExp(`^${RegExp.escape?.(key) ?? key}\\s*:`).test(line); - const amended = original.trim().split('\n') - .filter(line => - !line.includes(':') || - !originalTrailers.some(filterTrailer(line))); - for (let i = amended.length - 1; amended[i] === ''; i--) { + const amended = original.trim().split('\n'); + const stillInTrailers = () => { + const result = interpretTrailers(amended.join('\n')); + return result.length && originalTrailers.startsWith(result.trim()); + }; + for (let i = amended.length - 1; amended[i] === '' || stillInTrailers(); i--) { + // Remove last line until git no longer detects any trailers amended.pop(); } // Only keep existing trailers that we won't add ourselves const trailersToFilterOut = ['BACKPORT-PR-URL', 'REVIEWED-BY']; if (!this.backport) trailersToFilterOut.push('PR-URL'); - const keptTrailers = originalTrailers.filter( - ([key]) => !trailersToFilterOut.includes(key.toUpperCase()) - ); + const keptTrailers = + originalTrailers + .split('\n') + .map(trailer => { + const separatorIndex = trailer.indexOf(':'); + return [trailer.slice(0, separatorIndex), trailer.slice(separatorIndex + 2)]; + }) + .slice(0, -1) // Remove the last line return entry + .filter( + ([key]) => !trailersToFilterOut.includes(key.toUpperCase()) + ); amended.push('', ...keptTrailers.map(([key, value]) => `${key}: ${value}`)); for (const line of metadata.trim().split('\n')) { diff --git a/test/unit/amend.test.js b/test/unit/amend.test.js index c205c8df..1a23d2f7 100644 --- a/test/unit/amend.test.js +++ b/test/unit/amend.test.js @@ -112,6 +112,26 @@ describe('LandingSession.prototype.generateAmendedMessage', () => { t.assert.strictEqual(result, '\nPR-URL: http://example.com/123\nReviewed-By: user1 '); }); + it('should handle multi-line trailers', async(t) => { + const session = createSession(); + const result = await session.generateAmendedMessage( + 'subsystem: foobar\n\nSigned-off-by: user1\n \n' + ); + + t.assert.strictEqual(result, + 'subsystem: foobar\n\nSigned-off-by: user1 \nPR-URL: http://example.com/123\nReviewed-By: user1 '); + }); + + it('should not remove lines that look like trailers in the commit body', async(t) => { + const session = createSession(); + const result = await session.generateAmendedMessage( + 'subsystem: foobar\n\nNot-A-Trailer: http://example.com/\n\nSigned-off-by: user1\n \n' + ); + + t.assert.strictEqual(result, + 'subsystem: foobar\n\nNot-A-Trailer: http://example.com/\n\nSigned-off-by: user1 \nPR-URL: http://example.com/123\nReviewed-By: user1 '); + }); + it('should handle cherry-pick from upstream', async(t) => { const session = createSession({ metadata: 'Refs: https://github.com/v8/v8/commit/cf1bce40a5ef4c7c1da351754f5bf526c0c96463\n' }); const result = await session.generateAmendedMessage(`deps: V8: cherry-pick cf1bce40a5ef