╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const hx = @import("haxy"); 3 const evt = hx.event; 4 const xit = hx.xit; 5 const rp = xit.repo; 6 const hash = xit.hash; 7 8 test "rebase" { 9 const io = std.testing.io; 10 const allocator = std.testing.allocator; 11 const temp_dir_name = "temp-event-rebase"; 12 13 // create the temp dir 14 const cwd = std.Io.Dir.cwd(); 15 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 16 if (temp_dir_or_err) |*temp_dir| { 17 temp_dir.close(io); 18 try cwd.deleteTree(io, temp_dir_name); 19 } else |_| {} 20 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 21 defer cwd.deleteTree(io, temp_dir_name) catch {}; 22 defer temp_dir.close(io); 23 24 const cwd_path = try std.process.currentPathAlloc(io, allocator); 25 defer allocator.free(cwd_path); 26 27 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 28 defer allocator.free(work_path); 29 30 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 31 const Repo = rp.Repo(.xit, repo_opts); 32 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 33 defer repo.deinit(io, allocator); 34 35 // 36 // define test events 37 // 38 39 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 40 41 const first_event_id = evt.randomId(prng.random()); 42 43 const events_to_consume = [_]evt.Event{ 44 .{ 45 .id = std.fmt.bytesToHex(first_event_id, .lower), 46 .data = .{ 47 .issue = .{ 48 .title = "Login form clears password on validation error", 49 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 50 .tags = "bug\x00priority-high\x00ui", 51 }, 52 }, 53 }, 54 // this event edits the previous one because it has the same id 55 .{ 56 .id = std.fmt.bytesToHex(first_event_id, .lower), 57 .data = .{ 58 .issue = .{ 59 .title = "Login form clears password on validation error", 60 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 61 .tags = "bug\x00priority-low\x00ui", 62 }, 63 }, 64 }, 65 .{ 66 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 67 .data = .{ 68 .issue = .{ 69 .title = "Search results ignore archived project filter", 70 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 71 .tags = "bug\x00search\x00backend", 72 }, 73 }, 74 }, 75 .{ 76 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 77 .data = .{ 78 .issue = .{ 79 .title = "Issue list does not persist selected sort order", 80 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 81 .tags = "enhancement\x00frontend\x00preferences", 82 }, 83 }, 84 }, 85 }; 86 87 // 88 // insert issues as commits in the repo 89 // 90 91 var arena = std.heap.ArenaAllocator.init(allocator); 92 defer arena.deinit(); 93 94 { 95 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 96 defer json.deinit(); 97 98 for (events_to_consume) |event| { 99 json.clearRetainingCapacity(); 100 101 try std.json.Stringify.value(event, .{}, &json.writer); 102 103 // commit the event into a special branch 104 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 105 } 106 } 107 108 // 109 // read and parse all of the events from the repo 110 // 111 112 const Commit = struct { 113 parent_oid: ?[hash.byteLen(repo_opts.hash)]u8, 114 oid: [hash.byteLen(repo_opts.hash)]u8, 115 message: []const u8, 116 117 fn readCommitsFromRepo(inner_arena: *std.heap.ArenaAllocator, inner_repo: *Repo, branch_name: []const u8) ![]@This() { 118 const inner_allocator = inner_arena.allocator(); 119 120 var commits: std.ArrayList(@This()) = .empty; 121 122 const ref = try inner_repo.readRef(io, .{ .kind = .head, .name = branch_name }) orelse return error.RefNotFound; 123 var commit_iter = try inner_repo.log(io, inner_allocator, &.{ref}); 124 125 // read the message from each commit 126 while (try commit_iter.next()) |commit_object| { 127 try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); 128 const message = try commit_object.object_reader.interface.allocRemaining(inner_allocator, .unlimited); 129 130 var commit: @This() = .{ 131 .parent_oid = undefined, 132 .oid = undefined, 133 .message = message, 134 }; 135 136 _ = try std.fmt.hexToBytes(&commit.oid, &commit_object.oid); 137 138 if (commit_object.content.commit.metadata.parent_oids) |parent_oids| { 139 if (parent_oids.len > 0) { 140 _ = try std.fmt.hexToBytes(&(commit.parent_oid orelse unreachable), &parent_oids[0]); 141 } else { 142 commit.parent_oid = null; 143 } 144 } else { 145 commit.parent_oid = null; 146 } 147 148 try commits.append(inner_allocator, commit); 149 } 150 151 return commits.items; 152 } 153 }; 154 155 var events: std.ArrayList(evt.RepoEvent(repo_opts.hash)) = .empty; 156 defer events.deinit(allocator); 157 158 { 159 const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); 160 161 // parse commit messages as JSON into event values. 162 // add events in reverse order so the earliest event is first. 163 for (0..commits.len) |i| { 164 const commit = commits[commits.len - i - 1]; 165 const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{}); 166 try events.append(allocator, .{ 167 .parent_oid = commit.parent_oid, 168 .oid = commit.oid, 169 .event = event, 170 }); 171 } 172 } 173 174 const first_oid = events.items[0].oid; 175 176 // 177 // consume events into the database 178 // 179 180 { 181 try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items); 182 183 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 184 185 // read the moment we just created 186 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 187 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 188 189 // get the last object id 190 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 191 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 192 _ = try last_object_id_cursor.readBytes(&last_object_id); 193 194 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 195 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 196 197 try std.testing.expectEqual(1, try haxy.count()); 198 199 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 200 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 201 202 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 203 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 204 205 // get the views 206 const views_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "views")) orelse return error.NotFound; 207 const views = try Repo.DB.HashMap(.read_only).init(views_cursor); 208 209 // get the map of issues 210 const event_id_to_issue_cursor = try views.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 211 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 212 213 // get the issue out of the map that was edited 214 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 215 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 216 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 217 218 // the description was correctly edited 219 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.description, first_issue.issue.description); 220 221 // the tags were correctly edited 222 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.tags, first_issue.issue.tags); 223 } 224 225 // 226 // rebase the branch so it no longer includes the edit event 227 // 228 229 const rebased_events_to_consume = [_]evt.Event{ 230 events_to_consume[2], 231 events_to_consume[3], 232 }; 233 234 { 235 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 236 defer json.deinit(); 237 238 for (rebased_events_to_consume, 0..) |event, i| { 239 json.clearRetainingCapacity(); 240 241 try std.json.Stringify.value(event, .{}, &json.writer); 242 243 // commit the event into a special branch 244 _ = try repo.commitAtRef( 245 io, 246 allocator, 247 .{ 248 .message = json.written(), 249 // the first event should have the first oid as its parent, 250 // because we want to keep the very first commit. 251 // and the rest will have null which causes them 252 // to have the previous commit as their parent 253 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 254 }, 255 null, 256 .{ .kind = .head, .name = "haxy/meta" }, 257 ); 258 } 259 } 260 261 // 262 // read and parse all of the events from the repo 263 // 264 265 events.clearRetainingCapacity(); 266 267 { 268 const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); 269 270 // parse commit messages as JSON into event values. 271 // add events in reverse order so the earliest event is first. 272 for (0..commits.len) |i| { 273 const commit = commits[commits.len - i - 1]; 274 const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{}); 275 try events.append(allocator, .{ 276 .parent_oid = commit.parent_oid, 277 .oid = commit.oid, 278 .event = event, 279 }); 280 } 281 } 282 283 // 284 // consume events into the database 285 // 286 287 { 288 try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items); 289 290 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 291 292 // read the moment we just created 293 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 294 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 295 296 // get the last object id 297 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 298 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 299 _ = try last_object_id_cursor.readBytes(&last_object_id); 300 301 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 302 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 303 304 try std.testing.expectEqual(2, try haxy.count()); 305 306 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 307 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 308 309 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 310 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 311 312 // get the views 313 const views_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "views")) orelse return error.NotFound; 314 const views = try Repo.DB.HashMap(.read_only).init(views_cursor); 315 316 // get the map of issues 317 const event_id_to_issue_cursor = try views.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 318 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 319 320 // get the issue out of the map that was edited 321 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 322 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 323 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 324 325 // the description is no longer edited 326 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 327 328 // the tags are no longer edited 329 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 330 } 331 332 // 333 // rebase the branch so it no longer includes the original issue 334 // 335 336 { 337 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 338 defer json.deinit(); 339 340 for (rebased_events_to_consume, 0..) |event, i| { 341 json.clearRetainingCapacity(); 342 343 try std.json.Stringify.value(event, .{}, &json.writer); 344 345 // commit the event into a special branch 346 _ = try repo.commitAtRef( 347 io, 348 allocator, 349 .{ 350 .message = json.written(), 351 // the first event should have no parents, 352 // because we want to drop the very first commit. 353 // and the rest will have null which causes them 354 // to have the previous commit as their parent 355 .parent_oids = if (i == 0) &.{} else null, 356 }, 357 null, 358 .{ .kind = .head, .name = "haxy/meta" }, 359 ); 360 } 361 } 362 363 // 364 // read and parse all of the events from the repo 365 // 366 367 events.clearRetainingCapacity(); 368 369 { 370 const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); 371 372 // parse commit messages as JSON into event values. 373 // add events in reverse order so the earliest event is first. 374 for (0..commits.len) |i| { 375 const commit = commits[commits.len - i - 1]; 376 const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{}); 377 try events.append(allocator, .{ 378 .parent_oid = commit.parent_oid, 379 .oid = commit.oid, 380 .event = event, 381 }); 382 } 383 } 384 385 // 386 // consume events into the database 387 // 388 389 { 390 try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items); 391 392 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 393 394 // read the moment we just created 395 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 396 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 397 398 // get the last object id 399 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 400 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 401 _ = try last_object_id_cursor.readBytes(&last_object_id); 402 403 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 404 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 405 406 try std.testing.expectEqual(1, try haxy.count()); 407 408 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 409 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 410 411 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 412 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 413 414 // get the views 415 const views_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "views")) orelse return error.NotFound; 416 const views = try Repo.DB.HashMap(.read_only).init(views_cursor); 417 418 // get the map of issues 419 const event_id_to_issue_cursor = try views.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 420 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 421 422 // the first issue no longer exists 423 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 424 } 425 } 426