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