lib/sql/src/main.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty = @import("pretty");
3 const sql = @import("sql");
4 const sys = @import("sys");
5
6 const Allocator = std.mem.Allocator;
7 const io = std.Options.debug_io;
8
9 const default_database_path = "tiny.sql.db";
10 const default_wal_path = "tiny.sql.wal";
11 const default_history_path = "tiny.sql.history";
12 const default_remotes_path = "tiny.sql.remotes";
13 const default_checkout_path = "tiny.sql.checkout";
14 const default_upstreams_path = "tiny.sql.upstreams";
15
16 const CliOptions = struct {
17 database: []const u8 = default_database_path,
18 wal: []const u8 = default_wal_path,
19 history: []const u8 = default_history_path,
20 remotes: []const u8 = default_remotes_path,
21 checkout: []const u8 = default_checkout_path,
22 upstreams: []const u8 = default_upstreams_path,
23 name: []const u8 = "origin",
24 branch: ?[]const u8 = null,
25 lock: ?[]const u8 = null,
26 prune: bool = false,
27 max_wal_bytes: usize = sql.file.default_max_wal_bytes,
28 };
29
30 const AllowedOptions = struct {
31 database: bool = false,
32 wal: bool = false,
33 history: bool = false,
34 remotes: bool = false,
35 checkout: bool = false,
36 upstreams: bool = false,
37 name: bool = false,
38 branch: bool = false,
39 lock: bool = false,
40 prune: bool = false,
41 };
42
43 const ParsedArgs = struct {
44 options: CliOptions,
45 positionals: []const []const u8,
46
47 fn deinit(self: *ParsedArgs, allocator: Allocator) void {
48 if (self.positionals.len != 0) allocator.free(self.positionals);
49 self.* = undefined;
50 }
51 };
52
53 const AheadBehind = struct {
54 ahead: usize = 0,
55 behind: usize = 0,
56 };
57
58 const ResolveSide = enum {
59 current,
60 base,
61 ours,
62 theirs,
63 };
64
65 const VersionControlSqlTable = enum {
66 refs,
67 branches,
68 remote_branches,
69 tags,
70 status,
71 commits,
72 log,
73 commit_ancestors,
74 };
75
76 const VersionControlDynamicSqlTableKind = enum {
77 history,
78 diff,
79 working_diff,
80 };
81
82 const VersionControlDynamicSqlTable = struct {
83 kind: VersionControlDynamicSqlTableKind,
84 relation: []const u8,
85 };
86
87 const VersionControlSqlProcedure = enum {
88 commit,
89 checkout,
90 merge,
91 branch,
92 tag,
93 tag_delete,
94 fetch,
95 pull,
96 push,
97 };
98
99 const VersionControlSqlCall = struct {
100 procedure: VersionControlSqlProcedure,
101 args: [3][]const u8 = undefined,
102 arg_count: usize = 0,
103
104 fn arguments(self: *const VersionControlSqlCall) []const []const u8 {
105 return self.args[0..self.arg_count];
106 }
107 };
108
109 const FetchCallArgs = struct {
110 remote: []const u8 = "origin",
111 prune: bool = false,
112 };
113
114 const PullCallArgs = struct {
115 args: [2][]const u8 = undefined,
116 arg_count: usize = 0,
117 prune: bool = false,
118
119 fn positionals(self: *const PullCallArgs) []const []const u8 {
120 return self.args[0..self.arg_count];
121 }
122 };
123
124 const RemoteBranchNameParts = struct {
125 remote: []const u8,
126 branch: []const u8,
127 };
128
129 const RemoteBranchTarget = struct {
130 allocator: Allocator,
131 remote: []u8,
132 branch: []u8,
133
134 fn init(allocator: Allocator, remote_name: []const u8, branch_name: []const u8) !RemoteBranchTarget {
135 const owned_remote = try allocator.dupe(u8, remote_name);
136 errdefer allocator.free(owned_remote);
137 const owned_branch = try allocator.dupe(u8, branch_name);
138 return .{
139 .allocator = allocator,
140 .remote = owned_remote,
141 .branch = owned_branch,
142 };
143 }
144
145 fn deinit(self: *RemoteBranchTarget) void {
146 self.allocator.free(self.remote);
147 self.allocator.free(self.branch);
148 self.* = undefined;
149 }
150 };
151
152 const PushRefSpec = struct {
153 allocator: Allocator,
154 delete: bool = false,
155 local_ref: ?[]u8 = null,
156 remote_ref: []u8,
157
158 fn deinit(self: *PushRefSpec) void {
159 if (self.local_ref) |local_ref| self.allocator.free(local_ref);
160 self.allocator.free(self.remote_ref);
161 self.* = undefined;
162 }
163 };
164
165 const CheckoutState = struct {
166 allocator: Allocator,
167 branch: []u8,
168 working_root: ?sql.Hash = null,
169 merge_head: ?sql.Hash = null,
170
171 fn init(allocator: Allocator, branch: []const u8, merge_head: ?sql.Hash) !CheckoutState {
172 return try initWithWorking(allocator, branch, null, merge_head);
173 }
174
175 fn initWithWorking(allocator: Allocator, branch: []const u8, working_root: ?sql.Hash, merge_head: ?sql.Hash) !CheckoutState {
176 return .{
177 .allocator = allocator,
178 .branch = try allocator.dupe(u8, branch),
179 .working_root = working_root,
180 .merge_head = merge_head,
181 };
182 }
183
184 fn deinit(self: *CheckoutState) void {
185 self.allocator.free(self.branch);
186 self.* = undefined;
187 }
188 };
189
190 const LiveConnection = struct {
191 allocator: Allocator,
192 workspace: *sql.FileDatabaseWorkspace,
193 database: *sql.FileDatabase,
194 history: sql.History,
195 connection: sql.Connection,
196 state: CheckoutState,
197
198 fn deinit(self: *LiveConnection) void {
199 self.connection.deinit();
200 self.state.deinit();
201 self.history.deinit();
202 self.database.deinit();
203 self.allocator.destroy(self.database);
204 self.workspace.deallocate(self.allocator);
205 self.allocator.destroy(self.workspace);
206 self.* = undefined;
207 }
208 };
209
210 pub fn main(init: std.process.Init) u8 {
211 return mainWithArgs(init.minimal.args) catch |err| {
212 writeFatal(err);
213 return 1;
214 };
215 }
216
217 fn mainWithArgs(process_args: std.process.Args) !u8 {
218 const backing_allocator = sys.allocator.processAllocator();
219 var arena_state = std.heap.ArenaAllocator.init(backing_allocator);
220 defer arena_state.deinit();
221 const allocator = arena_state.allocator();
222
223 const args_z = try process_args.toSlice(allocator);
224 var args_list: std.ArrayList([]const u8) = .empty;
225 for (args_z) |arg_z| try args_list.append(allocator, arg_z[0..arg_z.len]);
226
227 var stdout_buffer: [8192]u8 = undefined;
228 var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
229 const stdout = &stdout_writer.interface;
230 defer stdout.flush() catch {};
231
232 var stderr_buffer: [8192]u8 = undefined;
233 var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
234 const stderr = &stderr_writer.interface;
235 defer stderr.flush() catch {};
236
237 return try run(allocator, args_list.items, stdout, stderr);
238 }
239
240 fn run(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer, stderr: *std.Io.Writer) !u8 {
241 var stdout_text_buffer: [8192]u8 = undefined;
242 var stdout_text = pretty.TextWriter.init(stdout, &stdout_text_buffer, .{ .width = 100 });
243 const human_stdout = &stdout_text.writer;
244 var stderr_text_buffer: [1024]u8 = undefined;
245 var stderr_text = pretty.TextWriter.init(stderr, &stderr_text_buffer, .{ .width = 100 });
246 const human_stderr = &stderr_text.writer;
247
248 if (args.len < 2 or isHelp(args[1])) {
249 try writeUsage(human_stdout);
250 try human_stdout.flush();
251 return 0;
252 }
253
254 const command = args[1];
255 const result = if (std.mem.eql(u8, command, "init"))
256 runInit(allocator, args[2..], human_stdout)
257 else if (std.mem.eql(u8, command, "sql"))
258 runSql(allocator, args[2..], human_stdout)
259 else if (std.mem.eql(u8, command, "dump"))
260 runDump(allocator, args[2..], stdout)
261 else if (std.mem.eql(u8, command, "restore"))
262 runRestore(allocator, args[2..], human_stdout)
263 else if (std.mem.eql(u8, command, "commit"))
264 runCommit(allocator, args[2..], human_stdout)
265 else if (std.mem.eql(u8, command, "pack"))
266 @import("pack.zig").run(args[2..], human_stdout, human_stderr)
267 else if (std.mem.eql(u8, command, "status"))
268 runStatus(allocator, args[2..], human_stdout)
269 else if (std.mem.eql(u8, command, "log"))
270 runLog(allocator, args[2..], human_stdout)
271 else if (std.mem.eql(u8, command, "blame"))
272 runBlame(allocator, args[2..], human_stdout)
273 else if (std.mem.eql(u8, command, "diff"))
274 runDiff(allocator, args[2..], human_stdout)
275 else if (std.mem.eql(u8, command, "checkout"))
276 runCheckout(allocator, args[2..], human_stdout)
277 else if (std.mem.eql(u8, command, "merge"))
278 runMerge(allocator, args[2..], human_stdout)
279 else if (std.mem.eql(u8, command, "conflicts"))
280 runConflicts(allocator, args[2..], human_stdout)
281 else if (std.mem.eql(u8, command, "resolve"))
282 runResolve(allocator, args[2..], human_stdout)
283 else if (std.mem.eql(u8, command, "refs"))
284 runRefs(allocator, args[2..], human_stdout)
285 else if (std.mem.eql(u8, command, "remote"))
286 runRemote(allocator, args[2..], human_stdout)
287 else if (std.mem.eql(u8, command, "upstream"))
288 runUpstream(allocator, args[2..], human_stdout)
289 else if (std.mem.eql(u8, command, "clone"))
290 runClone(allocator, args[2..], human_stdout)
291 else if (std.mem.eql(u8, command, "fetch"))
292 runFetch(allocator, args[2..], human_stdout)
293 else if (std.mem.eql(u8, command, "pull"))
294 runPull(allocator, args[2..], human_stdout)
295 else if (std.mem.eql(u8, command, "push"))
296 runPush(allocator, args[2..], human_stdout)
297 else if (std.mem.eql(u8, command, "tag"))
298 runTag(allocator, args[2..], human_stdout)
299 else if (std.mem.eql(u8, command, "branch"))
300 runBranch(allocator, args[2..], human_stdout)
301 else
302 error.UnknownCommand;
303
304 const status: u8 = result catch |err| status: {
305 try human_stderr.print("tiny-sql: {s}\n", .{errorText(err)});
306 break :status 1;
307 };
308 try human_stdout.flush();
309 try human_stderr.flush();
310 return status;
311 }
312
313 fn runInit(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
314 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
315 defer parsed.deinit(allocator);
316 if (parsed.positionals.len > 1) return error.InvalidArguments;
317 const branch_name = if (parsed.positionals.len == 1) parsed.positionals[0] else "main";
318
319 var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
320 defer database_workspace.deallocate(allocator);
321 var database = try openDatabase(allocator, &database_workspace, parsed.options);
322 defer database.deinit();
323 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .truncate });
324 defer history.deinit();
325 var connection = try sql.Connection.create(allocator, &database, &history, .{ .branch = branch_name });
326 defer connection.deinit();
327 var state = try CheckoutState.init(allocator, branch_name, null);
328 defer state.deinit();
329 try writeCheckoutState(allocator, parsed.options.checkout, &state);
330 try stdout.print("initialized {s}\n", .{branch_name});
331 return 0;
332 }
333
334 fn runDump(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
335 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .branch = true });
336 defer parsed.deinit(allocator);
337 if (parsed.positionals.len != 0) return error.InvalidArguments;
338 parsed.options.max_wal_bytes = try dumpWalLimit(parsed.options.wal);
339
340 var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
341 defer database_workspace.deallocate(allocator);
342 var database = try sql.FileDatabase.openReadOnly(allocator, &database_workspace, std.Io.Dir.cwd(), .{
343 .paths = .{ .database = parsed.options.database, .wal = parsed.options.wal },
344 .header = cliWalHeader(),
345 .max_wal_bytes = parsed.options.max_wal_bytes,
346 });
347 defer database.deinit();
348 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{
349 .path = parsed.options.history,
350 .create = false,
351 .read_only = true,
352 .recovery = .reject,
353 });
354 defer history.deinit();
355 var connection = try sql.Connection.open(allocator, &database, &history, .{ .branch = parsed.options.branch orelse "main" });
356 defer connection.deinit();
357
358 var names = try connection.catalog.relationNames(allocator);
359 defer names.deinit();
360 for (names.names) |name| {
361 var handle = try connection.catalog.openRelation(allocator, name);
362 defer handle.deinit();
363 var scan: sql.TableScan = undefined;
364 try handle.relation.scan(&scan, allocator, null, null);
365 defer scan.deinit();
366 while (try scan.next()) |entry| {
367 const view = try entry.view();
368 var json = pretty.json.Writer.init(stdout, .minified);
369 var object = try json.object();
370 try object.field("t", name);
371 try object.field("r", entry.rowid);
372 var values = try object.array("v");
373 var index: usize = 0;
374 while (index < view.count) : (index += 1) {
375 try writeDumpValue(values, try view.column(index));
376 }
377 try values.end();
378 try object.endLine();
379 }
380 }
381 try stdout.flush();
382 return 0;
383 }
384
385 fn writeDumpValue(values: pretty.json.Array, value: sql.RowValue) !void {
386 switch (value) {
387 .nil => try values.element(null),
388 .integer => |integer| try values.element(integer),
389 .text => |text| {
390 var pair = try values.array();
391 try pair.element(@as(u8, 0));
392 try pair.element(text);
393 try pair.end();
394 },
395 .blob => |blob| {
396 var pair = try values.array();
397 try pair.element(@as(u8, 1));
398 try pair.base64String(blob);
399 try pair.end();
400 },
401 }
402 }
403
404 fn runRestore(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
405 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .branch = true });
406 defer parsed.deinit(allocator);
407 if (parsed.positionals.len != 1) return error.InvalidArguments;
408
409 const payload = try std.Io.Dir.cwd().readFileAlloc(io, parsed.positionals[0], allocator, .limited(1 << 32));
410 defer allocator.free(payload);
411
412 var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
413 defer database_workspace.deallocate(allocator);
414 var database = try openDatabase(allocator, &database_workspace, parsed.options);
415 defer database.deinit();
416 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .truncate });
417 defer history.deinit();
418 var connection = try sql.Connection.open(allocator, &database, &history, .{ .branch = parsed.options.branch orelse "main" });
419 defer connection.deinit();
420
421 var restored: usize = 0;
422 {
423 var staged_names = std.StringArrayHashMapUnmanaged(bool){};
424 defer {
425 for (staged_names.keys()) |name| allocator.free(name);
426 staged_names.deinit(allocator);
427 }
428 const limits = try restoreLimits(allocator, payload, &staged_names);
429 var workspace = try sql.DatabaseWrite.Workspace.allocate(allocator, limits);
430 defer workspace.deallocate(allocator);
431 var write = try connection.beginWrite(&workspace, allocator, .{
432 .limits = limits,
433 .durability = .buffered,
434 });
435 defer write.deinit();
436
437 var lines = std.mem.splitScalar(u8, payload, '\n');
438 while (lines.next()) |line| {
439 if (line.len == 0) continue;
440 var arena = std.heap.ArenaAllocator.init(allocator);
441 defer arena.deinit();
442 const row = try parseRestoreRow(arena.allocator(), line);
443 const staged = staged_names.getPtr(row.table) orelse return error.InvalidArguments;
444 if (!staged.*) {
445 var relation_session = try sql.RelationSession.open(
446 allocator,
447 &connection.catalog,
448 row.table,
449 );
450 errdefer relation_session.deinit();
451 try write.stageRelation(&relation_session);
452 staged.* = true;
453 }
454 try write.stagePut(row.table, row.rowid, row.values);
455 restored += 1;
456 }
457 if (write.pendingRelations() != 0) {
458 var flush = try write.flush();
459 flush.deinit();
460 }
461 }
462 if ((try connection.checkout()).working.dirty()) {
463 try connection.stage();
464 _ = try connection.commit(&history);
465 try database.syncWal();
466 }
467 try stdout.print("restored {d} rows\n", .{restored});
468 try stdout.flush();
469 return 0;
470 }
471
472 const RestoreRow = struct {
473 table: []const u8,
474 rowid: i64,
475 values: []const sql.RowValue,
476 };
477
478 fn restoreLimits(
479 allocator: Allocator,
480 payload: []const u8,
481 tables: *std.StringArrayHashMapUnmanaged(bool),
482 ) !sql.DatabaseWrite.Limits {
483 var edits: usize = 0;
484 var payload_bytes: usize = 0;
485 var lines = std.mem.splitScalar(u8, payload, '\n');
486 while (lines.next()) |line| {
487 if (line.len == 0) continue;
488 var arena = std.heap.ArenaAllocator.init(allocator);
489 defer arena.deinit();
490 const row = try parseRestoreRow(arena.allocator(), line);
491 edits = std.math.add(usize, edits, 1) catch return error.CapacityOverflow;
492 payload_bytes = std.math.add(
493 usize,
494 payload_bytes,
495 try sql.row.encodedSize(row.values),
496 ) catch return error.CapacityOverflow;
497 if (!tables.contains(row.table)) {
498 try addRestoreTable(allocator, tables, row.table);
499 }
500 }
501 return .{
502 .relations = tables.count(),
503 .edits = edits,
504 .payload_bytes = payload_bytes,
505 .assignments = 0,
506 };
507 }
508
509 fn addRestoreTable(
510 allocator: Allocator,
511 tables: *std.StringArrayHashMapUnmanaged(bool),
512 name: []const u8,
513 ) !void {
514 const owned = try allocator.dupe(u8, name);
515 errdefer allocator.free(owned);
516 try tables.put(allocator, owned, false);
517 }
518
519 fn parseRestoreRow(allocator: Allocator, line: []const u8) !RestoreRow {
520 const parsed = std.json.parseFromSliceLeaky(
521 std.json.Value,
522 allocator,
523 line,
524 .{},
525 ) catch return error.InvalidArguments;
526 const object = switch (parsed) {
527 .object => |object| object,
528 else => return error.InvalidArguments,
529 };
530 const table = switch (object.get("t") orelse return error.InvalidArguments) {
531 .string => |value| value,
532 else => return error.InvalidArguments,
533 };
534 const rowid = switch (object.get("r") orelse return error.InvalidArguments) {
535 .integer => |value| value,
536 else => return error.InvalidArguments,
537 };
538 const columns = switch (object.get("v") orelse return error.InvalidArguments) {
539 .array => |value| value.items,
540 else => return error.InvalidArguments,
541 };
542 const values = try allocator.alloc(sql.RowValue, columns.len);
543 for (columns, values) |column, *value| value.* = try restoreValue(allocator, column);
544 return .{ .table = table, .rowid = rowid, .values = values };
545 }
546
547 fn restoreValue(allocator: Allocator, column: std.json.Value) !sql.RowValue {
548 return switch (column) {
549 .null => .nil,
550 .integer => |number| .{ .integer = number },
551 .array => |pair| blk: {
552 if (pair.items.len != 2) return error.InvalidArguments;
553 const tag = switch (pair.items[0]) {
554 .integer => |value| value,
555 else => return error.InvalidArguments,
556 };
557 const body = switch (pair.items[1]) {
558 .string => |value| value,
559 else => return error.InvalidArguments,
560 };
561 if (tag == 0) break :blk .{ .text = body };
562 if (tag != 1) return error.InvalidArguments;
563 const size = std.base64.standard.Decoder.calcSizeForSlice(body) catch
564 return error.InvalidArguments;
565 const decoded = try allocator.alloc(u8, size);
566 std.base64.standard.Decoder.decode(decoded, body) catch
567 return error.InvalidArguments;
568 break :blk .{ .blob = decoded };
569 },
570 else => error.InvalidArguments,
571 };
572 }
573
574 fn runSql(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
575 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true, .upstreams = true });
576 defer parsed.deinit(allocator);
577 if (parsed.positionals.len != 1) return error.InvalidArguments;
578
579 var live = try openLiveConnection(allocator, parsed.options);
580 defer live.deinit();
581 if (versionControlSqlTable(parsed.positionals[0])) |table| {
582 try writeVersionControlSqlTable(allocator, stdout, &live, parsed.options, table);
583 return 0;
584 }
585 if (versionControlDynamicSqlTable(parsed.positionals[0])) |table| {
586 try writeVersionControlDynamicSqlTable(allocator, stdout, &live, table);
587 return 0;
588 }
589 if (try versionControlSqlCall(parsed.positionals[0])) |call| {
590 try executeVersionControlSqlCall(allocator, stdout, &live, parsed.options, &call);
591 return 0;
592 }
593 var result = try live.connection.execute(allocator, parsed.positionals[0], .{ .durability = .synced });
594 defer result.deinit(allocator);
595 try writeSqlResult(stdout, &result);
596 if ((live.state.working_root != null or live.state.merge_head != null) and (try live.connection.checkout()).working.dirty()) {
597 try persistWorkingState(allocator, parsed.options, &live);
598 }
599 return 0;
600 }
601
602 fn commitLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, checkout_path: []const u8) !void {
603 try live.connection.stage();
604 const commit_hash = if (live.state.merge_head) |merge_head| commit: {
605 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
606 defer artifacts.deinit();
607 if (artifacts.artifacts.len != 0) return error.UnresolvedConflicts;
608 break :commit try live.connection.mergeCommit(&live.history, merge_head);
609 } else try live.connection.commit(&live.history);
610 live.state.merge_head = null;
611 live.state.working_root = null;
612 try writeCheckoutState(allocator, checkout_path, &live.state);
613 try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(commit_hash, .lower)});
614 }
615
616 fn checkoutLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, checkout_path: []const u8, branch_name: []const u8) !void {
617 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
618 try live.connection.checkoutBranch(allocator, &live.history, branch_name);
619 live.state.deinit();
620 live.state = try CheckoutState.init(allocator, branch_name, null);
621 try writeCheckoutState(allocator, checkout_path, &live.state);
622 try stdout.print("checkout {s}\n", .{branch_name});
623 }
624
625 fn mergeLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, ref_name: []const u8) !void {
626 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
627 const theirs = (try live.history.ref(ref_name)) orelse return error.RefNotFound;
628 var result = try live.connection.mergeBranch(allocator, &live.history, ref_name, .{});
629 defer result.deinit();
630 const conflicts = conflictCount(&result);
631 if ((try live.connection.checkout()).working.dirty()) {
632 live.state.merge_head = theirs.target;
633 try persistWorkingState(allocator, options, live);
634 }
635 if (conflicts == 0) {
636 try stdout.print("merged {s}\n", .{ref_name});
637 } else {
638 try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
639 }
640 }
641
642 fn branchLive(stdout: *std.Io.Writer, history: *sql.History, branch_name: []const u8, start_name: []const u8) !void {
643 const target = (try history.ref(start_name)) orelse return error.RefNotFound;
644 _ = try history.createBranch(branch_name, target.target);
645 try stdout.print("branch {s} {s}\n", .{ branch_name, start_name });
646 }
647
648 fn tagLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, state: *const CheckoutState, tag_name: []const u8, target_name: ?[]const u8) !void {
649 const tag_ref_name = try tagRefName(allocator, tag_name);
650 defer allocator.free(tag_ref_name);
651 if ((try history.ref(tag_ref_name)) != null) return error.RefExists;
652 const target_hash = if (target_name) |name| try resolveCommitish(allocator, history, name) else ((try history.ref(state.branch)) orelse return error.RefNotFound).target;
653 try history.putRef(.{
654 .name = tag_ref_name,
655 .target = target_hash,
656 });
657 try stdout.print("tag {s} {s}\n", .{ tag_name, std.fmt.bytesToHex(target_hash, .lower) });
658 }
659
660 fn tagDeleteLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, tag_name: []const u8) !void {
661 const tag_ref_name = try tagRefName(allocator, tag_name);
662 defer allocator.free(tag_ref_name);
663 try history.deleteRef(tag_ref_name);
664 try stdout.print("deleted tag {s}\n", .{tag_name});
665 }
666
667 fn fetchLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, options: CliOptions, remote_name: []const u8, prune: bool) !void {
668 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
669 defer config.deinit();
670 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), remote_name);
671 const stats = try sql.fetchHistoryFile(allocator, history, remote, .{ .remote = remote_name, .prune = prune });
672 try stdout.print("fetched {s}\n", .{remote_name});
673 try writeStats(stdout, stats);
674 }
675
676 fn pullLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, positionals: []const []const u8, prune: bool) !void {
677 if (positionals.len > 2) return error.InvalidArguments;
678 var target = try resolveRemoteBranchTarget(allocator, options.upstreams, live.state.branch, positionals);
679 defer target.deinit();
680
681 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
682 defer config.deinit();
683 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
684
685 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
686
687 const stats = try sql.fetchHistoryFile(allocator, &live.history, remote, .{ .remote = target.remote, .prune = prune });
688 const remote_ref_name = try remoteTrackingRefName(allocator, target.remote, target.branch);
689 defer allocator.free(remote_ref_name);
690 const remote_ref = (try live.history.ref(remote_ref_name)) orelse return error.RefNotFound;
691
692 var merged_ref: ?[]const u8 = null;
693 var conflicts: usize = 0;
694 live.connection.fastForwardBranch(allocator, &live.history, remote_ref.target) catch |err| switch (err) {
695 error.NonFastForward => {
696 var result = try live.connection.mergeBranch(allocator, &live.history, remote_ref_name, .{});
697 defer result.deinit();
698 conflicts = conflictCount(&result);
699 if ((try live.connection.checkout()).working.dirty()) {
700 live.state.merge_head = remote_ref.target;
701 try persistWorkingState(allocator, options, live);
702 }
703 merged_ref = remote_ref_name;
704 },
705 else => return err,
706 };
707
708 try writeCheckoutState(allocator, options.checkout, &live.state);
709 try stdout.print("pulled {s} {s}\n", .{ target.remote, target.branch });
710 if (merged_ref) |ref_name| {
711 if (conflicts == 0) {
712 try stdout.print("merged {s}\n", .{ref_name});
713 } else {
714 try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
715 }
716 }
717 try writeStats(stdout, stats);
718 }
719
720 fn pushLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, positionals: []const []const u8) !void {
721 if (positionals.len > 2) return error.InvalidArguments;
722 var target = try resolveRemoteBranchTarget(allocator, options.upstreams, live.state.branch, positionals[0..@min(positionals.len, 1)]);
723 defer target.deinit();
724 var refspec = if (positionals.len == 2) try resolvePushRefSpec(allocator, &live.history, positionals[1]) else try defaultPushRefSpec(allocator, live.state.branch, target.branch);
725 defer refspec.deinit();
726
727 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
728 defer config.deinit();
729 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
730
731 if (refspec.delete) {
732 const stats = try sql.deleteHistoryFileRemoteRef(allocator, remote, refspec.remote_ref);
733 try removeRemoteTrackingRefIfPresent(allocator, &live.history, target.remote, refspec.remote_ref);
734 try stdout.print("deleted {s} {s}\n", .{ target.remote, refspec.remote_ref });
735 try writeStats(stdout, stats);
736 return;
737 }
738
739 const local_ref_name = refspec.local_ref.?;
740 const stats = try sql.pushHistoryFileFastForwardTo(allocator, &live.history, remote, local_ref_name, refspec.remote_ref);
741 try updateRemoteTrackingRefAfterPush(allocator, &live.history, target.remote, refspec.remote_ref, local_ref_name);
742 if (std.mem.eql(u8, local_ref_name, refspec.remote_ref)) {
743 try stdout.print("pushed {s} {s}\n", .{ target.remote, local_ref_name });
744 } else {
745 try stdout.print("pushed {s} {s}:{s}\n", .{ target.remote, local_ref_name, refspec.remote_ref });
746 }
747 try writeStats(stdout, stats);
748 }
749
750 fn runCommit(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
751 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
752 defer parsed.deinit(allocator);
753 if (parsed.positionals.len != 0) return error.InvalidArguments;
754
755 var live = try openLiveConnection(allocator, parsed.options);
756 defer live.deinit();
757 try live.connection.stage();
758 const commit_hash = if (live.state.merge_head) |merge_head| commit: {
759 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
760 defer artifacts.deinit();
761 if (artifacts.artifacts.len != 0) return error.UnresolvedConflicts;
762 break :commit try live.connection.mergeCommit(&live.history, merge_head);
763 } else try live.connection.commit(&live.history);
764 live.state.merge_head = null;
765 live.state.working_root = null;
766 try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
767 try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(commit_hash, .lower)});
768 return 0;
769 }
770
771 fn runStatus(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
772 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true, .upstreams = true });
773 defer parsed.deinit(allocator);
774 if (parsed.positionals.len != 0) return error.InvalidArguments;
775
776 var live = try openLiveConnection(allocator, parsed.options);
777 defer live.deinit();
778 const checkout = (try live.connection.checkout());
779 try stdout.print("branch {s}\n", .{live.state.branch});
780 try stdout.print("head {s}\n", .{std.fmt.bytesToHex(checkout.head, .lower)});
781
782 var upstreams = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.upstreams });
783 defer upstreams.deinit();
784 if (upstreams.find(live.state.branch)) |upstream| {
785 try stdout.print("upstream {s} {s}\n", .{ upstream.remote, upstream.remote_branch });
786 const remote_ref_name = try remoteTrackingRefName(allocator, upstream.remote, upstream.remote_branch);
787 defer allocator.free(remote_ref_name);
788 if ((try live.history.ref(remote_ref_name))) |remote_ref| {
789 const counts = try aheadBehind(allocator, &live.history, checkout.head, remote_ref.target);
790 try stdout.print("ahead={d} behind={d}\n", .{ counts.ahead, counts.behind });
791 } else {
792 try stdout.writeAll("upstream-ref missing\n");
793 }
794 } else {
795 try stdout.writeAll("upstream none\n");
796 }
797
798 if (live.state.merge_head) |merge_head| {
799 try stdout.print("merge {s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
800 }
801 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
802 defer artifacts.deinit();
803 try stdout.print("conflicts={d}\n", .{artifacts.artifacts.len});
804 try stdout.print("working={s}\n", .{if (checkout.working.dirty()) "dirty" else "clean"});
805 try stdout.print("staged={s}\n", .{if (checkout.working.hasStaged()) "dirty" else "clean"});
806 return 0;
807 }
808
809 fn runLog(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
810 var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
811 defer parsed.deinit(allocator);
812 if (parsed.positionals.len > 1) return error.InvalidArguments;
813
814 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
815 defer history.deinit();
816 var state = try readCheckoutState(allocator, parsed.options.checkout);
817 defer state.deinit();
818 const ref_name = if (parsed.positionals.len == 1) parsed.positionals[0] else state.branch;
819 const target_hash = try resolveCommitish(allocator, &history, ref_name);
820 try writeCommitLog(allocator, &history, target_hash, stdout);
821 return 0;
822 }
823
824 fn runBlame(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
825 var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
826 defer parsed.deinit(allocator);
827 if (parsed.positionals.len == 0 or parsed.positionals.len > 2) return error.InvalidArguments;
828
829 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
830 defer history.deinit();
831 var state = try readCheckoutState(allocator, parsed.options.checkout);
832 defer state.deinit();
833 const relation_name = parsed.positionals[0];
834 const ref_name = if (parsed.positionals.len == 2) parsed.positionals[1] else state.branch;
835 const head = try resolveCommitish(allocator, &history, ref_name);
836
837 var result = try sql.blame.relation(allocator, &history, head, relation_name);
838 defer result.deinit();
839 for (result.attributions) |attribution| {
840 try stdout.print("{d}\t{s}\n", .{ attribution.rowid, std.fmt.bytesToHex(attribution.commit, .lower) });
841 }
842 return 0;
843 }
844
845 fn runDiff(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
846 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
847 defer parsed.deinit(allocator);
848 if (parsed.positionals.len == 0 or parsed.positionals.len > 3) return error.InvalidArguments;
849
850 var live = try openLiveConnection(allocator, parsed.options);
851 defer live.deinit();
852 const state_branch = live.state.branch;
853
854 if (parsed.positionals.len == 3) {
855 const from_hash = try resolveCommitish(allocator, &live.history, parsed.positionals[0]);
856 const to_hash = try resolveCommitish(allocator, &live.history, parsed.positionals[1]);
857 const relation_name = parsed.positionals[2];
858 var from_view = try relationViewAtCommit(allocator, &live.history, from_hash, relation_name);
859 defer from_view.deinit();
860 var to_view = try relationViewAtCommit(allocator, &live.history, to_hash, relation_name);
861 defer to_view.deinit();
862 var result = try sql.diff.relation(allocator, from_view.snapshot(), to_view.snapshot());
863 defer result.deinit();
864 try writeRelationDiff(stdout, relation_name, &result);
865 return 0;
866 }
867
868 const base_name = if (parsed.positionals.len == 2) parsed.positionals[0] else state_branch;
869 const relation_name = if (parsed.positionals.len == 2) parsed.positionals[1] else parsed.positionals[0];
870 const base_hash = try resolveCommitish(allocator, &live.history, base_name);
871 var base_view = try relationViewAtCommit(allocator, &live.history, base_hash, relation_name);
872 defer base_view.deinit();
873 var working_view = try live.connection.relationView(allocator, relation_name);
874 defer working_view.deinit();
875 var result = try sql.diff.relation(allocator, base_view.snapshot(), working_view.snapshot());
876 defer result.deinit();
877 try writeRelationDiff(stdout, relation_name, &result);
878 return 0;
879 }
880
881 fn runCheckout(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
882 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
883 defer parsed.deinit(allocator);
884 if (parsed.positionals.len != 1) return error.InvalidArguments;
885
886 var live = try openLiveConnection(allocator, parsed.options);
887 defer live.deinit();
888 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
889 try live.connection.checkoutBranch(allocator, &live.history, parsed.positionals[0]);
890 live.state.deinit();
891 live.state = try CheckoutState.init(allocator, parsed.positionals[0], null);
892 try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
893 try stdout.print("checkout {s}\n", .{parsed.positionals[0]});
894 return 0;
895 }
896
897 fn runMerge(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
898 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
899 defer parsed.deinit(allocator);
900 if (parsed.positionals.len != 1) return error.InvalidArguments;
901
902 var live = try openLiveConnection(allocator, parsed.options);
903 defer live.deinit();
904 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
905 const theirs = (try live.history.ref(parsed.positionals[0])) orelse return error.RefNotFound;
906 var result = try live.connection.mergeBranch(allocator, &live.history, parsed.positionals[0], .{});
907 defer result.deinit();
908 const conflicts = conflictCount(&result);
909 if ((try live.connection.checkout()).working.dirty()) {
910 live.state.merge_head = theirs.target;
911 try persistWorkingState(allocator, parsed.options, &live);
912 }
913 if (conflicts == 0) {
914 try stdout.print("merged {s}\n", .{parsed.positionals[0]});
915 } else {
916 try stdout.print("merged {s} conflicts={d}\n", .{ parsed.positionals[0], conflicts });
917 }
918 return 0;
919 }
920
921 fn runConflicts(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
922 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
923 defer parsed.deinit(allocator);
924 if (parsed.positionals.len != 0 and !std.mem.eql(u8, parsed.positionals[0], "show")) return error.InvalidArguments;
925
926 var live = try openLiveConnection(allocator, parsed.options);
927 defer live.deinit();
928 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
929 defer artifacts.deinit();
930 if (parsed.positionals.len != 0) {
931 if (parsed.positionals.len < 2) return error.InvalidArguments;
932 if (parsed.positionals.len == 2 and std.mem.eql(u8, parsed.positionals[1], "all")) {
933 for (artifacts.artifacts) |artifact| try writeConflictArtifactDetail(stdout, artifact);
934 return 0;
935 }
936 for (parsed.positionals[1..]) |hash_text| {
937 const hash = try parseHash(hash_text);
938 const artifact = findConflictArtifact(artifacts.artifacts, hash) orelse return error.ConflictNotFound;
939 try writeConflictArtifactDetail(stdout, artifact);
940 }
941 return 0;
942 }
943 if (artifacts.artifacts.len == 0) {
944 try stdout.writeAll("conflicts=0\n");
945 return 0;
946 }
947 for (artifacts.artifacts) |artifact| try writeConflictArtifact(stdout, artifact);
948 return 0;
949 }
950
951 fn runResolve(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
952 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
953 defer parsed.deinit(allocator);
954 if (parsed.positionals.len == 0) return error.InvalidArguments;
955 const side = resolveSide(parsed.positionals[0]);
956 const resolution_args = if (side == .current) parsed.positionals else parsed.positionals[1..];
957 if (resolution_args.len == 0) return error.InvalidArguments;
958
959 var live = try openLiveConnection(allocator, parsed.options);
960 defer live.deinit();
961 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
962 defer artifacts.deinit();
963
964 var resolved: std.ArrayList(sql.Hash) = .empty;
965 defer resolved.deinit(allocator);
966 if (resolution_args.len == 1 and std.mem.eql(u8, resolution_args[0], "all")) {
967 for (artifacts.artifacts) |artifact| try resolved.append(allocator, artifact.hash);
968 } else {
969 for (resolution_args) |hash_text| try resolved.append(allocator, try parseHash(hash_text));
970 }
971
972 if (side != .current) {
973 for (resolved.items) |hash| {
974 const artifact = findConflictArtifact(artifacts.artifacts, hash) orelse return error.ConflictNotFound;
975 try applyConflictSide(allocator, &live, artifact, side);
976 }
977 try refreshWorkingRootWithConflictRoot(allocator, &live);
978 }
979
980 _ = try live.connection.resolveConflicts(allocator, &live.history, resolved.items);
981 try persistWorkingState(allocator, parsed.options, &live);
982 try stdout.print("resolved {d}\n", .{resolved.items.len});
983 return 0;
984 }
985
986 fn runRefs(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
987 var parsed = try parseArgs(allocator, args, .{ .history = true });
988 defer parsed.deinit(allocator);
989 if (parsed.positionals.len != 0) return error.InvalidArguments;
990
991 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
992 defer history.deinit();
993
994 const refs = try history.refList(allocator);
995 defer sql.history.freeRefList(allocator, refs);
996 try writeRefs(stdout, refs, .all);
997 return 0;
998 }
999
1000 fn runRemote(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1001 if (args.len == 0) return error.InvalidArguments;
1002 if (std.mem.eql(u8, args[0], "list")) return try runRemoteList(allocator, args[1..], stdout);
1003 if (std.mem.eql(u8, args[0], "add")) return try runRemoteAdd(allocator, args[1..], stdout);
1004 return error.UnknownCommand;
1005 }
1006
1007 fn runRemoteList(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1008 var parsed = try parseArgs(allocator, args, .{ .remotes = true });
1009 defer parsed.deinit(allocator);
1010 if (parsed.positionals.len != 0) return error.InvalidArguments;
1011
1012 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1013 defer config.deinit();
1014 for (config.remotes) |remote| {
1015 if (remote.lock_path) |lock_path| {
1016 if (remote.default_branch) |branch_name| {
1017 try stdout.print("{s}\t{s}\tbranch={s}\tlock={s}\n", .{ remote.name, remote.history_path, branch_name, lock_path });
1018 } else {
1019 try stdout.print("{s}\t{s}\tlock={s}\n", .{ remote.name, remote.history_path, lock_path });
1020 }
1021 } else {
1022 if (remote.default_branch) |branch_name| {
1023 try stdout.print("{s}\t{s}\tbranch={s}\n", .{ remote.name, remote.history_path, branch_name });
1024 } else {
1025 try stdout.print("{s}\t{s}\n", .{ remote.name, remote.history_path });
1026 }
1027 }
1028 }
1029 return 0;
1030 }
1031
1032 fn runRemoteAdd(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1033 var parsed = try parseArgs(allocator, args, .{ .remotes = true, .branch = true, .lock = true });
1034 defer parsed.deinit(allocator);
1035 if (parsed.positionals.len != 2) return error.InvalidArguments;
1036
1037 try addRemote(allocator, parsed.options.remotes, parsed.positionals[0], parsed.positionals[1], parsed.options.lock, parsed.options.branch);
1038 try stdout.print("remote {s} {s}\n", .{ parsed.positionals[0], parsed.positionals[1] });
1039 return 0;
1040 }
1041
1042 fn runUpstream(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1043 if (args.len == 0) return error.InvalidArguments;
1044 if (std.mem.eql(u8, args[0], "list")) return try runUpstreamList(allocator, args[1..], stdout);
1045 if (std.mem.eql(u8, args[0], "set")) return try runUpstreamSet(allocator, args[1..], stdout);
1046 return error.UnknownCommand;
1047 }
1048
1049 fn runUpstreamList(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1050 var parsed = try parseArgs(allocator, args, .{ .upstreams = true });
1051 defer parsed.deinit(allocator);
1052 if (parsed.positionals.len != 0) return error.InvalidArguments;
1053
1054 var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.upstreams });
1055 defer config.deinit();
1056 for (config.upstreams) |upstream| {
1057 try stdout.print("{s}\t{s}\t{s}\n", .{ upstream.branch, upstream.remote, upstream.remote_branch });
1058 }
1059 return 0;
1060 }
1061
1062 fn runUpstreamSet(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1063 var parsed = try parseArgs(allocator, args, .{ .upstreams = true });
1064 defer parsed.deinit(allocator);
1065 if (parsed.positionals.len < 2 or parsed.positionals.len > 3) return error.InvalidArguments;
1066
1067 const branch_name = parsed.positionals[0];
1068 const remote_name = parsed.positionals[1];
1069 const remote_branch_name = if (parsed.positionals.len == 3) parsed.positionals[2] else branch_name;
1070 try setBranchUpstream(allocator, parsed.options.upstreams, branch_name, remote_name, remote_branch_name);
1071 try stdout.print("upstream {s} {s} {s}\n", .{ branch_name, remote_name, remote_branch_name });
1072 return 0;
1073 }
1074
1075 fn runClone(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1076 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .remotes = true, .checkout = true, .upstreams = true, .name = true, .branch = true, .lock = true });
1077 defer parsed.deinit(allocator);
1078 if (parsed.positionals.len < 1 or parsed.positionals.len > 2) return error.InvalidArguments;
1079 if (parsed.positionals.len == 2 and parsed.options.branch != null) return error.InvalidArguments;
1080
1081 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .reject });
1082 defer history.deinit();
1083
1084 const remote = sql.HistoryFileRemote{
1085 .dir = std.Io.Dir.cwd(),
1086 .path = parsed.positionals[0],
1087 .lock_path = parsed.options.lock,
1088 };
1089 const stats = try sql.cloneHistoryFile(allocator, remote, &history);
1090 const branch_name = try cloneBranchName(allocator, &history, if (parsed.positionals.len == 2) parsed.positionals[1] else parsed.options.branch);
1091 defer allocator.free(branch_name);
1092 try writeRemoteTrackingRefsForClone(allocator, &history, parsed.options.name);
1093 try addRemote(allocator, parsed.options.remotes, parsed.options.name, parsed.positionals[0], parsed.options.lock, branch_name);
1094 try setBranchUpstream(allocator, parsed.options.upstreams, branch_name, parsed.options.name, branch_name);
1095 try materializeCheckout(allocator, parsed.options, &history, branch_name);
1096 try stdout.print("cloned {s} into {s}\n", .{ parsed.positionals[0], parsed.options.history });
1097 try stdout.print("checkout {s}\n", .{branch_name});
1098 try writeStats(stdout, stats);
1099 return 0;
1100 }
1101
1102 fn runFetch(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1103 var parsed = try parseArgs(allocator, args, .{ .history = true, .remotes = true, .prune = true });
1104 defer parsed.deinit(allocator);
1105 if (parsed.positionals.len > 1) return error.InvalidArguments;
1106 const remote_name = if (parsed.positionals.len == 1) parsed.positionals[0] else "origin";
1107
1108 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1109 defer history.deinit();
1110 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1111 defer config.deinit();
1112 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), remote_name);
1113 const stats = try sql.fetchHistoryFile(allocator, &history, remote, .{ .remote = remote_name, .prune = parsed.options.prune });
1114 try stdout.print("fetched {s}\n", .{remote_name});
1115 try writeStats(stdout, stats);
1116 return 0;
1117 }
1118
1119 fn runPull(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1120 var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .remotes = true, .checkout = true, .upstreams = true, .prune = true });
1121 defer parsed.deinit(allocator);
1122 if (parsed.positionals.len > 2) return error.InvalidArguments;
1123 var state = try readCheckoutState(allocator, parsed.options.checkout);
1124 defer state.deinit();
1125 var target = try resolveRemoteBranchTarget(allocator, parsed.options.upstreams, state.branch, parsed.positionals);
1126 defer target.deinit();
1127
1128 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1129 defer config.deinit();
1130 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
1131
1132 var live = try openLiveConnection(allocator, parsed.options);
1133 defer live.deinit();
1134 if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
1135
1136 const stats = try sql.fetchHistoryFile(allocator, &live.history, remote, .{ .remote = target.remote, .prune = parsed.options.prune });
1137 const remote_ref_name = try remoteTrackingRefName(allocator, target.remote, target.branch);
1138 defer allocator.free(remote_ref_name);
1139 const remote_ref = (try live.history.ref(remote_ref_name)) orelse return error.RefNotFound;
1140
1141 var merged_ref: ?[]const u8 = null;
1142 var conflicts: usize = 0;
1143 live.connection.fastForwardBranch(allocator, &live.history, remote_ref.target) catch |err| switch (err) {
1144 error.NonFastForward => {
1145 var result = try live.connection.mergeBranch(allocator, &live.history, remote_ref_name, .{});
1146 defer result.deinit();
1147 conflicts = conflictCount(&result);
1148 if ((try live.connection.checkout()).working.dirty()) {
1149 live.state.merge_head = remote_ref.target;
1150 try persistWorkingState(allocator, parsed.options, &live);
1151 }
1152 merged_ref = remote_ref_name;
1153 },
1154 else => return err,
1155 };
1156
1157 try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
1158 try stdout.print("pulled {s} {s}\n", .{ target.remote, target.branch });
1159 if (merged_ref) |ref_name| {
1160 if (conflicts == 0) {
1161 try stdout.print("merged {s}\n", .{ref_name});
1162 } else {
1163 try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
1164 }
1165 }
1166 try writeStats(stdout, stats);
1167 return 0;
1168 }
1169
1170 fn runPush(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1171 var parsed = try parseArgs(allocator, args, .{ .history = true, .remotes = true, .checkout = true, .upstreams = true });
1172 defer parsed.deinit(allocator);
1173 if (parsed.positionals.len > 2) return error.InvalidArguments;
1174 var state = try readCheckoutState(allocator, parsed.options.checkout);
1175 defer state.deinit();
1176
1177 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1178 defer history.deinit();
1179 var target = try resolveRemoteBranchTarget(allocator, parsed.options.upstreams, state.branch, parsed.positionals[0..@min(parsed.positionals.len, 1)]);
1180 defer target.deinit();
1181 var refspec = if (parsed.positionals.len == 2) try resolvePushRefSpec(allocator, &history, parsed.positionals[1]) else try defaultPushRefSpec(allocator, state.branch, target.branch);
1182 defer refspec.deinit();
1183
1184 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1185 defer config.deinit();
1186 const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
1187
1188 if (refspec.delete) {
1189 const stats = try sql.deleteHistoryFileRemoteRef(allocator, remote, refspec.remote_ref);
1190 try removeRemoteTrackingRefIfPresent(allocator, &history, target.remote, refspec.remote_ref);
1191 try stdout.print("deleted {s} {s}\n", .{ target.remote, refspec.remote_ref });
1192 try writeStats(stdout, stats);
1193 return 0;
1194 }
1195
1196 const local_ref_name = refspec.local_ref.?;
1197 const stats = try sql.pushHistoryFileFastForwardTo(allocator, &history, remote, local_ref_name, refspec.remote_ref);
1198 try updateRemoteTrackingRefAfterPush(allocator, &history, target.remote, refspec.remote_ref, local_ref_name);
1199 if (std.mem.eql(u8, local_ref_name, refspec.remote_ref)) {
1200 try stdout.print("pushed {s} {s}\n", .{ target.remote, local_ref_name });
1201 } else {
1202 try stdout.print("pushed {s} {s}:{s}\n", .{ target.remote, local_ref_name, refspec.remote_ref });
1203 }
1204 try writeStats(stdout, stats);
1205 return 0;
1206 }
1207
1208 fn runTag(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1209 if (args.len != 0 and isTagDeleteFlag(args[0])) return try runTagDelete(allocator, args[1..], stdout);
1210
1211 var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
1212 defer parsed.deinit(allocator);
1213 if (parsed.positionals.len > 2) return error.InvalidArguments;
1214
1215 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1216 defer history.deinit();
1217
1218 if (parsed.positionals.len == 0) {
1219 const refs = try history.refList(allocator);
1220 defer sql.history.freeRefList(allocator, refs);
1221 try writeTagRefs(stdout, refs);
1222 return 0;
1223 }
1224
1225 const tag_ref_name = try tagRefName(allocator, parsed.positionals[0]);
1226 defer allocator.free(tag_ref_name);
1227 if ((try history.ref(tag_ref_name)) != null) return error.RefExists;
1228
1229 const target_hash = if (parsed.positionals.len == 2) try resolveCommitish(allocator, &history, parsed.positionals[1]) else target: {
1230 var state = try readCheckoutState(allocator, parsed.options.checkout);
1231 defer state.deinit();
1232 break :target ((try history.ref(state.branch)) orelse return error.RefNotFound).target;
1233 };
1234 try history.putRef(.{
1235 .name = tag_ref_name,
1236 .target = target_hash,
1237 });
1238 try stdout.print("tag {s} {s}\n", .{ parsed.positionals[0], std.fmt.bytesToHex(target_hash, .lower) });
1239 return 0;
1240 }
1241
1242 fn runTagDelete(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1243 var parsed = try parseArgs(allocator, args, .{ .history = true });
1244 defer parsed.deinit(allocator);
1245 if (parsed.positionals.len == 0) return error.InvalidArguments;
1246
1247 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1248 defer history.deinit();
1249
1250 for (parsed.positionals) |tag_name| {
1251 const tag_ref_name = try tagRefName(allocator, tag_name);
1252 defer allocator.free(tag_ref_name);
1253 try history.deleteRef(tag_ref_name);
1254 try stdout.print("deleted tag {s}\n", .{tag_name});
1255 }
1256 return 0;
1257 }
1258
1259 fn runBranch(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1260 var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
1261 defer parsed.deinit(allocator);
1262 if (parsed.positionals.len > 2) return error.InvalidArguments;
1263
1264 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1265 defer history.deinit();
1266
1267 if (parsed.positionals.len == 0) {
1268 const refs = try history.refList(allocator);
1269 defer sql.history.freeRefList(allocator, refs);
1270 try writeRefs(stdout, refs, .local);
1271 return 0;
1272 }
1273
1274 var state = try readCheckoutState(allocator, parsed.options.checkout);
1275 defer state.deinit();
1276 const start_name = if (parsed.positionals.len == 2) parsed.positionals[1] else state.branch;
1277 const target = (try history.ref(start_name)) orelse return error.RefNotFound;
1278 _ = try history.createBranch(parsed.positionals[0], target.target);
1279 try stdout.print("branch {s} {s}\n", .{ parsed.positionals[0], start_name });
1280 return 0;
1281 }
1282
1283 fn allocateDatabaseWorkspace(
1284 allocator: Allocator,
1285 options: CliOptions,
1286 ) !sql.FileDatabaseWorkspace {
1287 return try sql.FileDatabaseWorkspace.allocate(allocator, .{
1288 .header = cliWalHeader(),
1289 .max_wal_bytes = options.max_wal_bytes,
1290 .path_storage = sql.file.PathStorage.Limits.forDirect(.{
1291 .database = options.database,
1292 .wal = options.wal,
1293 }),
1294 });
1295 }
1296
1297 fn openDatabase(
1298 allocator: Allocator,
1299 workspace: *sql.FileDatabaseWorkspace,
1300 options: CliOptions,
1301 ) !sql.FileDatabase {
1302 var database = try sql.FileDatabase.open(allocator, workspace, std.Io.Dir.cwd(), .{
1303 .paths = .{ .database = options.database, .wal = options.wal },
1304 .header = cliWalHeader(),
1305 .max_wal_bytes = options.max_wal_bytes,
1306 });
1307 errdefer database.deinit();
1308 try database.reserve(.{ .wal_frames = 960 });
1309 return database;
1310 }
1311
1312 fn dumpWalLimit(path: []const u8) !usize {
1313 const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| switch (err) {
1314 error.FileNotFound => return sql.file.default_max_wal_bytes,
1315 else => return err,
1316 };
1317 const bytes = std.math.cast(usize, stat.size) orelse return error.StreamTooLong;
1318 const exclusive_read_limit = std.math.add(usize, bytes, 1) catch return error.StreamTooLong;
1319 return @max(sql.file.default_max_wal_bytes, exclusive_read_limit);
1320 }
1321
1322 fn openLiveConnection(allocator: Allocator, options: CliOptions) !LiveConnection {
1323 const workspace = try allocator.create(sql.FileDatabaseWorkspace);
1324 errdefer allocator.destroy(workspace);
1325 workspace.* = try allocateDatabaseWorkspace(allocator, options);
1326 errdefer workspace.deallocate(allocator);
1327 const database = try allocator.create(sql.FileDatabase);
1328 errdefer allocator.destroy(database);
1329 database.* = try openDatabase(allocator, workspace, options);
1330 errdefer database.deinit();
1331 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = options.history, .recovery = .truncate });
1332 errdefer history.deinit();
1333 var state = try readCheckoutState(allocator, options.checkout);
1334 errdefer state.deinit();
1335 const recovering = history.fastForwardRecovery() != null;
1336 var connection = try sql.Connection.open(allocator, database, &history, .{
1337 .branch = state.branch,
1338 });
1339 errdefer connection.deinit();
1340 if (recovering) {
1341 state.working_root = null;
1342 state.merge_head = null;
1343 try writeCheckoutState(allocator, options.checkout, &state);
1344 return .{
1345 .allocator = allocator,
1346 .workspace = workspace,
1347 .database = database,
1348 .history = history,
1349 .connection = connection,
1350 .state = state,
1351 };
1352 }
1353 const checkout = try history.checkoutBranch(state.branch);
1354 if (state.working_root) |working_root| {
1355 var root = try history.databaseRoot(allocator, working_root);
1356 errdefer root.deinit();
1357 try history.validateConflictRoot(root.conflicts);
1358 const live_checkout = checkout.withWorking(root.hash);
1359 connection.session.reinitWithRoot(live_checkout, &root);
1360 }
1361 return .{
1362 .allocator = allocator,
1363 .workspace = workspace,
1364 .database = database,
1365 .history = history,
1366 .connection = connection,
1367 .state = state,
1368 };
1369 }
1370
1371 fn materializeCheckout(allocator: Allocator, options: CliOptions, history: *sql.History, branch_name: []const u8) !void {
1372 var workspace = try allocateDatabaseWorkspace(allocator, options);
1373 defer workspace.deallocate(allocator);
1374 var database = try openDatabase(allocator, &workspace, options);
1375 defer database.deinit();
1376 var connection = try sql.Connection.open(allocator, &database, history, .{ .branch = branch_name });
1377 defer connection.deinit();
1378 try connection.checkoutBranch(allocator, history, branch_name);
1379 var state = try CheckoutState.init(allocator, branch_name, null);
1380 defer state.deinit();
1381 try writeCheckoutState(allocator, options.checkout, &state);
1382 }
1383
1384 fn persistWorkingState(allocator: Allocator, options: CliOptions, live: *LiveConnection) !void {
1385 var value = try live.connection.materializedWorkingValue(allocator);
1386 defer value.deinit();
1387 try live.history.putDatabaseValue(&value);
1388 live.state.working_root = (try live.connection.workingRoot());
1389 try writeCheckoutState(allocator, options.checkout, &live.state);
1390 }
1391
1392 fn readCheckoutState(allocator: Allocator, path: []const u8) !CheckoutState {
1393 return try readCheckoutStateFromDir(allocator, std.Io.Dir.cwd(), path);
1394 }
1395
1396 fn readCheckoutStateFromDir(allocator: Allocator, dir: std.Io.Dir, path: []const u8) !CheckoutState {
1397 const bytes = dir.readFileAlloc(io, path, allocator, .limited(1024)) catch |err| switch (err) {
1398 error.FileNotFound => return try CheckoutState.init(allocator, "main", null),
1399 else => return err,
1400 };
1401 defer allocator.free(bytes);
1402 var lines = std.mem.splitScalar(u8, std.mem.trim(u8, bytes, " \t\r\n"), '\n');
1403 const branch_line = lines.next() orelse return error.InvalidCheckout;
1404 if (branch_line.len == 0) return error.InvalidCheckout;
1405 var working_root: ?sql.Hash = null;
1406 var merge_head: ?sql.Hash = null;
1407 while (lines.next()) |line| {
1408 const trimmed = std.mem.trim(u8, line, " \t\r");
1409 if (std.mem.startsWith(u8, trimmed, "merge ")) {
1410 var decoded: sql.Hash = undefined;
1411 _ = try std.fmt.hexToBytes(decoded[0..], trimmed["merge ".len..]);
1412 merge_head = decoded;
1413 } else if (std.mem.startsWith(u8, trimmed, "working ")) {
1414 var decoded: sql.Hash = undefined;
1415 _ = try std.fmt.hexToBytes(decoded[0..], trimmed["working ".len..]);
1416 working_root = decoded;
1417 } else if (trimmed.len != 0) {
1418 return error.InvalidCheckout;
1419 }
1420 }
1421 return try CheckoutState.initWithWorking(allocator, branch_line, working_root, merge_head);
1422 }
1423
1424 fn writeCheckoutState(allocator: Allocator, path: []const u8, state: *const CheckoutState) !void {
1425 try writeCheckoutStateToDir(allocator, std.Io.Dir.cwd(), path, state);
1426 }
1427
1428 fn writeCheckoutStateToDir(allocator: Allocator, dir: std.Io.Dir, path: []const u8, state: *const CheckoutState) !void {
1429 var output: std.Io.Writer.Allocating = .init(allocator);
1430 defer output.deinit();
1431 try output.writer.print("{s}\n", .{state.branch});
1432 if (state.working_root) |working_root| try output.writer.print("working {s}\n", .{std.fmt.bytesToHex(working_root, .lower)});
1433 if (state.merge_head) |merge_head| try output.writer.print("merge {s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
1434 try dir.writeFile(io, .{ .sub_path = path, .data = output.writer.buffered(), .flags = .{ .truncate = true } });
1435 }
1436
1437 fn writeSqlResult(stdout: *std.Io.Writer, result: *sql.StatementResult) !void {
1438 switch (result.*) {
1439 .rows => {
1440 var count: usize = 0;
1441 while (result.nextRow()) |bytes| {
1442 try writeRow(stdout, bytes);
1443 count += 1;
1444 }
1445 if (count == 0) try stdout.writeAll("rows=0\n");
1446 },
1447 .mutation => try stdout.writeAll("ok\n"),
1448 .catalog => try stdout.writeAll("ok\n"),
1449 .staged => |count| try stdout.print("staged={d}\n", .{count}),
1450 }
1451 }
1452
1453 fn versionControlSqlTable(source: []const u8) ?VersionControlSqlTable {
1454 const table_name = selectAllTableName(source) orelse return null;
1455 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_refs")) return .refs;
1456 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_branches")) return .branches;
1457 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_remote_branches")) return .remote_branches;
1458 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_tags")) return .tags;
1459 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_status")) return .status;
1460 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_commits")) return .commits;
1461 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_log")) return .log;
1462 if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_commit_ancestors")) return .commit_ancestors;
1463 return null;
1464 }
1465
1466 fn versionControlDynamicSqlTable(source: []const u8) ?VersionControlDynamicSqlTable {
1467 const table_name = selectAllTableName(source) orelse return null;
1468 return dynamicSqlTable(table_name);
1469 }
1470
1471 fn selectAllTableName(source: []const u8) ?[]const u8 {
1472 const trimmed = std.mem.trim(u8, source, " \t\r\n;");
1473 var tokens = std.mem.tokenizeAny(u8, trimmed, " \t\r\n");
1474 const select_keyword = tokens.next() orelse return null;
1475 if (!std.ascii.eqlIgnoreCase(select_keyword, "select")) return null;
1476 const projection = tokens.next() orelse return null;
1477 if (!std.mem.eql(u8, projection, "*")) return null;
1478 const from_keyword = tokens.next() orelse return null;
1479 if (!std.ascii.eqlIgnoreCase(from_keyword, "from")) return null;
1480 const table_name = tokens.next() orelse return null;
1481 if (tokens.next() != null) return null;
1482 return table_name;
1483 }
1484
1485 fn dynamicSqlTable(table_name: []const u8) ?VersionControlDynamicSqlTable {
1486 if (dynamicSqlTableRelation(table_name, "tiny_sql_history_")) |relation| return .{ .kind = .history, .relation = relation };
1487 if (dynamicSqlTableRelation(table_name, "tiny_sql_diff_")) |relation| return .{ .kind = .diff, .relation = relation };
1488 if (dynamicSqlTableRelation(table_name, "tiny_sql_working_diff_")) |relation| return .{ .kind = .working_diff, .relation = relation };
1489 return null;
1490 }
1491
1492 fn dynamicSqlTableRelation(table_name: []const u8, prefix: []const u8) ?[]const u8 {
1493 if (table_name.len <= prefix.len) return null;
1494 if (!std.ascii.eqlIgnoreCase(table_name[0..prefix.len], prefix)) return null;
1495 return table_name[prefix.len..];
1496 }
1497
1498 fn versionControlSqlCall(source: []const u8) !?VersionControlSqlCall {
1499 var parser = SqlCallParser.init(source);
1500 if (!parser.consumeKeyword("call")) return null;
1501 const procedure_name = try parser.identifier();
1502 const procedure = versionControlSqlProcedureName(procedure_name) orelse return null;
1503 try parser.expectByte('(');
1504 var call = VersionControlSqlCall{ .procedure = procedure };
1505 if (parser.consumeByte(')')) {
1506 try parser.finish();
1507 return call;
1508 }
1509 while (true) {
1510 if (call.arg_count == call.args.len) return error.InvalidArguments;
1511 call.args[call.arg_count] = try parser.stringLiteral();
1512 call.arg_count += 1;
1513 if (parser.consumeByte(')')) break;
1514 try parser.expectByte(',');
1515 }
1516 try parser.finish();
1517 return call;
1518 }
1519
1520 fn versionControlSqlProcedureName(name: []const u8) ?VersionControlSqlProcedure {
1521 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_commit")) return .commit;
1522 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_checkout")) return .checkout;
1523 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_merge")) return .merge;
1524 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_branch")) return .branch;
1525 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_tag")) return .tag;
1526 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_tag_delete")) return .tag_delete;
1527 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_fetch")) return .fetch;
1528 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_pull")) return .pull;
1529 if (std.ascii.eqlIgnoreCase(name, "tiny_sql_push")) return .push;
1530 return null;
1531 }
1532
1533 fn executeVersionControlSqlCall(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, call: *const VersionControlSqlCall) !void {
1534 const args = call.arguments();
1535 switch (call.procedure) {
1536 .commit => {
1537 if (args.len != 0) return error.InvalidArguments;
1538 try commitLive(allocator, stdout, live, options.checkout);
1539 },
1540 .checkout => {
1541 if (args.len != 1) return error.InvalidArguments;
1542 try checkoutLive(allocator, stdout, live, options.checkout, args[0]);
1543 },
1544 .merge => {
1545 if (args.len != 1) return error.InvalidArguments;
1546 try mergeLive(allocator, stdout, live, options, args[0]);
1547 },
1548 .branch => {
1549 if (args.len == 0 or args.len > 2) return error.InvalidArguments;
1550 const start_name = if (args.len == 2) args[1] else live.state.branch;
1551 try branchLive(stdout, &live.history, args[0], start_name);
1552 },
1553 .tag => {
1554 if (args.len == 0 or args.len > 2) return error.InvalidArguments;
1555 try tagLive(allocator, stdout, &live.history, &live.state, args[0], if (args.len == 2) args[1] else null);
1556 },
1557 .tag_delete => {
1558 if (args.len != 1) return error.InvalidArguments;
1559 try tagDeleteLive(allocator, stdout, &live.history, args[0]);
1560 },
1561 .fetch => {
1562 const fetch_args = try fetchCallArgs(args);
1563 try fetchLive(allocator, stdout, &live.history, options, fetch_args.remote, fetch_args.prune);
1564 },
1565 .pull => {
1566 const pull_args = try pullCallArgs(args);
1567 try pullLive(allocator, stdout, live, options, pull_args.positionals(), pull_args.prune);
1568 },
1569 .push => {
1570 if (args.len > 2) return error.InvalidArguments;
1571 try pushLive(allocator, stdout, live, options, args);
1572 },
1573 }
1574 }
1575
1576 fn fetchCallArgs(args: []const []const u8) !FetchCallArgs {
1577 var result = FetchCallArgs{};
1578 var remote_set = false;
1579 for (args) |arg| {
1580 if (isPruneArgument(arg)) {
1581 result.prune = true;
1582 } else {
1583 if (remote_set) return error.InvalidArguments;
1584 result.remote = arg;
1585 remote_set = true;
1586 }
1587 }
1588 return result;
1589 }
1590
1591 fn pullCallArgs(args: []const []const u8) !PullCallArgs {
1592 var result = PullCallArgs{};
1593 for (args) |arg| {
1594 if (isPruneArgument(arg)) {
1595 result.prune = true;
1596 continue;
1597 }
1598 if (result.arg_count == result.args.len) return error.InvalidArguments;
1599 result.args[result.arg_count] = arg;
1600 result.arg_count += 1;
1601 }
1602 return result;
1603 }
1604
1605 fn isPruneArgument(arg: []const u8) bool {
1606 return std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--prune");
1607 }
1608
1609 const SqlCallParser = struct {
1610 source: []const u8,
1611 offset: usize = 0,
1612
1613 fn init(source: []const u8) SqlCallParser {
1614 return .{ .source = std.mem.trim(u8, source, " \t\r\n;") };
1615 }
1616
1617 fn consumeKeyword(self: *SqlCallParser, keyword: []const u8) bool {
1618 self.skipSpace();
1619 if (keyword.len > self.source.len - self.offset) return false;
1620 const candidate = self.source[self.offset..][0..keyword.len];
1621 if (!std.ascii.eqlIgnoreCase(candidate, keyword)) return false;
1622 if (self.offset + keyword.len < self.source.len and isIdentifierByte(self.source[self.offset + keyword.len])) return false;
1623 self.offset += keyword.len;
1624 return true;
1625 }
1626
1627 fn identifier(self: *SqlCallParser) ![]const u8 {
1628 self.skipSpace();
1629 const start = self.offset;
1630 while (self.offset < self.source.len and isIdentifierByte(self.source[self.offset])) self.offset += 1;
1631 if (start == self.offset) return error.InvalidArguments;
1632 return self.source[start..self.offset];
1633 }
1634
1635 fn stringLiteral(self: *SqlCallParser) ![]const u8 {
1636 self.skipSpace();
1637 if (self.offset >= self.source.len or self.source[self.offset] != '\'') return error.InvalidArguments;
1638 self.offset += 1;
1639 const start = self.offset;
1640 while (self.offset < self.source.len) : (self.offset += 1) {
1641 if (self.source[self.offset] == '\'') {
1642 const value = self.source[start..self.offset];
1643 self.offset += 1;
1644 return value;
1645 }
1646 }
1647 return error.InvalidArguments;
1648 }
1649
1650 fn consumeByte(self: *SqlCallParser, byte: u8) bool {
1651 self.skipSpace();
1652 if (self.offset >= self.source.len or self.source[self.offset] != byte) return false;
1653 self.offset += 1;
1654 return true;
1655 }
1656
1657 fn expectByte(self: *SqlCallParser, byte: u8) !void {
1658 if (!self.consumeByte(byte)) return error.InvalidArguments;
1659 }
1660
1661 fn finish(self: *SqlCallParser) !void {
1662 self.skipSpace();
1663 if (self.offset != self.source.len) return error.InvalidArguments;
1664 }
1665
1666 fn skipSpace(self: *SqlCallParser) void {
1667 while (self.offset < self.source.len and std.ascii.isWhitespace(self.source[self.offset])) self.offset += 1;
1668 }
1669 };
1670
1671 fn isIdentifierByte(byte: u8) bool {
1672 return std.ascii.isAlphanumeric(byte) or byte == '_';
1673 }
1674
1675 fn writeVersionControlSqlTable(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, table: VersionControlSqlTable) !void {
1676 switch (table) {
1677 .refs => try writeSqlRefs(allocator, stdout, &live.history),
1678 .branches => try writeSqlBranches(allocator, stdout, &live.history, live.state.branch),
1679 .remote_branches => try writeSqlRemoteBranches(allocator, stdout, &live.history),
1680 .tags => try writeSqlTags(allocator, stdout, &live.history),
1681 .status => try writeSqlStatus(allocator, stdout, live, options),
1682 .commits => try writeSqlCommits(allocator, stdout, &live.history),
1683 .log => try writeSqlLog(allocator, stdout, &live.history, (try live.connection.checkout()).head),
1684 .commit_ancestors => try writeSqlCommitAncestors(allocator, stdout, &live.history),
1685 }
1686 }
1687
1688 fn writeVersionControlDynamicSqlTable(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, table: VersionControlDynamicSqlTable) !void {
1689 switch (table.kind) {
1690 .history => try writeSqlRelationHistory(allocator, stdout, live, table.relation),
1691 .diff => try writeSqlRelationDiff(allocator, stdout, live, table.relation),
1692 .working_diff => try writeSqlRelationWorkingDiff(allocator, stdout, live, table.relation),
1693 }
1694 }
1695
1696 fn writeSqlRefs(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1697 const refs = try history.refList(allocator);
1698 defer sql.history.freeRefList(allocator, refs);
1699 if (refs.len == 0) {
1700 try stdout.writeAll("rows=0\n");
1701 return;
1702 }
1703 for (refs) |ref_value| {
1704 try stdout.print("{s}\t{s}\t{s}\n", .{ refKind(ref_value.name), ref_value.name, std.fmt.bytesToHex(ref_value.target, .lower) });
1705 }
1706 }
1707
1708 fn writeSqlBranches(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, current_branch: []const u8) !void {
1709 const refs = try history.refList(allocator);
1710 defer sql.history.freeRefList(allocator, refs);
1711 var count: usize = 0;
1712 for (refs) |ref_value| {
1713 if (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name)) continue;
1714 count += 1;
1715 try stdout.print("{s}\t{s}\t{s}\n", .{ ref_value.name, std.fmt.bytesToHex(ref_value.target, .lower), if (std.mem.eql(u8, ref_value.name, current_branch)) "true" else "false" });
1716 }
1717 if (count == 0) try stdout.writeAll("rows=0\n");
1718 }
1719
1720 fn writeSqlRemoteBranches(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1721 const refs = try history.refList(allocator);
1722 defer sql.history.freeRefList(allocator, refs);
1723 var count: usize = 0;
1724 for (refs) |ref_value| {
1725 if (!isRemoteTrackingRef(ref_value.name)) continue;
1726 const remote_branch = remoteBranchNameParts(ref_value.name) orelse continue;
1727 count += 1;
1728 try stdout.print("{s}\t{s}\t{s}\n", .{ remote_branch.remote, remote_branch.branch, std.fmt.bytesToHex(ref_value.target, .lower) });
1729 }
1730 if (count == 0) try stdout.writeAll("rows=0\n");
1731 }
1732
1733 fn writeSqlTags(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1734 const refs = try history.refList(allocator);
1735 defer sql.history.freeRefList(allocator, refs);
1736 var count: usize = 0;
1737 for (refs) |ref_value| {
1738 if (!isTagRef(ref_value.name)) continue;
1739 count += 1;
1740 try stdout.print("{s}\t{s}\n", .{ ref_value.name["refs/tags/".len..], std.fmt.bytesToHex(ref_value.target, .lower) });
1741 }
1742 if (count == 0) try stdout.writeAll("rows=0\n");
1743 }
1744
1745 fn writeSqlStatus(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions) !void {
1746 const checkout = (try live.connection.checkout());
1747 try stdout.print("branch\t{s}\n", .{live.state.branch});
1748 try stdout.print("head\t{s}\n", .{std.fmt.bytesToHex(checkout.head, .lower)});
1749
1750 var upstreams = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.upstreams });
1751 defer upstreams.deinit();
1752 if (upstreams.find(live.state.branch)) |upstream| {
1753 try stdout.print("upstream\t{s}/{s}\n", .{ upstream.remote, upstream.remote_branch });
1754 const remote_ref_name = try remoteTrackingRefName(allocator, upstream.remote, upstream.remote_branch);
1755 defer allocator.free(remote_ref_name);
1756 if ((try live.history.ref(remote_ref_name))) |remote_ref| {
1757 const counts = try aheadBehind(allocator, &live.history, checkout.head, remote_ref.target);
1758 try stdout.print("ahead\t{d}\n", .{counts.ahead});
1759 try stdout.print("behind\t{d}\n", .{counts.behind});
1760 }
1761 } else {
1762 try stdout.writeAll("upstream\tnone\n");
1763 }
1764 if (live.state.merge_head) |merge_head| try stdout.print("merge\t{s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
1765 var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
1766 defer artifacts.deinit();
1767 try stdout.print("conflicts\t{d}\n", .{artifacts.artifacts.len});
1768 try stdout.print("working\t{s}\n", .{if (checkout.working.dirty()) "dirty" else "clean"});
1769 try stdout.print("staged\t{s}\n", .{if (checkout.working.hasStaged()) "dirty" else "clean"});
1770 }
1771
1772 fn writeSqlCommits(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1773 const entries = try history.commitEntries(allocator);
1774 defer allocator.free(entries);
1775 if (entries.len == 0) {
1776 try stdout.writeAll("rows=0\n");
1777 return;
1778 }
1779 for (entries) |entry| {
1780 const commit = try history.commitValue(entry.hash);
1781 try stdout.print("{s}\t{s}\t{d}\n", .{ std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(commit.root, .lower), commit.parents.len });
1782 }
1783 }
1784
1785 fn writeSqlCommitAncestors(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1786 const entries = try history.commitEntries(allocator);
1787 defer allocator.free(entries);
1788 if (entries.len == 0) {
1789 try stdout.writeAll("rows=0\n");
1790 return;
1791 }
1792 for (entries) |entry| {
1793 if (entry.parents.len == 0) {
1794 try stdout.print("{s}\tNULL\t0\n", .{std.fmt.bytesToHex(entry.hash, .lower)});
1795 continue;
1796 }
1797 for (entry.parents, 0..) |parent, index| {
1798 try stdout.print("{s}\t{s}\t{d}\n", .{ std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(parent, .lower), index });
1799 }
1800 }
1801 }
1802
1803 const SqlLogStackEntry = struct {
1804 hash: sql.Hash,
1805 order: usize,
1806 };
1807
1808 fn writeSqlLog(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, start: sql.Hash) !void {
1809 var seen: std.ArrayList(sql.Hash) = .empty;
1810 defer seen.deinit(allocator);
1811 var stack: std.ArrayList(SqlLogStackEntry) = .empty;
1812 defer stack.deinit(allocator);
1813 try stack.append(allocator, .{ .hash = start, .order = 0 });
1814
1815 var count: usize = 0;
1816 while (stack.pop()) |entry| {
1817 if (containsHash(seen.items, entry.hash)) continue;
1818 try seen.append(allocator, entry.hash);
1819 const commit = try history.commitValue(entry.hash);
1820 try stdout.print("{d}\t{s}\t{s}\t{d}\n", .{ entry.order, std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(commit.root, .lower), commit.parents.len });
1821 count += 1;
1822 for (commit.parents) |parent| {
1823 try stack.append(allocator, .{ .hash = parent, .order = entry.order + 1 });
1824 }
1825 }
1826 if (count == 0) try stdout.writeAll("rows=0\n");
1827 }
1828
1829 fn writeSqlRelationHistory(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1830 const commits = try reachableCommits(allocator, &live.history, (try live.connection.checkout()).head);
1831 defer allocator.free(commits);
1832 var count: usize = 0;
1833 for (commits) |commit_hash| {
1834 var view = relationViewAtCommit(allocator, &live.history, commit_hash, relation_name) catch |err| switch (err) {
1835 error.RelationNotFound => continue,
1836 else => return err,
1837 };
1838 defer view.deinit();
1839 for (view.rows) |row_value| {
1840 try stdout.print("{s}\t{d}\t", .{ std.fmt.bytesToHex(commit_hash, .lower), row_value.rowid });
1841 try writeRow(stdout, row_value.bytes);
1842 count += 1;
1843 }
1844 }
1845 if (count == 0) try stdout.writeAll("rows=0\n");
1846 }
1847
1848 fn writeSqlRelationDiff(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1849 const commits = try reachableCommits(allocator, &live.history, (try live.connection.checkout()).head);
1850 defer allocator.free(commits);
1851 var count: usize = 0;
1852 for (commits) |commit_hash| {
1853 const commit = try live.history.commitValue(commit_hash);
1854 if (commit.parents.len == 0) {
1855 count += try writeSqlRelationCommitDiff(allocator, stdout, &live.history, relation_name, commit_hash, null);
1856 continue;
1857 }
1858 for (commit.parents) |parent_hash| {
1859 count += try writeSqlRelationCommitDiff(allocator, stdout, &live.history, relation_name, commit_hash, parent_hash);
1860 }
1861 }
1862 if (count == 0) try stdout.writeAll("rows=0\n");
1863 }
1864
1865 fn writeSqlRelationCommitDiff(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, relation_name: []const u8, commit_hash: sql.Hash, parent_hash: ?sql.Hash) !usize {
1866 var to_view = try optionalRelationViewAtCommit(allocator, history, commit_hash, relation_name);
1867 defer if (to_view) |*view| view.deinit();
1868 var from_view = if (parent_hash) |hash| try optionalRelationViewAtCommit(allocator, history, hash, relation_name) else null;
1869 defer if (from_view) |*view| view.deinit();
1870
1871 if (from_view == null and to_view == null) return 0;
1872 if (from_view == null) {
1873 var count: usize = 1;
1874 try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1875 for (to_view.?.rows) |row_value| {
1876 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "added", relation_name, row_value.rowid, null, row_value.bytes);
1877 count += 1;
1878 }
1879 return count;
1880 }
1881 if (to_view == null) {
1882 var count: usize = 1;
1883 try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1884 for (from_view.?.rows) |row_value| {
1885 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "removed", relation_name, row_value.rowid, null, row_value.bytes);
1886 count += 1;
1887 }
1888 return count;
1889 }
1890
1891 var result = try sql.diff.relation(allocator, from_view.?.snapshot(), to_view.?.snapshot());
1892 defer result.deinit();
1893 return try writeSqlRelationDiffResult(stdout, commit_hash, parent_hash, relation_name, &result);
1894 }
1895
1896 fn optionalRelationViewAtCommit(allocator: Allocator, history: *const sql.History, commit_hash: sql.Hash, relation_name: []const u8) !?sql.ConnectionRelationView {
1897 return relationViewAtCommit(allocator, history, commit_hash, relation_name) catch |err| switch (err) {
1898 error.RelationNotFound => null,
1899 else => return err,
1900 };
1901 }
1902
1903 fn writeSqlRelationDiffResult(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8, result: *const sql.RelationDiff) !usize {
1904 var count: usize = 0;
1905 if (result.schema_changed) {
1906 try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1907 count += 1;
1908 }
1909 for (result.changes) |change| {
1910 switch (change.kind) {
1911 .added => {
1912 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "added", relation_name, change.rowid, null, change.to.?);
1913 count += 1;
1914 },
1915 .removed => {
1916 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "removed", relation_name, change.rowid, null, change.from.?);
1917 count += 1;
1918 },
1919 .modified => {
1920 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "modified", relation_name, change.rowid, "from", change.from.?);
1921 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "modified", relation_name, change.rowid, "to", change.to.?);
1922 count += 2;
1923 },
1924 .schema => {
1925 try writeSqlRelationDiffSchemaRow(stdout, commit_hash, parent_hash, relation_name, change.rowid);
1926 count += 1;
1927 },
1928 }
1929 }
1930 return count;
1931 }
1932
1933 fn writeSqlRelationDiffSchema(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8) !void {
1934 try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1935 try stdout.print("schema\t{s}\n", .{relation_name});
1936 }
1937
1938 fn writeSqlRelationDiffSchemaRow(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8, rowid: i64) !void {
1939 try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1940 try stdout.print("schema_row\t{s}\t{d}\n", .{ relation_name, rowid });
1941 }
1942
1943 fn writeSqlRelationDiffRow(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, kind: []const u8, relation_name: []const u8, rowid: i64, side: ?[]const u8, bytes: []const u8) !void {
1944 try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1945 if (side) |side_name| {
1946 try stdout.print("{s}\t{s}\t{d}\t{s}\t", .{ kind, relation_name, rowid, side_name });
1947 } else {
1948 try stdout.print("{s}\t{s}\t{d}\t", .{ kind, relation_name, rowid });
1949 }
1950 try writeRow(stdout, bytes);
1951 }
1952
1953 fn writeSqlRelationDiffPrefix(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash) !void {
1954 try stdout.print("{s}\t", .{std.fmt.bytesToHex(commit_hash, .lower)});
1955 if (parent_hash) |hash| {
1956 try stdout.print("{s}\t", .{std.fmt.bytesToHex(hash, .lower)});
1957 } else {
1958 try stdout.writeAll("NULL\t");
1959 }
1960 }
1961
1962 fn writeSqlRelationWorkingDiff(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1963 const base_hash = (try live.connection.checkout()).head;
1964 var base_view = try relationViewAtCommit(allocator, &live.history, base_hash, relation_name);
1965 defer base_view.deinit();
1966 var working_view = try live.connection.relationView(allocator, relation_name);
1967 defer working_view.deinit();
1968 var result = try sql.diff.relation(allocator, base_view.snapshot(), working_view.snapshot());
1969 defer result.deinit();
1970 try writeRelationDiff(stdout, relation_name, &result);
1971 }
1972
1973 fn writeRow(stdout: *std.Io.Writer, bytes: []const u8) !void {
1974 try writeRowValues(stdout, bytes);
1975 try stdout.writeAll("\n");
1976 }
1977
1978 fn writeRowValues(stdout: *std.Io.Writer, bytes: []const u8) !void {
1979 const view = try sql.row.View.init(bytes);
1980 var index: usize = 0;
1981 while (index < view.columnCount()) : (index += 1) {
1982 if (index != 0) try stdout.writeAll("\t");
1983 try writeValue(stdout, try view.column(index));
1984 }
1985 }
1986
1987 fn writeValue(stdout: *std.Io.Writer, value: sql.row.Value) !void {
1988 switch (value) {
1989 .nil => try stdout.writeAll("NULL"),
1990 .integer => |integer| try stdout.print("{d}", .{integer}),
1991 .text => |text| try stdout.writeAll(text),
1992 .blob => |blob| {
1993 try stdout.writeAll("0x");
1994 try stdout.printHex(blob, .lower);
1995 },
1996 }
1997 }
1998
1999 fn resolveSide(text: []const u8) ResolveSide {
2000 if (std.mem.eql(u8, text, "base")) return .base;
2001 if (std.mem.eql(u8, text, "ours")) return .ours;
2002 if (std.mem.eql(u8, text, "theirs")) return .theirs;
2003 if (std.mem.eql(u8, text, "current")) return .current;
2004 return .current;
2005 }
2006
2007 fn findConflictArtifact(artifacts: []const sql.ConflictArtifact, hash: sql.Hash) ?sql.ConflictArtifact {
2008 for (artifacts) |artifact| {
2009 if (sql.version.same(artifact.hash, hash)) return artifact;
2010 }
2011 return null;
2012 }
2013
2014 fn applyConflictSide(allocator: Allocator, live: *LiveConnection, artifact: sql.ConflictArtifact, side: ResolveSide) !void {
2015 if (artifact.kind != .row) return error.UnsupportedConflictResolution;
2016 const value = switch (side) {
2017 .base => artifact.base,
2018 .ours => artifact.ours,
2019 .theirs => artifact.theirs,
2020 .current => unreachable,
2021 };
2022 var relation = try live.connection.catalog.openRelation(allocator, artifact.relation);
2023 defer relation.deinit();
2024 const row_bytes = try conflictRowBytes(value);
2025 if (row_bytes) |bytes| {
2026 _ = try relation.relation.putEncoded(allocator, artifact.rowid, bytes, .{ .durability = .synced });
2027 } else {
2028 _ = relation.relation.delete(allocator, artifact.rowid, .{ .durability = .synced }) catch |err| switch (err) {
2029 error.KeyNotFound => return,
2030 else => return err,
2031 };
2032 }
2033 }
2034
2035 fn refreshWorkingRootWithConflictRoot(allocator: Allocator, live: *LiveConnection) !void {
2036 const conflicts = live.connection.session.workingRoot().conflicts;
2037 var root = try sql.version.databaseRootMaintained(
2038 allocator,
2039 &live.connection.catalog,
2040 conflicts,
2041 );
2042 live.connection.session.applyRoot(&root);
2043 }
2044
2045 fn conflictRowBytes(value: ?sql.ConflictValue) !?[]const u8 {
2046 const conflict_value = value orelse return null;
2047 return switch (conflict_value) {
2048 .row => |bytes| bytes,
2049 .relation => error.UnsupportedConflictResolution,
2050 };
2051 }
2052
2053 fn writeConflictArtifact(stdout: *std.Io.Writer, artifact: sql.ConflictArtifact) !void {
2054 const hash = std.fmt.bytesToHex(artifact.hash, .lower);
2055 switch (artifact.kind) {
2056 .row => try stdout.print("{s}\trow\t{s}\t{d}\n", .{ hash[0..], artifact.relation, artifact.rowid }),
2057 .relation => try stdout.print("{s}\trelation\t{s}\n", .{ hash[0..], artifact.relation }),
2058 }
2059 }
2060
2061 fn writeConflictArtifactDetail(stdout: *std.Io.Writer, artifact: sql.ConflictArtifact) !void {
2062 try stdout.writeAll("conflict\t");
2063 try writeConflictArtifact(stdout, artifact);
2064 try writeConflictValue(stdout, "base", artifact.base);
2065 try writeConflictValue(stdout, "ours", artifact.ours);
2066 try writeConflictValue(stdout, "theirs", artifact.theirs);
2067 }
2068
2069 fn writeConflictValue(stdout: *std.Io.Writer, label: []const u8, value: ?sql.ConflictValue) !void {
2070 try stdout.writeAll(label);
2071 try stdout.writeAll("\t");
2072 const conflict_value = value orelse {
2073 try stdout.writeAll("missing\n");
2074 return;
2075 };
2076 switch (conflict_value) {
2077 .row => |bytes| {
2078 try stdout.writeAll("row\t");
2079 try writeRow(stdout, bytes);
2080 },
2081 .relation => |hash| try stdout.print("relation\t{s}\n", .{std.fmt.bytesToHex(hash, .lower)}),
2082 }
2083 }
2084
2085 fn conflictCount(result: *const sql.DatabaseMerge) usize {
2086 return result.conflict_root.count;
2087 }
2088
2089 fn cliWalHeader() sql.wal.Header {
2090 return .{
2091 .sequence = 0x7473_716c,
2092 .salt = .{ .first = 0x7469_6e79, .second = 0x7371_6c21 },
2093 };
2094 }
2095
2096 fn cloneBranchName(allocator: Allocator, history: *const sql.History, requested: ?[]const u8) ![]u8 {
2097 if (requested) |branch_name| {
2098 if ((try history.ref(branch_name)) == null) return error.RefNotFound;
2099 return try allocator.dupe(u8, branch_name);
2100 }
2101 if ((try history.ref("main")) != null) return try allocator.dupe(u8, "main");
2102 if ((try history.ref("master")) != null) return try allocator.dupe(u8, "master");
2103 const refs = try history.refList(allocator);
2104 defer sql.history.freeRefList(allocator, refs);
2105 for (refs) |ref_value| {
2106 if (!std.mem.startsWith(u8, ref_value.name, "refs/remotes/")) return try allocator.dupe(u8, ref_value.name);
2107 }
2108 return error.RefNotFound;
2109 }
2110
2111 fn addRemote(allocator: Allocator, config_path: []const u8, name: []const u8, history_path: []const u8, lock_path: ?[]const u8, default_branch: ?[]const u8) !void {
2112 var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path });
2113 defer config.deinit();
2114 var remote = try sql.HistoryRemoteConfigEntry.initWithDefaultBranch(allocator, name, history_path, lock_path, default_branch);
2115 defer remote.deinit(allocator);
2116
2117 const entries = try allocator.alloc(sql.HistoryRemoteConfigEntry, config.remotes.len + 1);
2118 defer allocator.free(entries);
2119 for (config.remotes, entries[0..config.remotes.len]) |existing, *target| target.* = existing;
2120 entries[config.remotes.len] = remote;
2121
2122 var updated = try sql.HistoryRemoteConfig.init(allocator, entries);
2123 defer updated.deinit();
2124 try sql.writeHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path }, &updated);
2125 }
2126
2127 fn writeRemoteTrackingRefsForClone(allocator: Allocator, history: *sql.History, remote_name: []const u8) !void {
2128 const refs = try history.refList(allocator);
2129 defer sql.history.freeRefList(allocator, refs);
2130 for (refs) |ref_value| {
2131 if (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name)) continue;
2132 const remote_ref_name = try remoteTrackingRefName(allocator, remote_name, ref_value.name);
2133 defer allocator.free(remote_ref_name);
2134 try history.putRef(.{
2135 .name = remote_ref_name,
2136 .target = ref_value.target,
2137 });
2138 }
2139 }
2140
2141 fn setBranchUpstream(allocator: Allocator, config_path: []const u8, branch_name: []const u8, remote_name: []const u8, remote_branch_name: []const u8) !void {
2142 var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path });
2143 defer config.deinit();
2144 var replacement = try sql.HistoryBranchUpstreamConfigEntry.init(allocator, branch_name, remote_name, remote_branch_name);
2145 defer replacement.deinit(allocator);
2146
2147 const has_existing = config.find(branch_name) != null;
2148 const entries_len = config.upstreams.len + @intFromBool(!has_existing);
2149 const entries = try allocator.alloc(sql.HistoryBranchUpstreamConfigEntry, entries_len);
2150 defer allocator.free(entries);
2151
2152 var index: usize = 0;
2153 var replaced = false;
2154 for (config.upstreams) |existing| {
2155 if (std.mem.eql(u8, existing.branch, branch_name)) {
2156 entries[index] = replacement;
2157 replaced = true;
2158 } else {
2159 entries[index] = existing;
2160 }
2161 index += 1;
2162 }
2163 if (!replaced) {
2164 entries[index] = replacement;
2165 index += 1;
2166 }
2167
2168 var updated = try sql.HistoryBranchUpstreamConfig.init(allocator, entries[0..index]);
2169 defer updated.deinit();
2170 try sql.writeHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path }, &updated);
2171 }
2172
2173 fn resolveRemoteBranchTarget(allocator: Allocator, upstreams_path: []const u8, current_branch_name: []const u8, positionals: []const []const u8) !RemoteBranchTarget {
2174 if (positionals.len >= 1) {
2175 const remote_branch_name = if (positionals.len >= 2) positionals[1] else current_branch_name;
2176 return try RemoteBranchTarget.init(allocator, positionals[0], remote_branch_name);
2177 }
2178
2179 var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = upstreams_path });
2180 defer config.deinit();
2181 if (config.find(current_branch_name)) |upstream| return try RemoteBranchTarget.init(allocator, upstream.remote, upstream.remote_branch);
2182 return try RemoteBranchTarget.init(allocator, "origin", current_branch_name);
2183 }
2184
2185 fn remoteTrackingRefName(allocator: Allocator, remote: []const u8, branch_name: []const u8) Allocator.Error![]u8 {
2186 return try std.fmt.allocPrint(allocator, "refs/remotes/{s}/{s}", .{ remote, branch_name });
2187 }
2188
2189 fn defaultPushRefSpec(allocator: Allocator, local_branch_name: []const u8, remote_branch_name: []const u8) !PushRefSpec {
2190 const local_ref = try allocator.dupe(u8, local_branch_name);
2191 errdefer allocator.free(local_ref);
2192 const remote_ref = try allocator.dupe(u8, remote_branch_name);
2193 return .{
2194 .allocator = allocator,
2195 .local_ref = local_ref,
2196 .remote_ref = remote_ref,
2197 };
2198 }
2199
2200 fn resolvePushRefSpec(allocator: Allocator, history: *const sql.History, text: []const u8) !PushRefSpec {
2201 if (std.mem.indexOfScalar(u8, text, ':')) |colon| {
2202 const local_text = text[0..colon];
2203 const remote_text = text[colon + 1 ..];
2204 if (remote_text.len == 0) return error.InvalidArguments;
2205 if (local_text.len == 0) {
2206 return .{
2207 .allocator = allocator,
2208 .delete = true,
2209 .remote_ref = try remoteRefNameForDelete(allocator, remote_text),
2210 };
2211 }
2212 const local_ref = try resolveLocalRefName(allocator, history, local_text);
2213 errdefer allocator.free(local_ref);
2214 return .{
2215 .allocator = allocator,
2216 .local_ref = local_ref,
2217 .remote_ref = try remoteRefNameFromLocal(allocator, local_ref, remote_text),
2218 };
2219 }
2220
2221 const local_ref = try resolveLocalRefName(allocator, history, text);
2222 errdefer allocator.free(local_ref);
2223 return .{
2224 .allocator = allocator,
2225 .local_ref = local_ref,
2226 .remote_ref = try allocator.dupe(u8, local_ref),
2227 };
2228 }
2229
2230 fn remoteRefNameFromLocal(allocator: Allocator, local_ref: []const u8, remote_text: []const u8) ![]u8 {
2231 if (remote_text.len == 0) return error.InvalidArguments;
2232 if (std.mem.startsWith(u8, remote_text, "refs/")) return try allocator.dupe(u8, remote_text);
2233 if (isTagRef(local_ref)) return try tagRefName(allocator, remote_text);
2234 return try allocator.dupe(u8, remote_text);
2235 }
2236
2237 fn remoteRefNameForDelete(allocator: Allocator, remote_text: []const u8) ![]u8 {
2238 if (remote_text.len == 0) return error.InvalidArguments;
2239 return try allocator.dupe(u8, remote_text);
2240 }
2241
2242 fn resolveLocalRefName(allocator: Allocator, history: *const sql.History, text: []const u8) ![]u8 {
2243 if ((try history.ref(text)) != null) return try allocator.dupe(u8, text);
2244 const tag_ref_name = tagRefName(allocator, text) catch |err| switch (err) {
2245 error.InvalidArguments => return error.RefNotFound,
2246 else => return err,
2247 };
2248 errdefer allocator.free(tag_ref_name);
2249 if ((try history.ref(tag_ref_name)) == null) return error.RefNotFound;
2250 return tag_ref_name;
2251 }
2252
2253 fn updateRemoteTrackingRefAfterPush(allocator: Allocator, history: *sql.History, remote_name: []const u8, remote_ref: []const u8, local_ref: []const u8) !void {
2254 if (isTagRef(remote_ref) or std.mem.startsWith(u8, remote_ref, "refs/")) return;
2255 const local = (try history.ref(local_ref)) orelse return error.RefNotFound;
2256 const tracking_ref = try remoteTrackingRefName(allocator, remote_name, remote_ref);
2257 defer allocator.free(tracking_ref);
2258 try history.putRef(.{
2259 .name = tracking_ref,
2260 .target = local.target,
2261 });
2262 }
2263
2264 fn removeRemoteTrackingRefIfPresent(allocator: Allocator, history: *sql.History, remote_name: []const u8, remote_ref: []const u8) !void {
2265 if (isTagRef(remote_ref) or std.mem.startsWith(u8, remote_ref, "refs/")) return;
2266 const tracking_ref = try remoteTrackingRefName(allocator, remote_name, remote_ref);
2267 defer allocator.free(tracking_ref);
2268 if ((try history.ref(tracking_ref)) != null) try history.deleteRef(tracking_ref);
2269 }
2270
2271 fn tagRefName(allocator: Allocator, tag_name: []const u8) ![]u8 {
2272 if (tag_name.len == 0) return error.InvalidArguments;
2273 if (std.mem.indexOfAny(u8, tag_name, " \t\n\r") != null) return error.InvalidArguments;
2274 if (std.mem.startsWith(u8, tag_name, "refs/")) return error.InvalidArguments;
2275 return try std.fmt.allocPrint(allocator, "refs/tags/{s}", .{tag_name});
2276 }
2277
2278 fn parseHash(text: []const u8) !sql.Hash {
2279 const hex = if (std.mem.startsWith(u8, text, "0x")) text[2..] else text;
2280 var hash: sql.Hash = undefined;
2281 _ = try std.fmt.hexToBytes(hash[0..], hex);
2282 return hash;
2283 }
2284
2285 fn writeRefs(stdout: *std.Io.Writer, refs: []const sql.Ref, filter: enum { all, local }) !void {
2286 for (refs) |ref_value| {
2287 if (filter == .local and (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name))) continue;
2288 const hex = std.fmt.bytesToHex(ref_value.target, .lower);
2289 try stdout.print("{s}\t{s}\n", .{ hex[0..], ref_value.name });
2290 }
2291 }
2292
2293 fn writeTagRefs(stdout: *std.Io.Writer, refs: []const sql.Ref) !void {
2294 for (refs) |ref_value| {
2295 if (!isTagRef(ref_value.name)) continue;
2296 const hex = std.fmt.bytesToHex(ref_value.target, .lower);
2297 try stdout.print("{s}\t{s}\n", .{ hex[0..], ref_value.name["refs/tags/".len..] });
2298 }
2299 }
2300
2301 fn isRemoteTrackingRef(name: []const u8) bool {
2302 return std.mem.startsWith(u8, name, "refs/remotes/");
2303 }
2304
2305 fn isTagRef(name: []const u8) bool {
2306 return std.mem.startsWith(u8, name, "refs/tags/");
2307 }
2308
2309 fn refKind(name: []const u8) []const u8 {
2310 if (isRemoteTrackingRef(name)) return "remote_branch";
2311 if (isTagRef(name)) return "tag";
2312 return "branch";
2313 }
2314
2315 fn remoteBranchNameParts(name: []const u8) ?RemoteBranchNameParts {
2316 if (!isRemoteTrackingRef(name)) return null;
2317 const rest = name["refs/remotes/".len..];
2318 const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null;
2319 if (slash == 0 or slash + 1 >= rest.len) return null;
2320 return .{
2321 .remote = rest[0..slash],
2322 .branch = rest[slash + 1 ..],
2323 };
2324 }
2325
2326 fn writeStats(stdout: *std.Io.Writer, stats: sql.HistorySyncStats) !void {
2327 try stdout.print("refs={d} commits={d} records={d}\n", .{
2328 stats.refs,
2329 stats.commits,
2330 stats.records,
2331 });
2332 }
2333
2334 fn writeCommitLog(allocator: Allocator, history: *const sql.History, start: sql.Hash, stdout: *std.Io.Writer) !void {
2335 var seen: std.ArrayList(sql.Hash) = .empty;
2336 defer seen.deinit(allocator);
2337 var stack: std.ArrayList(sql.Hash) = .empty;
2338 defer stack.deinit(allocator);
2339 try stack.append(allocator, start);
2340
2341 while (stack.pop()) |hash| {
2342 if (containsHash(seen.items, hash)) continue;
2343 try seen.append(allocator, hash);
2344 const commit = try history.commitValue(hash);
2345 try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(hash, .lower)});
2346 try stdout.print("root {s}\n", .{std.fmt.bytesToHex(commit.root, .lower)});
2347 try stdout.print("parents={d}", .{commit.parents.len});
2348 for (commit.parents) |parent| {
2349 try stdout.print(" {s}", .{std.fmt.bytesToHex(parent, .lower)});
2350 try stack.append(allocator, parent);
2351 }
2352 try stdout.writeAll("\n");
2353 }
2354 }
2355
2356 fn relationViewAtCommit(allocator: Allocator, history: *const sql.History, commit_hash: sql.Hash, relation_name: []const u8) !sql.ConnectionRelationView {
2357 const commit = try history.commitValue(commit_hash);
2358 var value = try history.databaseValue(allocator, commit.root);
2359 defer value.deinit();
2360 const relation = value.findRelation(relation_name) orelse return error.RelationNotFound;
2361 var root = try relation.root.clone(allocator);
2362 errdefer root.deinit();
2363 const rows = try sql.version.cloneRelationRows(allocator, relation.rows);
2364 errdefer sql.version.freeRelationRows(allocator, rows);
2365 return .{
2366 .allocator = allocator,
2367 .root = root,
2368 .rows = rows,
2369 };
2370 }
2371
2372 fn resolveCommitish(allocator: Allocator, history: *const sql.History, text: []const u8) !sql.Hash {
2373 if ((try history.ref(text))) |ref_value| return ref_value.target;
2374 const tag_ref_name = tagRefName(allocator, text) catch |err| switch (err) {
2375 error.InvalidArguments => null,
2376 else => return err,
2377 };
2378 if (tag_ref_name) |ref_name| {
2379 defer allocator.free(ref_name);
2380 if ((try history.ref(ref_name))) |ref_value| return ref_value.target;
2381 }
2382 const hash = parseHash(text) catch |err| switch (err) {
2383 error.InvalidLength, error.InvalidCharacter => return error.RefNotFound,
2384 else => return err,
2385 };
2386 _ = try history.commitValue(hash);
2387 return hash;
2388 }
2389
2390 fn writeRelationDiff(stdout: *std.Io.Writer, relation_name: []const u8, result: *const sql.RelationDiff) !void {
2391 if (result.schema_changed) try stdout.print("schema\t{s}\n", .{relation_name});
2392 if (result.changes.len == 0 and !result.schema_changed) {
2393 try stdout.writeAll("changes=0\n");
2394 return;
2395 }
2396 for (result.changes) |change| {
2397 switch (change.kind) {
2398 .added => try writeDiffRow(stdout, "added", relation_name, change.rowid, null, change.to.?),
2399 .removed => try writeDiffRow(stdout, "removed", relation_name, change.rowid, null, change.from.?),
2400 .modified => {
2401 try writeDiffRow(stdout, "modified", relation_name, change.rowid, "from", change.from.?);
2402 try writeDiffRow(stdout, "modified", relation_name, change.rowid, "to", change.to.?);
2403 },
2404 .schema => try stdout.print("schema\t{s}\t{d}\n", .{ relation_name, change.rowid }),
2405 }
2406 }
2407 }
2408
2409 fn writeDiffRow(stdout: *std.Io.Writer, kind: []const u8, relation_name: []const u8, rowid: i64, side: ?[]const u8, bytes: []const u8) !void {
2410 if (side) |side_name| {
2411 try stdout.print("{s}\t{s}\t{d}\t{s}\t", .{ kind, relation_name, rowid, side_name });
2412 } else {
2413 try stdout.print("{s}\t{s}\t{d}\t", .{ kind, relation_name, rowid });
2414 }
2415 try writeRow(stdout, bytes);
2416 }
2417
2418 fn aheadBehind(allocator: Allocator, history: *const sql.History, local: sql.Hash, remote: sql.Hash) !AheadBehind {
2419 const local_commits = try reachableCommits(allocator, history, local);
2420 defer allocator.free(local_commits);
2421 const remote_commits = try reachableCommits(allocator, history, remote);
2422 defer allocator.free(remote_commits);
2423
2424 var counts = AheadBehind{};
2425 for (local_commits) |hash| {
2426 if (!containsHash(remote_commits, hash)) counts.ahead += 1;
2427 }
2428 for (remote_commits) |hash| {
2429 if (!containsHash(local_commits, hash)) counts.behind += 1;
2430 }
2431 return counts;
2432 }
2433
2434 fn reachableCommits(allocator: Allocator, history: *const sql.History, start: sql.Hash) ![]sql.Hash {
2435 var seen: std.ArrayList(sql.Hash) = .empty;
2436 errdefer seen.deinit(allocator);
2437 var stack: std.ArrayList(sql.Hash) = .empty;
2438 defer stack.deinit(allocator);
2439 try stack.append(allocator, start);
2440
2441 while (stack.pop()) |hash| {
2442 if (containsHash(seen.items, hash)) continue;
2443 try seen.append(allocator, hash);
2444 const commit = try history.commitValue(hash);
2445 for (commit.parents) |parent| try stack.append(allocator, parent);
2446 }
2447
2448 return try seen.toOwnedSlice(allocator);
2449 }
2450
2451 fn containsHash(hashes: []const sql.Hash, needle: sql.Hash) bool {
2452 for (hashes) |hash| {
2453 if (sql.version.same(hash, needle)) return true;
2454 }
2455 return false;
2456 }
2457
2458 fn parseArgs(allocator: Allocator, args: []const []const u8, allowed: AllowedOptions) !ParsedArgs {
2459 var options = CliOptions{};
2460 var positionals: std.ArrayList([]const u8) = .empty;
2461 errdefer positionals.deinit(allocator);
2462
2463 var index: usize = 0;
2464 while (index < args.len) {
2465 const arg = args[index];
2466 if (!std.mem.startsWith(u8, arg, "--")) {
2467 if (std.mem.eql(u8, arg, "-p") and allowed.prune) {
2468 options.prune = true;
2469 index += 1;
2470 continue;
2471 }
2472 try positionals.append(allocator, arg);
2473 index += 1;
2474 continue;
2475 }
2476
2477 if (std.mem.eql(u8, arg, "--db") and allowed.database) {
2478 index += 1;
2479 if (index >= args.len) return error.MissingOptionValue;
2480 options.database = args[index];
2481 } else if (std.mem.eql(u8, arg, "--wal") and allowed.wal) {
2482 index += 1;
2483 if (index >= args.len) return error.MissingOptionValue;
2484 options.wal = args[index];
2485 } else if (std.mem.eql(u8, arg, "--history") and allowed.history) {
2486 index += 1;
2487 if (index >= args.len) return error.MissingOptionValue;
2488 options.history = args[index];
2489 } else if (std.mem.eql(u8, arg, "--remotes") and allowed.remotes) {
2490 index += 1;
2491 if (index >= args.len) return error.MissingOptionValue;
2492 options.remotes = args[index];
2493 } else if (std.mem.eql(u8, arg, "--checkout") and allowed.checkout) {
2494 index += 1;
2495 if (index >= args.len) return error.MissingOptionValue;
2496 options.checkout = args[index];
2497 } else if (std.mem.eql(u8, arg, "--upstreams") and allowed.upstreams) {
2498 index += 1;
2499 if (index >= args.len) return error.MissingOptionValue;
2500 options.upstreams = args[index];
2501 } else if (std.mem.eql(u8, arg, "--name") and allowed.name) {
2502 index += 1;
2503 if (index >= args.len) return error.MissingOptionValue;
2504 options.name = args[index];
2505 } else if (std.mem.eql(u8, arg, "--branch") and allowed.branch) {
2506 index += 1;
2507 if (index >= args.len) return error.MissingOptionValue;
2508 options.branch = args[index];
2509 } else if (std.mem.eql(u8, arg, "--lock") and allowed.lock) {
2510 index += 1;
2511 if (index >= args.len) return error.MissingOptionValue;
2512 options.lock = args[index];
2513 } else if (std.mem.eql(u8, arg, "--prune") and allowed.prune) {
2514 options.prune = true;
2515 } else {
2516 return error.UnknownOption;
2517 }
2518 index += 1;
2519 }
2520
2521 return .{
2522 .options = options,
2523 .positionals = try positionals.toOwnedSlice(allocator),
2524 };
2525 }
2526
2527 fn isHelp(arg: []const u8) bool {
2528 return std.mem.eql(u8, arg, "help") or std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help");
2529 }
2530
2531 fn isTagDeleteFlag(arg: []const u8) bool {
2532 return std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--delete");
2533 }
2534
2535 fn writeUsage(writer: *std.Io.Writer) !void {
2536 try writer.writeAll(
2537 \\usage: tiny-sql <command> [args]
2538 \\
2539 \\commands:
2540 \\ init [branch] [--db path] [--wal path] [--history path] [--checkout path]
2541 \\ sql <statement> [--db path] [--wal path] [--history path] [--checkout path] [--upstreams path]
2542 \\ pack repair INPUT OUTPUT --relation NAME --drop-row ROWID [--max-pack-bytes N]
2543 \\ dump [--db path] [--wal path] [--history path] [--branch name]
2544 \\ restore <dump.jsonl> [--db path] [--wal path] [--history path] [--branch name]
2545 \\ commit [--db path] [--wal path] [--history path] [--checkout path]
2546 \\ status [--db path] [--wal path] [--history path] [--checkout path] [--upstreams path]
2547 \\ log [ref] [--history path] [--checkout path]
2548 \\ blame <relation> [ref] [--history path] [--checkout path]
2549 \\ diff <relation>|<ref> <relation>|<from-ref> <to-ref> <relation> [--db path] [--wal path] [--history path] [--checkout path]
2550 \\ checkout <branch> [--db path] [--wal path] [--history path] [--checkout path]
2551 \\ merge <branch> [--db path] [--wal path] [--history path] [--checkout path]
2552 \\ conflicts [show <conflict-hash>...|all] [--db path] [--wal path] [--history path] [--checkout path]
2553 \\ resolve [current|base|ours|theirs] <conflict-hash>...|all [--db path] [--wal path] [--history path] [--checkout path]
2554 \\ refs [--history path]
2555 \\ branch [--history path] [--checkout path]
2556 \\ branch <name> [start-ref] [--history path] [--checkout path]
2557 \\ tag [--history path]
2558 \\ tag <name> [ref] [--history path] [--checkout path]
2559 \\ tag -d <name>... [--history path]
2560 \\ remote list [--remotes path]
2561 \\ remote add <name> <history-path> [--branch name] [--lock path] [--remotes path]
2562 \\ upstream list [--upstreams path]
2563 \\ upstream set <branch> <remote> [remote-branch] [--upstreams path]
2564 \\ clone <remote-history-path> [branch] [--branch name] [--db path] [--wal path] [--history path] [--checkout path] [--remotes path] [--upstreams path] [--name name] [--lock path]
2565 \\ fetch [remote] [-p|--prune] [--history path] [--remotes path]
2566 \\ pull [remote] [branch] [-p|--prune] [--db path] [--wal path] [--history path] [--checkout path] [--remotes path] [--upstreams path]
2567 \\ push [remote] [refspec] [--history path] [--checkout path] [--remotes path] [--upstreams path]
2568 \\
2569 );
2570 }
2571
2572 fn errorText(err: anyerror) []const u8 {
2573 return switch (err) {
2574 error.InvalidArguments => "invalid arguments",
2575 error.UnknownCommand => "unknown command",
2576 error.UnknownOption => "unknown option",
2577 error.MissingOptionValue => "missing option value",
2578 error.RemoteExists => "remote already exists",
2579 error.RefExists => "ref already exists",
2580 error.RemoteNotFound => "remote not found",
2581 error.InvalidRemoteName => "invalid remote name",
2582 error.InvalidCheckout => "invalid checkout state",
2583 error.InvalidBranchUpstreamConfig => "invalid branch upstream config",
2584 error.BranchUpstreamExists => "branch upstream already exists",
2585 error.WorkingSetDirty => "working set has uncommitted changes",
2586 error.WorkingSetChanged => "working set changed during fast-forward",
2587 error.RecoveryRequired => "store recovery required",
2588 error.NonFastForward => "non-fast-forward",
2589 error.RefNotFound => "ref not found",
2590 error.HistoryNotFound => "history not found",
2591 error.CommitNotFound => "commit not found",
2592 error.RelationNotFound => "relation not found",
2593 error.TableNotFound => "table not found",
2594 error.NoStagedRoot => "nothing to commit",
2595 error.ConflictNotFound => "conflict not found",
2596 error.UnsupportedConflictResolution => "unsupported conflict resolution",
2597 error.UnresolvedConflicts => "unresolved conflicts",
2598 error.InvalidLength => "invalid hash",
2599 error.InvalidCharacter => "invalid hash",
2600 error.RemoteBusy => "remote busy",
2601 else => @errorName(err),
2602 };
2603 }
2604
2605 fn writeFatal(err: anyerror) void {
2606 var buffer: [1024]u8 = undefined;
2607 var text = std.Io.Writer.fixed(&buffer);
2608 text.print("tiny-sql: {s}\n", .{@errorName(err)}) catch return;
2609 pretty.diagnostic.writeStderrDoc(.{ .text = text.buffered() }, .{}) catch {};
2610 }
2611
2612 fn roundTripStorePaths(allocator: Allocator, dir_path: []const u8, prefix: []const u8) !CliOptions {
2613 return .{
2614 .database = try std.fmt.allocPrint(allocator, "{s}/{s}.db", .{ dir_path, prefix }),
2615 .wal = try std.fmt.allocPrint(allocator, "{s}/{s}.wal", .{ dir_path, prefix }),
2616 .history = try std.fmt.allocPrint(allocator, "{s}/{s}.history", .{ dir_path, prefix }),
2617 .branch = "main",
2618 };
2619 }
2620
2621 fn roundTripCreateStore(allocator: Allocator, options: CliOptions, rows: bool) !void {
2622 var workspace = try allocateDatabaseWorkspace(allocator, options);
2623 defer workspace.deallocate(allocator);
2624 var database = try openDatabase(allocator, &workspace, options);
2625 defer database.deinit();
2626 var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = options.history, .recovery = .reject });
2627 defer history.deinit();
2628 var connection = try sql.Connection.create(allocator, &database, &history, .{ .branch = "main" });
2629 defer connection.deinit();
2630 var created = try connection.execute(allocator, "CREATE TABLE items (name, data)", .{ .durability = .buffered });
2631 created.deinit(allocator);
2632 if (rows) {
2633 var first = try connection.execute(allocator, "INSERT INTO items VALUES (1, 'alpha', 7)", .{ .durability = .buffered });
2634 first.deinit(allocator);
2635 var second = try connection.execute(allocator, "INSERT INTO items VALUES (2, NULL, 'two')", .{ .durability = .buffered });
2636 second.deinit(allocator);
2637 }
2638 try connection.stage();
2639 _ = try connection.commit(&history);
2640 try database.syncWal();
2641 }
2642
2643 test "tiny-sql dump and restore round trip" {
2644 const testing = std.testing;
2645 var tmp = testing.tmpDir(.{});
2646 defer tmp.cleanup();
2647 const dir_path = try tmp.dir.realPathFileAlloc(io, ".", testing.allocator);
2648 defer testing.allocator.free(dir_path);
2649
2650 const source = try roundTripStorePaths(testing.allocator, dir_path, "source");
2651 defer {
2652 testing.allocator.free(source.database);
2653 testing.allocator.free(source.wal);
2654 testing.allocator.free(source.history);
2655 }
2656 const target = try roundTripStorePaths(testing.allocator, dir_path, "target");
2657 defer {
2658 testing.allocator.free(target.database);
2659 testing.allocator.free(target.wal);
2660 testing.allocator.free(target.history);
2661 }
2662
2663 try roundTripCreateStore(testing.allocator, source, true);
2664 try roundTripCreateStore(testing.allocator, target, false);
2665
2666 var source_dump: std.Io.Writer.Allocating = .init(testing.allocator);
2667 defer source_dump.deinit();
2668 const dump_args = [_][]const u8{ "--db", source.database, "--wal", source.wal, "--history", source.history, "--branch", "main" };
2669 try testing.expectEqual(@as(u8, 0), try runDump(testing.allocator, dump_args[0..], &source_dump.writer));
2670 try testing.expectEqualStrings(
2671 "{\"t\":\"items\",\"r\":1,\"v\":[[0,\"alpha\"],7]}\n" ++
2672 "{\"t\":\"items\",\"r\":2,\"v\":[null,[0,\"two\"]]}\n",
2673 source_dump.writer.buffered(),
2674 );
2675
2676 const dump_path = try std.fmt.allocPrint(testing.allocator, "{s}/dump.jsonl", .{dir_path});
2677 defer testing.allocator.free(dump_path);
2678 {
2679 const file = try tmp.dir.createFile(io, "dump.jsonl", .{ .truncate = true });
2680 defer file.close(io);
2681 try file.writeStreamingAll(io, source_dump.writer.buffered());
2682 }
2683
2684 var restore_out: std.Io.Writer.Allocating = .init(testing.allocator);
2685 defer restore_out.deinit();
2686 const restore_args = [_][]const u8{ "--db", target.database, "--wal", target.wal, "--history", target.history, "--branch", "main", dump_path };
2687 try testing.expectEqual(@as(u8, 0), try runRestore(testing.allocator, restore_args[0..], &restore_out.writer));
2688
2689 var target_dump: std.Io.Writer.Allocating = .init(testing.allocator);
2690 defer target_dump.deinit();
2691 const target_args = [_][]const u8{ "--db", target.database, "--wal", target.wal, "--history", target.history, "--branch", "main" };
2692 try testing.expectEqual(@as(u8, 0), try runDump(testing.allocator, target_args[0..], &target_dump.writer));
2693
2694 try testing.expectEqualStrings(source_dump.writer.buffered(), target_dump.writer.buffered());
2695 }
2696
2697 test "tiny-sql dump admits an existing WAL above the write default" {
2698 const allocator = std.testing.allocator;
2699 var tmp = std.testing.tmpDir(.{});
2700 defer tmp.cleanup();
2701 const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
2702 defer allocator.free(dir_path);
2703 const source = try roundTripStorePaths(allocator, dir_path, "source");
2704 defer allocator.free(source.database);
2705 defer allocator.free(source.wal);
2706 defer allocator.free(source.history);
2707 try roundTripCreateStore(allocator, source, true);
2708
2709 const args = [_][]const u8{
2710 "--db", source.database, "--wal", source.wal, "--history", source.history,
2711 };
2712 var before: std.Io.Writer.Allocating = .init(allocator);
2713 defer before.deinit();
2714 try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &before.writer));
2715 try growRoundTripWal(allocator, source.wal);
2716 const actual = (try std.Io.Dir.cwd().statFile(io, source.wal, .{})).size;
2717 try std.testing.expect(actual > sql.file.default_max_wal_bytes);
2718 try std.testing.expectEqual(actual + 1, try dumpWalLimit(source.wal));
2719
2720 var ordinary_workspace = try allocateDatabaseWorkspace(allocator, source);
2721 defer ordinary_workspace.deallocate(allocator);
2722 try std.testing.expectError(
2723 error.StreamTooLong,
2724 openDatabase(allocator, &ordinary_workspace, source),
2725 );
2726 var after: std.Io.Writer.Allocating = .init(allocator);
2727 defer after.deinit();
2728 try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &after.writer));
2729 try std.testing.expectEqualStrings(before.writer.buffered(), after.writer.buffered());
2730 }
2731
2732 fn growRoundTripWal(allocator: Allocator, path: []const u8) !void {
2733 const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .unlimited);
2734 defer allocator.free(bytes);
2735 const limits = sql.wal.Writer.Limits{
2736 .header = cliWalHeader(),
2737 .frames = sql.file.default_max_wal_bytes / sql.wal.frame_size + 1,
2738 };
2739 const capacity = try sql.wal.Writer.Capacity.derive(limits);
2740 const storage = try allocator.alloc(u8, capacity.storage_bytes);
2741 defer allocator.free(storage);
2742 var writer = try sql.wal.Writer.init(storage, limits);
2743 defer _ = writer.deinit();
2744 writer.activate();
2745 try writer.load(bytes, bytes.len);
2746 var reader = try sql.wal.Reader.init(bytes);
2747 var last: ?sql.wal.Frame = null;
2748 const frames = (bytes.len - sql.wal.header_size) / sql.wal.frame_size;
2749 for (0..frames) |_| last = try reader.next();
2750 const frame = last.?;
2751 std.debug.assert(frame.committed());
2752 for (0..writer.remainingFrames()) |_| {
2753 try writer.append(frame.page_id, frame.db_page_count, frame.image[0..sql.page.size]);
2754 }
2755 try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = writer.bytes() });
2756 }
2757
2758 test "tiny-sql dump values preserve tagged JSON wire format" {
2759 var output: std.Io.Writer.Allocating = .init(std.testing.allocator);
2760 defer output.deinit();
2761 var json = pretty.json.Writer.init(&output.writer, .minified);
2762 var values = try json.array();
2763 try writeDumpValue(values, .nil);
2764 try writeDumpValue(values, .{ .integer = -7 });
2765 try writeDumpValue(values, .{ .text = "a\"b" });
2766 try writeDumpValue(values, .{ .blob = &.{ 0x00, 0xfb, 0xff } });
2767 try values.end();
2768 try std.testing.expectEqualStrings(
2769 "[null,-7,[0,\"a\\\"b\"],[1,\"APv/\"]]",
2770 output.writer.buffered(),
2771 );
2772 var parsed = try std.json.parseFromSlice(
2773 std.json.Value,
2774 std.testing.allocator,
2775 output.writer.buffered(),
2776 .{},
2777 );
2778 defer parsed.deinit();
2779 }
2780
2781 test "tiny-sql parses shared options and positionals" {
2782 var parsed = try parseArgs(std.testing.allocator, &.{ "origin", "main", "--history", "local.history", "--remotes", "remotes.bin" }, .{ .history = true, .remotes = true });
2783 defer parsed.deinit(std.testing.allocator);
2784 try std.testing.expectEqualStrings("local.history", parsed.options.history);
2785 try std.testing.expectEqualStrings("remotes.bin", parsed.options.remotes);
2786 try std.testing.expectEqual(@as(usize, 2), parsed.positionals.len);
2787 try std.testing.expectEqualStrings("origin", parsed.positionals[0]);
2788 try std.testing.expectEqualStrings("main", parsed.positionals[1]);
2789 }
2790
2791 test "tiny-sql parses database checkout options" {
2792 var parsed = try parseArgs(std.testing.allocator, &.{ "--db", "work.db", "--wal", "work.wal", "--history", "work.history", "--checkout", "work.checkout", "SELECT 1" }, .{ .database = true, .wal = true, .history = true, .checkout = true });
2793 defer parsed.deinit(std.testing.allocator);
2794 try std.testing.expectEqualStrings("work.db", parsed.options.database);
2795 try std.testing.expectEqualStrings("work.wal", parsed.options.wal);
2796 try std.testing.expectEqualStrings("work.history", parsed.options.history);
2797 try std.testing.expectEqualStrings("work.checkout", parsed.options.checkout);
2798 try std.testing.expectEqual(@as(usize, 1), parsed.positionals.len);
2799 try std.testing.expectEqualStrings("SELECT 1", parsed.positionals[0]);
2800 }
2801
2802 test "tiny-sql parses remote default branch option" {
2803 var parsed = try parseArgs(std.testing.allocator, &.{ "origin", "remote.history", "--branch", "trunk", "--remotes", "remotes.bin" }, .{ .branch = true, .remotes = true });
2804 defer parsed.deinit(std.testing.allocator);
2805 try std.testing.expectEqualStrings("trunk", parsed.options.branch.?);
2806 try std.testing.expectEqualStrings("remotes.bin", parsed.options.remotes);
2807 try std.testing.expectEqual(@as(usize, 2), parsed.positionals.len);
2808 try std.testing.expectEqualStrings("origin", parsed.positionals[0]);
2809 try std.testing.expectEqualStrings("remote.history", parsed.positionals[1]);
2810 }
2811
2812 test "tiny-sql parses upstream option" {
2813 var parsed = try parseArgs(std.testing.allocator, &.{ "main", "origin", "trunk", "--upstreams", "upstreams.bin" }, .{ .upstreams = true });
2814 defer parsed.deinit(std.testing.allocator);
2815 try std.testing.expectEqualStrings("upstreams.bin", parsed.options.upstreams);
2816 try std.testing.expectEqual(@as(usize, 3), parsed.positionals.len);
2817 try std.testing.expectEqualStrings("main", parsed.positionals[0]);
2818 try std.testing.expectEqualStrings("origin", parsed.positionals[1]);
2819 try std.testing.expectEqualStrings("trunk", parsed.positionals[2]);
2820 }
2821
2822 test "tiny-sql parses prune option" {
2823 var parsed_long = try parseArgs(std.testing.allocator, &.{ "origin", "--prune" }, .{ .prune = true });
2824 defer parsed_long.deinit(std.testing.allocator);
2825 try std.testing.expect(parsed_long.options.prune);
2826 try std.testing.expectEqual(@as(usize, 1), parsed_long.positionals.len);
2827 try std.testing.expectEqualStrings("origin", parsed_long.positionals[0]);
2828
2829 var parsed_short = try parseArgs(std.testing.allocator, &.{ "-p", "origin" }, .{ .prune = true });
2830 defer parsed_short.deinit(std.testing.allocator);
2831 try std.testing.expect(parsed_short.options.prune);
2832 try std.testing.expectEqual(@as(usize, 1), parsed_short.positionals.len);
2833 try std.testing.expectEqualStrings("origin", parsed_short.positionals[0]);
2834 }
2835
2836 test "tiny-sql rejects unavailable options" {
2837 try std.testing.expectError(error.UnknownOption, parseArgs(std.testing.allocator, &.{"--history"}, .{}));
2838 try std.testing.expectError(error.UnknownOption, parseArgs(std.testing.allocator, &.{"--prune"}, .{}));
2839 }
2840
2841 test "tiny-sql persists checkout branch and merge head" {
2842 var tmp = std.testing.tmpDir(.{});
2843 defer tmp.cleanup();
2844
2845 const working_root = sql.version.emptyHash("tiny-sql.working-root");
2846 const merge_head = sql.version.emptyHash("tiny-sql.merge-head");
2847 var state = try CheckoutState.initWithWorking(std.testing.allocator, "feature", working_root, merge_head);
2848 defer state.deinit();
2849 try writeCheckoutStateToDir(std.testing.allocator, tmp.dir, "checkout", &state);
2850
2851 var read = try readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "checkout");
2852 defer read.deinit();
2853 try std.testing.expectEqualStrings("feature", read.branch);
2854 try std.testing.expect(read.working_root != null);
2855 try std.testing.expect(sql.version.same(working_root, read.working_root.?));
2856 try std.testing.expect(read.merge_head != null);
2857 try std.testing.expect(sql.version.same(merge_head, read.merge_head.?));
2858
2859 try tmp.dir.writeFile(io, .{ .sub_path = "invalid-checkout", .data = "main\nmerge bad\nextra\n" });
2860 try std.testing.expectError(error.InvalidLength, readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "invalid-checkout"));
2861 try tmp.dir.writeFile(io, .{ .sub_path = "invalid-checkout", .data = "main\nnote nope\n" });
2862 try std.testing.expectError(error.InvalidCheckout, readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "invalid-checkout"));
2863 }
2864
2865 test "tiny-sql formats remote tracking refs" {
2866 const ref_name = try remoteTrackingRefName(std.testing.allocator, "origin", "main");
2867 defer std.testing.allocator.free(ref_name);
2868 try std.testing.expectEqualStrings("refs/remotes/origin/main", ref_name);
2869
2870 const parts = remoteBranchNameParts("refs/remotes/origin/main").?;
2871 try std.testing.expectEqualStrings("origin", parts.remote);
2872 try std.testing.expectEqualStrings("main", parts.branch);
2873 try std.testing.expect(remoteBranchNameParts("refs/tags/v1") == null);
2874 }
2875
2876 test "tiny-sql formats tag refs" {
2877 const ref_name = try tagRefName(std.testing.allocator, "v1");
2878 defer std.testing.allocator.free(ref_name);
2879 try std.testing.expectEqualStrings("refs/tags/v1", ref_name);
2880
2881 try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, ""));
2882 try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, "bad tag"));
2883 try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, "refs/tags/v1"));
2884 try std.testing.expect(isTagDeleteFlag("-d"));
2885 try std.testing.expect(isTagDeleteFlag("--delete"));
2886 try std.testing.expect(!isTagDeleteFlag("delete"));
2887 }
2888
2889 test "tiny-sql recognizes version control sql tables" {
2890 try std.testing.expectEqual(VersionControlSqlTable.refs, versionControlSqlTable("SELECT * FROM tiny_sql_refs").?);
2891 try std.testing.expectEqual(VersionControlSqlTable.branches, versionControlSqlTable("select * from tiny_sql_branches;").?);
2892 try std.testing.expectEqual(VersionControlSqlTable.remote_branches, versionControlSqlTable("SELECT * FROM tiny_sql_remote_branches").?);
2893 try std.testing.expectEqual(VersionControlSqlTable.tags, versionControlSqlTable("SELECT * FROM tiny_sql_tags").?);
2894 try std.testing.expectEqual(VersionControlSqlTable.status, versionControlSqlTable("SELECT * FROM tiny_sql_status").?);
2895 try std.testing.expectEqual(VersionControlSqlTable.commits, versionControlSqlTable("SELECT * FROM tiny_sql_commits").?);
2896 try std.testing.expectEqual(VersionControlSqlTable.log, versionControlSqlTable("SELECT * FROM tiny_sql_log").?);
2897 try std.testing.expectEqual(VersionControlSqlTable.commit_ancestors, versionControlSqlTable("SELECT * FROM tiny_sql_commit_ancestors").?);
2898 try std.testing.expect(versionControlSqlTable("SELECT name FROM tiny_sql_refs") == null);
2899 try std.testing.expect(versionControlSqlTable("SELECT * FROM items WHERE rowid = 1") == null);
2900
2901 const history_table = versionControlDynamicSqlTable("SELECT * FROM tiny_sql_history_items").?;
2902 try std.testing.expectEqual(VersionControlDynamicSqlTableKind.history, history_table.kind);
2903 try std.testing.expectEqualStrings("items", history_table.relation);
2904 const diff_table = versionControlDynamicSqlTable("select * from tiny_sql_diff_items;").?;
2905 try std.testing.expectEqual(VersionControlDynamicSqlTableKind.diff, diff_table.kind);
2906 try std.testing.expectEqualStrings("items", diff_table.relation);
2907 const working_diff_table = versionControlDynamicSqlTable("select * from tiny_sql_working_diff_items;").?;
2908 try std.testing.expectEqual(VersionControlDynamicSqlTableKind.working_diff, working_diff_table.kind);
2909 try std.testing.expectEqualStrings("items", working_diff_table.relation);
2910 try std.testing.expect(versionControlDynamicSqlTable("SELECT * FROM tiny_sql_history_") == null);
2911 try std.testing.expect(versionControlDynamicSqlTable("SELECT name FROM tiny_sql_history_items") == null);
2912 try std.testing.expect(versionControlDynamicSqlTable("SELECT * FROM items") == null);
2913 }
2914
2915 test "tiny-sql recognizes version control sql calls" {
2916 const commit = (try versionControlSqlCall("CALL tiny_sql_commit()")).?;
2917 try std.testing.expectEqual(VersionControlSqlProcedure.commit, commit.procedure);
2918 try std.testing.expectEqual(@as(usize, 0), commit.arguments().len);
2919
2920 const checkout = (try versionControlSqlCall("call tiny_sql_checkout('main');")).?;
2921 try std.testing.expectEqual(VersionControlSqlProcedure.checkout, checkout.procedure);
2922 try std.testing.expectEqual(@as(usize, 1), checkout.arguments().len);
2923 try std.testing.expectEqualStrings("main", checkout.arguments()[0]);
2924
2925 const branch = (try versionControlSqlCall("CALL tiny_sql_branch('feature', 'main')")).?;
2926 try std.testing.expectEqual(VersionControlSqlProcedure.branch, branch.procedure);
2927 try std.testing.expectEqual(@as(usize, 2), branch.arguments().len);
2928 try std.testing.expectEqualStrings("feature", branch.arguments()[0]);
2929 try std.testing.expectEqualStrings("main", branch.arguments()[1]);
2930
2931 const tag_delete = (try versionControlSqlCall("CALL tiny_sql_tag_delete('v1')")).?;
2932 try std.testing.expectEqual(VersionControlSqlProcedure.tag_delete, tag_delete.procedure);
2933 try std.testing.expectEqualStrings("v1", tag_delete.arguments()[0]);
2934
2935 const fetch = (try versionControlSqlCall("CALL tiny_sql_fetch('origin', '--prune')")).?;
2936 try std.testing.expectEqual(VersionControlSqlProcedure.fetch, fetch.procedure);
2937 const fetch_args = try fetchCallArgs(fetch.arguments());
2938 try std.testing.expectEqualStrings("origin", fetch_args.remote);
2939 try std.testing.expect(fetch_args.prune);
2940
2941 const pull = (try versionControlSqlCall("CALL tiny_sql_pull('origin', 'main', '-p')")).?;
2942 try std.testing.expectEqual(VersionControlSqlProcedure.pull, pull.procedure);
2943 const pull_args = try pullCallArgs(pull.arguments());
2944 try std.testing.expect(pull_args.prune);
2945 try std.testing.expectEqual(@as(usize, 2), pull_args.positionals().len);
2946 try std.testing.expectEqualStrings("origin", pull_args.positionals()[0]);
2947 try std.testing.expectEqualStrings("main", pull_args.positionals()[1]);
2948
2949 const push = (try versionControlSqlCall("CALL tiny_sql_push('origin', 'main:trunk')")).?;
2950 try std.testing.expectEqual(VersionControlSqlProcedure.push, push.procedure);
2951 try std.testing.expectEqual(@as(usize, 2), push.arguments().len);
2952
2953 try std.testing.expect((try versionControlSqlCall("SELECT * FROM items")) == null);
2954 try std.testing.expectError(error.InvalidArguments, versionControlSqlCall("CALL tiny_sql_commit('extra', 'too', 'many', 'four')"));
2955 try std.testing.expectError(error.InvalidArguments, fetchCallArgs(&.{ "origin", "backup" }));
2956 try std.testing.expectError(error.InvalidArguments, pullCallArgs(&.{ "origin", "main", "extra" }));
2957 }
2958
2959 test "tiny-sql resolves short tag names as local refs" {
2960 var tmp = std.testing.tmpDir(.{});
2961 defer tmp.cleanup();
2962
2963 var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
2964 defer history.deinit();
2965 const commit = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.tag-target"), &.{});
2966 try history.putCommit(commit);
2967 try history.putRef(.{
2968 .name = "refs/tags/v1",
2969 .target = commit.hash,
2970 });
2971
2972 const ref_name = try resolveLocalRefName(std.testing.allocator, &history, "v1");
2973 defer std.testing.allocator.free(ref_name);
2974 try std.testing.expectEqualStrings("refs/tags/v1", ref_name);
2975 try std.testing.expectError(error.RefNotFound, resolveLocalRefName(std.testing.allocator, &history, "missing"));
2976 }
2977
2978 test "tiny-sql resolves push refspecs" {
2979 var tmp = std.testing.tmpDir(.{});
2980 defer tmp.cleanup();
2981
2982 var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
2983 defer history.deinit();
2984 const commit = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.push-refspec"), &.{});
2985 try history.putCommit(commit);
2986 try history.putRef(.{
2987 .name = "main",
2988 .target = commit.hash,
2989 });
2990 try history.putRef(.{
2991 .name = "refs/tags/v1",
2992 .target = commit.hash,
2993 });
2994
2995 var branch_refspec = try resolvePushRefSpec(std.testing.allocator, &history, "main:trunk");
2996 defer branch_refspec.deinit();
2997 try std.testing.expect(!branch_refspec.delete);
2998 try std.testing.expectEqualStrings("main", branch_refspec.local_ref.?);
2999 try std.testing.expectEqualStrings("trunk", branch_refspec.remote_ref);
3000
3001 var tag_refspec = try resolvePushRefSpec(std.testing.allocator, &history, "v1:release");
3002 defer tag_refspec.deinit();
3003 try std.testing.expect(!tag_refspec.delete);
3004 try std.testing.expectEqualStrings("refs/tags/v1", tag_refspec.local_ref.?);
3005 try std.testing.expectEqualStrings("refs/tags/release", tag_refspec.remote_ref);
3006
3007 var delete_branch = try resolvePushRefSpec(std.testing.allocator, &history, ":trunk");
3008 defer delete_branch.deinit();
3009 try std.testing.expect(delete_branch.delete);
3010 try std.testing.expect(delete_branch.local_ref == null);
3011 try std.testing.expectEqualStrings("trunk", delete_branch.remote_ref);
3012
3013 var delete_tag = try resolvePushRefSpec(std.testing.allocator, &history, ":refs/tags/v1");
3014 defer delete_tag.deinit();
3015 try std.testing.expect(delete_tag.delete);
3016 try std.testing.expectEqualStrings("refs/tags/v1", delete_tag.remote_ref);
3017
3018 try std.testing.expectError(error.InvalidArguments, resolvePushRefSpec(std.testing.allocator, &history, "main:"));
3019 }
3020
3021 test "tiny-sql parses conflict hashes" {
3022 const expected = sql.version.emptyHash("tiny-sql.hash");
3023 const text = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{std.fmt.bytesToHex(expected, .lower)});
3024 defer std.testing.allocator.free(text);
3025 const parsed = try parseHash(text);
3026 try std.testing.expect(sql.version.same(expected, parsed));
3027 const prefixed = try std.fmt.allocPrint(std.testing.allocator, "0x{s}", .{text});
3028 defer std.testing.allocator.free(prefixed);
3029 const parsed_prefixed = try parseHash(prefixed);
3030 try std.testing.expect(sql.version.same(expected, parsed_prefixed));
3031 }
3032
3033 test "tiny-sql writes conflict details and parses resolve side" {
3034 const base_size = try sql.row.encodedSize(&.{.{ .text = "base" }});
3035 const base = try std.testing.allocator.alloc(u8, base_size);
3036 defer std.testing.allocator.free(base);
3037 _ = try sql.row.encode(base, &.{.{ .text = "base" }});
3038 const ours_size = try sql.row.encodedSize(&.{.{ .text = "ours" }});
3039 const ours = try std.testing.allocator.alloc(u8, ours_size);
3040 defer std.testing.allocator.free(ours);
3041 _ = try sql.row.encode(ours, &.{.{ .text = "ours" }});
3042 const theirs_size = try sql.row.encodedSize(&.{.{ .text = "theirs" }});
3043 const theirs = try std.testing.allocator.alloc(u8, theirs_size);
3044 defer std.testing.allocator.free(theirs);
3045 _ = try sql.row.encode(theirs, &.{.{ .text = "theirs" }});
3046 const artifact = sql.ConflictArtifact.init("items", 7, base, ours, theirs);
3047
3048 var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3049 defer out.deinit();
3050 try writeConflictArtifactDetail(&out.writer, artifact);
3051 const text = out.writer.buffered();
3052 try std.testing.expect(std.mem.indexOf(u8, text, "conflict\t") != null);
3053 try std.testing.expect(std.mem.indexOf(u8, text, "base\trow\tbase\n") != null);
3054 try std.testing.expect(std.mem.indexOf(u8, text, "ours\trow\tours\n") != null);
3055 try std.testing.expect(std.mem.indexOf(u8, text, "theirs\trow\ttheirs\n") != null);
3056 try std.testing.expectEqual(ResolveSide.ours, resolveSide("ours"));
3057 try std.testing.expectEqual(ResolveSide.current, resolveSide("7"));
3058 }
3059
3060 test "tiny-sql counts ahead behind and writes commit log" {
3061 var tmp = std.testing.tmpDir(.{});
3062 defer tmp.cleanup();
3063
3064 var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
3065 defer history.deinit();
3066
3067 const root = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.root"), &.{});
3068 var first_parents = [_]sql.Hash{root.hash};
3069 const first = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.first"), first_parents[0..]);
3070 var local_parents = [_]sql.Hash{first.hash};
3071 const local = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.local"), local_parents[0..]);
3072 var remote_parents = [_]sql.Hash{first.hash};
3073 const remote = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.remote"), remote_parents[0..]);
3074
3075 try history.putCommit(root);
3076 try history.putCommit(first);
3077 try history.putCommit(local);
3078 try history.putCommit(remote);
3079
3080 const counts = try aheadBehind(std.testing.allocator, &history, local.hash, remote.hash);
3081 try std.testing.expectEqual(@as(usize, 1), counts.ahead);
3082 try std.testing.expectEqual(@as(usize, 1), counts.behind);
3083
3084 var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3085 defer out.deinit();
3086 try writeCommitLog(std.testing.allocator, &history, local.hash, &out.writer);
3087 const text = out.writer.buffered();
3088 try std.testing.expect(std.mem.indexOf(u8, text, "commit ") != null);
3089 try std.testing.expect(std.mem.indexOf(u8, text, "parents=1") != null);
3090 }
3091
3092 test "tiny-sql writes tag refs separately from branch refs" {
3093 const branch = sql.Ref{
3094 .name = "main",
3095 .target = sql.version.emptyHash("tiny-sql.branch"),
3096 };
3097 const remote = sql.Ref{
3098 .name = "refs/remotes/origin/main",
3099 .target = sql.version.emptyHash("tiny-sql.remote-ref"),
3100 };
3101 const tag = sql.Ref{
3102 .name = "refs/tags/v1",
3103 .target = sql.version.emptyHash("tiny-sql.tag-ref"),
3104 };
3105 const refs = [_]sql.Ref{ branch, remote, tag };
3106
3107 var branch_out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3108 defer branch_out.deinit();
3109 try writeRefs(&branch_out.writer, &refs, .local);
3110 const branch_text = branch_out.writer.buffered();
3111 try std.testing.expect(std.mem.indexOf(u8, branch_text, "\tmain\n") != null);
3112 try std.testing.expect(std.mem.indexOf(u8, branch_text, "origin/main") == null);
3113 try std.testing.expect(std.mem.indexOf(u8, branch_text, "v1") == null);
3114
3115 var tag_out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3116 defer tag_out.deinit();
3117 try writeTagRefs(&tag_out.writer, &refs);
3118 const tag_text = tag_out.writer.buffered();
3119 try std.testing.expect(std.mem.indexOf(u8, tag_text, "\tv1\n") != null);
3120 try std.testing.expect(std.mem.indexOf(u8, tag_text, "\tmain\n") == null);
3121 }
3122
3123 test "tiny-sql writes relation diff rows" {
3124 const from_size = try sql.row.encodedSize(&.{.{ .text = "old" }});
3125 const from = try std.testing.allocator.alloc(u8, from_size);
3126 defer std.testing.allocator.free(from);
3127 _ = try sql.row.encode(from, &.{.{ .text = "old" }});
3128 const to_size = try sql.row.encodedSize(&.{.{ .text = "new" }});
3129 const to = try std.testing.allocator.alloc(u8, to_size);
3130 defer std.testing.allocator.free(to);
3131 _ = try sql.row.encode(to, &.{.{ .text = "new" }});
3132
3133 const changes = try std.testing.allocator.alloc(sql.RelationDiffChange, 1);
3134 defer std.testing.allocator.free(changes);
3135 changes[0] = .{
3136 .kind = .modified,
3137 .rowid = 2,
3138 .from = from,
3139 .to = to,
3140 };
3141 var result = sql.RelationDiff{
3142 .allocator = std.testing.allocator,
3143 .schema_changed = false,
3144 .changes = changes,
3145 };
3146
3147 var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3148 defer out.deinit();
3149 try writeRelationDiff(&out.writer, "items", &result);
3150 const text = out.writer.buffered();
3151 try std.testing.expect(std.mem.indexOf(u8, text, "modified\titems\t2\tfrom\told\n") != null);
3152 try std.testing.expect(std.mem.indexOf(u8, text, "modified\titems\t2\tto\tnew\n") != null);
3153 }
3154
3155 test "tiny-sql usage mentions database and sync commands" {
3156 var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3157 defer out.deinit();
3158 try writeUsage(&out.writer);
3159 const text = out.writer.buffered();
3160 try std.testing.expect(std.mem.indexOf(u8, text, "init [branch]") != null);
3161 try std.testing.expect(std.mem.indexOf(u8, text, "sql <statement>") != null);
3162 try std.testing.expect(std.mem.indexOf(u8, text, "status [--db path]") != null);
3163 try std.testing.expect(std.mem.indexOf(u8, text, "log [ref]") != null);
3164 try std.testing.expect(std.mem.indexOf(u8, text, "blame <relation>") != null);
3165 try std.testing.expect(std.mem.indexOf(u8, text, "diff <relation>") != null);
3166 try std.testing.expect(std.mem.indexOf(u8, text, "merge <branch>") != null);
3167 try std.testing.expect(std.mem.indexOf(u8, text, "conflicts") != null);
3168 try std.testing.expect(std.mem.indexOf(u8, text, "resolve [current|base|ours|theirs]") != null);
3169 try std.testing.expect(std.mem.indexOf(u8, text, "clone <remote-history-path>") != null);
3170 try std.testing.expect(std.mem.indexOf(u8, text, "--branch name") != null);
3171 try std.testing.expect(std.mem.indexOf(u8, text, "upstream set <branch>") != null);
3172 try std.testing.expect(std.mem.indexOf(u8, text, "--upstreams path") != null);
3173 try std.testing.expect(std.mem.indexOf(u8, text, "fetch [remote] [-p|--prune]") != null);
3174 try std.testing.expect(std.mem.indexOf(u8, text, "push [remote] [refspec]") != null);
3175 try std.testing.expect(std.mem.indexOf(u8, text, "tag <name> [ref]") != null);
3176 try std.testing.expect(std.mem.indexOf(u8, text, "tag -d <name>") != null);
3177 try std.testing.expect(std.mem.indexOf(u8, text, "\n gc ") == null);
3178 }
3179
3180 test {
3181 _ = @import("pack.zig");
3182 }
3183
3184 const DumpFileState = struct {
3185 size: u64,
3186 mtime: std.Io.Timestamp,
3187 digest: [32]u8,
3188
3189 fn read(path: []const u8) !DumpFileState {
3190 const allocator = std.testing.allocator;
3191 const stat = try std.Io.Dir.cwd().statFile(io, path, .{});
3192 const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(128 << 20));
3193 defer allocator.free(bytes);
3194 var digest: [32]u8 = undefined;
3195 std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
3196 return .{ .size = stat.size, .mtime = stat.mtime, .digest = digest };
3197 }
3198 };
3199
3200 fn dumpFileStates(paths: []const []const u8) ![4]DumpFileState {
3201 std.debug.assert(paths.len == 4);
3202 var result: [4]DumpFileState = undefined;
3203 for (paths, &result) |path, *state| state.* = try DumpFileState.read(path);
3204 return result;
3205 }
3206
3207 fn setDumpFilesReadOnly(paths: []const []const u8, read_only: bool) !void {
3208 for (paths) |path| {
3209 const file = try std.Io.Dir.cwd().openFile(io, path, .{});
3210 defer file.close(io);
3211 const stat = try file.stat(io);
3212 try file.setPermissions(io, stat.permissions.setReadOnly(read_only));
3213 }
3214 }
3215
3216 test "tiny-sql dump preserves files and mtimes with large WAL and read-only permissions" {
3217 const allocator = std.testing.allocator;
3218 var tmp = std.testing.tmpDir(.{});
3219 defer tmp.cleanup();
3220 const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3221 defer allocator.free(dir_path);
3222 const source = try roundTripStorePaths(allocator, dir_path, "source");
3223 defer allocator.free(source.database);
3224 defer allocator.free(source.wal);
3225 defer allocator.free(source.history);
3226 try roundTripCreateStore(allocator, source, true);
3227 try growRoundTripWal(allocator, source.wal);
3228 const refs = try std.fmt.allocPrint(allocator, "{s}.refs", .{source.history});
3229 defer allocator.free(refs);
3230 const paths = [_][]const u8{ source.database, source.wal, source.history, refs };
3231 const args = [_][]const u8{
3232 "--db", source.database, "--wal", source.wal, "--history", source.history,
3233 };
3234 for ([_]bool{ false, true }) |read_only| {
3235 var permission_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true });
3236 defer permission_dir.close(io);
3237 const mode = (try permission_dir.stat(io)).permissions;
3238 try permission_dir.setPermissions(io, mode.setReadOnly(read_only));
3239 defer permission_dir.setPermissions(io, mode) catch {};
3240 try setDumpFilesReadOnly(&paths, read_only);
3241 defer setDumpFilesReadOnly(&paths, false) catch {};
3242 const before = try dumpFileStates(&paths);
3243 try std.testing.expect(before[1].size > sql.file.default_max_wal_bytes);
3244 var output: std.Io.Writer.Allocating = .init(allocator);
3245 defer output.deinit();
3246 try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &output.writer));
3247 try std.testing.expectEqualDeep(before, try dumpFileStates(&paths));
3248 try std.testing.expectEqualStrings(
3249 "{\"t\":\"items\",\"r\":1,\"v\":[[0,\"alpha\"],7]}\n" ++
3250 "{\"t\":\"items\",\"r\":2,\"v\":[null,[0,\"two\"]]}\n",
3251 output.writer.buffered(),
3252 );
3253 }
3254 }
3255
3256 test "tiny-sql dump rejects a history tail without truncating any file" {
3257 const allocator = std.testing.allocator;
3258 var tmp = std.testing.tmpDir(.{});
3259 defer tmp.cleanup();
3260 const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3261 defer allocator.free(dir_path);
3262 const source = try roundTripStorePaths(allocator, dir_path, "source");
3263 defer allocator.free(source.database);
3264 defer allocator.free(source.wal);
3265 defer allocator.free(source.history);
3266 try roundTripCreateStore(allocator, source, true);
3267 const refs = try std.fmt.allocPrint(allocator, "{s}.refs", .{source.history});
3268 defer allocator.free(refs);
3269 const paths = [_][]const u8{ source.database, source.wal, source.history, refs };
3270 {
3271 const history = try std.Io.Dir.cwd().openFile(io, source.history, .{ .mode = .read_write });
3272 defer history.close(io);
3273 const length = (try history.stat(io)).size;
3274 try history.writePositionalAll(io, "incomplete", length);
3275 }
3276 const before = try dumpFileStates(&paths);
3277 var output: std.Io.Writer.Allocating = .init(allocator);
3278 defer output.deinit();
3279 const args = [_][]const u8{
3280 "--db", source.database, "--wal", source.wal, "--history", source.history,
3281 };
3282 try std.testing.expectError(error.TruncatedHistory, runDump(allocator, &args, &output.writer));
3283 try std.testing.expectEqualDeep(before, try dumpFileStates(&paths));
3284 try std.testing.expectEqual(@as(usize, 0), output.writer.buffered().len);
3285 }
3286
3287 fn testPackRepairCommand(corrupt: bool) !void {
3288 const allocator = std.testing.allocator;
3289 var tmp = std.testing.tmpDir(.{});
3290 defer tmp.cleanup();
3291 const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3292 defer allocator.free(dir_path);
3293 const input = try std.fs.path.join(allocator, &.{ dir_path, "input.pack" });
3294 defer allocator.free(input);
3295 const output = try std.fs.path.join(allocator, &.{ dir_path, "output.pack" });
3296 defer allocator.free(output);
3297 var pack = try sql.history.repair.fixturePack(corrupt);
3298 defer pack.deinit();
3299 const original = try sql.encodeHistoryPack(allocator, &pack);
3300 defer allocator.free(original);
3301 try tmp.dir.writeFile(io, .{ .sub_path = "input.pack", .data = original });
3302 const summary = try sql.history.repair.repair(allocator, &pack, .{
3303 .relation = "items",
3304 .rowid = 1,
3305 });
3306 const expected = try sql.encodeHistoryPack(allocator, &pack);
3307 defer allocator.free(expected);
3308 var stdout: std.Io.Writer.Allocating = .init(allocator);
3309 defer stdout.deinit();
3310 var stderr: std.Io.Writer.Allocating = .init(allocator);
3311 defer stderr.deinit();
3312 const args = [_][]const u8{
3313 "tiny-sql", "pack", "repair", input, output,
3314 "--relation", "items", "--drop-row", "1",
3315 };
3316 try std.testing.expectEqual(
3317 @as(u8, 0),
3318 try run(allocator, &args, &stdout.writer, &stderr.writer),
3319 );
3320 const actual = try tmp.dir.readFileAlloc(io, "output.pack", allocator, .limited(8 << 20));
3321 defer allocator.free(actual);
3322 try std.testing.expectEqualSlices(u8, expected, actual);
3323 if (!corrupt) try std.testing.expectEqualSlices(u8, original, actual);
3324 try std.testing.expectEqual(@as(usize, 0), stderr.writer.buffered().len);
3325 const line = try std.fmt.allocPrint(allocator, "roots_repaired={d} rows_dropped={d}\n", .{
3326 summary.roots_repaired, summary.rows_dropped,
3327 });
3328 defer allocator.free(line);
3329 try std.testing.expectEqualStrings(line, stdout.writer.buffered());
3330 }
3331
3332 test "tiny-sql pack repair command writes proved and consistent packs exactly" {
3333 try testPackRepairCommand(true);
3334 try testPackRepairCommand(false);
3335 }
3336
3337 test "tiny-sql pack repair refuses an existing output before opening missing input" {
3338 const allocator = std.testing.allocator;
3339 var tmp = std.testing.tmpDir(.{});
3340 defer tmp.cleanup();
3341 const dir = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3342 defer allocator.free(dir);
3343 const missing = try std.fs.path.join(allocator, &.{ dir, "missing.pack" });
3344 defer allocator.free(missing);
3345 const output = try std.fs.path.join(allocator, &.{ dir, "output.pack" });
3346 defer allocator.free(output);
3347 try tmp.dir.writeFile(io, .{ .sub_path = "output.pack", .data = "preserve this file" });
3348 const before = try DumpFileState.read(output);
3349 var stdout: std.Io.Writer.Allocating = .init(allocator);
3350 defer stdout.deinit();
3351 var stderr: std.Io.Writer.Allocating = .init(allocator);
3352 defer stderr.deinit();
3353 const args = [_][]const u8{
3354 "tiny-sql", "pack", "repair", missing, output,
3355 "--relation", "items", "--drop-row", "1",
3356 };
3357 try std.testing.expectEqual(
3358 @as(u8, 1),
3359 try run(allocator, &args, &stdout.writer, &stderr.writer),
3360 );
3361 try std.testing.expectEqualDeep(before, try DumpFileState.read(output));
3362 const refused_existing = std.mem.startsWith(
3363 u8,
3364 stderr.writer.buffered(),
3365 "pack repair: output already exists:",
3366 );
3367 try std.testing.expect(refused_existing);
3368 try std.testing.expectEqual(@as(usize, 0), stdout.writer.buffered().len);
3369 }