╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───╮ │src/test.zig│ ╰────────────╯ ╭───────────────────────────────────────────╮ │1 comptime { │ │2 _ = @import("test/event.zig"); │ │3 + _ = @import("test/event/admin.zig");│ │4 _ = @import("test/event/patch.zig");│ │5 _ = @import("test/ssh.zig"); │ │6 _ = @import("test/ui.zig"); │ ╰───────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1050 const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; │ │1051 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ │1052 return try evt.read(evt.Issue.Record, DB, hash_kind, arena, issue_map); │ │1053 -} │ │1054 - │ │1055 -test "user and repo" { │ │1056 - const io = std.testing.io; │ │1057 - const allocator = std.testing.allocator; │ │1058 - const temp_dir_name = "temp-event-user-and-repo"; │ │1059 - │ │1060 - // create the temp dir │ │1061 - const cwd = std.Io.Dir.cwd(); │ │1062 - var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); │ │1063 - if (temp_dir_or_err) |*temp_dir| { │ │1064 - temp_dir.close(io); │ │1065 - try cwd.deleteTree(io, temp_dir_name); │ │1066 - } else |_| {} │ │1067 - var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); │ │1068 - defer cwd.deleteTree(io, temp_dir_name) catch {}; │ │1069 - defer temp_dir.close(io); │ │1070 - │ │1071 - const cwd_path = try std.process.currentPathAlloc(io, allocator); │ │1072 - defer allocator.free(cwd_path); │ │1073 - │ │1074 - const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); │ │1075 - defer allocator.free(work_path); │ │1076 - │ │1077 - const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │1078 - const Repo = rp.Repo(.xit, repo_opts); │ │1079 - var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │1080 - defer repo.deinit(io, allocator); │ │1081 - │ │1082 - var arena = std.heap.ArenaAllocator.init(allocator); │ │1083 - defer arena.deinit(); │ │1084 - │ │1085 - // │ │1086 - // define test events │ │1087 - // │ │1088 - │ │1089 - var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │1090 - const user_event_id = evt.EventWithId.randomId(prng.random()); │ │1091 - const repo_event_id = evt.EventWithId.randomId(prng.random()); │ │1092 - │ │1093 - var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; │ │1094 - const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); │ │1095 - │ │1096 - var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; │ │1097 - const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); │ │1098 - │ │1099 - const events_to_consume = [_]evt.EventWithId{ │ │1100 - .{ │ │1101 - .id = std.fmt.bytesToHex(user_event_id, .lower), │ │1102 - .author = author, │ │1103 - .event = .{ │ │1104 - .user = .{ │ │1105 - .name = "alice", │ │1106 - .email = "alice@example.test", │ │1107 - .password_hash = first_password_hash, │ │1108 - }, │ │1109 - }, │ │1110 - }, │ │1111 - // this event edits the previous one because it has the same id │ │1112 - .{ │ │1113 - .id = std.fmt.bytesToHex(user_event_id, .lower), │ │1114 - .author = author, │ │1115 - .event = .{ │ │1116 - .user = .{ │ │1117 - .name = "alice", │ │1118 - .email = "alice@example.test", │ │1119 - .password_hash = second_password_hash, │ │1120 - }, │ │1121 - }, │ │1122 - }, │ │1123 - .{ │ │1124 - .id = std.fmt.bytesToHex(repo_event_id, .lower), │ │1125 - .author = author, │ │1126 - .event = .{ │ │1127 - .repo = .{ │ │1128 - .user_id = &user_event_id, │ │1129 - .name = "ziglings", │ │1130 - .description = "Learn the Zig programming language by fixing tiny broken programs", │ │1131 - }, │ │1132 - }, │ │1133 - }, │ │1134 - }; │ │1135 - │ │1136 - // commit and consume the seed events │ │1137 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); │ │1138 - │ │1139 - { │ │1140 - const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1141 - │ │1142 - // get the map of users │ │1143 - const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; │ │1144 - const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); │ │1145 - │ │1146 - // get the user out of the map that was edited │ │1147 - const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1148 - const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); │ │1149 - const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); │ │1150 - │ │1151 - // the password was correctly edited │ │1152 - try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); │ │1153 - │ │1154 - // get the map of repos │ │1155 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1156 - const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1157 - │ │1158 - // get the repo out of the map │ │1159 - const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1160 - const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1161 - const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1162 - │ │1163 - try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.event.user_id); │ │1164 - try std.testing.expectEqualStrings("ziglings", repo_event.event.name); │ │1165 - │ │1166 - // get the repos created by the user │ │1167 - const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; │ │1168 - const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │1169 - │ │1170 - const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1171 - const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); │ │1172 - │ │1173 - try std.testing.expectEqual(1, try user_repos.count()); │ │1174 - │ │1175 - // the set is keyed by orderKeyDesc([created-order][event-id]) │ │1176 - const order_key = evt.orderKeyDesc(repo_event.created_order, &repo_event_id); │ │1177 - try std.testing.expect(try user_repos.contains(&order_key)); │ │1178 - } │ │1179 - │ │1180 - // │ │1181 - // remove the repo │ │1182 - // │ │1183 - │ │1184 - const events_to_consume2 = [_]evt.EventWithId{ │ │1185 - .{ │ │1186 - .id = std.fmt.bytesToHex(repo_event_id, .lower), │ │1187 - .author = author, │ │1188 - .event = .{ .repo = null }, │ │1189 - }, │ │1190 - }; │ │1191 - │ │1192 - // commit and consume the removal │ │1193 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); │ │1194 - │ │1195 - { │ │1196 - const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1197 - │ │1198 - // get the map of repos │ │1199 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1200 - const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1201 - │ │1202 - const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1203 - const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1204 - const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1205 - try std.testing.expect(repo_event.removed); │ │1206 - │ │1207 - // get the repos created by the user │ │1208 - const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; │ │1209 - const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │1210 - │ │1211 - const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1212 - const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); │ │1213 - │ │1214 - // removing the repo emptied the user's set │ │1215 - try std.testing.expectEqual(0, try user_repos.count()); │ │1216 - } │ │1217 - │ │1218 - // a non-null payload restores the repo and its indexes │ │1219 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │1220 - .id = std.fmt.bytesToHex(repo_event_id, .lower), │ │1221 - .author = author, │ │1222 - .event = events_to_consume[2].event, │ │1223 - }}); │ │1224 - │ │1225 - { │ │1226 - const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1227 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1228 - const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1229 - const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1230 - const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1231 - const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1232 - try std.testing.expect(!repo_event.removed); │ │1233 - │ │1234 - const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; │ │1235 - const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │1236 - const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1237 - const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); │ │1238 - try std.testing.expectEqual(1, try user_repos.count()); │ │1239 - } │ │1240 - │ │1241 - // │ │1242 - // remove the user │ │1243 - // │ │1244 - │ │1245 - const events_to_consume3 = [_]evt.EventWithId{ │ │1246 - .{ │ │1247 - .id = std.fmt.bytesToHex(user_event_id, .lower), │ │1248 - .author = author, │ │1249 - .event = .{ .user = null }, │ │1250 - }, │ │1251 - }; │ │1252 - │ │1253 - // commit and consume the removal │ │1254 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); │ │1255 - │ │1256 - { │ │1257 - const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1258 - │ │1259 - // get the map of users │ │1260 - const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; │ │1261 - const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); │ │1262 - │ │1263 - const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1264 - const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); │ │1265 - const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); │ │1266 - try std.testing.expect(user_event.removed); │ │1267 - │ │1268 - // removing the user removes their active repos too │ │1269 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1270 - const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1271 - const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1272 - const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1273 - const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1274 - try std.testing.expect(repo_event.removed); │ │1275 - } │ │1276 -} │ │1277 - │ │1278 -test "repos and users paginate newest first" { │ │1279 - const io = std.testing.io; │ │1280 - const allocator = std.testing.allocator; │ │1281 - const temp_dir_name = "temp-event-order"; │ │1282 - │ │1283 - const cwd = std.Io.Dir.cwd(); │ │1284 - var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); │ │1285 - if (temp_dir_or_err) |*temp_dir| { │ │1286 - temp_dir.close(io); │ │1287 - try cwd.deleteTree(io, temp_dir_name); │ │1288 - } else |_| {} │ │1289 - var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); │ │1290 - defer cwd.deleteTree(io, temp_dir_name) catch {}; │ │1291 - defer temp_dir.close(io); │ │1292 - │ │1293 - const cwd_path = try std.process.currentPathAlloc(io, allocator); │ │1294 - defer allocator.free(cwd_path); │ │1295 - const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); │ │1296 - defer allocator.free(work_path); │ │1297 - │ │1298 - const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │1299 - const Repo = rp.Repo(.xit, repo_opts); │ │1300 - var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │1301 - defer repo.deinit(io, allocator); │ │1302 - │ │1303 - var arena = std.heap.ArenaAllocator.init(allocator); │ │1304 - defer arena.deinit(); │ │1305 - │ │1306 - var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │1307 - const user_id = evt.EventWithId.randomId(prng.random()); │ │1308 - var repo_ids: [4][evt.event_id_size]u8 = undefined; │ │1309 - for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); │ │1310 - │ │1311 - var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; │ │1312 - const pw = try evt.User.hashPassword("pw", &pw_buf, io); │ │1313 - │ │1314 - // asserts the repo-id-set holds exactly `expected` ids in that order │ │1315 - const Check = struct { │ │1316 - fn order( │ │1317 - comptime DB: type, │ │1318 - comptime hash_kind: hash.HashKind, │ │1319 - moment: DB.HashMap(.read_only), │ │1320 - expected: []const *const [evt.event_id_size]u8, │ │1321 - ) !void { │ │1322 - const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; │ │1323 - const set = try DB.SortedSet(.read_only).init(cursor); │ │1324 - try std.testing.expectEqual(expected.len, try set.count()); │ │1325 - for (expected, 0..) |id, i| { │ │1326 - const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; │ │1327 - // the key is orderKeyDesc ([created-order][event-id]); its trailing bytes are the id │ │1328 - var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │1329 - _ = try kv.key_cursor.readBytes(&order_key); │ │1330 - try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); │ │1331 - } │ │1332 - } │ │1333 - }; │ │1334 - │ │1335 - // one user, then four repos in creation order with deliberately skewed │ │1336 - // timestamps │ │1337 - const events = [_]evt.EventWithId{ │ │1338 - .{ .id = std.fmt.bytesToHex(user_id, .lower), .author = author, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = pw } } },│ │1339 - .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 10_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, │ │1340 - .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 10, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, │ │1341 - .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author = author, .timestamp = 5_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, │ │1342 - .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author = author, .timestamp = 20, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, │ │1343 - }; │ │1344 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); │ │1345 - │ │1346 - { │ │1347 - const moment = try evt.currentMoment(repo_opts, &repo); │ │1348 - try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); │ │1349 - │ │1350 - // the single user shows up in its own ordered set │ │1351 - const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; │ │1352 - const uset = try Repo.DB.SortedSet(.read_only).init(ucur); │ │1353 - try std.testing.expectEqual(1, try uset.count()); │ │1354 - } │ │1355 - │ │1356 - // removing repo1 retains its place in the canonical order │ │1357 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ │ │1358 - .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 200, .event = .{ .repo = null } }, │ │1359 - }); │ │1360 - { │ │1361 - const moment = try evt.currentMoment(repo_opts, &repo); │ │1362 - try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); │ │1363 - } │ │1364 - │ │1365 - // update repo0 at a later timestamp -> keeps its original place │ │1366 - try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ │ │1367 - .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, │ │1368 - }); │ │1369 - { │ │1370 - const moment = try evt.currentMoment(repo_opts, &repo); │ │1371 - try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); │ │1372 - │ │1373 - // the value really was updated │ │1374 - const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1375 - const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); │ │1376 - const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; │ │1377 - const rm = try Repo.DB.HashMap(.read_only).init(rc); │ │1378 - const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); │ │1379 - try std.testing.expectEqualStrings("updated", re.event.description); │ │1380 - } │ │1053 } │ │1054 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │424 defer allocator.free(root); │ │425 const repos_dir = try std.fs.path.join(allocator, &.{ root, "repos" }); │ │426 defer allocator.free(repos_dir); │ │427 + const admin_path = try std.fs.path.join(allocator, &.{ root, "admin" }); │ │428 + defer allocator.free(admin_path); │ │429 const upstream_path = try std.fs.path.join(allocator, &.{ root, "upstream" }); │ │430 defer allocator.free(upstream_path); │ │429 - const target_path = try std.fs.path.join(allocator, &.{ repos_dir, "target" }); │ │431 + │ │432 + var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │433 + const user_id = evt.EventWithId.randomId(prng.random()); │ │434 + const repo_id = evt.EventWithId.randomId(prng.random()); │ │435 + const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); │ │436 + const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); │ │437 defer allocator.free(target_path); │ │438 │ │439 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path });│ │440 + defer admin.deinit(io, allocator); │ │441 + try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ │ │442 + .{ │ │443 + .id = std.fmt.bytesToHex(user_id, .lower), │ │444 + .timestamp = 1, │ │445 + .author = author, │ │446 + .event = .{ .user = .{ │ │447 + .name = author.name, │ │448 + .email = author.email, │ │449 + .password_hash = "", │ │450 + } }, │ │451 + }, │ │452 + .{ │ │453 + .id = repo_id_hex, │ │454 + .timestamp = 1, │ │455 + .author = author, │ │456 + .event = .{ .repo = .{ │ │457 + .user_id = &user_id, │ │458 + .name = "repo", │ │459 + .description = "", │ │460 + } }, │ │461 + }, │ │462 + }); │ │463 + │ │464 // │ │465 // create the target repo │ │466 // │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │477 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); │ │478 defer target.deinit(io, allocator); │ │479 try target.addBranch(io, .{ .name = "next" }); │ │448 - │ │449 - var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │450 - const unused_id = evt.EventWithId.randomId(prng.random()); │ │451 - const unused_hex = std.fmt.bytesToHex(unused_id, .lower); │ │452 - │ │453 - const route_text = try std.fmt.allocPrint(allocator, "alice/repo/patch:{s}/branch:feature/topic", .{&unused_hex});│ │454 - defer allocator.free(route_text); │ │455 - const route = fork.parseRoute(route_text) orelse return error.InvalidRoute; │ │456 - try std.testing.expectEqualStrings("alice/repo", route.identity); │ │457 - try std.testing.expectEqualStrings("feature/topic", route.target); │ │458 - │ │459 - // │ │460 - // reserve an author-bound draft │ │461 - // │ │462 - │ │463 - const unused_path = try fork.create(repo_opts, io, allocator, repos_dir, &unused_hex, author.name); │ │464 - defer allocator.free(unused_path); │ │465 - var unused = try Repo.open(io, allocator, .{ .path = unused_path }); │ │466 - defer unused.deinit(io, allocator); │ │467 - const stored_author = (try fork.draftAuthor(repo_opts, &unused, io, allocator)) orelse return error.NotFound; │ │468 - defer allocator.free(stored_author); │ │469 - try std.testing.expectEqualStrings(author.name, stored_author); │ │470 - var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │471 - const head = try unused.head(io, &head_buf); │ │472 - switch (head) { │ │473 - .ref => |head_ref| try std.testing.expectEqualStrings(fork.ref.name, head_ref.name), │ │474 - .oid => return error.InvalidRef, │ │475 - } │ │480 │ │481 // │ │478 - // push a proposed commit into another draft │ │482 + // push a proposed commit into a draft │ │483 // │ │484 │ │485 const patch_id = evt.EventWithId.randomId(prng.random()); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │486 const patch_id_hex = std.fmt.bytesToHex(patch_id, .lower); │ │487 const draft_path = try fork.forkPath(allocator, repos_dir, &patch_id_hex); │ │488 defer allocator.free(draft_path); │ │489 + try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ │ │490 + .id = patch_id_hex, │ │491 + .timestamp = 2, │ │492 + .author = author, │ │493 + .event = .{ .fork = .{ │ │494 + .user_id = &user_id, │ │495 + .repo_id = &repo_id, │ │496 + .target = "master", │ │497 + } }, │ │498 + }}); │ │499 try cwd.createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPath); │ │500 var draft = try Repo.clone(io, allocator, upstream_path, draft_path, draft_path, null, .{}); │ │501 + var source_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │502 { │ │503 defer draft.deinit(io, allocator); │ │489 - try draft.addConfig(io, allocator, .{ .name = "haxy.author", .value = author.name }); │ │490 - try draft.addConfig(io, allocator, .{ .name = "haxy.target", .value = "master" }); │ │504 const file = try draft.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true });│ │505 defer file.close(io); │ │506 try file.writeStreamingAll(io, "pub const answer = 42;\n"); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │507 try draft.add(io, allocator, &.{"feature.zig"}); │ │495 - _ = try draft.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); │ │508 + source_oid = try draft.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 });│ │509 try draft.addBranch(io, .{ .name = fork.ref.name }); │ │510 } │ │511 + try fork.recordPush(io, allocator, &admin, &patch_id_hex, "master", &source_oid, author, 3); │ │512 │ │513 // │ │514 // publish the staged code and metadata │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │515 // │ │516 │ │503 - try fork.publish(repo_opts, io, allocator, &target, draft_path, .{ │ │517 + try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{│ │518 .id = patch_id_hex, │ │519 + .user_id = user_id, │ │520 + .repo_id = repo_id, │ │521 .title = "add the answer", │ │522 .tags = "enhancement", │ │523 .description = "adds a reusable answer constant", │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │533 const first_patchrev_id = try evt.parseEventId(&patch.event.patchrev_id); │ │534 const first_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)) orelse return error.NotFound; │ │535 try std.testing.expectEqualStrings("add feature", first_patchrev.event.message); │ │536 + const first_patchrev_hex = patch.event.patchrev_id; │ │537 │ │538 + var fork_arena = std.heap.ArenaAllocator.init(allocator); │ │539 + defer fork_arena.deinit(); │ │540 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │541 + const published_fork = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &fork_arena, &patch_id)) orelse return error.NotFound;│ │542 + try std.testing.expect(!published_fork.removed); │ │543 + │ │544 // │ │545 // edit metadata without replacing the code revision │ │546 // │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │547 │ │525 - try fork.publish(repo_opts, io, allocator, &target, draft_path, .{ │ │548 + try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{│ │549 .id = patch_id_hex, │ │550 + .user_id = user_id, │ │551 + .repo_id = repo_id, │ │552 .title = "add a reusable answer", │ │553 .tags = "enhancement", │ │554 .description = "adds a reusable answer constant", │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │558 _ = arena.reset(.retain_capacity); │ │559 moment = try evt.currentMoment(repo_opts, &target); │ │560 const edited = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound;│ │536 - try std.testing.expectEqualStrings(&patch.event.patchrev_id, &edited.event.patchrev_id); │ │561 + try std.testing.expectEqualStrings(&first_patchrev_hex, &edited.event.patchrev_id); │ │562 + const edited_patchrev_hex = edited.event.patchrev_id; │ │563 │ │564 // │ │565 // retargeting creates another code revision │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯