╭ fork.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const find = @import("find.zig"); 4 const xit = @import("xit"); 5 const rp = xit.repo; 6 const hash = xit.hash; 7 const bch = xit.branch; 8 const rf = xit.ref; 9 10 pub const ref = rf.Ref{ .kind = .head, .name = "patch" }; 11 12 pub const Route = struct { 13 identity: []const u8, 14 id: [evt.event_id_size * 2]u8, 15 }; 16 17 pub fn parseRoute(route_path: []const u8) ?Route { 18 const patch_segment = "/patch:"; 19 const patch_start = std.mem.indexOf(u8, route_path, patch_segment) orelse return null; 20 const identity = route_path[0..patch_start]; 21 const id_text = route_path[patch_start + patch_segment.len ..]; 22 if (identity.len == 0 or id_text.len != evt.event_id_size * 2) return null; 23 const id_bytes = evt.parseEventId(id_text) catch return null; 24 return .{ .identity = identity, .id = std.fmt.bytesToHex(id_bytes, .lower) }; 25 } 26 27 pub fn forkPath(allocator: std.mem.Allocator, repo_root_path: []const u8, id: []const u8) ![]u8 { 28 _ = try evt.parseEventId(id); 29 return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "forks", id }); 30 } 31 32 pub const CreateInput = struct { 33 id: [evt.event_id_size * 2]u8, 34 user_id: [evt.event_id_size]u8, 35 repo_id: [evt.event_id_size]u8, 36 title: []const u8, 37 description: []const u8, 38 tags: []const u8, 39 target_branch: []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 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; 53 if (!evt.Patch.branchValid(input.target_branch)) return error.InvalidTargetBranch; 54 55 // get the fork id and path 56 const fork_id = try evt.parseEventId(&input.id); 57 const fork_path = try forkPath(allocator, repo_root_path, &input.id); 58 errdefer allocator.free(fork_path); 59 60 // make sure the fork id doesn't already exist 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| 64 try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id) 65 else |err| switch (err) { 66 error.NotFound => null, 67 else => |other| return other, 68 }; 69 if (existing != null) return error.InvalidPatchDraft; 70 71 // get the target repo 72 const target_id = std.fmt.bytesToHex(input.repo_id, .lower); 73 const target_path = try std.fs.path.join(allocator, &.{ repo_root_path, &target_id }); 74 defer allocator.free(target_path); 75 var target_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path, .require_repo_root = true }); 76 defer target_repo.deinit(io, allocator); 77 if ((try target_repo.readRef(io, .{ .kind = .head, .name = input.target_branch })) == null) return error.TargetNotFound; 78 79 // create the fork repo dir 80 const forks_path = std.fs.path.dirname(fork_path) orelse return error.InvalidPatchDraft; 81 var forks_dir = try std.Io.Dir.cwd().createDirPathOpen(io, forks_path, .{}); 82 defer forks_dir.close(io); 83 const fork_name = std.fs.path.basename(fork_path); 84 forks_dir.createDir(io, fork_name, .default_dir) catch |err| switch (err) { 85 error.PathAlreadyExists => return error.InvalidPatchDraft, 86 else => |other| return other, 87 }; 88 errdefer forks_dir.deleteTree(io, fork_name) catch {}; 89 var fork_dir = try forks_dir.openDir(io, fork_name, .{}); 90 defer fork_dir.close(io); 91 var fork_repo_dir = try fork_dir.createDirPathOpen(io, ".xit", .{}); 92 defer fork_repo_dir.close(io); 93 94 // copy the target repo into the fork repo dir 95 { 96 try target_repo.core.db_file.lock(io, .shared); 97 defer target_repo.core.db_file.unlock(io); 98 99 // TODO: use reflink here when the filesystem supports it 100 const destination = try fork_repo_dir.createFile(io, "db", .{ .exclusive = true, .read = true }); 101 defer destination.close(io); 102 var read_buffer: [64 * 1024]u8 = undefined; 103 var write_buffer: [64 * 1024]u8 = undefined; 104 var reader = target_repo.core.db_file.reader(io, &read_buffer); 105 var writer = destination.writer(io, &write_buffer); 106 _ = try reader.interface.streamRemaining(&writer.interface); 107 try writer.interface.flush(); 108 try destination.sync(io); 109 } 110 111 var fork_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 112 defer fork_repo.deinit(io, allocator); 113 114 // clear the haxy state in the fork repo and create the patch branch 115 { 116 const DB = rp.Repo(.xit, repo_opts).DB; 117 const State = rp.Repo(.xit, repo_opts).State; 118 const Ctx = struct { 119 core: *rp.Repo(.xit, repo_opts).Core, 120 io: std.Io, 121 122 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 123 var moment = try DB.HashMap(.read_write).init(cursor.*); 124 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 125 const head_oid_maybe = try rf.readHeadRecurMaybe(.xit, repo_opts, state.readOnly(), ctx.io); 126 127 var path_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 128 const events_path = try evt.events_ref.toPath(&path_buffer); 129 rf.remove(.xit, repo_opts, state, ctx.io, events_path) catch |err| switch (err) { 130 error.RefNotFound => {}, 131 else => |other| return other, 132 }; 133 const patch_path = try ref.toPath(&path_buffer); 134 rf.remove(.xit, repo_opts, state, ctx.io, patch_path) catch |err| switch (err) { 135 error.RefNotFound => {}, 136 else => |other| return other, 137 }; 138 139 _ = try moment.remove(hash.hashInt(repo_opts.hash, evt.history_key)); 140 _ = try moment.remove(hash.hashInt(repo_opts.hash, evt.last_object_id_key)); 141 142 if (head_oid_maybe) |*head_oid| { 143 try rf.write(.xit, repo_opts, state, ctx.io, patch_path, .{ .oid = head_oid }); 144 } else { 145 try bch.add(.xit, repo_opts, state, ctx.io, .{ .name = ref.name, .target = .none }); 146 } 147 try rf.replaceHead(.xit, repo_opts, state, ctx.io, .{ .ref = ref }); 148 try xit.undo.writeMessage(repo_opts, state, .{ .custom = "create fork" }); 149 } 150 }; 151 152 try fork_repo.core.db_file.lock(io, .exclusive); 153 defer fork_repo.core.db_file.unlock(io); 154 155 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 156 try history.appendContext( 157 .{ .slot = try history.getSlot(-1) }, 158 Ctx{ .core = &fork_repo.core, .io = io }, 159 ); 160 } 161 162 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 163 try fork_repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); 164 165 // the copied db carries the target's file index, so the patch branch's 166 // entry starts from one of those rather than walking the tree 167 find.refresh(repo_opts, io, allocator, &fork_repo) catch |err| { 168 std.log.warn("failed to refresh file index: {s}", .{@errorName(err)}); 169 }; 170 171 // create the patch event 172 try evt.consume(.server, .fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 173 .id = input.id, 174 .timestamp = input.timestamp, 175 .author = input.author, 176 .event = .{ .patch = .{ 177 .title = input.title, 178 .description = input.description, 179 .tags = input.tags, 180 .target_branch = input.target_branch, 181 } }, 182 }}); 183 184 // create the fork event 185 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 186 .id = input.id, 187 .timestamp = input.timestamp, 188 .author = input.author, 189 .event = .{ .fork = .{ 190 .user_id = &input.user_id, 191 .repo_id = &input.repo_id, 192 } }, 193 }}); 194 195 return fork_path; 196 } 197 198 // tombstone first so a missing fork never remains visible 199 pub fn remove( 200 io: std.Io, 201 allocator: std.mem.Allocator, 202 repo_root_path: []const u8, 203 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 204 id: *const [evt.event_id_size * 2]u8, 205 expected_user_id: ?*const [evt.event_id_size]u8, 206 author: evt.CommitAuthor, 207 ) !void { 208 const fork_id = try evt.parseEventId(id); 209 var arena = std.heap.ArenaAllocator.init(allocator); 210 defer arena.deinit(); 211 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 212 const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft; 213 if (expected_user_id) |user_id| { 214 if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; 215 } 216 if (!record.removed) try evt.remove(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); 217 218 const path = try forkPath(allocator, repo_root_path, id); 219 defer allocator.free(path); 220 try std.Io.Dir.cwd().deleteTree(io, path); 221 } 222