╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const ui = @import("../ui.zig"); 4 const Commits = @import("../ui/Repo/Commits.zig"); 5 const Events = @import("../ui/Repo/Events.zig"); 6 const xit = @import("xit"); 7 8 test "diff windows span the net changes across commits" { 9 const io = std.testing.io; 10 const allocator = std.testing.allocator; 11 const Diff = Commits.Diff; 12 const opts: xit.repo.RepoOpts(.xit) = .{ .is_test = true }; 13 var temp = std.testing.tmpDir(.{}); 14 defer temp.cleanup(); 15 const path = try temp.dir.realPathFileAlloc(io, ".", allocator); 16 defer allocator.free(path); 17 var repo = try xit.repo.Repo(.xit, opts).init(io, allocator, .{ .path = path }); 18 defer repo.deinit(io, allocator); 19 var arena = std.heap.ArenaAllocator.init(allocator); 20 defer arena.deinit(); 21 22 // the shared base is absent from the diff 23 { 24 const file = try repo.core.work_dir.createFile(io, "shared.txt", .{}); 25 defer file.close(io); 26 try file.writeStreamingAll(io, "shared\n"); 27 } 28 try repo.add(io, allocator, &.{"shared.txt"}); 29 const base = try repo.commit(io, allocator, .{ .message = "base" }); 30 31 // changes from earlier commits must survive into the combined diff 32 var head = base; 33 for (0..Diff.page_size + 1) |i| { 34 var name_buf: [32]u8 = undefined; 35 const name = try std.fmt.bufPrint(&name_buf, "file-{d:0>2}.txt", .{i}); 36 { 37 const file = try repo.core.work_dir.createFile(io, name, .{}); 38 defer file.close(io); 39 try file.writeStreamingAll(io, "added\n"); 40 } 41 try repo.add(io, allocator, &.{name}); 42 head = try repo.commit(io, allocator, .{ .message = "add file" }); 43 } 44 const first = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, 0, ""); 45 try std.testing.expectEqual(Diff.page_size, first.hunks.len); 46 try std.testing.expect(first.start == 0 and first.has_more); 47 try std.testing.expectEqualStrings("file-00.txt", first.hunks[0].path orelse return error.MissingPath); 48 const last = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, Diff.page_size, ""); 49 try std.testing.expectEqual(1, last.hunks.len); 50 try std.testing.expect(last.start > 0 and !last.has_more); 51 try std.testing.expectEqualStrings("file-10.txt", last.hunks[0].path orelse return error.MissingPath); 52 const filtered = try Diff.render(.xit, opts, io, allocator, arena.allocator(), &repo, &base, head, 0, "file-00.txt"); 53 try std.testing.expectEqual(1, filtered.hunks.len); 54 try std.testing.expect(!filtered.has_more); 55 56 // the standalone pane links to the next fork diff window in both clients 57 const id = "11111111111111111111111111111111"; 58 const data = Diff{ .route = .{ .fork = .{ .identity = "alice/project", .id = id } }, .window = first }; 59 for ([_]bool{ true, false }) |terminal| { 60 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = terminal }; 61 session.data.current_page = ui.RoutablePage.forkDiffRoute("alice/project", id, 0, "") orelse return error.BadRoute; 62 var view = try Diff.View.init(allocator, &data, &session); 63 defer view.deinit(allocator); 64 const focus = view.getFocus(); 65 try view.build(allocator, .{ .min_size = .{ .width = null, .height = null }, .max_size = .{ .width = 100, .height = 30 } }, focus); 66 const box = &view.scroll.child.box; 67 try std.testing.expectEqual(first.hunks.len * 2 + 1, box.children.count()); 68 const file = ui.crossPageLink(focus, box.children.keys()[0], session.data) orelse return error.MissingFileLink; 69 try std.testing.expectEqualStrings("file-00.txt", file.fork_diff.path.slice()); 70 const next = ui.crossPageLink(focus, box.children.keys()[box.children.count() - 1], session.data) orelse return error.MissingLink; 71 try std.testing.expectEqual(Diff.page_size, next.fork_diff.start); 72 } 73 } 74 75 // the "next" row at the bottom of the commits list must be recognized as a 76 // cross-page link (so a click navigates), exactly like the diff pane's "next". 77 test "commits list next row is a cross-page link" { 78 const allocator = std.testing.allocator; 79 80 var arena = std.heap.ArenaAllocator.init(allocator); 81 defer arena.deinit(); 82 const identity = "alice/ziglings"; 83 const oid0 = "1111111111111111111111111111111111111111"; 84 const next_oid = "2222222222222222222222222222222222222222"; 85 86 const data = Commits{ 87 .location = .{ .repo = identity }, 88 .ref_or_oid = .object, 89 .ref_or_oid_value = oid0, 90 .commits = &.{ 91 .{ .oid = oid0, .date = "2024-01-01", .message = "first", .window = .{} }, 92 }, 93 .next_start = next_oid, 94 .header = try Commits.Header.init(arena.allocator(), .object, oid0), 95 }; 96 97 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = true }; 98 session.data.current_page = ui.RoutablePage.repoCommitsRoute(identity, .object, oid0, 0, "").?; 99 100 var view = try Commits.View.init(allocator, &data, &session); 101 defer view.deinit(allocator); 102 103 const root_focus = view.getFocus(); 104 try view.build(allocator, .{ 105 .min_size = .{ .width = null, .height = null }, 106 .max_size = .{ .width = 120, .height = 60 }, 107 }, root_focus); 108 109 // the "next" row is the last child of the list box: the view's outer box 110 // holds the sub-header then the list/diff split, whose first child is the 111 // list scroll. 112 const content = &view.box.children.values()[1].widget.box; 113 const lb = &content.children.values()[0].widget.scroll.child.box; 114 const next_id = lb.children.keys()[lb.children.count() - 1]; 115 116 const route = ui.crossPageLink(root_focus, next_id, session.data); 117 try std.testing.expect(route != null); 118 } 119 120 // a ref name with a '/' is url-encoded in the route, so it survives the 121 // '/'-delimited url round-trip as a single segment (kind + value) rather than 122 // being mis-parsed as extra path. 123 test "encoded ref name survives the commits url round-trip" { 124 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 125 defer arena.deinit(); 126 const RP = ui.RoutablePage; 127 128 // the route layer holds the value already url-encoded ("feature%2Ffoo"). 129 const route = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "").?; 130 const url = try route.toUrl(&arena); 131 try std.testing.expectEqualStrings("/repo/alice/ziglings/commits/branch:feature%2Ffoo", url); 132 133 // parsing it back yields the same route, and the ref splits out intact. 134 const parsed = RP.fromUrl(url); 135 try std.testing.expect(parsed != null); 136 const parsed_route = parsed.?.repo_commits; 137 try std.testing.expectEqual(RP.RefOrOid.branch, parsed_route.ref_or_oid.?); 138 try std.testing.expectEqualStrings("feature%2Ffoo", parsed_route.value.slice()); 139 } 140 141 test "sync creates missing event branches and preserves head" { 142 const io = std.testing.io; 143 const allocator = std.testing.allocator; 144 145 var temp = std.testing.tmpDir(.{}); 146 defer temp.cleanup(); 147 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 148 defer allocator.free(temp_path); 149 150 const local_path = try std.fs.path.join(allocator, &.{ temp_path, "local" }); 151 defer allocator.free(local_path); 152 const remote_path = try std.fs.path.join(allocator, &.{ temp_path, "remote" }); 153 defer allocator.free(remote_path); 154 155 const Repo = xit.repo.Repo(.git, .{}); 156 const RemoteRepo = xit.repo.Repo(.git, .{}); 157 { 158 var remote = try RemoteRepo.init(io, allocator, .{ .path = remote_path }); 159 remote.deinit(io, allocator); 160 } 161 { 162 var local = try Repo.init(io, allocator, .{ .path = local_path }); 163 defer local.deinit(io, allocator); 164 try local.addConfig(io, allocator, .{ .name = "user.name", .value = "user" }); 165 try local.addConfig(io, allocator, .{ .name = "user.email", .value = "user@haxy" }); 166 { 167 const file = try local.core.work_dir.createFile(io, "keep.txt", .{}); 168 defer file.close(io); 169 try file.writeStreamingAll(io, "keep"); 170 } 171 try local.add(io, allocator, &.{"keep.txt"}); 172 _ = try local.commit(io, allocator, .{ .message = "master" }); 173 try local.addRemote(io, allocator, .{ .name = "origin", .value = remote_path }); 174 175 const id = [_]u8{1} ** evt.event_id_size; 176 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 177 .id = std.fmt.bytesToHex(id, .lower), 178 .author = .{ .name = "haxy", .email = "user@haxy" }, 179 .event = .{ .issue = .{ .title = "sync", .description = "", .tags = "" } }, 180 }}); 181 } 182 183 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 184 { 185 var local = try Repo.open(io, allocator, .{ .path = local_path }); 186 defer local.deinit(io, allocator); 187 const id = [_]u8{2} ** evt.event_id_size; 188 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 189 .id = std.fmt.bytesToHex(id, .lower), 190 .author = .{ .name = "haxy", .email = "user@haxy" }, 191 .event = .{ .issue = .{ .title = "local", .description = "", .tags = "" } }, 192 }}); 193 } 194 { 195 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 196 defer remote.deinit(io, allocator); 197 const id = [_]u8{3} ** evt.event_id_size; 198 try evt.consume(.local, .repo, .git, .{}, io, allocator, &remote, evt.events_ref, &.{.{ 199 .id = std.fmt.bytesToHex(id, .lower), 200 .author = .{ .name = "haxy", .email = "user@haxy" }, 201 .event = .{ .issue = .{ .title = "remote", .description = "", .tags = "" } }, 202 }}); 203 } 204 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 205 { 206 var local = try Repo.open(io, allocator, .{ .path = local_path }); 207 defer local.deinit(io, allocator); 208 try local.removeBranch(io, .{ .name = evt.events_ref.name }); 209 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{}); 210 } 211 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 212 213 var local = try Repo.open(io, allocator, .{ .path = local_path }); 214 defer local.deinit(io, allocator); 215 var head_buffer: [xit.ref.MAX_REF_CONTENT_SIZE]u8 = undefined; 216 const head = try local.head(io, &head_buffer); 217 switch (head) { 218 .ref => |ref| try std.testing.expectEqualStrings("master", ref.name), 219 .oid => return error.TestExpectedEqual, 220 } 221 const kept = try local.core.work_dir.openFile(io, "keep.txt", .{}); 222 kept.close(io); 223 const local_tip = (try local.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 224 225 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 226 defer remote.deinit(io, allocator); 227 const remote_tip = (try remote.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 228 try std.testing.expectEqualStrings(&local_tip, &remote_tip); 229 230 var start_oids = [_][local_tip.len]u8{local_tip}; 231 var commit_iter = try local.log(io, allocator, .{ .start_oids = &start_oids }); 232 defer commit_iter.deinit(); 233 var merge_commit = (try commit_iter.next(allocator)) orelse return error.TestExpectedEqual; 234 defer merge_commit.deinit(); 235 const parent_oids = merge_commit.content.commit.metadata.parent_oids orelse return error.TestExpectedEqual; 236 try std.testing.expectEqual(2, parent_oids.len); 237 238 var arena = std.heap.ArenaAllocator.init(allocator); 239 defer arena.deinit(); 240 const status = try Events.Header.local(.git, .{}, arena.allocator(), &local, io, null); 241 try std.testing.expectEqualStrings("nothing to sync", status.sync_status orelse return error.TestExpectedEqual); 242 } 243