╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │30 pub const admin_repo_opts: rp.RepoOpts(.xit) = .{}; │ │31 pub const AdminDB = rp.Repo(.xit, admin_repo_opts).DB; │ │32 │ │33 +// options + db type for a user repo, which holds its user's repo and fork events│ │34 +pub const user_repo_opts: rp.RepoOpts(.xit) = .{}; │ │35 +pub const UserDB = rp.Repo(.xit, user_repo_opts).DB; │ │36 + │ │37 // the admin repo's file the home page renders │ │38 pub const admin_about_path = "ABOUT.md"; │ │39 │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 51 try repo.add(io, allocator, &.{admin_about_path}); │ │ 52 _ = try repo.commit(io, allocator, .{ .author = "haxy <haxy@localhost>", .message = "init" }); │ │ 53 return repo; │ │ 54 +} │ │ 55 + │ │ 56 +// create a user's bare repo. its events branch starts with the first consume. │ │ 57 +pub fn initUserRepo(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !rp.Repo(.xit, user_repo_opts) { │ │ 58 + return try rp.Repo(.xit, user_repo_opts).init(io, allocator, .{ .bare = true, .path = path }); │ │ 59 +} │ │ 60 + │ │ 61 +// a user repo's path, a sibling of the repos dir │ │ 62 +pub fn userRepoPath(allocator: std.mem.Allocator, repo_root_path: []const u8, user_id: *const [event_id_size]u8) ![]u8 { │ │ 63 + return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "users", &std.fmt.bytesToHex(user_id.*, .lower) }); │ │ 64 +} │ │ 65 + │ │ 66 +// open a user's repo, or null when it's missing │ │ 67 +pub fn openUserRepo(io: std.Io, allocator: std.mem.Allocator, repo_root_path: []const u8, user_id: *const [event_id_size]u8) !?rp.Repo(.xit, user_repo_opts) {│ │ 68 + const path = try userRepoPath(allocator, repo_root_path, user_id); │ │ 69 + defer allocator.free(path); │ │ 70 + return rp.Repo(.xit, user_repo_opts).open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| switch (err) { │ │ 71 + error.RepoNotFound, error.FileNotFound => null, │ │ 72 + else => |e| e, │ │ 73 + }; │ │ 74 +} │ │ 75 + │ │ 76 +// a user repo's moment, or null before its first event │ │ 77 +pub fn userMoment(repo: *rp.Repo(.xit, user_repo_opts)) !?UserDB.HashMap(.read_only) { │ │ 78 + return currentMoment(user_repo_opts, repo) catch |err| switch (err) { │ │ 79 + error.NotFound => null, │ │ 80 + else => |e| e, │ │ 81 + }; │ │ 82 +} │ │ 83 + │ │ 84 +// read a repo by its owner's name and its name, from the owner's user repo │ │ 85 +pub fn readRepoByOwnerAndName( │ │ 86 + io: std.Io, │ │ 87 + allocator: std.mem.Allocator, │ │ 88 + arena: *std.heap.ArenaAllocator, │ │ 89 + admin_moment: AdminDB.HashMap(.read_only), │ │ 90 + repo_root_path: []const u8, │ │ 91 + owner_name: []const u8, │ │ 92 + repo_name: []const u8, │ │ 93 +) !?Repo.RepoWithId { │ │ 94 + const owner_id = (try User.readIdByName(AdminDB, admin_repo_opts.hash, admin_moment, owner_name)) orelse return null; │ │ 95 + var user_repo = (try openUserRepo(io, allocator, repo_root_path, &owner_id)) orelse return null; │ │ 96 + defer user_repo.deinit(io, allocator); │ │ 97 + const moment = (try userMoment(&user_repo)) orelse return null; │ │ 98 + return try Repo.readByName(UserDB, user_repo_opts.hash, moment, arena, repo_name); │ │ 99 +} │ │100 + │ │101 +// read a repo by its owner's id and its id, from the owner's user repo │ │102 +pub fn readRepoById( │ │103 + io: std.Io, │ │104 + allocator: std.mem.Allocator, │ │105 + arena: *std.heap.ArenaAllocator, │ │106 + repo_root_path: []const u8, │ │107 + owner_id: *const [event_id_size]u8, │ │108 + repo_id: []const u8, │ │109 +) !?Repo.Record { │ │110 + var user_repo = (try openUserRepo(io, allocator, repo_root_path, owner_id)) orelse return null; │ │111 + defer user_repo.deinit(io, allocator); │ │112 + const moment = (try userMoment(&user_repo)) orelse return null; │ │113 + return try Repo.readById(UserDB, user_repo_opts.hash, moment, arena, repo_id); │ │114 } │ │115 │ │116 +// read a fork by its forker's id and its id, from the forker's user repo │ │117 +pub fn readForkById( │ │118 + io: std.Io, │ │119 + allocator: std.mem.Allocator, │ │120 + arena: *std.heap.ArenaAllocator, │ │121 + repo_root_path: []const u8, │ │122 + forker_id: *const [event_id_size]u8, │ │123 + fork_id: *const [event_id_size]u8, │ │124 +) !?Fork.Record { │ │125 + var user_repo = (try openUserRepo(io, allocator, repo_root_path, forker_id)) orelse return null; │ │126 + defer user_repo.deinit(io, allocator); │ │127 + const moment = (try userMoment(&user_repo)) orelse return null; │ │128 + return try Fork.readById(UserDB, user_repo_opts.hash, moment, arena, fork_id); │ │129 +} │ │130 + │ │131 +// the user who forked a published patch, found through the patch's author │ │132 +pub const Forker = struct { │ │133 + id: [event_id_size]u8, │ │134 + name: []const u8, │ │135 +}; │ │136 + │ │137 +pub fn readForker( │ │138 + arena: *std.heap.ArenaAllocator, │ │139 + admin_moment: AdminDB.HashMap(.read_only), │ │140 + author_email: ?[]const u8, │ │141 +) !?Forker { │ │142 + const id = (try User.readIdByEmail(AdminDB, admin_repo_opts.hash, admin_moment, author_email orelse return null)) orelse return null; │ │143 + const user = (try User.readById(AdminDB, admin_repo_opts.hash, admin_moment, arena, &id)) orelse return null; │ │144 + return .{ .id = id, .name = user.event.name }; │ │145 +} │ │146 + │ │147 // the xitdb type events are stored in. it only depends on the hash kind, so │ │148 // this matches the db of any xit repo using that hash kind. │ │149 pub fn EventDB(comptime hash_kind: hash.HashKind) type { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────╮ │180 │ │181 pub const RepoRole = enum {│ │182 admin, │ │183 + user, │ │184 repo, │ │185 fork, │ │186 │ ╰────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │187 fn allows(self: RepoRole, kind: EventKind) bool {│ │188 return switch (self) { │ │ 93 - .admin => switch (kind) { │ │ 94 - .user, .repo, .fork => true, │ │189 + .admin => kind == .user, │ │190 + .user => switch (kind) { │ │191 + .repo, .fork => true, │ │192 else => false, │ │193 }, │ │194 .repo => switch (kind) { │ ╰──────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1422 // leaves this one out rather than stranding that one behind a name it │ │1423 // no longer owns │ │1424 if (!merged.removed and @hasDecl(T, "name_index_key")) { │ │1328 - const index_key = try merged.indexKey(arena.allocator()); │ │1425 const name_index_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.name_index_key));│ │1426 const name_index = try DB.HashMap(.read_write).init(name_index_cursor); │ │1331 - if (try name_index.getCursor(hash.hashInt(hash_kind, index_key))) |owner_cursor| { │ │1427 + if (try name_index.getCursor(hash.hashInt(hash_kind, merged.event.name))) |owner_cursor| { │ │1428 var owner_id: [event_id_size]u8 = undefined; │ │1429 _ = try owner_cursor.readBytes(&owner_id); │ │1430 if (!std.mem.eql(u8, &owner_id, &event_id)) continue; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1740 repo_name: []const u8, │ │1741 create_options_maybe: ?CreateRepoOptions, │ │1742 ) !?[event_id_size * 2]u8 { │ │1647 - var repo = rp.Repo(.xit, admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) { │ │1743 + var admin_repo = rp.Repo(.xit, admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) {│ │1744 error.RepoNotFound => return null, │ │1745 else => |e| return e, │ │1746 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │1651 - defer repo.deinit(io, allocator); │ │1747 + defer admin_repo.deinit(io, allocator); │ │1748 │ │1749 var arena = std.heap.ArenaAllocator.init(allocator);│ │1750 defer arena.deinit(); │ ╰──────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1751 + │ │1752 + const admin_moment = try currentMoment(admin_repo_opts, &admin_repo); │ │1753 │ │1656 - const moment = try currentMoment(admin_repo_opts, &repo); │ │1754 + // an unknown owner can't own a repo, so the push is rejected │ │1755 + const owner_user_id = (try User.readIdByName(AdminDB, admin_repo_opts.hash, admin_moment, owner_name)) orelse return null;│ │1756 + const repo_root_path = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(admin_repo_path) orelse ".", "repos" }); │ │1757 + defer allocator.free(repo_root_path); │ │1758 + var user_repo = (try openUserRepo(io, allocator, repo_root_path, &owner_user_id)) orelse return null; │ │1759 + defer user_repo.deinit(io, allocator); │ │1760 │ │1761 // an already-registered repo reuses its event id, so a re-push (or a clone) │ │1762 // lands in the same repo │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1660 - if (try Repo.readByOwnerAndName(AdminDB, admin_repo_opts.hash, moment, &arena, owner_name, repo_name)) |found| {│ │1661 - return std.fmt.bytesToHex(found.event_id, .lower); │ │1763 + if (try userMoment(&user_repo)) |moment| { │ │1764 + if (try Repo.readByName(UserDB, user_repo_opts.hash, moment, &arena, repo_name)) |found| { │ │1765 + return std.fmt.bytesToHex(found.event_id, .lower); │ │1766 + } │ │1767 } │ │1768 │ │1769 const create_options = create_options_maybe orelse return null; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1665 - │ │1666 - // the new repo is owned by the named user, read from the name index; an │ │1667 - // unknown owner can't own a repo, so the push is rejected │ │1668 - const name_to_user_id_cursor = try moment.getCursor(hash.hashInt(admin_repo_opts.hash, "name->user-id")) orelse return null;│ │1669 - const name_to_user_id = try AdminDB.HashMap(.read_only).init(name_to_user_id_cursor); │ │1670 - const owner_id_cursor = try name_to_user_id.getCursor(hash.hashInt(admin_repo_opts.hash, owner_name)) orelse return null; │ │1671 - var owner_user_id: [event_id_size]u8 = undefined; │ │1672 - _ = try owner_id_cursor.readBytes(&owner_user_id); │ │1770 │ │1674 - const owner = (try User.readById(AdminDB, admin_repo_opts.hash, moment, &arena, &owner_user_id)) orelse unreachable; │ │1771 + const owner = (try User.readById(AdminDB, admin_repo_opts.hash, admin_moment, &arena, &owner_user_id)) orelse unreachable; │ │1772 │ │1773 var id_bytes: [event_id_size]u8 = undefined; │ │1774 io.random(&id_bytes); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯