╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │798 .description = "has no revision", │ │799 .tags = "enhancement", │ │800 .target_branch = "master", │ │750 - .status = .{ .merged = .{ .revision = .source, .before_oid = &base_oid, .after_oid = &base_oid } }, │ │801 + .status = .{ .merged = .{ .revision = .source, .patchrev_id = std.fmt.bytesToHex(invalid_merged_id, .lower) } },│ │802 } }, │ │803 }})); │ │804 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1119 const merged_moment = try evt.currentMoment(repo_opts, &target); │ │1120 const merged = (try evt.Patch.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patch_id)) orelse return error.NotFound; │ │1121 try std.testing.expectEqual(.merged, merged.event.status.kind()); │ │1071 - const merged_range = switch (merged.event.status) { │ │1072 - .merged => |range| range, │ │1122 + const merged_status = switch (merged.event.status) { │ │1123 + .merged => |value| value, │ │1124 else => return error.InvalidPatch, │ │1125 }; │ │1075 - try std.testing.expectEqual(merge_revision, merged_range.revision); │ │1076 - try std.testing.expectEqualStrings(&base_oid, merged_range.before_oid); │ │1077 - try std.testing.expectEqualStrings(&selected_oid, merged_range.after_oid); │ │1126 + try std.testing.expectEqual(merge_revision, merged_status.revision); │ │1127 + const result_id = try evt.parseEventId(&merged_status.patchrev_id); │ │1128 + const result = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &result_id)) orelse return error.NotFound;│ │1129 + try std.testing.expectEqualStrings(&base_oid, result.event.base_oid); │ │1130 + try std.testing.expectEqualStrings(&selected_oid, result.event.source_oid); │ │1131 try std.testing.expect(null != try evt.PatchRev.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &first_patchrev_id)); │ │1132 │ │1133 _ = arena.reset(.retain_capacity); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │761 const moment = try evt.currentMoment(repo_opts, &target); │ │762 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; │ │763 try std.testing.expectEqual(.merged, patch.event.status.kind()); │ │764 - const merged_range = switch (patch.event.status) { │ │765 - .merged => |range| range, │ │764 + const merged_status = switch (patch.event.status) { │ │765 + .merged => |value| value, │ │766 else => return error.InvalidPatch, │ │767 }; │ │768 - try std.testing.expectEqual(.source, merged_range.revision); │ │769 - try std.testing.expectEqualStrings(&base_oid, merged_range.before_oid); │ │770 - try std.testing.expectEqualStrings(&merge_oid, merged_range.after_oid); │ │768 + try std.testing.expectEqual(.source, merged_status.revision); │ │769 + const result_id = try evt.parseEventId(&merged_status.patchrev_id); │ │770 + const result = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &result_id)) orelse return error.NotFound; │ │771 + try std.testing.expectEqualStrings(&base_oid, result.event.base_oid); │ │772 + try std.testing.expectEqualStrings(&merge_oid, result.event.source_oid); │ │773 const imported = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &second_revision_id)) orelse return error.NotFound;│ │774 try std.testing.expectEqualStrings(&second_patch_oid, imported.patch_oid); │ │775 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/test/ui.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │76 } │ │77 │ │78 test "diff windows span the net changes across commits" { │ │79 + try testDiff(.git); │ │80 + try testDiff(.xit); │ │81 +} │ │82 + │ │83 +fn testDiff(comptime kind: xit.repo.RepoKind) !void { │ │84 const io = std.testing.io; │ │85 const allocator = std.testing.allocator; │ │81 - const Diff = Commits.Diff; │ │82 - const opts: xit.repo.RepoOpts(.xit) = .{ .is_test = true }; │ │86 + const Diff = ui.Repo.Diff; │ │87 + const opts: xit.repo.RepoOpts(kind) = .{ .is_test = true }; │ │88 var temp = std.testing.tmpDir(.{}); │ │89 defer temp.cleanup(); │ │90 const path = try temp.dir.realPathFileAlloc(io, ".", allocator);│ ╰────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │91 defer allocator.free(path); │ │87 - var repo = try xit.repo.Repo(.xit, opts).init(io, allocator, .{ .path = path });│ │92 + var repo = try xit.repo.Repo(kind, opts).init(io, allocator, .{ .path = path });│ │93 defer repo.deinit(io, allocator); │ │94 var arena = std.heap.ArenaAllocator.init(allocator); │ │95 defer arena.deinit(); │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 try repo.add(io, allocator, &.{name}); │ │117 head = try repo.commit(io, allocator, .{ .message = "add file" }); │ │118 } │ │114 - const first = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, 0, ""); │ │119 + const route = ui.RoutablePage.repoDiffRoute("alice/project", .object, &head, 0, "", &base) orelse return error.BadRoute;│ │120 + const diff = try Diff.init(kind, opts, &arena, &repo, io, allocator, route.repo_diff); │ │121 + const first = diff.window; │ │122 try std.testing.expectEqual(Diff.page_size, first.hunks.len); │ │123 try std.testing.expect(first.start == 0 and first.has_more); │ │124 try std.testing.expectEqualStrings("file-00.txt", first.hunks[0].path orelse return error.MissingPath); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │118 - const last = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, Diff.page_size, "");│ │125 + const last = try Diff.render(kind, opts, io, allocator, arena.allocator(), &repo, &base, head, Diff.page_size, "");│ │126 try std.testing.expectEqual(1, last.hunks.len); │ │127 try std.testing.expect(last.start > 0 and !last.has_more); │ │128 try std.testing.expectEqualStrings("file-10.txt", last.hunks[0].path orelse return error.MissingPath); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │122 - const filtered = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, 0, "file-00.txt");│ │129 + const filtered = try Diff.render(kind, opts, io, allocator, arena.allocator(), &repo, &base, head, 0, "file-00.txt");│ │130 try std.testing.expectEqual(1, filtered.hunks.len); │ │131 try std.testing.expect(!filtered.has_more); │ │132 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │126 - // the standalone pane links to the next fork diff window in both clients │ │133 + // tags and branches compare the same trees as an explicit tip │ │134 + _ = try repo.addTag(io, allocator, .{ .name = "tip", .message = "annotated" }); │ │135 + const sources = [_]struct { ref: ?ui.RoutablePage.RefOrOid, value: []const u8 }{ │ │136 + .{ .ref = null, .value = "" }, │ │137 + .{ .ref = .branch, .value = "master" }, │ │138 + .{ .ref = .tag, .value = "tip" }, │ │139 + }; │ │140 + for (sources) |source| { │ │141 + const selected = ui.RoutablePage.repoDiffRoute("alice/project", source.ref, source.value, 0, "", &base) orelse return error.BadRoute; │ │142 + const data = try Diff.init(kind, opts, &arena, &repo, io, allocator, selected.repo_diff); │ │143 + try std.testing.expectEqualDeep(first, data.window); │ │144 + } │ │145 + const unchanged = ui.RoutablePage.repoDiffRoute("alice/project", .object, &head, 0, "", &head) orelse return error.BadRoute; │ │146 + try std.testing.expectEqual(0, (try Diff.init(kind, opts, &arena, &repo, io, allocator, unchanged.repo_diff)).window.hunks.len); │ │147 + │ │148 + // a zero base includes files that were already present in the first commit │ │149 + const zero = [_]u8{'0'} ** xit.hash.hexLen(opts.hash); │ │150 + const created = ui.RoutablePage.repoDiffRoute("alice/project", .object, &head, 0, "shared.txt", &zero) orelse return error.BadRoute; │ │151 + const created_diff = try Diff.init(kind, opts, &arena, &repo, io, allocator, created.repo_diff); │ │152 + try std.testing.expectEqual(1, created_diff.window.hunks.len); │ │153 + try std.testing.expectEqualStrings("shared.txt", created_diff.window.hunks[0].path orelse return error.MissingPath); │ │154 + │ │155 + // standalone panes keep their comparison when filtering or paginating │ │156 const id = "11111111111111111111111111111111"; │ │128 - const data = Diff{ .route = .{ .fork = .{ .identity = "alice/project", .id = id } }, .window = first }; │ │129 - for ([_]bool{ true, false }) |terminal| { │ │130 - var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = terminal }; │ │131 - session.data.current_page = ui.RoutablePage.forkDiffRoute("alice/project", id, 0, "") orelse return error.BadRoute; │ │132 - var view = try Diff.View.init(allocator, &data, &session); │ │133 - defer view.deinit(allocator); │ │134 - const focus = view.getFocus(); │ │135 - try view.build(allocator, .{ .min_size = .{ .width = null, .height = null }, .max_size = .{ .width = 100, .height = 30 } }, focus); │ │136 - const box = &view.scroll.child.box; │ │137 - try std.testing.expectEqual(first.hunks.len * 2 + 1, box.children.count()); │ │138 - const file = ui.crossPageLink(focus, box.children.keys()[0], session.data) orelse return error.MissingFileLink; │ │139 - try std.testing.expectEqualStrings("file-00.txt", file.fork_diff.path.slice()); │ │140 - const next = ui.crossPageLink(focus, box.children.keys()[box.children.count() - 1], session.data) orelse return error.MissingLink; │ │141 - try std.testing.expectEqual(Diff.page_size, next.fork_diff.start); │ │157 + const fork_diff = Diff{ .route = .{ .fork = .{ .identity = "alice/project", .id = id } }, .window = first }; │ │158 + for ([_]Diff{ diff, fork_diff }) |data| { │ │159 + for ([_]bool{ true, false }) |terminal| { │ │160 + var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = terminal }; │ │161 + session.data.current_page = if (data.route == .repo) route else ui.RoutablePage.forkDiffRoute("alice/project", id, 0, "") orelse return error.BadRoute;│ │162 + var view = try Diff.View.init(allocator, &data, &session); │ │163 + defer view.deinit(allocator); │ │164 + const focus = view.getFocus(); │ │165 + try view.build(allocator, .{ .min_size = .{ .width = null, .height = null }, .max_size = .{ .width = 100, .height = 30 } }, focus); │ │166 + const box = &view.scroll.child.box; │ │167 + try std.testing.expectEqual(first.hunks.len * 2 + 1, box.children.count()); │ │168 + const file = ui.crossPageLink(focus, box.children.keys()[0], session.data) orelse return error.MissingFileLink; │ │169 + const next = ui.crossPageLink(focus, box.children.keys()[box.children.count() - 1], session.data) orelse return error.MissingLink; │ │170 + if (data.route == .repo) { │ │171 + try std.testing.expectEqualStrings("file-00.txt", file.repo_diff.path.slice()); │ │172 + try std.testing.expectEqualStrings(&base, file.repo_diff.base_oid.slice()); │ │173 + try std.testing.expectEqualStrings(&base, next.repo_diff.base_oid.slice()); │ │174 + try std.testing.expectEqualStrings(&head, next.repo_diff.value.slice()); │ │175 + try std.testing.expectEqual(Diff.page_size, next.repo_diff.start); │ │176 + } else { │ │177 + try std.testing.expectEqualStrings("file-00.txt", file.fork_diff.path.slice()); │ │178 + try std.testing.expectEqual(Diff.page_size, next.fork_diff.start); │ │179 + } │ │180 + } │ │181 } │ │182 } │ │183 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │87 }; │ │88 }, │ │89 .repo => switch (route) { │ │90 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) }, │ │90 + .repo_files, .repo_commits, .repo_diff, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ │91 else => return error.UnexpectedRoute, │ │92 }, │ │93 .fork => switch (route) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯