╭ fork.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const hash = xit.hash; 6 const obj = xit.object; 7 const mrg = xit.merge; 8 9 pub const ref = xit.ref.Ref{ .kind = .head, .name = "patch" }; 10 11 pub const Route = struct { 12 identity: []const u8, 13 id: [evt.event_id_size * 2]u8, 14 target: []const u8, 15 }; 16 17 pub fn parseRoute(route_path: []const u8) ?Route { 18 const patch_segment = "/patch:"; 19 const branch_segment = "/branch:"; 20 const patch_start = std.mem.indexOf(u8, route_path, patch_segment) orelse return null; 21 const branch_start = std.mem.indexOfPos(u8, route_path, patch_start + patch_segment.len, branch_segment) orelse return null; 22 const identity = route_path[0..patch_start]; 23 const id_text = route_path[patch_start + patch_segment.len .. branch_start]; 24 const target = route_path[branch_start + branch_segment.len ..]; 25 if (identity.len == 0 or target.len == 0 or id_text.len != evt.event_id_size * 2) return null; 26 const id_bytes = evt.parseEventId(id_text) catch return null; 27 return .{ .identity = identity, .id = std.fmt.bytesToHex(id_bytes, .lower), .target = target }; 28 } 29 30 pub fn forkPath(allocator: std.mem.Allocator, repo_root_path: []const u8, id: []const u8) ![]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, 49 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 50 input: CreateInput, 51 ) ![]u8 { 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 57 var created_repo = false; 58 errdefer if (created_repo) std.Io.Dir.cwd().deleteTree(io, draft_path) catch {}; 59 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 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 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 114 pub fn recordPush( 115 io: std.Io, 116 allocator: std.mem.Allocator, 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 { 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, 151 author: evt.CommitAuthor, 152 timestamp: u64, 153 }; 154 155 pub fn publish( 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, 163 ) !void { 164 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; 165 const patch_id = try evt.parseEventId(&input.id); 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, 182 }; 183 const existing = if (moment_maybe) |moment| try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) else null; 184 if (existing) |record| { 185 if (record.removed or !std.mem.eql(u8, record.author_email orelse "", input.author.email)) return error.InvalidPatch; 186 } 187 188 var stage = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 189 defer stage.deinit(io, allocator); 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(); 195 const stage_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &stage.core, .extra = .{ .moment = &stage_moment } }; 196 var objects = try obj.ObjectIterator(.xit, repo_opts).init(stage_state, io, allocator, .{ .kind = .all }); 197 defer objects.deinit(); 198 try objects.include(&source_oid); 199 try target_repo.copyObjects(.xit, repo_opts, &objects, io, null); 200 201 var target_moment = try target_repo.core.latestMoment(); 202 const target_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &target_moment } }; 203 const base_oid = try mrg.commonAncestor(.xit, repo_opts, target_state, io, allocator, &target_oid, &source_oid); 204 var base_object = try obj.Object(.xit, repo_opts).initCommit(target_state, io, allocator, &base_oid); 205 defer base_object.deinit(); 206 var source_object = try obj.Object(.xit, repo_opts).initCommit(target_state, io, allocator, &source_oid); 207 defer source_object.deinit(); 208 const base_tree_oid = base_object.content.commit.tree; 209 const head_tree_oid = source_object.content.commit.tree; 210 211 var message: std.ArrayList(u8) = .empty; 212 defer message.deinit(allocator); 213 try source_object.readMessage(allocator, &message, .limited(10_000)); 214 215 const target_ref = try std.fmt.allocPrint(arena.allocator(), "refs/heads/{s}", .{target_name}); 216 var revision_changed = true; 217 var patchrev_id: [evt.event_id_size]u8 = undefined; 218 if (existing) |record| { 219 patchrev_id = try evt.parseEventId(&record.event.patchrev_id); 220 if (std.mem.eql(u8, record.event.target_ref, target_ref)) { 221 const moment = moment_maybe orelse return error.PatchNotFound; 222 if (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patchrev_id)) |revision| { 223 revision_changed = !std.mem.eql(u8, revision.event.base_oid, &base_oid) or !std.mem.eql(u8, revision.event.source_oid, &source_oid); 224 } 225 } 226 } 227 if (revision_changed) io.random(&patchrev_id); 228 const patchrev_hex = std.fmt.bytesToHex(patchrev_id, .lower); 229 230 const tree_entries = [_]evt.EventTreeEntry{ 231 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 232 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 233 }; 234 const patch_event = evt.EventWithId{ 235 .id = input.id, 236 .timestamp = input.timestamp, 237 .author = input.author, 238 .event = .{ .patch = .{ 239 .title = input.title, 240 .description = input.description, 241 .tags = input.tags, 242 .target_ref = target_ref, 243 .target_patch_id = if (existing) |record| record.event.target_patch_id else null, 244 .patchrev_id = patchrev_hex, 245 } }, 246 }; 247 if (revision_changed) { 248 try evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{ 249 .{ 250 .id = patchrev_hex, 251 .timestamp = input.timestamp, 252 .author = input.author, 253 .tree_entries = &tree_entries, 254 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_oid, .message = message.items } }, 255 }, 256 patch_event, 257 }); 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