╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭─────────────────────────────────────────────────────────────╮ │ 5 const hash = xit.hash; │ │ 6 const obj = xit.object; │ │ 7 const mrg = xit.merge; │ │ 8 +const bch = xit.branch; │ │ 9 const rf = xit.ref; │ │10 │ │11 pub const ref = rf.Ref{ .kind = .head, .name = "patch" };│ ╰─────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │55 input: CreateInput, │ │56 ) ![]u8 { │ │57 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; │ │58 + │ │59 + // get the fork id and path │ │60 const fork_id = try evt.parseEventId(&input.id); │ │58 - const path = try forkPath(allocator, repo_root_path, &input.id); │ │59 - errdefer allocator.free(path); │ │61 + const fork_path = try forkPath(allocator, repo_root_path, &input.id); │ │62 + errdefer allocator.free(fork_path); │ │63 │ │64 + // make sure the fork id doesn't already exist │ │65 var arena = std.heap.ArenaAllocator.init(allocator); │ │66 defer arena.deinit(); │ │67 const existing = if (evt.currentMoment(evt.admin_repo_opts, admin_repo)) |moment|│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 70 error.NotFound => null, │ │ 71 else => |other| return other, │ │ 72 }; │ │ 69 - if (existing) |record| { │ │ 70 - if (record.removed or │ │ 71 - !std.mem.eql(u8, record.event.user_id, &input.user_id) or │ │ 72 - !std.mem.eql(u8, record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │ 73 - } │ │ 73 + if (existing != null) return error.InvalidPatchDraft; │ │ 74 + │ │ 75 + // get the target repo │ │ 76 + const target_id = std.fmt.bytesToHex(input.repo_id, .lower); │ │ 77 + const target_path = try std.fs.path.join(allocator, &.{ repo_root_path, &target_id }); │ │ 78 + defer allocator.free(target_path); │ │ 79 + var target_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path, .require_repo_root = true }); │ │ 80 + defer target_repo.deinit(io, allocator); │ │ 81 │ │ 75 - const path_exists = if (std.Io.Dir.accessAbsolute(io, path, .{})) │ │ 76 - true │ │ 77 - else |err| switch (err) { │ │ 78 - error.FileNotFound => false, │ │ 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 }; │ │ 81 - if (existing != null and !path_exists) return error.PatchDataUnavailable; │ │ 82 - if (existing == null and path_exists) return error.InvalidPatchDraft; │ │ 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 │ │ 84 - if (path_exists) { │ │ 85 - var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }); │ │ 86 - defer repo.deinit(io, allocator); │ │ 87 - const moment = try evt.currentMoment(repo_opts, &repo); │ │ 88 - const patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft;│ │ 89 - if (patch.removed or │ │ 90 - !std.mem.eql(u8, patch.event.title, input.title) or │ │ 91 - !std.mem.eql(u8, patch.event.description, input.description) or │ │ 92 - !std.mem.eql(u8, patch.event.tags, input.tags)) return error.InvalidPatchDraft; │ │ 93 - return path; │ │ 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 + try target_repo.core.chunk_store_file.lock(io, .shared); │ │102 + defer target_repo.core.chunk_store_file.unlock(io); │ │103 + │ │104 + // TODO: use reflinks here when the filesystem supports them │ │105 + for ([_]struct { file: std.Io.File, name: []const u8 }{ │ │106 + .{ .file = target_repo.core.db_file, .name = "db" }, │ │107 + .{ .file = target_repo.core.chunk_store_file, .name = "chunks" }, │ │108 + }) |source| { │ │109 + const destination = try fork_repo_dir.createFile(io, source.name, .{ .exclusive = true, .read = true }); │ │110 + defer destination.close(io); │ │111 + var read_buffer: [64 * 1024]u8 = undefined; │ │112 + var write_buffer: [64 * 1024]u8 = undefined; │ │113 + var reader = source.file.reader(io, &read_buffer); │ │114 + var writer = destination.writer(io, &write_buffer); │ │115 + _ = try reader.interface.streamRemaining(&writer.interface); │ │116 + try writer.interface.flush(); │ │117 + try destination.sync(io); │ │118 + } │ │119 } │ │120 + │ │121 + var fork_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); │ │122 + defer fork_repo.deinit(io, allocator); │ │123 + │ │124 + // clear the haxy state in the fork repo and create the patch branch │ │125 + { │ │126 + const DB = rp.Repo(.xit, repo_opts).DB; │ │127 + const State = rp.Repo(.xit, repo_opts).State; │ │128 + const Ctx = struct { │ │129 + core: *rp.Repo(.xit, repo_opts).Core, │ │130 + io: std.Io, │ │131 │ │ 96 - try std.Io.Dir.cwd().createDirPath(io, std.fs.path.dirname(path) orelse return error.InvalidPatchDraft); │ │ 97 - errdefer std.Io.Dir.cwd().deleteTree(io, path) catch {}; │ │ 98 - var repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .path = path, .create_default_branch = ref.name }); │ │ 99 - defer repo.deinit(io, allocator); │ │100 - try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │101 - try repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); │ │132 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │133 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │134 + const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │135 + const head_oid_maybe = try rf.readHeadRecurMaybe(.xit, repo_opts, state.readOnly(), ctx.io); │ │136 + │ │137 + var path_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │138 + const events_path = try evt.events_ref.toPath(&path_buffer); │ │139 + rf.remove(.xit, repo_opts, state, ctx.io, events_path) catch |err| switch (err) { │ │140 + error.RefNotFound => {}, │ │141 + else => |other| return other, │ │142 + }; │ │143 + const patch_path = try ref.toPath(&path_buffer); │ │144 + rf.remove(.xit, repo_opts, state, ctx.io, patch_path) catch |err| switch (err) { │ │145 + error.RefNotFound => {}, │ │146 + else => |other| return other, │ │147 + }; │ │148 + │ │149 + _ = try moment.remove(hash.hashInt(repo_opts.hash, "haxy")); │ │150 + _ = try moment.remove(hash.hashInt(repo_opts.hash, "haxy-last-object-id")); │ │151 + │ │152 + if (head_oid_maybe) |*head_oid| { │ │153 + try rf.write(.xit, repo_opts, state, ctx.io, patch_path, .{ .oid = head_oid }); │ │154 + } else { │ │155 + try bch.add(.xit, repo_opts, state, ctx.io, .{ .name = ref.name, .target = .none }); │ │156 + } │ │157 + try rf.replaceHead(.xit, repo_opts, state, ctx.io, .{ .ref = ref }); │ │158 + try xit.undo.writeMessage(repo_opts, state, .{ .custom = "create fork" }); │ │159 + } │ │160 + }; │ │161 + │ │162 + try fork_repo.core.db_file.lock(io, .exclusive); │ │163 + defer fork_repo.core.db_file.unlock(io); │ │164 │ │103 - try evt.consume(.fork, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{.{ │ │165 + const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); │ │166 + try history.appendContext( │ │167 + .{ .slot = try history.getSlot(-1) }, │ │168 + Ctx{ .core = &fork_repo.core, .io = io }, │ │169 + ); │ │170 + } │ │171 + │ │172 + try fork_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │173 + try fork_repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); │ │174 + │ │175 + // create the patch event │ │176 + try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ │ │177 .id = input.id, │ │178 .timestamp = input.timestamp, │ │179 .author = input.author, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │183 .tags = input.tags, │ │184 } }, │ │185 }}); │ │186 + │ │187 + // create the fork event │ │188 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{│ │189 .id = input.id, │ │190 .timestamp = input.timestamp, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │194 .repo_id = &input.repo_id,│ │195 } }, │ │196 }}); │ │122 - return path; │ │197 + │ │198 + return fork_path; │ │199 } │ │200 │ │201 pub fn receivePack( │ ╰───────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/admin.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │363 var prng = std.Random.DefaultPrng.init(std.testing.random_seed);│ │364 const user_id = evt.EventWithId.randomId(prng.random()); │ │365 const repo_id = evt.EventWithId.randomId(prng.random()); │ │366 + const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); │ │367 const fork_id = evt.EventWithId.randomId(prng.random()); │ │368 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); │ │369 │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │391 } }, │ │392 }, │ │393 }); │ │394 + │ │395 + const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); │ │396 + defer allocator.free(target_path); │ │397 + var target = try rp.Repo(.xit, fork_repo_opts).init(io, allocator, .{ .path = target_path });│ │398 + defer target.deinit(io, allocator); │ │399 │ │400 const draft_path = try fork.create(fork_repo_opts, io, allocator, repos_dir, &admin, .{ │ │401 .id = fork_id_hex, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │425 try std.testing.expectEqualStrings("add a feature", draft.event.title); │ │426 try std.testing.expectEqual(null, draft.event.patchrev_id); │ │427 │ │422 - const same_path = try fork.create(fork_repo_opts, io, allocator, repos_dir, &admin, .{ │ │423 - .id = fork_id_hex, │ │424 - .user_id = user_id, │ │425 - .repo_id = repo_id, │ │426 - .title = "add a feature", │ │427 - .description = "a draft patch", │ │428 - .tags = "enhancement", │ │429 - .author = author, │ │430 - .timestamp = 4, │ │431 - }); │ │432 - defer allocator.free(same_path); │ │433 - try std.testing.expectEqualStrings(draft_path, same_path); │ │434 - │ │428 var wrong_user_id = user_id; │ │429 wrong_user_id[0] ^= 1; │ │430 try std.testing.expectError(error.InvalidPatchDraft, fork.remove(io, allocator, repos_dir, &admin, &fork_id_hex, &wrong_user_id, author));│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯