╭ Repo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const hash = xit.hash; 5 6 user_id: []const u8, 7 name: []const u8, 8 description: []const u8, 9 read_access: Access = .private, 10 write_access: Access = .private, 11 12 // what the db stores: the event's data plus the commit-derived fields 13 pub const Record = struct { 14 event: Self, 15 removed: bool = false, 16 created_order: u64 = 0, 17 updated_order: u64 = 0, 18 }; 19 20 const Self = @This(); 21 22 pub const Access = enum { 23 private, 24 public, 25 }; 26 27 // what a user may do in a repo, ordered so a higher role includes the lower 28 // ones. holding no role is a null, not a value. 29 pub const Role = enum { 30 read, 31 write, 32 owner, 33 34 pub fn atLeast(self: Role, min: Role) bool { 35 return @backingInt(self) >= @backingInt(min); 36 } 37 38 // whether a user may change an event: anyone's with the write role, 39 // otherwise only one they authored 40 pub fn canModify(self: Role, user_email: []const u8, author_email_maybe: ?[]const u8) bool { 41 if (self.atLeast(.write)) return true; 42 const author_email = author_email_maybe orelse return false; 43 return std.mem.eql(u8, user_email, author_email); 44 } 45 }; 46 47 pub const name_max_len = 32; 48 49 // the role `user_id_maybe` holds in the repo, or null when they hold none: its 50 // creator owns it, and anyone else gets the higher of the base role and their 51 // grant. `user_moment` is the owner's, which holds the grants. a null user is 52 // logged out. 53 pub fn roleOf( 54 comptime DB: type, 55 comptime hash_kind: hash.HashKind, 56 user_moment: DB.HashMap(.read_only), 57 arena: *std.heap.ArenaAllocator, 58 record: Record, 59 repo_id: *const [evt.event_id_size]u8, 60 user_id_maybe: ?[evt.event_id_size]u8, 61 ) !?Role { 62 // what the access fields give everyone, logged in or not 63 const base_maybe: ?Role = if (record.event.write_access == .public) .write else if (record.event.read_access == .public) .read else null; 64 const user_id = user_id_maybe orelse return base_maybe; 65 if (std.mem.eql(u8, record.event.user_id, &user_id)) return .owner; 66 67 const granted = (try evt.Grant.readRole(DB, hash_kind, user_moment, arena, repo_id, &user_id)) orelse return base_maybe; 68 const base = base_maybe orelse return granted; 69 return if (granted.atLeast(base)) granted else base; 70 } 71 72 // the moment keys `evt.merge` reads and writes for this kind 73 pub const merge_policy: evt.MergePolicy = .target_wins; 74 pub const record_map_key = "event-id->repo"; 75 pub const all_id_set_key = "repo-id-set"; 76 pub const name_index_key = "name->repo-id"; 77 78 pub fn validateName(name: []const u8) !void { 79 if (name.len == 0) return error.NameEmpty; 80 if (name.len > name_max_len) return error.NameTooLong; 81 if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.InvalidName; 82 83 for (name) |c| { 84 if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_' and c != '.') 85 return error.InvalidName; 86 } 87 } 88 89 pub fn consume( 90 comptime DB: type, 91 comptime hash_kind: hash.HashKind, 92 haxy_moment: DB.HashMap(.read_write), 93 event_id: *const [evt.event_id_size]u8, 94 record_maybe: ?Record, 95 arena: *std.heap.ArenaAllocator, 96 _: ?[]const u8, 97 ) !void { 98 const repo_key = hash.hashInt(hash_kind, event_id); 99 100 const event_id_to_repo_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->repo")); 101 const event_id_to_repo = try DB.HashMap(.read_write).init(event_id_to_repo_cursor); 102 103 const name_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, name_index_key)); 104 const name_to_repo_id = try DB.SortedMap(.read_write).init(name_to_repo_id_cursor); 105 106 var existing_record_maybe: ?Record = null; 107 const existing_cursor_maybe = try event_id_to_repo.getCursor(repo_key); 108 if (existing_cursor_maybe) |existing_cursor| { 109 const existing_repo = try DB.HashMap(.read_only).init(existing_cursor); 110 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_repo); 111 } 112 113 var record_to_write = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_record_maybe); 114 115 if (!record_to_write.removed) try validateName(record_to_write.event.name); 116 117 if (existing_record_maybe) |existing_record| { 118 // updates preserve the original creation metadata 119 record_to_write.created_order = existing_record.created_order; 120 121 // drop the old active index; active values are re-added below 122 if (!existing_record.removed) { 123 _ = try name_to_repo_id.remove(existing_record.event.name); 124 } 125 } 126 127 const repo_cursor = try event_id_to_repo.putCursor(repo_key); 128 const repo = try DB.HashMap(.read_write).init(repo_cursor); 129 try evt.upsert(Record, DB, hash_kind, repo, record_to_write); 130 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .repo, existing_record_maybe, record_to_write); 131 132 const order_key = evt.orderKeyDesc(record_to_write.created_order, event_id); 133 134 // the id set retains removed records so merges can carry removals 135 if (existing_cursor_maybe == null) { 136 const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key)); 137 const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); 138 try repo_id_set.put(&order_key); 139 } 140 141 if (!record_to_write.removed) { 142 try name_to_repo_id.put(record_to_write.event.name, .{ .bytes = event_id }); 143 } 144 } 145 146 // a repo plus its event id (the id is the on-disk repo directory name, so the 147 // caller can locate the working repo under <server>/repos/<hex event id>). 148 pub const RepoWithId = struct { 149 repo: Record, 150 event_id: [evt.event_id_size]u8, 151 }; 152 153 // a repo as one viewer sees it. without a role, the viewer may not see it. 154 pub const RepoWithRole = struct { 155 repo: Record, 156 event_id: [evt.event_id_size]u8, 157 role: ?Role, 158 }; 159 160 // read a repo by its name from its owner's user repo 161 pub fn readByName( 162 comptime DB: type, 163 comptime hash_kind: hash.HashKind, 164 user_moment: DB.HashMap(.read_only), 165 arena: *std.heap.ArenaAllocator, 166 name: []const u8, 167 ) !?RepoWithId { 168 const name_to_repo_id_cursor = try user_moment.getCursor(hash.hashInt(hash_kind, name_index_key)) orelse return null; 169 const name_to_repo_id = try DB.SortedMap(.read_only).init(name_to_repo_id_cursor); 170 const repo_id_cursor = try name_to_repo_id.getCursor(name) orelse return null; 171 var repo_id: [evt.event_id_size]u8 = undefined; 172 _ = try repo_id_cursor.readBytes(&repo_id); 173 const repo = (try readById(DB, hash_kind, user_moment, arena, &repo_id)) orelse return null; 174 return .{ .repo = repo, .event_id = repo_id }; 175 } 176 177 pub fn readById( 178 comptime DB: type, 179 comptime hash_kind: hash.HashKind, 180 haxy_moment: DB.HashMap(.read_only), 181 arena: *std.heap.ArenaAllocator, 182 id: []const u8, 183 ) !?Record { 184 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 185 const records = try DB.HashMap(.read_only).init(records_cursor); 186 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 187 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 188 } 189