@gadmin2n/cli 0.0.169 → 0.0.170
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/actions/update.action.js +112 -11
- package/lib/template-merge/classify.d.ts +2 -1
- package/lib/template-merge/classify.js +15 -8
- package/lib/template-merge/git-history.d.ts +21 -0
- package/lib/template-merge/git-history.js +93 -0
- package/lib/template-merge/types.d.ts +2 -1
- package/package.json +1 -1
package/actions/update.action.js
CHANGED
|
@@ -23,6 +23,7 @@ const merge3way_1 = require("../lib/template-merge/merge3way");
|
|
|
23
23
|
const binary_detect_1 = require("../lib/template-merge/binary-detect");
|
|
24
24
|
const residual_scan_1 = require("../lib/template-merge/residual-scan");
|
|
25
25
|
const classify_1 = require("../lib/template-merge/classify");
|
|
26
|
+
const git_history_1 = require("../lib/template-merge/git-history");
|
|
26
27
|
const excludes_1 = require("../lib/template-merge/excludes");
|
|
27
28
|
const base_store_1 = require("../lib/template-merge/base-store");
|
|
28
29
|
const materialize_1 = require("../lib/template-merge/materialize");
|
|
@@ -487,6 +488,81 @@ function handleBinary(rel, baseRoot, oursRoot, theirsRoot, opts, conflicts) {
|
|
|
487
488
|
console.warn(chalk.gray(` template(theirs): ${theirsPath}`));
|
|
488
489
|
console.warn(chalk.gray(` base: ${basePath}`));
|
|
489
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Interactive batch picker for a group of files the instance could adopt from
|
|
493
|
+
* the template. Returns the subset the user chose to adopt. Default action is
|
|
494
|
+
* "skip all" so hitting Enter never pulls in unwanted files.
|
|
495
|
+
*/
|
|
496
|
+
function pickAdoptions(label, files) {
|
|
497
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
498
|
+
const { mode } = yield inquirer.prompt([
|
|
499
|
+
{
|
|
500
|
+
type: 'list',
|
|
501
|
+
name: 'mode',
|
|
502
|
+
message: `${files.length} ${label}. What would you like to do?`,
|
|
503
|
+
choices: [
|
|
504
|
+
{ name: 'Skip all — keep them out of your instance (default)', value: 'skip' },
|
|
505
|
+
{ name: 'Adopt all — copy them from the template', value: 'all' },
|
|
506
|
+
{ name: 'Choose per-file', value: 'choose' },
|
|
507
|
+
],
|
|
508
|
+
default: 'skip',
|
|
509
|
+
},
|
|
510
|
+
]);
|
|
511
|
+
if (mode === 'all')
|
|
512
|
+
return [...files];
|
|
513
|
+
if (mode === 'skip')
|
|
514
|
+
return [];
|
|
515
|
+
const { picks } = yield inquirer.prompt([
|
|
516
|
+
{
|
|
517
|
+
type: 'checkbox',
|
|
518
|
+
name: 'picks',
|
|
519
|
+
message: 'Select files to adopt:',
|
|
520
|
+
choices: files.map((f) => ({ name: f, value: f })),
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
return picks;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Decide which `adoptable` (never-tracked) and `deleteModifyConflict`
|
|
528
|
+
* (previously-deleted) files to bring in from the template, then fold the
|
|
529
|
+
* chosen ones into `plan.add` so applyPlan copies them like any other add.
|
|
530
|
+
* Unchosen files are left where they are: adoptable → silently kept absent;
|
|
531
|
+
* deleteModifyConflict → handled by applyPlan's existing conflict path.
|
|
532
|
+
*
|
|
533
|
+
* Resolution policy:
|
|
534
|
+
* --theirs → adopt everything; --ours / --yes → adopt nothing;
|
|
535
|
+
* interactive → batch prompt per group (default skip).
|
|
536
|
+
*/
|
|
537
|
+
function resolveAdoptions(plan, opts, autoYes) {
|
|
538
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
539
|
+
const groups = [
|
|
540
|
+
{ bucket: 'adoptable', label: 'template file(s) your instance has never had' },
|
|
541
|
+
{
|
|
542
|
+
bucket: 'deleteModifyConflict',
|
|
543
|
+
label: 'file(s) you previously deleted that the template has since modified',
|
|
544
|
+
},
|
|
545
|
+
];
|
|
546
|
+
for (const g of groups) {
|
|
547
|
+
const files = plan[g.bucket];
|
|
548
|
+
if (files.length === 0)
|
|
549
|
+
continue;
|
|
550
|
+
let adopt;
|
|
551
|
+
if (opts.theirs)
|
|
552
|
+
adopt = [...files];
|
|
553
|
+
else if (opts.ours || autoYes)
|
|
554
|
+
adopt = [];
|
|
555
|
+
else
|
|
556
|
+
adopt = yield pickAdoptions(g.label, files);
|
|
557
|
+
if (adopt.length === 0)
|
|
558
|
+
continue;
|
|
559
|
+
const adoptSet = new Set(adopt);
|
|
560
|
+
for (const rel of adopt)
|
|
561
|
+
plan.add.push(rel);
|
|
562
|
+
plan[g.bucket] = files.filter((f) => !adoptSet.has(f));
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
490
566
|
function applyPlan(plan, baseRoot, oursRoot, theirsRoot, opts) {
|
|
491
567
|
const conflicts = [];
|
|
492
568
|
for (const rel of plan.add) {
|
|
@@ -603,6 +679,13 @@ function printPlanFileLists(plan, opts) {
|
|
|
603
679
|
console.info(` ${f}`);
|
|
604
680
|
console.info();
|
|
605
681
|
}
|
|
682
|
+
if (plan.adoptable.length > 0) {
|
|
683
|
+
console.info(chalk.cyan(`🆕 Adoptable (optional) — ${plan.adoptable.length} file${plural(plan.adoptable.length)} the template provides that your instance never had`));
|
|
684
|
+
for (const f of plan.adoptable)
|
|
685
|
+
console.info(` ${f}`);
|
|
686
|
+
console.info(chalk.gray(' Not conflicts — skipped by default. Adopt interactively or with --theirs.'));
|
|
687
|
+
console.info();
|
|
688
|
+
}
|
|
606
689
|
if (plan.delete.length > 0) {
|
|
607
690
|
console.info(chalk.red(`🗑 Deleted (template removed) — ${plan.delete.length} file${plural(plan.delete.length)}`));
|
|
608
691
|
for (const f of plan.delete)
|
|
@@ -814,7 +897,8 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
814
897
|
? `(materialized from schematics ${meta.schematicsVersion})`
|
|
815
898
|
: '(not yet established — run `gadmin2 update` once to bootstrap)'}`);
|
|
816
899
|
console.info();
|
|
817
|
-
const
|
|
900
|
+
const oursEverTracked = (0, git_history_1.buildEverTrackedChecker)(instanceDir);
|
|
901
|
+
const plan = (0, classify_1.classifyAll)(baseRoot, instanceDir, theirsRoot, userExcludes, oursEverTracked);
|
|
818
902
|
if (filterFile) {
|
|
819
903
|
printPerFileTriDiff(filterFile, baseRoot, instanceDir, theirsRoot, metaExists);
|
|
820
904
|
return;
|
|
@@ -827,6 +911,11 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
827
911
|
plan.add.forEach((f) => console.info(` ${f}`));
|
|
828
912
|
console.info();
|
|
829
913
|
}
|
|
914
|
+
if (plan.adoptable.length > 0) {
|
|
915
|
+
console.info(chalk.cyan(`🆕 Adoptable — optional, never had (${plan.adoptable.length} files)`));
|
|
916
|
+
plan.adoptable.forEach((f) => console.info(` ${f}`));
|
|
917
|
+
console.info();
|
|
918
|
+
}
|
|
830
919
|
if (plan.delete.length > 0) {
|
|
831
920
|
console.info(chalk.red(`🗑 To delete (${plan.delete.length} files)`));
|
|
832
921
|
plan.delete.forEach((f) => console.info(` ${f}`));
|
|
@@ -847,7 +936,7 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
847
936
|
conflicts.forEach(([k, p]) => console.info(` [${k}] ${p}`));
|
|
848
937
|
console.info();
|
|
849
938
|
}
|
|
850
|
-
if (plan.add.length + plan.delete.length + plan.update.length + conflicts.length === 0) {
|
|
939
|
+
if (plan.add.length + plan.delete.length + plan.update.length + plan.adoptable.length + conflicts.length === 0) {
|
|
851
940
|
console.info(chalk.green('✅ No differences found. Instance matches template.\n'));
|
|
852
941
|
}
|
|
853
942
|
}
|
|
@@ -906,12 +995,19 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
906
995
|
console.error(chalk.red(formatMaterializeError(meta.schematicsVersion, err)));
|
|
907
996
|
process.exit(2);
|
|
908
997
|
}
|
|
909
|
-
const
|
|
998
|
+
const oursEverTracked = (0, git_history_1.buildEverTrackedChecker)(instanceDir);
|
|
999
|
+
const plan = (0, classify_1.classifyAll)(baseHandle.dir, instanceDir, theirsHandle.dir, userExcludes, oursEverTracked);
|
|
910
1000
|
const structural = [
|
|
911
1001
|
...plan.addAddConflict.map((p) => ({ kind: 'add/add', path: p })),
|
|
912
1002
|
...plan.modifyDeleteConflict.map((p) => ({ kind: 'modify/delete', path: p })),
|
|
913
1003
|
...plan.deleteModifyConflict.map((p) => ({ kind: 'delete/modify', path: p })),
|
|
914
1004
|
];
|
|
1005
|
+
// Adoptable files are optional (template features the instance never had),
|
|
1006
|
+
// NOT conflicts — surfaced informationally without affecting clean state.
|
|
1007
|
+
if (plan.adoptable.length > 0) {
|
|
1008
|
+
console.info(chalk.cyan(`Adoptable: ${plan.adoptable.length} optional template file(s) available`) +
|
|
1009
|
+
chalk.gray(' (run `gadmin2 update` to adopt, or `gadmin2 update diff` to list)\n'));
|
|
1010
|
+
}
|
|
915
1011
|
const totalConflicts = residual.length + structural.length;
|
|
916
1012
|
if (totalConflicts === 0) {
|
|
917
1013
|
console.info('Instance state: ' + chalk.green('✅ clean (no unresolved conflicts)\n'));
|
|
@@ -1034,9 +1130,11 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
1034
1130
|
process.exit(1);
|
|
1035
1131
|
}
|
|
1036
1132
|
const baseRoot = baseHandle.dir;
|
|
1037
|
-
// Classify three sides.
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1133
|
+
// Classify three sides. git history lets us tell "instance never had
|
|
1134
|
+
// this file" (adoptable) from "instance deleted it" (deleteModifyConflict).
|
|
1135
|
+
const oursEverTracked = (0, git_history_1.buildEverTrackedChecker)(instanceDir);
|
|
1136
|
+
const plan = (0, classify_1.classifyAll)(baseRoot, instanceDir, theirsRoot, userExcludes, oursEverTracked);
|
|
1137
|
+
const totalChanges = plan.add.length + plan.delete.length + plan.update.length + plan.adoptable.length +
|
|
1040
1138
|
plan.addAddConflict.length + plan.modifyDeleteConflict.length + plan.deleteModifyConflict.length;
|
|
1041
1139
|
if (totalChanges === 0) {
|
|
1042
1140
|
console.info(chalk.green('✅ Instance is up to date with template.\n'));
|
|
@@ -1050,7 +1148,10 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
1050
1148
|
}
|
|
1051
1149
|
else {
|
|
1052
1150
|
// Pre-apply preview + confirm (skipped under --yes).
|
|
1053
|
-
const applyAndReport = () => {
|
|
1151
|
+
const applyAndReport = () => __awaiter(this, void 0, void 0, function* () {
|
|
1152
|
+
// Decide which never-had / previously-deleted files to adopt, then
|
|
1153
|
+
// fold them into plan.add before applying.
|
|
1154
|
+
yield resolveAdoptions(plan, mergeOpts, autoYes);
|
|
1054
1155
|
const conflicts = applyPlan(plan, baseRoot, instanceDir, theirsRoot, mergeOpts);
|
|
1055
1156
|
changesApplied = true;
|
|
1056
1157
|
// Always advance meta to new version, even when conflicts present (invariant #2).
|
|
@@ -1064,13 +1165,13 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
1064
1165
|
try {
|
|
1065
1166
|
(0, executable_paths_1.ensureExecutableBits)(instanceDir);
|
|
1066
1167
|
}
|
|
1067
|
-
catch (
|
|
1168
|
+
catch (_j) {
|
|
1068
1169
|
// best-effort, never fatal
|
|
1069
1170
|
}
|
|
1070
1171
|
printSummary(plan, conflicts, templateName, schematicsVersion);
|
|
1071
|
-
};
|
|
1172
|
+
});
|
|
1072
1173
|
if (autoYes) {
|
|
1073
|
-
applyAndReport();
|
|
1174
|
+
yield applyAndReport();
|
|
1074
1175
|
}
|
|
1075
1176
|
else {
|
|
1076
1177
|
console.info(chalk.bold('\n═══════════════════════════════════════════════════'));
|
|
@@ -1087,7 +1188,7 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
1087
1188
|
},
|
|
1088
1189
|
]);
|
|
1089
1190
|
if (proceedTemplate) {
|
|
1090
|
-
applyAndReport();
|
|
1191
|
+
yield applyAndReport();
|
|
1091
1192
|
}
|
|
1092
1193
|
else {
|
|
1093
1194
|
console.info(chalk.gray('\nTemplate update skipped — base & files unchanged.\n'));
|
|
@@ -6,6 +6,7 @@ export interface ClassifyInput {
|
|
|
6
6
|
oursEqBase?: () => boolean;
|
|
7
7
|
oursEqTheirs?: () => boolean;
|
|
8
8
|
baseEqTheirs?: () => boolean;
|
|
9
|
+
oursEverTracked?: () => boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function classifyFile(input: ClassifyInput): ClassifyResult;
|
|
11
|
-
export declare function classifyAll(baseDir: string, oursDir: string, theirsDir: string, userExcludes: string[]): ClassifiedPlan;
|
|
12
|
+
export declare function classifyAll(baseDir: string, oursDir: string, theirsDir: string, userExcludes: string[], oursEverTracked?: (rel: string) => boolean): ClassifiedPlan;
|
|
@@ -6,7 +6,7 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const excludes_1 = require("./excludes");
|
|
8
8
|
function classifyFile(input) {
|
|
9
|
-
var _a, _b, _c, _d;
|
|
9
|
+
var _a, _b, _c, _d, _e;
|
|
10
10
|
const { baseExists, oursExists, theirsExists } = input;
|
|
11
11
|
if (!baseExists && !oursExists && !theirsExists) {
|
|
12
12
|
throw new Error('classifyFile: impossible state — file in none of the three sides');
|
|
@@ -20,14 +20,20 @@ function classifyFile(input) {
|
|
|
20
20
|
}
|
|
21
21
|
if (baseExists && !oursExists && !theirsExists)
|
|
22
22
|
return 'noop';
|
|
23
|
-
if (baseExists && !oursExists && theirsExists)
|
|
24
|
-
|
|
23
|
+
if (baseExists && !oursExists && theirsExists) {
|
|
24
|
+
// Instance lacks a file the template has. If the instance NEVER tracked it
|
|
25
|
+
// in git, it's a template feature the instance simply never had → optional
|
|
26
|
+
// adoption (adoptable). If it was tracked and later deleted, it's a genuine
|
|
27
|
+
// delete/modify conflict. When we can't tell (no callback / no git), stay
|
|
28
|
+
// conservative → deleteModifyConflict (the historical behaviour).
|
|
29
|
+
return ((_b = input.oursEverTracked) === null || _b === void 0 ? void 0 : _b.call(input)) === false ? 'adoptable' : 'deleteModifyConflict';
|
|
30
|
+
}
|
|
25
31
|
if (baseExists && oursExists && !theirsExists) {
|
|
26
|
-
return ((
|
|
32
|
+
return ((_c = input.oursEqBase) === null || _c === void 0 ? void 0 : _c.call(input)) ? 'delete' : 'modifyDeleteConflict';
|
|
27
33
|
}
|
|
28
34
|
// ✓✓✓ — all three sides exist.
|
|
29
35
|
// Fast path 1: ours already equals theirs → no merge needed.
|
|
30
|
-
if ((
|
|
36
|
+
if ((_d = input.oursEqTheirs) === null || _d === void 0 ? void 0 : _d.call(input))
|
|
31
37
|
return 'keep';
|
|
32
38
|
// Fast path 2: base equals theirs → template hasn't changed for this file
|
|
33
39
|
// since the recorded base. A 3-way merge would produce ours unchanged
|
|
@@ -35,7 +41,7 @@ function classifyFile(input) {
|
|
|
35
41
|
// avoid spurious "Updated" entries on every run when the instance has
|
|
36
42
|
// legitimately diverged from the template — rendered placeholders in
|
|
37
43
|
// init.sql, regenerated yarn.lock files, user-edited .env, etc.
|
|
38
|
-
if ((
|
|
44
|
+
if ((_e = input.baseEqTheirs) === null || _e === void 0 ? void 0 : _e.call(input))
|
|
39
45
|
return 'keep';
|
|
40
46
|
return 'update';
|
|
41
47
|
}
|
|
@@ -43,7 +49,7 @@ exports.classifyFile = classifyFile;
|
|
|
43
49
|
function fileBytesEqual(p, q) {
|
|
44
50
|
return fs.readFileSync(p).equals(fs.readFileSync(q));
|
|
45
51
|
}
|
|
46
|
-
function classifyAll(baseDir, oursDir, theirsDir, userExcludes) {
|
|
52
|
+
function classifyAll(baseDir, oursDir, theirsDir, userExcludes, oursEverTracked) {
|
|
47
53
|
const excludes = [...excludes_1.HARDCODED_EXCLUDES, ...userExcludes];
|
|
48
54
|
const baseFiles = new Set((0, excludes_1.collectFiles)(baseDir, excludes));
|
|
49
55
|
const oursFiles = new Set((0, excludes_1.collectFiles)(oursDir, excludes));
|
|
@@ -51,7 +57,7 @@ function classifyAll(baseDir, oursDir, theirsDir, userExcludes) {
|
|
|
51
57
|
const all = new Set([...baseFiles, ...oursFiles, ...theirsFiles]);
|
|
52
58
|
const plan = {
|
|
53
59
|
add: [], keep: [], noop: [],
|
|
54
|
-
delete: [], update: [],
|
|
60
|
+
delete: [], update: [], adoptable: [],
|
|
55
61
|
addAddConflict: [], modifyDeleteConflict: [], deleteModifyConflict: [],
|
|
56
62
|
};
|
|
57
63
|
for (const rel of [...all].sort()) {
|
|
@@ -65,6 +71,7 @@ function classifyAll(baseDir, oursDir, theirsDir, userExcludes) {
|
|
|
65
71
|
oursEqBase: () => baseExists && oursExists && fileBytesEqual(path.join(baseDir, rel), path.join(oursDir, rel)),
|
|
66
72
|
oursEqTheirs: () => oursExists && theirsExists && fileBytesEqual(path.join(oursDir, rel), path.join(theirsDir, rel)),
|
|
67
73
|
baseEqTheirs: () => baseExists && theirsExists && fileBytesEqual(path.join(baseDir, rel), path.join(theirsDir, rel)),
|
|
74
|
+
oursEverTracked: oursEverTracked ? () => oursEverTracked(rel) : undefined,
|
|
68
75
|
});
|
|
69
76
|
plan[result].push(rel);
|
|
70
77
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a checker that answers: "has this relative path EVER been tracked in
|
|
3
|
+
* the instance's git repository?"
|
|
4
|
+
*
|
|
5
|
+
* Used by the 3-way classifier to disambiguate the `base有 / ours无 / theirs有`
|
|
6
|
+
* state:
|
|
7
|
+
* - never tracked → the template carries a file the instance never had
|
|
8
|
+
* (a new feature) → `adoptable` (optional adoption).
|
|
9
|
+
* - was tracked → the instance had it and deleted it on purpose
|
|
10
|
+
* → genuine `deleteModifyConflict`.
|
|
11
|
+
*
|
|
12
|
+
* Safety-first fallback: when the directory is NOT a git repo, or git is not on
|
|
13
|
+
* PATH, we return a checker that reports EVERYTHING as tracked (`true`). That
|
|
14
|
+
* keeps the classifier's conservative default (deleteModifyConflict) and makes
|
|
15
|
+
* this feature a no-op for non-git projects — zero behaviour change for them.
|
|
16
|
+
*
|
|
17
|
+
* A git repo with no commits yet resolves to an empty set → every path reports
|
|
18
|
+
* `false` (never tracked). That's the correct signal: nothing has ever been
|
|
19
|
+
* committed, so any template file the instance lacks is genuinely "never had".
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildEverTrackedChecker(instanceDir: string): (rel: string) => boolean;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildEverTrackedChecker = void 0;
|
|
4
|
+
// cli/lib/template-merge/git-history.ts
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
/**
|
|
7
|
+
* Build a checker that answers: "has this relative path EVER been tracked in
|
|
8
|
+
* the instance's git repository?"
|
|
9
|
+
*
|
|
10
|
+
* Used by the 3-way classifier to disambiguate the `base有 / ours无 / theirs有`
|
|
11
|
+
* state:
|
|
12
|
+
* - never tracked → the template carries a file the instance never had
|
|
13
|
+
* (a new feature) → `adoptable` (optional adoption).
|
|
14
|
+
* - was tracked → the instance had it and deleted it on purpose
|
|
15
|
+
* → genuine `deleteModifyConflict`.
|
|
16
|
+
*
|
|
17
|
+
* Safety-first fallback: when the directory is NOT a git repo, or git is not on
|
|
18
|
+
* PATH, we return a checker that reports EVERYTHING as tracked (`true`). That
|
|
19
|
+
* keeps the classifier's conservative default (deleteModifyConflict) and makes
|
|
20
|
+
* this feature a no-op for non-git projects — zero behaviour change for them.
|
|
21
|
+
*
|
|
22
|
+
* A git repo with no commits yet resolves to an empty set → every path reports
|
|
23
|
+
* `false` (never tracked). That's the correct signal: nothing has ever been
|
|
24
|
+
* committed, so any template file the instance lacks is genuinely "never had".
|
|
25
|
+
*/
|
|
26
|
+
function buildEverTrackedChecker(instanceDir) {
|
|
27
|
+
if (!isGitRepo(instanceDir)) {
|
|
28
|
+
return () => true; // conservative: can't tell → treat as tracked
|
|
29
|
+
}
|
|
30
|
+
const tracked = collectEverTrackedPaths(instanceDir);
|
|
31
|
+
return (rel) => tracked.has(normalizeRel(rel));
|
|
32
|
+
}
|
|
33
|
+
exports.buildEverTrackedChecker = buildEverTrackedChecker;
|
|
34
|
+
function isGitRepo(instanceDir) {
|
|
35
|
+
try {
|
|
36
|
+
const out = (0, child_process_1.execFileSync)('git', ['-C', instanceDir, 'rev-parse', '--is-inside-work-tree'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
|
|
37
|
+
return out.trim() === 'true';
|
|
38
|
+
}
|
|
39
|
+
catch (_a) {
|
|
40
|
+
// ENOENT (git missing) or exit 128 (not a repo) → not usable
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Union of:
|
|
46
|
+
* - every path touched by any commit on any branch (`git log --all --name-only`)
|
|
47
|
+
* - every path currently tracked in the index (`git ls-files`)
|
|
48
|
+
* The first captures files that were added-then-deleted; the second captures
|
|
49
|
+
* staged-but-never-committed files. Together they answer "ever tracked".
|
|
50
|
+
*/
|
|
51
|
+
function collectEverTrackedPaths(instanceDir) {
|
|
52
|
+
const paths = new Set();
|
|
53
|
+
// Historical: all paths ever touched across all refs. Empty on a repo with
|
|
54
|
+
// no commits (git exits 128) — swallowed, leaving the set to be filled by
|
|
55
|
+
// ls-files (also empty for a fresh repo).
|
|
56
|
+
addLines(paths, runGitLines(instanceDir, [
|
|
57
|
+
'log',
|
|
58
|
+
'--all',
|
|
59
|
+
'--pretty=format:',
|
|
60
|
+
'--name-only',
|
|
61
|
+
]));
|
|
62
|
+
// Current index.
|
|
63
|
+
addLines(paths, runGitLines(instanceDir, ['ls-files']));
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
function runGitLines(instanceDir, args) {
|
|
67
|
+
try {
|
|
68
|
+
// core.quotePath=false → git emits UTF-8 paths verbatim instead of
|
|
69
|
+
// octal-escaping non-ASCII bytes inside quotes, so plain string compare works.
|
|
70
|
+
const out = (0, child_process_1.execFileSync)('git', ['-C', instanceDir, '-c', 'core.quotePath=false', ...args], {
|
|
71
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
return out.split('\n');
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
// No commits yet / any git error → treat as "no paths from this source".
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function addLines(set, lines) {
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const rel = normalizeRel(line);
|
|
85
|
+
if (rel)
|
|
86
|
+
set.add(rel);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function normalizeRel(rel) {
|
|
90
|
+
// classify.ts builds rel paths with '/'; git already uses '/'. Trim and
|
|
91
|
+
// strip any surrounding quotes git may add for paths with special chars.
|
|
92
|
+
return rel.trim().replace(/^"|"$/g, '');
|
|
93
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** 单个文件在三方合并中的分类结果。 */
|
|
2
|
-
export declare type ClassifyResult = 'add' | 'keep' | 'noop' | 'delete' | 'update' | 'addAddConflict' | 'modifyDeleteConflict' | 'deleteModifyConflict';
|
|
2
|
+
export declare type ClassifyResult = 'add' | 'keep' | 'noop' | 'delete' | 'update' | 'adoptable' | 'addAddConflict' | 'modifyDeleteConflict' | 'deleteModifyConflict';
|
|
3
3
|
export interface ClassifiedPlan {
|
|
4
4
|
add: string[];
|
|
5
5
|
keep: string[];
|
|
6
6
|
noop: string[];
|
|
7
7
|
delete: string[];
|
|
8
8
|
update: string[];
|
|
9
|
+
adoptable: string[];
|
|
9
10
|
addAddConflict: string[];
|
|
10
11
|
modifyDeleteConflict: string[];
|
|
11
12
|
deleteModifyConflict: string[];
|