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