╭ 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 // the "next" row at the bottom of the commits list must be recognized as a 9 // cross-page link (so a click navigates), exactly like the diff pane's "next". 10 test "commits list next row is a cross-page link" { 11 const allocator = std.testing.allocator; 12 13 var arena = std.heap.ArenaAllocator.init(allocator); 14 defer arena.deinit(); 15 const identity = "alice/ziglings"; 16 const oid0 = "1111111111111111111111111111111111111111"; 17 const next_oid = "2222222222222222222222222222222222222222"; 18 19 const data = Commits{ 20 .identity = identity, 21 .ref_or_oid = .object, 22 .ref_or_oid_value = oid0, 23 .commits = &.{ 24 .{ .oid = oid0, .date = "2024-01-01", .message = "first", .hunks = &.{}, .window_start = 0, .has_prev = false, .has_more = false }, 25 }, 26 .next_start = next_oid, 27 .header = try Commits.Header.init(arena.allocator(), .object, oid0), 28 }; 29 30 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = true }; 31 session.data.current_page = ui.RoutablePage.repoCommitsRoute(identity, .object, oid0, 0, "").?; 32 33 var view = try Commits.View.init(allocator, &data, &session); 34 defer view.deinit(allocator); 35 36 const root_focus = view.getFocus(); 37 try view.build(allocator, .{ 38 .min_size = .{ .width = null, .height = null }, 39 .max_size = .{ .width = 120, .height = 60 }, 40 }, root_focus); 41 42 // the "next" row is the last child of the list box: the view's outer box 43 // holds the sub-header then the list/diff split, whose first child is the 44 // list scroll. 45 const content = &view.box.children.values()[1].widget.box; 46 const lb = &content.children.values()[0].widget.scroll.child.box; 47 const next_id = lb.children.keys()[lb.children.count() - 1]; 48 49 const route = ui.crossPageLink(root_focus, next_id, session.data); 50 try std.testing.expect(route != null); 51 } 52 53 // a ref name with a '/' is url-encoded in the route, so it survives the 54 // '/'-delimited url round-trip as a single segment (kind + value) rather than 55 // being mis-parsed as extra path. 56 test "encoded ref name survives the commits url round-trip" { 57 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 58 defer arena.deinit(); 59 const RP = ui.RoutablePage; 60 61 // the route layer holds the value already url-encoded ("feature%2Ffoo"). 62 const route = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "").?; 63 const url = try route.toUrl(&arena); 64 try std.testing.expectEqualStrings("/repo/alice/ziglings/commits/branch:feature%2Ffoo", url); 65 66 // parsing it back yields the same route, and the ref splits out intact. 67 const parsed = RP.fromUrl(url); 68 try std.testing.expect(parsed != null); 69 const parsed_route = parsed.?.repo_commits; 70 try std.testing.expectEqual(RP.RefOrOid.branch, parsed_route.ref_or_oid.?); 71 try std.testing.expectEqualStrings("feature%2Ffoo", parsed_route.value.slice()); 72 } 73 74 test "sync creates missing event branches and preserves head" { 75 const io = std.testing.io; 76 const allocator = std.testing.allocator; 77 const cwd = std.Io.Dir.cwd(); 78 const temp_dir_name = "temp-ui-sync"; 79 80 cwd.deleteTree(io, temp_dir_name) catch {}; 81 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 82 defer cwd.deleteTree(io, temp_dir_name) catch {}; 83 defer temp_dir.close(io); 84 85 const cwd_path = try std.process.currentPathAlloc(io, allocator); 86 defer allocator.free(cwd_path); 87 const local_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "local" }); 88 defer allocator.free(local_path); 89 const remote_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "remote" }); 90 defer allocator.free(remote_path); 91 92 const Repo = xit.repo.Repo(.git, .{}); 93 const RemoteRepo = xit.repo.Repo(.git, .{}); 94 { 95 var remote = try RemoteRepo.init(io, allocator, .{ .path = remote_path }); 96 remote.deinit(io, allocator); 97 } 98 { 99 var local = try Repo.init(io, allocator, .{ .path = local_path }); 100 defer local.deinit(io, allocator); 101 try local.addConfig(io, allocator, .{ .name = "user.name", .value = "user" }); 102 try local.addConfig(io, allocator, .{ .name = "user.email", .value = "user@haxy" }); 103 { 104 const file = try local.core.work_dir.createFile(io, "keep.txt", .{}); 105 defer file.close(io); 106 try file.writeStreamingAll(io, "keep"); 107 } 108 try local.add(io, allocator, &.{"keep.txt"}); 109 _ = try local.commit(io, allocator, .{ .message = "master" }); 110 try local.addRemote(io, allocator, .{ .name = "origin", .value = remote_path }); 111 112 const id = [_]u8{1} ** evt.event_id_size; 113 try evt.consume(.repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 114 .id = std.fmt.bytesToHex(id, .lower), 115 .author = .{ .name = "haxy", .email = "user@haxy" }, 116 .event = .{ .issue = .{ .title = "sync", .description = "", .tags = "" } }, 117 }}); 118 } 119 120 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 121 { 122 var local = try Repo.open(io, allocator, .{ .path = local_path }); 123 defer local.deinit(io, allocator); 124 const id = [_]u8{2} ** evt.event_id_size; 125 try evt.consume(.repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 126 .id = std.fmt.bytesToHex(id, .lower), 127 .author = .{ .name = "haxy", .email = "user@haxy" }, 128 .event = .{ .issue = .{ .title = "local", .description = "", .tags = "" } }, 129 }}); 130 } 131 { 132 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 133 defer remote.deinit(io, allocator); 134 const id = [_]u8{3} ** evt.event_id_size; 135 try evt.consume(.repo, .git, .{}, io, allocator, &remote, evt.events_ref, &.{.{ 136 .id = std.fmt.bytesToHex(id, .lower), 137 .author = .{ .name = "haxy", .email = "user@haxy" }, 138 .event = .{ .issue = .{ .title = "remote", .description = "", .tags = "" } }, 139 }}); 140 } 141 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 142 { 143 var local = try Repo.open(io, allocator, .{ .path = local_path }); 144 defer local.deinit(io, allocator); 145 try local.removeBranch(io, .{ .name = evt.events_ref.name }); 146 try evt.consume(.repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{}); 147 } 148 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 149 150 var local = try Repo.open(io, allocator, .{ .path = local_path }); 151 defer local.deinit(io, allocator); 152 var head_buffer: [xit.ref.MAX_REF_CONTENT_SIZE]u8 = undefined; 153 const head = try local.head(io, &head_buffer); 154 switch (head) { 155 .ref => |ref| try std.testing.expectEqualStrings("master", ref.name), 156 .oid => return error.TestExpectedEqual, 157 } 158 const kept = try local.core.work_dir.openFile(io, "keep.txt", .{}); 159 kept.close(io); 160 const local_tip = (try local.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 161 162 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 163 defer remote.deinit(io, allocator); 164 const remote_tip = (try remote.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 165 try std.testing.expectEqualStrings(&local_tip, &remote_tip); 166 167 var start_oids = [_][local_tip.len]u8{local_tip}; 168 var commit_iter = try local.log(io, allocator, &start_oids); 169 defer commit_iter.deinit(); 170 var merge_commit = (try commit_iter.next(allocator)) orelse return error.TestExpectedEqual; 171 defer merge_commit.deinit(); 172 const parent_oids = merge_commit.content.commit.metadata.parent_oids orelse return error.TestExpectedEqual; 173 try std.testing.expectEqual(2, parent_oids.len); 174 175 var arena = std.heap.ArenaAllocator.init(allocator); 176 defer arena.deinit(); 177 const status = try Events.Header.local(.git, .{}, arena.allocator(), &local, io, null); 178 try std.testing.expectEqualStrings("nothing to sync", status.sync_status orelse return error.TestExpectedEqual); 179 } 180