@in-the-loop-labs/pair-review 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/css/pr.css +50 -17
- package/public/js/modules/diff-renderer.js +18 -1
- package/public/js/modules/hunk-parser.js +55 -0
- package/public/js/pr.js +46 -40
- package/src/ai/analyzer.js +64 -41
- package/src/ai/prompts/baseline/orchestration/balanced.js +0 -7
- package/src/ai/prompts/baseline/orchestration/fast.js +0 -7
- package/src/ai/prompts/baseline/orchestration/thorough.js +0 -7
- package/src/git/worktree.js +18 -9
- package/src/utils/paths.js +49 -1
package/package.json
CHANGED
package/public/css/pr.css
CHANGED
|
@@ -295,18 +295,6 @@
|
|
|
295
295
|
/* Styles handled by individual file items */
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
.file-item {
|
|
299
|
-
display: flex;
|
|
300
|
-
justify-content: space-between;
|
|
301
|
-
align-items: center;
|
|
302
|
-
padding: 8px 0;
|
|
303
|
-
border-bottom: 1px solid #f6f8fa;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
.file-item:last-child {
|
|
307
|
-
border-bottom: none;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
298
|
.file-name {
|
|
311
299
|
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;
|
|
312
300
|
font-size: 13px;
|
|
@@ -963,6 +951,8 @@
|
|
|
963
951
|
overflow: hidden;
|
|
964
952
|
text-overflow: ellipsis;
|
|
965
953
|
white-space: nowrap;
|
|
954
|
+
flex: 1;
|
|
955
|
+
min-width: 0;
|
|
966
956
|
}
|
|
967
957
|
|
|
968
958
|
.file-item .file-changes {
|
|
@@ -973,16 +963,38 @@
|
|
|
973
963
|
margin-left: 8px;
|
|
974
964
|
}
|
|
975
965
|
|
|
976
|
-
.file-
|
|
966
|
+
.file-additions {
|
|
977
967
|
color: var(--diff-addition-marker, #3fb950);
|
|
978
968
|
}
|
|
979
969
|
|
|
980
|
-
.file-
|
|
981
|
-
color: var(--
|
|
970
|
+
.file-deletions {
|
|
971
|
+
color: var(--diff-deletion-marker, #f85149);
|
|
972
|
+
margin-left: 6px;
|
|
982
973
|
}
|
|
983
974
|
|
|
984
|
-
.file-item[data-status="
|
|
985
|
-
color: var(--
|
|
975
|
+
.file-item[data-status="renamed"] .file-changes {
|
|
976
|
+
color: var(--color-fg-muted, #8b949e);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.file-rename-icon-wrapper {
|
|
980
|
+
display: inline-flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
flex-shrink: 0;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.file-rename-icon {
|
|
986
|
+
color: var(--color-fg-muted, #8b949e);
|
|
987
|
+
margin-right: 4px;
|
|
988
|
+
vertical-align: middle;
|
|
989
|
+
flex-shrink: 0;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.file-rename-old-path {
|
|
993
|
+
opacity: 0.7;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.file-rename-arrow {
|
|
997
|
+
margin: 0 4px;
|
|
986
998
|
}
|
|
987
999
|
|
|
988
1000
|
.file-item.generated {
|
|
@@ -8558,6 +8570,27 @@ body.resizing * {
|
|
|
8558
8570
|
.file-comment-card.user-comment {
|
|
8559
8571
|
/* Override file-comment-card defaults to use user-comment styling */
|
|
8560
8572
|
margin: 0; /* Remove margin since container handles spacing */
|
|
8573
|
+
background: linear-gradient(to right, #f5f0ff 0%, var(--file-comment-bg, #ffffff) 100%);
|
|
8574
|
+
border: 1px solid #8b5cf6;
|
|
8575
|
+
border-left: 4px solid #8b5cf6;
|
|
8576
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
8577
|
+
}
|
|
8578
|
+
|
|
8579
|
+
.file-comment-card.user-comment .file-comment-header {
|
|
8580
|
+
background: transparent;
|
|
8581
|
+
border-bottom-color: rgba(139, 92, 246, 0.1);
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8584
|
+
[data-theme="dark"] .file-comment-card.user-comment {
|
|
8585
|
+
background: linear-gradient(to right, rgba(139, 92, 246, 0.15) 0%, var(--file-comment-bg, #0d1117) 100%);
|
|
8586
|
+
border-color: #a78bfa;
|
|
8587
|
+
border-left-color: #a78bfa;
|
|
8588
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
[data-theme="dark"] .file-comment-card.user-comment .file-comment-header {
|
|
8592
|
+
background: transparent;
|
|
8593
|
+
border-bottom-color: rgba(167, 139, 250, 0.1);
|
|
8561
8594
|
}
|
|
8562
8595
|
|
|
8563
8596
|
/* ==========================================================================
|
|
@@ -333,6 +333,8 @@ class DiffRenderer {
|
|
|
333
333
|
* @param {boolean} [options.isViewed=false] - Whether file is marked as viewed
|
|
334
334
|
* @param {Object} [options.generatedInfo] - Info about generated file (insertions, deletions)
|
|
335
335
|
* @param {Object} [options.fileStats] - File stats for collapsed view {insertions, deletions}
|
|
336
|
+
* @param {boolean} [options.renamed=false] - Whether the file was renamed
|
|
337
|
+
* @param {string|null} [options.renamedFrom=null] - Original file path before rename
|
|
336
338
|
* @param {Function} [options.onToggleCollapse] - Callback for toggling collapse state
|
|
337
339
|
* @param {Function} [options.onToggleViewed] - Callback for toggling viewed state
|
|
338
340
|
* @returns {HTMLElement} File header element
|
|
@@ -344,6 +346,8 @@ class DiffRenderer {
|
|
|
344
346
|
isViewed = false,
|
|
345
347
|
generatedInfo = null,
|
|
346
348
|
fileStats = null,
|
|
349
|
+
renamed = false,
|
|
350
|
+
renamedFrom = null,
|
|
347
351
|
onToggleCollapse = null,
|
|
348
352
|
onToggleViewed = null
|
|
349
353
|
} = options;
|
|
@@ -375,7 +379,20 @@ class DiffRenderer {
|
|
|
375
379
|
// File name
|
|
376
380
|
const fileName = document.createElement('span');
|
|
377
381
|
fileName.className = 'd2h-file-name';
|
|
378
|
-
|
|
382
|
+
if (renamed && renamedFrom) {
|
|
383
|
+
const oldPath = document.createElement('span');
|
|
384
|
+
oldPath.className = 'file-rename-old-path';
|
|
385
|
+
oldPath.textContent = renamedFrom;
|
|
386
|
+
const arrow = document.createElement('span');
|
|
387
|
+
arrow.className = 'file-rename-arrow';
|
|
388
|
+
arrow.textContent = '\u2192';
|
|
389
|
+
const newPath = document.createTextNode(filePath);
|
|
390
|
+
fileName.appendChild(oldPath);
|
|
391
|
+
fileName.appendChild(arrow);
|
|
392
|
+
fileName.appendChild(newPath);
|
|
393
|
+
} else {
|
|
394
|
+
fileName.textContent = filePath;
|
|
395
|
+
}
|
|
379
396
|
fileHeader.appendChild(fileName);
|
|
380
397
|
|
|
381
398
|
// File stats summary (visible in collapsed view)
|
|
@@ -345,6 +345,61 @@ class HunkParser {
|
|
|
345
345
|
static shouldAutoExpand(gapSize) {
|
|
346
346
|
return gapSize < HunkParser.AUTO_EXPAND_THRESHOLD;
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a unified diff patch string into structured blocks (hunks).
|
|
351
|
+
* Each block contains the hunk header info and its content lines.
|
|
352
|
+
* Strips trailing empty-string artifacts from split('\n') on newline-terminated input,
|
|
353
|
+
* which would otherwise be misclassified as context lines and corrupt coordinate calculations.
|
|
354
|
+
* @param {string} patch - Unified diff patch string (may include diff --git headers)
|
|
355
|
+
* @returns {Array<{header: string, oldStart: number, newStart: number, lines: string[]}>}
|
|
356
|
+
*/
|
|
357
|
+
static parseDiffIntoBlocks(patch) {
|
|
358
|
+
const lines = patch.split('\n');
|
|
359
|
+
const blocks = [];
|
|
360
|
+
let currentBlock = null;
|
|
361
|
+
|
|
362
|
+
lines.forEach(line => {
|
|
363
|
+
if (line.startsWith('@@')) {
|
|
364
|
+
if (currentBlock) {
|
|
365
|
+
HunkParser._stripTrailingEmpty(currentBlock);
|
|
366
|
+
blocks.push(currentBlock);
|
|
367
|
+
}
|
|
368
|
+
currentBlock = null;
|
|
369
|
+
|
|
370
|
+
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
371
|
+
if (match) {
|
|
372
|
+
currentBlock = {
|
|
373
|
+
header: line,
|
|
374
|
+
oldStart: parseInt(match[1]),
|
|
375
|
+
newStart: parseInt(match[2]),
|
|
376
|
+
lines: []
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
} else if (currentBlock) {
|
|
380
|
+
currentBlock.lines.push(line);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (currentBlock) {
|
|
385
|
+
HunkParser._stripTrailingEmpty(currentBlock);
|
|
386
|
+
blocks.push(currentBlock);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return blocks;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Remove trailing empty string artifact from a block's lines array.
|
|
394
|
+
* split('\n') on newline-terminated input produces a trailing '' that has no
|
|
395
|
+
* diff prefix (+/-/space) and would be misclassified as a context line.
|
|
396
|
+
* @param {Object} block - Block with lines array to clean up
|
|
397
|
+
*/
|
|
398
|
+
static _stripTrailingEmpty(block) {
|
|
399
|
+
if (block.lines.length > 0 && block.lines[block.lines.length - 1] === '') {
|
|
400
|
+
block.lines.pop();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
348
403
|
}
|
|
349
404
|
|
|
350
405
|
// Make HunkParser available globally in browser
|
package/public/js/pr.js
CHANGED
|
@@ -675,6 +675,8 @@ class PRManager {
|
|
|
675
675
|
isViewed,
|
|
676
676
|
generatedInfo: isGenerated ? this.generatedFiles.get(file.file) : null,
|
|
677
677
|
fileStats,
|
|
678
|
+
renamed: file.renamed || false,
|
|
679
|
+
renamedFrom: file.renamedFrom || null,
|
|
678
680
|
onToggleCollapse: (path) => this.toggleFileCollapse(path),
|
|
679
681
|
onToggleViewed: (path, checked) => this.toggleFileViewed(path, checked)
|
|
680
682
|
});
|
|
@@ -738,40 +740,11 @@ class PRManager {
|
|
|
738
740
|
* @param {string} fileName - File name
|
|
739
741
|
*/
|
|
740
742
|
renderPatch(tbody, patch, fileName) {
|
|
741
|
-
const lines = patch.split('\n');
|
|
742
743
|
let diffPosition = 0; // GitHub diff_position (1-indexed, consecutive)
|
|
743
744
|
let prevBlockEnd = { old: 0, new: 0 };
|
|
744
745
|
let isFirstHunk = true;
|
|
745
746
|
|
|
746
|
-
|
|
747
|
-
const blocks = [];
|
|
748
|
-
let currentBlock = null;
|
|
749
|
-
|
|
750
|
-
lines.forEach(line => {
|
|
751
|
-
if (line.startsWith('@@')) {
|
|
752
|
-
// Start new block
|
|
753
|
-
if (currentBlock) {
|
|
754
|
-
blocks.push(currentBlock);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Parse hunk header
|
|
758
|
-
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
759
|
-
if (match) {
|
|
760
|
-
currentBlock = {
|
|
761
|
-
header: line,
|
|
762
|
-
oldStart: parseInt(match[1]),
|
|
763
|
-
newStart: parseInt(match[2]),
|
|
764
|
-
lines: []
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
} else if (currentBlock) {
|
|
768
|
-
currentBlock.lines.push(line);
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
if (currentBlock) {
|
|
773
|
-
blocks.push(currentBlock);
|
|
774
|
-
}
|
|
747
|
+
const blocks = window.HunkParser.parseDiffIntoBlocks(patch);
|
|
775
748
|
|
|
776
749
|
// Render blocks with gap sections
|
|
777
750
|
blocks.forEach((block, blockIndex) => {
|
|
@@ -784,7 +757,7 @@ class PRManager {
|
|
|
784
757
|
);
|
|
785
758
|
|
|
786
759
|
const gapStartOld = prevBlockEnd.old + 1;
|
|
787
|
-
const gapEndOld = (blockBounds.old
|
|
760
|
+
const gapEndOld = (blockBounds.old ?? block.oldStart) - 1;
|
|
788
761
|
const gapSize = gapEndOld - gapStartOld + 1;
|
|
789
762
|
// Calculate the corresponding NEW line number for correct right-side display
|
|
790
763
|
const gapStartNew = prevBlockEnd.new + 1;
|
|
@@ -889,8 +862,8 @@ class PRManager {
|
|
|
889
862
|
'last'
|
|
890
863
|
);
|
|
891
864
|
prevBlockEnd = {
|
|
892
|
-
old: endBounds.old
|
|
893
|
-
new: endBounds.new
|
|
865
|
+
old: endBounds.old ?? (block.oldStart + block.lines.filter(l => !l.startsWith('+')).length - 1),
|
|
866
|
+
new: endBounds.new ?? (block.newStart + block.lines.filter(l => !l.startsWith('-')).length - 1)
|
|
894
867
|
};
|
|
895
868
|
});
|
|
896
869
|
|
|
@@ -898,7 +871,9 @@ class PRManager {
|
|
|
898
871
|
// This handles the case where there are unchanged lines after the last change
|
|
899
872
|
// Use EOF_SENTINEL (-1) for endLine to indicate "rest of file" (unknown size)
|
|
900
873
|
// The gap is marked as pending validation and will be removed async if no lines exist
|
|
901
|
-
|
|
874
|
+
// Skip for new files: when gapStartOld <= 0, the old file has no content (e.g. @@ -0,0 +1,N @@)
|
|
875
|
+
// so there are no trailing unchanged lines to expand
|
|
876
|
+
if (blocks.length > 0 && prevBlockEnd.old > 0) {
|
|
902
877
|
const gapStartOld = prevBlockEnd.old + 1;
|
|
903
878
|
const gapStartNew = prevBlockEnd.new + 1;
|
|
904
879
|
const gapRow = window.HunkParser.createGapSection(
|
|
@@ -1164,6 +1139,14 @@ class PRManager {
|
|
|
1164
1139
|
const fileName = controls.dataset.fileName;
|
|
1165
1140
|
const startLine = parseInt(controls.dataset.startLine);
|
|
1166
1141
|
|
|
1142
|
+
// Safety net: remove gaps with invalid start lines (should not occur after
|
|
1143
|
+
// the prevBlockEnd.old > 0 guard in renderPatch, but handles edge cases defensively)
|
|
1144
|
+
if (startLine <= 0) {
|
|
1145
|
+
console.debug('Removing EOF gap with invalid startLine:', startLine, 'for file:', fileName);
|
|
1146
|
+
gapRow.remove();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1167
1150
|
try {
|
|
1168
1151
|
const data = await this.fetchFileContent(fileName);
|
|
1169
1152
|
if (!data) {
|
|
@@ -2651,7 +2634,9 @@ class PRManager {
|
|
|
2651
2634
|
additions: file.insertions,
|
|
2652
2635
|
deletions: file.deletions,
|
|
2653
2636
|
binary: file.binary,
|
|
2654
|
-
generated: file.generated || false
|
|
2637
|
+
generated: file.generated || false,
|
|
2638
|
+
renamed: file.renamed || false,
|
|
2639
|
+
renamedFrom: file.renamedFrom || null,
|
|
2655
2640
|
});
|
|
2656
2641
|
} else {
|
|
2657
2642
|
if (!current[part]) current[part] = {};
|
|
@@ -2663,6 +2648,10 @@ class PRManager {
|
|
|
2663
2648
|
}
|
|
2664
2649
|
|
|
2665
2650
|
getFileStatus(file) {
|
|
2651
|
+
if (file.renamed) {
|
|
2652
|
+
if (file.insertions > 0 || file.deletions > 0) return 'modified';
|
|
2653
|
+
return 'renamed';
|
|
2654
|
+
}
|
|
2666
2655
|
if (file.binary) return 'modified';
|
|
2667
2656
|
if (!file.deletions || file.deletions === 0) return 'added';
|
|
2668
2657
|
if (!file.insertions || file.insertions === 0) return 'deleted';
|
|
@@ -2685,7 +2674,9 @@ class PRManager {
|
|
|
2685
2674
|
additions: file.insertions,
|
|
2686
2675
|
deletions: file.deletions,
|
|
2687
2676
|
binary: file.binary,
|
|
2688
|
-
generated: file.generated || false
|
|
2677
|
+
generated: file.generated || false,
|
|
2678
|
+
renamed: file.renamed || false,
|
|
2679
|
+
renamedFrom: file.renamedFrom || null,
|
|
2689
2680
|
});
|
|
2690
2681
|
});
|
|
2691
2682
|
|
|
@@ -2770,6 +2761,13 @@ class PRManager {
|
|
|
2770
2761
|
item.dataset.status = file.status;
|
|
2771
2762
|
|
|
2772
2763
|
if (file.generated) item.classList.add('generated');
|
|
2764
|
+
if (file.renamed && file.renamedFrom) {
|
|
2765
|
+
item.title = `Renamed from: ${file.renamedFrom}`;
|
|
2766
|
+
const renameIcon = document.createElement('span');
|
|
2767
|
+
renameIcon.className = 'file-rename-icon-wrapper';
|
|
2768
|
+
renameIcon.innerHTML = '<svg class="file-rename-icon" viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm9.03 6.03-3.25 3.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.97-1.97H4.75a.75.75 0 0 1 0-1.5h4.69L7.47 5.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l3.25 3.25a.75.75 0 0 1 0 1.06Z"/></svg>';
|
|
2769
|
+
item.appendChild(renameIcon);
|
|
2770
|
+
}
|
|
2773
2771
|
|
|
2774
2772
|
const fileName = document.createElement('span');
|
|
2775
2773
|
fileName.className = 'file-name';
|
|
@@ -2781,10 +2779,18 @@ class PRManager {
|
|
|
2781
2779
|
if (file.binary) {
|
|
2782
2780
|
changes.textContent = 'BIN';
|
|
2783
2781
|
} else {
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2782
|
+
if (file.additions > 0) {
|
|
2783
|
+
const addSpan = document.createElement('span');
|
|
2784
|
+
addSpan.className = 'file-additions';
|
|
2785
|
+
addSpan.textContent = `+${file.additions}`;
|
|
2786
|
+
changes.appendChild(addSpan);
|
|
2787
|
+
}
|
|
2788
|
+
if (file.deletions > 0) {
|
|
2789
|
+
const delSpan = document.createElement('span');
|
|
2790
|
+
delSpan.className = 'file-deletions';
|
|
2791
|
+
delSpan.textContent = `-${file.deletions}`;
|
|
2792
|
+
changes.appendChild(delSpan);
|
|
2793
|
+
}
|
|
2788
2794
|
}
|
|
2789
2795
|
|
|
2790
2796
|
item.appendChild(fileName);
|
package/src/ai/analyzer.js
CHANGED
|
@@ -9,7 +9,7 @@ const execPromise = util.promisify(exec);
|
|
|
9
9
|
const logger = require('../utils/logger');
|
|
10
10
|
const { extractJSON } = require('../utils/json-extractor');
|
|
11
11
|
const { getGeneratedFilePatterns } = require('../git/gitattributes');
|
|
12
|
-
const { normalizePath, pathExistsInList } = require('../utils/paths');
|
|
12
|
+
const { normalizePath, pathExistsInList, resolveRenamedFile } = require('../utils/paths');
|
|
13
13
|
const { buildFileLineCountMap, validateSuggestionLineNumbers } = require('../utils/line-validation');
|
|
14
14
|
const { getPromptBuilder } = require('./prompts');
|
|
15
15
|
const { formatValidFiles } = require('./prompts/shared/valid-files');
|
|
@@ -185,7 +185,7 @@ class Analyzer {
|
|
|
185
185
|
level3: levelResults.level3.suggestions
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
-
const orchestrationResult = await this.orchestrateWithAI(allSuggestions, prMetadata, mergedInstructions,
|
|
188
|
+
const orchestrationResult = await this.orchestrateWithAI(allSuggestions, prMetadata, mergedInstructions, worktreePath, { analysisId, tier, progressCallback });
|
|
189
189
|
|
|
190
190
|
// Validate and finalize suggestions
|
|
191
191
|
const finalSuggestions = this.validateAndFinalizeSuggestions(
|
|
@@ -418,6 +418,54 @@ Your suggestions MUST reference the EXACT line where the issue exists:
|
|
|
418
418
|
`;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Build lightweight line number guidance for the orchestration step.
|
|
423
|
+
*
|
|
424
|
+
* Unlike buildLineNumberGuidance() (used for levels 1-3), this does NOT
|
|
425
|
+
* instruct the AI to routinely run git-diff-lines or verify every line number.
|
|
426
|
+
* The orchestration step receives pre-computed suggestions whose line numbers
|
|
427
|
+
* were already determined by the analysis levels; its primary job is to
|
|
428
|
+
* intelligently combine them. However, it retains access to git-diff-lines
|
|
429
|
+
* for cases where it needs to investigate conflicting suggestions or verify
|
|
430
|
+
* a specific concern.
|
|
431
|
+
*
|
|
432
|
+
* @param {string|null} worktreePath - Path to the git worktree (for --cwd option)
|
|
433
|
+
* @returns {string} Orchestration-specific line number guidance
|
|
434
|
+
*/
|
|
435
|
+
buildOrchestrationLineNumberGuidance(worktreePath = null) {
|
|
436
|
+
const scriptCommand = 'git-diff-lines';
|
|
437
|
+
const cwdOption = worktreePath ? ` --cwd "${worktreePath}"` : '';
|
|
438
|
+
const fullCommand = `${scriptCommand}${cwdOption}`;
|
|
439
|
+
return `
|
|
440
|
+
## Line Number Handling
|
|
441
|
+
|
|
442
|
+
You are receiving pre-computed suggestions from the analysis levels. Each suggestion
|
|
443
|
+
already carries a \`line\` number and \`old_or_new\` value determined during analysis.
|
|
444
|
+
Your primary focus is curation and synthesis, not line number verification.
|
|
445
|
+
|
|
446
|
+
**Your responsibilities:**
|
|
447
|
+
- **Preserve line numbers as-is** when passing suggestions through to the output.
|
|
448
|
+
- **Preserve \`old_or_new\` values** from input suggestions.
|
|
449
|
+
- **When merging duplicates or near-duplicates** that reference the same line,
|
|
450
|
+
keep the line number and \`old_or_new\` from the suggestion with the richest
|
|
451
|
+
context (prefer higher-level analysis when in doubt).
|
|
452
|
+
- **When levels conflict** on the line number or \`old_or_new\` for what appears to
|
|
453
|
+
be the same issue, use your judgment based on the nature of the concern:
|
|
454
|
+
- For **architectural or cross-cutting issues**, prefer the suggestion from the
|
|
455
|
+
level with broader context (Level 3 > Level 2 > Level 1).
|
|
456
|
+
- For **precise line-level bugs or typos**, prefer the suggestion from the level
|
|
457
|
+
that targets the specific line most directly (often Level 1, which works
|
|
458
|
+
closest to the raw diff).
|
|
459
|
+
|
|
460
|
+
**If you need to inspect a file diff** (e.g., to resolve conflicting suggestions or
|
|
461
|
+
verify a specific concern), use the annotated diff tool instead of \`git diff\`:
|
|
462
|
+
\`\`\`
|
|
463
|
+
${fullCommand}
|
|
464
|
+
\`\`\`
|
|
465
|
+
All git diff arguments work: \`${fullCommand} HEAD~1\`, \`${fullCommand} -- src/\`
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
|
|
421
469
|
/**
|
|
422
470
|
* Build the section of the prompt that includes custom review instructions
|
|
423
471
|
* @param {string} customInstructions - Custom instructions text
|
|
@@ -454,31 +502,6 @@ Do NOT create suggestions for any files not in this list. If you cannot find iss
|
|
|
454
502
|
`;
|
|
455
503
|
}
|
|
456
504
|
|
|
457
|
-
/**
|
|
458
|
-
* Build the file line counts section for orchestration prompt
|
|
459
|
-
* @param {Map<string, number>} fileLineCountMap - Map of file paths to line counts
|
|
460
|
-
* @returns {string} Prompt section or empty string
|
|
461
|
-
*/
|
|
462
|
-
buildFileLineCountsSection(fileLineCountMap) {
|
|
463
|
-
if (!fileLineCountMap || fileLineCountMap.size === 0) return '';
|
|
464
|
-
|
|
465
|
-
const lines = ['', '## File Line Counts for Validation'];
|
|
466
|
-
for (const [filePath, lineCount] of fileLineCountMap) {
|
|
467
|
-
if (lineCount === 0) {
|
|
468
|
-
// Empty files are valid text files - any line-specific suggestion would be invalid
|
|
469
|
-
lines.push(`- ${filePath}: 0 lines (empty file)`);
|
|
470
|
-
} else if (lineCount > 0) {
|
|
471
|
-
lines.push(`- ${filePath}: ${lineCount} lines`);
|
|
472
|
-
}
|
|
473
|
-
// Skip binary/missing files (lineCount === -1)
|
|
474
|
-
}
|
|
475
|
-
lines.push('');
|
|
476
|
-
lines.push('Verify that all suggestion line numbers are within these bounds.');
|
|
477
|
-
lines.push('If a suggestion has an invalid line number but valuable insight, convert it to a file-level suggestion.');
|
|
478
|
-
lines.push('');
|
|
479
|
-
return lines.join('\n');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
505
|
/**
|
|
483
506
|
* Get list of changed files from git diff
|
|
484
507
|
* @param {string} worktreePath - Path to the git worktree
|
|
@@ -556,13 +579,15 @@ Do NOT create suggestions for any files not in this list. If you cannot find iss
|
|
|
556
579
|
}
|
|
557
580
|
|
|
558
581
|
// Create a Set of normalized valid paths for efficient lookup
|
|
559
|
-
|
|
582
|
+
// Resolve git rename syntax (e.g., "tests/{old.js => new.js}" → "tests/new.js")
|
|
583
|
+
// so both the rename syntax path and the plain new filename will match
|
|
584
|
+
const normalizedValidPaths = new Set(validPaths.map(p => normalizePath(resolveRenamedFile(p))));
|
|
560
585
|
|
|
561
586
|
const validSuggestions = [];
|
|
562
587
|
const discardedSuggestions = [];
|
|
563
588
|
|
|
564
589
|
for (const suggestion of suggestions) {
|
|
565
|
-
const normalizedSuggestionPath = normalizePath(suggestion.file);
|
|
590
|
+
const normalizedSuggestionPath = normalizePath(resolveRenamedFile(suggestion.file));
|
|
566
591
|
|
|
567
592
|
if (normalizedValidPaths.has(normalizedSuggestionPath)) {
|
|
568
593
|
validSuggestions.push(suggestion);
|
|
@@ -1417,7 +1442,7 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
|
|
|
1417
1442
|
// Fallback to getValidFilePaths() which properly looks up pr_metadata via review.id
|
|
1418
1443
|
let validFilePaths;
|
|
1419
1444
|
if (changedFiles && changedFiles.length > 0) {
|
|
1420
|
-
validFilePaths = changedFiles.map(f => normalizePath(f));
|
|
1445
|
+
validFilePaths = changedFiles.map(f => normalizePath(resolveRenamedFile(f)));
|
|
1421
1446
|
} else {
|
|
1422
1447
|
// Fallback to pr_metadata lookup via review -> pr_metadata join
|
|
1423
1448
|
validFilePaths = await this.getValidFilePaths(reviewId);
|
|
@@ -1570,12 +1595,13 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
|
|
|
1570
1595
|
}
|
|
1571
1596
|
|
|
1572
1597
|
// Use O(1) Set lookup if validPaths is a Set, otherwise normalize and check
|
|
1573
|
-
|
|
1598
|
+
// Resolve git rename syntax so suggestions for renamed files match
|
|
1599
|
+
const normalizedSuggestionPath = normalizePath(resolveRenamedFile(suggestionPath));
|
|
1574
1600
|
if (validPaths instanceof Set) {
|
|
1575
1601
|
return validPaths.has(normalizedSuggestionPath);
|
|
1576
1602
|
}
|
|
1577
1603
|
// Fallback for array (convert to Set for lookup)
|
|
1578
|
-
const validPathsSet = new Set(validPaths.map(p => normalizePath(p)));
|
|
1604
|
+
const validPathsSet = new Set(validPaths.map(p => normalizePath(resolveRenamedFile(p))));
|
|
1579
1605
|
return validPathsSet.has(normalizedSuggestionPath);
|
|
1580
1606
|
}
|
|
1581
1607
|
|
|
@@ -2273,14 +2299,13 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2273
2299
|
* @param {Object} allSuggestions - Object containing suggestions from all levels: {level1: [...], level2: [...], level3: [...]}
|
|
2274
2300
|
* @param {Object} prMetadata - PR metadata for context
|
|
2275
2301
|
* @param {string} customInstructions - Optional custom instructions to guide prioritization/filtering
|
|
2276
|
-
* @param {Map<string, number>} fileLineCountMap - Optional map of file paths to line counts for validation
|
|
2277
2302
|
* @param {string} worktreePath - Path to the git worktree
|
|
2278
2303
|
* @param {Object} options - Additional options
|
|
2279
2304
|
* @param {string} options.analysisId - Analysis ID for process tracking (enables cancellation)
|
|
2280
2305
|
* @returns {Promise<Array>} Curated suggestions array
|
|
2281
2306
|
*/
|
|
2282
|
-
async orchestrateWithAI(allSuggestions, prMetadata, customInstructions = null,
|
|
2283
|
-
const { analysisId, tier = 'balanced' } = options;
|
|
2307
|
+
async orchestrateWithAI(allSuggestions, prMetadata, customInstructions = null, worktreePath = null, options = {}) {
|
|
2308
|
+
const { analysisId, tier = 'balanced', progressCallback } = options;
|
|
2284
2309
|
logger.section('[Orchestration] AI Orchestration Starting');
|
|
2285
2310
|
|
|
2286
2311
|
const totalSuggestions = (allSuggestions.level1?.length || 0) +
|
|
@@ -2299,7 +2324,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2299
2324
|
const aiProvider = createProvider(this.provider, this.model);
|
|
2300
2325
|
|
|
2301
2326
|
// Build the orchestration prompt
|
|
2302
|
-
const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions,
|
|
2327
|
+
const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions, worktreePath, tier);
|
|
2303
2328
|
|
|
2304
2329
|
// Execute Claude CLI for orchestration
|
|
2305
2330
|
logger.info('[Orchestration] Running AI orchestration to curate and merge suggestions...');
|
|
@@ -2402,12 +2427,11 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2402
2427
|
* @param {Object} allSuggestions - Suggestions from all levels
|
|
2403
2428
|
* @param {Object} prMetadata - PR metadata for context
|
|
2404
2429
|
* @param {string} customInstructions - Optional custom instructions to guide prioritization/filtering
|
|
2405
|
-
* @param {Map<string, number>} fileLineCountMap - Optional map of file paths to line counts for validation
|
|
2406
2430
|
* @param {string} worktreePath - Path to the git worktree
|
|
2407
2431
|
* @param {string} tier - Capability tier: 'fast', 'balanced', or 'thorough' (default: 'balanced')
|
|
2408
2432
|
* @returns {string} Orchestration prompt
|
|
2409
2433
|
*/
|
|
2410
|
-
buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions = null,
|
|
2434
|
+
buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions = null, worktreePath = null, tier = 'balanced') {
|
|
2411
2435
|
logger.debug(`[Orchestration] Building prompt with tier: ${tier}`);
|
|
2412
2436
|
const promptBuilder = getPromptBuilder('orchestration', tier, this.provider);
|
|
2413
2437
|
|
|
@@ -2420,14 +2444,13 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2420
2444
|
const context = {
|
|
2421
2445
|
reviewIntro: `You are orchestrating AI-powered code review suggestions for ${reviewDescription}.`,
|
|
2422
2446
|
customInstructions: customInstructions ? this.buildCustomInstructionsSection(customInstructions) : '',
|
|
2423
|
-
lineNumberGuidance: this.
|
|
2447
|
+
lineNumberGuidance: this.buildOrchestrationLineNumberGuidance(worktreePath),
|
|
2424
2448
|
level1Count: allSuggestions.level1?.length || 0,
|
|
2425
2449
|
level2Count: allSuggestions.level2?.length || 0,
|
|
2426
2450
|
level3Count: allSuggestions.level3?.length || 0,
|
|
2427
2451
|
level1Suggestions: this._formatSuggestionsForOrchestration(allSuggestions.level1),
|
|
2428
2452
|
level2Suggestions: this._formatSuggestionsForOrchestration(allSuggestions.level2),
|
|
2429
|
-
level3Suggestions: this._formatSuggestionsForOrchestration(allSuggestions.level3)
|
|
2430
|
-
fileLineCounts: this.buildFileLineCountsSection(fileLineCountMap)
|
|
2453
|
+
level3Suggestions: this._formatSuggestionsForOrchestration(allSuggestions.level3)
|
|
2431
2454
|
};
|
|
2432
2455
|
|
|
2433
2456
|
return promptBuilder.build(context);
|
|
@@ -32,7 +32,6 @@ const { ORCHESTRATION_INPUT_SCHEMA_DOCS } = require('../../shared/output-schema'
|
|
|
32
32
|
* - {{level1Suggestions}} - Level 1 suggestions as JSON array
|
|
33
33
|
* - {{level2Suggestions}} - Level 2 suggestions as JSON array
|
|
34
34
|
* - {{level3Suggestions}} - Level 3 suggestions as JSON array
|
|
35
|
-
* - {{fileLineCounts}} - File line count validation data (optional)
|
|
36
35
|
*/
|
|
37
36
|
const taggedPrompt = `<section name="role" required="true">
|
|
38
37
|
{{reviewIntro}}
|
|
@@ -74,10 +73,6 @@ ${ORCHESTRATION_INPUT_SCHEMA_DOCS}
|
|
|
74
73
|
{{level3Suggestions}}
|
|
75
74
|
</section>
|
|
76
75
|
|
|
77
|
-
<section name="file-line-counts" optional="true">
|
|
78
|
-
{{fileLineCounts}}
|
|
79
|
-
</section>
|
|
80
|
-
|
|
81
76
|
<section name="intelligent-merging" required="true">
|
|
82
77
|
## Orchestration Guidelines
|
|
83
78
|
|
|
@@ -194,7 +189,6 @@ const sections = [
|
|
|
194
189
|
{ name: 'role-description', required: true },
|
|
195
190
|
{ name: 'custom-instructions', optional: true },
|
|
196
191
|
{ name: 'input-suggestions', locked: true },
|
|
197
|
-
{ name: 'file-line-counts', optional: true },
|
|
198
192
|
{ name: 'intelligent-merging', required: true },
|
|
199
193
|
{ name: 'priority-curation', required: true },
|
|
200
194
|
{ name: 'balanced-output', required: true },
|
|
@@ -216,7 +210,6 @@ const defaultOrder = [
|
|
|
216
210
|
'role-description',
|
|
217
211
|
'custom-instructions',
|
|
218
212
|
'input-suggestions',
|
|
219
|
-
'file-line-counts',
|
|
220
213
|
'intelligent-merging',
|
|
221
214
|
'priority-curation',
|
|
222
215
|
'balanced-output',
|
|
@@ -35,7 +35,6 @@ const { ORCHESTRATION_INPUT_SCHEMA_DOCS } = require('../../shared/output-schema'
|
|
|
35
35
|
* - {{level1Suggestions}} - Level 1 suggestions as JSON array
|
|
36
36
|
* - {{level2Suggestions}} - Level 2 suggestions as JSON array
|
|
37
37
|
* - {{level3Suggestions}} - Level 3 suggestions as JSON array
|
|
38
|
-
* - {{fileLineCounts}} - File line count validation data (optional)
|
|
39
38
|
*/
|
|
40
39
|
const taggedPrompt = `<section name="role" required="true" tier="fast">
|
|
41
40
|
{{reviewIntro}}
|
|
@@ -77,10 +76,6 @@ ${ORCHESTRATION_INPUT_SCHEMA_DOCS}
|
|
|
77
76
|
{{level3Suggestions}}
|
|
78
77
|
</section>
|
|
79
78
|
|
|
80
|
-
<section name="file-line-counts" optional="true" tier="fast,balanced,thorough">
|
|
81
|
-
{{fileLineCounts}}
|
|
82
|
-
</section>
|
|
83
|
-
|
|
84
79
|
<section name="intelligent-merging" required="true" tier="fast">
|
|
85
80
|
## Rules
|
|
86
81
|
Combine related suggestions. Merge overlaps. Preserve unique insights. Never mention levels.
|
|
@@ -149,7 +144,6 @@ const sections = [
|
|
|
149
144
|
{ name: 'role-description', required: true, tier: ['fast'] },
|
|
150
145
|
{ name: 'custom-instructions', optional: true, tier: ['fast', 'balanced', 'thorough'] },
|
|
151
146
|
{ name: 'input-suggestions', locked: true },
|
|
152
|
-
{ name: 'file-line-counts', optional: true, tier: ['fast', 'balanced', 'thorough'] },
|
|
153
147
|
{ name: 'intelligent-merging', required: true, tier: ['fast'] },
|
|
154
148
|
{ name: 'priority-curation', required: true, tier: ['fast'] },
|
|
155
149
|
{ name: 'balanced-output', required: true, tier: ['fast'] },
|
|
@@ -171,7 +165,6 @@ const defaultOrder = [
|
|
|
171
165
|
'role-description',
|
|
172
166
|
'custom-instructions',
|
|
173
167
|
'input-suggestions',
|
|
174
|
-
'file-line-counts',
|
|
175
168
|
'intelligent-merging',
|
|
176
169
|
'priority-curation',
|
|
177
170
|
'balanced-output',
|
|
@@ -39,7 +39,6 @@ const { ORCHESTRATION_INPUT_SCHEMA_DOCS } = require('../../shared/output-schema'
|
|
|
39
39
|
* - {{level1Suggestions}} - Level 1 suggestions as JSON array
|
|
40
40
|
* - {{level2Suggestions}} - Level 2 suggestions as JSON array
|
|
41
41
|
* - {{level3Suggestions}} - Level 3 suggestions as JSON array
|
|
42
|
-
* - {{fileLineCounts}} - File line count validation data (optional)
|
|
43
42
|
*/
|
|
44
43
|
const taggedPrompt = `<section name="role" required="true" tier="thorough">
|
|
45
44
|
{{reviewIntro}}
|
|
@@ -101,10 +100,6 @@ ${ORCHESTRATION_INPUT_SCHEMA_DOCS}
|
|
|
101
100
|
{{level3Suggestions}}
|
|
102
101
|
</section>
|
|
103
102
|
|
|
104
|
-
<section name="file-line-counts" optional="true" tier="balanced,thorough">
|
|
105
|
-
{{fileLineCounts}}
|
|
106
|
-
</section>
|
|
107
|
-
|
|
108
103
|
<section name="intelligent-merging" required="true" tier="thorough">
|
|
109
104
|
## Orchestration Guidelines
|
|
110
105
|
|
|
@@ -375,7 +370,6 @@ const sections = [
|
|
|
375
370
|
{ name: 'reasoning-encouragement', required: true, tier: ['thorough'] },
|
|
376
371
|
{ name: 'custom-instructions', optional: true, tier: ['balanced', 'thorough'] },
|
|
377
372
|
{ name: 'input-suggestions', locked: true },
|
|
378
|
-
{ name: 'file-line-counts', optional: true, tier: ['balanced', 'thorough'] },
|
|
379
373
|
{ name: 'intelligent-merging', required: true, tier: ['thorough'] },
|
|
380
374
|
{ name: 'priority-curation', required: true, tier: ['thorough'] },
|
|
381
375
|
{ name: 'balanced-output', required: true, tier: ['thorough'] },
|
|
@@ -401,7 +395,6 @@ const defaultOrder = [
|
|
|
401
395
|
'reasoning-encouragement',
|
|
402
396
|
'custom-instructions',
|
|
403
397
|
'input-suggestions',
|
|
404
|
-
'file-line-counts',
|
|
405
398
|
'intelligent-merging',
|
|
406
399
|
'priority-curation',
|
|
407
400
|
'balanced-output',
|
package/src/git/worktree.js
CHANGED
|
@@ -6,7 +6,7 @@ const os = require('os');
|
|
|
6
6
|
const { getConfigDir } = require('../config');
|
|
7
7
|
const { WorktreeRepository, generateWorktreeId } = require('../database');
|
|
8
8
|
const { getGeneratedFilePatterns } = require('./gitattributes');
|
|
9
|
-
const { normalizeRepository } = require('../utils/paths');
|
|
9
|
+
const { normalizeRepository, resolveRenamedFile, resolveRenamedFileOld } = require('../utils/paths');
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Git worktree manager for handling PR branch checkouts and diffs
|
|
@@ -294,14 +294,23 @@ class GitWorktreeManager {
|
|
|
294
294
|
// Parse .gitattributes to identify generated files
|
|
295
295
|
const gitattributes = await getGeneratedFilePatterns(worktreePath);
|
|
296
296
|
|
|
297
|
-
return diffSummary.files.map(file =>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
return diffSummary.files.map(file => {
|
|
298
|
+
const resolvedFile = resolveRenamedFile(file.file);
|
|
299
|
+
const isRenamed = resolvedFile !== file.file;
|
|
300
|
+
const result = {
|
|
301
|
+
file: resolvedFile,
|
|
302
|
+
insertions: file.insertions,
|
|
303
|
+
deletions: file.deletions,
|
|
304
|
+
changes: file.changes,
|
|
305
|
+
binary: file.binary || false,
|
|
306
|
+
generated: gitattributes.isGenerated(resolvedFile)
|
|
307
|
+
};
|
|
308
|
+
if (isRenamed) {
|
|
309
|
+
result.renamed = true;
|
|
310
|
+
result.renamedFrom = resolveRenamedFileOld(file.file);
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
});
|
|
305
314
|
|
|
306
315
|
} catch (error) {
|
|
307
316
|
console.error('Error getting changed files:', error);
|
package/src/utils/paths.js
CHANGED
|
@@ -152,10 +152,58 @@ function normalizeRepository(owner, repo) {
|
|
|
152
152
|
return `${trimmedOwner.toLowerCase()}/${trimmedRepo.toLowerCase()}`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Resolve git rename syntax to extract the new filename.
|
|
157
|
+
*
|
|
158
|
+
* Git represents renames with curly-brace syntax:
|
|
159
|
+
* "tests/{old.js => new.js}" → "tests/new.js"
|
|
160
|
+
* "{old-dir => new-dir}/file.js" → "new-dir/file.js"
|
|
161
|
+
* "a/{b => c}/d.js" → "a/c/d.js"
|
|
162
|
+
*
|
|
163
|
+
* @param {string} fileName - File name possibly containing rename syntax
|
|
164
|
+
* @returns {string} Resolved file name with the new path, or original if no rename syntax
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* resolveRenamedFile('tests/{old.js => new.js}') // => 'tests/new.js'
|
|
168
|
+
* resolveRenamedFile('{old-dir => new-dir}/file.js') // => 'new-dir/file.js'
|
|
169
|
+
* resolveRenamedFile('a/{b => c}/d.js') // => 'a/c/d.js'
|
|
170
|
+
* resolveRenamedFile('src/foo.js') // => 'src/foo.js'
|
|
171
|
+
* resolveRenamedFile(null) // => null
|
|
172
|
+
*/
|
|
173
|
+
function resolveRenamedFile(fileName) {
|
|
174
|
+
if (!fileName) return fileName;
|
|
175
|
+
return fileName.replace(/\{[^}]*\s*=>\s*([^}]*)\}/, '$1').replace(/\/+/g, '/').replace(/^\//, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve git rename syntax to extract the OLD filename.
|
|
180
|
+
*
|
|
181
|
+
* Git represents renames with curly-brace syntax:
|
|
182
|
+
* "tests/{old.js => new.js}" → "tests/old.js"
|
|
183
|
+
* "{old-dir => new-dir}/file.js" → "old-dir/file.js"
|
|
184
|
+
* "a/{b => c}/d.js" → "a/b/d.js"
|
|
185
|
+
*
|
|
186
|
+
* @param {string} fileName - File name possibly containing rename syntax
|
|
187
|
+
* @returns {string} Resolved file name with the old path, or original if no rename syntax
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* resolveRenamedFileOld('tests/{old.js => new.js}') // => 'tests/old.js'
|
|
191
|
+
* resolveRenamedFileOld('{old-dir => new-dir}/file.js') // => 'old-dir/file.js'
|
|
192
|
+
* resolveRenamedFileOld('a/{b => c}/d.js') // => 'a/b/d.js'
|
|
193
|
+
* resolveRenamedFileOld('src/foo.js') // => 'src/foo.js'
|
|
194
|
+
* resolveRenamedFileOld(null) // => null
|
|
195
|
+
*/
|
|
196
|
+
function resolveRenamedFileOld(fileName) {
|
|
197
|
+
if (!fileName) return fileName;
|
|
198
|
+
return fileName.replace(/\{([^}]*?)\s*=>\s*[^}]*\}/, '$1').replace(/\/+/g, '/').replace(/^\//, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
module.exports = {
|
|
156
202
|
normalizePath,
|
|
157
203
|
pathsEqual,
|
|
158
204
|
pathExistsInList,
|
|
159
205
|
pathExistsInSet,
|
|
160
|
-
normalizeRepository
|
|
206
|
+
normalizeRepository,
|
|
207
|
+
resolveRenamedFile,
|
|
208
|
+
resolveRenamedFileOld
|
|
161
209
|
};
|