lib/sql/src/sync.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const branch = @import("branch.zig");
3 const connection_mod = @import("connection.zig");
4 const file = @import("file.zig");
5 const history_mod = @import("history/root.zig");
6 const version = @import("version.zig");
7 const wal = @import("wal.zig");
8
9 const Allocator = std.mem.Allocator;
10 const testing_io = std.Options.debug_io;
11
12 const pack_magic: u32 = 0x7473_706b;
13 const pack_format_version: u32 = 1;
14 const remote_config_magic: u32 = 0x7473_726d;
15 const remote_config_format_version: u32 = 2;
16 const branch_upstream_config_magic: u32 = 0x7473_7570;
17 const branch_upstream_config_format_version: u32 = 1;
18 const advertisement_magic: u32 = 0x7473_6164;
19 const advertisement_format_version: u32 = 1;
20 const fetch_request_magic: u32 = 0x7473_6672;
21 const fetch_request_format_version: u32 = 1;
22
23 pub const Error = Allocator.Error || history_mod.Error || branch.Error || error{
24 InvalidPack,
25 InvalidRemoteConfig,
26 InvalidBranchUpstreamConfig,
27 InvalidAdvertisement,
28 InvalidFetchRequest,
29 InvalidRemoteName,
30 RemoteExists,
31 RemoteNotFound,
32 BranchUpstreamExists,
33 RemoteBusy,
34 };
35
36 pub const Stats = struct {
37 refs: usize = 0,
38 commits: usize = 0,
39 records: usize = 0,
40 };
41
42 pub const HeadHex = [version.hash_bytes * 2]u8;
43
44 pub const FetchOptions = struct {
45 remote: []const u8 = "origin",
46 prune: bool = false,
47 };
48
49 pub const HistoryDirection = enum { push, pull };
50
51 pub const HistoryRelation = struct {
52 local_head: version.Hash,
53 remote_head: ?version.Hash,
54 ahead: usize,
55 behind: usize,
56
57 pub fn upToDate(self: HistoryRelation) bool {
58 const remote_head = self.remote_head orelse return false;
59 return version.same(self.local_head, remote_head);
60 }
61
62 pub fn diverged(self: HistoryRelation) bool {
63 return self.ahead != 0 and self.behind != 0;
64 }
65 };
66
67 pub const HistoryTransferPlan = struct {
68 commits: usize = 0,
69 records: usize = 0,
70 };
71
72 pub const HistoryPlanError = Error || error{
73 HistoryDiverged,
74 HistoryRemoteAhead,
75 };
76
77 pub const FileRemote = struct {
78 io: std.Io = std.Options.debug_io,
79 dir: std.Io.Dir,
80 path: []const u8 = "tiny.sql.history",
81 lock_path: ?[]const u8 = null,
82 };
83
84 pub const RemoteConfigOptions = struct {
85 io: std.Io = std.Options.debug_io,
86 path: []const u8 = "tiny.sql.remotes",
87 max_bytes: usize = 64 * 1024,
88 };
89
90 pub const BranchUpstreamOptions = struct {
91 io: std.Io = std.Options.debug_io,
92 path: []const u8 = "tiny.sql.upstreams",
93 max_bytes: usize = 64 * 1024,
94 };
95
96 pub const RemoteConfigEntry = struct {
97 name: []u8,
98 history_path: []u8,
99 lock_path: ?[]u8 = null,
100 default_branch: ?[]u8 = null,
101
102 pub fn init(allocator: Allocator, name: []const u8, history_path: []const u8, lock_path: ?[]const u8) Error!RemoteConfigEntry {
103 return try initWithDefaultBranch(allocator, name, history_path, lock_path, null);
104 }
105
106 pub fn initWithDefaultBranch(allocator: Allocator, name: []const u8, history_path: []const u8, lock_path: ?[]const u8, default_branch: ?[]const u8) Error!RemoteConfigEntry {
107 if (!validRemoteName(name)) return error.InvalidRemoteName;
108 if (default_branch) |branch_name| {
109 if (branch_name.len == 0) return error.InvalidRemoteConfig;
110 }
111 const owned_name = try allocator.dupe(u8, name);
112 errdefer allocator.free(owned_name);
113 const owned_history_path = try allocator.dupe(u8, history_path);
114 errdefer allocator.free(owned_history_path);
115 const owned_lock_path = if (lock_path) |path| try allocator.dupe(u8, path) else null;
116 errdefer if (owned_lock_path) |path| allocator.free(path);
117 const owned_default_branch = if (default_branch) |branch_name| try allocator.dupe(u8, branch_name) else null;
118 errdefer if (owned_default_branch) |branch_name| allocator.free(branch_name);
119 return .{
120 .name = owned_name,
121 .history_path = owned_history_path,
122 .lock_path = owned_lock_path,
123 .default_branch = owned_default_branch,
124 };
125 }
126
127 pub fn deinit(self: *RemoteConfigEntry, allocator: Allocator) void {
128 allocator.free(self.name);
129 allocator.free(self.history_path);
130 if (self.lock_path) |path| allocator.free(path);
131 if (self.default_branch) |branch_name| allocator.free(branch_name);
132 self.* = undefined;
133 }
134
135 pub fn fileRemote(self: *const RemoteConfigEntry, io: std.Io, dir: std.Io.Dir) FileRemote {
136 return .{
137 .io = io,
138 .dir = dir,
139 .path = self.history_path,
140 .lock_path = self.lock_path,
141 };
142 }
143 };
144
145 pub const RemoteConfig = struct {
146 allocator: Allocator,
147 remotes: []RemoteConfigEntry,
148
149 pub fn init(allocator: Allocator, remotes: []const RemoteConfigEntry) Error!RemoteConfig {
150 try validateRemoteEntries(remotes);
151 const owned = try allocator.alloc(RemoteConfigEntry, remotes.len);
152 var count: usize = 0;
153 errdefer {
154 for (owned[0..count]) |*remote| remote.deinit(allocator);
155 if (owned.len != 0) allocator.free(owned);
156 }
157 for (remotes, owned) |remote, *target| {
158 target.* = try RemoteConfigEntry.initWithDefaultBranch(allocator, remote.name, remote.history_path, remote.lock_path, remote.default_branch);
159 count += 1;
160 }
161 return .{
162 .allocator = allocator,
163 .remotes = owned,
164 };
165 }
166
167 pub fn deinit(self: *RemoteConfig) void {
168 for (self.remotes) |*remote| remote.deinit(self.allocator);
169 if (self.remotes.len != 0) self.allocator.free(self.remotes);
170 self.* = undefined;
171 }
172
173 pub fn find(self: *const RemoteConfig, name: []const u8) ?*const RemoteConfigEntry {
174 for (self.remotes) |*remote| {
175 if (std.mem.eql(u8, remote.name, name)) return remote;
176 }
177 return null;
178 }
179
180 pub fn fileRemote(self: *const RemoteConfig, io: std.Io, dir: std.Io.Dir, name: []const u8) Error!FileRemote {
181 const remote = self.find(name) orelse return error.RemoteNotFound;
182 return remote.fileRemote(io, dir);
183 }
184 };
185
186 pub const BranchUpstreamEntry = struct {
187 branch: []u8,
188 remote: []u8,
189 remote_branch: []u8,
190
191 pub fn init(allocator: Allocator, branch_name: []const u8, remote_name: []const u8, remote_branch_name: []const u8) Error!BranchUpstreamEntry {
192 if (branch_name.len == 0 or remote_branch_name.len == 0) return error.InvalidBranchUpstreamConfig;
193 if (!validRemoteName(remote_name)) return error.InvalidRemoteName;
194 const owned_branch = try allocator.dupe(u8, branch_name);
195 errdefer allocator.free(owned_branch);
196 const owned_remote = try allocator.dupe(u8, remote_name);
197 errdefer allocator.free(owned_remote);
198 const owned_remote_branch = try allocator.dupe(u8, remote_branch_name);
199 errdefer allocator.free(owned_remote_branch);
200 return .{
201 .branch = owned_branch,
202 .remote = owned_remote,
203 .remote_branch = owned_remote_branch,
204 };
205 }
206
207 pub fn deinit(self: *BranchUpstreamEntry, allocator: Allocator) void {
208 allocator.free(self.branch);
209 allocator.free(self.remote);
210 allocator.free(self.remote_branch);
211 self.* = undefined;
212 }
213 };
214
215 pub const BranchUpstreamConfig = struct {
216 allocator: Allocator,
217 upstreams: []BranchUpstreamEntry,
218
219 pub fn init(allocator: Allocator, upstreams: []const BranchUpstreamEntry) Error!BranchUpstreamConfig {
220 try validateBranchUpstreamEntries(upstreams);
221 const owned = try allocator.alloc(BranchUpstreamEntry, upstreams.len);
222 var count: usize = 0;
223 errdefer {
224 for (owned[0..count]) |*upstream| upstream.deinit(allocator);
225 if (owned.len != 0) allocator.free(owned);
226 }
227 for (upstreams, owned) |upstream, *target| {
228 target.* = try BranchUpstreamEntry.init(allocator, upstream.branch, upstream.remote, upstream.remote_branch);
229 count += 1;
230 }
231 return .{
232 .allocator = allocator,
233 .upstreams = owned,
234 };
235 }
236
237 pub fn deinit(self: *BranchUpstreamConfig) void {
238 for (self.upstreams) |*upstream| upstream.deinit(self.allocator);
239 if (self.upstreams.len != 0) self.allocator.free(self.upstreams);
240 self.* = undefined;
241 }
242
243 pub fn find(self: *const BranchUpstreamConfig, branch_name: []const u8) ?*const BranchUpstreamEntry {
244 for (self.upstreams) |*upstream| {
245 if (std.mem.eql(u8, upstream.branch, branch_name)) return upstream;
246 }
247 return null;
248 }
249 };
250
251 pub const FileRemoteLock = struct {
252 io: std.Io,
253 file: std.Io.File,
254
255 pub fn release(self: *FileRemoteLock) void {
256 self.file.close(self.io);
257 self.* = undefined;
258 }
259 };
260
261 pub const Advertisement = struct {
262 allocator: Allocator,
263 refs: []RefObject,
264
265 pub fn deinit(self: *Advertisement) void {
266 for (self.refs) |*ref_value| ref_value.deinit(self.allocator);
267 if (self.refs.len != 0) self.allocator.free(self.refs);
268 self.* = undefined;
269 }
270
271 pub fn ref(self: *const Advertisement, name: []const u8) ?version.Ref {
272 for (self.refs) |ref_value| {
273 if (std.mem.eql(u8, ref_value.name, name)) {
274 return .{
275 .name = ref_value.name,
276 .target = ref_value.target,
277 };
278 }
279 }
280 return null;
281 }
282 };
283
284 pub const FetchRequest = struct {
285 allocator: Allocator,
286 wants: [][]u8,
287 haves: []version.Hash,
288
289 pub fn init(allocator: Allocator, wants: []const []const u8, haves: []const version.Hash) Allocator.Error!FetchRequest {
290 const owned_wants = try allocator.alloc([]u8, wants.len);
291 var want_count: usize = 0;
292 errdefer {
293 for (owned_wants[0..want_count]) |want| allocator.free(want);
294 if (owned_wants.len != 0) allocator.free(owned_wants);
295 }
296 for (wants, owned_wants) |want, *target| {
297 target.* = try allocator.dupe(u8, want);
298 want_count += 1;
299 }
300 const owned_haves = try allocator.dupe(version.Hash, haves);
301 errdefer if (owned_haves.len != 0) allocator.free(owned_haves);
302 return .{
303 .allocator = allocator,
304 .wants = owned_wants,
305 .haves = owned_haves,
306 };
307 }
308
309 pub fn deinit(self: *FetchRequest) void {
310 for (self.wants) |want| self.allocator.free(want);
311 if (self.wants.len != 0) self.allocator.free(self.wants);
312 if (self.haves.len != 0) self.allocator.free(self.haves);
313 self.* = undefined;
314 }
315 };
316
317 pub const CommitObject = struct {
318 hash: version.Hash,
319 root: version.Hash,
320 parents: []version.Hash,
321
322 fn deinit(self: *CommitObject, allocator: Allocator) void {
323 if (self.parents.len != 0) allocator.free(self.parents);
324 self.* = undefined;
325 }
326 };
327
328 pub const RefObject = struct {
329 name: []u8,
330 target: version.Hash,
331
332 fn deinit(self: *RefObject, allocator: Allocator) void {
333 allocator.free(self.name);
334 self.* = undefined;
335 }
336 };
337
338 pub const PackFrame = struct {
339 kind: history_mod.PackRecordKind,
340 payload: []u8,
341
342 fn deinit(self: *PackFrame, allocator: Allocator) void {
343 allocator.free(self.payload);
344 self.* = undefined;
345 }
346 };
347
348 pub const Pack = struct {
349 allocator: Allocator,
350 refs: []RefObject,
351 commits: []CommitObject,
352 frames: []PackFrame,
353
354 pub fn deinit(self: *Pack) void {
355 for (self.refs) |*ref_value| ref_value.deinit(self.allocator);
356 for (self.commits) |*commit| commit.deinit(self.allocator);
357 for (self.frames) |*frame| frame.deinit(self.allocator);
358 if (self.refs.len != 0) self.allocator.free(self.refs);
359 if (self.commits.len != 0) self.allocator.free(self.commits);
360 if (self.frames.len != 0) self.allocator.free(self.frames);
361 self.* = undefined;
362 }
363
364 pub fn stats(self: *const Pack) Stats {
365 return .{
366 .refs = self.refs.len,
367 .commits = self.commits.len,
368 .records = self.frames.len,
369 };
370 }
371 };
372
373 pub fn encodePack(allocator: Allocator, pack: *const Pack) Error![]u8 {
374 var bytes: std.ArrayList(u8) = .empty;
375 errdefer bytes.deinit(allocator);
376 try appendU32(allocator, &bytes, pack_magic);
377 try appendU32(allocator, &bytes, pack_format_version);
378 try appendCount(allocator, &bytes, pack.refs.len);
379 try appendCount(allocator, &bytes, pack.commits.len);
380 try appendCount(allocator, &bytes, pack.frames.len);
381 for (pack.refs) |ref_value| {
382 try appendBytes(allocator, &bytes, ref_value.name);
383 try appendHash(allocator, &bytes, ref_value.target);
384 }
385 for (pack.commits) |commit| {
386 try appendHash(allocator, &bytes, commit.hash);
387 try appendHash(allocator, &bytes, commit.root);
388 try appendCount(allocator, &bytes, commit.parents.len);
389 for (commit.parents) |parent| try appendHash(allocator, &bytes, parent);
390 }
391 for (pack.frames) |frame| {
392 try appendU32(allocator, &bytes, @backingInt(frame.kind));
393 try appendBytes(allocator, &bytes, frame.payload);
394 }
395 return try bytes.toOwnedSlice(allocator);
396 }
397
398 const PackSections = struct {
399 refs: []RefObject,
400 commits: []CommitObject,
401 frame_count: usize,
402
403 fn deinit(self: *PackSections, allocator: Allocator) void {
404 for (self.refs) |*ref_value| ref_value.deinit(allocator);
405 for (self.commits) |*commit| commit.deinit(allocator);
406 if (self.refs.len != 0) allocator.free(self.refs);
407 if (self.commits.len != 0) allocator.free(self.commits);
408 self.* = undefined;
409 }
410 };
411
412 fn decodePackSections(allocator: Allocator, reader: *ByteReader) Error!PackSections {
413 if (try reader.readU32() != pack_magic) return error.InvalidPack;
414 if (try reader.readU32() != pack_format_version) return error.InvalidPack;
415
416 const ref_count = try reader.readCount();
417 const commit_count = try reader.readCount();
418 const frame_count = try reader.readCount();
419
420 const refs = try allocator.alloc(RefObject, ref_count);
421 var refs_read: usize = 0;
422 errdefer {
423 for (refs[0..refs_read]) |*ref_value| ref_value.deinit(allocator);
424 if (refs.len != 0) allocator.free(refs);
425 }
426 for (refs) |*ref_value| {
427 const name = try reader.readOwnedBytes(allocator);
428 errdefer allocator.free(name);
429 ref_value.* = .{
430 .name = name,
431 .target = try reader.hash(),
432 };
433 refs_read += 1;
434 }
435
436 const commits = try allocator.alloc(CommitObject, commit_count);
437 var commits_read: usize = 0;
438 errdefer {
439 for (commits[0..commits_read]) |*commit| commit.deinit(allocator);
440 if (commits.len != 0) allocator.free(commits);
441 }
442 for (commits) |*commit| {
443 const hash = try reader.hash();
444 const root = try reader.hash();
445 const parent_count = try reader.readCount();
446 const parents = try allocator.alloc(version.Hash, parent_count);
447 errdefer if (parents.len != 0) allocator.free(parents);
448 for (parents) |*parent| parent.* = try reader.hash();
449 const canonical = version.Commit.init(root, parents);
450 if (!version.same(canonical.hash, hash)) return error.InvalidPack;
451 commit.* = .{
452 .hash = hash,
453 .root = root,
454 .parents = parents,
455 };
456 commits_read += 1;
457 }
458
459 return .{
460 .refs = refs,
461 .commits = commits,
462 .frame_count = frame_count,
463 };
464 }
465
466 fn decodeFrameKind(value: u32) Error!history_mod.PackRecordKind {
467 return std.enums.fromInt(history_mod.PackRecordKind, value) orelse error.InvalidPack;
468 }
469
470 pub fn decodePack(allocator: Allocator, bytes: []const u8) Error!Pack {
471 var reader = ByteReader.init(bytes);
472 var sections = try decodePackSections(allocator, &reader);
473 errdefer sections.deinit(allocator);
474
475 const frames = try allocator.alloc(PackFrame, sections.frame_count);
476 var frames_read: usize = 0;
477 errdefer {
478 for (frames[0..frames_read]) |*frame| frame.deinit(allocator);
479 if (frames.len != 0) allocator.free(frames);
480 }
481 for (frames) |*frame| {
482 const kind = try decodeFrameKind(try reader.readU32());
483 frame.* = .{
484 .kind = kind,
485 .payload = try reader.readOwnedBytes(allocator),
486 };
487 frames_read += 1;
488 }
489
490 try reader.finish();
491 return .{
492 .allocator = allocator,
493 .refs = sections.refs,
494 .commits = sections.commits,
495 .frames = frames,
496 };
497 }
498
499 pub fn encodeRemoteConfig(allocator: Allocator, config: *const RemoteConfig) Error![]u8 {
500 try validateRemoteEntries(config.remotes);
501 var bytes: std.ArrayList(u8) = .empty;
502 errdefer bytes.deinit(allocator);
503 try appendU32(allocator, &bytes, remote_config_magic);
504 try appendU32(allocator, &bytes, remote_config_format_version);
505 try appendCount(allocator, &bytes, config.remotes.len);
506 for (config.remotes) |remote| {
507 try appendBytes(allocator, &bytes, remote.name);
508 try appendBytes(allocator, &bytes, remote.history_path);
509 if (remote.lock_path) |lock_path| {
510 try appendU8(allocator, &bytes, 1);
511 try appendBytes(allocator, &bytes, lock_path);
512 } else {
513 try appendU8(allocator, &bytes, 0);
514 }
515 if (remote.default_branch) |default_branch| {
516 try appendU8(allocator, &bytes, 1);
517 try appendBytes(allocator, &bytes, default_branch);
518 } else {
519 try appendU8(allocator, &bytes, 0);
520 }
521 }
522 return try bytes.toOwnedSlice(allocator);
523 }
524
525 pub fn decodeRemoteConfig(allocator: Allocator, bytes: []const u8) Error!RemoteConfig {
526 var reader = RemoteConfigReader.init(bytes);
527 if (try reader.readU32() != remote_config_magic) return error.InvalidRemoteConfig;
528 const format_version = try reader.readU32();
529 if (format_version != 1 and format_version != remote_config_format_version) return error.InvalidRemoteConfig;
530 const count = try reader.readCount();
531 const remotes = try allocator.alloc(RemoteConfigEntry, count);
532 var read: usize = 0;
533 errdefer {
534 for (remotes[0..read]) |*remote| remote.deinit(allocator);
535 if (remotes.len != 0) allocator.free(remotes);
536 }
537 for (remotes) |*remote| {
538 const name = try reader.readBytes();
539 const history_path = try reader.readBytes();
540 const lock_path = switch (try reader.readU8()) {
541 0 => null,
542 1 => try reader.readBytes(),
543 else => return error.InvalidRemoteConfig,
544 };
545 const default_branch = if (format_version >= 2) switch (try reader.readU8()) {
546 0 => null,
547 1 => try reader.readBytes(),
548 else => return error.InvalidRemoteConfig,
549 } else null;
550 remote.* = try RemoteConfigEntry.initWithDefaultBranch(allocator, name, history_path, lock_path, default_branch);
551 read += 1;
552 }
553 try reader.finish();
554 try validateRemoteEntries(remotes);
555 return .{
556 .allocator = allocator,
557 .remotes = remotes,
558 };
559 }
560
561 pub fn readRemoteConfig(allocator: Allocator, dir: std.Io.Dir, options: RemoteConfigOptions) Error!RemoteConfig {
562 const bytes = dir.readFileAlloc(options.io, options.path, allocator, .limited(options.max_bytes)) catch |err| switch (err) {
563 error.FileNotFound => {
564 return .{
565 .allocator = allocator,
566 .remotes = &.{},
567 };
568 },
569 else => return err,
570 };
571 defer allocator.free(bytes);
572 return try decodeRemoteConfig(allocator, bytes);
573 }
574
575 pub fn writeRemoteConfig(allocator: Allocator, dir: std.Io.Dir, options: RemoteConfigOptions, config: *const RemoteConfig) Error!void {
576 const bytes = try encodeRemoteConfig(allocator, config);
577 defer allocator.free(bytes);
578 var out = try dir.createFile(options.io, options.path, .{ .read = true, .truncate = true });
579 defer out.close(options.io);
580 try out.writePositionalAll(options.io, bytes, 0);
581 try out.setLength(options.io, bytes.len);
582 try out.sync(options.io);
583 }
584
585 pub fn encodeBranchUpstreamConfig(allocator: Allocator, config: *const BranchUpstreamConfig) Error![]u8 {
586 try validateBranchUpstreamEntries(config.upstreams);
587 var bytes: std.ArrayList(u8) = .empty;
588 errdefer bytes.deinit(allocator);
589 try appendU32(allocator, &bytes, branch_upstream_config_magic);
590 try appendU32(allocator, &bytes, branch_upstream_config_format_version);
591 try appendCount(allocator, &bytes, config.upstreams.len);
592 for (config.upstreams) |upstream| {
593 try appendBytes(allocator, &bytes, upstream.branch);
594 try appendBytes(allocator, &bytes, upstream.remote);
595 try appendBytes(allocator, &bytes, upstream.remote_branch);
596 }
597 return try bytes.toOwnedSlice(allocator);
598 }
599
600 pub fn decodeBranchUpstreamConfig(allocator: Allocator, bytes: []const u8) Error!BranchUpstreamConfig {
601 var reader = BranchUpstreamConfigReader.init(bytes);
602 if (try reader.readU32() != branch_upstream_config_magic) return error.InvalidBranchUpstreamConfig;
603 if (try reader.readU32() != branch_upstream_config_format_version) return error.InvalidBranchUpstreamConfig;
604 const count = try reader.readCount();
605 const upstreams = try allocator.alloc(BranchUpstreamEntry, count);
606 var read: usize = 0;
607 errdefer {
608 for (upstreams[0..read]) |*upstream| upstream.deinit(allocator);
609 if (upstreams.len != 0) allocator.free(upstreams);
610 }
611 for (upstreams) |*upstream| {
612 const branch_name = try reader.readBytes();
613 const remote_name = try reader.readBytes();
614 const remote_branch_name = try reader.readBytes();
615 upstream.* = try BranchUpstreamEntry.init(allocator, branch_name, remote_name, remote_branch_name);
616 read += 1;
617 }
618 try reader.finish();
619 try validateBranchUpstreamEntries(upstreams);
620 return .{
621 .allocator = allocator,
622 .upstreams = upstreams,
623 };
624 }
625
626 pub fn readBranchUpstreamConfig(allocator: Allocator, dir: std.Io.Dir, options: BranchUpstreamOptions) Error!BranchUpstreamConfig {
627 const bytes = dir.readFileAlloc(options.io, options.path, allocator, .limited(options.max_bytes)) catch |err| switch (err) {
628 error.FileNotFound => {
629 return .{
630 .allocator = allocator,
631 .upstreams = &.{},
632 };
633 },
634 else => return err,
635 };
636 defer allocator.free(bytes);
637 return try decodeBranchUpstreamConfig(allocator, bytes);
638 }
639
640 pub fn writeBranchUpstreamConfig(allocator: Allocator, dir: std.Io.Dir, options: BranchUpstreamOptions, config: *const BranchUpstreamConfig) Error!void {
641 const bytes = try encodeBranchUpstreamConfig(allocator, config);
642 defer allocator.free(bytes);
643 var out = try dir.createFile(options.io, options.path, .{ .read = true, .truncate = true });
644 defer out.close(options.io);
645 try out.writePositionalAll(options.io, bytes, 0);
646 try out.setLength(options.io, bytes.len);
647 try out.sync(options.io);
648 }
649
650 pub fn lockFileRemote(allocator: Allocator, remote: FileRemote) Error!FileRemoteLock {
651 const lock_path = if (remote.lock_path) |path| path else try std.fmt.allocPrint(allocator, "{s}.lock", .{remote.path});
652 defer if (remote.lock_path == null) allocator.free(lock_path);
653 const lock_file = remote.dir.createFile(remote.io, lock_path, .{
654 .read = true,
655 .truncate = false,
656 .lock = .exclusive,
657 .lock_nonblocking = true,
658 }) catch |err| switch (err) {
659 error.WouldBlock => return error.RemoteBusy,
660 else => return err,
661 };
662 return .{ .io = remote.io, .file = lock_file };
663 }
664
665 pub fn advertiseRefs(allocator: Allocator, history: *const history_mod.History) Error!Advertisement {
666 const refs = try history.refList(allocator);
667 defer history_mod.freeRefList(allocator, refs);
668 const owned = try allocator.alloc(RefObject, refs.len);
669 var count: usize = 0;
670 errdefer {
671 for (owned[0..count]) |*ref_value| ref_value.deinit(allocator);
672 if (owned.len != 0) allocator.free(owned);
673 }
674 for (refs, owned) |ref_value, *target| {
675 const name = try allocator.dupe(u8, ref_value.name);
676 errdefer allocator.free(name);
677 target.* = .{
678 .name = name,
679 .target = ref_value.target,
680 };
681 count += 1;
682 }
683 return .{
684 .allocator = allocator,
685 .refs = owned,
686 };
687 }
688
689 pub fn encodeAdvertisement(allocator: Allocator, advertisement: *const Advertisement) Error![]u8 {
690 var bytes: std.ArrayList(u8) = .empty;
691 errdefer bytes.deinit(allocator);
692 try appendU32(allocator, &bytes, advertisement_magic);
693 try appendU32(allocator, &bytes, advertisement_format_version);
694 try appendCount(allocator, &bytes, advertisement.refs.len);
695 for (advertisement.refs) |ref_value| {
696 try appendBytes(allocator, &bytes, ref_value.name);
697 try appendHash(allocator, &bytes, ref_value.target);
698 }
699 return try bytes.toOwnedSlice(allocator);
700 }
701
702 pub fn decodeAdvertisement(allocator: Allocator, bytes: []const u8) Error!Advertisement {
703 var reader = AdvertisementReader.init(bytes);
704 if (try reader.readU32() != advertisement_magic) return error.InvalidAdvertisement;
705 if (try reader.readU32() != advertisement_format_version) return error.InvalidAdvertisement;
706 const count = try reader.readCount();
707 const refs = try allocator.alloc(RefObject, count);
708 var read: usize = 0;
709 errdefer {
710 for (refs[0..read]) |*ref_value| ref_value.deinit(allocator);
711 if (refs.len != 0) allocator.free(refs);
712 }
713 for (refs) |*ref_value| {
714 const name = try reader.readOwnedBytes(allocator);
715 errdefer allocator.free(name);
716 ref_value.* = .{
717 .name = name,
718 .target = try reader.hash(),
719 };
720 read += 1;
721 }
722 try reader.finish();
723 return .{
724 .allocator = allocator,
725 .refs = refs,
726 };
727 }
728
729 pub fn fetchRequestFromHistory(allocator: Allocator, local: *const history_mod.History, wants: []const []const u8) Error!FetchRequest {
730 const entries = try local.commitEntries(allocator);
731 defer allocator.free(entries);
732 const haves = try allocator.alloc(version.Hash, entries.len);
733 defer allocator.free(haves);
734 for (entries, haves) |entry, *hash| hash.* = entry.hash;
735 return try FetchRequest.init(allocator, wants, haves);
736 }
737
738 pub fn encodeFetchRequest(allocator: Allocator, request: *const FetchRequest) Error![]u8 {
739 var bytes: std.ArrayList(u8) = .empty;
740 errdefer bytes.deinit(allocator);
741 try appendU32(allocator, &bytes, fetch_request_magic);
742 try appendU32(allocator, &bytes, fetch_request_format_version);
743 try appendCount(allocator, &bytes, request.wants.len);
744 try appendCount(allocator, &bytes, request.haves.len);
745 for (request.wants) |want| try appendBytes(allocator, &bytes, want);
746 for (request.haves) |have| try appendHash(allocator, &bytes, have);
747 return try bytes.toOwnedSlice(allocator);
748 }
749
750 pub fn decodeFetchRequest(allocator: Allocator, bytes: []const u8) Error!FetchRequest {
751 var reader = FetchRequestReader.init(bytes);
752 if (try reader.readU32() != fetch_request_magic) return error.InvalidFetchRequest;
753 if (try reader.readU32() != fetch_request_format_version) return error.InvalidFetchRequest;
754 const want_count = try reader.readCount();
755 const have_count = try reader.readCount();
756 const wants = try allocator.alloc([]u8, want_count);
757 var wants_read: usize = 0;
758 errdefer {
759 for (wants[0..wants_read]) |want| allocator.free(want);
760 if (wants.len != 0) allocator.free(wants);
761 }
762 for (wants) |*want| {
763 want.* = try reader.readOwnedBytes(allocator);
764 wants_read += 1;
765 }
766 const haves = try allocator.alloc(version.Hash, have_count);
767 errdefer if (haves.len != 0) allocator.free(haves);
768 for (haves) |*have| have.* = try reader.hash();
769 try reader.finish();
770 return .{
771 .allocator = allocator,
772 .wants = wants,
773 .haves = haves,
774 };
775 }
776
777 const HashSet = std.AutoHashMapUnmanaged(version.Hash, void);
778
779 const FrameGroup = struct {
780 kind: history_mod.PackRecordKind,
781 hashes: []const version.Hash,
782 };
783
784 const PackTreeTraversalEntry = struct {
785 key: version.Hash,
786 expanded: bool,
787 };
788
789 const PackBuilder = struct {
790 allocator: Allocator,
791 source: *const history_mod.History,
792 have: ?*const history_mod.History = null,
793 have_commits: []const version.Hash = &.{},
794 refs: std.ArrayList(RefObject) = .empty,
795 commits: std.ArrayList(CommitObject) = .empty,
796 seen_commits: HashSet = .empty,
797 seen_database_roots: HashSet = .empty,
798 seen_relation_roots: HashSet = .empty,
799 seen_relation_rows: HashSet = .empty,
800 seen_pages: HashSet = .empty,
801 seen_chunks: HashSet = .empty,
802 seen_nodes: HashSet = .empty,
803 seen_conflicts: HashSet = .empty,
804 seen_conflict_roots: HashSet = .empty,
805 chunks: std.ArrayList(version.Hash) = .empty,
806 pages: std.ArrayList(version.Hash) = .empty,
807 nodes: std.ArrayList(version.Hash) = .empty,
808 relation_rows: std.ArrayList(version.Hash) = .empty,
809 relation_roots: std.ArrayList(version.Hash) = .empty,
810 conflicts: std.ArrayList(version.Hash) = .empty,
811 conflict_roots: std.ArrayList(version.Hash) = .empty,
812 database_roots: std.ArrayList(version.Hash) = .empty,
813
814 fn init(allocator: Allocator, source: *const history_mod.History) PackBuilder {
815 return .{
816 .allocator = allocator,
817 .source = source,
818 };
819 }
820
821 fn initMissing(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History) PackBuilder {
822 return .{
823 .allocator = allocator,
824 .source = source,
825 .have = have,
826 };
827 }
828
829 fn initWithHaveCommits(allocator: Allocator, source: *const history_mod.History, haves: []const version.Hash) PackBuilder {
830 return .{
831 .allocator = allocator,
832 .source = source,
833 .have_commits = haves,
834 };
835 }
836
837 fn deinit(self: *PackBuilder) void {
838 for (self.refs.items) |*ref_value| ref_value.deinit(self.allocator);
839 for (self.commits.items) |*commit| commit.deinit(self.allocator);
840 self.refs.deinit(self.allocator);
841 self.commits.deinit(self.allocator);
842 self.seen_commits.deinit(self.allocator);
843 self.seen_database_roots.deinit(self.allocator);
844 self.seen_relation_roots.deinit(self.allocator);
845 self.seen_relation_rows.deinit(self.allocator);
846 self.seen_pages.deinit(self.allocator);
847 self.seen_chunks.deinit(self.allocator);
848 self.seen_nodes.deinit(self.allocator);
849 self.seen_conflicts.deinit(self.allocator);
850 self.seen_conflict_roots.deinit(self.allocator);
851 self.chunks.deinit(self.allocator);
852 self.pages.deinit(self.allocator);
853 self.nodes.deinit(self.allocator);
854 self.relation_rows.deinit(self.allocator);
855 self.relation_roots.deinit(self.allocator);
856 self.conflicts.deinit(self.allocator);
857 self.conflict_roots.deinit(self.allocator);
858 self.database_roots.deinit(self.allocator);
859 self.* = undefined;
860 }
861
862 fn frameGroups(self: *const PackBuilder) [8]FrameGroup {
863 return .{
864 .{ .kind = .row_chunk, .hashes = self.chunks.items },
865 .{ .kind = .chunk_index_page, .hashes = self.pages.items },
866 .{ .kind = .tree_node, .hashes = self.nodes.items },
867 .{ .kind = .relation_rows, .hashes = self.relation_rows.items },
868 .{ .kind = .relation_root, .hashes = self.relation_roots.items },
869 .{ .kind = .conflict, .hashes = self.conflicts.items },
870 .{ .kind = .conflict_root, .hashes = self.conflict_roots.items },
871 .{ .kind = .database_root, .hashes = self.database_roots.items },
872 };
873 }
874
875 fn frameCount(self: *const PackBuilder) usize {
876 var count: usize = 0;
877 for (self.frameGroups()) |group| count += group.hashes.len;
878 return count;
879 }
880
881 fn finish(self: *PackBuilder) Error!Pack {
882 var frames: std.ArrayList(PackFrame) = .empty;
883 errdefer {
884 for (frames.items) |*frame| frame.deinit(self.allocator);
885 frames.deinit(self.allocator);
886 }
887 try frames.ensureTotalCapacity(self.allocator, self.frameCount());
888 var payload: std.ArrayList(u8) = .empty;
889 defer payload.deinit(self.allocator);
890 for (self.frameGroups()) |group| {
891 for (group.hashes) |hash| {
892 payload.clearRetainingCapacity();
893 try self.source.appendPackRecordPayload(self.allocator, &payload, group.kind, hash);
894 const owned = try self.allocator.dupe(u8, payload.items);
895 frames.appendAssumeCapacity(.{
896 .kind = group.kind,
897 .payload = owned,
898 });
899 }
900 }
901 const owned_frames = try frames.toOwnedSlice(self.allocator);
902 errdefer {
903 for (owned_frames) |*frame| frame.deinit(self.allocator);
904 if (owned_frames.len != 0) self.allocator.free(owned_frames);
905 }
906 const refs = try self.refs.toOwnedSlice(self.allocator);
907 errdefer {
908 for (refs) |*ref_value| ref_value.deinit(self.allocator);
909 if (refs.len != 0) self.allocator.free(refs);
910 }
911 const commits = try self.commits.toOwnedSlice(self.allocator);
912 return .{
913 .allocator = self.allocator,
914 .refs = refs,
915 .commits = commits,
916 .frames = owned_frames,
917 };
918 }
919
920 fn encodeBytes(self: *PackBuilder) Error![]u8 {
921 var bytes: std.ArrayList(u8) = .empty;
922 errdefer bytes.deinit(self.allocator);
923 try appendU32(self.allocator, &bytes, pack_magic);
924 try appendU32(self.allocator, &bytes, pack_format_version);
925 try appendCount(self.allocator, &bytes, self.refs.items.len);
926 try appendCount(self.allocator, &bytes, self.commits.items.len);
927 try appendCount(self.allocator, &bytes, self.frameCount());
928 for (self.refs.items) |ref_value| {
929 try appendBytes(self.allocator, &bytes, ref_value.name);
930 try appendHash(self.allocator, &bytes, ref_value.target);
931 }
932 for (self.commits.items) |commit| {
933 try appendHash(self.allocator, &bytes, commit.hash);
934 try appendHash(self.allocator, &bytes, commit.root);
935 try appendCount(self.allocator, &bytes, commit.parents.len);
936 for (commit.parents) |parent| try appendHash(self.allocator, &bytes, parent);
937 }
938 var payload: std.ArrayList(u8) = .empty;
939 defer payload.deinit(self.allocator);
940 for (self.frameGroups()) |group| {
941 for (group.hashes) |hash| {
942 payload.clearRetainingCapacity();
943 try self.source.appendPackRecordPayload(self.allocator, &payload, group.kind, hash);
944 try appendU32(self.allocator, &bytes, @backingInt(group.kind));
945 try appendBytes(self.allocator, &bytes, payload.items);
946 }
947 }
948 return try bytes.toOwnedSlice(self.allocator);
949 }
950
951 fn addRef(self: *PackBuilder, name: []const u8, target: version.Hash) Error!void {
952 if (containsRef(self.refs.items, name)) return;
953 const owned_name = try self.allocator.dupe(u8, name);
954 errdefer self.allocator.free(owned_name);
955 try self.refs.append(self.allocator, .{
956 .name = owned_name,
957 .target = target,
958 });
959 }
960
961 fn addReachableCommit(self: *PackBuilder, hash: version.Hash) Error!void {
962 var pending: std.ArrayList(version.Hash) = .empty;
963 defer pending.deinit(self.allocator);
964 try pending.append(self.allocator, hash);
965 while (pending.pop()) |next| {
966 if (self.seen_commits.contains(next)) continue;
967 try self.seen_commits.put(self.allocator, next, {});
968 if (containsHash(self.have_commits, next)) continue;
969 if (self.have) |have| {
970 if (have.hasCommit(next)) continue;
971 }
972 const commit = try self.source.commitValue(next);
973 try self.commits.ensureUnusedCapacity(self.allocator, 1);
974 const parents = try self.allocator.dupe(version.Hash, commit.parents);
975 self.commits.appendAssumeCapacity(.{
976 .hash = commit.hash,
977 .root = commit.root,
978 .parents = parents,
979 });
980 try self.addDatabaseRoot(commit.root);
981 var index = commit.parents.len;
982 while (index > 0) {
983 index -= 1;
984 try pending.append(self.allocator, commit.parents[index]);
985 }
986 }
987 }
988
989 fn addDatabaseRoot(self: *PackBuilder, hash: version.Hash) Error!void {
990 if (self.seen_database_roots.contains(hash)) return;
991 try self.seen_database_roots.put(self.allocator, hash, {});
992 if (self.have) |have| {
993 if (have.hasDatabaseRoot(hash)) return;
994 }
995 const view = self.source.databaseRootView(hash) orelse return error.DatabaseRootNotFound;
996 for (view.entries) |entry| {
997 try self.addRelationRoot(entry.hash);
998 try self.addRelationRows(entry.hash);
999 }
1000 try self.addConflictRoot(view.conflicts);
1001 try self.database_roots.append(self.allocator, hash);
1002 }
1003
1004 fn addRelationRoot(self: *PackBuilder, hash: version.Hash) Error!void {
1005 if (self.seen_relation_roots.contains(hash)) return;
1006 try self.seen_relation_roots.put(self.allocator, hash, {});
1007 if (self.have) |have| {
1008 if (have.hasRelationRoot(hash)) return;
1009 }
1010 var keys = (try self.source.relationKeysView(self.allocator, hash)) orelse return error.RelationRootNotFound;
1011 defer keys.deinit();
1012 if (keys.table_key) |key| try self.addTreeNode(key);
1013 for (keys.index_keys) |index_key| {
1014 const key = index_key orelse continue;
1015 try self.addTreeNode(key);
1016 }
1017 try self.relation_roots.append(self.allocator, hash);
1018 }
1019
1020 fn addTreeNode(self: *PackBuilder, key: version.Hash) Error!void {
1021 var stack: std.ArrayList(PackTreeTraversalEntry) = .empty;
1022 defer stack.deinit(self.allocator);
1023 try stack.append(self.allocator, .{ .key = key, .expanded = false });
1024 while (stack.pop()) |entry| {
1025 if (entry.expanded) {
1026 try self.nodes.append(self.allocator, entry.key);
1027 continue;
1028 }
1029 if (self.seen_nodes.contains(entry.key)) continue;
1030 try self.seen_nodes.put(self.allocator, entry.key, {});
1031 if (self.have) |have| {
1032 if (have.hasTreeNode(entry.key)) continue;
1033 }
1034 try stack.append(self.allocator, .{ .key = entry.key, .expanded = true });
1035 const children = try self.source.treeNodeChildren(self.allocator, entry.key);
1036 defer self.allocator.free(children);
1037 for (children) |child| try stack.append(self.allocator, .{ .key = child, .expanded = false });
1038 }
1039 }
1040
1041 fn addRelationRows(self: *PackBuilder, root: version.Hash) Error!void {
1042 if (self.seen_relation_rows.contains(root)) return;
1043 try self.seen_relation_rows.put(self.allocator, root, {});
1044 if (self.have) |have| {
1045 if (have.hasRelationRows(root)) return;
1046 }
1047 var pages = (try self.source.relationRowsPages(self.allocator, root)) orelse return error.RelationRowsNotFound;
1048 defer pages.deinit();
1049 for (pages.items) |digest| try self.addPage(digest);
1050 try self.relation_rows.append(self.allocator, root);
1051 }
1052
1053 fn addPage(self: *PackBuilder, digest: version.Hash) Error!void {
1054 if (self.seen_pages.contains(digest)) return;
1055 try self.seen_pages.put(self.allocator, digest, {});
1056 if (self.have) |have| {
1057 if (have.hasIndexPage(digest)) return;
1058 }
1059 var chunks = (try self.source.indexPageChunks(self.allocator, digest)) orelse return error.RelationRowsNotFound;
1060 defer chunks.deinit();
1061 for (chunks.items) |chunk_digest| try self.addChunk(chunk_digest);
1062 try self.pages.append(self.allocator, digest);
1063 }
1064
1065 fn addChunk(self: *PackBuilder, digest: version.Hash) Error!void {
1066 if (self.seen_chunks.contains(digest)) return;
1067 try self.seen_chunks.put(self.allocator, digest, {});
1068 if (self.have) |have| {
1069 if (have.hasRowChunk(digest)) return;
1070 }
1071 try self.chunks.append(self.allocator, digest);
1072 }
1073
1074 fn addConflictRoot(self: *PackBuilder, root_hash: version.Hash) Error!void {
1075 if (version.same(root_hash, version.ConflictRoot.empty().hash)) return;
1076 if (self.seen_conflict_roots.contains(root_hash)) return;
1077 try self.seen_conflict_roots.put(self.allocator, root_hash, {});
1078 if (self.have) |have| {
1079 if (try have.hasConflictRoot(root_hash)) return;
1080 }
1081 var artifacts = try self.source.conflictArtifacts(self.allocator, root_hash);
1082 defer artifacts.deinit();
1083 for (artifacts.artifacts) |artifact| try self.addConflict(artifact.hash);
1084 try self.conflict_roots.append(self.allocator, root_hash);
1085 }
1086
1087 fn addConflict(self: *PackBuilder, hash: version.Hash) Error!void {
1088 if (self.seen_conflicts.contains(hash)) return;
1089 try self.seen_conflicts.put(self.allocator, hash, {});
1090 if (self.have) |have| {
1091 if (have.hasConflict(hash)) return;
1092 }
1093 try self.conflicts.append(self.allocator, hash);
1094 }
1095 };
1096
1097 fn builderForRefs(allocator: Allocator, source: *const history_mod.History, refs: []const version.Ref) Error!PackBuilder {
1098 var builder = PackBuilder.init(allocator, source);
1099 errdefer builder.deinit();
1100 for (refs) |ref_value| {
1101 try builder.addRef(ref_value.name, ref_value.target);
1102 try builder.addReachableCommit(ref_value.target);
1103 }
1104 return builder;
1105 }
1106
1107 fn builderForMissingRefs(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, refs: []const version.Ref) Error!PackBuilder {
1108 var builder = PackBuilder.initMissing(allocator, source, have);
1109 errdefer builder.deinit();
1110 for (refs) |ref_value| {
1111 try builder.addRef(ref_value.name, ref_value.target);
1112 try builder.addReachableCommit(ref_value.target);
1113 }
1114 return builder;
1115 }
1116
1117 fn refsForNames(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error![]version.Ref {
1118 const refs = try allocator.alloc(version.Ref, names.len);
1119 errdefer allocator.free(refs);
1120 for (names, refs) |name, *ref_value| {
1121 ref_value.* = (try source.ref(name)) orelse return error.RefNotFound;
1122 }
1123 return refs;
1124 }
1125
1126 pub fn exportAll(allocator: Allocator, source: *const history_mod.History) Error!Pack {
1127 const refs = try source.refList(allocator);
1128 defer history_mod.freeRefList(allocator, refs);
1129 return try exportRefs(allocator, source, refs);
1130 }
1131
1132 pub fn exportRefNames(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error!Pack {
1133 const refs = try refsForNames(allocator, source, names);
1134 defer allocator.free(refs);
1135 return try exportRefs(allocator, source, refs);
1136 }
1137
1138 pub fn exportRefs(allocator: Allocator, source: *const history_mod.History, refs: []const version.Ref) Error!Pack {
1139 var builder = try builderForRefs(allocator, source, refs);
1140 defer builder.deinit();
1141 return try builder.finish();
1142 }
1143
1144 pub fn exportMissingRefs(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, refs: []const version.Ref) Error!Pack {
1145 var builder = try builderForMissingRefs(allocator, source, have, refs);
1146 defer builder.deinit();
1147 return try builder.finish();
1148 }
1149
1150 pub fn exportMissingRefNames(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, names: []const []const u8) Error!Pack {
1151 const refs = try refsForNames(allocator, source, names);
1152 defer allocator.free(refs);
1153 return try exportMissingRefs(allocator, source, have, refs);
1154 }
1155
1156 pub fn exportForFetchRequest(allocator: Allocator, source: *const history_mod.History, request: *const FetchRequest) Error!Pack {
1157 var builder = PackBuilder.initWithHaveCommits(allocator, source, request.haves);
1158 defer builder.deinit();
1159 for (request.wants) |name| {
1160 const ref_value = (try source.ref(name)) orelse return error.RefNotFound;
1161 try builder.addRef(ref_value.name, ref_value.target);
1162 try builder.addReachableCommit(ref_value.target);
1163 }
1164 return try builder.finish();
1165 }
1166
1167 pub fn exportBytesForFetchRequest(allocator: Allocator, source: *const history_mod.History, request: *const FetchRequest) Error![]u8 {
1168 var builder = PackBuilder.initWithHaveCommits(allocator, source, request.haves);
1169 defer builder.deinit();
1170 for (request.wants) |name| {
1171 const ref_value = (try source.ref(name)) orelse return error.RefNotFound;
1172 try builder.addRef(ref_value.name, ref_value.target);
1173 try builder.addReachableCommit(ref_value.target);
1174 }
1175 return try builder.encodeBytes();
1176 }
1177
1178 pub fn exportMissingBytesAll(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History) Error![]u8 {
1179 const refs = try source.refList(allocator);
1180 defer history_mod.freeRefList(allocator, refs);
1181 var builder = try builderForMissingRefs(allocator, source, have, refs);
1182 defer builder.deinit();
1183 return try builder.encodeBytes();
1184 }
1185
1186 pub fn exportMissingRefNameBytes(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, names: []const []const u8) Error![]u8 {
1187 const refs = try refsForNames(allocator, source, names);
1188 defer allocator.free(refs);
1189 var builder = try builderForMissingRefs(allocator, source, have, refs);
1190 defer builder.deinit();
1191 return try builder.encodeBytes();
1192 }
1193
1194 pub fn exportBytesAll(allocator: Allocator, source: *const history_mod.History) Error![]u8 {
1195 const refs = try source.refList(allocator);
1196 defer history_mod.freeRefList(allocator, refs);
1197 var builder = try builderForRefs(allocator, source, refs);
1198 defer builder.deinit();
1199 return try builder.encodeBytes();
1200 }
1201
1202 pub fn exportRefNameBytes(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error![]u8 {
1203 const refs = try refsForNames(allocator, source, names);
1204 defer allocator.free(refs);
1205 var builder = try builderForRefs(allocator, source, refs);
1206 defer builder.deinit();
1207 return try builder.encodeBytes();
1208 }
1209
1210 pub fn importObjects(target: *history_mod.History, pack: *const Pack) Error!Stats {
1211 var stats = Stats{};
1212 for (pack.frames) |frame| {
1213 if (try target.importPackRecord(frame.kind, frame.payload)) stats.records += 1;
1214 }
1215 for (pack.commits) |commit_object| {
1216 const commit = version.Commit.init(commit_object.root, commit_object.parents);
1217 if (!version.same(commit.hash, commit_object.hash)) return error.InvalidPack;
1218 if (try target.importPackCommit(commit_object.root, commit_object.parents)) stats.commits += 1;
1219 }
1220 try target.flushSync();
1221 return stats;
1222 }
1223
1224 pub fn importPack(target: *history_mod.History, pack: *const Pack) Error!Stats {
1225 var stats = try importObjects(target, pack);
1226 for (pack.refs) |ref_value| {
1227 try target.putRef(.{
1228 .name = ref_value.name,
1229 .target = ref_value.target,
1230 });
1231 stats.refs += 1;
1232 }
1233 return stats;
1234 }
1235
1236 pub fn importBytes(allocator: Allocator, target: *history_mod.History, bytes: []const u8) Error!Stats {
1237 var reader = ByteReader.init(bytes);
1238 var sections = try decodePackSections(allocator, &reader);
1239 defer sections.deinit(allocator);
1240 var stats = Stats{};
1241 var index: usize = 0;
1242 while (index < sections.frame_count) : (index += 1) {
1243 const kind = try decodeFrameKind(try reader.readU32());
1244 const payload = try reader.readBytes();
1245 if (try target.importPackRecord(kind, payload)) stats.records += 1;
1246 }
1247 try reader.finish();
1248 for (sections.commits) |commit_object| {
1249 const commit = version.Commit.init(commit_object.root, commit_object.parents);
1250 if (!version.same(commit.hash, commit_object.hash)) return error.InvalidPack;
1251 if (try target.importPackCommit(commit_object.root, commit_object.parents)) stats.commits += 1;
1252 }
1253 for (sections.refs) |ref_value| {
1254 try target.putRef(.{
1255 .name = ref_value.name,
1256 .target = ref_value.target,
1257 });
1258 stats.refs += 1;
1259 }
1260 try target.flushSync();
1261 return stats;
1262 }
1263
1264 pub fn headHex(hash: version.Hash) HeadHex {
1265 return std.fmt.bytesToHex(hash, .lower);
1266 }
1267
1268 pub fn packRefTarget(pack: *const Pack, ref_name: []const u8) ?version.Hash {
1269 for (pack.refs) |ref_value| {
1270 if (std.mem.eql(u8, ref_value.name, ref_name)) return ref_value.target;
1271 }
1272 return null;
1273 }
1274
1275 pub fn missingPackCounts(local: *const history_mod.History, pack: *const Pack) Error!HistoryTransferPlan {
1276 var counts = HistoryTransferPlan{};
1277 for (pack.commits) |commit| {
1278 if (!local.hasCommit(commit.hash)) counts.commits += 1;
1279 }
1280 for (pack.frames) |frame| {
1281 if (!try local.packRecordPresent(frame.kind, frame.payload)) counts.records += 1;
1282 }
1283 return counts;
1284 }
1285
1286 pub fn missingHistoryCommitCount(allocator: Allocator, source: *const history_mod.History, head: version.Hash, other: *const history_mod.History) Error!usize {
1287 var visited = std.AutoHashMap(version.Hash, void).init(allocator);
1288 defer visited.deinit();
1289 var stack: std.ArrayList(version.Hash) = .empty;
1290 defer stack.deinit(allocator);
1291 try stack.append(allocator, head);
1292
1293 var missing: usize = 0;
1294 while (stack.pop()) |hash| {
1295 if (visited.contains(hash)) continue;
1296 try visited.put(hash, {});
1297 if (other.hasCommit(hash)) continue;
1298 missing += 1;
1299 const commit = try source.commitValue(hash);
1300 for (commit.parents) |parent| try stack.append(allocator, parent);
1301 }
1302 return missing;
1303 }
1304
1305 pub fn historyRelation(allocator: Allocator, local: *const history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!HistoryRelation {
1306 const local_ref = (try local.ref(ref_name)) orelse return error.RefNotFound;
1307 const remote_ref = (try remote.ref(ref_name));
1308 const ahead = try missingHistoryCommitCount(allocator, local, local_ref.target, remote);
1309 const behind = if (remote_ref) |ref_value| try missingHistoryCommitCount(allocator, remote, ref_value.target, local) else 0;
1310 return .{
1311 .local_head = local_ref.target,
1312 .remote_head = if (remote_ref) |ref_value| ref_value.target else null,
1313 .ahead = ahead,
1314 .behind = behind,
1315 };
1316 }
1317
1318 pub fn planHistoryAdopt(allocator: Allocator, local: *const history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!HistoryTransferPlan {
1319 var names = [_][]const u8{ref_name};
1320 var pack = try exportMissingRefNames(allocator, remote, local, names[0..]);
1321 defer pack.deinit();
1322 return .{ .commits = pack.commits.len, .records = pack.frames.len };
1323 }
1324
1325 pub fn planHistoryTransfer(
1326 allocator: Allocator,
1327 local: *const history_mod.History,
1328 remote: *const history_mod.History,
1329 ref_name: []const u8,
1330 related: HistoryRelation,
1331 direction: HistoryDirection,
1332 ) HistoryPlanError!HistoryTransferPlan {
1333 if (related.upToDate()) return .{};
1334 if (related.diverged()) return error.HistoryDiverged;
1335 switch (direction) {
1336 .push => {
1337 if (related.behind != 0) return error.HistoryRemoteAhead;
1338 if (related.ahead == 0 and related.remote_head != null) return .{};
1339 var names = [_][]const u8{ref_name};
1340 var pack = try exportMissingRefNames(allocator, local, remote, names[0..]);
1341 defer pack.deinit();
1342 return .{ .commits = pack.commits.len, .records = pack.frames.len };
1343 },
1344 .pull => {
1345 if (related.remote_head == null or related.behind == 0) return .{};
1346 var names = [_][]const u8{ref_name};
1347 var pack = try exportMissingRefNames(allocator, remote, local, names[0..]);
1348 defer pack.deinit();
1349 return .{ .commits = pack.commits.len, .records = pack.frames.len };
1350 },
1351 }
1352 }
1353
1354 pub fn cloneAll(allocator: Allocator, source: *const history_mod.History, target: *history_mod.History) Error!Stats {
1355 var pack = try exportAll(allocator, source);
1356 defer pack.deinit();
1357 return try importPack(target, &pack);
1358 }
1359
1360 pub fn fetch(allocator: Allocator, local: *history_mod.History, remote: *const history_mod.History, options: FetchOptions) Error!Stats {
1361 const refs = try remote.refList(allocator);
1362 defer history_mod.freeRefList(allocator, refs);
1363 var pack = try exportMissingRefs(allocator, remote, local, refs);
1364 defer pack.deinit();
1365 var stats = try fetchPack(allocator, local, &pack, options);
1366 if (options.prune) stats.refs += try pruneRemoteTrackingRefs(allocator, local, options.remote, refs);
1367 return stats;
1368 }
1369
1370 pub fn fetchPack(allocator: Allocator, local: *history_mod.History, pack: *const Pack, options: FetchOptions) Error!Stats {
1371 var stats = try importObjects(local, pack);
1372 for (pack.refs) |ref_value| {
1373 const name = try fetchRefName(allocator, options.remote, ref_value.name);
1374 defer allocator.free(name);
1375 try local.putRef(.{
1376 .name = name,
1377 .target = ref_value.target,
1378 });
1379 stats.refs += 1;
1380 }
1381 return stats;
1382 }
1383
1384 pub fn fetchBytes(allocator: Allocator, local: *history_mod.History, bytes: []const u8, options: FetchOptions) Error!Stats {
1385 var pack = try decodePack(allocator, bytes);
1386 defer pack.deinit();
1387 return try fetchPack(allocator, local, &pack, options);
1388 }
1389
1390 pub fn pushFastForward(allocator: Allocator, local: *const history_mod.History, remote: *history_mod.History, ref_name: []const u8) Error!Stats {
1391 return try pushFastForwardTo(allocator, local, remote, ref_name, ref_name);
1392 }
1393
1394 pub fn pushFastForwardTo(allocator: Allocator, local: *const history_mod.History, remote: *history_mod.History, local_ref_name: []const u8, remote_ref_name: []const u8) Error!Stats {
1395 const ref_value = (try local.ref(local_ref_name)) orelse return error.RefNotFound;
1396 var names = [_][]const u8{local_ref_name};
1397 var pack = try exportMissingRefNames(allocator, local, remote, names[0..]);
1398 defer pack.deinit();
1399 const expected = if ((try remote.ref(remote_ref_name))) |remote_ref| remote_ref.target else null;
1400 if (expected) |remote_target| {
1401 if (!try canFastForwardWithPack(allocator, remote, &pack, remote_target, ref_value.target)) return error.NonFastForward;
1402 }
1403 var stats = try importObjects(remote, &pack);
1404 try remote.putRefIfMatches(.{
1405 .name = remote_ref_name,
1406 .target = ref_value.target,
1407 }, expected);
1408 stats.refs += 1;
1409 return stats;
1410 }
1411
1412 pub fn deleteRemoteRef(remote: *history_mod.History, ref_name: []const u8) Error!Stats {
1413 try remote.deleteRef(ref_name);
1414 return .{ .refs = 1 };
1415 }
1416
1417 pub fn pullFastForward(allocator: Allocator, local: *history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!Stats {
1418 const ref_value = (try remote.ref(ref_name)) orelse return error.RefNotFound;
1419 var names = [_][]const u8{ref_name};
1420 var pack = try exportMissingRefNames(allocator, remote, local, names[0..]);
1421 defer pack.deinit();
1422 const expected = if ((try local.ref(ref_name))) |local_ref| local_ref.target else null;
1423 if (expected) |local_target| {
1424 if (!try canFastForwardWithPack(allocator, local, &pack, local_target, ref_value.target)) return error.NonFastForward;
1425 }
1426 var stats = try importObjects(local, &pack);
1427 try local.putRefIfMatches(.{
1428 .name = ref_name,
1429 .target = ref_value.target,
1430 }, expected);
1431 stats.refs += 1;
1432 return stats;
1433 }
1434
1435 pub fn cloneFile(allocator: Allocator, remote: FileRemote, target: *history_mod.History) Error!Stats {
1436 var lock = try lockFileRemote(allocator, remote);
1437 defer lock.release();
1438 var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject });
1439 defer remote_history.deinit();
1440 return try cloneAll(allocator, &remote_history, target);
1441 }
1442
1443 pub fn fetchFile(allocator: Allocator, local: *history_mod.History, remote: FileRemote, options: FetchOptions) Error!Stats {
1444 var lock = try lockFileRemote(allocator, remote);
1445 defer lock.release();
1446 var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject });
1447 defer remote_history.deinit();
1448 return try fetch(allocator, local, &remote_history, options);
1449 }
1450
1451 pub fn pushFileFastForward(allocator: Allocator, local: *const history_mod.History, remote: FileRemote, ref_name: []const u8) Error!Stats {
1452 return try pushFileFastForwardTo(allocator, local, remote, ref_name, ref_name);
1453 }
1454
1455 pub fn pushFileFastForwardTo(allocator: Allocator, local: *const history_mod.History, remote: FileRemote, local_ref_name: []const u8, remote_ref_name: []const u8) Error!Stats {
1456 var lock = try lockFileRemote(allocator, remote);
1457 defer lock.release();
1458 var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .recovery = .reject });
1459 defer remote_history.deinit();
1460 return try pushFastForwardTo(allocator, local, &remote_history, local_ref_name, remote_ref_name);
1461 }
1462
1463 pub fn deleteFileRemoteRef(allocator: Allocator, remote: FileRemote, ref_name: []const u8) Error!Stats {
1464 var lock = try lockFileRemote(allocator, remote);
1465 defer lock.release();
1466 var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject });
1467 defer remote_history.deinit();
1468 return try deleteRemoteRef(&remote_history, ref_name);
1469 }
1470
1471 pub fn pullFileFastForward(allocator: Allocator, local: *history_mod.History, remote: FileRemote, ref_name: []const u8) Error!Stats {
1472 var lock = try lockFileRemote(allocator, remote);
1473 defer lock.release();
1474 var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject });
1475 defer remote_history.deinit();
1476 return try pullFastForward(allocator, local, &remote_history, ref_name);
1477 }
1478
1479 fn remoteTrackingName(allocator: Allocator, remote: []const u8, name: []const u8) Allocator.Error![]u8 {
1480 return try std.fmt.allocPrint(allocator, "refs/remotes/{s}/{s}", .{ remote, name });
1481 }
1482
1483 fn fetchRefName(allocator: Allocator, remote: []const u8, name: []const u8) Allocator.Error![]u8 {
1484 if (isTagRef(name)) return try allocator.dupe(u8, name);
1485 return try remoteTrackingName(allocator, remote, name);
1486 }
1487
1488 fn pruneRemoteTrackingRefs(allocator: Allocator, local: *history_mod.History, remote: []const u8, remote_refs: []const version.Ref) Error!usize {
1489 const prefix = try std.fmt.allocPrint(allocator, "refs/remotes/{s}/", .{remote});
1490 defer allocator.free(prefix);
1491 const refs = try local.refList(allocator);
1492 defer history_mod.freeRefList(allocator, refs);
1493
1494 var pruned: usize = 0;
1495 for (refs) |ref_value| {
1496 if (!std.mem.startsWith(u8, ref_value.name, prefix)) continue;
1497 const remote_ref_name = ref_value.name[prefix.len..];
1498 if (containsVersionRef(remote_refs, remote_ref_name)) continue;
1499 try local.deleteRef(ref_value.name);
1500 pruned += 1;
1501 }
1502 return pruned;
1503 }
1504
1505 fn containsVersionRef(refs: []const version.Ref, name: []const u8) bool {
1506 for (refs) |ref_value| {
1507 if (std.mem.eql(u8, ref_value.name, name)) return true;
1508 }
1509 return false;
1510 }
1511
1512 fn isTagRef(name: []const u8) bool {
1513 return std.mem.startsWith(u8, name, "refs/tags/");
1514 }
1515
1516 const ByteReader = struct {
1517 bytes_value: []const u8,
1518 cursor: usize = 0,
1519
1520 fn init(bytes: []const u8) ByteReader {
1521 return .{ .bytes_value = bytes };
1522 }
1523
1524 fn finish(self: *const ByteReader) Error!void {
1525 if (self.cursor != self.bytes_value.len) return error.InvalidPack;
1526 }
1527
1528 fn hash(self: *ByteReader) Error!version.Hash {
1529 if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidPack;
1530 const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*;
1531 self.cursor += version.hash_bytes;
1532 return value;
1533 }
1534
1535 fn readBytes(self: *ByteReader) Error![]const u8 {
1536 const len = try self.readCount();
1537 if (len > self.bytes_value.len - self.cursor) return error.InvalidPack;
1538 const value = self.bytes_value[self.cursor..][0..len];
1539 self.cursor += len;
1540 return value;
1541 }
1542
1543 fn readOwnedBytes(self: *ByteReader, allocator: Allocator) Error![]u8 {
1544 return try allocator.dupe(u8, try self.readBytes());
1545 }
1546
1547 fn readCount(self: *ByteReader) Error!usize {
1548 return @intCast(try self.readU32());
1549 }
1550
1551 fn readU32(self: *ByteReader) Error!u32 {
1552 if (4 > self.bytes_value.len - self.cursor) return error.InvalidPack;
1553 const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big);
1554 self.cursor += 4;
1555 return value;
1556 }
1557 };
1558
1559 const RemoteConfigReader = struct {
1560 bytes_value: []const u8,
1561 cursor: usize = 0,
1562
1563 fn init(bytes: []const u8) RemoteConfigReader {
1564 return .{ .bytes_value = bytes };
1565 }
1566
1567 fn finish(self: *const RemoteConfigReader) Error!void {
1568 if (self.cursor != self.bytes_value.len) return error.InvalidRemoteConfig;
1569 }
1570
1571 fn readBytes(self: *RemoteConfigReader) Error![]const u8 {
1572 const len = try self.readCount();
1573 if (len > self.bytes_value.len - self.cursor) return error.InvalidRemoteConfig;
1574 const value = self.bytes_value[self.cursor..][0..len];
1575 self.cursor += len;
1576 return value;
1577 }
1578
1579 fn readCount(self: *RemoteConfigReader) Error!usize {
1580 return @intCast(try self.readU32());
1581 }
1582
1583 fn readU8(self: *RemoteConfigReader) Error!u8 {
1584 if (self.cursor >= self.bytes_value.len) return error.InvalidRemoteConfig;
1585 const value = self.bytes_value[self.cursor];
1586 self.cursor += 1;
1587 return value;
1588 }
1589
1590 fn readU32(self: *RemoteConfigReader) Error!u32 {
1591 if (4 > self.bytes_value.len - self.cursor) return error.InvalidRemoteConfig;
1592 const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big);
1593 self.cursor += 4;
1594 return value;
1595 }
1596 };
1597
1598 const BranchUpstreamConfigReader = struct {
1599 bytes_value: []const u8,
1600 cursor: usize = 0,
1601
1602 fn init(bytes: []const u8) BranchUpstreamConfigReader {
1603 return .{ .bytes_value = bytes };
1604 }
1605
1606 fn finish(self: *const BranchUpstreamConfigReader) Error!void {
1607 if (self.cursor != self.bytes_value.len) return error.InvalidBranchUpstreamConfig;
1608 }
1609
1610 fn readBytes(self: *BranchUpstreamConfigReader) Error![]const u8 {
1611 const len = try self.readCount();
1612 if (len > self.bytes_value.len - self.cursor) return error.InvalidBranchUpstreamConfig;
1613 const value = self.bytes_value[self.cursor..][0..len];
1614 self.cursor += len;
1615 return value;
1616 }
1617
1618 fn readCount(self: *BranchUpstreamConfigReader) Error!usize {
1619 return @intCast(try self.readU32());
1620 }
1621
1622 fn readU32(self: *BranchUpstreamConfigReader) Error!u32 {
1623 if (4 > self.bytes_value.len - self.cursor) return error.InvalidBranchUpstreamConfig;
1624 const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big);
1625 self.cursor += 4;
1626 return value;
1627 }
1628 };
1629
1630 const AdvertisementReader = struct {
1631 bytes_value: []const u8,
1632 cursor: usize = 0,
1633
1634 fn init(bytes: []const u8) AdvertisementReader {
1635 return .{ .bytes_value = bytes };
1636 }
1637
1638 fn finish(self: *const AdvertisementReader) Error!void {
1639 if (self.cursor != self.bytes_value.len) return error.InvalidAdvertisement;
1640 }
1641
1642 fn hash(self: *AdvertisementReader) Error!version.Hash {
1643 if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement;
1644 const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*;
1645 self.cursor += version.hash_bytes;
1646 return value;
1647 }
1648
1649 fn readOwnedBytes(self: *AdvertisementReader, allocator: Allocator) Error![]u8 {
1650 return try allocator.dupe(u8, try self.readBytes());
1651 }
1652
1653 fn readBytes(self: *AdvertisementReader) Error![]const u8 {
1654 const len = try self.readCount();
1655 if (len > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement;
1656 const value = self.bytes_value[self.cursor..][0..len];
1657 self.cursor += len;
1658 return value;
1659 }
1660
1661 fn readCount(self: *AdvertisementReader) Error!usize {
1662 return @intCast(try self.readU32());
1663 }
1664
1665 fn readU32(self: *AdvertisementReader) Error!u32 {
1666 if (4 > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement;
1667 const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big);
1668 self.cursor += 4;
1669 return value;
1670 }
1671 };
1672
1673 const FetchRequestReader = struct {
1674 bytes_value: []const u8,
1675 cursor: usize = 0,
1676
1677 fn init(bytes: []const u8) FetchRequestReader {
1678 return .{ .bytes_value = bytes };
1679 }
1680
1681 fn finish(self: *const FetchRequestReader) Error!void {
1682 if (self.cursor != self.bytes_value.len) return error.InvalidFetchRequest;
1683 }
1684
1685 fn hash(self: *FetchRequestReader) Error!version.Hash {
1686 if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest;
1687 const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*;
1688 self.cursor += version.hash_bytes;
1689 return value;
1690 }
1691
1692 fn readOwnedBytes(self: *FetchRequestReader, allocator: Allocator) Error![]u8 {
1693 return try allocator.dupe(u8, try self.readBytes());
1694 }
1695
1696 fn readBytes(self: *FetchRequestReader) Error![]const u8 {
1697 const len = try self.readCount();
1698 if (len > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest;
1699 const value = self.bytes_value[self.cursor..][0..len];
1700 self.cursor += len;
1701 return value;
1702 }
1703
1704 fn readCount(self: *FetchRequestReader) Error!usize {
1705 return @intCast(try self.readU32());
1706 }
1707
1708 fn readU32(self: *FetchRequestReader) Error!u32 {
1709 if (4 > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest;
1710 const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big);
1711 self.cursor += 4;
1712 return value;
1713 }
1714 };
1715
1716 fn validRemoteName(name: []const u8) bool {
1717 if (name.len == 0) return false;
1718 return std.mem.indexOfAny(u8, name, " \t\n\r./\\!@#$%^&*(){}[],.<>'\"?=+|") == null;
1719 }
1720
1721 fn validateRemoteEntries(remotes: []const RemoteConfigEntry) Error!void {
1722 for (remotes, 0..) |remote, index| {
1723 if (!validRemoteName(remote.name)) return error.InvalidRemoteName;
1724 if (remote.history_path.len == 0) return error.InvalidRemoteConfig;
1725 if (remote.default_branch) |branch_name| {
1726 if (branch_name.len == 0) return error.InvalidRemoteConfig;
1727 }
1728 for (remotes[0..index]) |previous| {
1729 if (std.mem.eql(u8, previous.name, remote.name)) return error.RemoteExists;
1730 }
1731 }
1732 }
1733
1734 fn validateBranchUpstreamEntries(upstreams: []const BranchUpstreamEntry) Error!void {
1735 for (upstreams, 0..) |upstream, index| {
1736 if (upstream.branch.len == 0 or upstream.remote_branch.len == 0) return error.InvalidBranchUpstreamConfig;
1737 if (!validRemoteName(upstream.remote)) return error.InvalidRemoteName;
1738 for (upstreams[0..index]) |previous| {
1739 if (std.mem.eql(u8, previous.branch, upstream.branch)) return error.BranchUpstreamExists;
1740 }
1741 }
1742 }
1743
1744 fn appendHash(allocator: Allocator, target: *std.ArrayList(u8), hash: version.Hash) Allocator.Error!void {
1745 try target.appendSlice(allocator, hash[0..]);
1746 }
1747
1748 fn appendU8(allocator: Allocator, target: *std.ArrayList(u8), value: u8) Allocator.Error!void {
1749 try target.append(allocator, value);
1750 }
1751
1752 fn appendBytes(allocator: Allocator, target: *std.ArrayList(u8), bytes: []const u8) Error!void {
1753 if (bytes.len > std.math.maxInt(u32)) return error.InvalidPack;
1754 try appendU32(allocator, target, @intCast(bytes.len));
1755 try target.appendSlice(allocator, bytes);
1756 }
1757
1758 fn appendCount(allocator: Allocator, target: *std.ArrayList(u8), count: usize) Error!void {
1759 if (count > std.math.maxInt(u32)) return error.InvalidPack;
1760 try appendU32(allocator, target, @intCast(count));
1761 }
1762
1763 fn appendU32(allocator: Allocator, target: *std.ArrayList(u8), value: u32) Allocator.Error!void {
1764 var encoded: [4]u8 = undefined;
1765 std.mem.writeInt(u32, encoded[0..], value, .big);
1766 try target.appendSlice(allocator, encoded[0..]);
1767 }
1768
1769 fn canFastForwardWithPack(allocator: Allocator, history: *const history_mod.History, pack: *const Pack, current: version.Hash, target: version.Hash) Error!bool {
1770 const history_entries = try history.commitEntries(allocator);
1771 defer allocator.free(history_entries);
1772 var entries: std.ArrayList(branch.CommitEntry) = .empty;
1773 defer entries.deinit(allocator);
1774 try entries.appendSlice(allocator, history_entries);
1775 for (pack.commits) |commit| {
1776 try entries.append(allocator, .{
1777 .hash = commit.hash,
1778 .parents = commit.parents,
1779 });
1780 }
1781 return try branch.canFastForward(allocator, entries.items, current, target);
1782 }
1783
1784 fn containsRef(refs: []const RefObject, name: []const u8) bool {
1785 for (refs) |ref_value| {
1786 if (std.mem.eql(u8, ref_value.name, name)) return true;
1787 }
1788 return false;
1789 }
1790
1791 fn containsHash(hashes: []const version.Hash, hash: version.Hash) bool {
1792 for (hashes) |value| {
1793 if (version.same(value, hash)) return true;
1794 }
1795 return false;
1796 }
1797
1798 fn deinitConflictValue(allocator: Allocator, value: ?version.ConflictValue) void {
1799 if (value) |conflict_value| switch (conflict_value) {
1800 .row => |bytes| allocator.free(bytes),
1801 .relation => {},
1802 };
1803 }
1804
1805 fn conflictRowValue(value: ?version.ConflictValue) ?[]const u8 {
1806 const conflict_value = value orelse return null;
1807 return switch (conflict_value) {
1808 .row => |bytes| bytes,
1809 .relation => unreachable,
1810 };
1811 }
1812
1813 fn conflictRelationValue(value: ?version.ConflictValue) ?version.Hash {
1814 const conflict_value = value orelse return null;
1815 return switch (conflict_value) {
1816 .row => unreachable,
1817 .relation => |hash| hash,
1818 };
1819 }
1820
1821 fn testingHeader(sequence: u32) wal.Header {
1822 return .{
1823 .sequence = sequence,
1824 .salt = .{
1825 .first = 0x7379_6e63,
1826 .second = 0x6869_7374 + sequence,
1827 },
1828 };
1829 }
1830
1831 fn executeStatement(connection: *connection_mod.Connection, sql: []const u8) !void {
1832 var result = try connection.execute(std.testing.allocator, sql, .{ .durability = .buffered });
1833 defer result.deinit(std.testing.allocator);
1834 }
1835
1836 fn relationRows(history: *const history_mod.History, commit_hash: version.Hash, name: []const u8) ![]version.RelationRow {
1837 const commit = try history.commitValue(commit_hash);
1838 var value = try history.databaseValue(std.testing.allocator, commit.root);
1839 defer value.deinit();
1840 const relation = value.findRelation(name) orelse return error.RelationRootNotFound;
1841 return try version.cloneRelationRows(std.testing.allocator, relation.rows);
1842 }
1843
1844 fn createStore(tmp: std.testing.TmpDir, database_name: []const u8, wal_name: []const u8, history_name: []const u8, sequence: u32) !struct {
1845 database: file.Database,
1846 history: history_mod.History,
1847 } {
1848 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1849 .paths = .{ .database = database_name, .wal = wal_name },
1850 .header = testingHeader(sequence),
1851 });
1852 errdefer database.deinit();
1853 try database.reserve(.{ .wal_frames = 960 });
1854 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = history_name, .recovery = .reject });
1855 errdefer history.deinit();
1856 return .{
1857 .database = database,
1858 .history = history,
1859 };
1860 }
1861
1862 test "sync clone copies reachable history and refs" {
1863 var tmp = std.testing.tmpDir(.{});
1864 defer tmp.cleanup();
1865
1866 var source_store = try createStore(tmp, "clone-source.db", "clone-source.wal", "clone-source.history", 1);
1867 defer source_store.history.deinit();
1868 defer source_store.database.deinit();
1869
1870 var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{});
1871 defer source.deinit();
1872 try executeStatement(&source, "CREATE TABLE items (name)");
1873 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
1874 try source.stage();
1875 const main_commit = try source.commit(&source_store.history);
1876 _ = try source.createBranch(&source_store.history, "side");
1877 try source.checkoutBranch(std.testing.allocator, &source_store.history, "side");
1878 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (2, 'side')");
1879 try source.stage();
1880 const side_commit = try source.commit(&source_store.history);
1881
1882 var target_store = try createStore(tmp, "clone-target.db", "clone-target.wal", "clone-target.history", 2);
1883 defer target_store.history.deinit();
1884 defer target_store.database.deinit();
1885
1886 const stats = try cloneAll(std.testing.allocator, &source_store.history, &target_store.history);
1887 try std.testing.expectEqual(@as(usize, 2), stats.refs);
1888 try std.testing.expect(version.same(main_commit, (try target_store.history.ref("main")).?.target));
1889 try std.testing.expect(version.same(side_commit, (try target_store.history.ref("side")).?.target));
1890
1891 const main_rows = try relationRows(&target_store.history, main_commit, "items");
1892 defer version.freeRelationRows(std.testing.allocator, main_rows);
1893 try std.testing.expectEqual(@as(usize, 1), main_rows.len);
1894 const side_rows = try relationRows(&target_store.history, side_commit, "items");
1895 defer version.freeRelationRows(std.testing.allocator, side_rows);
1896 try std.testing.expectEqual(@as(usize, 2), side_rows.len);
1897 }
1898
1899 test "sync history planning reports relation and missing pack counts" {
1900 var tmp = std.testing.tmpDir(.{});
1901 defer tmp.cleanup();
1902
1903 var local_store = try createStore(tmp, "plan-local.db", "plan-local.wal", "plan-local.history", 11);
1904 defer local_store.history.deinit();
1905 defer local_store.database.deinit();
1906
1907 var remote_store = try createStore(tmp, "plan-remote.db", "plan-remote.wal", "plan-remote.history", 12);
1908 defer remote_store.history.deinit();
1909 defer remote_store.database.deinit();
1910
1911 var local = try connection_mod.Connection.create(std.testing.allocator, &local_store.database, &local_store.history, .{});
1912 defer local.deinit();
1913 try executeStatement(&local, "CREATE TABLE items (name)");
1914 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
1915 try local.stage();
1916 _ = try local.commit(&local_store.history);
1917
1918 _ = try cloneAll(std.testing.allocator, &local_store.history, &remote_store.history);
1919 const current = try historyRelation(std.testing.allocator, &local_store.history, &remote_store.history, "main");
1920 try std.testing.expect(current.upToDate());
1921 try std.testing.expectEqual(@as(usize, 0), current.ahead);
1922 try std.testing.expectEqual(@as(usize, 0), current.behind);
1923
1924 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (2, 'tip')");
1925 try local.stage();
1926 const tip = try local.commit(&local_store.history);
1927
1928 const related = try historyRelation(std.testing.allocator, &local_store.history, &remote_store.history, "main");
1929 try std.testing.expect(!related.upToDate());
1930 try std.testing.expect(!related.diverged());
1931 try std.testing.expectEqual(@as(usize, 1), related.ahead);
1932 try std.testing.expectEqual(@as(usize, 0), related.behind);
1933
1934 const push_plan = try planHistoryTransfer(std.testing.allocator, &local_store.history, &remote_store.history, "main", related, .push);
1935 try std.testing.expectEqual(@as(usize, 1), push_plan.commits);
1936 try std.testing.expect(push_plan.records != 0);
1937
1938 const pull_plan = try planHistoryTransfer(std.testing.allocator, &local_store.history, &remote_store.history, "main", related, .pull);
1939 try std.testing.expectEqual(@as(usize, 0), pull_plan.commits);
1940 try std.testing.expectEqual(@as(usize, 0), pull_plan.records);
1941
1942 var names = [_][]const u8{"main"};
1943 var pack = try exportMissingRefNames(std.testing.allocator, &local_store.history, &remote_store.history, names[0..]);
1944 defer pack.deinit();
1945 try std.testing.expect(version.same(tip, packRefTarget(&pack, "main").?));
1946
1947 const missing = try missingPackCounts(&remote_store.history, &pack);
1948 try std.testing.expectEqual(push_plan.commits, missing.commits);
1949 try std.testing.expectEqual(push_plan.records, missing.records);
1950
1951 var fresh_store = try createStore(tmp, "plan-fresh.db", "plan-fresh.wal", "plan-fresh.history", 13);
1952 defer fresh_store.history.deinit();
1953 defer fresh_store.database.deinit();
1954 const adopt_plan = try planHistoryAdopt(std.testing.allocator, &fresh_store.history, &local_store.history, "main");
1955 try std.testing.expect(adopt_plan.commits >= 2);
1956 try std.testing.expect(adopt_plan.records >= push_plan.records);
1957 }
1958
1959 test "sync record packs stay within history size" {
1960 var tmp = std.testing.tmpDir(.{});
1961 defer tmp.cleanup();
1962
1963 var store = try createStore(tmp, "bound.db", "bound.wal", "bound.history", 70);
1964 defer store.history.deinit();
1965 defer store.database.deinit();
1966
1967 var connection = try connection_mod.Connection.create(std.testing.allocator, &store.database, &store.history, .{});
1968 defer connection.deinit();
1969 try executeStatement(&connection, "CREATE TABLE items (name)");
1970 try connection.stage();
1971 _ = try connection.commit(&store.history);
1972 var sequence: usize = 0;
1973 var statement_buffer: [128]u8 = undefined;
1974 while (sequence < 24) : (sequence += 1) {
1975 const statement = try std.fmt.bufPrint(statement_buffer[0..], "INSERT INTO items (rowid, name) VALUES ({d}, 'value-{d}')", .{ sequence + 1, sequence });
1976 try executeStatement(&connection, statement);
1977 try connection.stage();
1978 _ = try connection.commit(&store.history);
1979 }
1980
1981 const bytes = try exportBytesAll(std.testing.allocator, &store.history);
1982 defer std.testing.allocator.free(bytes);
1983 try std.testing.expect(bytes.len <= store.history.len());
1984 }
1985
1986 test "sync pack import rejects tampered row chunks" {
1987 var tmp = std.testing.tmpDir(.{});
1988 defer tmp.cleanup();
1989
1990 var store = try createStore(tmp, "tamper.db", "tamper.wal", "tamper.history", 71);
1991 defer store.history.deinit();
1992 defer store.database.deinit();
1993
1994 var connection = try connection_mod.Connection.create(std.testing.allocator, &store.database, &store.history, .{});
1995 defer connection.deinit();
1996 try executeStatement(&connection, "CREATE TABLE items (name)");
1997 try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (1, 'payload')");
1998 try connection.stage();
1999 _ = try connection.commit(&store.history);
2000
2001 var pack = try exportAll(std.testing.allocator, &store.history);
2002 defer pack.deinit();
2003 var tampered = false;
2004 for (pack.frames) |frame| {
2005 if (frame.kind != .row_chunk) continue;
2006 frame.payload[frame.payload.len - 1] +%= 1;
2007 tampered = true;
2008 break;
2009 }
2010 try std.testing.expect(tampered);
2011 const bytes = try encodePack(std.testing.allocator, &pack);
2012 defer std.testing.allocator.free(bytes);
2013
2014 var target_store = try createStore(tmp, "tamper-target.db", "tamper-target.wal", "tamper-target.history", 72);
2015 defer target_store.history.deinit();
2016 defer target_store.database.deinit();
2017 try std.testing.expectError(error.InvalidHistory, importBytes(std.testing.allocator, &target_store.history, bytes));
2018 }
2019
2020 test "sync record packs re-export from imported stores" {
2021 var tmp = std.testing.tmpDir(.{});
2022 defer tmp.cleanup();
2023
2024 var source_store = try createStore(tmp, "relay-source.db", "relay-source.wal", "relay-source.history", 73);
2025 defer source_store.history.deinit();
2026 defer source_store.database.deinit();
2027
2028 var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{});
2029 defer source.deinit();
2030 try executeStatement(&source, "CREATE TABLE items (name)");
2031 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2032 try source.stage();
2033 _ = try source.commit(&source_store.history);
2034 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (2, 'tip')");
2035 try source.stage();
2036 const tip_commit = try source.commit(&source_store.history);
2037
2038 var middle_store = try createStore(tmp, "relay-middle.db", "relay-middle.wal", "relay-middle.history", 74);
2039 defer middle_store.history.deinit();
2040 defer middle_store.database.deinit();
2041 const source_bytes = try exportBytesAll(std.testing.allocator, &source_store.history);
2042 defer std.testing.allocator.free(source_bytes);
2043 _ = try importBytes(std.testing.allocator, &middle_store.history, source_bytes);
2044
2045 var final_store = try createStore(tmp, "relay-final.db", "relay-final.wal", "relay-final.history", 75);
2046 defer final_store.history.deinit();
2047 defer final_store.database.deinit();
2048 const middle_bytes = try exportBytesAll(std.testing.allocator, &middle_store.history);
2049 defer std.testing.allocator.free(middle_bytes);
2050 _ = try importBytes(std.testing.allocator, &final_store.history, middle_bytes);
2051
2052 try std.testing.expect(version.same(tip_commit, (try final_store.history.ref("main")).?.target));
2053 const rows = try relationRows(&final_store.history, tip_commit, "items");
2054 defer version.freeRelationRows(std.testing.allocator, rows);
2055 try std.testing.expectEqual(@as(usize, 2), rows.len);
2056 }
2057
2058 test "sync fetch writes remote tracking refs" {
2059 var tmp = std.testing.tmpDir(.{});
2060 defer tmp.cleanup();
2061
2062 var remote_store = try createStore(tmp, "fetch-remote.db", "fetch-remote.wal", "fetch-remote.history", 10);
2063 defer remote_store.history.deinit();
2064 defer remote_store.database.deinit();
2065
2066 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2067 defer remote.deinit();
2068 try executeStatement(&remote, "CREATE TABLE items (name)");
2069 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'remote')");
2070 try remote.stage();
2071 const remote_commit = try remote.commit(&remote_store.history);
2072 try remote_store.history.putRef(.{
2073 .name = "refs/tags/v1",
2074 .target = remote_commit,
2075 });
2076 try remote_store.history.putRef(.{
2077 .name = "side",
2078 .target = remote_commit,
2079 });
2080
2081 var local_store = try createStore(tmp, "fetch-local.db", "fetch-local.wal", "fetch-local.history", 11);
2082 defer local_store.history.deinit();
2083 defer local_store.database.deinit();
2084
2085 const stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream" });
2086 try std.testing.expectEqual(@as(usize, 3), stats.refs);
2087 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/main")).?.target));
2088 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/side")).?.target));
2089 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/tags/v1")).?.target));
2090 try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/refs/tags/v1")) == null);
2091 const rows = try relationRows(&local_store.history, remote_commit, "items");
2092 defer version.freeRelationRows(std.testing.allocator, rows);
2093 try std.testing.expectEqual(@as(usize, 1), rows.len);
2094
2095 try remote_store.history.deleteRef("side");
2096 const stale_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream" });
2097 try std.testing.expectEqual(@as(usize, 2), stale_stats.refs);
2098 try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/side")) != null);
2099 const prune_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream", .prune = true });
2100 try std.testing.expectEqual(@as(usize, 3), prune_stats.refs);
2101 try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/side")) == null);
2102 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/main")).?.target));
2103 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/tags/v1")).?.target));
2104 }
2105
2106 test "sync fetch and push transfer only missing objects" {
2107 var tmp = std.testing.tmpDir(.{});
2108 defer tmp.cleanup();
2109
2110 var remote_store = try createStore(tmp, "missing-remote.db", "missing-remote.wal", "missing-remote.history", 11);
2111 defer remote_store.history.deinit();
2112 defer remote_store.database.deinit();
2113
2114 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2115 defer remote.deinit();
2116 try executeStatement(&remote, "CREATE TABLE items (name)");
2117 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2118 try remote.stage();
2119 _ = try remote.commit(&remote_store.history);
2120
2121 var local_store = try createStore(tmp, "missing-local.db", "missing-local.wal", "missing-local.history", 12);
2122 defer local_store.history.deinit();
2123 defer local_store.database.deinit();
2124 _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history);
2125
2126 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')");
2127 try remote.stage();
2128 const remote_commit = try remote.commit(&remote_store.history);
2129
2130 const fetch_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "origin" });
2131 try std.testing.expectEqual(@as(usize, 1), fetch_stats.refs);
2132 try std.testing.expectEqual(@as(usize, 1), fetch_stats.commits);
2133 try std.testing.expect(fetch_stats.records != 0);
2134 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target));
2135
2136 const fetch_again = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "origin" });
2137 try std.testing.expectEqual(@as(usize, 1), fetch_again.refs);
2138 try std.testing.expectEqual(@as(usize, 0), fetch_again.commits);
2139 try std.testing.expectEqual(@as(usize, 0), fetch_again.records);
2140
2141 try local_store.history.fastForwardBranch(std.testing.allocator, "main", remote_commit);
2142 var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{});
2143 defer local.deinit();
2144 try local.checkoutBranch(std.testing.allocator, &local_store.history, "main");
2145 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local')");
2146 try local.stage();
2147 const local_commit = try local.commit(&local_store.history);
2148
2149 const push_stats = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main");
2150 try std.testing.expectEqual(@as(usize, 1), push_stats.refs);
2151 try std.testing.expectEqual(@as(usize, 1), push_stats.commits);
2152 try std.testing.expect(push_stats.records != 0);
2153 try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("main")).?.target));
2154 }
2155
2156 test "sync negotiated fetch advertises refs and returns requested missing pack" {
2157 var tmp = std.testing.tmpDir(.{});
2158 defer tmp.cleanup();
2159
2160 var remote_store = try createStore(tmp, "negotiated-remote.db", "negotiated-remote.wal", "negotiated-remote.history", 12);
2161 defer remote_store.history.deinit();
2162 defer remote_store.database.deinit();
2163
2164 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2165 defer remote.deinit();
2166 try executeStatement(&remote, "CREATE TABLE items (name)");
2167 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2168 try remote.stage();
2169 _ = try remote.commit(&remote_store.history);
2170
2171 var local_store = try createStore(tmp, "negotiated-local.db", "negotiated-local.wal", "negotiated-local.history", 13);
2172 defer local_store.history.deinit();
2173 defer local_store.database.deinit();
2174 _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history);
2175
2176 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')");
2177 try remote.stage();
2178 const remote_commit = try remote.commit(&remote_store.history);
2179
2180 var advertisement = try advertiseRefs(std.testing.allocator, &remote_store.history);
2181 defer advertisement.deinit();
2182 const advertisement_bytes = try encodeAdvertisement(std.testing.allocator, &advertisement);
2183 defer std.testing.allocator.free(advertisement_bytes);
2184 var decoded_advertisement = try decodeAdvertisement(std.testing.allocator, advertisement_bytes);
2185 defer decoded_advertisement.deinit();
2186 try std.testing.expect(version.same(remote_commit, decoded_advertisement.ref("main").?.target));
2187
2188 const wants = [_][]const u8{"main"};
2189 var request = try fetchRequestFromHistory(std.testing.allocator, &local_store.history, wants[0..]);
2190 defer request.deinit();
2191 const request_bytes = try encodeFetchRequest(std.testing.allocator, &request);
2192 defer std.testing.allocator.free(request_bytes);
2193 var decoded_request = try decodeFetchRequest(std.testing.allocator, request_bytes);
2194 defer decoded_request.deinit();
2195
2196 var pack = try exportForFetchRequest(std.testing.allocator, &remote_store.history, &decoded_request);
2197 defer pack.deinit();
2198 try std.testing.expectEqual(@as(usize, 1), pack.refs.len);
2199 try std.testing.expectEqual(@as(usize, 1), pack.commits.len);
2200 try std.testing.expect(pack.frames.len != 0);
2201
2202 const pack_bytes = try exportBytesForFetchRequest(std.testing.allocator, &remote_store.history, &decoded_request);
2203 defer std.testing.allocator.free(pack_bytes);
2204 _ = try fetchBytes(std.testing.allocator, &local_store.history, pack_bytes, .{ .remote = "origin" });
2205 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target));
2206
2207 const broken_advertisement = try std.testing.allocator.dupe(u8, advertisement_bytes);
2208 defer std.testing.allocator.free(broken_advertisement);
2209 broken_advertisement[0] ^= 0xff;
2210 try std.testing.expectError(error.InvalidAdvertisement, decodeAdvertisement(std.testing.allocator, broken_advertisement));
2211
2212 const broken_request = try std.testing.allocator.dupe(u8, request_bytes);
2213 defer std.testing.allocator.free(broken_request);
2214 broken_request[0] ^= 0xff;
2215 try std.testing.expectError(error.InvalidFetchRequest, decodeFetchRequest(std.testing.allocator, broken_request));
2216 }
2217
2218 test "sync byte packs import and fetch remote refs" {
2219 var tmp = std.testing.tmpDir(.{});
2220 defer tmp.cleanup();
2221
2222 var remote_store = try createStore(tmp, "byte-remote.db", "byte-remote.wal", "byte-remote.history", 12);
2223 defer remote_store.history.deinit();
2224 defer remote_store.database.deinit();
2225
2226 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2227 defer remote.deinit();
2228 try executeStatement(&remote, "CREATE TABLE items (name)");
2229 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'remote')");
2230 try remote.stage();
2231 const remote_commit = try remote.commit(&remote_store.history);
2232
2233 const bytes = try exportBytesAll(std.testing.allocator, &remote_store.history);
2234 defer std.testing.allocator.free(bytes);
2235
2236 var imported_store = try createStore(tmp, "byte-imported.db", "byte-imported.wal", "byte-imported.history", 13);
2237 defer imported_store.history.deinit();
2238 defer imported_store.database.deinit();
2239 _ = try importBytes(std.testing.allocator, &imported_store.history, bytes);
2240 try std.testing.expect(version.same(remote_commit, (try imported_store.history.ref("main")).?.target));
2241 const imported_rows = try relationRows(&imported_store.history, remote_commit, "items");
2242 defer version.freeRelationRows(std.testing.allocator, imported_rows);
2243 try std.testing.expectEqual(@as(usize, 1), imported_rows.len);
2244
2245 var fetched_store = try createStore(tmp, "byte-fetched.db", "byte-fetched.wal", "byte-fetched.history", 14);
2246 defer fetched_store.history.deinit();
2247 defer fetched_store.database.deinit();
2248 _ = try fetchBytes(std.testing.allocator, &fetched_store.history, bytes, .{ .remote = "origin" });
2249 try std.testing.expect(version.same(remote_commit, (try fetched_store.history.ref("refs/remotes/origin/main")).?.target));
2250
2251 const broken = try std.testing.allocator.dupe(u8, bytes);
2252 defer std.testing.allocator.free(broken);
2253 broken[0] ^= 0xff;
2254 try std.testing.expectError(error.InvalidPack, importBytes(std.testing.allocator, &fetched_store.history, broken));
2255 }
2256
2257 test "sync remote config stores named file remotes" {
2258 var tmp = std.testing.tmpDir(.{});
2259 defer tmp.cleanup();
2260
2261 var origin = try RemoteConfigEntry.init(std.testing.allocator, "origin", "origin.history", null);
2262 defer origin.deinit(std.testing.allocator);
2263 var backup = try RemoteConfigEntry.initWithDefaultBranch(std.testing.allocator, "backup", "backup.history", "backup.lock", "main");
2264 defer backup.deinit(std.testing.allocator);
2265 const entries = [_]RemoteConfigEntry{ origin, backup };
2266 var config = try RemoteConfig.init(std.testing.allocator, entries[0..]);
2267 defer config.deinit();
2268
2269 try writeRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "remotes.bin" }, &config);
2270 var read = try readRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "remotes.bin" });
2271 defer read.deinit();
2272 try std.testing.expectEqual(@as(usize, 2), read.remotes.len);
2273 try std.testing.expectEqualStrings("origin.history", read.find("origin").?.history_path);
2274 try std.testing.expect(read.find("origin").?.default_branch == null);
2275 try std.testing.expect(read.find("missing") == null);
2276
2277 const remote = try read.fileRemote(testing_io, tmp.dir, "backup");
2278 try std.testing.expectEqualStrings("backup.history", remote.path);
2279 try std.testing.expectEqualStrings("backup.lock", remote.lock_path.?);
2280 try std.testing.expectEqualStrings("main", read.find("backup").?.default_branch.?);
2281 try std.testing.expectError(error.RemoteNotFound, read.fileRemote(testing_io, tmp.dir, "missing"));
2282
2283 const duplicate = [_]RemoteConfigEntry{ origin, origin };
2284 try std.testing.expectError(error.RemoteExists, RemoteConfig.init(std.testing.allocator, duplicate[0..]));
2285 try std.testing.expectError(error.InvalidRemoteName, RemoteConfigEntry.init(std.testing.allocator, "bad/name", "bad.history", null));
2286 try std.testing.expectError(error.InvalidRemoteConfig, RemoteConfigEntry.initWithDefaultBranch(std.testing.allocator, "bad-branch", "bad.history", null, ""));
2287
2288 var empty = try readRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "missing-remotes.bin" });
2289 defer empty.deinit();
2290 try std.testing.expectEqual(@as(usize, 0), empty.remotes.len);
2291
2292 var legacy_bytes: std.ArrayList(u8) = .empty;
2293 defer legacy_bytes.deinit(std.testing.allocator);
2294 try appendU32(std.testing.allocator, &legacy_bytes, remote_config_magic);
2295 try appendU32(std.testing.allocator, &legacy_bytes, 1);
2296 try appendCount(std.testing.allocator, &legacy_bytes, 1);
2297 try appendBytes(std.testing.allocator, &legacy_bytes, "legacy");
2298 try appendBytes(std.testing.allocator, &legacy_bytes, "legacy.history");
2299 try appendU8(std.testing.allocator, &legacy_bytes, 0);
2300 var legacy = try decodeRemoteConfig(std.testing.allocator, legacy_bytes.items);
2301 defer legacy.deinit();
2302 try std.testing.expectEqualStrings("legacy.history", legacy.find("legacy").?.history_path);
2303 try std.testing.expect(legacy.find("legacy").?.default_branch == null);
2304 }
2305
2306 test "sync branch upstream config stores tracking metadata" {
2307 var tmp = std.testing.tmpDir(.{});
2308 defer tmp.cleanup();
2309
2310 var main_origin = try BranchUpstreamEntry.init(std.testing.allocator, "main", "origin", "trunk");
2311 defer main_origin.deinit(std.testing.allocator);
2312 var feature_backup = try BranchUpstreamEntry.init(std.testing.allocator, "feature", "backup", "feature");
2313 defer feature_backup.deinit(std.testing.allocator);
2314 const entries = [_]BranchUpstreamEntry{ main_origin, feature_backup };
2315 var config = try BranchUpstreamConfig.init(std.testing.allocator, entries[0..]);
2316 defer config.deinit();
2317
2318 try writeBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "upstreams.bin" }, &config);
2319 var read = try readBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "upstreams.bin" });
2320 defer read.deinit();
2321 try std.testing.expectEqual(@as(usize, 2), read.upstreams.len);
2322 try std.testing.expectEqualStrings("origin", read.find("main").?.remote);
2323 try std.testing.expectEqualStrings("trunk", read.find("main").?.remote_branch);
2324 try std.testing.expectEqualStrings("backup", read.find("feature").?.remote);
2325 try std.testing.expect(read.find("missing") == null);
2326
2327 var duplicate = try BranchUpstreamEntry.init(std.testing.allocator, "main", "backup", "main");
2328 defer duplicate.deinit(std.testing.allocator);
2329 const duplicate_entries = [_]BranchUpstreamEntry{ main_origin, duplicate };
2330 try std.testing.expectError(error.BranchUpstreamExists, BranchUpstreamConfig.init(std.testing.allocator, duplicate_entries[0..]));
2331 try std.testing.expectError(error.InvalidRemoteName, BranchUpstreamEntry.init(std.testing.allocator, "main", "bad/name", "main"));
2332 try std.testing.expectError(error.InvalidBranchUpstreamConfig, BranchUpstreamEntry.init(std.testing.allocator, "", "origin", "main"));
2333 try std.testing.expectError(error.InvalidBranchUpstreamConfig, BranchUpstreamEntry.init(std.testing.allocator, "main", "origin", ""));
2334
2335 var empty = try readBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "missing-upstreams.bin" });
2336 defer empty.deinit();
2337 try std.testing.expectEqual(@as(usize, 0), empty.upstreams.len);
2338
2339 const bytes = try encodeBranchUpstreamConfig(std.testing.allocator, &config);
2340 defer std.testing.allocator.free(bytes);
2341 const broken = try std.testing.allocator.dupe(u8, bytes);
2342 defer std.testing.allocator.free(broken);
2343 broken[0] ^= 0xff;
2344 try std.testing.expectError(error.InvalidBranchUpstreamConfig, decodeBranchUpstreamConfig(std.testing.allocator, broken));
2345 }
2346
2347 test "sync file remote lock rejects concurrent access" {
2348 var tmp = std.testing.tmpDir(.{});
2349 defer tmp.cleanup();
2350
2351 const remote = FileRemote{
2352 .dir = tmp.dir,
2353 .path = "locked.history",
2354 };
2355 var first = try lockFileRemote(std.testing.allocator, remote);
2356 try std.testing.expectError(error.RemoteBusy, lockFileRemote(std.testing.allocator, remote));
2357 first.release();
2358 var second = try lockFileRemote(std.testing.allocator, remote);
2359 second.release();
2360 }
2361
2362 test "sync file transfers require an existing remote history" {
2363 var tmp = std.testing.tmpDir(.{});
2364 defer tmp.cleanup();
2365
2366 const remote = FileRemote{
2367 .dir = tmp.dir,
2368 .path = "absent.history",
2369 };
2370
2371 var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "transfer-target.history", .recovery = .reject });
2372 defer target.deinit();
2373 try std.testing.expectError(error.HistoryNotFound, cloneFile(std.testing.allocator, remote, &target));
2374 try std.testing.expectError(error.HistoryNotFound, fetchFile(std.testing.allocator, &target, remote, .{}));
2375 try std.testing.expectError(error.HistoryNotFound, pullFileFastForward(std.testing.allocator, &target, remote, "main"));
2376 try std.testing.expectError(error.HistoryNotFound, deleteFileRemoteRef(std.testing.allocator, remote, "main"));
2377 try std.testing.expectError(error.FileNotFound, tmp.dir.readFileAlloc(testing_io, "absent.history", std.testing.allocator, .unlimited));
2378 }
2379
2380 test "sync file remotes reject truncated history without repairing it" {
2381 var tmp = std.testing.tmpDir(.{});
2382 defer tmp.cleanup();
2383
2384 var clean_length: usize = 0;
2385 {
2386 var remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{
2387 .path = "truncated-remote.history",
2388 .recovery = .reject,
2389 });
2390 defer remote.deinit();
2391 try remote.putCommit(version.Commit.init(version.emptyHash("truncated-remote"), &.{}));
2392 clean_length = remote.len();
2393 }
2394 const corrupted_length = clean_length + "corrupt".len;
2395 {
2396 var remote_file = try tmp.dir.createFile(testing_io, "truncated-remote.history", .{ .read = true, .truncate = false });
2397 try remote_file.writePositionalAll(testing_io, "corrupt", clean_length);
2398 try remote_file.setLength(testing_io, corrupted_length);
2399 remote_file.close(testing_io);
2400 }
2401
2402 var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{
2403 .path = "truncated-target.history",
2404 .recovery = .reject,
2405 });
2406 defer target.deinit();
2407 try std.testing.expectError(error.TruncatedHistory, cloneFile(std.testing.allocator, .{
2408 .dir = tmp.dir,
2409 .path = "truncated-remote.history",
2410 }, &target));
2411 try std.testing.expectEqual(corrupted_length, @as(usize, @intCast((try tmp.dir.statFile(testing_io, "truncated-remote.history", .{})).size)));
2412 }
2413
2414 test "sync clone copies committed conflict roots" {
2415 var tmp = std.testing.tmpDir(.{});
2416 defer tmp.cleanup();
2417
2418 var source_store = try createStore(tmp, "conflict-source.db", "conflict-source.wal", "conflict-source.history", 15);
2419 defer source_store.history.deinit();
2420 defer source_store.database.deinit();
2421
2422 var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{});
2423 defer source.deinit();
2424 try executeStatement(&source, "CREATE TABLE items (name)");
2425 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2426 try source.stage();
2427 _ = try source.commit(&source_store.history);
2428
2429 _ = try source.createBranch(&source_store.history, "side");
2430 try source.checkoutBranch(std.testing.allocator, &source_store.history, "side");
2431 try executeStatement(&source, "DELETE FROM items WHERE rowid = 1");
2432 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'side')");
2433 try source.stage();
2434 const side_commit = try source.commit(&source_store.history);
2435
2436 try source.checkoutBranch(std.testing.allocator, &source_store.history, "main");
2437 try executeStatement(&source, "DELETE FROM items WHERE rowid = 1");
2438 try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'main')");
2439 try source.stage();
2440 _ = try source.commit(&source_store.history);
2441
2442 var merged = try source.mergeBranch(std.testing.allocator, &source_store.history, "side", .{});
2443 defer merged.deinit();
2444 try std.testing.expect(merged.hasConflicts());
2445 const conflict_root = merged.conflict_root.hash;
2446 const conflict_hash = merged.artifacts[0].hash;
2447 try source.stage();
2448 const merge_commit = try source.mergeCommit(&source_store.history, side_commit);
2449
2450 var target_store = try createStore(tmp, "conflict-target.db", "conflict-target.wal", "conflict-target.history", 16);
2451 defer target_store.history.deinit();
2452 defer target_store.database.deinit();
2453
2454 _ = try cloneAll(std.testing.allocator, &source_store.history, &target_store.history);
2455 try std.testing.expect(version.same(merge_commit, (try target_store.history.ref("main")).?.target));
2456 const commit = try target_store.history.commitValue(merge_commit);
2457 var clone = try connection_mod.Connection.open(std.testing.allocator, &target_store.database, &target_store.history, .{});
2458 defer clone.deinit();
2459 try clone.checkoutBranch(std.testing.allocator, &target_store.history, "main");
2460 try std.testing.expect(version.same(commit.root, (try clone.workingRoot())));
2461 var artifacts = try clone.conflictArtifacts(std.testing.allocator, &target_store.history);
2462 defer artifacts.deinit();
2463 try std.testing.expect(version.same(conflict_root, artifacts.root.hash));
2464 try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len);
2465 try std.testing.expect(version.same(conflict_hash, artifacts.artifacts[0].hash));
2466 }
2467
2468 test "sync pack import rejects a conflict root without its artifact" {
2469 var tmp = std.testing.tmpDir(.{});
2470 defer tmp.cleanup();
2471
2472 var source = try history_mod.History.open(std.testing.allocator, tmp.dir, .{
2473 .path = "missing-conflict-source.history",
2474 .recovery = .reject,
2475 });
2476 defer source.deinit();
2477 const artifact = version.ConflictArtifact.init("items", 1, "base", "ours", "theirs");
2478 try source.putConflict(artifact);
2479 const root = try source.putConflictRoot(&.{artifact.entry()});
2480 var payload: std.ArrayList(u8) = .empty;
2481 defer payload.deinit(std.testing.allocator);
2482 try source.appendPackRecordPayload(std.testing.allocator, &payload, .conflict_root, root.hash);
2483 var frames = [_]PackFrame{.{ .kind = .conflict_root, .payload = payload.items }};
2484 const incomplete = Pack{
2485 .allocator = std.testing.allocator,
2486 .refs = &.{},
2487 .commits = &.{},
2488 .frames = frames[0..],
2489 };
2490
2491 var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{
2492 .path = "missing-conflict-target.history",
2493 .recovery = .reject,
2494 });
2495 defer target.deinit();
2496 try std.testing.expectError(error.InvalidHistory, importObjects(&target, &incomplete));
2497 }
2498
2499 test "sync push fast forwards remote refs and rejects divergence" {
2500 var tmp = std.testing.tmpDir(.{});
2501 defer tmp.cleanup();
2502
2503 var remote_store = try createStore(tmp, "push-remote.db", "push-remote.wal", "push-remote.history", 20);
2504 defer remote_store.history.deinit();
2505 defer remote_store.database.deinit();
2506
2507 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2508 defer remote.deinit();
2509 try executeStatement(&remote, "CREATE TABLE items (name)");
2510 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2511 try remote.stage();
2512 _ = try remote.commit(&remote_store.history);
2513
2514 var local_store = try createStore(tmp, "push-local.db", "push-local.wal", "push-local.history", 21);
2515 defer local_store.history.deinit();
2516 defer local_store.database.deinit();
2517 _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history);
2518
2519 var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{});
2520 defer local.deinit();
2521 try local.checkoutBranch(std.testing.allocator, &local_store.history, "main");
2522 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (2, 'local')");
2523 try local.stage();
2524 const local_commit = try local.commit(&local_store.history);
2525
2526 _ = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main");
2527 try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("main")).?.target));
2528 _ = try pushFastForwardTo(std.testing.allocator, &local_store.history, &remote_store.history, "main", "trunk");
2529 try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("trunk")).?.target));
2530 try local_store.history.putRef(.{
2531 .name = "refs/tags/v1",
2532 .target = local_commit,
2533 });
2534 _ = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "refs/tags/v1");
2535 try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("refs/tags/v1")).?.target));
2536 const pushed_rows = try relationRows(&remote_store.history, local_commit, "items");
2537 defer version.freeRelationRows(std.testing.allocator, pushed_rows);
2538 try std.testing.expectEqual(@as(usize, 2), pushed_rows.len);
2539
2540 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local-again')");
2541 try local.stage();
2542 const local_divergent = try local.commit(&local_store.history);
2543
2544 try remote.checkoutBranch(std.testing.allocator, &remote_store.history, "main");
2545 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (4, 'remote')");
2546 try remote.stage();
2547 const remote_commit = try remote.commit(&remote_store.history);
2548
2549 try std.testing.expectError(error.NonFastForward, pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main"));
2550 try std.testing.expect(version.same(remote_commit, (try remote_store.history.ref("main")).?.target));
2551 try std.testing.expectError(error.CommitNotFound, remote_store.history.commitValue(local_divergent));
2552 }
2553
2554 test "sync file remotes clone fetch push and pull refs" {
2555 var tmp = std.testing.tmpDir(.{});
2556 defer tmp.cleanup();
2557
2558 var remote_store = try createStore(tmp, "file-remote.db", "file-remote.wal", "file-remote.history", 30);
2559 defer remote_store.history.deinit();
2560 defer remote_store.database.deinit();
2561
2562 var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{});
2563 defer remote.deinit();
2564 try executeStatement(&remote, "CREATE TABLE items (name)");
2565 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2566 try remote.stage();
2567 const base_commit = try remote.commit(&remote_store.history);
2568
2569 var local_store = try createStore(tmp, "file-local.db", "file-local.wal", "file-local.history", 31);
2570 defer local_store.history.deinit();
2571 defer local_store.database.deinit();
2572
2573 const remote_file = FileRemote{
2574 .dir = tmp.dir,
2575 .path = "file-remote.history",
2576 };
2577 _ = try cloneFile(std.testing.allocator, remote_file, &local_store.history);
2578 try std.testing.expect(version.same(base_commit, (try local_store.history.ref("main")).?.target));
2579
2580 try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')");
2581 try remote.stage();
2582 const remote_commit = try remote.commit(&remote_store.history);
2583
2584 _ = try fetchFile(std.testing.allocator, &local_store.history, remote_file, .{ .remote = "origin" });
2585 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target));
2586
2587 _ = try pullFileFastForward(std.testing.allocator, &local_store.history, remote_file, "main");
2588 try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("main")).?.target));
2589
2590 var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{});
2591 defer local.deinit();
2592 try local.checkoutBranch(std.testing.allocator, &local_store.history, "main");
2593 try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local')");
2594 try local.stage();
2595 const local_commit = try local.commit(&local_store.history);
2596
2597 _ = try pushFileFastForward(std.testing.allocator, &local_store.history, remote_file, "main");
2598 {
2599 var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject });
2600 defer verified_remote.deinit();
2601 try std.testing.expect(version.same(local_commit, (try verified_remote.ref("main")).?.target));
2602
2603 const rows = try relationRows(&verified_remote, local_commit, "items");
2604 defer version.freeRelationRows(std.testing.allocator, rows);
2605 try std.testing.expectEqual(@as(usize, 3), rows.len);
2606 }
2607
2608 try local_store.history.putRef(.{
2609 .name = "refs/tags/file-v1",
2610 .target = local_commit,
2611 });
2612 _ = try pushFileFastForward(std.testing.allocator, &local_store.history, remote_file, "refs/tags/file-v1");
2613 {
2614 var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject });
2615 defer verified_remote.deinit();
2616 try std.testing.expect(version.same(local_commit, (try verified_remote.ref("refs/tags/file-v1")).?.target));
2617 }
2618
2619 const delete_stats = try deleteFileRemoteRef(std.testing.allocator, remote_file, "refs/tags/file-v1");
2620 try std.testing.expectEqual(@as(usize, 1), delete_stats.refs);
2621 {
2622 var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject });
2623 defer verified_remote.deinit();
2624 try std.testing.expect((try verified_remote.ref("refs/tags/file-v1")) == null);
2625 }
2626 try std.testing.expectError(error.RefNotFound, deleteFileRemoteRef(std.testing.allocator, remote_file, "refs/tags/file-v1"));
2627 }