╭ changed ──────╮ │src/test/ui.zig│ ╰───────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │152 const identity = "alice/ziglings"; │ │153 const oid0 = "1111111111111111111111111111111111111111"; │ │154 const next_oid = "2222222222222222222222222222222222222222";│ │155 + const base_oid = "3333333333333333333333333333333333333333";│ │156 │ │157 const data = Commits{ │ │158 .location = .{ .repo = identity }, │ ╰─────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │159 .ref_or_oid = .object, │ │160 .ref_or_oid_value = oid0, │ │161 + .base_oid = base_oid, │ │162 .commits = &.{ │ │163 .{ .oid = oid0, .date = "2024-01-01", .message = "first", .window = .{} },│ │164 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │167 }; │ │168 │ │169 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = true }; │ │168 - session.data.current_page = ui.RoutablePage.repoCommitsRoute(identity, .object, oid0, 0, "").?; │ │170 + session.data.current_page = ui.RoutablePage.repoCommitsRoute(identity, .object, oid0, 0, "", base_oid).?;│ │171 │ │172 var view = try Commits.View.init(allocator, &data, &session); │ │173 defer view.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │187 │ │188 const route = ui.crossPageLink(root_focus, next_id, session.data); │ │189 try std.testing.expect(route != null); │ │190 + try std.testing.expectEqualStrings(base_oid, route.?.repo_commits.base_oid.slice());│ │191 } │ │192 │ │193 // a ref name with a '/' is url-encoded in the route, so it survives the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │197 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); │ │198 defer arena.deinit(); │ │199 const RP = ui.RoutablePage; │ │200 + const base_oid = "3333333333333333333333333333333333333333"; │ │201 │ │202 // the route layer holds the value already url-encoded ("feature%2Ffoo"). │ │199 - const route = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "").?; │ │203 + const route = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "", base_oid).?; │ │204 const url = try route.toUrl(&arena); │ │201 - try std.testing.expectEqualStrings("/repo/alice/ziglings/commits/branch:feature%2Ffoo", url); │ │205 + try std.testing.expectEqualStrings("/repo/alice/ziglings/commits/branch:feature%2Ffoo/base:" ++ base_oid, url);│ │206 │ │207 // parsing it back yields the same route, and the ref splits out intact. │ │208 const parsed = RP.fromUrl(url); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │210 const parsed_route = parsed.?.repo_commits; │ │211 try std.testing.expectEqual(RP.RefOrOid.branch, parsed_route.ref_or_oid.?); │ │212 try std.testing.expectEqualStrings("feature%2Ffoo", parsed_route.value.slice()); │ │213 + try std.testing.expectEqualStrings(base_oid, parsed_route.base_oid.slice()); │ │214 + const unfiltered = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "", "").?; │ │215 + try std.testing.expect(!RP.eql(route, unfiltered)); │ │216 } │ │217 │ │218 +test "commit pages stop at the base without excluding other ancestors" { │ │219 + try testCommitBase(.git); │ │220 + try testCommitBase(.xit); │ │221 +} │ │222 + │ │223 +fn testCommitBase(comptime kind: xit.repo.RepoKind) !void { │ │224 + const io = std.testing.io; │ │225 + const allocator = std.testing.allocator; │ │226 + const opts: xit.repo.RepoOpts(kind) = .{ .is_test = true }; │ │227 + var temp = std.testing.tmpDir(.{}); │ │228 + defer temp.cleanup(); │ │229 + const path = try temp.dir.realPathFileAlloc(io, ".", allocator); │ │230 + defer allocator.free(path); │ │231 + var repo = try xit.repo.Repo(kind, opts).init(io, allocator, .{ .path = path }); │ │232 + defer repo.deinit(io, allocator); │ │233 + var arena = std.heap.ArenaAllocator.init(allocator); │ │234 + defer arena.deinit(); │ │235 + const location = ui.RoutablePage.RepoLocation{ .repo = "" }; │ │236 + const content = ui.RoutablePage.RepoCommitsRoute.Content{ .diff = .{} }; │ │237 + const branch = xit.ref.Ref{ .kind = .head, .name = "master" }; │ │238 + │ │239 + // one more commit than fits on the first page │ │240 + const base = try repo.commitAtRef(io, allocator, .{ .message = "base" }, null, branch); │ │241 + var tip = base; │ │242 + var previous = base; │ │243 + for (0..21) |_| { │ │244 + previous = tip; │ │245 + tip = try repo.commitAtRef(io, allocator, .{ .message = "next" }, null, branch); │ │246 + } │ │247 + const first = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, &tip, content, &base); │ │248 + try std.testing.expectEqual(20, first.commits.len); │ │249 + try std.testing.expectEqual(@as(?u64, if (kind == .xit) 21 else null), first.commit_count); │ │250 + const last = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, first.next_start orelse return error.MissingNext, content, &base);│ │251 + try std.testing.expectEqual(1, last.commits.len); │ │252 + try std.testing.expectEqual(null, last.next_start); │ │253 + │ │254 + // the base at a page boundary must not create an empty next page │ │255 + const full = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, &previous, content, &base); │ │256 + try std.testing.expectEqual(20, full.commits.len); │ │257 + try std.testing.expectEqual(null, full.next_start); │ │258 + _ = try repo.addTag(io, allocator, .{ .name = "tip", .message = "annotated" }); │ │259 + const sources = [_]struct { ref: ui.RoutablePage.RefOrOid, value: []const u8 }{ │ │260 + .{ .ref = .object, .value = &tip }, │ │261 + .{ .ref = .branch, .value = branch.name }, │ │262 + .{ .ref = .tag, .value = "tip" }, │ │263 + }; │ │264 + for (sources) |source| { │ │265 + const empty = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, source.ref, source.value, content, &tip); │ │266 + try std.testing.expectEqual(0, empty.commits.len); │ │267 + try std.testing.expectEqual(@as(?u64, if (kind == .xit) 0 else null), empty.commit_count); │ │268 + } │ │269 + │ │270 + // an off-chain stopping point must not hide its parent on this chain │ │271 + const other = try repo.commitAtRef(io, allocator, .{ .message = "other", .parent_oids = &.{base} }, null, .{ .kind = .head, .name = "other" }); │ │272 + const off_chain = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, last.commits[0].oid, content, &other); │ │273 + try std.testing.expectEqual(2, off_chain.commits.len); │ │274 + try std.testing.expectEqualStrings(&base, off_chain.commits[1].oid); │ │275 + try std.testing.expectEqual(null, off_chain.commit_count); │ │276 +} │ │277 + │ │278 test "sync creates missing event branches and preserves head" { │ │279 const io = std.testing.io; │ │280 const allocator = std.testing.allocator; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯