╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────╮ │ 9 title: []const u8, │ │10 description: []const u8, │ │11 tags: []const u8, // space-separated │ │12 +source_branch: ?[]const u8 = null, │ │13 target_branch: []const u8, │ │14 target_patch_id: ?[evt.event_id_size * 2]u8 = null,│ │15 revision: ?Revision = null, │ ╰───────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │134 pub const status_to_id_set_key = "status->patch-id-set"; │ │135 pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; │ │136 pub const revision_to_id_set_key = "target-branch+oid->patch-id-set"; │ │137 +pub const source_to_id_set_key = "source-branch->patch-id-set"; │ │138 │ │139 pub const TagStatusKey = [tag_max_len + 1 + StatusKind.longest_len]u8;│ │140 │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │181 │ │182 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; │ │183 if (!rf.validateName(record.event.target_branch)) return error.InvalidTarget; │ │184 + if (record.event.source_branch) |branch| { │ │185 + if (!rf.validateName(branch) or std.mem.eql(u8, branch, evt.events_ref.name) or │ │186 + std.mem.eql(u8, record.event.target_branch, evt.events_ref.name)) return error.InvalidSourceBranch;│ │187 + } │ │188 const revision_id = if (record.event.revision) |*revision| blk: { │ │189 try evt.PatchRev.validateOid(hash_kind, revision.squash_oid); │ │190 try evt.PatchRev.validateOid(hash_kind, revision.source_oid); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │197 const status_kind = record.event.status.kind(); │ │198 │ │199 if (existing_maybe) |existing| { │ │200 + if (!evt.fieldEqual(?[]const u8, existing.event.source_branch, record.event.source_branch)) return error.PatchSourceChanged;│ │201 record.created_order = existing.created_order; │ │202 record.author_email = existing.author_email; │ │203 const existing_status_kind = existing.event.status.kind(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │242 } │ │243 } │ │244 } │ │245 + } │ │246 + } │ │247 + │ │248 + if (record.event.source_branch) |branch| { │ │249 + const sources = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, source_to_id_set_key)));│ │250 + if (!record.removed and status_kind != .merged) { │ │251 + const ids = try DB.CountedHashSet(.read_write).init(try sources.putCursor(branch)); │ │252 + try ids.put(record_key, .{ .bytes = event_id }); │ │253 + } else if (try sources.getCursor(branch) != null) { │ │254 + const ids = try DB.CountedHashSet(.read_write).init(try sources.putCursor(branch)); │ │255 + _ = try ids.remove(record_key); │ │256 + if (try ids.count() == 0) _ = try sources.remove(branch); │ │257 } │ │258 } │ │259 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │345 }; │ │346 }, │ │347 .fields => |fields| { │ │329 - if (!rf.validateName(fields.target_branch)) return error.InvalidFields; │ │330 - if ((try repo.readRef(io, .{ .kind = .head, .name = fields.target_branch })) == null) return error.InvalidFields; │ │348 + if (!rf.validateName(fields.target_branch)) return error.InvalidTargetBranch; │ │349 + if ((try repo.readRef(io, .{ .kind = .head, .name = fields.target_branch })) == null) return error.InvalidTargetBranch;│ │350 updated.title = fields.title; │ │351 updated.tags = fields.tags; │ │352 updated.description = fields.description; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │359 } │ │360 if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; │ │361 │ │362 + if (updated.source_branch != null and updated.revision == null) return @import("../patch.zig").writeBranchPatch(host_kind, repo_kind, repo_opts, io, allocator, repo, std.fmt.bytesToHex(id.*, .lower), updated, record.event, author);│ │363 try evt.consume(host_kind, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ │ │364 .id = std.fmt.bytesToHex(id.*, .lower), │ │365 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │436 return false; │ │437 } │ │438 │ │419 -fn readFromRepo( │ │439 +pub fn readFromRepo( │ │440 comptime repo_kind: rp.RepoKind, │ │441 comptime repo_opts: rp.RepoOpts(repo_kind),│ │442 io: std.Io, │ ╰────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/event/PatchRev.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 pub const merge_policy: evt.MergePolicy = .target_wins; │ │27 pub const record_map_key = "event-id->patchrev"; │ │28 pub const all_id_set_key = "patchrev-id-set"; │ │29 + │ │30 +// prepare a revision event and its patch reference in the caller's arena │ │31 +pub fn prepare( │ │32 + comptime repo_kind: rp.RepoKind, │ │33 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │34 + state: rp.Repo(repo_kind, repo_opts).State(.read_write), │ │35 + io: std.Io, │ │36 + arena: *std.heap.ArenaAllocator, │ │37 + base_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │38 + source_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │39 + message: []const u8, │ │40 + author: evt.CommitAuthor, │ │41 + timestamp: u64, │ │42 +) !struct { event: evt.EventWithId, revision: evt.Patch.Revision } { │ │43 + const allocator = arena.allocator(); │ │44 + var base_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, base_oid); │ │45 + defer base_object.deinit(); │ │46 + var source_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, source_oid); │ │47 + defer source_object.deinit(); │ │48 + const revision = Self{ .base_oid = try allocator.dupe(u8, base_oid), .source_oid = try allocator.dupe(u8, source_oid), .message = message }; │ │49 + const identity = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ author.name, author.email }); │ │50 + defer allocator.free(identity); │ │51 + const squash = try writeSquashCommit(repo_kind, repo_opts, state, io, allocator, revision, &source_object.content.commit.tree, identity, identity, timestamp);│ │52 + var id: [evt.event_id_size]u8 = undefined; │ │53 + io.random(&id); │ │54 + const id_hex = std.fmt.bytesToHex(id, .lower); │ │55 + const entries = try allocator.alloc(evt.EventTreeEntry, 2); │ │56 + entries[0] = .{ .tree = .{ .name = "base", .oid = try allocator.dupe(u8, &base_object.content.commit.tree) } }; │ │57 + entries[1] = .{ .tree = .{ .name = "head", .oid = try allocator.dupe(u8, &source_object.content.commit.tree) } }; │ │58 + return .{ │ │59 + .event = .{ .id = id_hex, .author = author, .timestamp = timestamp, .tree_entries = entries, .event = .{ .patchrev = revision } }, │ │60 + .revision = .{ .id = id_hex, .source_oid = revision.source_oid, .squash_oid = try allocator.dupe(u8, &squash) }, │ │61 + }; │ │62 +} │ │63 │ │64 // create the mergeable squash commit represented by this patch revision │ │65 pub fn writeSquashCommit( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 8 const mrg = xit.merge; │ │ 9 const fork = @import("fork.zig"); │ │ 10 const serve_common = @import("serve_common.zig"); │ │ 11 + │ │ 12 +// capture an existing branch without creating a fork │ │ 13 +pub fn writeBranchPatch( │ │ 14 + host_kind: evt.HostKind, │ │ 15 + comptime repo_kind: rp.RepoKind, │ │ 16 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 17 + io: std.Io, │ │ 18 + allocator: std.mem.Allocator, │ │ 19 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 20 + id: [evt.event_id_size * 2]u8, │ │ 21 + patch: evt.Patch, │ │ 22 + expected_patch: ?evt.Patch, │ │ 23 + author: evt.CommitAuthor, │ │ 24 +) !void { │ │ 25 + var arena = std.heap.ArenaAllocator.init(allocator); │ │ 26 + defer arena.deinit(); │ │ 27 + if (repo_kind == .git) { │ │ 28 + if (expected_patch) |expected| { │ │ 29 + const current = (try evt.Patch.readFromRepo(repo_kind, repo_opts, io, allocator, &arena, repo, &try evt.parseEventId(&id))) orelse return error.PatchOutOfDate; │ │ 30 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 31 + } │ │ 32 + const events = try branchEvents(repo_kind, repo_opts, .{ .core = &repo.core, .extra = .{} }, io, &arena, id, patch, author); │ │ 33 + try evt.consume(host_kind, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &events); │ │ 34 + } else { │ │ 35 + const DB = evt.EventDB(repo_opts.hash); │ │ 36 + var first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8 = null; │ │ 37 + if (try repo.readRef(io, evt.events_ref) == null) { │ │ 38 + var remotes = try repo.listRemotes(io, allocator); │ │ 39 + defer remotes.deinit(); │ │ 40 + for (remotes.sections.keys()) |name| { │ │ 41 + if (try repo.readRef(io, .{ .kind = .{ .remote = name }, .name = evt.events_ref.name })) |oid| { │ │ 42 + first_parent = .{oid}; │ │ 43 + break; │ │ 44 + } │ │ 45 + } │ │ 46 + } │ │ 47 + const Ctx = struct { │ │ 48 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 49 + io: std.Io, │ │ 50 + arena: *std.heap.ArenaAllocator, │ │ 51 + id: [evt.event_id_size * 2]u8, │ │ 52 + patch: evt.Patch, │ │ 53 + expected_patch: ?evt.Patch, │ │ 54 + author: evt.CommitAuthor, │ │ 55 + first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │ 56 + │ │ 57 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 58 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 59 + const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │ 60 + if (ctx.expected_patch) |expected| { │ │ 61 + const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │ 62 + const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, ctx.arena, &try evt.parseEventId(&ctx.id))) orelse return error.PatchOutOfDate; │ │ 63 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 64 + } │ │ 65 + const events = try branchEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.author); │ │ 66 + const parent = if (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │ 67 + try evt.commitEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena.child_allocator, evt.events_ref, &events, parent); │ │ 68 + if (!try evt.consumeInTransaction(.repo, repo_kind, repo_opts, state, &ctx.repo.core.db, &moment, ctx.io, ctx.arena.child_allocator, evt.events_ref)) return error.CancelTransaction; │ │ 69 + try xit.undo.writeMessage(repo_opts, state, .{ .custom = "update branch patch" }); │ │ 70 + } │ │ 71 + }; │ │ 72 + { │ │ 73 + try repo.core.db_file.lock(io, .exclusive); │ │ 74 + defer repo.core.db_file.unlock(io); │ │ 75 + const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │ 76 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo = repo, .io = io, .arena = &arena, .id = id, .patch = patch, .expected_patch = expected_patch, .author = author, .first_parent = first_parent });│ │ 77 + } │ │ 78 + if (host_kind == .server) refreshMergeability(repo_opts, io, allocator, repo, try evt.parseEventId(&id)); │ │ 79 + } │ │ 80 +} │ │ 81 + │ │ 82 +fn branchEvents( │ │ 83 + comptime repo_kind: rp.RepoKind, │ │ 84 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 85 + state: rp.Repo(repo_kind, repo_opts).State(.read_write), │ │ 86 + io: std.Io, │ │ 87 + arena: *std.heap.ArenaAllocator, │ │ 88 + id: [evt.event_id_size * 2]u8, │ │ 89 + patch: evt.Patch, │ │ 90 + author: evt.CommitAuthor, │ │ 91 +) ![2]evt.EventWithId { │ │ 92 + const branch = patch.source_branch orelse return error.InvalidSourceBranch; │ │ 93 + if (!evt.Patch.fieldsValid(patch.title, patch.tags)) return error.InvalidFields; │ │ 94 + if (!rf.validateName(branch) or std.mem.eql(u8, branch, evt.events_ref.name)) return error.InvalidSourceBranch; │ │ 95 + if (!rf.validateName(patch.target_branch) or std.mem.eql(u8, patch.target_branch, evt.events_ref.name)) return error.InvalidTargetBranch; │ │ 96 + const source = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse return error.InvalidSourceBranch; │ │ 97 + const target = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } })) orelse return error.InvalidTargetBranch; │ │ 98 + const base = mrg.commonAncestor(repo_kind, repo_opts, state.readOnly(), io, arena.allocator(), &target, &source) catch |err| switch (err) { │ │ 99 + error.NoCommonAncestor => return error.UnrelatedBranches, │ │100 + else => return err, │ │101 + }; │ │102 + const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │103 + const prepared = try evt.PatchRev.prepare(repo_kind, repo_opts, state, io, arena, &base, &source, patch.title, author, timestamp); │ │104 + var updated = patch; │ │105 + updated.revision = prepared.revision; │ │106 + return .{ │ │107 + prepared.event, │ │108 + .{ .id = id, .author = author, .timestamp = timestamp, .event = .{ .patch = updated } }, │ │109 + }; │ │110 +} │ │111 + │ │112 +// refresh tracked branches outside the caller's transaction │ │113 +pub fn refreshBranches( │ │114 + host_kind: evt.HostKind, │ │115 + comptime repo_kind: rp.RepoKind, │ │116 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │117 + io: std.Io, │ │118 + allocator: std.mem.Allocator, │ │119 + repo: *rp.Repo(repo_kind, repo_opts), │ │120 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │121 +) !void { │ │122 + const DB = evt.EventDB(repo_opts.hash); │ │123 + var arena = std.heap.ArenaAllocator.init(allocator); │ │124 + defer arena.deinit(); │ │125 + var patches: std.ArrayList(struct { id: [evt.event_id_size]u8, record: evt.Patch.Record }) = .empty; │ │126 + defer patches.deinit(allocator); │ │127 + { │ │128 + var local_db: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null; │ │129 + defer if (local_db) |*db| db.deinit(io, allocator); │ │130 + const moment = (if (local_db) |*db| evt.currentMomentFromDb(repo_opts.hash, db.db) else if (repo_kind == .git) return else evt.currentMoment(repo_opts, repo)) catch |err| switch (err) { │ │131 + error.NotFound => return, │ │132 + else => return err, │ │133 + }; │ │134 + const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse return; │ │135 + const sources = try DB.SortedMap(.read_only).init(source_cursor); │ │136 + const conflicts = if (try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key))) |cursor| try DB.SortedMap(.read_only).init(cursor) else null; │ │137 + var iter = try sources.iterator(); │ │138 + while (try iter.next()) |cursor| { │ │139 + const pair = try cursor.readKeyValuePair(); │ │140 + const branch = try pair.key_cursor.readBytesAlloc(arena.allocator(), null); │ │141 + if (updates) |changed| { │ │142 + var found = false; │ │143 + for (changed) |update| { │ │144 + if (std.mem.startsWith(u8, update.ref_name, "refs/heads/") and std.mem.eql(u8, update.ref_name["refs/heads/".len..], branch)) { │ │145 + found = true; │ │146 + break; │ │147 + } │ │148 + } │ │149 + if (!found) continue; │ │150 + } │ │151 + const source = (try repo.readRef(io, .{ .kind = .head, .name = branch })) orelse { │ │152 + std.log.warn("patch source branch missing: {s}", .{branch}); │ │153 + continue; │ │154 + }; │ │155 + const ids = try DB.CountedHashSet(.read_only).init(pair.value_cursor); │ │156 + var ids_iter = try ids.iterator(); │ │157 + while (try ids_iter.next()) |id_cursor| { │ │158 + const id_pair = try id_cursor.readKeyValuePair(); │ │159 + var id: [evt.event_id_size]u8 = undefined; │ │160 + if ((try id_pair.key_cursor.readBytes(&id)).len != id.len) return error.InvalidPatch; │ │161 + const record = (try evt.Patch.readById(DB, repo_opts.hash, moment, &arena, &id)) orelse continue; │ │162 + if (conflicts) |map| if (try map.getCursor(&evt.orderKeyDesc(record.created_order, &id)) != null) continue; │ │163 + if (record.event.revision) |revision| if (std.mem.eql(u8, revision.source_oid, &source)) continue; │ │164 + try patches.append(allocator, .{ .id = id, .record = record }); │ │165 + } │ │166 + } │ │167 + } │ │168 + for (patches.items) |patch| { │ │169 + const author = evt.CommitAuthor{ .name = "haxy", .email = patch.record.author_email orelse "user@haxy" }; │ │170 + writeBranchPatch(host_kind, repo_kind, repo_opts, io, allocator, repo, std.fmt.bytesToHex(patch.id, .lower), patch.record.event, patch.record.event, author) catch |err| { │ │171 + std.log.warn("failed to refresh branch patch: {s}", .{@errorName(err)}); │ │172 + }; │ │173 + } │ │174 +} │ │175 │ │176 pub const PublishInput = struct { │ │177 id: [evt.event_id_size * 2]u8, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯