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 }