forked from microsoft/vscode-pull-request-github
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgitUtils.ts
More file actions
400 lines (334 loc) · 11.5 KB
/
Copy pathgitUtils.ts
File metadata and controls
400 lines (334 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Repository } from '../api/api';
import { GitApiImpl } from '../api/api1';
/**
* Removes `Co-authored-by:` trailer lines from a commit message body.
*
* These lines are typically added at the bottom of a commit message (for example by
* the Copilot CLI or `git commit --trailer`) and are not useful when the commit body
* is reused as the default PR description.
*/
export function stripCoAuthoredByTrailers(body: string): string {
if (!body) {
return body;
}
const lines = body.split('\n');
const filtered = lines.filter(line => !/^\s*Co-authored-by:\s/i.test(line));
// Remove any trailing blank lines that may have been left behind once the
// trailer block is gone, but preserve internal blank lines / paragraph breaks.
let end = filtered.length;
while (end > 0 && filtered[end - 1].trim() === '') {
end--;
}
return filtered.slice(0, end).join('\n');
}
/**
* Unwraps lines that were wrapped for conventional commit message formatting (typically at 72 characters).
* Similar to GitHub's behavior when converting commit messages to PR descriptions.
*
* Rules:
* - Preserves blank lines as paragraph breaks
* - Preserves fenced code blocks (```)
* - Preserves list items (-, *, +, numbered)
* - Preserves blockquotes (>)
* - Preserves indented code blocks (4+ spaces at start, when not in a list context)
* - Joins consecutive plain text lines that appear to be wrapped mid-sentence
*/
export function unwrapCommitMessageBody(body: string): string {
if (!body) {
return body;
}
// Pattern to detect list item markers at the start of a line and capture the marker
const LIST_ITEM_PATTERN = /^(?<leadingWhitespace>[ \t]*)(?<marker>[*+\-]|\d+\.)(?<markerTrailingWhitespace>[ \t]+)/;
// Pattern to detect blockquote markers
const BLOCKQUOTE_PATTERN = /^[ \t]*>/;
// Pattern to detect fenced code block markers
const FENCE_PATTERN = /^[ \t]*```/;
const getLeadingWhitespaceLength = (text: string): number => text.match(/^[ \t]*/)?.[0].length ?? 0;
const hasHardLineBreak = (text: string): boolean => / {2}$/.test(text);
const appendWithSpace = (base: string, addition: string): string => {
if (!addition) {
return base;
}
return base.length > 0 && !/\s$/.test(base) ? `${base} ${addition}` : `${base}${addition}`;
};
// Get the content indent for a list item (position where actual content starts)
const getListItemContentIndent = (line: string): number => {
const match = line.match(LIST_ITEM_PATTERN);
if (!match?.groups) {
return 0;
}
// Content indent = leading whitespace + marker + space after marker
return match.groups.leadingWhitespace.length + match.groups.marker.length + match.groups.markerTrailingWhitespace.length;
};
const lines = body.split('\n');
const result: string[] = [];
let i = 0;
let inFencedBlock = false;
// Stack stores { markerIndent, contentIndent } for each nesting level
const listStack: { markerIndent: number; contentIndent: number }[] = [];
// Find the active list context for a given line indent
// Returns the content indent if the line is within an active list context
const getActiveListContentIndent = (lineIndent: number): number | undefined => {
for (let idx = listStack.length - 1; idx >= 0; idx--) {
const { markerIndent, contentIndent } = listStack[idx];
// A line is part of a list item if it has at least 1 space indent
// (but less than contentIndent + 4 which would be a code block)
if (lineIndent >= 1 && lineIndent >= markerIndent) {
listStack.length = idx + 1;
return contentIndent;
}
listStack.pop();
}
return undefined;
};
const shouldJoinListContinuation = (lineIndex: number, contentIndent: number, baseLine: string): boolean => {
const currentLine = lines[lineIndex];
if (!currentLine) {
return false;
}
const trimmed = currentLine.trim();
if (!trimmed) {
return false;
}
if (hasHardLineBreak(baseLine) || hasHardLineBreak(currentLine)) {
return false;
}
if (LIST_ITEM_PATTERN.test(currentLine)) {
return false;
}
if (BLOCKQUOTE_PATTERN.test(currentLine) || FENCE_PATTERN.test(currentLine)) {
return false;
}
const currentIndent = getLeadingWhitespaceLength(currentLine);
// Need at least 1 space to be a continuation
if (currentIndent < 1) {
return false;
}
// 4+ spaces beyond content indent is an indented code block
if (currentIndent >= contentIndent + 4) {
return false;
}
return true;
};
while (i < lines.length) {
const line = lines[i];
// Preserve blank lines but don't clear list context
// (multi-paragraph lists are allowed in GitHub markdown)
if (line.trim() === '') {
result.push(line);
i++;
continue;
}
// Check for fenced code block markers
if (FENCE_PATTERN.test(line)) {
inFencedBlock = !inFencedBlock;
result.push(line);
i++;
continue;
}
// Preserve everything inside fenced code blocks
if (inFencedBlock) {
result.push(line);
i++;
continue;
}
const lineIndent = getLeadingWhitespaceLength(line);
const listItemMatch = line.match(LIST_ITEM_PATTERN);
if (listItemMatch?.groups) {
const markerIndent = listItemMatch.groups.leadingWhitespace.length;
const contentIndent = getListItemContentIndent(line);
// Pop list levels that are at or beyond this indent
while (listStack.length && markerIndent <= listStack[listStack.length - 1].markerIndent) {
listStack.pop();
}
listStack.push({ markerIndent, contentIndent });
result.push(line);
i++;
continue;
}
// Handle non-indented lines that should be joined to a previous list item
// This happens when commit messages are wrapped at 72 characters
// Check this BEFORE calling getActiveListContentIndent which would clear the stack
if (listStack.length > 0 && lineIndent === 0 && !LIST_ITEM_PATTERN.test(line)) {
const isBlockquote = BLOCKQUOTE_PATTERN.test(line);
if (!isBlockquote) {
const baseIndex = result.length - 1;
const baseLine = baseIndex >= 0 ? result[baseIndex] : '';
const previousLineIsBlank = baseLine.trim() === '';
if (!previousLineIsBlank && baseIndex >= 0) {
// Join this line and any following non-list-item lines with the previous list item
let joinedLine = baseLine;
let currentIndex = i;
while (currentIndex < lines.length) {
const currentLine = lines[currentIndex];
const trimmed = currentLine.trim();
// Stop at blank lines
if (!trimmed) {
break;
}
// Stop at list items
if (LIST_ITEM_PATTERN.test(currentLine)) {
break;
}
// Stop at blockquotes or fences
if (BLOCKQUOTE_PATTERN.test(currentLine) || FENCE_PATTERN.test(currentLine)) {
break;
}
// Stop at indented code blocks
const currentLineIndent = getLeadingWhitespaceLength(currentLine);
if (currentLineIndent >= 4) {
break;
}
// Stop if previous line has hard line break
if (hasHardLineBreak(joinedLine)) {
break;
}
joinedLine = appendWithSpace(joinedLine, trimmed);
currentIndex++;
}
if (currentIndex > i) {
result[baseIndex] = joinedLine;
i = currentIndex;
continue;
}
}
}
}
const activeContentIndent = getActiveListContentIndent(lineIndent);
const codeIndentThreshold = activeContentIndent !== undefined ? activeContentIndent + 4 : 4;
const isBlockquote = BLOCKQUOTE_PATTERN.test(line);
const isIndentedCode = lineIndent >= codeIndentThreshold;
if (isBlockquote || isIndentedCode) {
result.push(line);
i++;
continue;
}
// Handle list item continuations
if (activeContentIndent !== undefined && lineIndent >= 1) {
const baseIndex = result.length - 1;
// Only try to join with previous line if it's not blank
// Multi-paragraph lists have blank lines that should be preserved
const baseLine = baseIndex >= 0 ? result[baseIndex] : '';
const previousLineIsBlank = baseLine.trim() === '';
if (!previousLineIsBlank && baseIndex >= 0) {
let joinedLine = baseLine;
let appended = false;
let currentIndex = i;
while (
currentIndex < lines.length &&
shouldJoinListContinuation(currentIndex, activeContentIndent, joinedLine)
) {
const continuationText = lines[currentIndex].trim();
if (continuationText) {
joinedLine = appendWithSpace(joinedLine, continuationText);
appended = true;
}
currentIndex++;
}
if (appended) {
result[baseIndex] = joinedLine;
i = currentIndex;
continue;
}
}
// For multi-paragraph continuations or standalone indented lines,
// preserve indentation but unwrap consecutive continuation lines
let joinedLine = line;
i++;
while (i < lines.length) {
const nextLine = lines[i];
if (nextLine.trim() === '') {
break;
}
if (FENCE_PATTERN.test(nextLine)) {
break;
}
if (LIST_ITEM_PATTERN.test(nextLine)) {
break;
}
if (BLOCKQUOTE_PATTERN.test(nextLine)) {
break;
}
const nextIndent = getLeadingWhitespaceLength(nextLine);
// Check for code block
if (nextIndent >= activeContentIndent + 4) {
break;
}
// Must have at least 1 space to be a continuation
if (nextIndent < 1) {
break;
}
// Check for hard line break
if (hasHardLineBreak(joinedLine)) {
break;
}
// Join this line - preserve the original indentation for the first line
joinedLine = appendWithSpace(joinedLine, nextLine.trim());
i++;
}
result.push(joinedLine);
continue;
}
// Start accumulating lines that should be joined (plain text)
let joinedLine = line;
i++;
// Keep joining lines until we hit a blank line or a line that shouldn't be joined
while (i < lines.length) {
const nextLine = lines[i];
// Stop at blank lines
if (nextLine.trim() === '') {
break;
}
// Stop at fenced code blocks
if (FENCE_PATTERN.test(nextLine)) {
break;
}
// Stop at list items
if (LIST_ITEM_PATTERN.test(nextLine)) {
break;
}
// Stop at blockquotes
if (BLOCKQUOTE_PATTERN.test(nextLine)) {
break;
}
// Check if next line is indented code (4+ spaces, when not in a list context)
const nextLeadingSpaces = getLeadingWhitespaceLength(nextLine);
const nextIsIndentedCode = nextLeadingSpaces >= 4;
if (nextIsIndentedCode) {
break;
}
// Join this line with a space
joinedLine = appendWithSpace(joinedLine, nextLine.trim());
i++;
}
result.push(joinedLine);
}
return result.join('\n');
}
/**
* Determines if a repository is a submodule by checking if its path
* appears in any other repository's submodules list.
*/
export function isSubmodule(repo: Repository, git: GitApiImpl): boolean {
const repoPath = repo.rootUri.fsPath;
// Check all other repositories to see if this repo is listed as a submodule
for (const otherRepo of git.repositories) {
if (otherRepo.rootUri.toString() === repo.rootUri.toString()) {
continue; // Skip self
}
// Check if this repo's path appears in the other repo's submodules
for (const submodule of otherRepo.state.submodules) {
// The submodule path is relative to the parent repo, so we need to resolve it
const submodulePath = vscode.Uri.joinPath(otherRepo.rootUri, submodule.path).fsPath;
if (submodulePath === repoPath) {
return true;
}
}
}
return false;
}