╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/test/ui.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 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(.git, .{}, io, allocator, &local, evt.events_ref, &.{.{ │ │114 + .id = std.fmt.bytesToHex(id, .lower), │ │115 + .author_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(.git, .{}, io, allocator, &local, evt.events_ref, &.{.{ │ │126 + .id = std.fmt.bytesToHex(id, .lower), │ │127 + .author_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(.git, .{}, io, allocator, &remote, evt.events_ref, &.{.{ │ │136 + .id = std.fmt.bytesToHex(id, .lower), │ │137 + .author_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(.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.status); │ │179 } │ │180 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 }; │ │55 }, │ │56 .repo => switch (route) { │ │57 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) }, │ │57 + .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_events, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ │58 else => return error.UnexpectedRoute, │ │59 }, │ │60 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │107 // the comment shown in the detail pane (empty = the issue thread).│ │108 comment: Array(evt.event_id_size * 2) = .{}, │ │109 }, │ │110 + repo_events: RepoEventsRoute, │ │111 repo_settings: Array(repo_route_max_len), │ │112 repo_auth: Array(repo_route_max_len), │ │113 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │285 }, │ │286 message, │ │287 }; │ │288 + }; │ │289 + │ │290 + pub const RepoEventsRoute = struct { │ │291 + name: Array(repo_identity_max_len), │ │292 + kind: ?evt.EventKind = null, │ │293 + selected: Array(evt.event_id_size * 2) = .{}, │ │294 }; │ │295 │ │296 // build a `.repo_files` route (a null ref_kind = the bare default-branch│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │346 } }; │ │347 } │ │348 │ │349 + // build the events list or a window rooted at one event. │ │350 + pub fn repoEventsRoute(identity: []const u8, kind: ?evt.EventKind, selected: []const u8) ?RoutablePage {│ │351 + if ((kind == null) != (selected.len == 0)) return null; │ │352 + return .{ .repo_events = .{ │ │353 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │354 + .kind = kind, │ │355 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │356 + } }; │ │357 + } │ │358 + │ │359 // build a `.repo_issues` route for "owner/name" (identity) showing │ │360 // `status`'s list, filtered to the url-encoded `tag` ("" = unfiltered) and │ │361 // rooted at the issue with hex event id `selected` ("" = the first window). │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │571 else │ │572 try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(i.view), i.tag.slice() });│ │573 }, │ │574 + .repo_events => |e| blk: { │ │575 + const prefix = try repoUrlPrefix(arena, e.name.slice()); │ │576 + break :blk if (e.kind) |kind| │ │577 + try std.fmt.allocPrint(arena.allocator(), "{s}/event:{s}/kind:{s}", .{ prefix, e.selected.slice(), @tagName(kind) }) │ │578 + else │ │579 + try std.fmt.allocPrint(arena.allocator(), "{s}/events", .{prefix}); │ │580 + }, │ │581 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/settings", .{try repoUrlPrefix(arena, name.slice())}), │ │582 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{try repoUrlPrefix(arena, name.slice())}), │ │583 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │652 .repo_commits => |*c| c.name.slice(), │ │653 .repo_refs => |*r| r.name.slice(), │ │654 .repo_issues => |*i| i.name.slice(), │ │655 + .repo_events => |*e| e.name.slice(), │ │656 .repo_settings, .repo_auth => |*name| name.slice(),│ │657 else => null, │ │658 }; │ ╰────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │677 return switch (self) { │ │678 .home_users, .home_repos, .home_settings, .home_auth => .home, │ │679 .user_repos, .user_settings, .user_auth => .user, │ │655 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_settings, .repo_auth => .repo, │ │680 + .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_events, .repo_settings, .repo_auth => .repo,│ │681 }; │ │682 } │ │683 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │763 if (std.mem.eql(u8, w, "tags")) return repoIssuesTagsRoute(pair, tag_value); │ │764 if (std.mem.eql(u8, w, "new")) return if (tag_value.len == 0) repoIssuesNewRoute(pair) else null; │ │765 return null; │ │766 + } │ │767 + if (std.mem.eql(u8, tab, "events")) │ │768 + return if (segments.next() == null) repoEventsRoute(pair, null, "") else null; │ │769 + if (std.mem.startsWith(u8, tab, "event:")) { │ │770 + const event_id = tab["event:".len..]; │ │771 + const kind_segment = segments.next() orelse return null; │ │772 + if (!std.mem.startsWith(u8, kind_segment, "kind:")) return null; │ │773 + const kind = std.meta.stringToEnum(evt.EventKind, kind_segment["kind:".len..]) orelse return null;│ │774 + if (event_id.len == 0 or segments.next() != null) return null; │ │775 + return repoEventsRoute(pair, kind, event_id); │ │776 } │ │777 if (std.mem.eql(u8, tab, files_seg)) { │ │778 params.scanPairs(&segments) catch return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │831 a_i.view == b.repo_issues.view and │ │832 a_i.comments_start == b.repo_issues.comments_start and │ │833 std.mem.eql(u8, a_i.comment.slice(), b.repo_issues.comment.slice()), │ │834 + .repo_events => |a_e| std.mem.eql(u8, a_e.name.slice(), b.repo_events.name.slice()) and│ │835 + a_e.kind == b.repo_events.kind and │ │836 + std.mem.eql(u8, a_e.selected.slice(), b.repo_events.selected.slice()), │ │837 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │838 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │839 else => true, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯