@coderook/cli 0.29.4 → 0.30.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/api.js +46 -1
- package/dist/cli/src/cli.js +118 -7
- package/dist/cli/src/diff_command.js +218 -0
- package/dist/desktop-app/src/main/detect.js +91 -5
- package/dist/desktop-app/src/main/rules.js +21 -3
- package/dist/desktop-app/src/main/transfer_progress.js +109 -0
- package/dist/desktop-app/src/main/upload.js +617 -559
- package/dist/desktop-app/src/main/worktree.js +158 -5
- package/dist/desktop-app/src/shared/merge_diff.js +191 -0
- package/package.json +1 -1
|
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
6
|
+
exports.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.CODEROOK_TAIL = exports.CODEROOK_TAIL_HEADING = exports.KEPT_ANYWAY = exports.KEPT_ANYWAY_HEADING = exports.ALSO_LEFT_OUT = exports.ALSO_LEFT_OUT_HEADING = exports.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
7
7
|
exports.starterAuthored = starterAuthored;
|
|
8
8
|
exports.missingStarterRules = missingStarterRules;
|
|
9
|
+
exports.missingEngineRules = missingEngineRules;
|
|
9
10
|
exports.isRepository = isRepository;
|
|
10
11
|
exports.migrateRetiredRules = migrateRetiredRules;
|
|
12
|
+
exports.starterFor = starterFor;
|
|
11
13
|
exports.readRules = readRules;
|
|
12
14
|
exports.writeRules = writeRules;
|
|
13
15
|
exports.collectLayers = collectLayers;
|
|
@@ -31,6 +33,8 @@ const node_fs_1 = require("node:fs");
|
|
|
31
33
|
const promises_1 = require("node:fs/promises");
|
|
32
34
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
35
|
const node_util_1 = require("node:util");
|
|
36
|
+
const detect_js_1 = require("./detect.js");
|
|
37
|
+
const ignore_templates_js_1 = require("./ignore_templates.js");
|
|
34
38
|
const rules_js_1 = require("./rules.js");
|
|
35
39
|
const secret_patterns_js_1 = require("./secret_patterns.js");
|
|
36
40
|
const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
@@ -57,6 +61,73 @@ async function git(root, ...args) {
|
|
|
57
61
|
* secrets are warned about instead (docs/UPLOAD_POLICY.md).
|
|
58
62
|
*/
|
|
59
63
|
exports.STARTER_MARKER = "# CodeRook starter rules — edit freely below";
|
|
64
|
+
/**
|
|
65
|
+
* What github/gitignore has no template for, kept beside whichever it picks.
|
|
66
|
+
*
|
|
67
|
+
* A browser or Electron profile left in a project folder holds files another
|
|
68
|
+
* program keeps open — a LOCK that cannot be read while it runs will stop a
|
|
69
|
+
* save outright — and none of it is the work. No language template mentions
|
|
70
|
+
* them, because no language puts them there.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Where CodeRook and git want different things, and CodeRook wins.
|
|
74
|
+
*
|
|
75
|
+
* A .gitignore is written for a repository, where an archive is something you
|
|
76
|
+
* could rebuild from the source beside it, so github/gitignore leaves
|
|
77
|
+
* `*.unitypackage` out. CodeRook is not that. A version here is meant to be
|
|
78
|
+
* the whole of a folder, and an asset-store package sitting inside `Assets`
|
|
79
|
+
* is not rebuildable from anything in the project — it is a thing somebody
|
|
80
|
+
* bought, and on the other machine it would simply be gone.
|
|
81
|
+
*
|
|
82
|
+
* Measured on a real project: forty-two of them, one per render pipeline and
|
|
83
|
+
* Unity version, all inside `Assets`. Exactly the kind of "missing stuff"
|
|
84
|
+
* that a save is supposed to make impossible.
|
|
85
|
+
*
|
|
86
|
+
* The `.meta` line guards against this application's own layering, and
|
|
87
|
+
* nothing more. Unity's template keeps metas; `*.meta` is build output to
|
|
88
|
+
* Visual Studio and its template says so, and a Unity folder has a .sln in it
|
|
89
|
+
* because Unity writes one. Placed after every template so it is the last
|
|
90
|
+
* word among the rules we stack — it cannot, and should not, outrank a line
|
|
91
|
+
* somebody writes in their own file afterwards.
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* Caches and weights the chosen template has no reason to mention.
|
|
95
|
+
*
|
|
96
|
+
* A github/gitignore template is written for a repository on that platform,
|
|
97
|
+
* not for every way somebody lays a project out. Xcode is the plain case:
|
|
98
|
+
* DerivedData normally sits in your home folder, so GitHub never mentions it,
|
|
99
|
+
* and a project configured to keep it alongside the source sent seven
|
|
100
|
+
* thousand files of build output that the old generic list had caught.
|
|
101
|
+
*
|
|
102
|
+
* Anchored, every one of them. `models/` unanchored is what used to reach
|
|
103
|
+
* into a Unity project's `Assets/Art/models`, which is the whole reason any
|
|
104
|
+
* of this was looked at.
|
|
105
|
+
*/
|
|
106
|
+
exports.ALSO_LEFT_OUT_HEADING = "# Also left out by CodeRook: caches and weights no template mentions.";
|
|
107
|
+
exports.ALSO_LEFT_OUT = `/DerivedData/
|
|
108
|
+
/models/
|
|
109
|
+
*.safetensors
|
|
110
|
+
*.ckpt
|
|
111
|
+
*.pt
|
|
112
|
+
*.pth`;
|
|
113
|
+
exports.KEPT_ANYWAY_HEADING = "# Kept even though the template leaves them out: not rebuildable from this folder.";
|
|
114
|
+
exports.KEPT_ANYWAY = `!/[Aa]ssets/**/*.meta
|
|
115
|
+
!/[Aa]ssets/**/*.unitypackage
|
|
116
|
+
!/[Aa]ssets/**/*.unitypackage.meta`;
|
|
117
|
+
exports.CODEROOK_TAIL_HEADING = "# Left out by CodeRook, whatever the project is.";
|
|
118
|
+
exports.CODEROOK_TAIL = `IndexedDB/
|
|
119
|
+
Local Storage/
|
|
120
|
+
Session Storage/
|
|
121
|
+
Service Worker/
|
|
122
|
+
Network/
|
|
123
|
+
GPUCache/
|
|
124
|
+
Code Cache/
|
|
125
|
+
blob_storage/
|
|
126
|
+
Local State
|
|
127
|
+
Preferences
|
|
128
|
+
|
|
129
|
+
# Archives of the project, inside the project
|
|
130
|
+
*.cbx`;
|
|
60
131
|
exports.STARTER_IGNORE = `${exports.STARTER_MARKER}
|
|
61
132
|
|
|
62
133
|
# Dependencies and generated output
|
|
@@ -177,9 +248,32 @@ function starterAuthored(text) {
|
|
|
177
248
|
* line on purpose, and re-adding it silently would be this application
|
|
178
249
|
* overruling them in their own file.
|
|
179
250
|
*/
|
|
180
|
-
function missingStarterRules(text) {
|
|
251
|
+
function missingStarterRules(text, reference = exports.STARTER_IGNORE) {
|
|
181
252
|
const present = new Set(ruleLines(text));
|
|
182
|
-
return ruleLines(
|
|
253
|
+
return ruleLines(reference).filter((line) => !present.has(line));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The rules this folder's own file is missing for the kind of project it is.
|
|
257
|
+
*
|
|
258
|
+
* The gap nothing could reach. A folder with a `.gitignore` of its own keeps
|
|
259
|
+
* it — that is right, and a real one must always win — but it also meant that
|
|
260
|
+
* every improvement to the rules could never arrive anywhere that needed
|
|
261
|
+
* them. A Unity project whose file had been written before the engine rules
|
|
262
|
+
* existed went on sending its whole import cache, and nothing said so.
|
|
263
|
+
*
|
|
264
|
+
* Reported rather than applied. A person may have deleted a line on purpose,
|
|
265
|
+
* and putting it back without asking is this application overruling them in
|
|
266
|
+
* their own file.
|
|
267
|
+
*/
|
|
268
|
+
async function missingEngineRules(root) {
|
|
269
|
+
const entries = await (0, promises_1.readdir)(root).catch(() => []);
|
|
270
|
+
const kind = (0, detect_js_1.templatesFor)(entries);
|
|
271
|
+
if (!kind.length)
|
|
272
|
+
return { kind, missing: [] };
|
|
273
|
+
const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
|
|
274
|
+
if (shared === null)
|
|
275
|
+
return { kind, missing: [] };
|
|
276
|
+
return { kind, missing: missingStarterRules(shared, await starterFor(root)) };
|
|
183
277
|
}
|
|
184
278
|
/** The shared rules file, committed with the project. */
|
|
185
279
|
exports.IGNORE_FILE = ".gitignore";
|
|
@@ -231,6 +325,58 @@ function migrateRetiredRules(ignore, keep) {
|
|
|
231
325
|
}
|
|
232
326
|
return `${lines.join("\n")}\n`;
|
|
233
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* The rules a folder gets when it has none of its own.
|
|
330
|
+
*
|
|
331
|
+
* github/gitignore's templates have been carried in this application all
|
|
332
|
+
* along, and until now nothing chose one: every folder got the same generic
|
|
333
|
+
* starter. For a game project that is the wrong list. It carries `build/`,
|
|
334
|
+
* `dist/`, `out/` and `models/**` from the JavaScript and Python worlds, none
|
|
335
|
+
* of them anchored, so a folder like `Assets/Art/models` or
|
|
336
|
+
* `Assets/Plugins/Binaries` is matched by a rule that was written about
|
|
337
|
+
* somewhere else entirely — and the files inside it are quietly not sent.
|
|
338
|
+
*
|
|
339
|
+
* So the folder is asked what it is, and the template that knows about its
|
|
340
|
+
* layout is used. GitHub's Unity rules anchor every one of theirs — `/[Oo]bj/`
|
|
341
|
+
* rather than `obj/` — precisely so they cannot reach into `Assets`.
|
|
342
|
+
*
|
|
343
|
+
* The starter still follows, for the things no language template covers: a
|
|
344
|
+
* browser profile left in the folder, and CodeRook's own archives.
|
|
345
|
+
*/
|
|
346
|
+
/**
|
|
347
|
+
* Engines github/gitignore has no template for.
|
|
348
|
+
*
|
|
349
|
+
* Kept deliberately small and anchored. An unanchored `library/` would reach
|
|
350
|
+
* a folder of that name anywhere in the project, which is the mistake the
|
|
351
|
+
* generic starter was already making.
|
|
352
|
+
*/
|
|
353
|
+
const EXTRA_TEMPLATES = {
|
|
354
|
+
cocos: {
|
|
355
|
+
name: "Cocos",
|
|
356
|
+
body: `/[Ll]ibrary/
|
|
357
|
+
/[Tt]emp/
|
|
358
|
+
/[Bb]uild/
|
|
359
|
+
/.creator/`,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
async function starterFor(root) {
|
|
363
|
+
const entries = await (0, promises_1.readdir)(root).catch(() => []);
|
|
364
|
+
const keys = (0, detect_js_1.templatesFor)(entries);
|
|
365
|
+
if (!keys.length)
|
|
366
|
+
return exports.STARTER_IGNORE;
|
|
367
|
+
const parts = [exports.STARTER_MARKER];
|
|
368
|
+
for (const key of keys) {
|
|
369
|
+
const template = ignore_templates_js_1.IGNORE_TEMPLATES[key] ?? EXTRA_TEMPLATES[key];
|
|
370
|
+
if (!template)
|
|
371
|
+
continue;
|
|
372
|
+
const from = ignore_templates_js_1.IGNORE_TEMPLATES[key] ? ", from github/gitignore" : "";
|
|
373
|
+
parts.push("", `# ${template.name}${from}`, template.body.trimEnd());
|
|
374
|
+
}
|
|
375
|
+
parts.push("", exports.ALSO_LEFT_OUT_HEADING, exports.ALSO_LEFT_OUT);
|
|
376
|
+
parts.push("", exports.KEPT_ANYWAY_HEADING, exports.KEPT_ANYWAY);
|
|
377
|
+
parts.push("", exports.CODEROOK_TAIL_HEADING, exports.CODEROOK_TAIL);
|
|
378
|
+
return `${parts.join("\n")}\n`;
|
|
379
|
+
}
|
|
234
380
|
async function readRules(root) {
|
|
235
381
|
const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
|
|
236
382
|
const local = await readIfPresent(node_path_1.default.join(root, exports.LOCAL_EXCLUDE_FILE));
|
|
@@ -239,7 +385,7 @@ async function readRules(root) {
|
|
|
239
385
|
// No .gitignore yet. Anything an earlier build wrote is carried over, so a
|
|
240
386
|
// project that was already set up does not silently lose its rules.
|
|
241
387
|
const migrated = migrateRetiredRules(await readIfPresent(node_path_1.default.join(root, RETIRED_IGNORE)), await readIfPresent(node_path_1.default.join(root, RETIRED_KEEP)));
|
|
242
|
-
return { shared: migrated ??
|
|
388
|
+
return { shared: migrated ?? (await starterFor(root)), local: local ?? "" };
|
|
243
389
|
}
|
|
244
390
|
/** Write the rules where the project, the CLI and git all expect them. */
|
|
245
391
|
async function writeRules(root, rules) {
|
|
@@ -893,7 +1039,14 @@ async function evaluateRules(root, rules, options = {}) {
|
|
|
893
1039
|
// The last rule to speak wins, across every .gitignore that governs
|
|
894
1040
|
// this path. A `!` rule brings the file back and is worth reporting,
|
|
895
1041
|
// because it is how "always include" is expressed.
|
|
896
|
-
|
|
1042
|
+
/*
|
|
1043
|
+
The directories above it count. Asking `decide` about the file alone
|
|
1044
|
+
is what made this screen report a Unity project's whole Library as
|
|
1045
|
+
going up: `/[Ll]ibrary/` speaks about the directory, so no rule
|
|
1046
|
+
matched the file and every one of them looked included — while the
|
|
1047
|
+
save, which has always walked the ancestors, left them all behind.
|
|
1048
|
+
*/
|
|
1049
|
+
const rule = (0, rules_js_1.decidingRule)(relative, false, layers);
|
|
897
1050
|
if (rule?.negated) {
|
|
898
1051
|
keptFiles += 1;
|
|
899
1052
|
keptBytes += size;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Showing what two people actually did to the same file.
|
|
4
|
+
*
|
|
5
|
+
* The merge screen used to offer four buttons — keep yours, keep theirs, keep
|
|
6
|
+
* both, remove it — above nothing at all. You were asked which side wins for
|
|
7
|
+
* a file you could not look at, which is not a decision so much as a guess,
|
|
8
|
+
* and the safe guess ("keep both") leaves the project holding two copies of
|
|
9
|
+
* everything anybody ever disagreed about.
|
|
10
|
+
*
|
|
11
|
+
* So the two sides are laid out line by line. Nothing here is a merge
|
|
12
|
+
* algorithm: the service already tried that and put the file on a Track
|
|
13
|
+
* precisely because the automatic answer was wrong. This is for reading.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.linesOf = linesOf;
|
|
17
|
+
exports.diffLines = diffLines;
|
|
18
|
+
exports.foldUnchanged = foldUnchanged;
|
|
19
|
+
exports.conflictDraft = conflictDraft;
|
|
20
|
+
exports.hasConflictMarkers = hasConflictMarkers;
|
|
21
|
+
/**
|
|
22
|
+
* The lines two texts share, longest run first.
|
|
23
|
+
*
|
|
24
|
+
* A plain longest-common-subsequence table. Both sides of a conflicted file
|
|
25
|
+
* are one file's worth of lines, so the quadratic table is a few hundred
|
|
26
|
+
* kilobytes at worst — and the alternative, a heuristic diff, sometimes
|
|
27
|
+
* reports a change where there is none, which on this screen would mean
|
|
28
|
+
* showing somebody a disagreement that does not exist.
|
|
29
|
+
*/
|
|
30
|
+
function commonRuns(left, right) {
|
|
31
|
+
const table = Array.from({ length: left.length + 1 }, () => new Array(right.length + 1).fill(0));
|
|
32
|
+
for (let a = left.length - 1; a >= 0; a -= 1) {
|
|
33
|
+
for (let b = right.length - 1; b >= 0; b -= 1) {
|
|
34
|
+
table[a][b] =
|
|
35
|
+
left[a] === right[b]
|
|
36
|
+
? table[a + 1][b + 1] + 1
|
|
37
|
+
: Math.max(table[a + 1][b], table[a][b + 1]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return table;
|
|
41
|
+
}
|
|
42
|
+
/** Split into lines without inventing a trailing empty one. */
|
|
43
|
+
function linesOf(text) {
|
|
44
|
+
if (text === "")
|
|
45
|
+
return [];
|
|
46
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
47
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
48
|
+
lines.pop();
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How the saved file and the uploaded file differ, in order.
|
|
53
|
+
*
|
|
54
|
+
* `target` is what the project already held and `candidate` is what was
|
|
55
|
+
* uploaded, named the way the service names them rather than "old" and "new"
|
|
56
|
+
* — neither side is older, which is the whole reason there is a decision to
|
|
57
|
+
* make.
|
|
58
|
+
*/
|
|
59
|
+
function diffLines(target, candidate) {
|
|
60
|
+
const left = linesOf(target);
|
|
61
|
+
const right = linesOf(candidate);
|
|
62
|
+
const table = commonRuns(left, right);
|
|
63
|
+
const out = [];
|
|
64
|
+
let a = 0;
|
|
65
|
+
let b = 0;
|
|
66
|
+
while (a < left.length && b < right.length) {
|
|
67
|
+
if (left[a] === right[b]) {
|
|
68
|
+
out.push({
|
|
69
|
+
side: "same",
|
|
70
|
+
targetLine: a + 1,
|
|
71
|
+
candidateLine: b + 1,
|
|
72
|
+
text: left[a],
|
|
73
|
+
});
|
|
74
|
+
a += 1;
|
|
75
|
+
b += 1;
|
|
76
|
+
}
|
|
77
|
+
else if (table[a + 1][b] >= table[a][b + 1]) {
|
|
78
|
+
out.push({ side: "target", targetLine: a + 1, candidateLine: null, text: left[a] });
|
|
79
|
+
a += 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
out.push({
|
|
83
|
+
side: "candidate",
|
|
84
|
+
targetLine: null,
|
|
85
|
+
candidateLine: b + 1,
|
|
86
|
+
text: right[b],
|
|
87
|
+
});
|
|
88
|
+
b += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
while (a < left.length) {
|
|
92
|
+
out.push({ side: "target", targetLine: a + 1, candidateLine: null, text: left[a] });
|
|
93
|
+
a += 1;
|
|
94
|
+
}
|
|
95
|
+
while (b < right.length) {
|
|
96
|
+
out.push({
|
|
97
|
+
side: "candidate",
|
|
98
|
+
targetLine: null,
|
|
99
|
+
candidateLine: b + 1,
|
|
100
|
+
text: right[b],
|
|
101
|
+
});
|
|
102
|
+
b += 1;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function foldUnchanged(lines, context = 3) {
|
|
107
|
+
const interesting = new Set();
|
|
108
|
+
lines.forEach((line, index) => {
|
|
109
|
+
if (line.side === "same")
|
|
110
|
+
return;
|
|
111
|
+
for (let at = Math.max(0, index - context); at <= Math.min(lines.length - 1, index + context); at += 1) {
|
|
112
|
+
interesting.add(at);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
/*
|
|
116
|
+
A file with no differences at all is still worth showing — a conflict can
|
|
117
|
+
be about two people creating the same path with identical content, or
|
|
118
|
+
about a file one side deleted, and folding the whole thing away would
|
|
119
|
+
leave the screen empty with no explanation.
|
|
120
|
+
*/
|
|
121
|
+
if (!interesting.size)
|
|
122
|
+
return [{ kind: "lines", lines }];
|
|
123
|
+
const chunks = [];
|
|
124
|
+
let run = [];
|
|
125
|
+
let skipped = 0;
|
|
126
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
127
|
+
if (interesting.has(index)) {
|
|
128
|
+
if (skipped) {
|
|
129
|
+
chunks.push({ kind: "gap", skipped });
|
|
130
|
+
skipped = 0;
|
|
131
|
+
}
|
|
132
|
+
run.push(lines[index]);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
if (run.length) {
|
|
136
|
+
chunks.push({ kind: "lines", lines: run });
|
|
137
|
+
run = [];
|
|
138
|
+
}
|
|
139
|
+
skipped += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (run.length)
|
|
143
|
+
chunks.push({ kind: "lines", lines: run });
|
|
144
|
+
if (skipped)
|
|
145
|
+
chunks.push({ kind: "gap", skipped });
|
|
146
|
+
return chunks;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A starting point for somebody who wants to edit rather than choose.
|
|
150
|
+
*
|
|
151
|
+
* Both sides, marked the way every merge tool has marked them since RCS, so
|
|
152
|
+
* the text is immediately familiar and every editor already highlights it.
|
|
153
|
+
* Agreed lines appear once; the rest are stacked with the markers between
|
|
154
|
+
* them, and it is the person's job to delete the markers along with whatever
|
|
155
|
+
* they decided against.
|
|
156
|
+
*/
|
|
157
|
+
function conflictDraft(target, candidate, labels = {
|
|
158
|
+
target: "saved on the project",
|
|
159
|
+
candidate: "your upload",
|
|
160
|
+
}) {
|
|
161
|
+
const out = [];
|
|
162
|
+
let held = { target: [], candidate: [] };
|
|
163
|
+
const flush = () => {
|
|
164
|
+
if (!held.target.length && !held.candidate.length)
|
|
165
|
+
return;
|
|
166
|
+
out.push(`<<<<<<< ${labels.target}`);
|
|
167
|
+
out.push(...held.target);
|
|
168
|
+
out.push("=======");
|
|
169
|
+
out.push(...held.candidate);
|
|
170
|
+
out.push(`>>>>>>> ${labels.candidate}`);
|
|
171
|
+
held = { target: [], candidate: [] };
|
|
172
|
+
};
|
|
173
|
+
for (const line of diffLines(target, candidate)) {
|
|
174
|
+
if (line.side === "same") {
|
|
175
|
+
flush();
|
|
176
|
+
out.push(line.text);
|
|
177
|
+
}
|
|
178
|
+
else if (line.side === "target") {
|
|
179
|
+
held.target.push(line.text);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
held.candidate.push(line.text);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
flush();
|
|
186
|
+
return out.join("\n");
|
|
187
|
+
}
|
|
188
|
+
/** Whether any conflict marker is still in the text somebody is about to send. */
|
|
189
|
+
function hasConflictMarkers(text) {
|
|
190
|
+
return /^(?:<{7}|={7}|>{7})(?:\s|$)/m.test(text);
|
|
191
|
+
}
|