@coldge.com/gitbase 1.0.2 → 1.0.3
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 +65 -15
- package/dist/api/supabase.js +139 -32
- package/dist/commands/branch.js +86 -14
- package/dist/commands/diff.js +180 -94
- package/dist/commands/init.js +8 -3
- package/dist/commands/log.js +85 -11
- package/dist/commands/merge.js +226 -15
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.js +324 -71
- package/dist/commands/revert.js +247 -95
- package/dist/commands/snapshot.js +49 -0
- package/dist/commands/stash.js +211 -0
- package/dist/commands/status.js +46 -38
- package/dist/commands/verify.js +120 -0
- package/dist/index.js +90 -10
- package/dist/schema/extractor.js +183 -26
- package/dist/schema/queries.js +160 -8
- package/dist/utils/hashing.js +52 -3
- package/dist/utils/sqlDiff.js +245 -0
- package/package.json +2 -1
package/dist/commands/revert.js
CHANGED
|
@@ -3,7 +3,10 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import readline from 'readline';
|
|
5
5
|
import { readCommit, readTree, readObject } from '../storage/git.js';
|
|
6
|
-
import { runQuery } from '../api/supabase.js';
|
|
6
|
+
import { runQuery, runTransaction } from '../api/supabase.js';
|
|
7
|
+
import { extractSchema } from '../schema/extractor.js';
|
|
8
|
+
import { canonicalize, hashString } from '../utils/hashing.js';
|
|
9
|
+
import { generateTableAlter, generateEnumAlter } from '../utils/sqlDiff.js';
|
|
7
10
|
import toposort from 'toposort';
|
|
8
11
|
const GITBASE_DIR = '.gitbase';
|
|
9
12
|
const CONFIG_FILE = path.join(GITBASE_DIR, 'config');
|
|
@@ -13,7 +16,7 @@ export async function revert(argv) {
|
|
|
13
16
|
await fs.access(CONFIG_FILE);
|
|
14
17
|
}
|
|
15
18
|
catch {
|
|
16
|
-
console.error(chalk.red('Not initialized.'));
|
|
19
|
+
console.error(chalk.red('Not initialized. Run `gitb init` first.'));
|
|
17
20
|
return;
|
|
18
21
|
}
|
|
19
22
|
const config = JSON.parse(await fs.readFile(CONFIG_FILE, 'utf-8'));
|
|
@@ -51,169 +54,294 @@ export async function revert(argv) {
|
|
|
51
54
|
console.error(chalk.red(`Commit ${commitHash} not found.`));
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
|
-
console.log(chalk.blue(
|
|
55
|
-
|
|
56
|
-
console.log(chalk.blue(`Checking current database state...`));
|
|
57
|
-
const { extractSchema } = await import('../schema/extractor.js');
|
|
57
|
+
console.log(chalk.blue(`\nReverting to commit ${commitHash.substring(0, 7)} ("${commit.message}")...`));
|
|
58
|
+
console.log(chalk.blue(`Fetching current database state...`));
|
|
58
59
|
const liveSchema = await extractSchema(projectRef);
|
|
59
|
-
//
|
|
60
|
+
// 3. Load stored schema from the target commit tree
|
|
60
61
|
const tree = await readTree(commit.tree);
|
|
61
|
-
const schema = {
|
|
62
|
+
const schema = {
|
|
63
|
+
types: {}, sequences: {}, tables: {}, matviews: {},
|
|
64
|
+
views: {}, functions: {}, triggers: {}, policies: {}
|
|
65
|
+
};
|
|
62
66
|
const filterFiles = argv.files || [];
|
|
63
67
|
for (const [relPath, hash] of Object.entries(tree)) {
|
|
64
68
|
if (filterFiles.length > 0 && !filterFiles.includes(relPath))
|
|
65
69
|
continue;
|
|
66
70
|
const parts = relPath.split('/');
|
|
67
|
-
// relPath corresponds to file structure, e.g., "tables/users.sql"
|
|
68
71
|
const type = parts[0];
|
|
69
72
|
const name = path.basename(relPath, '.sql');
|
|
70
73
|
const content = await readObject(hash);
|
|
71
|
-
if (schema[type]) {
|
|
74
|
+
if (schema[type] !== undefined) {
|
|
72
75
|
schema[type][name] = content;
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
|
-
const rl = readline.createInterface({
|
|
76
|
-
input: process.stdin,
|
|
77
|
-
output: process.stdout
|
|
78
|
-
});
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
79
79
|
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
80
|
+
const same = (a, b) => canonicalize(a) === canonicalize(b);
|
|
81
|
+
// =========================================================================
|
|
82
|
+
// PHASE 1: PLAN — determine all SQL changes, ask backup questions up front
|
|
83
|
+
// No DB writes happen in this phase.
|
|
84
|
+
// =========================================================================
|
|
85
|
+
const plan = [];
|
|
86
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
87
|
+
// EXTENSIONS — safe to add, never drop
|
|
88
|
+
for (const [name, sql] of Object.entries(schema.extensions)) {
|
|
89
|
+
if (liveSchema.extensions[name] && same(sql, liveSchema.extensions[name]))
|
|
90
|
+
continue;
|
|
91
|
+
plan.push({ label: `Extension: ${name}`, sqls: [sql] });
|
|
92
|
+
}
|
|
93
|
+
// TYPES (enums, composites, domains)
|
|
84
94
|
for (const [name, sql] of Object.entries(schema.types)) {
|
|
85
|
-
if (liveSchema.types[name] &&
|
|
95
|
+
if (liveSchema.types[name] && same(sql, liveSchema.types[name]))
|
|
86
96
|
continue;
|
|
97
|
+
const isEnum = /AS ENUM/i.test(sql);
|
|
98
|
+
if (isEnum && liveSchema.types[name]) {
|
|
99
|
+
const alterStmts = generateEnumAlter(name, liveSchema.types[name], sql);
|
|
100
|
+
if (alterStmts !== null) {
|
|
101
|
+
if (alterStmts.length > 0) {
|
|
102
|
+
plan.push({ label: `Enum Type (ADD VALUE): ${name}`, sqls: alterStmts });
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.yellow(`\nEnum '${name}' has removed/reordered values — full recreation required.`));
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
plan.push({
|
|
109
|
+
label: `Type: ${name}`,
|
|
110
|
+
sqls: [`DROP TYPE IF EXISTS public."${name}" CASCADE;`, sql]
|
|
111
|
+
});
|
|
92
112
|
}
|
|
93
|
-
//
|
|
113
|
+
// SEQUENCES — IF NOT EXISTS to preserve sequence counter values
|
|
114
|
+
for (const [name, sql] of Object.entries(schema.sequences)) {
|
|
115
|
+
if (liveSchema.sequences[name] && same(sql, liveSchema.sequences[name]))
|
|
116
|
+
continue;
|
|
117
|
+
plan.push({ label: `Sequence: ${name}`, sqls: [sql] });
|
|
118
|
+
}
|
|
119
|
+
// TABLES — topological sort respecting FK dependencies
|
|
94
120
|
const tableEdges = [];
|
|
95
121
|
const tables = schema.tables;
|
|
96
122
|
const tableNames = Object.keys(tables);
|
|
97
123
|
for (const [name, sql] of Object.entries(tables)) {
|
|
98
|
-
const content = sql;
|
|
99
124
|
const regex = /REFERENCES\s+(?:public\.)?(\w+)/gi;
|
|
100
125
|
let match;
|
|
101
|
-
while ((match = regex.exec(
|
|
126
|
+
while ((match = regex.exec(sql)) !== null) {
|
|
102
127
|
const target = match[1];
|
|
103
|
-
if (target !== name && tables[target])
|
|
104
|
-
tableEdges.push([target, name]);
|
|
105
|
-
}
|
|
128
|
+
if (target !== name && tables[target])
|
|
129
|
+
tableEdges.push([target, name]);
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
let sortedTables = tableNames;
|
|
109
133
|
try {
|
|
110
134
|
sortedTables = toposort.array(tableNames, tableEdges);
|
|
111
135
|
}
|
|
112
|
-
catch
|
|
113
|
-
console.warn(chalk.yellow('Circular dependency detected
|
|
136
|
+
catch {
|
|
137
|
+
console.warn(chalk.yellow('Circular dependency detected — falling back to alphabetical order.'));
|
|
114
138
|
sortedTables = tableNames.sort();
|
|
115
139
|
}
|
|
116
140
|
for (const name of sortedTables) {
|
|
117
141
|
const sql = tables[name];
|
|
118
142
|
if (!sql)
|
|
119
143
|
continue;
|
|
120
|
-
if (liveSchema.tables[name] &&
|
|
144
|
+
if (liveSchema.tables[name] && same(sql, liveSchema.tables[name]))
|
|
121
145
|
continue;
|
|
122
|
-
}
|
|
123
|
-
madeDbChanges = true;
|
|
124
|
-
console.log(chalk.yellow(`\nRestoring Table: ${name}`));
|
|
125
146
|
if (liveSchema.tables[name]) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
// Table exists in live DB — generate ALTER statements
|
|
148
|
+
const alterStmts = generateTableAlter(name, liveSchema.tables[name], sql);
|
|
149
|
+
if (alterStmts.length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
console.log(chalk.yellow(`\nTable schema differs: ${name}`));
|
|
152
|
+
console.log(chalk.gray(` Changes that will be applied (${alterStmts.length} statement${alterStmts.length > 1 ? 's' : ''}):`));
|
|
153
|
+
for (const s of alterStmts)
|
|
154
|
+
console.log(chalk.gray(` → ${s}`));
|
|
155
|
+
console.log();
|
|
156
|
+
const answer = await ask(chalk.white(` Apply smart ALTER in-place (data preserved) or backup + recreate?\n`) +
|
|
157
|
+
chalk.cyan(` [A]lter (default) `) +
|
|
158
|
+
chalk.yellow(`[B]ackup + recreate `) +
|
|
159
|
+
chalk.red(`[D]rop + recreate (data loss)`) +
|
|
160
|
+
chalk.white(`\n Choice [A/b/d] > `));
|
|
161
|
+
const choice = answer.trim().toLowerCase();
|
|
162
|
+
if (choice === 'b') {
|
|
129
163
|
const backupName = `${name}_backup_${timestamp}`;
|
|
130
|
-
console.log(chalk.gray(`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
164
|
+
console.log(chalk.gray(` → Will rename '${name}' → '${backupName}' then restore from snapshot.`));
|
|
165
|
+
plan.push({
|
|
166
|
+
label: `Table (backup+restore): ${name}`,
|
|
167
|
+
sqls: [
|
|
168
|
+
`ALTER TABLE public."${name}" RENAME TO "${backupName}";`,
|
|
169
|
+
sql
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (choice === 'd') {
|
|
174
|
+
console.log(chalk.red(` → Will DROP '${name}' and restore from snapshot (current data lost).`));
|
|
175
|
+
plan.push({
|
|
176
|
+
label: `Table (drop+restore): ${name}`,
|
|
177
|
+
sqls: [`DROP TABLE IF EXISTS public."${name}" CASCADE;`, sql]
|
|
178
|
+
});
|
|
138
179
|
}
|
|
139
180
|
else {
|
|
140
|
-
|
|
141
|
-
await
|
|
181
|
+
// Default: smart ALTER — optionally with pre-transaction data snapshot
|
|
182
|
+
const snapshotAnswer = await ask(chalk.white(` Save a data snapshot of '${name}' before altering? (y/N) > `));
|
|
183
|
+
const wantsSnapshot = snapshotAnswer.trim().toLowerCase() === 'y';
|
|
184
|
+
const snapshotName = `${name}_snapshot_${timestamp}`;
|
|
185
|
+
if (wantsSnapshot) {
|
|
186
|
+
console.log(chalk.gray(` → Will snapshot data to '${snapshotName}', then ALTER in-place.`));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(chalk.cyan(` → Will ALTER in-place.`));
|
|
190
|
+
}
|
|
191
|
+
plan.push({
|
|
192
|
+
label: `Table (ALTER): ${name}`,
|
|
193
|
+
sqls: alterStmts,
|
|
194
|
+
preTransactionSqls: wantsSnapshot
|
|
195
|
+
? [`CREATE TABLE public."${snapshotName}" AS SELECT * FROM public."${name}";`]
|
|
196
|
+
: undefined
|
|
197
|
+
});
|
|
142
198
|
}
|
|
143
199
|
}
|
|
144
200
|
else {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
await runQuery(projectRef, sql);
|
|
150
|
-
}
|
|
151
|
-
catch (e) {
|
|
152
|
-
console.error(chalk.red(`Failed to create table: ${e.message}`));
|
|
201
|
+
// Table missing from live DB — restore from snapshot
|
|
202
|
+
console.log(chalk.green(`\nRestoring missing table: ${name}`));
|
|
203
|
+
plan.push({ label: `Table (RESTORE): ${name}`, sqls: [sql] });
|
|
153
204
|
}
|
|
154
205
|
}
|
|
155
|
-
// VIEWS
|
|
206
|
+
// MATERIALIZED VIEWS
|
|
207
|
+
for (const [name, sql] of Object.entries(schema.matviews)) {
|
|
208
|
+
if (liveSchema.matviews[name] && same(sql, liveSchema.matviews[name]))
|
|
209
|
+
continue;
|
|
210
|
+
plan.push({
|
|
211
|
+
label: `Materialized View: ${name}`,
|
|
212
|
+
sqls: [`DROP MATERIALIZED VIEW IF EXISTS public."${name}" CASCADE;`, sql]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// VIEWS — CREATE OR REPLACE, no DROP needed
|
|
156
216
|
for (const [name, sql] of Object.entries(schema.views)) {
|
|
157
|
-
if (liveSchema.views[name] &&
|
|
217
|
+
if (liveSchema.views[name] && same(sql, liveSchema.views[name]))
|
|
158
218
|
continue;
|
|
159
|
-
}
|
|
160
|
-
madeDbChanges = true;
|
|
161
|
-
console.log(chalk.cyan(`Restoring View: ${name}`));
|
|
162
|
-
await runQuery(projectRef, `DROP VIEW IF EXISTS public."${name}" CASCADE;`);
|
|
163
|
-
await runQuery(projectRef, sql);
|
|
219
|
+
plan.push({ label: `View: ${name}`, sqls: [sql] });
|
|
164
220
|
}
|
|
165
|
-
// FUNCTIONS
|
|
221
|
+
// FUNCTIONS — CREATE OR REPLACE, no drop needed
|
|
166
222
|
for (const [name, sql] of Object.entries(schema.functions)) {
|
|
167
|
-
if (liveSchema.functions[name] &&
|
|
223
|
+
if (liveSchema.functions[name] && same(sql, liveSchema.functions[name]))
|
|
168
224
|
continue;
|
|
169
|
-
}
|
|
170
|
-
madeDbChanges = true;
|
|
171
|
-
console.log(chalk.cyan(`Restoring Function: ${name}`));
|
|
172
|
-
await runQuery(projectRef, sql);
|
|
225
|
+
plan.push({ label: `Function: ${name}`, sqls: [sql] });
|
|
173
226
|
}
|
|
174
227
|
// TRIGGERS
|
|
175
228
|
for (const [name, sql] of Object.entries(schema.triggers)) {
|
|
176
|
-
if (liveSchema.triggers[name] &&
|
|
229
|
+
if (liveSchema.triggers[name] && same(sql, liveSchema.triggers[name]))
|
|
177
230
|
continue;
|
|
178
|
-
}
|
|
179
|
-
madeDbChanges = true;
|
|
180
|
-
console.log(chalk.cyan(`Restoring Trigger: ${name}`));
|
|
181
231
|
const match = sql.match(/ON\s+(public\.)?("?\w+"?)/i);
|
|
232
|
+
const triggerSqls = [];
|
|
182
233
|
if (match) {
|
|
183
234
|
const tableName = match[2].replace(/"/g, '');
|
|
184
|
-
|
|
235
|
+
triggerSqls.push(`DROP TRIGGER IF EXISTS "${name}" ON public."${tableName}";`);
|
|
185
236
|
}
|
|
186
|
-
|
|
237
|
+
triggerSqls.push(sql);
|
|
238
|
+
plan.push({ label: `Trigger: ${name}`, sqls: triggerSqls });
|
|
187
239
|
}
|
|
188
240
|
// POLICIES
|
|
189
241
|
for (const [key, sql] of Object.entries(schema.policies)) {
|
|
190
|
-
if (liveSchema.policies[key] &&
|
|
242
|
+
if (liveSchema.policies[key] && same(sql, liveSchema.policies[key]))
|
|
191
243
|
continue;
|
|
192
|
-
}
|
|
193
|
-
madeDbChanges = true;
|
|
194
244
|
const match = sql.match(/CREATE POLICY "([^"]+)"\s+ON\s+(public\.)?("?\w+"?)/i);
|
|
195
245
|
if (match) {
|
|
196
246
|
const policyName = match[1];
|
|
197
247
|
const tableName = match[3].replace(/"/g, '');
|
|
198
|
-
|
|
199
|
-
|
|
248
|
+
plan.push({
|
|
249
|
+
label: `Policy: ${policyName} on ${tableName}`,
|
|
250
|
+
sqls: [
|
|
251
|
+
`ALTER TABLE public."${tableName}" ENABLE ROW LEVEL SECURITY;`,
|
|
252
|
+
`DROP POLICY IF EXISTS "${policyName}" ON public."${tableName}";`,
|
|
253
|
+
sql
|
|
254
|
+
]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// GRANTS — REVOKE ALL then re-GRANT
|
|
259
|
+
for (const [key, sql] of Object.entries(schema.grants)) {
|
|
260
|
+
if (liveSchema.grants[key] && same(sql, liveSchema.grants[key]))
|
|
261
|
+
continue;
|
|
262
|
+
const grantSqls = [];
|
|
263
|
+
const isTable = key.startsWith('table__');
|
|
264
|
+
const isFunc = key.startsWith('function__');
|
|
265
|
+
const objName = key.replace(/^(table|function)__/, '');
|
|
266
|
+
const grantees = [...new Set((sql.match(/TO (\w+)/gi) || []).map(m => m.replace(/^TO /i, '')))];
|
|
267
|
+
if (grantees.length > 0) {
|
|
268
|
+
if (isTable) {
|
|
269
|
+
grantSqls.push(`REVOKE ALL PRIVILEGES ON TABLE public."${objName}" FROM ${grantees.join(', ')};`);
|
|
270
|
+
}
|
|
271
|
+
else if (isFunc) {
|
|
272
|
+
grantSqls.push(`REVOKE ALL PRIVILEGES ON FUNCTION public."${objName}" FROM ${grantees.join(', ')};`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const grantStmt of sql.split('\n').filter(Boolean)) {
|
|
276
|
+
grantSqls.push(grantStmt);
|
|
277
|
+
}
|
|
278
|
+
plan.push({ label: `Grants on ${key.replace('__', ': ')}`, sqls: grantSqls });
|
|
279
|
+
}
|
|
280
|
+
// PUBLICATIONS
|
|
281
|
+
for (const [name, sql] of Object.entries(schema.publications)) {
|
|
282
|
+
if (liveSchema.publications[name] && same(sql, liveSchema.publications[name]))
|
|
283
|
+
continue;
|
|
284
|
+
plan.push({
|
|
285
|
+
label: `Publication: ${name}`,
|
|
286
|
+
sqls: [`DROP PUBLICATION IF EXISTS "${name}";`, sql]
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
rl.close();
|
|
290
|
+
if (plan.length === 0) {
|
|
291
|
+
console.log(chalk.green('\nNo database changes required. Live DB already matches this commit.'));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// =====================================================================
|
|
295
|
+
// PHASE 2: EXECUTE — all statements in a single atomic transaction
|
|
296
|
+
// =====================================================================
|
|
297
|
+
console.log(chalk.blue(`\nApplying ${plan.length} change(s)...`));
|
|
298
|
+
for (const step of plan) {
|
|
299
|
+
const prefix = step.preTransactionSqls?.length ? '(+data snapshot) ' : '';
|
|
300
|
+
console.log(chalk.cyan(` → ${prefix}${step.label}`));
|
|
301
|
+
}
|
|
302
|
+
// Pre-transaction data snapshots — run outside the transaction so they
|
|
303
|
+
// survive even if the subsequent ALTER rolls back
|
|
304
|
+
const preSteps = plan.filter(p => p.preTransactionSqls?.length);
|
|
305
|
+
if (preSteps.length > 0) {
|
|
306
|
+
console.log(chalk.blue(`\nCreating ${preSteps.length} data snapshot(s)...`));
|
|
200
307
|
try {
|
|
201
|
-
|
|
308
|
+
for (const step of preSteps) {
|
|
309
|
+
for (const sql of step.preTransactionSqls) {
|
|
310
|
+
await runQuery(projectRef, sql);
|
|
311
|
+
console.log(chalk.gray(` ✓ ${sql}`));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
console.error(chalk.red(`\n🔥 Data snapshot failed — aborting revert.`));
|
|
317
|
+
console.error(chalk.red(` Reason: ${e.message}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const allSqls = plan.flatMap(p => p.sqls);
|
|
322
|
+
try {
|
|
323
|
+
await runTransaction(projectRef, allSqls);
|
|
324
|
+
console.log(chalk.green('\n✅ Database reset complete!'));
|
|
325
|
+
if (preSteps.length > 0) {
|
|
326
|
+
console.log(chalk.gray(' Data snapshot table(s) preserved. Drop them manually when no longer needed.'));
|
|
202
327
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
console.error(chalk.red(`\n🔥 Revert failed — database unchanged (transaction rolled back).`));
|
|
331
|
+
if (preSteps.length > 0) {
|
|
332
|
+
console.error(chalk.yellow(` Data snapshot(s) were created before the failure and are still intact.`));
|
|
333
|
+
}
|
|
334
|
+
console.error(chalk.red(` Reason: ${e.message}`));
|
|
335
|
+
return;
|
|
206
336
|
}
|
|
207
337
|
}
|
|
208
|
-
// 5. Update local working tree
|
|
209
|
-
// Clear existing supabase directory safely
|
|
338
|
+
// 5. Update local working tree to match the commit
|
|
210
339
|
if (filterFiles.length === 0) {
|
|
211
340
|
try {
|
|
212
341
|
await fs.rm('supabase', { recursive: true, force: true });
|
|
213
342
|
}
|
|
214
|
-
catch
|
|
343
|
+
catch { }
|
|
215
344
|
}
|
|
216
|
-
// Recreate files from tree
|
|
217
345
|
for (const [relPath, hash] of Object.entries(tree)) {
|
|
218
346
|
if (filterFiles.length > 0 && !filterFiles.includes(relPath))
|
|
219
347
|
continue;
|
|
@@ -222,12 +350,36 @@ export async function revert(argv) {
|
|
|
222
350
|
const content = await readObject(hash);
|
|
223
351
|
await fs.writeFile(fullPath, content, 'utf-8');
|
|
224
352
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
353
|
+
console.log(chalk.gray('Local files synchronized with reverted state.'));
|
|
354
|
+
// 3.3 — Create a new commit recording this revert in history
|
|
355
|
+
// (mirrors git revert: creates a new commit rather than moving HEAD back)
|
|
356
|
+
if (filterFiles.length === 0) {
|
|
357
|
+
try {
|
|
358
|
+
const revertedHash = commitHash ?? 'HEAD';
|
|
359
|
+
const config = JSON.parse(await fs.readFile(CONFIG_FILE, 'utf-8'));
|
|
360
|
+
const currentHead = await fs.readFile(path.join(GITBASE_DIR, 'HEAD'), 'utf-8').catch(() => null);
|
|
361
|
+
// Re-use the reverted commit's tree directly
|
|
362
|
+
const revertedCommit = await readCommit(Array.isArray(revertedHash) ? revertedHash : commitHash);
|
|
363
|
+
const revertCommitObj = {
|
|
364
|
+
tree: revertedCommit.tree,
|
|
365
|
+
parent: currentHead,
|
|
366
|
+
message: `Revert to ${(commitHash ?? '').substring(0, 7)}`,
|
|
367
|
+
timestamp: new Date().toISOString(),
|
|
368
|
+
author: process.env.USERNAME || process.env.USER || 'unknown'
|
|
369
|
+
};
|
|
370
|
+
const revertCommitJson = JSON.stringify(revertCommitObj, null, 2);
|
|
371
|
+
const revertCommitHash = hashString(revertCommitJson);
|
|
372
|
+
await fs.mkdir(path.join(GITBASE_DIR, 'objects'), { recursive: true });
|
|
373
|
+
await fs.writeFile(path.join(GITBASE_DIR, 'objects', revertCommitHash), revertCommitJson, 'utf-8');
|
|
374
|
+
await fs.writeFile(path.join(GITBASE_DIR, 'HEAD'), revertCommitHash, 'utf-8');
|
|
375
|
+
if (config.branches && config.currentBranch) {
|
|
376
|
+
config.branches[config.currentBranch].head = revertCommitHash;
|
|
377
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
378
|
+
}
|
|
379
|
+
console.log(chalk.green(`[${revertCommitHash.substring(0, 7)}] Revert to ${(commitHash ?? '').substring(0, 7)}`));
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
console.warn(chalk.yellow(`Warning: could not create revert commit: ${e.message}`));
|
|
383
|
+
}
|
|
231
384
|
}
|
|
232
|
-
console.log(chalk.gray('Local files have been synchronized.'));
|
|
233
385
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { pull } from './pull.js';
|
|
3
|
+
import { commit } from './commit.js';
|
|
4
|
+
export async function snapshot(argv) {
|
|
5
|
+
const intervalMinutes = argv.interval;
|
|
6
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
7
|
+
console.log(chalk.blue(`Starting GitBase snapshot daemon...`));
|
|
8
|
+
console.log(chalk.cyan(`Interval: Every ${intervalMinutes} minutes`));
|
|
9
|
+
console.log(chalk.gray(`Press Ctrl+C to exit.\n`));
|
|
10
|
+
// Force pull to require a manual run once if we want, or just auto-run immediately
|
|
11
|
+
await runSnapshotCycle();
|
|
12
|
+
setInterval(async () => {
|
|
13
|
+
await runSnapshotCycle();
|
|
14
|
+
}, intervalMs);
|
|
15
|
+
}
|
|
16
|
+
async function runSnapshotCycle() {
|
|
17
|
+
console.log(chalk.gray(`[${new Date().toISOString()}] Checking for database changes...`));
|
|
18
|
+
try {
|
|
19
|
+
// Run a silent status check (returns true if changes exist)
|
|
20
|
+
const hasChanges = await checkStatusSilent();
|
|
21
|
+
if (hasChanges) {
|
|
22
|
+
console.log(chalk.yellow(` Changes detected in live database.`));
|
|
23
|
+
// 1. Pull changes
|
|
24
|
+
console.log(chalk.blue(` Pulling changes...`));
|
|
25
|
+
await pull({ _: [], $0: '' }); // Simulate argv
|
|
26
|
+
// 2. Commit changes
|
|
27
|
+
const message = `Auto-snapshot: ${new Date().toISOString()}`;
|
|
28
|
+
console.log(chalk.blue(` Committing: "${message}"`));
|
|
29
|
+
await commit({ message, _: [], $0: '' });
|
|
30
|
+
console.log(chalk.green(` ✅ Snapshot complete.`));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log(chalk.gray(` No changes. Sleeping.`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(chalk.red(` ❌ Snapshot failed: ${e.message}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A modified version of status that returns a boolean instead of printing to stdout,
|
|
42
|
+
* so we can run it quietly.
|
|
43
|
+
*/
|
|
44
|
+
async function checkStatusSilent() {
|
|
45
|
+
const { getStatus } = await import('./status.js');
|
|
46
|
+
const changes = await getStatus(false);
|
|
47
|
+
// changes is an array like [{ type: 'modified', ... }]
|
|
48
|
+
return changes ? changes.length > 0 : false;
|
|
49
|
+
}
|