shilo-cli 0.7.0 → 0.9.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 +338 -255
- package/bin/shilo.js +211 -165
- package/package.json +1 -1
- package/src/ShiloClient.js +1066 -837
- package/src/args.js +32 -8
- package/src/checks.js +57 -0
- package/src/commands/deploys.js +105 -0
- package/src/commands/diff.js +19 -3
- package/src/commands/export.js +226 -0
- package/src/commands/init.js +8 -0
- package/src/commands/log.js +4 -0
- package/src/commands/pull.js +86 -2
- package/src/commands/push.js +668 -69
- package/src/commands/review.js +101 -0
- package/src/commands/status.js +148 -14
- package/src/exportgit.js +417 -0
- package/src/gitobjects.js +532 -0
- package/src/projectconfig.js +76 -0
- package/src/safelist.js +101 -0
- package/src/secrets.js +75 -9
- package/src/sync.js +56 -5
- package/src/walk.js +30 -0
package/src/args.js
CHANGED
|
@@ -10,14 +10,32 @@
|
|
|
10
10
|
* -m "msg" -> alias corto (ver `aliases`)
|
|
11
11
|
* positionals -> array de argumentos sueltos
|
|
12
12
|
*
|
|
13
|
+
* Las claves de `multiple` acumulan en un array en vez de pisarse, y quedan
|
|
14
|
+
* SIEMPRE como array (aunque haya una sola aparición, o ninguna): así el
|
|
15
|
+
* consumidor no tiene que distinguir "uno" de "varios", que es justo donde se
|
|
16
|
+
* cuelan los bugs de `--only`.
|
|
17
|
+
*
|
|
13
18
|
* Devuelve { _: [positionals...], ...flags }.
|
|
14
19
|
*/
|
|
15
|
-
function parseArgs(argv, { booleans = [], aliases = {} } = {}) {
|
|
20
|
+
function parseArgs(argv, { booleans = [], aliases = {}, multiple = [] } = {}) {
|
|
16
21
|
const out = { _: [] };
|
|
17
22
|
const boolSet = new Set(booleans);
|
|
23
|
+
const multiSet = new Set(multiple);
|
|
18
24
|
|
|
19
25
|
const canonical = (k) => aliases[k] || k;
|
|
20
26
|
|
|
27
|
+
const set = (key, value) => {
|
|
28
|
+
if (!multiSet.has(key)) {
|
|
29
|
+
out[key] = value;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// `--only` sin valor (`--only --yes`) no es una lista de nada: se descarta
|
|
33
|
+
// en vez de meter un `true` que el consumidor tendría que filtrar.
|
|
34
|
+
if (value === true) return;
|
|
35
|
+
if (!Array.isArray(out[key])) out[key] = [];
|
|
36
|
+
out[key].push(value);
|
|
37
|
+
};
|
|
38
|
+
|
|
21
39
|
for (let i = 0; i < argv.length; i++) {
|
|
22
40
|
let token = argv[i];
|
|
23
41
|
|
|
@@ -37,17 +55,17 @@ function parseArgs(argv, { booleans = [], aliases = {} } = {}) {
|
|
|
37
55
|
}
|
|
38
56
|
key = canonical(key);
|
|
39
57
|
if (value !== undefined) {
|
|
40
|
-
|
|
58
|
+
set(key, value);
|
|
41
59
|
} else if (boolSet.has(key)) {
|
|
42
|
-
|
|
60
|
+
set(key, true);
|
|
43
61
|
} else {
|
|
44
62
|
const next = argv[i + 1];
|
|
45
63
|
// `-` a secas es un valor legítimo ("leer de stdin"), no otra bandera.
|
|
46
64
|
if (next !== undefined && (next === '-' || !next.startsWith('-'))) {
|
|
47
|
-
|
|
65
|
+
set(key, next);
|
|
48
66
|
i++;
|
|
49
67
|
} else {
|
|
50
|
-
|
|
68
|
+
set(key, true);
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
continue;
|
|
@@ -56,15 +74,15 @@ function parseArgs(argv, { booleans = [], aliases = {} } = {}) {
|
|
|
56
74
|
if (token.startsWith('-') && token.length > 1) {
|
|
57
75
|
const key = canonical(token.slice(1));
|
|
58
76
|
if (boolSet.has(key)) {
|
|
59
|
-
|
|
77
|
+
set(key, true);
|
|
60
78
|
} else {
|
|
61
79
|
const next = argv[i + 1];
|
|
62
80
|
// `-` a secas es un valor legítimo ("leer de stdin"), no otra bandera.
|
|
63
81
|
if (next !== undefined && (next === '-' || !next.startsWith('-'))) {
|
|
64
|
-
|
|
82
|
+
set(key, next);
|
|
65
83
|
i++;
|
|
66
84
|
} else {
|
|
67
|
-
|
|
85
|
+
set(key, true);
|
|
68
86
|
}
|
|
69
87
|
}
|
|
70
88
|
continue;
|
|
@@ -73,6 +91,12 @@ function parseArgs(argv, { booleans = [], aliases = {} } = {}) {
|
|
|
73
91
|
out._.push(token);
|
|
74
92
|
}
|
|
75
93
|
|
|
94
|
+
// Normalización: una clave `multiple` que no apareció igual llega como array
|
|
95
|
+
// vacío, para que el consumidor pueda hacer `.length` sin chequear antes.
|
|
96
|
+
for (const key of multiSet) {
|
|
97
|
+
if (!Array.isArray(out[key])) out[key] = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
return out;
|
|
77
101
|
}
|
|
78
102
|
|
package/src/checks.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chequeos de propuesta (EP-S2, MU-93).
|
|
3
|
+
*
|
|
4
|
+
* Un proyecto declara en `shilo.config.json` los comandos que tienen que pasar
|
|
5
|
+
* antes de proponer (`"chequeos": ["npm test"]`). `shilo push` los corre en la
|
|
6
|
+
* carpeta, uno por uno y en orden, con la salida a la vista, y el resultado
|
|
7
|
+
* viaja con la propuesta: la web la muestra verificada o fallida, y aplicar una
|
|
8
|
+
* fallida exige forzar.
|
|
9
|
+
*
|
|
10
|
+
* Se corta en el primero que falla, como un hook de git: lo que viene después
|
|
11
|
+
* suele depender de lo anterior (instalar, compilar, probar) y correrlo sólo
|
|
12
|
+
* hace más largo el error. El resultado registra lo que se corrió.
|
|
13
|
+
*
|
|
14
|
+
* `run` se inyecta para probarlo sin lanzar procesos.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const childProcess = require('child_process');
|
|
20
|
+
|
|
21
|
+
// Un chequeo colgado no puede dejar colgado el push: a los diez minutos se corta
|
|
22
|
+
// y cuenta como fallido.
|
|
23
|
+
const CHECK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
function spawnCheck(comando, dir) {
|
|
26
|
+
const r = childProcess.spawnSync(comando, {
|
|
27
|
+
cwd: dir,
|
|
28
|
+
shell: true,
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
timeout: CHECK_TIMEOUT_MS,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
// Sin `status` = no llegó a terminar (no se pudo lanzar, o se cortó por tiempo).
|
|
34
|
+
return { codigo: typeof r.status === 'number' ? r.status : null };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} dir - la carpeta donde se corren
|
|
39
|
+
* @param {string[]} comandos
|
|
40
|
+
* @param {{ run?: Function, now?: Function, onStart?: Function }} [opts]
|
|
41
|
+
* @returns {{ resultado: 'ok'|'fallido', comandos: Array<{ comando, ok, codigo?, ms }> }}
|
|
42
|
+
*/
|
|
43
|
+
function runChecks(dir, comandos, { run = spawnCheck, now = Date.now, onStart = () => {} } = {}) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const comando of comandos) {
|
|
46
|
+
onStart(comando, out.length + 1, comandos.length);
|
|
47
|
+
const t0 = now();
|
|
48
|
+
const { codigo } = run(comando, dir);
|
|
49
|
+
const entry = { comando, ok: codigo === 0, ms: Math.max(0, Math.round(now() - t0)) };
|
|
50
|
+
if (codigo !== null) entry.codigo = codigo;
|
|
51
|
+
out.push(entry);
|
|
52
|
+
if (!entry.ok) break;
|
|
53
|
+
}
|
|
54
|
+
return { resultado: out.every((c) => c.ok) ? 'ok' : 'fallido', comandos: out };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { runChecks, CHECK_TIMEOUT_MS };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const sync = require('../sync');
|
|
6
|
+
const { resolveContext } = require('../context');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LIMIT = 10;
|
|
9
|
+
|
|
10
|
+
const STATUS = {
|
|
11
|
+
queued: (s) => ui.c.dim(s),
|
|
12
|
+
running: (s) => ui.c.cyan(s),
|
|
13
|
+
uploading: (s) => ui.c.cyan(s),
|
|
14
|
+
building: (s) => ui.c.cyan(s),
|
|
15
|
+
ready: (s) => ui.c.green(s),
|
|
16
|
+
error: (s) => ui.c.red(s),
|
|
17
|
+
canceled: (s) => ui.c.dim(s),
|
|
18
|
+
superseded: (s) => ui.c.dim(s),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const AUTO = {
|
|
22
|
+
off: 'sólo manual',
|
|
23
|
+
preview: 'preview en cada guardado',
|
|
24
|
+
production: 'producción en cada guardado',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* shilo deploys [-n <N>] [--project <uuid>] [--json]
|
|
29
|
+
*
|
|
30
|
+
* Destino e historial de deploys a Vercel del proyecto (PLAN_DEPLOY_VERCEL,
|
|
31
|
+
* Etapa 4.2). Sólo lectura. El servidor devuelve como mucho los últimos 20.
|
|
32
|
+
*/
|
|
33
|
+
async function deploys(args) {
|
|
34
|
+
const ctx = resolveContext({ ...args, _: [] });
|
|
35
|
+
if (!ctx.ok) return ctx.code;
|
|
36
|
+
|
|
37
|
+
const { projectId, baseURL, cfg, client, mgr } = ctx;
|
|
38
|
+
|
|
39
|
+
const limit = parseLimit(args.n || args.number);
|
|
40
|
+
if (limit === null) {
|
|
41
|
+
ui.error('El valor de -n debe ser un entero entre 1 y 20.');
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let info;
|
|
46
|
+
try {
|
|
47
|
+
info = await client.getDeploys(projectId, await mgr.valid());
|
|
48
|
+
} catch (e) {
|
|
49
|
+
ui.error(e.notSupported ? 'Este servidor de Shilo todavía no tiene deploys.' : e.message);
|
|
50
|
+
return 1;
|
|
51
|
+
} finally {
|
|
52
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const list = info.deployments.slice(0, limit);
|
|
56
|
+
|
|
57
|
+
if (args.json) {
|
|
58
|
+
console.log(JSON.stringify({ projectId, connected: info.connected, target: info.target, deployments: list }, null, 2));
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ui.info(`Deploys del proyecto — ${ui.c.cyan(baseURL)}`);
|
|
63
|
+
if (!info.target) {
|
|
64
|
+
ui.info('');
|
|
65
|
+
ui.info('Este proyecto no despliega a ningún lado. El owner lo configura en la');
|
|
66
|
+
ui.info('pestaña "Deploys" del proyecto en la web.');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const t = info.target;
|
|
71
|
+
ui.info(`Vercel: ${ui.c.bold(t.vercel_project_name || t.vercel_project)} · ${AUTO[t.auto_target] || t.auto_target}`);
|
|
72
|
+
if (!info.connected) {
|
|
73
|
+
ui.warn('La conexión con Vercel no está activa: el owner tiene que volver a conectar.');
|
|
74
|
+
}
|
|
75
|
+
ui.info('');
|
|
76
|
+
|
|
77
|
+
if (list.length === 0) {
|
|
78
|
+
ui.info('Todavía no hubo deploys.');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const d of list) {
|
|
83
|
+
const paint = STATUS[d.status] || ((s) => s);
|
|
84
|
+
const commit = d.commit_oid ? String(d.commit_oid).slice(0, 8) : '--------';
|
|
85
|
+
const target = d.target === 'production' ? ui.c.bold('prod ') : 'preview';
|
|
86
|
+
const card = d.card ? ui.c.dim(` [card: ${d.card}]`) : '';
|
|
87
|
+
ui.info(`${paint(pad(d.status, 10))} ${target} ${ui.c.yellow(commit)} ${ui.c.dim(sync.formatDate(d.created_at))}${card}`);
|
|
88
|
+
if (d.url) ui.info(` ${d.url}`);
|
|
89
|
+
if (d.error) ui.info(` ${ui.c.red(d.error)}`);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pad(s, n) {
|
|
95
|
+
return String(s).padEnd(n);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseLimit(raw) {
|
|
99
|
+
if (raw === undefined || raw === null || raw === '') return DEFAULT_LIMIT;
|
|
100
|
+
const n = Number(raw);
|
|
101
|
+
if (!Number.isInteger(n) || n < 1 || n > 20) return null;
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = deploys;
|
package/src/commands/diff.js
CHANGED
|
@@ -126,17 +126,33 @@ async function diffAll({ dir, projectId, baseURL, cfg, client, mgr }) {
|
|
|
126
126
|
config.persistCredsIfChanged(cfg, mgr.current);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const cmp = sync.compareTrees(local.files, tree.files);
|
|
130
|
-
|
|
129
|
+
const cmp = sync.refineByHash(sync.compareTrees(local.files, tree.files), { dir, projectId, baseURL });
|
|
130
|
+
|
|
131
|
+
// Un archivo ignorado localmente no es una diferencia que haya que resolver:
|
|
132
|
+
// el push no lo sube y el pull no lo baja. Se nombra aparte, no como "sólo
|
|
133
|
+
// remoto", que invita a un pull que igual no lo traería.
|
|
134
|
+
const isIgnoredPath = sync.makePathMatcher(dir);
|
|
135
|
+
const onlyRemote = cmp.onlyRemote.filter((i) => !isIgnoredPath(i.repoPath));
|
|
136
|
+
const ignoredRemote = cmp.onlyRemote.filter((i) => isIgnoredPath(i.repoPath));
|
|
137
|
+
|
|
138
|
+
const total = cmp.onlyLocal.length + onlyRemote.length + cmp.differing.length;
|
|
131
139
|
|
|
132
140
|
ui.info('');
|
|
133
141
|
if (total === 0) {
|
|
134
142
|
ui.success('Sin diferencias de tamaño ni de archivos entre local y remoto.');
|
|
143
|
+
if (ignoredRemote.length > 0) {
|
|
144
|
+
ui.info(
|
|
145
|
+
ui.c.dim(`(${ignoredRemote.length} archivo(s) del remoto están ignorados localmente.)`)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
135
148
|
return 0;
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
for (const item of cmp.onlyLocal) ui.info(` ${ui.c.green('+')} ${item.repoPath} ${ui.c.dim('(sólo local)')}`);
|
|
139
|
-
for (const item of
|
|
152
|
+
for (const item of onlyRemote) ui.info(` ${ui.c.red('-')} ${item.repoPath} ${ui.c.dim('(sólo remoto)')}`);
|
|
153
|
+
for (const item of ignoredRemote) {
|
|
154
|
+
ui.info(` ${ui.c.dim('·')} ${ui.c.dim(`${item.repoPath} (sólo remoto, ignorado localmente)`)}`);
|
|
155
|
+
}
|
|
140
156
|
for (const item of cmp.differing) {
|
|
141
157
|
const delta = item.local.size - item.remote.size;
|
|
142
158
|
const sign = delta > 0 ? `+${delta}` : String(delta);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const remote = require('../remote');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const sync = require('../sync');
|
|
8
|
+
const { ShiloClient } = require('../ShiloClient');
|
|
9
|
+
const { DEFAULT_BASE_URL } = require('../context');
|
|
10
|
+
const { exportarProyecto, leerMapa } = require('../exportgit');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* shilo export <carpeta> [--project <uuid>] [--base-url <url>] [--json]
|
|
14
|
+
*
|
|
15
|
+
* Escribe la historia del proyecto como un repositorio Git en `carpeta`, con
|
|
16
|
+
* `main` y `refs/notes/shilo`. La primera vez la carpeta tiene que estar vacía
|
|
17
|
+
* o no existir; después, repetir el comando sobre la misma carpeta trae sólo
|
|
18
|
+
* lo nuevo. No toca el proyecto: es de sólo lectura.
|
|
19
|
+
*
|
|
20
|
+
* El proyecto sale de `--project`, de la exportación anterior que haya en la
|
|
21
|
+
* carpeta, o del `.shilo.json` de la carpeta actual, en ese orden. Un proyecto
|
|
22
|
+
* público se exporta sin sesión.
|
|
23
|
+
*/
|
|
24
|
+
async function exportCmd(args) {
|
|
25
|
+
const destinoArg = typeof args._[0] === 'string' ? args._[0] : null;
|
|
26
|
+
if (!destinoArg) {
|
|
27
|
+
ui.error('Falta la carpeta de destino: shilo export <carpeta>.');
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const destino = path.resolve(destinoArg);
|
|
31
|
+
const asJson = !!args.json;
|
|
32
|
+
|
|
33
|
+
let previo = null;
|
|
34
|
+
let atada = null;
|
|
35
|
+
try {
|
|
36
|
+
previo = leerMapa(path.join(destino, '.git'));
|
|
37
|
+
atada = remote.read(process.cwd());
|
|
38
|
+
} catch (e) {
|
|
39
|
+
ui.error(e.message);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pedido = typeof args.project === 'string' && args.project.trim() ? args.project.trim() : null;
|
|
44
|
+
if (pedido && previo && previo.projectId !== pedido) {
|
|
45
|
+
ui.error(`${destino} ya tiene la exportación de otro proyecto (${previo.projectId}).`);
|
|
46
|
+
ui.info('Exportá a una carpeta nueva.');
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const projectId = pedido || (previo && previo.projectId) || (atada && atada.projectId) || null;
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
ui.error('No sé qué proyecto exportar.');
|
|
52
|
+
ui.info('Pasá --project <uuid>, o corré el comando dentro de una carpeta atada con "shilo init".');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cfg = config.load() || {};
|
|
57
|
+
const baseURL = args['base-url'] || (previo && previo.baseURL) || (atada && atada.baseURL) || cfg.baseURL || DEFAULT_BASE_URL;
|
|
58
|
+
const client = new ShiloClient({
|
|
59
|
+
baseURL,
|
|
60
|
+
supabaseUrl: cfg.supabaseUrl || null,
|
|
61
|
+
supabaseAnonKey: cfg.supabaseAnonKey || null,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Sin sesión se intenta igual: un proyecto público se exporta sin cuenta.
|
|
65
|
+
let creds = null;
|
|
66
|
+
if (cfg.credentials) {
|
|
67
|
+
try {
|
|
68
|
+
creds = await sync.makeCredsManager(client, cfg).valid();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
ui.error(`La sesión guardada no sirve: ${e.message}`);
|
|
71
|
+
ui.info('Corré "shilo login" de nuevo.');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let progreso = null;
|
|
77
|
+
if (!asJson) {
|
|
78
|
+
ui.info(`${previo ? 'Actualizando la exportación' : 'Exportando'} de ${ui.c.bold(projectId)} en ${ui.c.bold(destino)}`);
|
|
79
|
+
ui.info(ui.c.dim(`Servidor: ${baseURL}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let informe;
|
|
83
|
+
try {
|
|
84
|
+
informe = await exportarProyecto({
|
|
85
|
+
client,
|
|
86
|
+
projectId,
|
|
87
|
+
creds,
|
|
88
|
+
destino,
|
|
89
|
+
baseURL,
|
|
90
|
+
onEtapa: (nombre, detalle) => {
|
|
91
|
+
if (asJson) return;
|
|
92
|
+
if (nombre === 'archivos' && detalle > 0) {
|
|
93
|
+
ui.info(`Bajando ${detalle} archivo(s)…`);
|
|
94
|
+
progreso = ui.progress(detalle, 'archivos');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
onProgreso: (hechos) => { if (progreso) progreso.update(hechos); },
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (progreso) progreso.clear();
|
|
101
|
+
return explicarError(e, { destino, projectId, conSesion: Boolean(creds), asJson });
|
|
102
|
+
} finally {
|
|
103
|
+
if (creds) config.persistCredsIfChanged(cfg, creds);
|
|
104
|
+
}
|
|
105
|
+
if (progreso) progreso.clear();
|
|
106
|
+
if (informe.credentials) config.persistCredsIfChanged(cfg, informe.credentials);
|
|
107
|
+
|
|
108
|
+
if (asJson) {
|
|
109
|
+
const { credentials, ...resto } = informe;
|
|
110
|
+
console.log(JSON.stringify({ projectId, ...resto }, null, 2));
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mostrarInforme(informe);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mostrarInforme(inf) {
|
|
119
|
+
ui.info('');
|
|
120
|
+
if (inf.incremental && inf.mainAntes === inf.main) {
|
|
121
|
+
ui.success('Ya estaba al día: no hay puntos de guardado nuevos.');
|
|
122
|
+
} else if (inf.incremental) {
|
|
123
|
+
ui.success(`${inf.nuevos} punto(s) de guardado nuevo(s); ${inf.commits} en total.`);
|
|
124
|
+
} else {
|
|
125
|
+
ui.success(`${inf.commits} punto(s) de guardado exportado(s).`);
|
|
126
|
+
}
|
|
127
|
+
ui.info(` main ${ui.c.yellow(inf.main.slice(0, 12))}`);
|
|
128
|
+
if (inf.notas) ui.info(` procedencia ${ui.c.yellow(inf.notas.slice(0, 12))} ${ui.c.dim('(refs/notes/shilo)')}`);
|
|
129
|
+
ui.info(ui.c.dim(` ${inf.archivos} archivo(s) y ${inf.carpetas} carpeta(s) bajados, ${formatoBytes(inf.bytes)}.`));
|
|
130
|
+
|
|
131
|
+
if (inf.vacias.length > 0) {
|
|
132
|
+
ui.info('');
|
|
133
|
+
ui.warn(`Git no guarda carpetas vacías; quedaron afuera ${inf.vacias.length}:`);
|
|
134
|
+
for (const r of inf.vacias.slice(0, 20)) ui.info(` ${r}/`);
|
|
135
|
+
if (inf.vacias.length > 20) ui.info(ui.c.dim(` … y ${inf.vacias.length - 20} más.`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!inf.carpetaActualizada) {
|
|
139
|
+
ui.info('');
|
|
140
|
+
ui.info(ui.c.dim('La carpeta no está en main: se movió la rama pero no se tocaron tus archivos.'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ui.info('');
|
|
144
|
+
ui.info('Para subirlo a otro servidor Git:');
|
|
145
|
+
ui.info(ui.c.cyan(` git -C "${inf.destino}" push <remoto> main`));
|
|
146
|
+
if (inf.notas) {
|
|
147
|
+
// La ref de notas se rehace entera en cada exportación (no tiene padre),
|
|
148
|
+
// así que un push normal la rechaza a partir de la segunda: va con `+`.
|
|
149
|
+
ui.info(ui.c.cyan(` git -C "${inf.destino}" push <remoto> +refs/notes/shilo`));
|
|
150
|
+
if (inf.notasReemplazadas) {
|
|
151
|
+
ui.info(ui.c.dim(' La procedencia se reemplazó entera; por eso el + en el push.'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function explicarError(e, { destino, projectId, conSesion, asJson }) {
|
|
157
|
+
const code = e && e.code;
|
|
158
|
+
if (asJson) {
|
|
159
|
+
console.log(JSON.stringify({
|
|
160
|
+
ok: false, code: code || null, status: (e && e.status) || null, message: e.message, path: e.path || null,
|
|
161
|
+
}, null, 2));
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ui.error(e.message);
|
|
166
|
+
switch (code) {
|
|
167
|
+
case 'SIN_GIT':
|
|
168
|
+
ui.info('Instalá Git (https://git-scm.com) y volvé a correr el comando.');
|
|
169
|
+
break;
|
|
170
|
+
case 'DESTINO_OCUPADO':
|
|
171
|
+
ui.info('La primera exportación va a una carpeta vacía o que no existe.');
|
|
172
|
+
break;
|
|
173
|
+
case 'OTRO_PROYECTO':
|
|
174
|
+
ui.info('Exportá a una carpeta nueva.');
|
|
175
|
+
break;
|
|
176
|
+
case 'MAIN_AJENA':
|
|
177
|
+
ui.info('Hay puntos de guardado en main que no vienen de Shilo. Llevalos a otra rama');
|
|
178
|
+
ui.info('(git branch <nombre> main) y devolvé main a lo exportado, o exportá a una carpeta nueva.');
|
|
179
|
+
break;
|
|
180
|
+
case 'CAMBIOS_LOCALES':
|
|
181
|
+
ui.info('Guardalos o descartalos (git stash) antes de actualizar la exportación.');
|
|
182
|
+
break;
|
|
183
|
+
case 'MAPA_ILEGIBLE':
|
|
184
|
+
case 'REPO_INCOMPLETO':
|
|
185
|
+
ui.info('La exportación anterior está dañada. Exportá a una carpeta nueva.');
|
|
186
|
+
break;
|
|
187
|
+
case 'SIN_IDENTIDADES':
|
|
188
|
+
ui.info('Hay que aplicar la migración 018 en el servidor antes de exportar.');
|
|
189
|
+
break;
|
|
190
|
+
case 'PROYECTO_VACIO':
|
|
191
|
+
ui.info('Guardá una versión primero.');
|
|
192
|
+
break;
|
|
193
|
+
case 'NOMBRE_INVALIDO':
|
|
194
|
+
ui.info(`Git no acepta «${e.path || e.name}». Renombralo en Shilo y exportá de nuevo.`);
|
|
195
|
+
break;
|
|
196
|
+
case 'SHA1_MOVIDO':
|
|
197
|
+
ui.info('Lo ya exportado daría otro resultado: es un error de Shilo, no de tu carpeta. Avisanos.');
|
|
198
|
+
break;
|
|
199
|
+
case 'GRAFO_INCOMPLETO':
|
|
200
|
+
case 'BLOB_CORRUPTO':
|
|
201
|
+
case 'ARBOL_CORRUPTO':
|
|
202
|
+
case 'FALTA_OBJETO':
|
|
203
|
+
ui.info('A la historia del proyecto le falta algo en el servidor. No se escribió ninguna rama.');
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
if (e && e.status === 404) {
|
|
207
|
+
ui.info(conSesion
|
|
208
|
+
? `El proyecto ${projectId} no existe o no tenés acceso.`
|
|
209
|
+
: 'Si el proyecto es privado, iniciá sesión con "shilo login".');
|
|
210
|
+
} else if (e && e.status === 401) {
|
|
211
|
+
ui.info('Corré "shilo login" de nuevo.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (code && code !== 'DESTINO_OCUPADO' && code !== 'SIN_GIT' && code !== 'OTRO_PROYECTO') {
|
|
215
|
+
ui.info(ui.c.dim(`Carpeta: ${destino}`));
|
|
216
|
+
}
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatoBytes(n) {
|
|
221
|
+
if (n < 1024) return `${n} B`;
|
|
222
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
223
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = exportCmd;
|
package/src/commands/init.js
CHANGED
|
@@ -106,6 +106,14 @@ async function init(args) {
|
|
|
106
106
|
const written = remote.write(dir, { projectId, baseURL });
|
|
107
107
|
ui.success(`Carpeta atada al proyecto ${ui.c.bold(projectId)}.`);
|
|
108
108
|
ui.info(ui.c.dim(`Remote guardado en ${written}`));
|
|
109
|
+
// Se lee siempre (ver walk.js), pero no decirlo hacía que la gente copiara
|
|
110
|
+
// a mano cada línea del .gitignore al .shiloignore.
|
|
111
|
+
if (fs.existsSync(path.join(dir, '.gitignore'))) {
|
|
112
|
+
ui.info(
|
|
113
|
+
ui.c.dim('Se respeta tu .gitignore: lo que git ignora tampoco se sube. ') +
|
|
114
|
+
ui.c.dim('.shiloignore es sólo para exclusiones extra.')
|
|
115
|
+
);
|
|
116
|
+
}
|
|
109
117
|
|
|
110
118
|
// --push/--pull no implican confirmación: el usuario pidió *encadenar* el
|
|
111
119
|
// comando, no saltearse su preflight. Sin --yes, push muestra el plan (y el
|
package/src/commands/log.js
CHANGED
|
@@ -4,6 +4,7 @@ const config = require('../config');
|
|
|
4
4
|
const ui = require('../ui');
|
|
5
5
|
const sync = require('../sync');
|
|
6
6
|
const { resolveContext } = require('../context');
|
|
7
|
+
const { marcaDeRevision } = require('./review');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_LIMIT = 20;
|
|
9
10
|
|
|
@@ -90,6 +91,7 @@ async function log(args) {
|
|
|
90
91
|
message: c.message,
|
|
91
92
|
author: sync.describeAuthor(c.users),
|
|
92
93
|
date: c.created_at,
|
|
94
|
+
reviews: Array.isArray(c.reviews) ? c.reviews : [],
|
|
93
95
|
files: (c.files || []).map((cf) => ({
|
|
94
96
|
name: cf.files ? cf.files.name : null,
|
|
95
97
|
action: cf.action,
|
|
@@ -122,6 +124,8 @@ async function log(args) {
|
|
|
122
124
|
const hash = commit.hash ? String(commit.hash).slice(0, 8) : '(sin hash)';
|
|
123
125
|
ui.info(`${ui.c.yellow(hash)} ${ui.c.bold(sync.describeAuthor(commit.users))} ${ui.c.dim(sync.formatDate(commit.created_at))}`);
|
|
124
126
|
ui.info(` ${commit.message}`);
|
|
127
|
+
const marca = marcaDeRevision(commit.reviews);
|
|
128
|
+
if (marca) ui.info(` ${ui.c.dim('revisión:')} ${marca}`);
|
|
125
129
|
if (showFiles && Array.isArray(commit.files) && commit.files.length > 0) {
|
|
126
130
|
const shown = repoPath
|
|
127
131
|
? commit.files.filter((cf) => cf.files && cf.files.id === targetId)
|