╭ 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 "preloaded thread forms receive typing after switching tabs" { 9 const allocator = std.testing.allocator; 10 var arena = std.heap.ArenaAllocator.init(allocator); 11 defer arena.deinit(); 12 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = true, .data = .{ .host_kind = .local } }; 13 session.data.current_page = ui.RoutablePage.repoIssuesRoute("", .open, "", "") orelse return error.BadRoute; 14 const data = try ui.Repo.Issues.emptyResult(arena.allocator(), "", "", "", "", 0, "", .open); 15 var root = ui.Widget{ .repo_issues = try ui.Repo.Issues.View.init(allocator, &data, &session) }; 16 defer root.deinit(allocator); 17 const focus = root.getFocus(); 18 const constraint = xit.xitui.layout.Constraint{ 19 .min_size = .{ .width = null, .height = null }, 20 .max_size = .{ .width = 100, .height = 40 }, 21 }; 22 try root.build(allocator, constraint, focus); 23 24 // switch to the preloaded new tab without recreating the page 25 for ([_]xit.xitui.input.Key{ .arrow_right, .arrow_right, .arrow_right, .arrow_down }) |key| { 26 try ui.inputKey(allocator, &root, key, &session); 27 try root.build(allocator, constraint, focus); 28 } 29 for ([_][]const u8{ "title", "tags", "description" }) |name| { 30 const input = session.text_inputs.get(focus.grandchild_id orelse return error.NoFocus) orelse return error.MissingInput; 31 try std.testing.expectEqualStrings(name, input.options.name); 32 try ui.inputKey(allocator, &root, .{ .codepoint = 'x' }, &session); 33 try root.build(allocator, constraint, focus); 34 const value = try input.text(allocator); 35 defer allocator.free(value); 36 try std.testing.expectEqualStrings("x", value); 37 try ui.inputKey(allocator, &root, .tab, &session); 38 try root.build(allocator, constraint, focus); 39 } 40 } 41 42 test "web controls omit hidden inputs but retain off-screen inputs" { 43 const wgt = xit.xitui.widget; 44 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 45 defer arena.deinit(); 46 const allocator = arena.allocator(); 47 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = false }; 48 var root = ui.Widget{ .box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }) }; 49 defer root.deinit(allocator); 50 var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }) }, .{ .web_native = true }); 51 for ([_][]const u8{ "visible", "hidden", "offscreen" }, 0..) |name, i| { 52 var input = try wgt.TextInput.init(allocator, .{ .name = name, .render_content = false }); 53 input.getFocus().mode = .all; 54 try scroll.child.box.children.put(allocator, input.getFocus().id, .{ 55 .widget = .{ .text_input = input }, 56 .rect = null, 57 .min_size = .{ .width = 20, .height = 3 }, 58 .hidden = i == 1, 59 }); 60 } 61 for (scroll.child.box.children.values()) |*child| { 62 const input = &child.widget.text_input; 63 try session.text_inputs.put(allocator, input.getFocus().id, input); 64 } 65 try root.box.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null }); 66 const focus = root.getFocus(); 67 try root.build(allocator, .{ .min_size = .{ .width = null, .height = null }, .max_size = .{ .width = 30, .height = 3 } }, focus); 68 69 const children = &root.box.children.values()[0].widget.scroll.child.box.children; 70 const offscreen = children.values()[2].rect orelse return error.MissingRect; 71 try std.testing.expect(offscreen.y >= 3); 72 const html = try @import("../web.zig").generateHtml(allocator, &root, &session); 73 try std.testing.expect(std.mem.indexOf(u8, html, "name=\"visible\"") != null); 74 try std.testing.expect(std.mem.indexOf(u8, html, "name=\"hidden\"") == null); 75 try std.testing.expect(std.mem.indexOf(u8, html, "name=\"offscreen\"") != null); 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; 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); 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(); 96 97 // the shared base is absent from the diff 98 { 99 const file = try repo.core.work_dir.createFile(io, "shared.txt", .{}); 100 defer file.close(io); 101 try file.writeStreamingAll(io, "shared\n"); 102 } 103 try repo.add(io, allocator, &.{"shared.txt"}); 104 const base = try repo.commit(io, allocator, .{ .message = "base" }); 105 106 // changes from earlier commits must survive into the combined diff 107 var head = base; 108 for (0..Diff.page_size + 1) |i| { 109 var name_buf: [32]u8 = undefined; 110 const name = try std.fmt.bufPrint(&name_buf, "file-{d:0>2}.txt", .{i}); 111 { 112 const file = try repo.core.work_dir.createFile(io, name, .{}); 113 defer file.close(io); 114 try file.writeStreamingAll(io, "added\n"); 115 } 116 try repo.add(io, allocator, &.{name}); 117 head = try repo.commit(io, allocator, .{ .message = "add file" }); 118 } 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); 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); 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 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"; 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 184 // the "next" row at the bottom of the commits list must be recognized as a 185 // cross-page link (so a click navigates), exactly like the diff pane's "next". 186 test "commits list next row is a cross-page link" { 187 const allocator = std.testing.allocator; 188 189 var arena = std.heap.ArenaAllocator.init(allocator); 190 defer arena.deinit(); 191 const identity = "alice/ziglings"; 192 const oid0 = "1111111111111111111111111111111111111111"; 193 const next_oid = "2222222222222222222222222222222222222222"; 194 const base_oid = "3333333333333333333333333333333333333333"; 195 196 const data = Commits{ 197 .location = .{ .repo = identity }, 198 .ref_or_oid = .object, 199 .ref_or_oid_value = oid0, 200 .base_oid = base_oid, 201 .commits = &.{ 202 .{ .oid = oid0, .date = "2024-01-01", .message = "first", .window = .{} }, 203 }, 204 .next_start = next_oid, 205 }; 206 207 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .is_terminal = true }; 208 session.data.current_page = ui.RoutablePage.repoCommitsRoute(identity, .object, oid0, 0, "", base_oid).?; 209 210 var view = try Commits.View.init(allocator, &data, &session); 211 defer view.deinit(allocator); 212 213 const root_focus = view.getFocus(); 214 try view.build(allocator, .{ 215 .min_size = .{ .width = null, .height = null }, 216 .max_size = .{ .width = 120, .height = 60 }, 217 }, root_focus); 218 219 // the "next" row is the last child of the list box: the view's outer box 220 // holds the sub-header then the list/diff split, whose first child is the 221 // list scroll. 222 const content = &view.box.children.values()[1].widget.box; 223 const lb = &content.children.values()[0].widget.scroll.child.box; 224 const next_id = lb.children.keys()[lb.children.count() - 1]; 225 226 const route = ui.crossPageLink(root_focus, next_id, session.data); 227 try std.testing.expect(route != null); 228 try std.testing.expectEqualStrings(base_oid, route.?.repo_commits.base_oid.slice()); 229 } 230 231 // a ref name with a '/' is url-encoded in the route, so it survives the 232 // '/'-delimited url round-trip as a single segment (kind + value) rather than 233 // being mis-parsed as extra path. 234 test "encoded ref name survives the commits url round-trip" { 235 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 236 defer arena.deinit(); 237 const RP = ui.RoutablePage; 238 const base_oid = "3333333333333333333333333333333333333333"; 239 240 // the route layer holds the value already url-encoded ("feature%2Ffoo"). 241 const route = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "", base_oid).?; 242 const url = try route.toUrl(&arena); 243 try std.testing.expectEqualStrings("/repo/alice/ziglings/commits/branch:feature%2Ffoo/base:" ++ base_oid, url); 244 245 // parsing it back yields the same route, and the ref splits out intact. 246 const parsed = RP.fromUrl(url); 247 try std.testing.expect(parsed != null); 248 const parsed_route = parsed.?.repo_commits; 249 try std.testing.expectEqual(RP.RefOrOid.branch, parsed_route.ref_or_oid.?); 250 try std.testing.expectEqualStrings("feature%2Ffoo", parsed_route.value.slice()); 251 try std.testing.expectEqualStrings(base_oid, parsed_route.base_oid.slice()); 252 const unfiltered = RP.repoCommitsRoute("alice/ziglings", .branch, "feature%2Ffoo", 0, "", "").?; 253 try std.testing.expect(!RP.eql(route, unfiltered)); 254 } 255 256 test "commit pages stop at the base without excluding other ancestors" { 257 try testCommitBase(.git); 258 try testCommitBase(.xit); 259 } 260 261 fn testCommitBase(comptime kind: xit.repo.RepoKind) !void { 262 const io = std.testing.io; 263 const allocator = std.testing.allocator; 264 const opts: xit.repo.RepoOpts(kind) = .{ .is_test = true }; 265 var temp = std.testing.tmpDir(.{}); 266 defer temp.cleanup(); 267 const path = try temp.dir.realPathFileAlloc(io, ".", allocator); 268 defer allocator.free(path); 269 var repo = try xit.repo.Repo(kind, opts).init(io, allocator, .{ .path = path }); 270 defer repo.deinit(io, allocator); 271 var arena = std.heap.ArenaAllocator.init(allocator); 272 defer arena.deinit(); 273 const location = ui.RoutablePage.RepoLocation{ .repo = "" }; 274 const content = ui.RoutablePage.RepoCommitsRoute.Content{ .diff = .{} }; 275 const branch = xit.ref.Ref{ .kind = .head, .name = "master" }; 276 277 // one more commit than fits on the first page 278 const base = try repo.commitAtRef(io, allocator, .{ .message = "base" }, null, branch); 279 var tip = base; 280 var previous = base; 281 for (0..21) |_| { 282 previous = tip; 283 tip = try repo.commitAtRef(io, allocator, .{ .message = "next" }, null, branch); 284 } 285 const first = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, &tip, content, &base); 286 try std.testing.expectEqual(20, first.commits.len); 287 try std.testing.expectEqual(@as(?u64, if (kind == .xit) 21 else null), first.commit_count); 288 const last = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, first.next_start orelse return error.MissingNext, content, &base); 289 try std.testing.expectEqual(1, last.commits.len); 290 try std.testing.expectEqual(null, last.next_start); 291 292 // the base at a page boundary must not create an empty next page 293 const full = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, &previous, content, &base); 294 try std.testing.expectEqual(20, full.commits.len); 295 try std.testing.expectEqual(null, full.next_start); 296 _ = try repo.addTag(io, allocator, .{ .name = "tip", .message = "annotated" }); 297 const sources = [_]struct { ref: ui.RoutablePage.RefOrOid, value: []const u8 }{ 298 .{ .ref = .object, .value = &tip }, 299 .{ .ref = .branch, .value = branch.name }, 300 .{ .ref = .tag, .value = "tip" }, 301 }; 302 for (sources) |source| { 303 const empty = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, source.ref, source.value, content, &tip); 304 try std.testing.expectEqual(0, empty.commits.len); 305 try std.testing.expectEqual(@as(?u64, if (kind == .xit) 0 else null), empty.commit_count); 306 } 307 308 // an off-chain stopping point must not hide its parent on this chain 309 const other = try repo.commitAtRef(io, allocator, .{ .message = "other", .parent_oids = &.{base} }, null, .{ .kind = .head, .name = "other" }); 310 const off_chain = try Commits.init(kind, opts, &arena, &repo, io, allocator, null, location, .object, last.commits[0].oid, content, &other); 311 try std.testing.expectEqual(2, off_chain.commits.len); 312 try std.testing.expectEqualStrings(&base, off_chain.commits[1].oid); 313 try std.testing.expectEqual(null, off_chain.commit_count); 314 } 315 316 test "sync creates missing event branches and preserves head" { 317 const io = std.testing.io; 318 const allocator = std.testing.allocator; 319 320 var temp = std.testing.tmpDir(.{}); 321 defer temp.cleanup(); 322 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 323 defer allocator.free(temp_path); 324 325 const local_path = try std.fs.path.join(allocator, &.{ temp_path, "local" }); 326 defer allocator.free(local_path); 327 const remote_path = try std.fs.path.join(allocator, &.{ temp_path, "remote" }); 328 defer allocator.free(remote_path); 329 330 const Repo = xit.repo.Repo(.git, .{}); 331 const RemoteRepo = xit.repo.Repo(.git, .{}); 332 { 333 var remote = try RemoteRepo.init(io, allocator, .{ .path = remote_path }); 334 remote.deinit(io, allocator); 335 } 336 { 337 var local = try Repo.init(io, allocator, .{ .path = local_path }); 338 defer local.deinit(io, allocator); 339 try local.addConfig(io, allocator, .{ .name = "user.name", .value = "user" }); 340 try local.addConfig(io, allocator, .{ .name = "user.email", .value = "user@haxy" }); 341 { 342 const file = try local.core.work_dir.createFile(io, "keep.txt", .{}); 343 defer file.close(io); 344 try file.writeStreamingAll(io, "keep"); 345 } 346 try local.add(io, allocator, &.{"keep.txt"}); 347 _ = try local.commit(io, allocator, .{ .message = "master" }); 348 try local.addRemote(io, allocator, .{ .name = "origin", .value = remote_path }); 349 350 const id = [_]u8{1} ** evt.event_id_size; 351 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 352 .id = std.fmt.bytesToHex(id, .lower), 353 .author = .{ .name = "haxy", .email = "user@haxy" }, 354 .event = .{ .issue = .{ .title = "sync", .description = "", .tags = "" } }, 355 }}); 356 } 357 358 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 359 { 360 var local = try Repo.open(io, allocator, .{ .path = local_path }); 361 defer local.deinit(io, allocator); 362 const id = [_]u8{2} ** evt.event_id_size; 363 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{.{ 364 .id = std.fmt.bytesToHex(id, .lower), 365 .author = .{ .name = "haxy", .email = "user@haxy" }, 366 .event = .{ .issue = .{ .title = "local", .description = "", .tags = "" } }, 367 }}); 368 } 369 { 370 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 371 defer remote.deinit(io, allocator); 372 const id = [_]u8{3} ** evt.event_id_size; 373 try evt.consume(.local, .repo, .git, .{}, io, allocator, &remote, evt.events_ref, &.{.{ 374 .id = std.fmt.bytesToHex(id, .lower), 375 .author = .{ .name = "haxy", .email = "user@haxy" }, 376 .event = .{ .issue = .{ .title = "remote", .description = "", .tags = "" } }, 377 }}); 378 } 379 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 380 { 381 var local = try Repo.open(io, allocator, .{ .path = local_path }); 382 defer local.deinit(io, allocator); 383 try local.removeBranch(io, .{ .name = evt.events_ref.name }); 384 try evt.consume(.local, .repo, .git, .{}, io, allocator, &local, evt.events_ref, &.{}); 385 } 386 try std.testing.expectEqual(null, try Events.sync(io, allocator, .{ .path = local_path, .repo_kind = .git })); 387 388 var local = try Repo.open(io, allocator, .{ .path = local_path }); 389 defer local.deinit(io, allocator); 390 var head_buffer: [xit.ref.MAX_REF_CONTENT_SIZE]u8 = undefined; 391 const head = try local.head(io, &head_buffer); 392 switch (head) { 393 .ref => |ref| try std.testing.expectEqualStrings("master", ref.name), 394 .oid => return error.TestExpectedEqual, 395 } 396 const kept = try local.core.work_dir.openFile(io, "keep.txt", .{}); 397 kept.close(io); 398 const local_tip = (try local.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 399 400 var remote = try RemoteRepo.open(io, allocator, .{ .path = remote_path }); 401 defer remote.deinit(io, allocator); 402 const remote_tip = (try remote.readRef(io, evt.events_ref)) orelse return error.TestExpectedEqual; 403 try std.testing.expectEqualStrings(&local_tip, &remote_tip); 404 405 var start_oids = [_][local_tip.len]u8{local_tip}; 406 var commit_iter = try local.log(io, allocator, .{ .start_oids = &start_oids }); 407 defer commit_iter.deinit(); 408 var merge_commit = (try commit_iter.next(allocator)) orelse return error.TestExpectedEqual; 409 defer merge_commit.deinit(); 410 const parent_oids = merge_commit.content.commit.metadata.parent_oids orelse return error.TestExpectedEqual; 411 try std.testing.expectEqual(2, parent_oids.len); 412 413 var arena = std.heap.ArenaAllocator.init(allocator); 414 defer arena.deinit(); 415 const status = try Events.Header.local(.git, .{}, arena.allocator(), &local, io, null); 416 try std.testing.expectEqualStrings("nothing to sync", status.sync_status orelse return error.TestExpectedEqual); 417 } 418