@openvcs/git-plugin 0.3.2-edge.20260603.125 → 0.3.2-edge.20260603.128
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/bin/git.js +130 -0
- package/bin/plugin-request-handler.js +5 -0
- package/package.json +2 -2
- package/src/git.ts +133 -0
- package/src/plugin-request-handler.ts +9 -0
package/bin/git.js
CHANGED
|
@@ -517,6 +517,136 @@ export class GitCommand {
|
|
|
517
517
|
stdin: patch,
|
|
518
518
|
});
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Stages structured hunk/line selections (VCS-agnostic).
|
|
522
|
+
*
|
|
523
|
+
* For each file, fetches the worktree diff, extracts the selected hunks/lines,
|
|
524
|
+
* builds a combined sub-patch, and applies it to the index atomically.
|
|
525
|
+
*/
|
|
526
|
+
stageSelections(selections) {
|
|
527
|
+
const filePatches = [];
|
|
528
|
+
for (const sel of selections) {
|
|
529
|
+
// Fetch only the worktree diff (not --cached) — cached changes are
|
|
530
|
+
// already staged and must not be re-applied.
|
|
531
|
+
const raw = this.runChecked(['diff', '--no-ext-diff', '--no-color', '--', sel.path], 'git-diff-failed').stdout;
|
|
532
|
+
if (!raw.trim())
|
|
533
|
+
continue;
|
|
534
|
+
const lines = this.splitDiffLines(raw);
|
|
535
|
+
if (lines.length === 0)
|
|
536
|
+
continue;
|
|
537
|
+
const normPath = sel.path.replace(/\\/g, '/');
|
|
538
|
+
// Find the first @@ line to separate prelude from hunks
|
|
539
|
+
const firstHunk = lines.findIndex(l => l.startsWith('@@'));
|
|
540
|
+
if (firstHunk < 0)
|
|
541
|
+
continue;
|
|
542
|
+
const prelude = lines.slice(0, firstHunk);
|
|
543
|
+
const rest = lines.slice(firstHunk);
|
|
544
|
+
// Build headerExtras: metadata lines that aren't diff --git, ---, or +++
|
|
545
|
+
const headerExtras = prelude.filter(l => !!l &&
|
|
546
|
+
!l.startsWith('diff --git') &&
|
|
547
|
+
!l.startsWith('--- ') &&
|
|
548
|
+
!l.startsWith('+++ '));
|
|
549
|
+
// Locate all hunk @@ positions within rest
|
|
550
|
+
const starts = [];
|
|
551
|
+
for (let i = 0; i < rest.length; i++) {
|
|
552
|
+
if (rest[i].startsWith('@@'))
|
|
553
|
+
starts.push(i);
|
|
554
|
+
}
|
|
555
|
+
if (starts.length === 0)
|
|
556
|
+
continue;
|
|
557
|
+
starts.push(rest.length);
|
|
558
|
+
const isAdd = prelude.some(l => l.startsWith('--- /dev/null'));
|
|
559
|
+
const isDel = prelude.some(l => l.startsWith('+++ /dev/null'));
|
|
560
|
+
const wantWhole = new Set(sel.whole_hunks.filter(n => Number.isFinite(n)));
|
|
561
|
+
let out = `diff --git a/${normPath} b/${normPath}\n`;
|
|
562
|
+
if (headerExtras.length)
|
|
563
|
+
out += headerExtras.join('\n') + '\n';
|
|
564
|
+
if (isAdd)
|
|
565
|
+
out += `--- /dev/null\n+++ b/${normPath}\n`;
|
|
566
|
+
else if (isDel)
|
|
567
|
+
out += `--- a/${normPath}\n+++ /dev/null\n`;
|
|
568
|
+
else
|
|
569
|
+
out += `--- a/${normPath}\n+++ b/${normPath}\n`;
|
|
570
|
+
for (let h = 0; h < starts.length - 1; h++) {
|
|
571
|
+
const s = starts[h];
|
|
572
|
+
const e = starts[h + 1];
|
|
573
|
+
const block = rest.slice(s, e);
|
|
574
|
+
const header = block[0] || '';
|
|
575
|
+
const m = /@@\s*-([0-9]+),?([0-9]*)\s*\+([0-9]+),?([0-9]*)\s*@@/.exec(header);
|
|
576
|
+
if (!m)
|
|
577
|
+
continue;
|
|
578
|
+
const aStart = parseInt(m[1] || '0', 10) || 0;
|
|
579
|
+
const cStart = parseInt(m[3] || '0', 10) || 0;
|
|
580
|
+
const content = block.slice(1);
|
|
581
|
+
if (wantWhole.has(h)) {
|
|
582
|
+
out += header + '\n' + content.join('\n') + '\n';
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const picksRaw = (sel.partial_hunks && Array.isArray(sel.partial_hunks[h]))
|
|
586
|
+
? sel.partial_hunks[h]
|
|
587
|
+
: (sel.partial_hunks && sel.partial_hunks[h] ? sel.partial_hunks[h] : []);
|
|
588
|
+
const picksAdj = Array.isArray(picksRaw)
|
|
589
|
+
? picksRaw.map((i) => i - 1).filter((i) => i >= 0 && i < content.length)
|
|
590
|
+
: [];
|
|
591
|
+
const pickSet = new Set(picksAdj);
|
|
592
|
+
if (pickSet.size === 0)
|
|
593
|
+
continue;
|
|
594
|
+
// prefix counts for old/new positions
|
|
595
|
+
const prefOld = new Array(content.length + 1).fill(0);
|
|
596
|
+
const prefNew = new Array(content.length + 1).fill(0);
|
|
597
|
+
for (let i = 0; i < content.length; i++) {
|
|
598
|
+
const ch = (content[i] || '')[0] || ' ';
|
|
599
|
+
const isMeta = ch === '\\';
|
|
600
|
+
prefOld[i + 1] = prefOld[i] + (isMeta ? 0 : (ch === '+' ? 0 : 1));
|
|
601
|
+
prefNew[i + 1] = prefNew[i] + (isMeta ? 0 : (ch === '-' ? 0 : 1));
|
|
602
|
+
}
|
|
603
|
+
// Group consecutive selected lines into mini-hunks
|
|
604
|
+
const sorted = Array.from(pickSet).sort((x, y) => x - y);
|
|
605
|
+
let group = [];
|
|
606
|
+
const flush = () => {
|
|
607
|
+
if (group.length === 0)
|
|
608
|
+
return;
|
|
609
|
+
const i0 = group[0];
|
|
610
|
+
const old_start = aStart + prefOld[i0];
|
|
611
|
+
const new_start = cStart + prefNew[i0];
|
|
612
|
+
const slice = group.map(i => content[i]);
|
|
613
|
+
const contentLines = slice.filter(l => (l || '')[0] !== '\\');
|
|
614
|
+
const metaLines = slice.filter(l => (l || '')[0] === '\\');
|
|
615
|
+
const old_count = contentLines.filter(l => { const c = (l || '')[0]; return c !== '+'; }).length;
|
|
616
|
+
const new_count = contentLines.filter(l => { const c = (l || '')[0]; return c !== '-'; }).length;
|
|
617
|
+
if (old_count === 0 && new_count === 0) {
|
|
618
|
+
group = [];
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
out += `@@ -${old_start},${old_count} +${new_start},${new_count} @@\n`;
|
|
622
|
+
out += contentLines.join('\n') + '\n';
|
|
623
|
+
if (metaLines.length)
|
|
624
|
+
out += metaLines.join('\n') + '\n';
|
|
625
|
+
group = [];
|
|
626
|
+
};
|
|
627
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
628
|
+
if (group.length === 0) {
|
|
629
|
+
group.push(sorted[i]);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (sorted[i] === group[group.length - 1] + 1)
|
|
633
|
+
group.push(sorted[i]);
|
|
634
|
+
else {
|
|
635
|
+
flush();
|
|
636
|
+
group.push(sorted[i]);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
flush();
|
|
640
|
+
}
|
|
641
|
+
filePatches.push(out.trimEnd());
|
|
642
|
+
}
|
|
643
|
+
if (filePatches.length === 0)
|
|
644
|
+
return;
|
|
645
|
+
const combinedPatch = filePatches.join('\n') + '\n';
|
|
646
|
+
this.runChecked(['apply', '--cached', '--unidiff-zero'], 'git-stage-patch-failed', {
|
|
647
|
+
stdin: combinedPatch,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
520
650
|
applyReversePatch(patch) {
|
|
521
651
|
this.runChecked(['apply', '-R', '--unidiff-zero'], 'git-apply-reverse-failed', {
|
|
522
652
|
stdin: patch,
|
|
@@ -295,6 +295,11 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
295
295
|
git.stagePatch(asString(params.patch));
|
|
296
296
|
return null;
|
|
297
297
|
}
|
|
298
|
+
stageSelections(params, _context) {
|
|
299
|
+
const git = this.requireGit(params.session_id);
|
|
300
|
+
git.stageSelections(params.selections);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
298
303
|
stagePaths(params, _context) {
|
|
299
304
|
const git = this.requireGit(params.session_id);
|
|
300
305
|
git.stagePaths(asStringArray(params.paths));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/git-plugin",
|
|
3
|
-
"version": "0.3.2-edge.20260603.
|
|
3
|
+
"version": "0.3.2-edge.20260603.128",
|
|
4
4
|
"description": "Git VCS backend plugin for OpenVCS",
|
|
5
5
|
"author": "OpenVCS Contributors",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
|
-
"version": "0.3.2-edge.20260603.
|
|
38
|
+
"version": "0.3.2-edge.20260603.128"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"lint": "tsc -p tsconfig.json --noEmit",
|
package/src/git.ts
CHANGED
|
@@ -698,6 +698,139 @@ export class GitCommand {
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Stages structured hunk/line selections (VCS-agnostic).
|
|
703
|
+
*
|
|
704
|
+
* For each file, fetches the worktree diff, extracts the selected hunks/lines,
|
|
705
|
+
* builds a combined sub-patch, and applies it to the index atomically.
|
|
706
|
+
*/
|
|
707
|
+
stageSelections(
|
|
708
|
+
selections: Array<{ path: string; whole_hunks: number[]; partial_hunks: Record<number, number[]> }>,
|
|
709
|
+
): void {
|
|
710
|
+
const filePatches: string[] = [];
|
|
711
|
+
|
|
712
|
+
for (const sel of selections) {
|
|
713
|
+
// Fetch only the worktree diff (not --cached) — cached changes are
|
|
714
|
+
// already staged and must not be re-applied.
|
|
715
|
+
const raw = this.runChecked(
|
|
716
|
+
['diff', '--no-ext-diff', '--no-color', '--', sel.path],
|
|
717
|
+
'git-diff-failed',
|
|
718
|
+
).stdout;
|
|
719
|
+
if (!raw.trim()) continue;
|
|
720
|
+
|
|
721
|
+
const lines = this.splitDiffLines(raw);
|
|
722
|
+
if (lines.length === 0) continue;
|
|
723
|
+
|
|
724
|
+
const normPath = sel.path.replace(/\\/g, '/');
|
|
725
|
+
|
|
726
|
+
// Find the first @@ line to separate prelude from hunks
|
|
727
|
+
const firstHunk = lines.findIndex(l => l.startsWith('@@'));
|
|
728
|
+
if (firstHunk < 0) continue;
|
|
729
|
+
|
|
730
|
+
const prelude = lines.slice(0, firstHunk);
|
|
731
|
+
const rest = lines.slice(firstHunk);
|
|
732
|
+
|
|
733
|
+
// Build headerExtras: metadata lines that aren't diff --git, ---, or +++
|
|
734
|
+
const headerExtras = prelude.filter(l =>
|
|
735
|
+
!!l &&
|
|
736
|
+
!l.startsWith('diff --git') &&
|
|
737
|
+
!l.startsWith('--- ') &&
|
|
738
|
+
!l.startsWith('+++ ')
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Locate all hunk @@ positions within rest
|
|
742
|
+
const starts: number[] = [];
|
|
743
|
+
for (let i = 0; i < rest.length; i++) {
|
|
744
|
+
if (rest[i].startsWith('@@')) starts.push(i);
|
|
745
|
+
}
|
|
746
|
+
if (starts.length === 0) continue;
|
|
747
|
+
starts.push(rest.length);
|
|
748
|
+
|
|
749
|
+
const isAdd = prelude.some(l => l.startsWith('--- /dev/null'));
|
|
750
|
+
const isDel = prelude.some(l => l.startsWith('+++ /dev/null'));
|
|
751
|
+
const wantWhole = new Set(sel.whole_hunks.filter(n => Number.isFinite(n)));
|
|
752
|
+
|
|
753
|
+
let out = `diff --git a/${normPath} b/${normPath}\n`;
|
|
754
|
+
if (headerExtras.length) out += headerExtras.join('\n') + '\n';
|
|
755
|
+
if (isAdd) out += `--- /dev/null\n+++ b/${normPath}\n`;
|
|
756
|
+
else if (isDel) out += `--- a/${normPath}\n+++ /dev/null\n`;
|
|
757
|
+
else out += `--- a/${normPath}\n+++ b/${normPath}\n`;
|
|
758
|
+
|
|
759
|
+
for (let h = 0; h < starts.length - 1; h++) {
|
|
760
|
+
const s = starts[h];
|
|
761
|
+
const e = starts[h + 1];
|
|
762
|
+
const block = rest.slice(s, e);
|
|
763
|
+
const header = block[0] || '';
|
|
764
|
+
const m = /@@\s*-([0-9]+),?([0-9]*)\s*\+([0-9]+),?([0-9]*)\s*@@/.exec(header);
|
|
765
|
+
if (!m) continue;
|
|
766
|
+
const aStart = parseInt(m[1] || '0', 10) || 0;
|
|
767
|
+
const cStart = parseInt(m[3] || '0', 10) || 0;
|
|
768
|
+
const content = block.slice(1);
|
|
769
|
+
|
|
770
|
+
if (wantWhole.has(h)) {
|
|
771
|
+
out += header + '\n' + content.join('\n') + '\n';
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const picksRaw = (sel.partial_hunks && Array.isArray(sel.partial_hunks[h]))
|
|
776
|
+
? sel.partial_hunks[h]
|
|
777
|
+
: (sel.partial_hunks && sel.partial_hunks[h] ? sel.partial_hunks[h] : []);
|
|
778
|
+
const picksAdj = Array.isArray(picksRaw)
|
|
779
|
+
? picksRaw.map((i: number) => i - 1).filter((i: number) => i >= 0 && i < content.length)
|
|
780
|
+
: [];
|
|
781
|
+
const pickSet = new Set<number>(picksAdj);
|
|
782
|
+
if (pickSet.size === 0) continue;
|
|
783
|
+
|
|
784
|
+
// prefix counts for old/new positions
|
|
785
|
+
const prefOld: number[] = new Array(content.length + 1).fill(0);
|
|
786
|
+
const prefNew: number[] = new Array(content.length + 1).fill(0);
|
|
787
|
+
for (let i = 0; i < content.length; i++) {
|
|
788
|
+
const ch = (content[i] || '')[0] || ' ';
|
|
789
|
+
const isMeta = ch === '\\';
|
|
790
|
+
prefOld[i + 1] = prefOld[i] + (isMeta ? 0 : (ch === '+' ? 0 : 1));
|
|
791
|
+
prefNew[i + 1] = prefNew[i] + (isMeta ? 0 : (ch === '-' ? 0 : 1));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Group consecutive selected lines into mini-hunks
|
|
795
|
+
const sorted = Array.from(pickSet).sort((x, y) => x - y);
|
|
796
|
+
let group: number[] = [];
|
|
797
|
+
|
|
798
|
+
const flush = (): void => {
|
|
799
|
+
if (group.length === 0) return;
|
|
800
|
+
const i0 = group[0];
|
|
801
|
+
const old_start = aStart + prefOld[i0];
|
|
802
|
+
const new_start = cStart + prefNew[i0];
|
|
803
|
+
const slice = group.map(i => content[i]);
|
|
804
|
+
const contentLines = slice.filter(l => (l || '')[0] !== '\\');
|
|
805
|
+
const metaLines = slice.filter(l => (l || '')[0] === '\\');
|
|
806
|
+
const old_count = contentLines.filter(l => { const c = (l || '')[0]; return c !== '+'; }).length;
|
|
807
|
+
const new_count = contentLines.filter(l => { const c = (l || '')[0]; return c !== '-'; }).length;
|
|
808
|
+
if (old_count === 0 && new_count === 0) { group = []; return; }
|
|
809
|
+
out += `@@ -${old_start},${old_count} +${new_start},${new_count} @@\n`;
|
|
810
|
+
out += contentLines.join('\n') + '\n';
|
|
811
|
+
if (metaLines.length) out += metaLines.join('\n') + '\n';
|
|
812
|
+
group = [];
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
816
|
+
if (group.length === 0) { group.push(sorted[i]); continue; }
|
|
817
|
+
if (sorted[i] === group[group.length - 1] + 1) group.push(sorted[i]);
|
|
818
|
+
else { flush(); group.push(sorted[i]); }
|
|
819
|
+
}
|
|
820
|
+
flush();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
filePatches.push(out.trimEnd());
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (filePatches.length === 0) return;
|
|
827
|
+
|
|
828
|
+
const combinedPatch = filePatches.join('\n') + '\n';
|
|
829
|
+
this.runChecked(['apply', '--cached', '--unidiff-zero'], 'git-stage-patch-failed', {
|
|
830
|
+
stdin: combinedPatch,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
701
834
|
applyReversePatch(patch: string): void {
|
|
702
835
|
this.runChecked(['apply', '-R', '--unidiff-zero'], 'git-apply-reverse-failed', {
|
|
703
836
|
stdin: patch,
|
|
@@ -484,6 +484,15 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
484
484
|
return null;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
override stageSelections(
|
|
488
|
+
params: OpenVcs.VcsStageSelectionsParams,
|
|
489
|
+
_context: PluginRuntimeContext,
|
|
490
|
+
): null {
|
|
491
|
+
const git = this.requireGit(params.session_id);
|
|
492
|
+
git.stageSelections(params.selections);
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
487
496
|
override stagePaths(
|
|
488
497
|
params: OpenVcs.VcsStagePathsParams,
|
|
489
498
|
_context: PluginRuntimeContext,
|