╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────╮ │ 7 const mrg = xit.merge; │ │ 8 │ │ 9 pub const ref = xit.ref.Ref{ .kind = .head, .name = "patch" };│ │10 - │ │11 -const author_config = "haxy.author"; │ │12 -const target_config = "haxy.target"; │ │10 │ │11 pub const Route = struct { │ │12 identity: []const u8, │ ╰──────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │31 _ = try evt.parseEventId(id); │ │32 return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "forks", id });│ │33 } │ │34 + │ │35 +pub const CreateInput = struct { │ │36 + id: [evt.event_id_size * 2]u8, │ │37 + user_id: [evt.event_id_size]u8, │ │38 + repo_id: [evt.event_id_size]u8, │ │39 + target: []const u8, │ │40 + author: evt.CommitAuthor, │ │41 + timestamp: u64, │ │42 +}; │ │43 │ │44 pub fn create( │ │45 comptime repo_opts: rp.RepoOpts(.xit), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 46 io: std.Io, │ │ 47 allocator: std.mem.Allocator, │ │ 48 repo_root_path: []const u8, │ │ 43 - id: []const u8, │ │ 44 - author_name: []const u8, │ │ 49 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │ 50 + input: CreateInput, │ │ 51 ) ![]u8 { │ │ 46 - const draft_path = try forkPath(allocator, repo_root_path, id); │ │ 52 + 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); │ │ 56 │ │ 49 - if (std.Io.Dir.accessAbsolute(io, draft_path, .{})) { │ │ 50 - var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); │ │ 51 - defer repo.deinit(io, allocator); │ │ 52 - const stored = try draftAuthor(repo_opts, &repo, io, allocator); │ │ 53 - defer if (stored) |value| allocator.free(value); │ │ 54 - const stored_author = stored orelse return error.InvalidPatchDraft; │ │ 55 - if (!std.mem.eql(u8, stored_author, author_name)) return error.InvalidPatchDraft; │ │ 56 - return draft_path; │ │ 57 - } else |_| {} │ │ 57 + var created_repo = false; │ │ 58 + errdefer if (created_repo) std.Io.Dir.cwd().deleteTree(io, draft_path) catch {}; │ │ 59 │ │ 59 - try std.Io.Dir.cwd().createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPatchDraft); │ │ 60 - var repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .path = draft_path, .create_default_branch = ref.name }); │ │ 61 - defer repo.deinit(io, allocator); │ │ 62 - try repo.addConfig(io, allocator, .{ .name = author_config, .value = author_name }); │ │ 63 - try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │ 64 - return draft_path; │ │ 65 -} │ │ 60 + var arena = std.heap.ArenaAllocator.init(allocator); │ │ 61 + defer arena.deinit(); │ │ 62 + const existing = if (evt.currentMoment(evt.admin_repo_opts, admin_repo)) |moment| │ │ 63 + try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id) │ │ 64 + else |err| switch (err) { │ │ 65 + error.NotFound => null, │ │ 66 + else => |other| return other, │ │ 67 + }; │ │ 68 + if (existing) |record| { │ │ 69 + if (!std.mem.eql(u8, record.event.user_id, &input.user_id) or │ │ 70 + !std.mem.eql(u8, record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │ 71 + } │ │ 72 │ │ 67 -pub fn draftAuthor( │ │ 68 - comptime repo_opts: rp.RepoOpts(.xit), │ │ 69 - repo: *rp.Repo(.xit, repo_opts), │ │ 70 - io: std.Io, │ │ 71 - allocator: std.mem.Allocator, │ │ 72 -) !?[]u8 { │ │ 73 - return configValue(repo_opts, repo, io, allocator, author_config); │ │ 74 -} │ │ 73 + var source_oid: ?[]const u8 = null; │ │ 74 + const draft_exists = if (std.Io.Dir.accessAbsolute(io, draft_path, .{})) │ │ 75 + true │ │ 76 + else |err| switch (err) { │ │ 77 + error.FileNotFound => false, │ │ 78 + else => |other| return other, │ │ 79 + }; │ │ 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 }); │ │ 89 + defer repo.deinit(io, allocator); │ │ 90 + try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │ 91 + } │ │ 92 │ │ 76 -pub fn draftTarget( │ │ 77 - comptime repo_opts: rp.RepoOpts(.xit), │ │ 78 - repo: *rp.Repo(.xit, repo_opts), │ │ 79 - io: std.Io, │ │ 80 - allocator: std.mem.Allocator, │ │ 81 -) !?[]u8 { │ │ 82 - return configValue(repo_opts, repo, io, allocator, target_config); │ │ 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; │ │112 } │ │113 │ │ 85 -pub fn setTarget( │ │ 86 - comptime repo_opts: rp.RepoOpts(.xit), │ │ 87 - repo: *rp.Repo(.xit, repo_opts), │ │114 +pub fn recordPush( │ │115 io: std.Io, │ │116 allocator: std.mem.Allocator, │ │ 90 - value: []const u8, │ │117 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │118 + id: *const [evt.event_id_size * 2]u8, │ │119 + target: []const u8, │ │120 + source_oid: []const u8, │ │121 + author: evt.CommitAuthor, │ │122 + timestamp: u64, │ │123 ) !void { │ │ 92 - try repo.addConfig(io, allocator, .{ .name = target_config, .value = value }); │ │ 93 -} │ │ 94 - │ │ 95 -fn configValue( │ │ 96 - comptime repo_opts: rp.RepoOpts(.xit), │ │ 97 - repo: *rp.Repo(.xit, repo_opts), │ │ 98 - io: std.Io, │ │ 99 - allocator: std.mem.Allocator, │ │100 - name: []const u8, │ │101 -) !?[]u8 { │ │102 - const dot = std.mem.indexOfScalar(u8, name, '.') orelse return error.InvalidConfigName; │ │103 - var config = try repo.listConfig(io, allocator); │ │104 - defer config.deinit(); │ │105 - const variables = config.sections.get(name[0..dot]) orelse return null; │ │106 - const value = variables.get(name[dot + 1 ..]) orelse return null; │ │107 - return try allocator.dupe(u8, value); │ │124 + const fork_id = try evt.parseEventId(id); │ │125 + if (!xit.ref.validateName(target)) return error.InvalidTarget; │ │126 + var arena = std.heap.ArenaAllocator.init(allocator); │ │127 + 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, &.{.{ │ │132 + .id = id.*, │ │133 + .timestamp = timestamp, │ │134 + .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, │ │140 + } }, │ │141 + }}); │ │142 } │ │143 │ │144 pub const PublishInput = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │145 id: [evt.event_id_size * 2]u8, │ │146 + user_id: [evt.event_id_size]u8,│ │147 + repo_id: [evt.event_id_size]u8,│ │148 title: []const u8, │ │149 tags: []const u8, │ │150 description: []const u8, │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │156 comptime repo_opts: rp.RepoOpts(.xit), │ │157 io: std.Io, │ │158 allocator: std.mem.Allocator, │ │159 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts),│ │160 target_repo: *rp.Repo(.xit, repo_opts), │ │161 draft_path: []const u8, │ │162 input: PublishInput, │ ╰─────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │166 │ │167 var arena = std.heap.ArenaAllocator.init(allocator); │ │168 defer arena.deinit(); │ │169 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │170 + const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; │ │171 + if (fork_record.removed or │ │172 + !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or │ │173 + !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │174 + 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; │ │178 + │ │179 const moment_maybe = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │180 error.NotFound => null, │ │181 else => |other| return other, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 │ │188 var stage = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); │ │189 defer stage.deinit(io, allocator); │ │143 - const stored_author_maybe = try draftAuthor(repo_opts, &stage, io, allocator); │ │144 - defer if (stored_author_maybe) |value| allocator.free(value); │ │145 - const stored_author = stored_author_maybe orelse return error.InvalidPatchDraft; │ │146 - if (!std.mem.eql(u8, stored_author, input.author.name)) return error.InvalidPatchDraft; │ │147 - const target_name = (try draftTarget(repo_opts, &stage, io, allocator)) orelse return error.PatchNotPushed; │ │148 - defer allocator.free(target_name); │ │149 - if (!xit.ref.validateName(target_name)) return error.InvalidPatchDraft; │ │190 const source_oid = (try stage.readRef(io, ref)) orelse return error.PatchNotPushed; │ │191 + if (!std.mem.eql(u8, recorded_source_oid, &source_oid)) return error.InvalidPatchDraft; │ │192 │ │193 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_name })) orelse return error.TargetNotFound;│ │194 var stage_moment = try stage.core.latestMoment(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │258 } else { │ │259 try evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{patch_event}); │ │260 } │ │261 +} │ │262 + │ │263 +// delete first so a failed tombstone can be retried safely │ │264 +pub fn remove( │ │265 + io: std.Io, │ │266 + allocator: std.mem.Allocator, │ │267 + repo_root_path: []const u8, │ │268 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │269 + id: *const [evt.event_id_size * 2]u8, │ │270 + user_id: *const [evt.event_id_size]u8, │ │271 + author: evt.CommitAuthor, │ │272 +) !void { │ │273 + const fork_id = try evt.parseEventId(id); │ │274 + var arena = std.heap.ArenaAllocator.init(allocator); │ │275 + defer arena.deinit(); │ │276 + const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │277 + const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft;│ │278 + if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; │ │279 + │ │280 + const draft_path = try forkPath(allocator, repo_root_path, id); │ │281 + defer allocator.free(draft_path); │ │282 + try std.Io.Dir.cwd().deleteTree(io, draft_path); │ │283 + if (!record.removed) try evt.remove(.xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); │ │284 } │ │285 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯