╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────╮ │ 8 │ │ 9 pub const User = @import("event/User.zig"); │ │10 pub const Repo = @import("event/Repo.zig"); │ │11 +pub const Fork = @import("event/Fork.zig"); │ │12 pub const Issue = @import("event/Issue.zig"); │ │13 pub const Discussion = @import("event/Discussion.zig");│ │14 pub const Comment = @import("event/Comment.zig"); │ ╰───────────────────────────────────────────────────────────╯ ╭────────────────────────────────╮ │51 pub const EventKind = enum {│ │52 user, │ │53 repo, │ │54 + fork, │ │55 issue, │ │56 discuss, │ │57 comment, │ ╰────────────────────────────────╯ ╭────────────────────────────────────────╮ │70 pub const Event = union(EventKind) {│ │71 user: ?User, │ │72 repo: ?Repo, │ │73 + fork: ?Fork, │ │74 issue: ?Issue, │ │75 discuss: ?Discussion, │ │76 comment: ?Comment, │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 .repo => .{ │ │152 .repo = if (json_event.data) |value| │ │153 try std.json.parseFromValueLeaky(Repo, arena.allocator(), value, .{ .ignore_unknown_fields = true })│ │154 + else │ │155 + null, │ │156 + }, │ │157 + .fork => .{ │ │158 + .fork = if (json_event.data) |value| │ │159 + try std.json.parseFromValueLeaky(Fork, arena.allocator(), value, .{ .ignore_unknown_fields = true })│ │160 else │ │161 null, │ │162 }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────╮ │237 const event: Event = switch (kind) { │ │238 .user => .{ .user = null }, │ │239 .repo => .{ .repo = null }, │ │240 + .fork => .{ .fork = null }, │ │241 .issue => .{ .issue = null }, │ │242 .discuss => .{ .discuss = null },│ │243 .comment => .{ .comment = null },│ ╰──────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │918 const record_maybe: ?Repo.Record = if (event_maybe) |event| .{ .event = event, .created_order = event_order } else null;│ │919 try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │920 }, │ │921 + .fork => |event_maybe| { │ │922 + const record_maybe: ?Fork.Record = if (event_maybe) |event| .{ .event = event, .created_order = event_order } else null;│ │923 + try Fork.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │924 + }, │ │925 .issue => |event_maybe| { │ │926 const record_maybe: ?Issue.Record = if (event_maybe) |event| .{ │ │927 .event = event, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ───────────╮ │src/event/Fork.zig│ ╰──────────────────╯ ╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │ 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 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯