Skip to content

Commit 21ff4a3

Browse files
Copilotalexr00
andcommitted
Implement commit message line unwrapping for PR creation
- Add unwrapCommitMessageBody function to join wrapped lines - Preserve blank lines, list items, quotes, and indented content - Add comprehensive tests for various unwrapping scenarios - Matches GitHub's behavior when converting commit messages to PR descriptions Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 7777100 commit 21ff4a3

File tree

2 files changed

+141
-1
lines changed

2 files changed

+141
-1
lines changed

src/github/folderRepositoryManager.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2877,15 +2877,83 @@ const ownedByMe: AsyncPredicate<GitHubRepository> = async repo => {
28772877
export const byRemoteName = (name: string): Predicate<GitHubRepository> => ({ remote: { remoteName } }) =>
28782878
remoteName === name;
28792879

2880+
/**
2881+
* Unwraps lines that were wrapped for conventional commit message formatting (typically at 72 characters).
2882+
* Similar to GitHub's behavior when converting commit messages to PR descriptions.
2883+
*
2884+
* Rules:
2885+
* - Preserves blank lines as paragraph breaks
2886+
* - Preserves lines that start with special characters (list items, quotes, indentation)
2887+
* - Joins consecutive lines that appear to be wrapped mid-sentence
2888+
*/
2889+
function unwrapCommitMessageBody(body: string): string {
2890+
if (!body) {
2891+
return body;
2892+
}
2893+
2894+
const lines = body.split('\n');
2895+
const result: string[] = [];
2896+
let i = 0;
2897+
2898+
while (i < lines.length) {
2899+
const line = lines[i];
2900+
2901+
// Preserve blank lines
2902+
if (line.trim() === '') {
2903+
result.push(line);
2904+
i++;
2905+
continue;
2906+
}
2907+
2908+
// Check if this line should NOT be joined with the next
2909+
// Lines that start with special formatting characters should be preserved
2910+
const shouldPreserveLine = /^[\s*\-+>]|^\d+\./.test(line);
2911+
2912+
if (shouldPreserveLine) {
2913+
result.push(line);
2914+
i++;
2915+
continue;
2916+
}
2917+
2918+
// Start accumulating lines that should be joined
2919+
let joinedLine = line;
2920+
i++;
2921+
2922+
// Keep joining lines until we hit a blank line or a line that shouldn't be joined
2923+
while (i < lines.length) {
2924+
const nextLine = lines[i];
2925+
2926+
// Stop at blank lines
2927+
if (nextLine.trim() === '') {
2928+
break;
2929+
}
2930+
2931+
// Stop at lines that start with special formatting
2932+
if (/^[\s*\-+>]|^\d+\./.test(nextLine)) {
2933+
break;
2934+
}
2935+
2936+
// Join this line with a space
2937+
joinedLine += ' ' + nextLine;
2938+
i++;
2939+
}
2940+
2941+
result.push(joinedLine);
2942+
}
2943+
2944+
return result.join('\n');
2945+
}
2946+
28802947
export const titleAndBodyFrom = async (promise: Promise<string | undefined>): Promise<{ title: string; body: string } | undefined> => {
28812948
const message = await promise;
28822949
if (!message) {
28832950
return;
28842951
}
28852952
const idxLineBreak = message.indexOf('\n');
2953+
const rawBody = idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim();
28862954
return {
28872955
title: idxLineBreak === -1 ? message : message.substr(0, idxLineBreak),
28882956

2889-
body: idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(),
2957+
body: unwrapCommitMessageBody(rawBody),
28902958
};
28912959
};

src/test/github/folderRepositoryManager.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,76 @@ describe('titleAndBodyFrom', function () {
9494
assert.strictEqual(result?.title, 'title');
9595
assert.strictEqual(result?.body, '');
9696
});
97+
98+
it('unwraps wrapped lines in body', async function () {
99+
const message = Promise.resolve('title\n\nThis is a long line that has been wrapped at 72 characters\nto fit the conventional commit message format.');
100+
101+
const result = await titleAndBodyFrom(message);
102+
assert.strictEqual(result?.title, 'title');
103+
assert.strictEqual(result?.body, 'This is a long line that has been wrapped at 72 characters to fit the conventional commit message format.');
104+
});
105+
106+
it('preserves blank lines as paragraph breaks', async function () {
107+
const message = Promise.resolve('title\n\nFirst paragraph that is wrapped\nacross multiple lines.\n\nSecond paragraph that is also wrapped\nacross multiple lines.');
108+
109+
const result = await titleAndBodyFrom(message);
110+
assert.strictEqual(result?.title, 'title');
111+
assert.strictEqual(result?.body, 'First paragraph that is wrapped across multiple lines.\n\nSecond paragraph that is also wrapped across multiple lines.');
112+
});
113+
114+
it('preserves list items', async function () {
115+
const message = Promise.resolve('title\n\n- First item\n- Second item\n- Third item');
116+
117+
const result = await titleAndBodyFrom(message);
118+
assert.strictEqual(result?.title, 'title');
119+
assert.strictEqual(result?.body, '- First item\n- Second item\n- Third item');
120+
});
121+
122+
it('preserves numbered list items', async function () {
123+
const message = Promise.resolve('title\n\n1. First item\n2. Second item\n3. Third item');
124+
125+
const result = await titleAndBodyFrom(message);
126+
assert.strictEqual(result?.title, 'title');
127+
assert.strictEqual(result?.body, '1. First item\n2. Second item\n3. Third item');
128+
});
129+
130+
it('preserves indented lines', async function () {
131+
const message = Promise.resolve('title\n\nNormal paragraph.\n\n Indented code block\n More code');
132+
133+
const result = await titleAndBodyFrom(message);
134+
assert.strictEqual(result?.title, 'title');
135+
assert.strictEqual(result?.body, 'Normal paragraph.\n\n Indented code block\n More code');
136+
});
137+
138+
it('unwraps but preserves asterisk list items', async function () {
139+
const message = Promise.resolve('title\n\n* First item\n* Second item');
140+
141+
const result = await titleAndBodyFrom(message);
142+
assert.strictEqual(result?.title, 'title');
143+
assert.strictEqual(result?.body, '* First item\n* Second item');
144+
});
145+
146+
it('handles mixed content with wrapped paragraphs and lists', async function () {
147+
const message = Promise.resolve('title\n\nThis is a paragraph that has been wrapped\nat 72 characters.\n\n- Item 1\n- Item 2\n\nAnother wrapped paragraph\nthat continues here.');
148+
149+
const result = await titleAndBodyFrom(message);
150+
assert.strictEqual(result?.title, 'title');
151+
assert.strictEqual(result?.body, 'This is a paragraph that has been wrapped at 72 characters.\n\n- Item 1\n- Item 2\n\nAnother wrapped paragraph that continues here.');
152+
});
153+
154+
it('preserves lines with special characters at the start', async function () {
155+
const message = Promise.resolve('title\n\n> Quote line 1\n> Quote line 2');
156+
157+
const result = await titleAndBodyFrom(message);
158+
assert.strictEqual(result?.title, 'title');
159+
assert.strictEqual(result?.body, '> Quote line 1\n> Quote line 2');
160+
});
161+
162+
it('handles wrapped lines with punctuation', async function () {
163+
const message = Promise.resolve('title\n\nThis is a sentence.\nThis is another sentence on a new line.');
164+
165+
const result = await titleAndBodyFrom(message);
166+
assert.strictEqual(result?.title, 'title');
167+
assert.strictEqual(result?.body, 'This is a sentence. This is another sentence on a new line.');
168+
});
97169
});

0 commit comments

Comments
 (0)