tiny.sql.sync
Defined in tiny.sql.
API (76)
Actions
Public operations.
HistoryRelation.divergedHistoryRelation.upToDateadvertiseRefscloneAllcloneFiledecodeAdvertisementdecodeBranchUpstreamConfigdecodeFetchRequestdecodePackdecodeRemoteConfigdeleteFileRemoteRefdeleteRemoteRefencodeAdvertisementencodeBranchUpstreamConfigencodeFetchRequestencodePackencodeRemoteConfigexportAllexportBytesAllexportBytesForFetchRequestexportForFetchRequestexportMissingBytesAllexportMissingRefNameBytesexportMissingRefNamesexportMissingRefsexportRefNameBytesexportRefNamesexportRefsfetchfetchBytesfetchFilefetchPackfetchRequestFromHistoryheadHexhistoryRelationimportBytesimportObjectsimportPacklockFileRemotemissingHistoryCommitCountmissingPackCountspackRefTargetplanHistoryAdoptplanHistoryTransferpullFastForwardpullFileFastForwardpushFastForwardpushFastForwardTopushFileFastForwardpushFileFastForwardToreadBranchUpstreamConfigreadRemoteConfigwriteBranchUpstreamConfigwriteRemoteConfig
Types and contracts
Public types and contracts.
AdvertisementBranchUpstreamConfigBranchUpstreamEntryBranchUpstreamOptionsCommitObjectErrorFetchOptionsFetchRequestFileRemoteFileRemoteLockHeadHexHistoryDirectionHistoryPlanErrorHistoryRelationHistoryTransferPlanPackPackFrameRefObjectRemoteConfigRemoteConfigEntryRemoteConfigOptionsStats
Source
Source: lib/sql/src/root.zig:44
zig
pub const sync = @import("sync.zig");Source: lib/sql/src/sync.zig
zig
const std = @import("std");const branch = @import("branch.zig");const connection_mod = @import("connection.zig");const file = @import("file.zig");const history_mod = @import("history/root.zig");const version = @import("version.zig");const wal = @import("wal.zig");const Allocator = std.mem.Allocator;const testing_io = std.Options.debug_io;const pack_magic: u32 = 0x7473_706b;const pack_format_version: u32 = 1;const remote_config_magic: u32 = 0x7473_726d;const remote_config_format_version: u32 = 2;const branch_upstream_config_magic: u32 = 0x7473_7570;const branch_upstream_config_format_version: u32 = 1;const advertisement_magic: u32 = 0x7473_6164;const advertisement_format_version: u32 = 1;const fetch_request_magic: u32 = 0x7473_6672;const fetch_request_format_version: u32 = 1;pub const Error = Allocator.Error || history_mod.Error || branch.Error || error{ InvalidPack, InvalidRemoteConfig, InvalidBranchUpstreamConfig, InvalidAdvertisement, InvalidFetchRequest, InvalidRemoteName, RemoteExists, RemoteNotFound, BranchUpstreamExists, RemoteBusy,};pub const Stats = struct { refs: usize = 0, commits: usize = 0, records: usize = 0,};pub const HeadHex = [version.hash_bytes * 2]u8;pub const FetchOptions = struct { remote: []const u8 = "origin", prune: bool = false,};pub const HistoryDirection = enum { push, pull };pub const HistoryRelation = struct { local_head: version.Hash, remote_head: ?version.Hash, ahead: usize, behind: usize, pub fn upToDate(self: HistoryRelation) bool { const remote_head = self.remote_head orelse return false; return version.same(self.local_head, remote_head); } pub fn diverged(self: HistoryRelation) bool { return self.ahead != 0 and self.behind != 0; }};pub const HistoryTransferPlan = struct { commits: usize = 0, records: usize = 0,};pub const HistoryPlanError = Error || error{ HistoryDiverged, HistoryRemoteAhead,};pub const FileRemote = struct { io: std.Io = std.Options.debug_io, dir: std.Io.Dir, path: []const u8 = "tiny.sql.history", lock_path: ?[]const u8 = null,};pub const RemoteConfigOptions = struct { io: std.Io = std.Options.debug_io, path: []const u8 = "tiny.sql.remotes", max_bytes: usize = 64 * 1024,};pub const BranchUpstreamOptions = struct { io: std.Io = std.Options.debug_io, path: []const u8 = "tiny.sql.upstreams", max_bytes: usize = 64 * 1024,};pub const RemoteConfigEntry = struct { name: []u8, history_path: []u8, lock_path: ?[]u8 = null, default_branch: ?[]u8 = null, pub fn init(allocator: Allocator, name: []const u8, history_path: []const u8, lock_path: ?[]const u8) Error!RemoteConfigEntry { return try initWithDefaultBranch(allocator, name, history_path, lock_path, null); } pub fn initWithDefaultBranch(allocator: Allocator, name: []const u8, history_path: []const u8, lock_path: ?[]const u8, default_branch: ?[]const u8) Error!RemoteConfigEntry { if (!validRemoteName(name)) return error.InvalidRemoteName; if (default_branch) |branch_name| { if (branch_name.len == 0) return error.InvalidRemoteConfig; } const owned_name = try allocator.dupe(u8, name); errdefer allocator.free(owned_name); const owned_history_path = try allocator.dupe(u8, history_path); errdefer allocator.free(owned_history_path); const owned_lock_path = if (lock_path) |path| try allocator.dupe(u8, path) else null; errdefer if (owned_lock_path) |path| allocator.free(path); const owned_default_branch = if (default_branch) |branch_name| try allocator.dupe(u8, branch_name) else null; errdefer if (owned_default_branch) |branch_name| allocator.free(branch_name); return .{ .name = owned_name, .history_path = owned_history_path, .lock_path = owned_lock_path, .default_branch = owned_default_branch, }; } pub fn deinit(self: *RemoteConfigEntry, allocator: Allocator) void { allocator.free(self.name); allocator.free(self.history_path); if (self.lock_path) |path| allocator.free(path); if (self.default_branch) |branch_name| allocator.free(branch_name); self.* = undefined; } pub fn fileRemote(self: *const RemoteConfigEntry, io: std.Io, dir: std.Io.Dir) FileRemote { return .{ .io = io, .dir = dir, .path = self.history_path, .lock_path = self.lock_path, }; }};pub const RemoteConfig = struct { allocator: Allocator, remotes: []RemoteConfigEntry, pub fn init(allocator: Allocator, remotes: []const RemoteConfigEntry) Error!RemoteConfig { try validateRemoteEntries(remotes); const owned = try allocator.alloc(RemoteConfigEntry, remotes.len); var count: usize = 0; errdefer { for (owned[0..count]) |*remote| remote.deinit(allocator); if (owned.len != 0) allocator.free(owned); } for (remotes, owned) |remote, *target| { target.* = try RemoteConfigEntry.initWithDefaultBranch(allocator, remote.name, remote.history_path, remote.lock_path, remote.default_branch); count += 1; } return .{ .allocator = allocator, .remotes = owned, }; } pub fn deinit(self: *RemoteConfig) void { for (self.remotes) |*remote| remote.deinit(self.allocator); if (self.remotes.len != 0) self.allocator.free(self.remotes); self.* = undefined; } pub fn find(self: *const RemoteConfig, name: []const u8) ?*const RemoteConfigEntry { for (self.remotes) |*remote| { if (std.mem.eql(u8, remote.name, name)) return remote; } return null; } pub fn fileRemote(self: *const RemoteConfig, io: std.Io, dir: std.Io.Dir, name: []const u8) Error!FileRemote { const remote = self.find(name) orelse return error.RemoteNotFound; return remote.fileRemote(io, dir); }};pub const BranchUpstreamEntry = struct { branch: []u8, remote: []u8, remote_branch: []u8, pub fn init(allocator: Allocator, branch_name: []const u8, remote_name: []const u8, remote_branch_name: []const u8) Error!BranchUpstreamEntry { if (branch_name.len == 0 or remote_branch_name.len == 0) return error.InvalidBranchUpstreamConfig; if (!validRemoteName(remote_name)) return error.InvalidRemoteName; const owned_branch = try allocator.dupe(u8, branch_name); errdefer allocator.free(owned_branch); const owned_remote = try allocator.dupe(u8, remote_name); errdefer allocator.free(owned_remote); const owned_remote_branch = try allocator.dupe(u8, remote_branch_name); errdefer allocator.free(owned_remote_branch); return .{ .branch = owned_branch, .remote = owned_remote, .remote_branch = owned_remote_branch, }; } pub fn deinit(self: *BranchUpstreamEntry, allocator: Allocator) void { allocator.free(self.branch); allocator.free(self.remote); allocator.free(self.remote_branch); self.* = undefined; }};pub const BranchUpstreamConfig = struct { allocator: Allocator, upstreams: []BranchUpstreamEntry, pub fn init(allocator: Allocator, upstreams: []const BranchUpstreamEntry) Error!BranchUpstreamConfig { try validateBranchUpstreamEntries(upstreams); const owned = try allocator.alloc(BranchUpstreamEntry, upstreams.len); var count: usize = 0; errdefer { for (owned[0..count]) |*upstream| upstream.deinit(allocator); if (owned.len != 0) allocator.free(owned); } for (upstreams, owned) |upstream, *target| { target.* = try BranchUpstreamEntry.init(allocator, upstream.branch, upstream.remote, upstream.remote_branch); count += 1; } return .{ .allocator = allocator, .upstreams = owned, }; } pub fn deinit(self: *BranchUpstreamConfig) void { for (self.upstreams) |*upstream| upstream.deinit(self.allocator); if (self.upstreams.len != 0) self.allocator.free(self.upstreams); self.* = undefined; } pub fn find(self: *const BranchUpstreamConfig, branch_name: []const u8) ?*const BranchUpstreamEntry { for (self.upstreams) |*upstream| { if (std.mem.eql(u8, upstream.branch, branch_name)) return upstream; } return null; }};pub const FileRemoteLock = struct { io: std.Io, file: std.Io.File, pub fn release(self: *FileRemoteLock) void { self.file.close(self.io); self.* = undefined; }};pub const Advertisement = struct { allocator: Allocator, refs: []RefObject, pub fn deinit(self: *Advertisement) void { for (self.refs) |*ref_value| ref_value.deinit(self.allocator); if (self.refs.len != 0) self.allocator.free(self.refs); self.* = undefined; } pub fn ref(self: *const Advertisement, name: []const u8) ?version.Ref { for (self.refs) |ref_value| { if (std.mem.eql(u8, ref_value.name, name)) { return .{ .name = ref_value.name, .target = ref_value.target, }; } } return null; }};pub const FetchRequest = struct { allocator: Allocator, wants: [][]u8, haves: []version.Hash, pub fn init(allocator: Allocator, wants: []const []const u8, haves: []const version.Hash) Allocator.Error!FetchRequest { const owned_wants = try allocator.alloc([]u8, wants.len); var want_count: usize = 0; errdefer { for (owned_wants[0..want_count]) |want| allocator.free(want); if (owned_wants.len != 0) allocator.free(owned_wants); } for (wants, owned_wants) |want, *target| { target.* = try allocator.dupe(u8, want); want_count += 1; } const owned_haves = try allocator.dupe(version.Hash, haves); errdefer if (owned_haves.len != 0) allocator.free(owned_haves); return .{ .allocator = allocator, .wants = owned_wants, .haves = owned_haves, }; } pub fn deinit(self: *FetchRequest) void { for (self.wants) |want| self.allocator.free(want); if (self.wants.len != 0) self.allocator.free(self.wants); if (self.haves.len != 0) self.allocator.free(self.haves); self.* = undefined; }};pub const CommitObject = struct { hash: version.Hash, root: version.Hash, parents: []version.Hash, fn deinit(self: *CommitObject, allocator: Allocator) void { if (self.parents.len != 0) allocator.free(self.parents); self.* = undefined; }};pub const RefObject = struct { name: []u8, target: version.Hash, fn deinit(self: *RefObject, allocator: Allocator) void { allocator.free(self.name); self.* = undefined; }};pub const PackFrame = struct { kind: history_mod.PackRecordKind, payload: []u8, fn deinit(self: *PackFrame, allocator: Allocator) void { allocator.free(self.payload); self.* = undefined; }};pub const Pack = struct { allocator: Allocator, refs: []RefObject, commits: []CommitObject, frames: []PackFrame, pub fn deinit(self: *Pack) void { for (self.refs) |*ref_value| ref_value.deinit(self.allocator); for (self.commits) |*commit| commit.deinit(self.allocator); for (self.frames) |*frame| frame.deinit(self.allocator); if (self.refs.len != 0) self.allocator.free(self.refs); if (self.commits.len != 0) self.allocator.free(self.commits); if (self.frames.len != 0) self.allocator.free(self.frames); self.* = undefined; } pub fn stats(self: *const Pack) Stats { return .{ .refs = self.refs.len, .commits = self.commits.len, .records = self.frames.len, }; }};pub fn encodePack(allocator: Allocator, pack: *const Pack) Error![]u8 { var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(allocator); try appendU32(allocator, &bytes, pack_magic); try appendU32(allocator, &bytes, pack_format_version); try appendCount(allocator, &bytes, pack.refs.len); try appendCount(allocator, &bytes, pack.commits.len); try appendCount(allocator, &bytes, pack.frames.len); for (pack.refs) |ref_value| { try appendBytes(allocator, &bytes, ref_value.name); try appendHash(allocator, &bytes, ref_value.target); } for (pack.commits) |commit| { try appendHash(allocator, &bytes, commit.hash); try appendHash(allocator, &bytes, commit.root); try appendCount(allocator, &bytes, commit.parents.len); for (commit.parents) |parent| try appendHash(allocator, &bytes, parent); } for (pack.frames) |frame| { try appendU32(allocator, &bytes, @backingInt(frame.kind)); try appendBytes(allocator, &bytes, frame.payload); } return try bytes.toOwnedSlice(allocator);}const PackSections = struct { refs: []RefObject, commits: []CommitObject, frame_count: usize, fn deinit(self: *PackSections, allocator: Allocator) void { for (self.refs) |*ref_value| ref_value.deinit(allocator); for (self.commits) |*commit| commit.deinit(allocator); if (self.refs.len != 0) allocator.free(self.refs); if (self.commits.len != 0) allocator.free(self.commits); self.* = undefined; }};fn decodePackSections(allocator: Allocator, reader: *ByteReader) Error!PackSections { if (try reader.readU32() != pack_magic) return error.InvalidPack; if (try reader.readU32() != pack_format_version) return error.InvalidPack; const ref_count = try reader.readCount(); const commit_count = try reader.readCount(); const frame_count = try reader.readCount(); const refs = try allocator.alloc(RefObject, ref_count); var refs_read: usize = 0; errdefer { for (refs[0..refs_read]) |*ref_value| ref_value.deinit(allocator); if (refs.len != 0) allocator.free(refs); } for (refs) |*ref_value| { const name = try reader.readOwnedBytes(allocator); errdefer allocator.free(name); ref_value.* = .{ .name = name, .target = try reader.hash(), }; refs_read += 1; } const commits = try allocator.alloc(CommitObject, commit_count); var commits_read: usize = 0; errdefer { for (commits[0..commits_read]) |*commit| commit.deinit(allocator); if (commits.len != 0) allocator.free(commits); } for (commits) |*commit| { const hash = try reader.hash(); const root = try reader.hash(); const parent_count = try reader.readCount(); const parents = try allocator.alloc(version.Hash, parent_count); errdefer if (parents.len != 0) allocator.free(parents); for (parents) |*parent| parent.* = try reader.hash(); const canonical = version.Commit.init(root, parents); if (!version.same(canonical.hash, hash)) return error.InvalidPack; commit.* = .{ .hash = hash, .root = root, .parents = parents, }; commits_read += 1; } return .{ .refs = refs, .commits = commits, .frame_count = frame_count, };}fn decodeFrameKind(value: u32) Error!history_mod.PackRecordKind { return std.enums.fromInt(history_mod.PackRecordKind, value) orelse error.InvalidPack;}pub fn decodePack(allocator: Allocator, bytes: []const u8) Error!Pack { var reader = ByteReader.init(bytes); var sections = try decodePackSections(allocator, &reader); errdefer sections.deinit(allocator); const frames = try allocator.alloc(PackFrame, sections.frame_count); var frames_read: usize = 0; errdefer { for (frames[0..frames_read]) |*frame| frame.deinit(allocator); if (frames.len != 0) allocator.free(frames); } for (frames) |*frame| { const kind = try decodeFrameKind(try reader.readU32()); frame.* = .{ .kind = kind, .payload = try reader.readOwnedBytes(allocator), }; frames_read += 1; } try reader.finish(); return .{ .allocator = allocator, .refs = sections.refs, .commits = sections.commits, .frames = frames, };}pub fn encodeRemoteConfig(allocator: Allocator, config: *const RemoteConfig) Error![]u8 { try validateRemoteEntries(config.remotes); var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(allocator); try appendU32(allocator, &bytes, remote_config_magic); try appendU32(allocator, &bytes, remote_config_format_version); try appendCount(allocator, &bytes, config.remotes.len); for (config.remotes) |remote| { try appendBytes(allocator, &bytes, remote.name); try appendBytes(allocator, &bytes, remote.history_path); if (remote.lock_path) |lock_path| { try appendU8(allocator, &bytes, 1); try appendBytes(allocator, &bytes, lock_path); } else { try appendU8(allocator, &bytes, 0); } if (remote.default_branch) |default_branch| { try appendU8(allocator, &bytes, 1); try appendBytes(allocator, &bytes, default_branch); } else { try appendU8(allocator, &bytes, 0); } } return try bytes.toOwnedSlice(allocator);}pub fn decodeRemoteConfig(allocator: Allocator, bytes: []const u8) Error!RemoteConfig { var reader = RemoteConfigReader.init(bytes); if (try reader.readU32() != remote_config_magic) return error.InvalidRemoteConfig; const format_version = try reader.readU32(); if (format_version != 1 and format_version != remote_config_format_version) return error.InvalidRemoteConfig; const count = try reader.readCount(); const remotes = try allocator.alloc(RemoteConfigEntry, count); var read: usize = 0; errdefer { for (remotes[0..read]) |*remote| remote.deinit(allocator); if (remotes.len != 0) allocator.free(remotes); } for (remotes) |*remote| { const name = try reader.readBytes(); const history_path = try reader.readBytes(); const lock_path = switch (try reader.readU8()) { 0 => null, 1 => try reader.readBytes(), else => return error.InvalidRemoteConfig, }; const default_branch = if (format_version >= 2) switch (try reader.readU8()) { 0 => null, 1 => try reader.readBytes(), else => return error.InvalidRemoteConfig, } else null; remote.* = try RemoteConfigEntry.initWithDefaultBranch(allocator, name, history_path, lock_path, default_branch); read += 1; } try reader.finish(); try validateRemoteEntries(remotes); return .{ .allocator = allocator, .remotes = remotes, };}pub fn readRemoteConfig(allocator: Allocator, dir: std.Io.Dir, options: RemoteConfigOptions) Error!RemoteConfig { const bytes = dir.readFileAlloc(options.io, options.path, allocator, .limited(options.max_bytes)) catch |err| switch (err) { error.FileNotFound => { return .{ .allocator = allocator, .remotes = &.{}, }; }, else => return err, }; defer allocator.free(bytes); return try decodeRemoteConfig(allocator, bytes);}pub fn writeRemoteConfig(allocator: Allocator, dir: std.Io.Dir, options: RemoteConfigOptions, config: *const RemoteConfig) Error!void { const bytes = try encodeRemoteConfig(allocator, config); defer allocator.free(bytes); var out = try dir.createFile(options.io, options.path, .{ .read = true, .truncate = true }); defer out.close(options.io); try out.writePositionalAll(options.io, bytes, 0); try out.setLength(options.io, bytes.len); try out.sync(options.io);}pub fn encodeBranchUpstreamConfig(allocator: Allocator, config: *const BranchUpstreamConfig) Error![]u8 { try validateBranchUpstreamEntries(config.upstreams); var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(allocator); try appendU32(allocator, &bytes, branch_upstream_config_magic); try appendU32(allocator, &bytes, branch_upstream_config_format_version); try appendCount(allocator, &bytes, config.upstreams.len); for (config.upstreams) |upstream| { try appendBytes(allocator, &bytes, upstream.branch); try appendBytes(allocator, &bytes, upstream.remote); try appendBytes(allocator, &bytes, upstream.remote_branch); } return try bytes.toOwnedSlice(allocator);}pub fn decodeBranchUpstreamConfig(allocator: Allocator, bytes: []const u8) Error!BranchUpstreamConfig { var reader = BranchUpstreamConfigReader.init(bytes); if (try reader.readU32() != branch_upstream_config_magic) return error.InvalidBranchUpstreamConfig; if (try reader.readU32() != branch_upstream_config_format_version) return error.InvalidBranchUpstreamConfig; const count = try reader.readCount(); const upstreams = try allocator.alloc(BranchUpstreamEntry, count); var read: usize = 0; errdefer { for (upstreams[0..read]) |*upstream| upstream.deinit(allocator); if (upstreams.len != 0) allocator.free(upstreams); } for (upstreams) |*upstream| { const branch_name = try reader.readBytes(); const remote_name = try reader.readBytes(); const remote_branch_name = try reader.readBytes(); upstream.* = try BranchUpstreamEntry.init(allocator, branch_name, remote_name, remote_branch_name); read += 1; } try reader.finish(); try validateBranchUpstreamEntries(upstreams); return .{ .allocator = allocator, .upstreams = upstreams, };}pub fn readBranchUpstreamConfig(allocator: Allocator, dir: std.Io.Dir, options: BranchUpstreamOptions) Error!BranchUpstreamConfig { const bytes = dir.readFileAlloc(options.io, options.path, allocator, .limited(options.max_bytes)) catch |err| switch (err) { error.FileNotFound => { return .{ .allocator = allocator, .upstreams = &.{}, }; }, else => return err, }; defer allocator.free(bytes); return try decodeBranchUpstreamConfig(allocator, bytes);}pub fn writeBranchUpstreamConfig(allocator: Allocator, dir: std.Io.Dir, options: BranchUpstreamOptions, config: *const BranchUpstreamConfig) Error!void { const bytes = try encodeBranchUpstreamConfig(allocator, config); defer allocator.free(bytes); var out = try dir.createFile(options.io, options.path, .{ .read = true, .truncate = true }); defer out.close(options.io); try out.writePositionalAll(options.io, bytes, 0); try out.setLength(options.io, bytes.len); try out.sync(options.io);}pub fn lockFileRemote(allocator: Allocator, remote: FileRemote) Error!FileRemoteLock { const lock_path = if (remote.lock_path) |path| path else try std.fmt.allocPrint(allocator, "{s}.lock", .{remote.path}); defer if (remote.lock_path == null) allocator.free(lock_path); const lock_file = remote.dir.createFile(remote.io, lock_path, .{ .read = true, .truncate = false, .lock = .exclusive, .lock_nonblocking = true, }) catch |err| switch (err) { error.WouldBlock => return error.RemoteBusy, else => return err, }; return .{ .io = remote.io, .file = lock_file };}pub fn advertiseRefs(allocator: Allocator, history: *const history_mod.History) Error!Advertisement { const refs = try history.refList(allocator); defer history_mod.freeRefList(allocator, refs); const owned = try allocator.alloc(RefObject, refs.len); var count: usize = 0; errdefer { for (owned[0..count]) |*ref_value| ref_value.deinit(allocator); if (owned.len != 0) allocator.free(owned); } for (refs, owned) |ref_value, *target| { const name = try allocator.dupe(u8, ref_value.name); errdefer allocator.free(name); target.* = .{ .name = name, .target = ref_value.target, }; count += 1; } return .{ .allocator = allocator, .refs = owned, };}pub fn encodeAdvertisement(allocator: Allocator, advertisement: *const Advertisement) Error![]u8 { var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(allocator); try appendU32(allocator, &bytes, advertisement_magic); try appendU32(allocator, &bytes, advertisement_format_version); try appendCount(allocator, &bytes, advertisement.refs.len); for (advertisement.refs) |ref_value| { try appendBytes(allocator, &bytes, ref_value.name); try appendHash(allocator, &bytes, ref_value.target); } return try bytes.toOwnedSlice(allocator);}pub fn decodeAdvertisement(allocator: Allocator, bytes: []const u8) Error!Advertisement { var reader = AdvertisementReader.init(bytes); if (try reader.readU32() != advertisement_magic) return error.InvalidAdvertisement; if (try reader.readU32() != advertisement_format_version) return error.InvalidAdvertisement; const count = try reader.readCount(); const refs = try allocator.alloc(RefObject, count); var read: usize = 0; errdefer { for (refs[0..read]) |*ref_value| ref_value.deinit(allocator); if (refs.len != 0) allocator.free(refs); } for (refs) |*ref_value| { const name = try reader.readOwnedBytes(allocator); errdefer allocator.free(name); ref_value.* = .{ .name = name, .target = try reader.hash(), }; read += 1; } try reader.finish(); return .{ .allocator = allocator, .refs = refs, };}pub fn fetchRequestFromHistory(allocator: Allocator, local: *const history_mod.History, wants: []const []const u8) Error!FetchRequest { const entries = try local.commitEntries(allocator); defer allocator.free(entries); const haves = try allocator.alloc(version.Hash, entries.len); defer allocator.free(haves); for (entries, haves) |entry, *hash| hash.* = entry.hash; return try FetchRequest.init(allocator, wants, haves);}pub fn encodeFetchRequest(allocator: Allocator, request: *const FetchRequest) Error![]u8 { var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(allocator); try appendU32(allocator, &bytes, fetch_request_magic); try appendU32(allocator, &bytes, fetch_request_format_version); try appendCount(allocator, &bytes, request.wants.len); try appendCount(allocator, &bytes, request.haves.len); for (request.wants) |want| try appendBytes(allocator, &bytes, want); for (request.haves) |have| try appendHash(allocator, &bytes, have); return try bytes.toOwnedSlice(allocator);}pub fn decodeFetchRequest(allocator: Allocator, bytes: []const u8) Error!FetchRequest { var reader = FetchRequestReader.init(bytes); if (try reader.readU32() != fetch_request_magic) return error.InvalidFetchRequest; if (try reader.readU32() != fetch_request_format_version) return error.InvalidFetchRequest; const want_count = try reader.readCount(); const have_count = try reader.readCount(); const wants = try allocator.alloc([]u8, want_count); var wants_read: usize = 0; errdefer { for (wants[0..wants_read]) |want| allocator.free(want); if (wants.len != 0) allocator.free(wants); } for (wants) |*want| { want.* = try reader.readOwnedBytes(allocator); wants_read += 1; } const haves = try allocator.alloc(version.Hash, have_count); errdefer if (haves.len != 0) allocator.free(haves); for (haves) |*have| have.* = try reader.hash(); try reader.finish(); return .{ .allocator = allocator, .wants = wants, .haves = haves, };}const HashSet = std.AutoHashMapUnmanaged(version.Hash, void);const FrameGroup = struct { kind: history_mod.PackRecordKind, hashes: []const version.Hash,};const PackTreeTraversalEntry = struct { key: version.Hash, expanded: bool,};const PackBuilder = struct { allocator: Allocator, source: *const history_mod.History, have: ?*const history_mod.History = null, have_commits: []const version.Hash = &.{}, refs: std.ArrayList(RefObject) = .empty, commits: std.ArrayList(CommitObject) = .empty, seen_commits: HashSet = .empty, seen_database_roots: HashSet = .empty, seen_relation_roots: HashSet = .empty, seen_relation_rows: HashSet = .empty, seen_pages: HashSet = .empty, seen_chunks: HashSet = .empty, seen_nodes: HashSet = .empty, seen_conflicts: HashSet = .empty, seen_conflict_roots: HashSet = .empty, chunks: std.ArrayList(version.Hash) = .empty, pages: std.ArrayList(version.Hash) = .empty, nodes: std.ArrayList(version.Hash) = .empty, relation_rows: std.ArrayList(version.Hash) = .empty, relation_roots: std.ArrayList(version.Hash) = .empty, conflicts: std.ArrayList(version.Hash) = .empty, conflict_roots: std.ArrayList(version.Hash) = .empty, database_roots: std.ArrayList(version.Hash) = .empty, fn init(allocator: Allocator, source: *const history_mod.History) PackBuilder { return .{ .allocator = allocator, .source = source, }; } fn initMissing(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History) PackBuilder { return .{ .allocator = allocator, .source = source, .have = have, }; } fn initWithHaveCommits(allocator: Allocator, source: *const history_mod.History, haves: []const version.Hash) PackBuilder { return .{ .allocator = allocator, .source = source, .have_commits = haves, }; } fn deinit(self: *PackBuilder) void { for (self.refs.items) |*ref_value| ref_value.deinit(self.allocator); for (self.commits.items) |*commit| commit.deinit(self.allocator); self.refs.deinit(self.allocator); self.commits.deinit(self.allocator); self.seen_commits.deinit(self.allocator); self.seen_database_roots.deinit(self.allocator); self.seen_relation_roots.deinit(self.allocator); self.seen_relation_rows.deinit(self.allocator); self.seen_pages.deinit(self.allocator); self.seen_chunks.deinit(self.allocator); self.seen_nodes.deinit(self.allocator); self.seen_conflicts.deinit(self.allocator); self.seen_conflict_roots.deinit(self.allocator); self.chunks.deinit(self.allocator); self.pages.deinit(self.allocator); self.nodes.deinit(self.allocator); self.relation_rows.deinit(self.allocator); self.relation_roots.deinit(self.allocator); self.conflicts.deinit(self.allocator); self.conflict_roots.deinit(self.allocator); self.database_roots.deinit(self.allocator); self.* = undefined; } fn frameGroups(self: *const PackBuilder) [8]FrameGroup { return .{ .{ .kind = .row_chunk, .hashes = self.chunks.items }, .{ .kind = .chunk_index_page, .hashes = self.pages.items }, .{ .kind = .tree_node, .hashes = self.nodes.items }, .{ .kind = .relation_rows, .hashes = self.relation_rows.items }, .{ .kind = .relation_root, .hashes = self.relation_roots.items }, .{ .kind = .conflict, .hashes = self.conflicts.items }, .{ .kind = .conflict_root, .hashes = self.conflict_roots.items }, .{ .kind = .database_root, .hashes = self.database_roots.items }, }; } fn frameCount(self: *const PackBuilder) usize { var count: usize = 0; for (self.frameGroups()) |group| count += group.hashes.len; return count; } fn finish(self: *PackBuilder) Error!Pack { var frames: std.ArrayList(PackFrame) = .empty; errdefer { for (frames.items) |*frame| frame.deinit(self.allocator); frames.deinit(self.allocator); } try frames.ensureTotalCapacity(self.allocator, self.frameCount()); var payload: std.ArrayList(u8) = .empty; defer payload.deinit(self.allocator); for (self.frameGroups()) |group| { for (group.hashes) |hash| { payload.clearRetainingCapacity(); try self.source.appendPackRecordPayload(self.allocator, &payload, group.kind, hash); const owned = try self.allocator.dupe(u8, payload.items); frames.appendAssumeCapacity(.{ .kind = group.kind, .payload = owned, }); } } const owned_frames = try frames.toOwnedSlice(self.allocator); errdefer { for (owned_frames) |*frame| frame.deinit(self.allocator); if (owned_frames.len != 0) self.allocator.free(owned_frames); } const refs = try self.refs.toOwnedSlice(self.allocator); errdefer { for (refs) |*ref_value| ref_value.deinit(self.allocator); if (refs.len != 0) self.allocator.free(refs); } const commits = try self.commits.toOwnedSlice(self.allocator); return .{ .allocator = self.allocator, .refs = refs, .commits = commits, .frames = owned_frames, }; } fn encodeBytes(self: *PackBuilder) Error![]u8 { var bytes: std.ArrayList(u8) = .empty; errdefer bytes.deinit(self.allocator); try appendU32(self.allocator, &bytes, pack_magic); try appendU32(self.allocator, &bytes, pack_format_version); try appendCount(self.allocator, &bytes, self.refs.items.len); try appendCount(self.allocator, &bytes, self.commits.items.len); try appendCount(self.allocator, &bytes, self.frameCount()); for (self.refs.items) |ref_value| { try appendBytes(self.allocator, &bytes, ref_value.name); try appendHash(self.allocator, &bytes, ref_value.target); } for (self.commits.items) |commit| { try appendHash(self.allocator, &bytes, commit.hash); try appendHash(self.allocator, &bytes, commit.root); try appendCount(self.allocator, &bytes, commit.parents.len); for (commit.parents) |parent| try appendHash(self.allocator, &bytes, parent); } var payload: std.ArrayList(u8) = .empty; defer payload.deinit(self.allocator); for (self.frameGroups()) |group| { for (group.hashes) |hash| { payload.clearRetainingCapacity(); try self.source.appendPackRecordPayload(self.allocator, &payload, group.kind, hash); try appendU32(self.allocator, &bytes, @backingInt(group.kind)); try appendBytes(self.allocator, &bytes, payload.items); } } return try bytes.toOwnedSlice(self.allocator); } fn addRef(self: *PackBuilder, name: []const u8, target: version.Hash) Error!void { if (containsRef(self.refs.items, name)) return; const owned_name = try self.allocator.dupe(u8, name); errdefer self.allocator.free(owned_name); try self.refs.append(self.allocator, .{ .name = owned_name, .target = target, }); } fn addReachableCommit(self: *PackBuilder, hash: version.Hash) Error!void { var pending: std.ArrayList(version.Hash) = .empty; defer pending.deinit(self.allocator); try pending.append(self.allocator, hash); while (pending.pop()) |next| { if (self.seen_commits.contains(next)) continue; try self.seen_commits.put(self.allocator, next, {}); if (containsHash(self.have_commits, next)) continue; if (self.have) |have| { if (have.hasCommit(next)) continue; } const commit = try self.source.commitValue(next); try self.commits.ensureUnusedCapacity(self.allocator, 1); const parents = try self.allocator.dupe(version.Hash, commit.parents); self.commits.appendAssumeCapacity(.{ .hash = commit.hash, .root = commit.root, .parents = parents, }); try self.addDatabaseRoot(commit.root); var index = commit.parents.len; while (index > 0) { index -= 1; try pending.append(self.allocator, commit.parents[index]); } } } fn addDatabaseRoot(self: *PackBuilder, hash: version.Hash) Error!void { if (self.seen_database_roots.contains(hash)) return; try self.seen_database_roots.put(self.allocator, hash, {}); if (self.have) |have| { if (have.hasDatabaseRoot(hash)) return; } const view = self.source.databaseRootView(hash) orelse return error.DatabaseRootNotFound; for (view.entries) |entry| { try self.addRelationRoot(entry.hash); try self.addRelationRows(entry.hash); } try self.addConflictRoot(view.conflicts); try self.database_roots.append(self.allocator, hash); } fn addRelationRoot(self: *PackBuilder, hash: version.Hash) Error!void { if (self.seen_relation_roots.contains(hash)) return; try self.seen_relation_roots.put(self.allocator, hash, {}); if (self.have) |have| { if (have.hasRelationRoot(hash)) return; } var keys = (try self.source.relationKeysView(self.allocator, hash)) orelse return error.RelationRootNotFound; defer keys.deinit(); if (keys.table_key) |key| try self.addTreeNode(key); for (keys.index_keys) |index_key| { const key = index_key orelse continue; try self.addTreeNode(key); } try self.relation_roots.append(self.allocator, hash); } fn addTreeNode(self: *PackBuilder, key: version.Hash) Error!void { var stack: std.ArrayList(PackTreeTraversalEntry) = .empty; defer stack.deinit(self.allocator); try stack.append(self.allocator, .{ .key = key, .expanded = false }); while (stack.pop()) |entry| { if (entry.expanded) { try self.nodes.append(self.allocator, entry.key); continue; } if (self.seen_nodes.contains(entry.key)) continue; try self.seen_nodes.put(self.allocator, entry.key, {}); if (self.have) |have| { if (have.hasTreeNode(entry.key)) continue; } try stack.append(self.allocator, .{ .key = entry.key, .expanded = true }); const children = try self.source.treeNodeChildren(self.allocator, entry.key); defer self.allocator.free(children); for (children) |child| try stack.append(self.allocator, .{ .key = child, .expanded = false }); } } fn addRelationRows(self: *PackBuilder, root: version.Hash) Error!void { if (self.seen_relation_rows.contains(root)) return; try self.seen_relation_rows.put(self.allocator, root, {}); if (self.have) |have| { if (have.hasRelationRows(root)) return; } var pages = (try self.source.relationRowsPages(self.allocator, root)) orelse return error.RelationRowsNotFound; defer pages.deinit(); for (pages.items) |digest| try self.addPage(digest); try self.relation_rows.append(self.allocator, root); } fn addPage(self: *PackBuilder, digest: version.Hash) Error!void { if (self.seen_pages.contains(digest)) return; try self.seen_pages.put(self.allocator, digest, {}); if (self.have) |have| { if (have.hasIndexPage(digest)) return; } var chunks = (try self.source.indexPageChunks(self.allocator, digest)) orelse return error.RelationRowsNotFound; defer chunks.deinit(); for (chunks.items) |chunk_digest| try self.addChunk(chunk_digest); try self.pages.append(self.allocator, digest); } fn addChunk(self: *PackBuilder, digest: version.Hash) Error!void { if (self.seen_chunks.contains(digest)) return; try self.seen_chunks.put(self.allocator, digest, {}); if (self.have) |have| { if (have.hasRowChunk(digest)) return; } try self.chunks.append(self.allocator, digest); } fn addConflictRoot(self: *PackBuilder, root_hash: version.Hash) Error!void { if (version.same(root_hash, version.ConflictRoot.empty().hash)) return; if (self.seen_conflict_roots.contains(root_hash)) return; try self.seen_conflict_roots.put(self.allocator, root_hash, {}); if (self.have) |have| { if (try have.hasConflictRoot(root_hash)) return; } var artifacts = try self.source.conflictArtifacts(self.allocator, root_hash); defer artifacts.deinit(); for (artifacts.artifacts) |artifact| try self.addConflict(artifact.hash); try self.conflict_roots.append(self.allocator, root_hash); } fn addConflict(self: *PackBuilder, hash: version.Hash) Error!void { if (self.seen_conflicts.contains(hash)) return; try self.seen_conflicts.put(self.allocator, hash, {}); if (self.have) |have| { if (have.hasConflict(hash)) return; } try self.conflicts.append(self.allocator, hash); }};fn builderForRefs(allocator: Allocator, source: *const history_mod.History, refs: []const version.Ref) Error!PackBuilder { var builder = PackBuilder.init(allocator, source); errdefer builder.deinit(); for (refs) |ref_value| { try builder.addRef(ref_value.name, ref_value.target); try builder.addReachableCommit(ref_value.target); } return builder;}fn builderForMissingRefs(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, refs: []const version.Ref) Error!PackBuilder { var builder = PackBuilder.initMissing(allocator, source, have); errdefer builder.deinit(); for (refs) |ref_value| { try builder.addRef(ref_value.name, ref_value.target); try builder.addReachableCommit(ref_value.target); } return builder;}fn refsForNames(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error![]version.Ref { const refs = try allocator.alloc(version.Ref, names.len); errdefer allocator.free(refs); for (names, refs) |name, *ref_value| { ref_value.* = (try source.ref(name)) orelse return error.RefNotFound; } return refs;}pub fn exportAll(allocator: Allocator, source: *const history_mod.History) Error!Pack { const refs = try source.refList(allocator); defer history_mod.freeRefList(allocator, refs); return try exportRefs(allocator, source, refs);}pub fn exportRefNames(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error!Pack { const refs = try refsForNames(allocator, source, names); defer allocator.free(refs); return try exportRefs(allocator, source, refs);}pub fn exportRefs(allocator: Allocator, source: *const history_mod.History, refs: []const version.Ref) Error!Pack { var builder = try builderForRefs(allocator, source, refs); defer builder.deinit(); return try builder.finish();}pub fn exportMissingRefs(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, refs: []const version.Ref) Error!Pack { var builder = try builderForMissingRefs(allocator, source, have, refs); defer builder.deinit(); return try builder.finish();}pub fn exportMissingRefNames(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, names: []const []const u8) Error!Pack { const refs = try refsForNames(allocator, source, names); defer allocator.free(refs); return try exportMissingRefs(allocator, source, have, refs);}pub fn exportForFetchRequest(allocator: Allocator, source: *const history_mod.History, request: *const FetchRequest) Error!Pack { var builder = PackBuilder.initWithHaveCommits(allocator, source, request.haves); defer builder.deinit(); for (request.wants) |name| { const ref_value = (try source.ref(name)) orelse return error.RefNotFound; try builder.addRef(ref_value.name, ref_value.target); try builder.addReachableCommit(ref_value.target); } return try builder.finish();}pub fn exportBytesForFetchRequest(allocator: Allocator, source: *const history_mod.History, request: *const FetchRequest) Error![]u8 { var builder = PackBuilder.initWithHaveCommits(allocator, source, request.haves); defer builder.deinit(); for (request.wants) |name| { const ref_value = (try source.ref(name)) orelse return error.RefNotFound; try builder.addRef(ref_value.name, ref_value.target); try builder.addReachableCommit(ref_value.target); } return try builder.encodeBytes();}pub fn exportMissingBytesAll(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History) Error![]u8 { const refs = try source.refList(allocator); defer history_mod.freeRefList(allocator, refs); var builder = try builderForMissingRefs(allocator, source, have, refs); defer builder.deinit(); return try builder.encodeBytes();}pub fn exportMissingRefNameBytes(allocator: Allocator, source: *const history_mod.History, have: *const history_mod.History, names: []const []const u8) Error![]u8 { const refs = try refsForNames(allocator, source, names); defer allocator.free(refs); var builder = try builderForMissingRefs(allocator, source, have, refs); defer builder.deinit(); return try builder.encodeBytes();}pub fn exportBytesAll(allocator: Allocator, source: *const history_mod.History) Error![]u8 { const refs = try source.refList(allocator); defer history_mod.freeRefList(allocator, refs); var builder = try builderForRefs(allocator, source, refs); defer builder.deinit(); return try builder.encodeBytes();}pub fn exportRefNameBytes(allocator: Allocator, source: *const history_mod.History, names: []const []const u8) Error![]u8 { const refs = try refsForNames(allocator, source, names); defer allocator.free(refs); var builder = try builderForRefs(allocator, source, refs); defer builder.deinit(); return try builder.encodeBytes();}pub fn importObjects(target: *history_mod.History, pack: *const Pack) Error!Stats { var stats = Stats{}; for (pack.frames) |frame| { if (try target.importPackRecord(frame.kind, frame.payload)) stats.records += 1; } for (pack.commits) |commit_object| { const commit = version.Commit.init(commit_object.root, commit_object.parents); if (!version.same(commit.hash, commit_object.hash)) return error.InvalidPack; if (try target.importPackCommit(commit_object.root, commit_object.parents)) stats.commits += 1; } try target.flushSync(); return stats;}pub fn importPack(target: *history_mod.History, pack: *const Pack) Error!Stats { var stats = try importObjects(target, pack); for (pack.refs) |ref_value| { try target.putRef(.{ .name = ref_value.name, .target = ref_value.target, }); stats.refs += 1; } return stats;}pub fn importBytes(allocator: Allocator, target: *history_mod.History, bytes: []const u8) Error!Stats { var reader = ByteReader.init(bytes); var sections = try decodePackSections(allocator, &reader); defer sections.deinit(allocator); var stats = Stats{}; var index: usize = 0; while (index < sections.frame_count) : (index += 1) { const kind = try decodeFrameKind(try reader.readU32()); const payload = try reader.readBytes(); if (try target.importPackRecord(kind, payload)) stats.records += 1; } try reader.finish(); for (sections.commits) |commit_object| { const commit = version.Commit.init(commit_object.root, commit_object.parents); if (!version.same(commit.hash, commit_object.hash)) return error.InvalidPack; if (try target.importPackCommit(commit_object.root, commit_object.parents)) stats.commits += 1; } for (sections.refs) |ref_value| { try target.putRef(.{ .name = ref_value.name, .target = ref_value.target, }); stats.refs += 1; } try target.flushSync(); return stats;}pub fn headHex(hash: version.Hash) HeadHex { return std.fmt.bytesToHex(hash, .lower);}pub fn packRefTarget(pack: *const Pack, ref_name: []const u8) ?version.Hash { for (pack.refs) |ref_value| { if (std.mem.eql(u8, ref_value.name, ref_name)) return ref_value.target; } return null;}pub fn missingPackCounts(local: *const history_mod.History, pack: *const Pack) Error!HistoryTransferPlan { var counts = HistoryTransferPlan{}; for (pack.commits) |commit| { if (!local.hasCommit(commit.hash)) counts.commits += 1; } for (pack.frames) |frame| { if (!try local.packRecordPresent(frame.kind, frame.payload)) counts.records += 1; } return counts;}pub fn missingHistoryCommitCount(allocator: Allocator, source: *const history_mod.History, head: version.Hash, other: *const history_mod.History) Error!usize { var visited = std.AutoHashMap(version.Hash, void).init(allocator); defer visited.deinit(); var stack: std.ArrayList(version.Hash) = .empty; defer stack.deinit(allocator); try stack.append(allocator, head); var missing: usize = 0; while (stack.pop()) |hash| { if (visited.contains(hash)) continue; try visited.put(hash, {}); if (other.hasCommit(hash)) continue; missing += 1; const commit = try source.commitValue(hash); for (commit.parents) |parent| try stack.append(allocator, parent); } return missing;}pub fn historyRelation(allocator: Allocator, local: *const history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!HistoryRelation { const local_ref = (try local.ref(ref_name)) orelse return error.RefNotFound; const remote_ref = (try remote.ref(ref_name)); const ahead = try missingHistoryCommitCount(allocator, local, local_ref.target, remote); const behind = if (remote_ref) |ref_value| try missingHistoryCommitCount(allocator, remote, ref_value.target, local) else 0; return .{ .local_head = local_ref.target, .remote_head = if (remote_ref) |ref_value| ref_value.target else null, .ahead = ahead, .behind = behind, };}pub fn planHistoryAdopt(allocator: Allocator, local: *const history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!HistoryTransferPlan { var names = [_][]const u8{ref_name}; var pack = try exportMissingRefNames(allocator, remote, local, names[0..]); defer pack.deinit(); return .{ .commits = pack.commits.len, .records = pack.frames.len };}pub fn planHistoryTransfer( allocator: Allocator, local: *const history_mod.History, remote: *const history_mod.History, ref_name: []const u8, related: HistoryRelation, direction: HistoryDirection,) HistoryPlanError!HistoryTransferPlan { if (related.upToDate()) return .{}; if (related.diverged()) return error.HistoryDiverged; switch (direction) { .push => { if (related.behind != 0) return error.HistoryRemoteAhead; if (related.ahead == 0 and related.remote_head != null) return .{}; var names = [_][]const u8{ref_name}; var pack = try exportMissingRefNames(allocator, local, remote, names[0..]); defer pack.deinit(); return .{ .commits = pack.commits.len, .records = pack.frames.len }; }, .pull => { if (related.remote_head == null or related.behind == 0) return .{}; var names = [_][]const u8{ref_name}; var pack = try exportMissingRefNames(allocator, remote, local, names[0..]); defer pack.deinit(); return .{ .commits = pack.commits.len, .records = pack.frames.len }; }, }}pub fn cloneAll(allocator: Allocator, source: *const history_mod.History, target: *history_mod.History) Error!Stats { var pack = try exportAll(allocator, source); defer pack.deinit(); return try importPack(target, &pack);}pub fn fetch(allocator: Allocator, local: *history_mod.History, remote: *const history_mod.History, options: FetchOptions) Error!Stats { const refs = try remote.refList(allocator); defer history_mod.freeRefList(allocator, refs); var pack = try exportMissingRefs(allocator, remote, local, refs); defer pack.deinit(); var stats = try fetchPack(allocator, local, &pack, options); if (options.prune) stats.refs += try pruneRemoteTrackingRefs(allocator, local, options.remote, refs); return stats;}pub fn fetchPack(allocator: Allocator, local: *history_mod.History, pack: *const Pack, options: FetchOptions) Error!Stats { var stats = try importObjects(local, pack); for (pack.refs) |ref_value| { const name = try fetchRefName(allocator, options.remote, ref_value.name); defer allocator.free(name); try local.putRef(.{ .name = name, .target = ref_value.target, }); stats.refs += 1; } return stats;}pub fn fetchBytes(allocator: Allocator, local: *history_mod.History, bytes: []const u8, options: FetchOptions) Error!Stats { var pack = try decodePack(allocator, bytes); defer pack.deinit(); return try fetchPack(allocator, local, &pack, options);}pub fn pushFastForward(allocator: Allocator, local: *const history_mod.History, remote: *history_mod.History, ref_name: []const u8) Error!Stats { return try pushFastForwardTo(allocator, local, remote, ref_name, ref_name);}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 { const ref_value = (try local.ref(local_ref_name)) orelse return error.RefNotFound; var names = [_][]const u8{local_ref_name}; var pack = try exportMissingRefNames(allocator, local, remote, names[0..]); defer pack.deinit(); const expected = if ((try remote.ref(remote_ref_name))) |remote_ref| remote_ref.target else null; if (expected) |remote_target| { if (!try canFastForwardWithPack(allocator, remote, &pack, remote_target, ref_value.target)) return error.NonFastForward; } var stats = try importObjects(remote, &pack); try remote.putRefIfMatches(.{ .name = remote_ref_name, .target = ref_value.target, }, expected); stats.refs += 1; return stats;}pub fn deleteRemoteRef(remote: *history_mod.History, ref_name: []const u8) Error!Stats { try remote.deleteRef(ref_name); return .{ .refs = 1 };}pub fn pullFastForward(allocator: Allocator, local: *history_mod.History, remote: *const history_mod.History, ref_name: []const u8) Error!Stats { const ref_value = (try remote.ref(ref_name)) orelse return error.RefNotFound; var names = [_][]const u8{ref_name}; var pack = try exportMissingRefNames(allocator, remote, local, names[0..]); defer pack.deinit(); const expected = if ((try local.ref(ref_name))) |local_ref| local_ref.target else null; if (expected) |local_target| { if (!try canFastForwardWithPack(allocator, local, &pack, local_target, ref_value.target)) return error.NonFastForward; } var stats = try importObjects(local, &pack); try local.putRefIfMatches(.{ .name = ref_name, .target = ref_value.target, }, expected); stats.refs += 1; return stats;}pub fn cloneFile(allocator: Allocator, remote: FileRemote, target: *history_mod.History) Error!Stats { var lock = try lockFileRemote(allocator, remote); defer lock.release(); var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject }); defer remote_history.deinit(); return try cloneAll(allocator, &remote_history, target);}pub fn fetchFile(allocator: Allocator, local: *history_mod.History, remote: FileRemote, options: FetchOptions) Error!Stats { var lock = try lockFileRemote(allocator, remote); defer lock.release(); var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject }); defer remote_history.deinit(); return try fetch(allocator, local, &remote_history, options);}pub fn pushFileFastForward(allocator: Allocator, local: *const history_mod.History, remote: FileRemote, ref_name: []const u8) Error!Stats { return try pushFileFastForwardTo(allocator, local, remote, ref_name, ref_name);}pub fn pushFileFastForwardTo(allocator: Allocator, local: *const history_mod.History, remote: FileRemote, local_ref_name: []const u8, remote_ref_name: []const u8) Error!Stats { var lock = try lockFileRemote(allocator, remote); defer lock.release(); var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .recovery = .reject }); defer remote_history.deinit(); return try pushFastForwardTo(allocator, local, &remote_history, local_ref_name, remote_ref_name);}pub fn deleteFileRemoteRef(allocator: Allocator, remote: FileRemote, ref_name: []const u8) Error!Stats { var lock = try lockFileRemote(allocator, remote); defer lock.release(); var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject }); defer remote_history.deinit(); return try deleteRemoteRef(&remote_history, ref_name);}pub fn pullFileFastForward(allocator: Allocator, local: *history_mod.History, remote: FileRemote, ref_name: []const u8) Error!Stats { var lock = try lockFileRemote(allocator, remote); defer lock.release(); var remote_history = try history_mod.History.open(allocator, remote.dir, .{ .io = remote.io, .path = remote.path, .create = false, .recovery = .reject }); defer remote_history.deinit(); return try pullFastForward(allocator, local, &remote_history, ref_name);}fn remoteTrackingName(allocator: Allocator, remote: []const u8, name: []const u8) Allocator.Error![]u8 { return try std.fmt.allocPrint(allocator, "refs/remotes/{s}/{s}", .{ remote, name });}fn fetchRefName(allocator: Allocator, remote: []const u8, name: []const u8) Allocator.Error![]u8 { if (isTagRef(name)) return try allocator.dupe(u8, name); return try remoteTrackingName(allocator, remote, name);}fn pruneRemoteTrackingRefs(allocator: Allocator, local: *history_mod.History, remote: []const u8, remote_refs: []const version.Ref) Error!usize { const prefix = try std.fmt.allocPrint(allocator, "refs/remotes/{s}/", .{remote}); defer allocator.free(prefix); const refs = try local.refList(allocator); defer history_mod.freeRefList(allocator, refs); var pruned: usize = 0; for (refs) |ref_value| { if (!std.mem.startsWith(u8, ref_value.name, prefix)) continue; const remote_ref_name = ref_value.name[prefix.len..]; if (containsVersionRef(remote_refs, remote_ref_name)) continue; try local.deleteRef(ref_value.name); pruned += 1; } return pruned;}fn containsVersionRef(refs: []const version.Ref, name: []const u8) bool { for (refs) |ref_value| { if (std.mem.eql(u8, ref_value.name, name)) return true; } return false;}fn isTagRef(name: []const u8) bool { return std.mem.startsWith(u8, name, "refs/tags/");}const ByteReader = struct { bytes_value: []const u8, cursor: usize = 0, fn init(bytes: []const u8) ByteReader { return .{ .bytes_value = bytes }; } fn finish(self: *const ByteReader) Error!void { if (self.cursor != self.bytes_value.len) return error.InvalidPack; } fn hash(self: *ByteReader) Error!version.Hash { if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidPack; const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*; self.cursor += version.hash_bytes; return value; } fn readBytes(self: *ByteReader) Error![]const u8 { const len = try self.readCount(); if (len > self.bytes_value.len - self.cursor) return error.InvalidPack; const value = self.bytes_value[self.cursor..][0..len]; self.cursor += len; return value; } fn readOwnedBytes(self: *ByteReader, allocator: Allocator) Error![]u8 { return try allocator.dupe(u8, try self.readBytes()); } fn readCount(self: *ByteReader) Error!usize { return @intCast(try self.readU32()); } fn readU32(self: *ByteReader) Error!u32 { if (4 > self.bytes_value.len - self.cursor) return error.InvalidPack; const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big); self.cursor += 4; return value; }};const RemoteConfigReader = struct { bytes_value: []const u8, cursor: usize = 0, fn init(bytes: []const u8) RemoteConfigReader { return .{ .bytes_value = bytes }; } fn finish(self: *const RemoteConfigReader) Error!void { if (self.cursor != self.bytes_value.len) return error.InvalidRemoteConfig; } fn readBytes(self: *RemoteConfigReader) Error![]const u8 { const len = try self.readCount(); if (len > self.bytes_value.len - self.cursor) return error.InvalidRemoteConfig; const value = self.bytes_value[self.cursor..][0..len]; self.cursor += len; return value; } fn readCount(self: *RemoteConfigReader) Error!usize { return @intCast(try self.readU32()); } fn readU8(self: *RemoteConfigReader) Error!u8 { if (self.cursor >= self.bytes_value.len) return error.InvalidRemoteConfig; const value = self.bytes_value[self.cursor]; self.cursor += 1; return value; } fn readU32(self: *RemoteConfigReader) Error!u32 { if (4 > self.bytes_value.len - self.cursor) return error.InvalidRemoteConfig; const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big); self.cursor += 4; return value; }};const BranchUpstreamConfigReader = struct { bytes_value: []const u8, cursor: usize = 0, fn init(bytes: []const u8) BranchUpstreamConfigReader { return .{ .bytes_value = bytes }; } fn finish(self: *const BranchUpstreamConfigReader) Error!void { if (self.cursor != self.bytes_value.len) return error.InvalidBranchUpstreamConfig; } fn readBytes(self: *BranchUpstreamConfigReader) Error![]const u8 { const len = try self.readCount(); if (len > self.bytes_value.len - self.cursor) return error.InvalidBranchUpstreamConfig; const value = self.bytes_value[self.cursor..][0..len]; self.cursor += len; return value; } fn readCount(self: *BranchUpstreamConfigReader) Error!usize { return @intCast(try self.readU32()); } fn readU32(self: *BranchUpstreamConfigReader) Error!u32 { if (4 > self.bytes_value.len - self.cursor) return error.InvalidBranchUpstreamConfig; const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big); self.cursor += 4; return value; }};const AdvertisementReader = struct { bytes_value: []const u8, cursor: usize = 0, fn init(bytes: []const u8) AdvertisementReader { return .{ .bytes_value = bytes }; } fn finish(self: *const AdvertisementReader) Error!void { if (self.cursor != self.bytes_value.len) return error.InvalidAdvertisement; } fn hash(self: *AdvertisementReader) Error!version.Hash { if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement; const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*; self.cursor += version.hash_bytes; return value; } fn readOwnedBytes(self: *AdvertisementReader, allocator: Allocator) Error![]u8 { return try allocator.dupe(u8, try self.readBytes()); } fn readBytes(self: *AdvertisementReader) Error![]const u8 { const len = try self.readCount(); if (len > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement; const value = self.bytes_value[self.cursor..][0..len]; self.cursor += len; return value; } fn readCount(self: *AdvertisementReader) Error!usize { return @intCast(try self.readU32()); } fn readU32(self: *AdvertisementReader) Error!u32 { if (4 > self.bytes_value.len - self.cursor) return error.InvalidAdvertisement; const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big); self.cursor += 4; return value; }};const FetchRequestReader = struct { bytes_value: []const u8, cursor: usize = 0, fn init(bytes: []const u8) FetchRequestReader { return .{ .bytes_value = bytes }; } fn finish(self: *const FetchRequestReader) Error!void { if (self.cursor != self.bytes_value.len) return error.InvalidFetchRequest; } fn hash(self: *FetchRequestReader) Error!version.Hash { if (version.hash_bytes > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest; const value = self.bytes_value[self.cursor..][0..version.hash_bytes].*; self.cursor += version.hash_bytes; return value; } fn readOwnedBytes(self: *FetchRequestReader, allocator: Allocator) Error![]u8 { return try allocator.dupe(u8, try self.readBytes()); } fn readBytes(self: *FetchRequestReader) Error![]const u8 { const len = try self.readCount(); if (len > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest; const value = self.bytes_value[self.cursor..][0..len]; self.cursor += len; return value; } fn readCount(self: *FetchRequestReader) Error!usize { return @intCast(try self.readU32()); } fn readU32(self: *FetchRequestReader) Error!u32 { if (4 > self.bytes_value.len - self.cursor) return error.InvalidFetchRequest; const value = std.mem.readInt(u32, self.bytes_value[self.cursor..][0..4], .big); self.cursor += 4; return value; }};fn validRemoteName(name: []const u8) bool { if (name.len == 0) return false; return std.mem.indexOfAny(u8, name, " \t\n\r./\\!@#$%^&*(){}[],.<>'\"?=+|") == null;}fn validateRemoteEntries(remotes: []const RemoteConfigEntry) Error!void { for (remotes, 0..) |remote, index| { if (!validRemoteName(remote.name)) return error.InvalidRemoteName; if (remote.history_path.len == 0) return error.InvalidRemoteConfig; if (remote.default_branch) |branch_name| { if (branch_name.len == 0) return error.InvalidRemoteConfig; } for (remotes[0..index]) |previous| { if (std.mem.eql(u8, previous.name, remote.name)) return error.RemoteExists; } }}fn validateBranchUpstreamEntries(upstreams: []const BranchUpstreamEntry) Error!void { for (upstreams, 0..) |upstream, index| { if (upstream.branch.len == 0 or upstream.remote_branch.len == 0) return error.InvalidBranchUpstreamConfig; if (!validRemoteName(upstream.remote)) return error.InvalidRemoteName; for (upstreams[0..index]) |previous| { if (std.mem.eql(u8, previous.branch, upstream.branch)) return error.BranchUpstreamExists; } }}fn appendHash(allocator: Allocator, target: *std.ArrayList(u8), hash: version.Hash) Allocator.Error!void { try target.appendSlice(allocator, hash[0..]);}fn appendU8(allocator: Allocator, target: *std.ArrayList(u8), value: u8) Allocator.Error!void { try target.append(allocator, value);}fn appendBytes(allocator: Allocator, target: *std.ArrayList(u8), bytes: []const u8) Error!void { if (bytes.len > std.math.maxInt(u32)) return error.InvalidPack; try appendU32(allocator, target, @intCast(bytes.len)); try target.appendSlice(allocator, bytes);}fn appendCount(allocator: Allocator, target: *std.ArrayList(u8), count: usize) Error!void { if (count > std.math.maxInt(u32)) return error.InvalidPack; try appendU32(allocator, target, @intCast(count));}fn appendU32(allocator: Allocator, target: *std.ArrayList(u8), value: u32) Allocator.Error!void { var encoded: [4]u8 = undefined; std.mem.writeInt(u32, encoded[0..], value, .big); try target.appendSlice(allocator, encoded[0..]);}fn canFastForwardWithPack(allocator: Allocator, history: *const history_mod.History, pack: *const Pack, current: version.Hash, target: version.Hash) Error!bool { const history_entries = try history.commitEntries(allocator); defer allocator.free(history_entries); var entries: std.ArrayList(branch.CommitEntry) = .empty; defer entries.deinit(allocator); try entries.appendSlice(allocator, history_entries); for (pack.commits) |commit| { try entries.append(allocator, .{ .hash = commit.hash, .parents = commit.parents, }); } return try branch.canFastForward(allocator, entries.items, current, target);}fn containsRef(refs: []const RefObject, name: []const u8) bool { for (refs) |ref_value| { if (std.mem.eql(u8, ref_value.name, name)) return true; } return false;}fn containsHash(hashes: []const version.Hash, hash: version.Hash) bool { for (hashes) |value| { if (version.same(value, hash)) return true; } return false;}fn deinitConflictValue(allocator: Allocator, value: ?version.ConflictValue) void { if (value) |conflict_value| switch (conflict_value) { .row => |bytes| allocator.free(bytes), .relation => {}, };}fn conflictRowValue(value: ?version.ConflictValue) ?[]const u8 { const conflict_value = value orelse return null; return switch (conflict_value) { .row => |bytes| bytes, .relation => unreachable, };}fn conflictRelationValue(value: ?version.ConflictValue) ?version.Hash { const conflict_value = value orelse return null; return switch (conflict_value) { .row => unreachable, .relation => |hash| hash, };}fn testingHeader(sequence: u32) wal.Header { return .{ .sequence = sequence, .salt = .{ .first = 0x7379_6e63, .second = 0x6869_7374 + sequence, }, };}fn executeStatement(connection: *connection_mod.Connection, sql: []const u8) !void { var result = try connection.execute(std.testing.allocator, sql, .{ .durability = .buffered }); defer result.deinit(std.testing.allocator);}fn relationRows(history: *const history_mod.History, commit_hash: version.Hash, name: []const u8) ![]version.RelationRow { const commit = try history.commitValue(commit_hash); var value = try history.databaseValue(std.testing.allocator, commit.root); defer value.deinit(); const relation = value.findRelation(name) orelse return error.RelationRootNotFound; return try version.cloneRelationRows(std.testing.allocator, relation.rows);}fn createStore(tmp: std.testing.TmpDir, database_name: []const u8, wal_name: []const u8, history_name: []const u8, sequence: u32) !struct { database: file.Database, history: history_mod.History,} { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = database_name, .wal = wal_name }, .header = testingHeader(sequence), }); errdefer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = history_name, .recovery = .reject }); errdefer history.deinit(); return .{ .database = database, .history = history, };}test "sync clone copies reachable history and refs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var source_store = try createStore(tmp, "clone-source.db", "clone-source.wal", "clone-source.history", 1); defer source_store.history.deinit(); defer source_store.database.deinit(); var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{}); defer source.deinit(); try executeStatement(&source, "CREATE TABLE items (name)"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try source.stage(); const main_commit = try source.commit(&source_store.history); _ = try source.createBranch(&source_store.history, "side"); try source.checkoutBranch(std.testing.allocator, &source_store.history, "side"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (2, 'side')"); try source.stage(); const side_commit = try source.commit(&source_store.history); var target_store = try createStore(tmp, "clone-target.db", "clone-target.wal", "clone-target.history", 2); defer target_store.history.deinit(); defer target_store.database.deinit(); const stats = try cloneAll(std.testing.allocator, &source_store.history, &target_store.history); try std.testing.expectEqual(@as(usize, 2), stats.refs); try std.testing.expect(version.same(main_commit, (try target_store.history.ref("main")).?.target)); try std.testing.expect(version.same(side_commit, (try target_store.history.ref("side")).?.target)); const main_rows = try relationRows(&target_store.history, main_commit, "items"); defer version.freeRelationRows(std.testing.allocator, main_rows); try std.testing.expectEqual(@as(usize, 1), main_rows.len); const side_rows = try relationRows(&target_store.history, side_commit, "items"); defer version.freeRelationRows(std.testing.allocator, side_rows); try std.testing.expectEqual(@as(usize, 2), side_rows.len);}test "sync history planning reports relation and missing pack counts" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var local_store = try createStore(tmp, "plan-local.db", "plan-local.wal", "plan-local.history", 11); defer local_store.history.deinit(); defer local_store.database.deinit(); var remote_store = try createStore(tmp, "plan-remote.db", "plan-remote.wal", "plan-remote.history", 12); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var local = try connection_mod.Connection.create(std.testing.allocator, &local_store.database, &local_store.history, .{}); defer local.deinit(); try executeStatement(&local, "CREATE TABLE items (name)"); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try local.stage(); _ = try local.commit(&local_store.history); _ = try cloneAll(std.testing.allocator, &local_store.history, &remote_store.history); const current = try historyRelation(std.testing.allocator, &local_store.history, &remote_store.history, "main"); try std.testing.expect(current.upToDate()); try std.testing.expectEqual(@as(usize, 0), current.ahead); try std.testing.expectEqual(@as(usize, 0), current.behind); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (2, 'tip')"); try local.stage(); const tip = try local.commit(&local_store.history); const related = try historyRelation(std.testing.allocator, &local_store.history, &remote_store.history, "main"); try std.testing.expect(!related.upToDate()); try std.testing.expect(!related.diverged()); try std.testing.expectEqual(@as(usize, 1), related.ahead); try std.testing.expectEqual(@as(usize, 0), related.behind); const push_plan = try planHistoryTransfer(std.testing.allocator, &local_store.history, &remote_store.history, "main", related, .push); try std.testing.expectEqual(@as(usize, 1), push_plan.commits); try std.testing.expect(push_plan.records != 0); const pull_plan = try planHistoryTransfer(std.testing.allocator, &local_store.history, &remote_store.history, "main", related, .pull); try std.testing.expectEqual(@as(usize, 0), pull_plan.commits); try std.testing.expectEqual(@as(usize, 0), pull_plan.records); var names = [_][]const u8{"main"}; var pack = try exportMissingRefNames(std.testing.allocator, &local_store.history, &remote_store.history, names[0..]); defer pack.deinit(); try std.testing.expect(version.same(tip, packRefTarget(&pack, "main").?)); const missing = try missingPackCounts(&remote_store.history, &pack); try std.testing.expectEqual(push_plan.commits, missing.commits); try std.testing.expectEqual(push_plan.records, missing.records); var fresh_store = try createStore(tmp, "plan-fresh.db", "plan-fresh.wal", "plan-fresh.history", 13); defer fresh_store.history.deinit(); defer fresh_store.database.deinit(); const adopt_plan = try planHistoryAdopt(std.testing.allocator, &fresh_store.history, &local_store.history, "main"); try std.testing.expect(adopt_plan.commits >= 2); try std.testing.expect(adopt_plan.records >= push_plan.records);}test "sync record packs stay within history size" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = try createStore(tmp, "bound.db", "bound.wal", "bound.history", 70); defer store.history.deinit(); defer store.database.deinit(); var connection = try connection_mod.Connection.create(std.testing.allocator, &store.database, &store.history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try connection.stage(); _ = try connection.commit(&store.history); var sequence: usize = 0; var statement_buffer: [128]u8 = undefined; while (sequence < 24) : (sequence += 1) { const statement = try std.fmt.bufPrint(statement_buffer[0..], "INSERT INTO items (rowid, name) VALUES ({d}, 'value-{d}')", .{ sequence + 1, sequence }); try executeStatement(&connection, statement); try connection.stage(); _ = try connection.commit(&store.history); } const bytes = try exportBytesAll(std.testing.allocator, &store.history); defer std.testing.allocator.free(bytes); try std.testing.expect(bytes.len <= store.history.len());}test "sync pack import rejects tampered row chunks" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = try createStore(tmp, "tamper.db", "tamper.wal", "tamper.history", 71); defer store.history.deinit(); defer store.database.deinit(); var connection = try connection_mod.Connection.create(std.testing.allocator, &store.database, &store.history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (1, 'payload')"); try connection.stage(); _ = try connection.commit(&store.history); var pack = try exportAll(std.testing.allocator, &store.history); defer pack.deinit(); var tampered = false; for (pack.frames) |frame| { if (frame.kind != .row_chunk) continue; frame.payload[frame.payload.len - 1] +%= 1; tampered = true; break; } try std.testing.expect(tampered); const bytes = try encodePack(std.testing.allocator, &pack); defer std.testing.allocator.free(bytes); var target_store = try createStore(tmp, "tamper-target.db", "tamper-target.wal", "tamper-target.history", 72); defer target_store.history.deinit(); defer target_store.database.deinit(); try std.testing.expectError(error.InvalidHistory, importBytes(std.testing.allocator, &target_store.history, bytes));}test "sync record packs re-export from imported stores" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var source_store = try createStore(tmp, "relay-source.db", "relay-source.wal", "relay-source.history", 73); defer source_store.history.deinit(); defer source_store.database.deinit(); var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{}); defer source.deinit(); try executeStatement(&source, "CREATE TABLE items (name)"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try source.stage(); _ = try source.commit(&source_store.history); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (2, 'tip')"); try source.stage(); const tip_commit = try source.commit(&source_store.history); var middle_store = try createStore(tmp, "relay-middle.db", "relay-middle.wal", "relay-middle.history", 74); defer middle_store.history.deinit(); defer middle_store.database.deinit(); const source_bytes = try exportBytesAll(std.testing.allocator, &source_store.history); defer std.testing.allocator.free(source_bytes); _ = try importBytes(std.testing.allocator, &middle_store.history, source_bytes); var final_store = try createStore(tmp, "relay-final.db", "relay-final.wal", "relay-final.history", 75); defer final_store.history.deinit(); defer final_store.database.deinit(); const middle_bytes = try exportBytesAll(std.testing.allocator, &middle_store.history); defer std.testing.allocator.free(middle_bytes); _ = try importBytes(std.testing.allocator, &final_store.history, middle_bytes); try std.testing.expect(version.same(tip_commit, (try final_store.history.ref("main")).?.target)); const rows = try relationRows(&final_store.history, tip_commit, "items"); defer version.freeRelationRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 2), rows.len);}test "sync fetch writes remote tracking refs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "fetch-remote.db", "fetch-remote.wal", "fetch-remote.history", 10); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); try remote_store.history.putRef(.{ .name = "refs/tags/v1", .target = remote_commit, }); try remote_store.history.putRef(.{ .name = "side", .target = remote_commit, }); var local_store = try createStore(tmp, "fetch-local.db", "fetch-local.wal", "fetch-local.history", 11); defer local_store.history.deinit(); defer local_store.database.deinit(); const stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream" }); try std.testing.expectEqual(@as(usize, 3), stats.refs); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/main")).?.target)); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/side")).?.target)); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/tags/v1")).?.target)); try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/refs/tags/v1")) == null); const rows = try relationRows(&local_store.history, remote_commit, "items"); defer version.freeRelationRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 1), rows.len); try remote_store.history.deleteRef("side"); const stale_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream" }); try std.testing.expectEqual(@as(usize, 2), stale_stats.refs); try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/side")) != null); const prune_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "upstream", .prune = true }); try std.testing.expectEqual(@as(usize, 3), prune_stats.refs); try std.testing.expect((try local_store.history.ref("refs/remotes/upstream/side")) == null); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/upstream/main")).?.target)); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/tags/v1")).?.target));}test "sync fetch and push transfer only missing objects" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "missing-remote.db", "missing-remote.wal", "missing-remote.history", 11); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try remote.stage(); _ = try remote.commit(&remote_store.history); var local_store = try createStore(tmp, "missing-local.db", "missing-local.wal", "missing-local.history", 12); defer local_store.history.deinit(); defer local_store.database.deinit(); _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); const fetch_stats = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "origin" }); try std.testing.expectEqual(@as(usize, 1), fetch_stats.refs); try std.testing.expectEqual(@as(usize, 1), fetch_stats.commits); try std.testing.expect(fetch_stats.records != 0); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target)); const fetch_again = try fetch(std.testing.allocator, &local_store.history, &remote_store.history, .{ .remote = "origin" }); try std.testing.expectEqual(@as(usize, 1), fetch_again.refs); try std.testing.expectEqual(@as(usize, 0), fetch_again.commits); try std.testing.expectEqual(@as(usize, 0), fetch_again.records); try local_store.history.fastForwardBranch(std.testing.allocator, "main", remote_commit); var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{}); defer local.deinit(); try local.checkoutBranch(std.testing.allocator, &local_store.history, "main"); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local')"); try local.stage(); const local_commit = try local.commit(&local_store.history); const push_stats = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main"); try std.testing.expectEqual(@as(usize, 1), push_stats.refs); try std.testing.expectEqual(@as(usize, 1), push_stats.commits); try std.testing.expect(push_stats.records != 0); try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("main")).?.target));}test "sync negotiated fetch advertises refs and returns requested missing pack" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "negotiated-remote.db", "negotiated-remote.wal", "negotiated-remote.history", 12); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try remote.stage(); _ = try remote.commit(&remote_store.history); var local_store = try createStore(tmp, "negotiated-local.db", "negotiated-local.wal", "negotiated-local.history", 13); defer local_store.history.deinit(); defer local_store.database.deinit(); _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); var advertisement = try advertiseRefs(std.testing.allocator, &remote_store.history); defer advertisement.deinit(); const advertisement_bytes = try encodeAdvertisement(std.testing.allocator, &advertisement); defer std.testing.allocator.free(advertisement_bytes); var decoded_advertisement = try decodeAdvertisement(std.testing.allocator, advertisement_bytes); defer decoded_advertisement.deinit(); try std.testing.expect(version.same(remote_commit, decoded_advertisement.ref("main").?.target)); const wants = [_][]const u8{"main"}; var request = try fetchRequestFromHistory(std.testing.allocator, &local_store.history, wants[0..]); defer request.deinit(); const request_bytes = try encodeFetchRequest(std.testing.allocator, &request); defer std.testing.allocator.free(request_bytes); var decoded_request = try decodeFetchRequest(std.testing.allocator, request_bytes); defer decoded_request.deinit(); var pack = try exportForFetchRequest(std.testing.allocator, &remote_store.history, &decoded_request); defer pack.deinit(); try std.testing.expectEqual(@as(usize, 1), pack.refs.len); try std.testing.expectEqual(@as(usize, 1), pack.commits.len); try std.testing.expect(pack.frames.len != 0); const pack_bytes = try exportBytesForFetchRequest(std.testing.allocator, &remote_store.history, &decoded_request); defer std.testing.allocator.free(pack_bytes); _ = try fetchBytes(std.testing.allocator, &local_store.history, pack_bytes, .{ .remote = "origin" }); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target)); const broken_advertisement = try std.testing.allocator.dupe(u8, advertisement_bytes); defer std.testing.allocator.free(broken_advertisement); broken_advertisement[0] ^= 0xff; try std.testing.expectError(error.InvalidAdvertisement, decodeAdvertisement(std.testing.allocator, broken_advertisement)); const broken_request = try std.testing.allocator.dupe(u8, request_bytes); defer std.testing.allocator.free(broken_request); broken_request[0] ^= 0xff; try std.testing.expectError(error.InvalidFetchRequest, decodeFetchRequest(std.testing.allocator, broken_request));}test "sync byte packs import and fetch remote refs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "byte-remote.db", "byte-remote.wal", "byte-remote.history", 12); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); const bytes = try exportBytesAll(std.testing.allocator, &remote_store.history); defer std.testing.allocator.free(bytes); var imported_store = try createStore(tmp, "byte-imported.db", "byte-imported.wal", "byte-imported.history", 13); defer imported_store.history.deinit(); defer imported_store.database.deinit(); _ = try importBytes(std.testing.allocator, &imported_store.history, bytes); try std.testing.expect(version.same(remote_commit, (try imported_store.history.ref("main")).?.target)); const imported_rows = try relationRows(&imported_store.history, remote_commit, "items"); defer version.freeRelationRows(std.testing.allocator, imported_rows); try std.testing.expectEqual(@as(usize, 1), imported_rows.len); var fetched_store = try createStore(tmp, "byte-fetched.db", "byte-fetched.wal", "byte-fetched.history", 14); defer fetched_store.history.deinit(); defer fetched_store.database.deinit(); _ = try fetchBytes(std.testing.allocator, &fetched_store.history, bytes, .{ .remote = "origin" }); try std.testing.expect(version.same(remote_commit, (try fetched_store.history.ref("refs/remotes/origin/main")).?.target)); const broken = try std.testing.allocator.dupe(u8, bytes); defer std.testing.allocator.free(broken); broken[0] ^= 0xff; try std.testing.expectError(error.InvalidPack, importBytes(std.testing.allocator, &fetched_store.history, broken));}test "sync remote config stores named file remotes" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var origin = try RemoteConfigEntry.init(std.testing.allocator, "origin", "origin.history", null); defer origin.deinit(std.testing.allocator); var backup = try RemoteConfigEntry.initWithDefaultBranch(std.testing.allocator, "backup", "backup.history", "backup.lock", "main"); defer backup.deinit(std.testing.allocator); const entries = [_]RemoteConfigEntry{ origin, backup }; var config = try RemoteConfig.init(std.testing.allocator, entries[0..]); defer config.deinit(); try writeRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "remotes.bin" }, &config); var read = try readRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "remotes.bin" }); defer read.deinit(); try std.testing.expectEqual(@as(usize, 2), read.remotes.len); try std.testing.expectEqualStrings("origin.history", read.find("origin").?.history_path); try std.testing.expect(read.find("origin").?.default_branch == null); try std.testing.expect(read.find("missing") == null); const remote = try read.fileRemote(testing_io, tmp.dir, "backup"); try std.testing.expectEqualStrings("backup.history", remote.path); try std.testing.expectEqualStrings("backup.lock", remote.lock_path.?); try std.testing.expectEqualStrings("main", read.find("backup").?.default_branch.?); try std.testing.expectError(error.RemoteNotFound, read.fileRemote(testing_io, tmp.dir, "missing")); const duplicate = [_]RemoteConfigEntry{ origin, origin }; try std.testing.expectError(error.RemoteExists, RemoteConfig.init(std.testing.allocator, duplicate[0..])); try std.testing.expectError(error.InvalidRemoteName, RemoteConfigEntry.init(std.testing.allocator, "bad/name", "bad.history", null)); try std.testing.expectError(error.InvalidRemoteConfig, RemoteConfigEntry.initWithDefaultBranch(std.testing.allocator, "bad-branch", "bad.history", null, "")); var empty = try readRemoteConfig(std.testing.allocator, tmp.dir, .{ .path = "missing-remotes.bin" }); defer empty.deinit(); try std.testing.expectEqual(@as(usize, 0), empty.remotes.len); var legacy_bytes: std.ArrayList(u8) = .empty; defer legacy_bytes.deinit(std.testing.allocator); try appendU32(std.testing.allocator, &legacy_bytes, remote_config_magic); try appendU32(std.testing.allocator, &legacy_bytes, 1); try appendCount(std.testing.allocator, &legacy_bytes, 1); try appendBytes(std.testing.allocator, &legacy_bytes, "legacy"); try appendBytes(std.testing.allocator, &legacy_bytes, "legacy.history"); try appendU8(std.testing.allocator, &legacy_bytes, 0); var legacy = try decodeRemoteConfig(std.testing.allocator, legacy_bytes.items); defer legacy.deinit(); try std.testing.expectEqualStrings("legacy.history", legacy.find("legacy").?.history_path); try std.testing.expect(legacy.find("legacy").?.default_branch == null);}test "sync branch upstream config stores tracking metadata" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var main_origin = try BranchUpstreamEntry.init(std.testing.allocator, "main", "origin", "trunk"); defer main_origin.deinit(std.testing.allocator); var feature_backup = try BranchUpstreamEntry.init(std.testing.allocator, "feature", "backup", "feature"); defer feature_backup.deinit(std.testing.allocator); const entries = [_]BranchUpstreamEntry{ main_origin, feature_backup }; var config = try BranchUpstreamConfig.init(std.testing.allocator, entries[0..]); defer config.deinit(); try writeBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "upstreams.bin" }, &config); var read = try readBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "upstreams.bin" }); defer read.deinit(); try std.testing.expectEqual(@as(usize, 2), read.upstreams.len); try std.testing.expectEqualStrings("origin", read.find("main").?.remote); try std.testing.expectEqualStrings("trunk", read.find("main").?.remote_branch); try std.testing.expectEqualStrings("backup", read.find("feature").?.remote); try std.testing.expect(read.find("missing") == null); var duplicate = try BranchUpstreamEntry.init(std.testing.allocator, "main", "backup", "main"); defer duplicate.deinit(std.testing.allocator); const duplicate_entries = [_]BranchUpstreamEntry{ main_origin, duplicate }; try std.testing.expectError(error.BranchUpstreamExists, BranchUpstreamConfig.init(std.testing.allocator, duplicate_entries[0..])); try std.testing.expectError(error.InvalidRemoteName, BranchUpstreamEntry.init(std.testing.allocator, "main", "bad/name", "main")); try std.testing.expectError(error.InvalidBranchUpstreamConfig, BranchUpstreamEntry.init(std.testing.allocator, "", "origin", "main")); try std.testing.expectError(error.InvalidBranchUpstreamConfig, BranchUpstreamEntry.init(std.testing.allocator, "main", "origin", "")); var empty = try readBranchUpstreamConfig(std.testing.allocator, tmp.dir, .{ .path = "missing-upstreams.bin" }); defer empty.deinit(); try std.testing.expectEqual(@as(usize, 0), empty.upstreams.len); const bytes = try encodeBranchUpstreamConfig(std.testing.allocator, &config); defer std.testing.allocator.free(bytes); const broken = try std.testing.allocator.dupe(u8, bytes); defer std.testing.allocator.free(broken); broken[0] ^= 0xff; try std.testing.expectError(error.InvalidBranchUpstreamConfig, decodeBranchUpstreamConfig(std.testing.allocator, broken));}test "sync file remote lock rejects concurrent access" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const remote = FileRemote{ .dir = tmp.dir, .path = "locked.history", }; var first = try lockFileRemote(std.testing.allocator, remote); try std.testing.expectError(error.RemoteBusy, lockFileRemote(std.testing.allocator, remote)); first.release(); var second = try lockFileRemote(std.testing.allocator, remote); second.release();}test "sync file transfers require an existing remote history" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const remote = FileRemote{ .dir = tmp.dir, .path = "absent.history", }; var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "transfer-target.history", .recovery = .reject }); defer target.deinit(); try std.testing.expectError(error.HistoryNotFound, cloneFile(std.testing.allocator, remote, &target)); try std.testing.expectError(error.HistoryNotFound, fetchFile(std.testing.allocator, &target, remote, .{})); try std.testing.expectError(error.HistoryNotFound, pullFileFastForward(std.testing.allocator, &target, remote, "main")); try std.testing.expectError(error.HistoryNotFound, deleteFileRemoteRef(std.testing.allocator, remote, "main")); try std.testing.expectError(error.FileNotFound, tmp.dir.readFileAlloc(testing_io, "absent.history", std.testing.allocator, .unlimited));}test "sync file remotes reject truncated history without repairing it" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var clean_length: usize = 0; { var remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "truncated-remote.history", .recovery = .reject, }); defer remote.deinit(); try remote.putCommit(version.Commit.init(version.emptyHash("truncated-remote"), &.{})); clean_length = remote.len(); } const corrupted_length = clean_length + "corrupt".len; { var remote_file = try tmp.dir.createFile(testing_io, "truncated-remote.history", .{ .read = true, .truncate = false }); try remote_file.writePositionalAll(testing_io, "corrupt", clean_length); try remote_file.setLength(testing_io, corrupted_length); remote_file.close(testing_io); } var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "truncated-target.history", .recovery = .reject, }); defer target.deinit(); try std.testing.expectError(error.TruncatedHistory, cloneFile(std.testing.allocator, .{ .dir = tmp.dir, .path = "truncated-remote.history", }, &target)); try std.testing.expectEqual(corrupted_length, @as(usize, @intCast((try tmp.dir.statFile(testing_io, "truncated-remote.history", .{})).size)));}test "sync clone copies committed conflict roots" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var source_store = try createStore(tmp, "conflict-source.db", "conflict-source.wal", "conflict-source.history", 15); defer source_store.history.deinit(); defer source_store.database.deinit(); var source = try connection_mod.Connection.create(std.testing.allocator, &source_store.database, &source_store.history, .{}); defer source.deinit(); try executeStatement(&source, "CREATE TABLE items (name)"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try source.stage(); _ = try source.commit(&source_store.history); _ = try source.createBranch(&source_store.history, "side"); try source.checkoutBranch(std.testing.allocator, &source_store.history, "side"); try executeStatement(&source, "DELETE FROM items WHERE rowid = 1"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'side')"); try source.stage(); const side_commit = try source.commit(&source_store.history); try source.checkoutBranch(std.testing.allocator, &source_store.history, "main"); try executeStatement(&source, "DELETE FROM items WHERE rowid = 1"); try executeStatement(&source, "INSERT INTO items (rowid, name) VALUES (1, 'main')"); try source.stage(); _ = try source.commit(&source_store.history); var merged = try source.mergeBranch(std.testing.allocator, &source_store.history, "side", .{}); defer merged.deinit(); try std.testing.expect(merged.hasConflicts()); const conflict_root = merged.conflict_root.hash; const conflict_hash = merged.artifacts[0].hash; try source.stage(); const merge_commit = try source.mergeCommit(&source_store.history, side_commit); var target_store = try createStore(tmp, "conflict-target.db", "conflict-target.wal", "conflict-target.history", 16); defer target_store.history.deinit(); defer target_store.database.deinit(); _ = try cloneAll(std.testing.allocator, &source_store.history, &target_store.history); try std.testing.expect(version.same(merge_commit, (try target_store.history.ref("main")).?.target)); const commit = try target_store.history.commitValue(merge_commit); var clone = try connection_mod.Connection.open(std.testing.allocator, &target_store.database, &target_store.history, .{}); defer clone.deinit(); try clone.checkoutBranch(std.testing.allocator, &target_store.history, "main"); try std.testing.expect(version.same(commit.root, (try clone.workingRoot()))); var artifacts = try clone.conflictArtifacts(std.testing.allocator, &target_store.history); defer artifacts.deinit(); try std.testing.expect(version.same(conflict_root, artifacts.root.hash)); try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len); try std.testing.expect(version.same(conflict_hash, artifacts.artifacts[0].hash));}test "sync pack import rejects a conflict root without its artifact" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var source = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "missing-conflict-source.history", .recovery = .reject, }); defer source.deinit(); const artifact = version.ConflictArtifact.init("items", 1, "base", "ours", "theirs"); try source.putConflict(artifact); const root = try source.putConflictRoot(&.{artifact.entry()}); var payload: std.ArrayList(u8) = .empty; defer payload.deinit(std.testing.allocator); try source.appendPackRecordPayload(std.testing.allocator, &payload, .conflict_root, root.hash); var frames = [_]PackFrame{.{ .kind = .conflict_root, .payload = payload.items }}; const incomplete = Pack{ .allocator = std.testing.allocator, .refs = &.{}, .commits = &.{}, .frames = frames[0..], }; var target = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "missing-conflict-target.history", .recovery = .reject, }); defer target.deinit(); try std.testing.expectError(error.InvalidHistory, importObjects(&target, &incomplete));}test "sync push fast forwards remote refs and rejects divergence" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "push-remote.db", "push-remote.wal", "push-remote.history", 20); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try remote.stage(); _ = try remote.commit(&remote_store.history); var local_store = try createStore(tmp, "push-local.db", "push-local.wal", "push-local.history", 21); defer local_store.history.deinit(); defer local_store.database.deinit(); _ = try cloneAll(std.testing.allocator, &remote_store.history, &local_store.history); var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{}); defer local.deinit(); try local.checkoutBranch(std.testing.allocator, &local_store.history, "main"); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (2, 'local')"); try local.stage(); const local_commit = try local.commit(&local_store.history); _ = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main"); try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("main")).?.target)); _ = try pushFastForwardTo(std.testing.allocator, &local_store.history, &remote_store.history, "main", "trunk"); try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("trunk")).?.target)); try local_store.history.putRef(.{ .name = "refs/tags/v1", .target = local_commit, }); _ = try pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "refs/tags/v1"); try std.testing.expect(version.same(local_commit, (try remote_store.history.ref("refs/tags/v1")).?.target)); const pushed_rows = try relationRows(&remote_store.history, local_commit, "items"); defer version.freeRelationRows(std.testing.allocator, pushed_rows); try std.testing.expectEqual(@as(usize, 2), pushed_rows.len); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local-again')"); try local.stage(); const local_divergent = try local.commit(&local_store.history); try remote.checkoutBranch(std.testing.allocator, &remote_store.history, "main"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (4, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); try std.testing.expectError(error.NonFastForward, pushFastForward(std.testing.allocator, &local_store.history, &remote_store.history, "main")); try std.testing.expect(version.same(remote_commit, (try remote_store.history.ref("main")).?.target)); try std.testing.expectError(error.CommitNotFound, remote_store.history.commitValue(local_divergent));}test "sync file remotes clone fetch push and pull refs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var remote_store = try createStore(tmp, "file-remote.db", "file-remote.wal", "file-remote.history", 30); defer remote_store.history.deinit(); defer remote_store.database.deinit(); var remote = try connection_mod.Connection.create(std.testing.allocator, &remote_store.database, &remote_store.history, .{}); defer remote.deinit(); try executeStatement(&remote, "CREATE TABLE items (name)"); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try remote.stage(); const base_commit = try remote.commit(&remote_store.history); var local_store = try createStore(tmp, "file-local.db", "file-local.wal", "file-local.history", 31); defer local_store.history.deinit(); defer local_store.database.deinit(); const remote_file = FileRemote{ .dir = tmp.dir, .path = "file-remote.history", }; _ = try cloneFile(std.testing.allocator, remote_file, &local_store.history); try std.testing.expect(version.same(base_commit, (try local_store.history.ref("main")).?.target)); try executeStatement(&remote, "INSERT INTO items (rowid, name) VALUES (2, 'remote')"); try remote.stage(); const remote_commit = try remote.commit(&remote_store.history); _ = try fetchFile(std.testing.allocator, &local_store.history, remote_file, .{ .remote = "origin" }); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("refs/remotes/origin/main")).?.target)); _ = try pullFileFastForward(std.testing.allocator, &local_store.history, remote_file, "main"); try std.testing.expect(version.same(remote_commit, (try local_store.history.ref("main")).?.target)); var local = try connection_mod.Connection.open(std.testing.allocator, &local_store.database, &local_store.history, .{}); defer local.deinit(); try local.checkoutBranch(std.testing.allocator, &local_store.history, "main"); try executeStatement(&local, "INSERT INTO items (rowid, name) VALUES (3, 'local')"); try local.stage(); const local_commit = try local.commit(&local_store.history); _ = try pushFileFastForward(std.testing.allocator, &local_store.history, remote_file, "main"); { var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject }); defer verified_remote.deinit(); try std.testing.expect(version.same(local_commit, (try verified_remote.ref("main")).?.target)); const rows = try relationRows(&verified_remote, local_commit, "items"); defer version.freeRelationRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 3), rows.len); } try local_store.history.putRef(.{ .name = "refs/tags/file-v1", .target = local_commit, }); _ = try pushFileFastForward(std.testing.allocator, &local_store.history, remote_file, "refs/tags/file-v1"); { var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject }); defer verified_remote.deinit(); try std.testing.expect(version.same(local_commit, (try verified_remote.ref("refs/tags/file-v1")).?.target)); } const delete_stats = try deleteFileRemoteRef(std.testing.allocator, remote_file, "refs/tags/file-v1"); try std.testing.expectEqual(@as(usize, 1), delete_stats.refs); { var verified_remote = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "file-remote.history", .recovery = .reject }); defer verified_remote.deinit(); try std.testing.expect((try verified_remote.ref("refs/tags/file-v1")) == null); } try std.testing.expectError(error.RefNotFound, deleteFileRemoteRef(std.testing.allocator, remote_file, "refs/tags/file-v1"));}Audit
| Definitions | 23 |
|---|---|
| Public names | 23 |
| Members | 17 |
| Version | 26.7.0 |
| Revision | daab053ee433 |