shilo-cli 0.7.0 → 0.8.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/README.md +319 -255
- package/bin/shilo.js +197 -165
- package/package.json +1 -1
- package/src/ShiloClient.js +1027 -837
- package/src/args.js +32 -8
- package/src/commands/diff.js +18 -2
- package/src/commands/export.js +226 -0
- package/src/commands/log.js +4 -0
- package/src/commands/pull.js +86 -2
- package/src/commands/push.js +233 -8
- package/src/commands/review.js +101 -0
- package/src/commands/status.js +36 -0
- package/src/exportgit.js +417 -0
- package/src/gitobjects.js +532 -0
- package/src/sync.js +4 -1
- package/src/walk.js +30 -0
package/src/exportgit.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Etapa 5, B3: escribir el grafo de un proyecto como un repositorio Git.
|
|
5
|
+
*
|
|
6
|
+
* Lee por la API (B1), convierte con la Parte A (`gitobjects.js`) y firma con
|
|
7
|
+
* las identidades congeladas (B2). El resultado es un repo Git normal con
|
|
8
|
+
* `main` y `refs/notes/shilo`; llevarlo a GitHub es un `git push` que hace la
|
|
9
|
+
* persona con sus credenciales.
|
|
10
|
+
*
|
|
11
|
+
* Incremental: la correspondencia `oid → sha1` queda en `.git/shilo/mapa`, por
|
|
12
|
+
* tipo. La segunda vez sólo se bajan los árboles y archivos nuevos; los puntos
|
|
13
|
+
* de guardado se bajan y se recalculan todos (son filas, es barato), y cada
|
|
14
|
+
* uno tiene que dar el mismo SHA-1 que la vez anterior (`SHA1_MOVIDO`).
|
|
15
|
+
*
|
|
16
|
+
* Los objetos se escriben sueltos, a mano (zlib en `.git/objects/xx/…`), sin
|
|
17
|
+
* un proceso de `git` por objeto. `git` hace falta igual para crear el repo,
|
|
18
|
+
* mover las refs con compare-and-swap y actualizar la carpeta de trabajo.
|
|
19
|
+
*
|
|
20
|
+
* Los errores llevan `code` estable, que lee el comando para explicar qué
|
|
21
|
+
* hacer. Los de la conversión (`ErrorConversion`) pasan tal cual.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const zlib = require('zlib');
|
|
27
|
+
const { createHash } = require('crypto');
|
|
28
|
+
const { execFileSync } = require('child_process');
|
|
29
|
+
const { convertirGrafo, gitBlob, gitNotas, REF_NOTAS } = require('./gitobjects');
|
|
30
|
+
|
|
31
|
+
const VERSION_MAPA = 1;
|
|
32
|
+
const REF_MAIN = 'refs/heads/main';
|
|
33
|
+
const OID_VACIO = createHash('sha256').update('').digest('hex');
|
|
34
|
+
const CERO = '0'.repeat(40);
|
|
35
|
+
const DESCARGAS_EN_PARALELO = 8;
|
|
36
|
+
|
|
37
|
+
class ErrorExportacion extends Error {
|
|
38
|
+
constructor(code, message, extra = {}) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ErrorExportacion';
|
|
41
|
+
this.code = code;
|
|
42
|
+
Object.assign(this, extra);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Git
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function gitEn(dir) {
|
|
53
|
+
return (args, { input } = {}) => execFileSync('git', ['-C', dir, ...args], {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
input,
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `null` si `git` no está en el PATH. */
|
|
61
|
+
function versionDeGit() {
|
|
62
|
+
try {
|
|
63
|
+
return execFileSync('git', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** El SHA-1 de una ref, o `null` si no existe. */
|
|
70
|
+
function leerRef(git, ref) {
|
|
71
|
+
try {
|
|
72
|
+
return git(['rev-parse', '-q', '--verify', `${ref}^{commit}`]).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Un objeto suelto: `.git/objects/xx/yyyy`, comprimido con zlib. Si ya está, no se toca. */
|
|
79
|
+
function escribirObjeto(gitDir, { tipo, cuerpo, sha1 }) {
|
|
80
|
+
const dir = path.join(gitDir, 'objects', sha1.slice(0, 2));
|
|
81
|
+
const destino = path.join(dir, sha1.slice(2));
|
|
82
|
+
if (fs.existsSync(destino)) return false;
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
const crudo = Buffer.concat([Buffer.from(`${tipo} ${cuerpo.length}\0`), cuerpo]);
|
|
85
|
+
// Temporal y rename, como Git: un corte a la mitad no deja un objeto trunco
|
|
86
|
+
// con el nombre de uno bueno.
|
|
87
|
+
const tmp = `${destino}.tmp-${process.pid}`;
|
|
88
|
+
fs.writeFileSync(tmp, zlib.deflateSync(crudo));
|
|
89
|
+
fs.renameSync(tmp, destino);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// El mapa
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function rutaMapa(gitDir) {
|
|
98
|
+
return path.join(gitDir, 'shilo', 'mapa');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function leerMapa(gitDir) {
|
|
102
|
+
const p = rutaMapa(gitDir);
|
|
103
|
+
if (!fs.existsSync(p)) return null;
|
|
104
|
+
let mapa;
|
|
105
|
+
try {
|
|
106
|
+
mapa = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new ErrorExportacion('MAPA_ILEGIBLE', `No se pudo leer ${p}: ${e.message}`, { path: p });
|
|
109
|
+
}
|
|
110
|
+
if (!mapa || mapa.version !== VERSION_MAPA || !mapa.commits || !mapa.trees || !mapa.blobs) {
|
|
111
|
+
throw new ErrorExportacion('MAPA_ILEGIBLE', `${p} no tiene la forma esperada.`, { path: p });
|
|
112
|
+
}
|
|
113
|
+
return mapa;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function guardarMapa(gitDir, mapa) {
|
|
117
|
+
const p = rutaMapa(gitDir);
|
|
118
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
119
|
+
const tmp = `${p}.tmp-${process.pid}`;
|
|
120
|
+
// Claves ordenadas: los archivos se bajan en paralelo y entran al mapa en el
|
|
121
|
+
// orden en que llegan. Ordenado, el mismo grafo da el mismo archivo.
|
|
122
|
+
const ordenado = (o) => Object.fromEntries(Object.keys(o).sort().map((k) => [k, o[k]]));
|
|
123
|
+
const salida = { ...mapa };
|
|
124
|
+
for (const tipo of ['commits', 'trees', 'blobs']) salida[tipo] = ordenado(mapa[tipo]);
|
|
125
|
+
fs.writeFileSync(tmp, `${JSON.stringify(salida)}\n`);
|
|
126
|
+
fs.renameSync(tmp, p);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Lectura del grafo
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Los árboles que faltan, por niveles: se arranca por las raíces que no están
|
|
135
|
+
* en el mapa y se corta en cada subárbol que ya se exportó.
|
|
136
|
+
*/
|
|
137
|
+
async function bajarArboles(client, projectId, creds, raices, yaExportados) {
|
|
138
|
+
const trees = {};
|
|
139
|
+
let nivel = [...new Set(raices)].filter((oid) => oid !== OID_VACIO && !yaExportados[oid]);
|
|
140
|
+
|
|
141
|
+
while (nivel.length > 0) {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
const r = await client.graphTrees(projectId, nivel, creds);
|
|
144
|
+
creds = r.credentials || creds;
|
|
145
|
+
if (r.faltan.length > 0) {
|
|
146
|
+
throw new ErrorExportacion('GRAFO_INCOMPLETO', `El servidor no tiene ${r.faltan.length} carpeta(s) de la historia.`, { oids: r.faltan });
|
|
147
|
+
}
|
|
148
|
+
Object.assign(trees, r.trees);
|
|
149
|
+
nivel = [...new Set(Object.values(r.trees).flat().filter((e) => e.type === 'tree').map((e) => e.oid))]
|
|
150
|
+
.filter((oid) => oid !== OID_VACIO && !trees[oid] && !yaExportados[oid]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { trees, creds };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Baja los archivos nuevos y los escribe como blobs de Git a medida que llegan,
|
|
158
|
+
* así la memoria no crece con el tamaño del proyecto. Cada uno se compara
|
|
159
|
+
* contra su oid antes de escribirlo. Devuelve `oid → sha1` de lo escrito.
|
|
160
|
+
*/
|
|
161
|
+
async function bajarArchivos(client, projectId, creds, oids, gitDir, onProgreso) {
|
|
162
|
+
const escritos = {};
|
|
163
|
+
let bytes = 0;
|
|
164
|
+
if (oids.length === 0) return { escritos, bytes, creds };
|
|
165
|
+
|
|
166
|
+
const r = await client.graphBlobs(projectId, oids, creds);
|
|
167
|
+
creds = r.credentials || creds;
|
|
168
|
+
if (r.faltan.length > 0) {
|
|
169
|
+
throw new ErrorExportacion('GRAFO_INCOMPLETO', `El servidor no tiene ${r.faltan.length} archivo(s) de la historia.`, { oids: r.faltan });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let hechos = 0;
|
|
173
|
+
const cola = [...oids];
|
|
174
|
+
const trabajador = async () => {
|
|
175
|
+
while (cola.length > 0) {
|
|
176
|
+
const oid = cola.shift();
|
|
177
|
+
const { url } = r.blobs[oid];
|
|
178
|
+
// eslint-disable-next-line no-await-in-loop
|
|
179
|
+
const contenido = url ? await client.downloadBinary(url) : Buffer.alloc(0);
|
|
180
|
+
if (sha256(contenido) !== oid) {
|
|
181
|
+
throw new ErrorExportacion('BLOB_CORRUPTO', `Un archivo bajado no coincide con su identidad (${oid.slice(0, 12)}).`, { oid });
|
|
182
|
+
}
|
|
183
|
+
const blob = gitBlob(contenido);
|
|
184
|
+
escribirObjeto(gitDir, blob);
|
|
185
|
+
escritos[oid] = blob.sha1;
|
|
186
|
+
bytes += contenido.length;
|
|
187
|
+
hechos += 1;
|
|
188
|
+
if (onProgreso) onProgreso(hechos, oids.length);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
await Promise.all(Array.from({ length: Math.min(DESCARGAS_EN_PARALELO, oids.length) }, trabajador));
|
|
192
|
+
|
|
193
|
+
return { escritos, bytes, creds };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// La exportación
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Prepara la carpeta: un repo nuevo, o uno exportado antes de este mismo
|
|
202
|
+
* proyecto. Cualquier otra cosa se rechaza: no se escribe sobre lo ajeno.
|
|
203
|
+
*/
|
|
204
|
+
function prepararDestino(destino, projectId) {
|
|
205
|
+
const gitDir = path.join(destino, '.git');
|
|
206
|
+
const git = gitEn(destino);
|
|
207
|
+
|
|
208
|
+
if (fs.existsSync(rutaMapa(gitDir))) {
|
|
209
|
+
const previo = leerMapa(gitDir);
|
|
210
|
+
if (previo.projectId !== projectId) {
|
|
211
|
+
throw new ErrorExportacion('OTRO_PROYECTO', `${destino} tiene la exportación de otro proyecto (${previo.projectId}).`, { projectId: previo.projectId });
|
|
212
|
+
}
|
|
213
|
+
return { gitDir, git, previo };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (fs.existsSync(destino)) {
|
|
217
|
+
if (!fs.statSync(destino).isDirectory()) {
|
|
218
|
+
throw new ErrorExportacion('DESTINO_OCUPADO', `${destino} existe y no es una carpeta.`);
|
|
219
|
+
}
|
|
220
|
+
if (fs.readdirSync(destino).length > 0) {
|
|
221
|
+
throw new ErrorExportacion('DESTINO_OCUPADO', `${destino} no está vacía y no tiene una exportación anterior.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
226
|
+
git(['init', '-q', '--object-format=sha1']);
|
|
227
|
+
git(['symbolic-ref', 'HEAD', REF_MAIN]);
|
|
228
|
+
// A15: los bytes van crudos. Sin esto, un `core.autocrlf=true` global (el
|
|
229
|
+
// default de Git para Windows) cambiaría los finales de línea al sacar los
|
|
230
|
+
// archivos a la carpeta, y `git status` los mostraría modificados.
|
|
231
|
+
git(['config', 'core.autocrlf', 'false']);
|
|
232
|
+
return { gitDir, git, previo: null };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* La `main` de la carpeta tiene que ser algo que exportamos nosotros: si
|
|
237
|
+
* alguien guardó encima, pisarla le borraría ese trabajo. Se acepta cualquier
|
|
238
|
+
* punto de guardado del mapa, no sólo el último, para que una exportación
|
|
239
|
+
* cortada entre guardar el mapa y mover la ref se pueda reintentar.
|
|
240
|
+
*/
|
|
241
|
+
function comprobarMain(git, previo) {
|
|
242
|
+
const actual = leerRef(git, REF_MAIN);
|
|
243
|
+
if (!actual) return null;
|
|
244
|
+
const nuestros = new Set(Object.values(previo ? previo.commits : {}));
|
|
245
|
+
if (!nuestros.has(actual)) {
|
|
246
|
+
throw new ErrorExportacion('MAIN_AJENA', `main apunta a ${actual.slice(0, 12)}, que no salió de Shilo.`, { sha1: actual });
|
|
247
|
+
}
|
|
248
|
+
return actual;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function hayCambiosLocales(git) {
|
|
252
|
+
return git(['status', '--porcelain', '--untracked-files=no']).trim() !== '';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Exporta el proyecto a `destino`.
|
|
257
|
+
*
|
|
258
|
+
* @param {Object} opciones
|
|
259
|
+
* @param {import('./ShiloClient').ShiloClient} opciones.client
|
|
260
|
+
* @param {string} opciones.projectId
|
|
261
|
+
* @param {Object|null} opciones.creds - `null` para un proyecto público sin sesión
|
|
262
|
+
* @param {string} opciones.destino
|
|
263
|
+
* @param {string} [opciones.baseURL] - se anota en el mapa
|
|
264
|
+
* @param {function} [opciones.onEtapa] - `(nombre, detalle)` para el informe en vivo
|
|
265
|
+
* @param {function} [opciones.onProgreso] - `(hechos, total)` al bajar archivos
|
|
266
|
+
* @returns {Promise<Object>} el informe
|
|
267
|
+
*/
|
|
268
|
+
async function exportarProyecto({ client, projectId, creds, destino, baseURL = null, onEtapa, onProgreso }) {
|
|
269
|
+
const etapa = (nombre, detalle) => { if (onEtapa) onEtapa(nombre, detalle); };
|
|
270
|
+
|
|
271
|
+
if (!versionDeGit()) {
|
|
272
|
+
throw new ErrorExportacion('SIN_GIT', 'No se encontró git en el PATH.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const dest = path.resolve(destino);
|
|
276
|
+
const existia = fs.existsSync(dest);
|
|
277
|
+
const preparado = prepararDestino(dest, projectId);
|
|
278
|
+
try {
|
|
279
|
+
return await exportarEn(preparado, { client, projectId, creds, dest, baseURL, etapa, onProgreso });
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// Una primera exportación que no terminó no deja nada: si quedara el
|
|
282
|
+
// `.git` a medias, el reintento vería la carpeta ocupada.
|
|
283
|
+
if (!preparado.previo) fs.rmSync(existia ? preparado.gitDir : dest, { recursive: true, force: true });
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function exportarEn({ gitDir, git, previo }, { client, projectId, creds, dest, baseURL, etapa, onProgreso }) {
|
|
289
|
+
const mainAntes = comprobarMain(git, previo);
|
|
290
|
+
const enMain = (() => {
|
|
291
|
+
try {
|
|
292
|
+
return git(['symbolic-ref', '-q', 'HEAD']).trim() === REF_MAIN;
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
if (enMain && mainAntes && hayCambiosLocales(git)) {
|
|
298
|
+
throw new ErrorExportacion('CAMBIOS_LOCALES', `${dest} tiene cambios sin guardar sobre main.`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 1. La historia entera, siempre: los puntos de guardado no se siembran.
|
|
302
|
+
etapa('historia');
|
|
303
|
+
const hist = await client.graphHistory(projectId, creds);
|
|
304
|
+
creds = hist.credentials || creds;
|
|
305
|
+
if (!hist.refs.main || hist.commits.length === 0) {
|
|
306
|
+
throw new ErrorExportacion('PROYECTO_VACIO', 'El proyecto todavía no tiene puntos de guardado.');
|
|
307
|
+
}
|
|
308
|
+
if (!hist.identities) {
|
|
309
|
+
throw new ErrorExportacion('SIN_IDENTIDADES', 'El servidor todavía no puede firmar la exportación (falta la migración 018).');
|
|
310
|
+
}
|
|
311
|
+
const commits = Object.fromEntries(hist.commits.map((c) => [c.oid, c]));
|
|
312
|
+
|
|
313
|
+
const semilla = {
|
|
314
|
+
commits: previo ? previo.commits : {},
|
|
315
|
+
trees: { ...(previo ? previo.trees : {}) },
|
|
316
|
+
blobs: { ...(previo ? previo.blobs : {}) },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// 2. Los árboles que no se exportaron antes.
|
|
320
|
+
etapa('carpetas');
|
|
321
|
+
const arboles = await bajarArboles(client, projectId, creds, hist.commits.map((c) => c.tree_oid), semilla.trees);
|
|
322
|
+
creds = arboles.creds;
|
|
323
|
+
|
|
324
|
+
// 3. Los archivos que no se exportaron antes, escritos a medida que llegan.
|
|
325
|
+
const faltantes = [...new Set(Object.values(arboles.trees).flat().filter((e) => e.type === 'blob').map((e) => e.oid))]
|
|
326
|
+
.filter((oid) => oid !== OID_VACIO && !semilla.blobs[oid]);
|
|
327
|
+
etapa('archivos', faltantes.length);
|
|
328
|
+
const archivos = await bajarArchivos(client, projectId, creds, faltantes, gitDir, onProgreso);
|
|
329
|
+
creds = archivos.creds;
|
|
330
|
+
Object.assign(semilla.blobs, archivos.escritos);
|
|
331
|
+
|
|
332
|
+
// 4. La conversión. Lo bajado recién ya está escrito, así que entra como
|
|
333
|
+
// sembrado; el archivo vacío no se baja nunca.
|
|
334
|
+
etapa('conversion');
|
|
335
|
+
const convertido = convertirGrafo({
|
|
336
|
+
commits,
|
|
337
|
+
trees: arboles.trees,
|
|
338
|
+
blobs: { [OID_VACIO]: Buffer.alloc(0) },
|
|
339
|
+
heads: { main: hist.refs.main },
|
|
340
|
+
identidades: hist.identities,
|
|
341
|
+
mapaPrevio: semilla,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 5. Los objetos, y la ref de notas: sin padre y fechada en el punto de
|
|
345
|
+
// guardado anotado más reciente, para que dependa sólo del grafo.
|
|
346
|
+
etapa('escritura');
|
|
347
|
+
let escritos = Object.keys(archivos.escritos).length;
|
|
348
|
+
for (const objeto of convertido.objetos.values()) {
|
|
349
|
+
if (escribirObjeto(gitDir, objeto)) escritos += 1;
|
|
350
|
+
}
|
|
351
|
+
const porSha1 = new Map(Object.entries(convertido.mapa.commits).map(([oid, sha1]) => [sha1, oid]));
|
|
352
|
+
const fechas = Object.keys(convertido.notas).map((sha1) => commits[porSha1.get(sha1)].created_at);
|
|
353
|
+
const ultima = fechas.reduce((a, b) => (new Date(b) > new Date(a) ? b : a), fechas[0]);
|
|
354
|
+
const notas = fechas.length > 0 ? gitNotas(convertido.notas, ultima) : null;
|
|
355
|
+
if (notas) for (const objeto of notas.objetos) if (escribirObjeto(gitDir, objeto)) escritos += 1;
|
|
356
|
+
|
|
357
|
+
// Todo lo que `main` alcanza tiene que estar: lo sembrado incluido. Si falta
|
|
358
|
+
// algo acá, la carpeta perdió objetos desde la última vez (un `git gc` con
|
|
359
|
+
// la ref movida a mano, por ejemplo) y el mapa miente.
|
|
360
|
+
const mainNueva = convertido.refs.main;
|
|
361
|
+
try {
|
|
362
|
+
git(['rev-list', '--objects', '--quiet', mainNueva]);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
throw new ErrorExportacion('REPO_INCOMPLETO', `Faltan objetos en ${dest}: ${String(e.stderr || e.message).trim().split('\n')[0]}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 6. El mapa antes que las refs: si se corta entre los dos, la próxima vez
|
|
368
|
+
// `main` sigue siendo un punto de guardado del mapa y se puede reintentar.
|
|
369
|
+
guardarMapa(gitDir, {
|
|
370
|
+
version: VERSION_MAPA,
|
|
371
|
+
projectId,
|
|
372
|
+
baseURL,
|
|
373
|
+
refs: { main: mainNueva, notas: notas ? notas.sha1 : null },
|
|
374
|
+
...convertido.mapa,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// 7. La carpeta de trabajo, si está en main, y las refs con compare-and-swap.
|
|
378
|
+
if (enMain && mainAntes !== mainNueva) {
|
|
379
|
+
if (mainAntes) git(['read-tree', '-m', '-u', mainAntes, mainNueva]);
|
|
380
|
+
else git(['read-tree', '-u', '--reset', mainNueva]);
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
git(['update-ref', '-m', 'shilo export', REF_MAIN, mainNueva, mainAntes || CERO]);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
throw new ErrorExportacion('MAIN_AJENA', `main se movió mientras se exportaba: ${String(e.stderr || e.message).trim()}`);
|
|
386
|
+
}
|
|
387
|
+
const notasAntes = leerRef(git, REF_NOTAS);
|
|
388
|
+
if (notas && notasAntes !== notas.sha1) git(['update-ref', '-m', 'shilo export', REF_NOTAS, notas.sha1]);
|
|
389
|
+
|
|
390
|
+
const nuevos = hist.commits.filter((c) => !semilla.commits[c.oid]).length;
|
|
391
|
+
return {
|
|
392
|
+
destino: dest,
|
|
393
|
+
incremental: Boolean(previo),
|
|
394
|
+
commits: hist.commits.length,
|
|
395
|
+
nuevos,
|
|
396
|
+
carpetas: Object.keys(arboles.trees).length,
|
|
397
|
+
archivos: faltantes.length,
|
|
398
|
+
bytes: archivos.bytes,
|
|
399
|
+
objetos: escritos,
|
|
400
|
+
vacias: convertido.vacias,
|
|
401
|
+
main: mainNueva,
|
|
402
|
+
mainAntes,
|
|
403
|
+
notas: notas ? notas.sha1 : null,
|
|
404
|
+
notasReemplazadas: Boolean(notas && notasAntes && notasAntes !== notas.sha1),
|
|
405
|
+
carpetaActualizada: enMain,
|
|
406
|
+
credentials: creds,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = {
|
|
411
|
+
exportarProyecto,
|
|
412
|
+
escribirObjeto,
|
|
413
|
+
leerMapa,
|
|
414
|
+
versionDeGit,
|
|
415
|
+
ErrorExportacion,
|
|
416
|
+
REF_MAIN,
|
|
417
|
+
};
|