shilo-cli 0.2.0 → 0.3.1

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.
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Diff de texto por líneas, estilo unified, sin dependencias.
5
+ *
6
+ * Usado por `shilo diff` y por el `--dry-run` de push/pull para mostrar qué
7
+ * cambia realmente entre la versión local y la remota de un archivo.
8
+ *
9
+ * El algoritmo es LCS con programación dinámica: simple y exacto, pero O(n·m)
10
+ * en memoria. Para archivos grandes eso explota, así que por encima de
11
+ * `MAX_CELLS` se degrada a un resumen ("N líneas locales vs M remotas") en vez
12
+ * de intentar el diff completo.
13
+ */
14
+
15
+ const MAX_CELLS = 4_000_000; // ~4M celdas: unos 2000x2000 líneas
16
+ const DEFAULT_CONTEXT = 3;
17
+
18
+ function splitLines(text) {
19
+ if (text === '' || text == null) return [];
20
+ const normalized = String(text).replace(/\r\n/g, '\n');
21
+ const lines = normalized.split('\n');
22
+ // Un texto terminado en \n produce un último elemento vacío que no es línea.
23
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
24
+ return lines;
25
+ }
26
+
27
+ /**
28
+ * Matriz de longitudes LCS entre dos arrays de líneas.
29
+ */
30
+ function lcsTable(a, b) {
31
+ const rows = a.length + 1;
32
+ const cols = b.length + 1;
33
+ const table = new Array(rows);
34
+ for (let i = 0; i < rows; i++) table[i] = new Uint32Array(cols);
35
+
36
+ for (let i = a.length - 1; i >= 0; i--) {
37
+ for (let j = b.length - 1; j >= 0; j--) {
38
+ table[i][j] =
39
+ a[i] === b[j]
40
+ ? table[i + 1][j + 1] + 1
41
+ : Math.max(table[i + 1][j], table[i][j + 1]);
42
+ }
43
+ }
44
+ return table;
45
+ }
46
+
47
+ /**
48
+ * Secuencia de operaciones para transformar `a` en `b`.
49
+ * Devuelve [{ type: 'equal'|'del'|'add', line }] o `null` si es demasiado grande.
50
+ */
51
+ function diffLines(a, b) {
52
+ if ((a.length + 1) * (b.length + 1) > MAX_CELLS) return null;
53
+
54
+ const table = lcsTable(a, b);
55
+ const ops = [];
56
+ let i = 0;
57
+ let j = 0;
58
+
59
+ while (i < a.length && j < b.length) {
60
+ if (a[i] === b[j]) {
61
+ ops.push({ type: 'equal', line: a[i] });
62
+ i++;
63
+ j++;
64
+ } else if (table[i + 1][j] >= table[i][j + 1]) {
65
+ ops.push({ type: 'del', line: a[i] });
66
+ i++;
67
+ } else {
68
+ ops.push({ type: 'add', line: b[j] });
69
+ j++;
70
+ }
71
+ }
72
+ while (i < a.length) ops.push({ type: 'del', line: a[i++] });
73
+ while (j < b.length) ops.push({ type: 'add', line: b[j++] });
74
+
75
+ return ops;
76
+ }
77
+
78
+ /**
79
+ * Agrupa las operaciones en hunks con `context` líneas de contexto, al estilo
80
+ * unified. Devuelve [{ aStart, aCount, bStart, bCount, ops }] (1-indexado).
81
+ */
82
+ function toHunks(ops, context = DEFAULT_CONTEXT) {
83
+ // Índice de línea (1-based) de cada op en cada lado.
84
+ const annotated = [];
85
+ let aLine = 1;
86
+ let bLine = 1;
87
+ for (const op of ops) {
88
+ annotated.push({ ...op, aLine, bLine });
89
+ if (op.type === 'equal') {
90
+ aLine++;
91
+ bLine++;
92
+ } else if (op.type === 'del') {
93
+ aLine++;
94
+ } else {
95
+ bLine++;
96
+ }
97
+ }
98
+
99
+ const changed = annotated
100
+ .map((op, idx) => (op.type === 'equal' ? -1 : idx))
101
+ .filter((idx) => idx >= 0);
102
+ if (changed.length === 0) return [];
103
+
104
+ // Agrupar índices cambiados que estén a <= 2*context de distancia.
105
+ const groups = [];
106
+ let start = changed[0];
107
+ let end = changed[0];
108
+ for (const idx of changed.slice(1)) {
109
+ if (idx - end <= context * 2) {
110
+ end = idx;
111
+ } else {
112
+ groups.push([start, end]);
113
+ start = idx;
114
+ end = idx;
115
+ }
116
+ }
117
+ groups.push([start, end]);
118
+
119
+ return groups.map(([gStart, gEnd]) => {
120
+ const from = Math.max(0, gStart - context);
121
+ const to = Math.min(annotated.length - 1, gEnd + context);
122
+ const slice = annotated.slice(from, to + 1);
123
+
124
+ let aCount = 0;
125
+ let bCount = 0;
126
+ for (const op of slice) {
127
+ if (op.type !== 'add') aCount++;
128
+ if (op.type !== 'del') bCount++;
129
+ }
130
+
131
+ return {
132
+ aStart: aCount ? slice[0].aLine : 0,
133
+ aCount,
134
+ bStart: bCount ? slice[0].bLine : 0,
135
+ bCount,
136
+ ops: slice,
137
+ };
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Diff completo entre dos textos.
143
+ * Devuelve:
144
+ * { identical: true }
145
+ * { tooBig: true, aLines, bLines }
146
+ * { hunks, added, removed, aLines, bLines }
147
+ */
148
+ function diffText(aText, bText, { context = DEFAULT_CONTEXT } = {}) {
149
+ const a = splitLines(aText);
150
+ const b = splitLines(bText);
151
+
152
+ if (aText === bText) return { identical: true, aLines: a.length, bLines: b.length };
153
+
154
+ const ops = diffLines(a, b);
155
+ if (!ops) return { tooBig: true, aLines: a.length, bLines: b.length };
156
+
157
+ const added = ops.filter((o) => o.type === 'add').length;
158
+ const removed = ops.filter((o) => o.type === 'del').length;
159
+ if (added === 0 && removed === 0) {
160
+ return { identical: true, aLines: a.length, bLines: b.length };
161
+ }
162
+
163
+ return { hunks: toHunks(ops, context), added, removed, aLines: a.length, bLines: b.length };
164
+ }
165
+
166
+ /**
167
+ * Render de un resultado de `diffText` a líneas de texto listas para imprimir.
168
+ * `paint` permite inyectar colores (`ui.c`); por defecto sale en plano.
169
+ */
170
+ function renderDiff(result, { paint = null } = {}) {
171
+ const color = paint || { green: (s) => s, red: (s) => s, cyan: (s) => s, dim: (s) => s };
172
+ if (result.identical) return [];
173
+ if (result.tooBig) {
174
+ return [
175
+ color.dim(
176
+ `(archivo demasiado grande para diff línea a línea: ` +
177
+ `${result.aLines} líneas de un lado, ${result.bLines} del otro)`
178
+ ),
179
+ ];
180
+ }
181
+
182
+ const out = [];
183
+ for (const hunk of result.hunks) {
184
+ out.push(
185
+ color.cyan(`@@ -${hunk.aStart},${hunk.aCount} +${hunk.bStart},${hunk.bCount} @@`)
186
+ );
187
+ for (const op of hunk.ops) {
188
+ if (op.type === 'equal') out.push(color.dim(` ${op.line}`));
189
+ else if (op.type === 'del') out.push(color.red(`-${op.line}`));
190
+ else out.push(color.green(`+${op.line}`));
191
+ }
192
+ }
193
+ return out;
194
+ }
195
+
196
+ module.exports = { splitLines, diffLines, diffText, renderDiff, toHunks, MAX_CELLS };