╭ 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 var arena = std.heap.ArenaAllocator.init(allocator); 36 defer arena.deinit(); 37 38 // 39 // define test events 40 // 41 42 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 43 44 const first_event_id = evt.randomId(prng.random()); 45 46 const events_to_consume = [_]evt.Event{ 47 .{ 48 .id = std.fmt.bytesToHex(first_event_id, .lower), 49 .data = .{ 50 .issue = .{ 51 .title = "Login form clears password on validation error", 52 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 53 .tags = "bug\x00priority-high\x00ui", 54 }, 55 }, 56 }, 57 // this event edits the previous one because it has the same id 58 .{ 59 .id = std.fmt.bytesToHex(first_event_id, .lower), 60 .data = .{ 61 .issue = .{ 62 .title = "Login form clears password on validation error", 63 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 64 .tags = "bug\x00priority-low\x00ui", 65 }, 66 }, 67 }, 68 .{ 69 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 70 .data = .{ 71 .issue = .{ 72 .title = "Search results ignore archived project filter", 73 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 74 .tags = "bug\x00search\x00backend", 75 }, 76 }, 77 }, 78 .{ 79 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 80 .data = .{ 81 .issue = .{ 82 .title = "Issue list does not persist selected sort order", 83 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 84 .tags = "enhancement\x00frontend\x00preferences", 85 }, 86 }, 87 }, 88 }; 89 90 // 91 // insert issues as commits in the repo 92 // 93 94 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 95 96 { 97 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 98 defer json.deinit(); 99 100 for (events_to_consume, 0..) |event, i| { 101 json.clearRetainingCapacity(); 102 103 try std.json.Stringify.value(event, .{}, &json.writer); 104 105 // commit the event into a special branch 106 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 107 if (i == 0) { 108 _ = try std.fmt.hexToBytes(&first_oid, &oid); 109 } 110 } 111 } 112 113 // 114 // consume events into the database 115 // 116 117 { 118 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 119 120 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 121 122 // read the moment we just created 123 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 124 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 125 126 // get the last object id 127 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 128 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 129 _ = try last_object_id_cursor.readBytes(&last_object_id); 130 131 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 132 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 133 134 try std.testing.expectEqual(1, try haxy.count()); 135 136 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 137 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 138 139 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 140 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 141 142 // get the map of issues 143 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 144 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 145 146 // get the issue out of the map that was edited 147 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 148 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 149 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 150 151 // the description was correctly edited 152 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.description, first_issue.issue.description); 153 154 // the tags were correctly edited 155 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.tags, first_issue.issue.tags); 156 } 157 158 // 159 // add another event 160 // 161 162 const events_to_consume2 = [_]evt.Event{ 163 .{ 164 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 165 .data = .{ 166 .issue = .{ 167 .title = "Double clicking causes the form to submit twice", 168 .description = "When I double click the post button I see duplicate posts.", 169 .tags = "bug\x00priority-high\x00ui", 170 }, 171 }, 172 }, 173 }; 174 175 { 176 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 177 defer json.deinit(); 178 179 for (events_to_consume2) |event| { 180 json.clearRetainingCapacity(); 181 182 try std.json.Stringify.value(event, .{}, &json.writer); 183 184 // commit the event into a special branch 185 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 186 } 187 } 188 189 // 190 // consume events into the database 191 // 192 193 { 194 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 195 196 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 197 198 // read the moment we just created 199 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 200 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 201 202 // get the last object id 203 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 204 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 205 _ = try last_object_id_cursor.readBytes(&last_object_id); 206 207 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 208 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 209 210 try std.testing.expectEqual(2, try haxy.count()); 211 } 212 213 // 214 // rebase the branch so it no longer includes the edit event 215 // 216 217 const events_to_consume3 = [_]evt.Event{ 218 events_to_consume[2], 219 events_to_consume[3], 220 }; 221 222 { 223 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 224 defer json.deinit(); 225 226 for (events_to_consume3, 0..) |event, i| { 227 json.clearRetainingCapacity(); 228 229 try std.json.Stringify.value(event, .{}, &json.writer); 230 231 // commit the event into a special branch 232 _ = try repo.commitAtRef( 233 io, 234 allocator, 235 .{ 236 .message = json.written(), 237 // the first event should have the first oid as its parent, 238 // because we want to keep the very first commit. 239 // and the rest will have null which causes them 240 // to have the previous commit as their parent 241 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 242 }, 243 null, 244 .{ .kind = .head, .name = "haxy/meta" }, 245 ); 246 } 247 } 248 249 // 250 // consume events into the database 251 // 252 253 { 254 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 255 256 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 257 258 // read the moment we just created 259 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 260 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 261 262 // get the last object id 263 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 264 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 265 _ = try last_object_id_cursor.readBytes(&last_object_id); 266 267 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 268 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 269 270 try std.testing.expectEqual(2, try haxy.count()); 271 272 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 273 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 274 275 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 276 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 277 278 // get the map of issues 279 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 280 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 281 282 // get the issue out of the map that was edited 283 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 284 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 285 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 286 287 // the description is no longer edited 288 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 289 290 // the tags are no longer edited 291 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 292 293 // an event added by the second push is no longer there because it was wiped out by the rebase 294 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &events_to_consume2[0].id))); 295 } 296 297 // 298 // rebase the branch so it no longer includes the original issue 299 // 300 301 { 302 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 303 defer json.deinit(); 304 305 for (events_to_consume3, 0..) |event, i| { 306 json.clearRetainingCapacity(); 307 308 try std.json.Stringify.value(event, .{}, &json.writer); 309 310 // commit the event into a special branch 311 _ = try repo.commitAtRef( 312 io, 313 allocator, 314 .{ 315 .message = json.written(), 316 // the first event should have no parents, 317 // because we want to drop the very first commit. 318 // and the rest will have null which causes them 319 // to have the previous commit as their parent 320 .parent_oids = if (i == 0) &.{} else null, 321 }, 322 null, 323 .{ .kind = .head, .name = "haxy/meta" }, 324 ); 325 } 326 } 327 328 // 329 // consume events into the database 330 // 331 332 { 333 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 334 335 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 336 337 // read the moment we just created 338 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 339 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 340 341 // get the last object id 342 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 343 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 344 _ = try last_object_id_cursor.readBytes(&last_object_id); 345 346 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 347 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 348 349 try std.testing.expectEqual(1, try haxy.count()); 350 351 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 352 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 353 354 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 355 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 356 357 // get the map of issues 358 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 359 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 360 361 // the first issue no longer exists 362 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 363 } 364 } 365 366 test "merge" { 367 const io = std.testing.io; 368 const allocator = std.testing.allocator; 369 const temp_dir_name = "temp-event-merge"; 370 371 // create the temp dir 372 const cwd = std.Io.Dir.cwd(); 373 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 374 if (temp_dir_or_err) |*temp_dir| { 375 temp_dir.close(io); 376 try cwd.deleteTree(io, temp_dir_name); 377 } else |_| {} 378 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 379 defer cwd.deleteTree(io, temp_dir_name) catch {}; 380 defer temp_dir.close(io); 381 382 const cwd_path = try std.process.currentPathAlloc(io, allocator); 383 defer allocator.free(cwd_path); 384 385 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 386 defer allocator.free(work_path); 387 388 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 389 const Repo = rp.Repo(.xit, repo_opts); 390 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 391 defer repo.deinit(io, allocator); 392 393 var arena = std.heap.ArenaAllocator.init(allocator); 394 defer arena.deinit(); 395 396 // 397 // define test events 398 // 399 400 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 401 402 const events_to_consume = [_]evt.Event{ 403 .{ 404 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 405 .data = .{ 406 .issue = .{ 407 .title = "Login form clears password on validation error", 408 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 409 .tags = "bug\x00priority-high\x00ui", 410 }, 411 }, 412 }, 413 .{ 414 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 415 .data = .{ 416 .issue = .{ 417 .title = "Search results ignore archived project filter", 418 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 419 .tags = "bug\x00search\x00backend", 420 }, 421 }, 422 }, 423 .{ 424 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 425 .data = .{ 426 .issue = .{ 427 .title = "Issue list does not persist selected sort order", 428 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 429 .tags = "enhancement\x00frontend\x00preferences", 430 }, 431 }, 432 }, 433 }; 434 435 // 436 // insert issues as commits in the repo 437 // 438 439 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 440 441 { 442 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 443 defer json.deinit(); 444 445 for (events_to_consume, 0..) |event, i| { 446 json.clearRetainingCapacity(); 447 448 try std.json.Stringify.value(event, .{}, &json.writer); 449 450 // commit the event into a special branch 451 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 452 if (i == 0) { 453 _ = try std.fmt.hexToBytes(&first_oid, &oid); 454 } 455 } 456 } 457 458 // 459 // consume events into the database 460 // 461 462 { 463 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 464 465 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 466 467 // read the moment we just created 468 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 469 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 470 471 // get the last object id 472 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 473 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 474 _ = try last_object_id_cursor.readBytes(&last_object_id); 475 476 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 477 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 478 479 try std.testing.expectEqual(1, try haxy.count()); 480 481 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 482 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 483 484 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 485 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 486 487 // get the map of issues 488 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 489 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 490 491 // get the issue out of the map 492 var first_issue_id: [evt.event_id_size]u8 = undefined; 493 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 494 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 495 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 496 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 497 498 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 499 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 500 } 501 502 // 503 // define more test events 504 // 505 506 const events_to_consume2 = [_]evt.Event{ 507 .{ 508 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 509 .data = .{ 510 .issue = .{ 511 .title = "Kanban card status badge falls behind after drag", 512 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 513 .tags = "bug\x00kanban\x00frontend", 514 }, 515 }, 516 }, 517 .{ 518 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 519 .data = .{ 520 .issue = .{ 521 .title = "Assignee autocomplete omits recently invited users", 522 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 523 .tags = "bug\x00assignees\x00api", 524 }, 525 }, 526 }, 527 .{ 528 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 529 .data = .{ 530 .issue = .{ 531 .title = "Add due date warning for issues blocked by dependencies", 532 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 533 .tags = "enhancement\x00planning\x00dependencies", 534 }, 535 }, 536 }, 537 }; 538 539 // 540 // insert issues as commits in the repo 541 // 542 543 { 544 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 545 defer json.deinit(); 546 547 for (events_to_consume2, 0..) |event, i| { 548 json.clearRetainingCapacity(); 549 550 try std.json.Stringify.value(event, .{}, &json.writer); 551 552 // commit the event into a special branch 553 _ = try repo.commitAtRef( 554 io, 555 allocator, 556 // make the first commit a child of the first commit on the haxy/meta branch 557 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 558 null, 559 .{ .kind = .head, .name = "haxy/other" }, 560 ); 561 } 562 } 563 564 // 565 // check out the haxy/meta branch and merge the haxy/other branch into it 566 // 567 568 const merge_oid = blk: { 569 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 570 defer result.deinit(); 571 572 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 573 defer merge.deinit(); 574 575 try std.testing.expect(.success == merge.result); 576 577 break :blk merge.result.success.oid; 578 }; 579 580 // 581 // consume events into the database 582 // 583 584 { 585 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 586 587 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 588 589 // read the moment we just created 590 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 591 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 592 593 // get the last object id 594 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 595 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 596 _ = try last_object_id_cursor.readBytes(&last_object_id); 597 598 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 599 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 600 601 try std.testing.expectEqual(2, try haxy.count()); 602 603 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 604 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 605 606 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 607 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 608 609 // get the map of issues 610 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 611 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 612 613 // make sure one of the original issues is still there 614 { 615 // get the issue out of the map 616 var first_issue_id: [evt.event_id_size]u8 = undefined; 617 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 618 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 619 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 620 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 621 622 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 623 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 624 } 625 626 // make sure one of the new issues is there 627 { 628 // get the issue out of the map 629 var first_issue_id: [evt.event_id_size]u8 = undefined; 630 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 631 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 632 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 633 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 634 635 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.description, first_issue.issue.description); 636 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.tags, first_issue.issue.tags); 637 } 638 } 639 640 // 641 // insert issues as commits in the repo 642 // 643 644 { 645 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 646 defer json.deinit(); 647 648 for (events_to_consume, 0..) |event, i| { 649 json.clearRetainingCapacity(); 650 651 try std.json.Stringify.value(event, .{}, &json.writer); 652 653 // commit the event into a special branch 654 _ = try repo.commitAtRef( 655 io, 656 allocator, 657 // make the first event have no parents so we completely rebase the branch, 658 // undoing the merge that we did above 659 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 660 null, 661 .{ .kind = .head, .name = "haxy/meta" }, 662 ); 663 } 664 } 665 666 // 667 // consume events into the database 668 // 669 670 { 671 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 672 673 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 674 675 // read the moment we just created 676 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 677 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 678 679 // get the last object id 680 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 681 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 682 _ = try last_object_id_cursor.readBytes(&last_object_id); 683 684 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 685 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 686 687 try std.testing.expectEqual(1, try haxy.count()); 688 689 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 690 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 691 692 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 693 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 694 695 // get the map of issues 696 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 697 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 698 699 // make sure the new issue is not there 700 { 701 // get the issue out of the map 702 var first_issue_id: [evt.event_id_size]u8 = undefined; 703 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 704 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 705 try std.testing.expect(null == first_issue_cursor); 706 } 707 } 708 709 // 710 // define test events that modify the same issue in conflicting ways 711 // 712 713 const events_to_consume3 = [_]evt.Event{ 714 .{ 715 .id = events_to_consume[0].id, 716 .data = .{ 717 .issue = .{ 718 .title = "Login form clears password on validation error", 719 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 720 .tags = "bug\x00priority-low\x00ui", 721 }, 722 }, 723 }, 724 .{ 725 .id = events_to_consume[0].id, 726 .data = .{ 727 .issue = .{ 728 .title = "Login form clears password on validation error", 729 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 730 .tags = "bug\x00priority-medium\x00ui", 731 }, 732 }, 733 }, 734 }; 735 736 // 737 // insert issues as commits in the repo on different branches 738 // 739 740 { 741 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 742 defer json.deinit(); 743 744 for (events_to_consume3, 0..) |event, i| { 745 json.clearRetainingCapacity(); 746 747 try std.json.Stringify.value(event, .{}, &json.writer); 748 749 // commit the event into a special branch 750 _ = try repo.commitAtRef( 751 io, 752 allocator, 753 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 754 null, 755 .{ .kind = .head, .name = switch (i) { 756 0 => "haxy/meta", 757 1 => "haxy/other", 758 else => unreachable, 759 } }, 760 ); 761 } 762 } 763 764 // 765 // check out the haxy/meta branch and merge the haxy/other branch into it 766 // 767 768 { 769 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 770 defer result.deinit(); 771 772 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 773 defer merge.deinit(); 774 775 try std.testing.expect(.success == merge.result); 776 } 777 778 // 779 // consume events into the database 780 // 781 782 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); 783 } 784