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