╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#75e02678115d02ab65ebc976d63a01cf847c1a41",│ │ 7 - .hash = "xit-0.0.0-Ub3nDAugUAHIT-CZ2To43s9yf6EHZy83HjS7JIs-wCiP", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#f56a6e23026c7a426e0f4646f6844b7ef91204eb",│ │ 7 + .hash = "xit-0.0.0-Ub3nDBqiUAHF5XHUn74V2wGkxS8jg_nG_jzzFobB2KOt", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │ 98 } │ │ 99 }; │ │100 │ │101 -// consume the events on `ref` into the db the repo's views read: the repo's │ │102 -// own db for a xit repo, or the standalone event db next to a git repo │ │101 +// commit `events` (if any) as JSON commit messages on `ref`, then consume the│ │102 +// events on `ref` into the db the repo's views read: the repo's own db for a │ │103 +// xit repo, or the standalone event db next to a git repo. for a xit repo the│ │104 +// commits and the consume run in one transaction. │ │105 pub fn consume( │ │106 comptime repo_kind: rp.RepoKind, │ │107 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │109 allocator: std.mem.Allocator, │ │110 repo: *rp.Repo(repo_kind, repo_opts), │ │111 ref: rf.Ref, │ │112 + events: []const EventWithId, │ │113 ) !void { │ │111 - // a branch that only exists on a remote is consumed from its remote-tracking ref │ │112 - const resolved_ref: rf.Ref, const tip: [hash.hexLen(repo_opts.hash)]u8 = blk: { │ │113 - if (try repo.readRef(io, ref)) |oid| break :blk .{ ref, oid }; │ │114 + // a branch that only exists on a remote is consumed from its remote-tracking │ │115 + // ref, while commits go on the local branch, continuing from the remote tip │ │116 + // so a cloned repo's local branch stays connected to the server history │ │117 + const resolved_ref: rf.Ref, const first_parent_oids: ?[1][hash.hexLen(repo_opts.hash)]u8 = blk: {│ │118 + if (null != try repo.readRef(io, ref)) break :blk .{ ref, null }; │ │119 var remotes = try repo.listRemotes(io, allocator); │ │120 defer remotes.deinit(); │ │121 for (remotes.sections.keys()) |remote_name| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │122 const remote_ref: rf.Ref = .{ .kind = .{ .remote = remote_name }, .name = ref.name };│ │118 - if (try repo.readRef(io, remote_ref)) |oid| break :blk .{ remote_ref, oid }; │ │123 + if (try repo.readRef(io, remote_ref)) |oid| { │ │124 + if (events.len > 0) break :blk .{ ref, .{oid} }; │ │125 + break :blk .{ remote_ref, null }; │ │126 + } │ │127 } │ │128 + if (events.len > 0) break :blk .{ ref, null }; │ │129 // the branch is gone (or never existed): drop any db from a previous │ │130 // sync rather than serving its issues indefinitely │ │131 if (repo_kind == .git) try LocalEventDB(repo_opts.hash).delete(io, repo.core.repo_dir); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │132 return; │ │133 }; │ │125 - var tip_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined;│ │126 - _ = try std.fmt.hexToBytes(&tip_bytes, &tip); │ │134 │ │135 switch (repo_kind) { │ │136 .git => { │ ╰─────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │137 + try commitEvents(.git, repo_opts, .{ .core = &repo.core, .extra = .{} }, io, allocator, ref, events, first_parent_oids);│ │138 + │ │139 var event_db = try LocalEventDB(repo_opts.hash).open(io, allocator, repo.core.repo_dir); │ │140 defer event_db.deinit(io, allocator); │ │132 - │ │133 - if (try lastConsumedOid(repo_opts.hash, event_db.db)) |last_oid| { │ │134 - if (std.mem.eql(u8, &last_oid, &tip_bytes)) return; │ │135 - } │ │141 │ │142 try event_db.consume(repo_kind, repo_opts, io, allocator, repo, resolved_ref); │ │143 }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │150 io: std.Io, │ │151 allocator: std.mem.Allocator, │ │152 ref: rf.Ref, │ │153 + events: []const EventWithId, │ │154 + first_parent_oids: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │155 │ │156 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void {│ │157 var moment = try DB.HashMap(.read_write).init(cursor.*); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │152 - try consumeInTransaction(.xit, repo_opts, state.readOnly(), &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.ref); │ │159 + │ │160 + try commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, ctx.first_parent_oids); │ │161 + if (!try consumeInTransaction(.xit, repo_opts, state.readOnly(), &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction;│ │162 + try xit.undo.writeMessage(repo_opts, state, .{ .custom = "event" }); │ │163 + │ │164 + // fsync the chunk store, so any chunks written by the │ │165 + // commits are durable before the transaction commits │ │166 + try ctx.core.chunk_store_file.sync(ctx.io); │ │167 } │ │168 }; │ │169 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │170 try repo.core.db_file.lock(io, .exclusive); │ │171 defer repo.core.db_file.unlock(io); │ │172 │ │159 - if (try lastConsumedOid(repo_opts.hash, &repo.core.db)) |last_oid| { │ │160 - if (std.mem.eql(u8, &last_oid, &tip_bytes)) return; │ │161 - } │ │162 - │ │173 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor());│ │164 - try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │174 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │175 .core = &repo.core, │ │176 .io = io, │ │177 .allocator = allocator, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │178 .ref = resolved_ref, │ │169 - }); │ │179 + .events = events, │ │180 + .first_parent_oids = first_parent_oids, │ │181 + }) catch |err| switch (err) { │ │182 + error.CancelTransaction => {}, │ │183 + else => |e| return e, │ │184 + }; │ │185 }, │ │186 + } │ │187 +} │ │188 + │ │189 +// commit each event as a JSON commit message on `ref` through `state`, so a │ │190 +// xit repo can write them inside an already-open transaction │ │191 +fn commitEvents( │ │192 + comptime repo_kind: rp.RepoKind, │ │193 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │194 + state: rp.Repo(repo_kind, repo_opts).State(.read_write), │ │195 + io: std.Io, │ │196 + allocator: std.mem.Allocator, │ │197 + ref: rf.Ref, │ │198 + events: []const EventWithId, │ │199 + first_parent_oids: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │200 +) !void { │ │201 + var parent_oids: ?[]const [hash.hexLen(repo_opts.hash)]u8 = if (first_parent_oids) |*oids| oids else null; │ │202 + │ │203 + var json: std.Io.Writer.Allocating = .init(allocator); │ │204 + defer json.deinit(); │ │205 + │ │206 + for (events) |event| { │ │207 + json.clearRetainingCapacity(); │ │208 + try std.json.Stringify.value(event, .{ .whitespace = .indent_2 }, &json.writer); │ │209 + _ = try obj.writeCommit(repo_kind, repo_opts, state, io, allocator, .{ .author = "haxy <user@haxy>", .message = json.written(), .timestamp = event.timestamp, .parent_oids = parent_oids }, null, ref);│ │210 + // later events parent on the ref's new tip │ │211 + parent_oids = null; │ │212 } │ │213 } │ │214 + │ │215 +// serve a receive-pack and consume any events it pushed to the events branch, │ │216 +// all in one transaction: the push and the views derived from it commit │ │217 +// atomically, and a failed consume cancels the push │ │218 +pub fn receivePackAndConsume( │ │219 + comptime repo_opts: rp.RepoOpts(.xit), │ │220 + io: std.Io, │ │221 + allocator: std.mem.Allocator, │ │222 + repo: *rp.Repo(.xit, repo_opts), │ │223 + reader: *std.Io.Reader, │ │224 + writer: *std.Io.Writer, │ │225 + options: xit.net_server_receive_pack.Options, │ │226 +) !void { │ │227 + const DB = rp.Repo(.xit, repo_opts).DB; │ │228 + const State = rp.Repo(.xit, repo_opts).State; │ │229 │ │230 + const Ctx = struct { │ │231 + core: *rp.Repo(.xit, repo_opts).Core, │ │232 + io: std.Io, │ │233 + allocator: std.mem.Allocator, │ │234 + reader: *std.Io.Reader, │ │235 + writer: *std.Io.Writer, │ │236 + options: xit.net_server_receive_pack.Options, │ │237 + │ │238 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │239 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │240 + const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │241 + try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, ctx.options); │ │242 + try xit.undo.writeMessage(repo_opts, state, .push); │ │243 + │ │244 + // a repo without an events branch has no events to consume. a │ │245 + // no-op consume must not cancel here, since the push shares the │ │246 + // transaction and must commit regardless. │ │247 + if (null == try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = events_ref })) return; │ │248 + _ = try consumeInTransaction(.xit, repo_opts, state.readOnly(), &ctx.core.db, &moment, ctx.io, ctx.allocator, events_ref); │ │249 + } │ │250 + }; │ │251 + │ │252 + try repo.core.db_file.lock(io, .exclusive); │ │253 + defer repo.core.db_file.unlock(io); │ │254 + │ │255 + const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │256 + history.appendContext( │ │257 + .{ .slot = try history.getSlot(-1) }, │ │258 + Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .reader = reader, .writer = writer, .options = options }, │ │259 + ) catch |err| switch (err) { │ │260 + error.CancelTransaction => {}, │ │261 + else => |e| return e, │ │262 + }; │ │263 + │ │264 + // pkt-line writes are buffered until the transaction settles, so the │ │265 + // report-status reaches the client only after the push truly committed │ │266 + try writer.flush(); │ │267 +} │ │268 + │ │269 // a standalone event db holding events consumed from a local repo's events │ │270 // branch. it lives inside the repo dir but is not part of the repo's own data, │ │271 // so viewing a repo never mutates it. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯