╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────╮ │ 5 const hash = xit.hash; │ │ 6 const obj = xit.object; │ │ 7 const mrg = xit.merge; │ │ 8 +const rf = xit.ref; │ │ 9 │ │ 9 -pub const ref = xit.ref.Ref{ .kind = .head, .name = "patch" };│ │10 +pub const ref = rf.Ref{ .kind = .head, .name = "patch" }; │ │11 +const ref_path = "refs/heads/patch"; │ │12 │ │13 pub const Route = struct { │ │14 identity: []const u8, │ ╰──────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────╮ │38 id: [evt.event_id_size * 2]u8, │ │39 user_id: [evt.event_id_size]u8,│ │40 repo_id: [evt.event_id_size]u8,│ │39 - target: []const u8, │ │41 + title: []const u8, │ │42 + description: []const u8, │ │43 + tags: []const u8, │ │44 author: evt.CommitAuthor, │ │45 timestamp: u64, │ │46 }; │ ╰───────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │53 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │54 input: CreateInput, │ │55 ) ![]u8 { │ │56 + if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; │ │57 const fork_id = try evt.parseEventId(&input.id); │ │53 - if (!xit.ref.validateName(input.target)) return error.InvalidTarget; │ │54 - const draft_path = try forkPath(allocator, repo_root_path, &input.id); │ │55 - errdefer allocator.free(draft_path); │ │58 + const path = try forkPath(allocator, repo_root_path, &input.id); │ │59 + errdefer allocator.free(path); │ │60 │ │57 - var created_repo = false; │ │58 - errdefer if (created_repo) std.Io.Dir.cwd().deleteTree(io, draft_path) catch {}; │ │59 - │ │61 var arena = std.heap.ArenaAllocator.init(allocator); │ │62 defer arena.deinit(); │ │63 const existing = if (evt.currentMoment(evt.admin_repo_opts, admin_repo)) |moment|│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 else => |other| return other, │ │68 }; │ │69 if (existing) |record| { │ │69 - if (!std.mem.eql(u8, record.event.user_id, &input.user_id) or │ │70 + if (record.removed or │ │71 + !std.mem.eql(u8, record.event.user_id, &input.user_id) or │ │72 !std.mem.eql(u8, record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft;│ │73 } │ │74 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │73 - var source_oid: ?[]const u8 = null; │ │74 - const draft_exists = if (std.Io.Dir.accessAbsolute(io, draft_path, .{}))│ │75 + const path_exists = if (std.Io.Dir.accessAbsolute(io, path, .{})) │ │76 true │ │77 else |err| switch (err) { │ │78 error.FileNotFound => false, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 79 else => |other| return other, │ │ 80 }; │ │ 80 - if (draft_exists) { │ │ 81 - if (existing == null) return error.InvalidPatchDraft; │ │ 82 - if (existing) |record| { │ │ 83 - if (!record.removed and std.mem.eql(u8, record.event.target, input.target)) source_oid = record.event.source_oid; │ │ 84 - } │ │ 85 - } else { │ │ 86 - try std.Io.Dir.cwd().createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPatchDraft); │ │ 87 - created_repo = true; │ │ 88 - var repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .path = draft_path, .create_default_branch = ref.name }); │ │ 81 + if (existing != null and !path_exists) return error.PatchDataUnavailable; │ │ 82 + if (existing == null and path_exists) return error.InvalidPatchDraft; │ │ 83 + │ │ 84 + if (path_exists) { │ │ 85 + var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }); │ │ 86 defer repo.deinit(io, allocator); │ │ 90 - try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │ 87 + const moment = try evt.currentMoment(repo_opts, &repo); │ │ 88 + const patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft; │ │ 89 + if (patch.removed or │ │ 90 + !std.mem.eql(u8, patch.event.title, input.title) or │ │ 91 + !std.mem.eql(u8, patch.event.description, input.description) or │ │ 92 + !std.mem.eql(u8, patch.event.tags, input.tags)) return error.InvalidPatchDraft; │ │ 93 + return path; │ │ 94 } │ │ 95 │ │ 93 - const unchanged = if (existing) |record| │ │ 94 - !record.removed and std.mem.eql(u8, record.event.target, input.target) and │ │ 95 - evt.fieldEqual(?[]const u8, record.event.source_oid, source_oid) │ │ 96 - else │ │ 97 - false; │ │ 98 - if (!unchanged) { │ │ 99 - try evt.consume(.xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ │ │100 - .id = input.id, │ │101 - .timestamp = input.timestamp, │ │102 - .author = input.author, │ │103 - .event = .{ .fork = .{ │ │104 - .user_id = &input.user_id, │ │105 - .repo_id = &input.repo_id, │ │106 - .target = input.target, │ │107 - .source_oid = source_oid, │ │108 - } }, │ │109 - }}); │ │110 - } │ │111 - return draft_path; │ │ 96 + try std.Io.Dir.cwd().createDirPath(io, std.fs.path.dirname(path) orelse return error.InvalidPatchDraft); │ │ 97 + errdefer std.Io.Dir.cwd().deleteTree(io, path) catch {}; │ │ 98 + var repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .path = path, .create_default_branch = ref.name }); │ │ 99 + defer repo.deinit(io, allocator); │ │100 + try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │101 + try repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); │ │102 + │ │103 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{.{ │ │104 + .id = input.id, │ │105 + .timestamp = input.timestamp, │ │106 + .author = input.author, │ │107 + .event = .{ .patch = .{ │ │108 + .title = input.title, │ │109 + .description = input.description, │ │110 + .tags = input.tags, │ │111 + } }, │ │112 + }}); │ │113 + try evt.consume(.xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ │ │114 + .id = input.id, │ │115 + .timestamp = input.timestamp, │ │116 + .author = input.author, │ │117 + .event = .{ .fork = .{ │ │118 + .user_id = &input.user_id, │ │119 + .repo_id = &input.repo_id, │ │120 + } }, │ │121 + }}); │ │122 + return path; │ │123 } │ │124 │ │114 -pub fn recordPush( │ │125 +pub fn receivePack( │ │126 + comptime repo_opts: rp.RepoOpts(.xit), │ │127 io: std.Io, │ │128 allocator: std.mem.Allocator, │ │117 - admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │129 + fork_repo: *rp.Repo(.xit, repo_opts), │ │130 + target_repo: *rp.Repo(.xit, repo_opts), │ │131 id: *const [evt.event_id_size * 2]u8, │ │119 - target: []const u8, │ │120 - source_oid: []const u8, │ │132 + target_branch: []const u8, │ │133 author: evt.CommitAuthor, │ │134 timestamp: u64, │ │135 + reader: *std.Io.Reader, │ │136 + writer: *std.Io.Writer, │ │137 ) !void { │ │124 - const fork_id = try evt.parseEventId(id); │ │125 - if (!xit.ref.validateName(target)) return error.InvalidTarget; │ │138 + if (!rf.validateName(target_branch)) return error.InvalidTarget; │ │139 + const patch_id = try evt.parseEventId(id); │ │140 var arena = std.heap.ArenaAllocator.init(allocator); │ │141 defer arena.deinit(); │ │128 - const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │129 - const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft; │ │130 - if (record.removed) return error.InvalidPatchDraft; │ │131 - try evt.consume(.xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ │ │142 + │ │143 + const fork_moment = evt.currentMoment(repo_opts, fork_repo) catch return error.PatchDataUnavailable; │ │144 + const fork_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; │ │145 + │ │146 + const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │147 + error.NotFound => null, │ │148 + else => |other| return other, │ │149 + }; │ │150 + const target_patch = if (target_moment) |moment| │ │151 + try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) │ │152 + else │ │153 + null; │ │154 + if (fork_patch.removed and target_patch == null) return error.PatchDataUnavailable; │ │155 + if (target_patch) |patch| { │ │156 + if (!std.mem.eql(u8, patch.author_email orelse "", author.email)) return error.PatchDataUnavailable; │ │157 + } │ │158 + const published = target_patch != null; │ │159 + const title = if (target_patch) |patch| patch.event.title else fork_patch.event.title; │ │160 + │ │161 + const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_branch })) orelse return error.TargetNotFound; │ │162 + const newest = try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena); │ │163 + const target_ref = try std.fmt.allocPrint(arena.allocator(), "refs/heads/{s}", .{target_branch}); │ │164 + var revision_id_maybe: ?[evt.event_id_size]u8 = null; │ │165 + │ │166 + const DB = rp.Repo(.xit, repo_opts).DB; │ │167 + const State = rp.Repo(.xit, repo_opts).State; │ │168 + const Ctx = struct { │ │169 + core: *rp.Repo(.xit, repo_opts).Core, │ │170 + target_core: *rp.Repo(.xit, repo_opts).Core, │ │171 + io: std.Io, │ │172 + allocator: std.mem.Allocator, │ │173 + reader: *std.Io.Reader, │ │174 + writer: *std.Io.Writer, │ │175 + patch_id: [evt.event_id_size * 2]u8, │ │176 + patch: evt.Patch.Record, │ │177 + published: bool, │ │178 + target_oid: [hash.hexLen(repo_opts.hash)]u8, │ │179 + target_ref: []const u8, │ │180 + title: []const u8, │ │181 + author: evt.CommitAuthor, │ │182 + timestamp: u64, │ │183 + newest: ?evt.PatchRev.WithId, │ │184 + revision_id_maybe: *?[evt.event_id_size]u8, │ │185 + │ │186 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │187 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │188 + const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │189 + try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, .{ .allowed_ref = ref_path }); │ │190 + │ │191 + const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = ref })) orelse return error.CancelTransaction; │ │192 + var target_repo_moment = try ctx.target_core.latestMoment(); │ │193 + const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; │ │194 + var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); │ │195 + defer objects.deinit(); │ │196 + try objects.include(&ctx.target_oid); │ │197 + try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); │ │198 + │ │199 + const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); │ │200 + const existing_revision = if (ctx.newest) |latest| │ │201 + if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and │ │202 + std.mem.eql(u8, latest.record.event.source_oid, &source_oid) and │ │203 + std.mem.eql(u8, latest.record.event.target_ref, ctx.target_ref)) latest else null │ │204 + else │ │205 + null; │ │206 + │ │207 + var events: [2]evt.EventWithId = undefined; │ │208 + var event_count: usize = 0; │ │209 + var base_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │210 + var head_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │211 + var tree_entries: [2]evt.EventTreeEntry = undefined; │ │212 + if (existing_revision) |latest| { │ │213 + if (ctx.published) ctx.revision_id_maybe.* = latest.id; │ │214 + } else { │ │215 + var base_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &base_oid); │ │216 + defer base_object.deinit(); │ │217 + var source_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &source_oid); │ │218 + defer source_object.deinit(); │ │219 + base_tree_oid = base_object.content.commit.tree; │ │220 + head_tree_oid = source_object.content.commit.tree; │ │221 + │ │222 + var revision_id: [evt.event_id_size]u8 = undefined; │ │223 + ctx.io.random(&revision_id); │ │224 + const revision_hex = std.fmt.bytesToHex(revision_id, .lower); │ │225 + tree_entries = .{ │ │226 + .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, │ │227 + .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, │ │228 + }; │ │229 + events[event_count] = .{ │ │230 + .id = revision_hex, │ │231 + .timestamp = ctx.timestamp, │ │232 + .author = ctx.author, │ │233 + .tree_entries = &tree_entries, │ │234 + .event = .{ .patchrev = .{ │ │235 + .base_oid = &base_oid, │ │236 + .source_oid = &source_oid, │ │237 + .target_ref = ctx.target_ref, │ │238 + .message = ctx.title, │ │239 + } }, │ │240 + }; │ │241 + event_count += 1; │ │242 + ctx.revision_id_maybe.* = revision_id; │ │243 + │ │244 + if (!ctx.published) { │ │245 + events[event_count] = .{ │ │246 + .id = ctx.patch_id, │ │247 + .timestamp = ctx.timestamp, │ │248 + .author = ctx.author, │ │249 + .event = .{ .patch = .{ │ │250 + .title = ctx.patch.event.title, │ │251 + .description = ctx.patch.event.description, │ │252 + .tags = ctx.patch.event.tags, │ │253 + .target_patch_id = ctx.patch.event.target_patch_id, │ │254 + .patchrev_id = revision_hex, │ │255 + } }, │ │256 + }; │ │257 + event_count += 1; │ │258 + } │ │259 + } │ │260 + │ │261 + if (ctx.published and !ctx.patch.removed) { │ │262 + events[event_count] = .{ │ │263 + .id = ctx.patch_id, │ │264 + .timestamp = ctx.timestamp, │ │265 + .author = ctx.author, │ │266 + .event = .{ .patch = null }, │ │267 + }; │ │268 + event_count += 1; │ │269 + } │ │270 + if (event_count > 0) { │ │271 + try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); │ │272 + if (!try evt.consumeInTransaction(.xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction;│ │273 + } │ │274 + try xit.undo.writeMessage(repo_opts, state, .push); │ │275 + try ctx.core.chunk_store_file.sync(ctx.io); │ │276 + } │ │277 + }; │ │278 + │ │279 + try fork_repo.core.db_file.lock(io, .exclusive); │ │280 + defer fork_repo.core.db_file.unlock(io); │ │281 + const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); │ │282 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │283 + .core = &fork_repo.core, │ │284 + .target_core = &target_repo.core, │ │285 + .io = io, │ │286 + .allocator = allocator, │ │287 + .reader = reader, │ │288 + .writer = writer, │ │289 + .patch_id = id.*, │ │290 + .patch = fork_patch, │ │291 + .published = published, │ │292 + .target_oid = target_oid, │ │293 + .target_ref = target_ref, │ │294 + .title = title, │ │295 + .author = author, │ │296 + .timestamp = timestamp, │ │297 + .newest = newest, │ │298 + .revision_id_maybe = &revision_id_maybe, │ │299 + }) catch |err| switch (err) { │ │300 + error.CancelTransaction => {}, │ │301 + else => |other| return other, │ │302 + }; │ │303 + try writer.flush(); │ │304 + │ │305 + const revision_id = revision_id_maybe orelse return; │ │306 + if (!published) return; │ │307 + var update_arena = std.heap.ArenaAllocator.init(allocator); │ │308 + defer update_arena.deinit(); │ │309 + const update_moment = evt.currentMoment(repo_opts, target_repo) catch return; │ │310 + const published_patch = (evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, update_moment, &update_arena, &patch_id) catch return) orelse return; │ │311 + if (published_patch.removed) return; │ │312 + const revision_hex = std.fmt.bytesToHex(revision_id, .lower); │ │313 + if (published_patch.event.patchrev_id) |*published_revision| { │ │314 + if (std.mem.eql(u8, published_revision, &revision_hex)) return; │ │315 + } │ │316 + evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ │ │317 .id = id.*, │ │318 .timestamp = timestamp, │ │319 .author = author, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │135 - .event = .{ .fork = .{ │ │136 - .user_id = record.event.user_id, │ │137 - .repo_id = record.event.repo_id, │ │138 - .target = target, │ │139 - .source_oid = source_oid, │ │320 + .event = .{ .patch = .{ │ │321 + .title = published_patch.event.title, │ │322 + .description = published_patch.event.description, │ │323 + .tags = published_patch.event.tags, │ │324 + .target_patch_id = published_patch.event.target_patch_id,│ │325 + .patchrev_id = revision_hex, │ │326 } }, │ │141 - }}); │ │327 + }}) catch {}; │ │328 } │ │329 │ │330 pub const PublishInput = struct { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │331 id: [evt.event_id_size * 2]u8, │ │332 user_id: [evt.event_id_size]u8,│ │333 repo_id: [evt.event_id_size]u8,│ │148 - title: []const u8, │ │149 - tags: []const u8, │ │150 - description: []const u8, │ │334 author: evt.CommitAuthor, │ │335 timestamp: u64, │ │336 }; │ ╰────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │341 allocator: std.mem.Allocator, │ │342 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │343 target_repo: *rp.Repo(.xit, repo_opts), │ │161 - draft_path: []const u8, │ │344 + path: []const u8, │ │345 input: PublishInput, │ │346 ) !void { │ │164 - if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; │ │347 const patch_id = try evt.parseEventId(&input.id); │ │166 - │ │348 var arena = std.heap.ArenaAllocator.init(allocator); │ │349 defer arena.deinit(); │ │350 + │ │351 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │352 const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft;│ │353 if (fork_record.removed or │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │354 !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or │ │355 !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │356 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; │ │175 - if (user.removed or !std.mem.eql(u8, user.event.name, input.author.name) or !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft;│ │176 - const target_name = fork_record.event.target; │ │177 - const recorded_source_oid = fork_record.event.source_oid orelse return error.PatchNotPushed; │ │357 + if (user.removed or │ │358 + !std.mem.eql(u8, user.event.name, input.author.name) or │ │359 + !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; │ │360 │ │179 - const moment_maybe = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │361 + var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }) catch return error.PatchDataUnavailable; │ │362 + defer fork_repo.deinit(io, allocator); │ │363 + const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; │ │364 + const local_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable;│ │365 + │ │366 + const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │367 error.NotFound => null, │ │368 else => |other| return other, │ │369 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯