╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │822 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); │ │823 defer template_dir.close(io); │ │824 │ │821 - for (repo_event_ids) |id_bytes| { │ │825 + for (repo_event_ids, 0..) |id_bytes, repo_index| { │ │826 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); │ │827 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); │ │824 - var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); │ │825 - defer dest_dir.close(io); │ │826 - try copyDir(io, template_dir, dest_dir); │ │828 + { │ │829 + var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); │ │830 + defer dest_dir.close(io); │ │831 + try copyDir(io, template_dir, dest_dir); │ │832 + } │ │833 + │ │834 + if (repo_index + 1 == repo_data.len) { │ │835 + var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); │ │836 + defer target_repo.deinit(io, allocator); │ │837 + try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random());│ │838 + } │ │839 } │ │840 } │ │841 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 972 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); │ │ 973 _ = try repo.addTag(io, allocator, .{ .name = name }); │ │ 974 n.* += 1; │ │ 975 +} │ │ 976 + │ │ 977 +const PatchRevisionFixture = struct { │ │ 978 + id: [evt.event_id_size]u8, │ │ 979 + oid: [hash.hexLen(.sha1)]u8, │ │ 980 + tree_oid: [hash.hexLen(.sha1)]u8, │ │ 981 + timestamp: u64, │ │ 982 + │ │ 983 + fn event(self: *const PatchRevisionFixture, title: []const u8) evt.PatchRev { │ │ 984 + return .{ │ │ 985 + .base_oid = &self.oid, │ │ 986 + .source_oid = &self.oid, │ │ 987 + .target_ref = "refs/heads/master", │ │ 988 + .message = title, │ │ 989 + }; │ │ 990 + } │ │ 991 +}; │ │ 992 + │ │ 993 +fn commitTree( │ │ 994 + io: std.Io, │ │ 995 + allocator: std.mem.Allocator, │ │ 996 + repo: *rp.Repo(.xit, .{}), │ │ 997 + oid: *const [hash.hexLen(.sha1)]u8, │ │ 998 +) ![hash.hexLen(.sha1)]u8 { │ │ 999 + var moment = try repo.core.latestMoment(); │ │1000 + const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │1001 + var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); │ │1002 + defer object.deinit(); │ │1003 + return switch (object.content) { │ │1004 + .commit => |commit| commit.tree, │ │1005 + else => error.InvalidObject, │ │1006 + }; │ │1007 +} │ │1008 + │ │1009 +fn seedPatchRevision( │ │1010 + io: std.Io, │ │1011 + allocator: std.mem.Allocator, │ │1012 + repos_path: []const u8, │ │1013 + patch_id: *const [evt.event_id_size]u8, │ │1014 + title: []const u8, │ │1015 + timestamp: u64, │ │1016 + random: std.Random, │ │1017 +) !PatchRevisionFixture { │ │1018 + const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); │ │1019 + const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); │ │1020 + defer allocator.free(fork_path); │ │1021 + var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); │ │1022 + defer fork_repo.deinit(io, allocator); │ │1023 + │ │1024 + const source_oid = (try fork_repo.readRef(io, fork.ref)) orelse return error.NotFound; │ │1025 + var fixture = PatchRevisionFixture{ │ │1026 + .id = evt.EventWithId.randomId(random), │ │1027 + .oid = source_oid, │ │1028 + .tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid), │ │1029 + .timestamp = timestamp, │ │1030 + }; │ │1031 + const revision = fixture.event(title); │ │1032 + const tree_entries = [_]evt.EventTreeEntry{ │ │1033 + .{ .tree = .{ .name = "base", .oid = &fixture.tree_oid } }, │ │1034 + .{ .tree = .{ .name = "head", .oid = &fixture.tree_oid } }, │ │1035 + }; │ │1036 + try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ │ │1037 + .id = std.fmt.bytesToHex(fixture.id, .lower), │ │1038 + .timestamp = timestamp, │ │1039 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │1040 + .tree_entries = &tree_entries, │ │1041 + .event = .{ .patchrev = revision }, │ │1042 + }}); │ │1043 + │ │1044 + var arena = std.heap.ArenaAllocator.init(allocator); │ │1045 + defer arena.deinit(); │ │1046 + const moment = try evt.currentMoment(.{}, &fork_repo); │ │1047 + const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fixture.id)) orelse return error.NotFound; │ │1048 + const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; │ │1049 + var patch = patch_record.event; │ │1050 + patch.revision = evt.Patch.Revision.fromRecord(fixture.id, revision_record); │ │1051 + try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ │ │1052 + .id = patch_hex, │ │1053 + .timestamp = timestamp + 1, │ │1054 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │1055 + .event = .{ .patch = patch }, │ │1056 + }}); │ │1057 + return fixture; │ │1058 } │ │1059 + │ │1060 +fn seedPatches( │ │1061 + io: std.Io, │ │1062 + allocator: std.mem.Allocator, │ │1063 + server_path: []const u8, │ │1064 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │1065 + target_repo: *rp.Repo(.xit, .{}), │ │1066 + repo_id: *const [evt.event_id_size]u8, │ │1067 + user_id: *const [evt.event_id_size]u8, │ │1068 + random: std.Random, │ │1069 +) !void { │ │1070 + const patch_data = [_]struct { │ │1071 + title: []const u8, │ │1072 + description: []const u8, │ │1073 + tags: []const u8, │ │1074 + status: ?evt.Patch.Status, │ │1075 + }{ │ │1076 + .{ │ │1077 + .title = "Draft a faster dependency scanner", │ │1078 + .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", │ │1079 + .tags = "performance build", │ │1080 + .status = null, │ │1081 + }, │ │1082 + .{ │ │1083 + .title = "Remove the legacy configuration loader", │ │1084 + .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.",│ │1085 + .tags = "cleanup config", │ │1086 + .status = .merged, │ │1087 + }, │ │1088 + .{ │ │1089 + .title = "Cache parsed manifests between commands", │ │1090 + .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", │ │1091 + .tags = "performance cache", │ │1092 + .status = .closed, │ │1093 + }, │ │1094 + .{ │ │1095 + .title = "Add structured output to the inspect command", │ │1096 + .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", │ │1097 + .tags = "enhancement cli", │ │1098 + .status = .open, │ │1099 + }, │ │1100 + .{ │ │1101 + .title = "Preserve file permissions during export", │ │1102 + .description = "Carry executable bits through archive exports so unpacked command-line tools remain runnable without a manual chmod step.", │ │1103 + .tags = "bug export permissions", │ │1104 + .status = .open, │ │1105 + }, │ │1106 + }; │ │1107 │ │1108 + const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); │ │1109 + defer allocator.free(repos_path); │ │1110 + var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; │ │1111 + for (patch_data, 0..) |patch, i| { │ │1112 + patch_ids[i] = evt.EventWithId.randomId(random); │ │1113 + const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); │ │1114 + const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ │ │1115 + .id = patch_hex, │ │1116 + .user_id = user_id.*, │ │1117 + .repo_id = repo_id.*, │ │1118 + .title = patch.title, │ │1119 + .description = patch.description, │ │1120 + .tags = patch.tags, │ │1121 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │1122 + .timestamp = @intCast(500 + i * 10), │ │1123 + }); │ │1124 + allocator.free(path); │ │1125 + const status = patch.status orelse continue; │ │1126 + │ │1127 + const revision = try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, @intCast(502 + i * 10), random); │ │1128 + const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); │ │1129 + defer allocator.free(fork_path); │ │1130 + try pch.post(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ │ │1131 + .id = patch_hex, │ │1132 + .user_id = user_id.*, │ │1133 + .repo_id = repo_id.*, │ │1134 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │1135 + .timestamp = @intCast(505 + i * 10), │ │1136 + }); │ │1137 + │ │1138 + if (status == .merged) { │ │1139 + const event = revision.event(patch.title); │ │1140 + const tree_entries = [_]evt.EventTreeEntry{ │ │1141 + .{ .tree = .{ .name = "base", .oid = &revision.tree_oid } }, │ │1142 + .{ .tree = .{ .name = "head", .oid = &revision.tree_oid } }, │ │1143 + }; │ │1144 + try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{.{ │ │1145 + .id = std.fmt.bytesToHex(revision.id, .lower), │ │1146 + .timestamp = revision.timestamp, │ │1147 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │1148 + .tree_entries = &tree_entries, │ │1149 + .event = .{ .patchrev = event }, │ │1150 + }}); │ │1151 + } │ │1152 + if (status != .open) { │ │1153 + try evt.Patch.update(.xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, .{ │ │1154 + .name = "admin", │ │1155 + .email = "admin@example.test", │ │1156 + }); │ │1157 + } │ │1158 + } │ │1159 + │ │1160 + // seed a small comment tree on the newest patch │ │1161 + var comment_ids: [5][evt.event_id_size]u8 = undefined; │ │1162 + for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); │ │1163 + const comment_bodies = [_][]const u8{ │ │1164 + "This also needs to preserve the executable bit for files nested inside generated directories.", │ │1165 + "I tested the archive path and the direct-copy path; both currently lose the mode.", │ │1166 + "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", │ │1167 + "Please include a regression fixture with a mix of executable and ordinary files.", │ │1168 + "I'll add that fixture and cover both supported archive formats.", │ │1169 + }; │ │1170 + var comments: [comment_ids.len]evt.EventWithId = undefined; │ │1171 + for (&comments, 0..) |*comment, i| { │ │1172 + comment.* = .{ │ │1173 + .id = std.fmt.bytesToHex(comment_ids[i], .lower), │ │1174 + .timestamp = @intCast(700 + i), │ │1175 + .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, │ │1176 + .event = .{ .comment = .{ │ │1177 + .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), │ │1178 + .parent_id = switch (i) { │ │1179 + 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), │ │1180 + 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), │ │1181 + 3 => std.fmt.bytesToHex(comment_ids[2], .lower), │ │1182 + else => unreachable, │ │1183 + }, │ │1184 + .body = comment_bodies[i], │ │1185 + } }, │ │1186 + }; │ │1187 + } │ │1188 + try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); │ │1189 + │ │1190 + // create divergent metadata edits on the next three patches │ │1191 + var patch_arena = std.heap.ArenaAllocator.init(allocator); │ │1192 + defer patch_arena.deinit(); │ │1193 + const moment = try evt.currentMoment(.{}, target_repo); │ │1194 + var values: [3]evt.Patch = undefined; │ │1195 + for (1..4) |i| { │ │1196 + const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; │ │1197 + values[i - 1] = record.event; │ │1198 + } │ │1199 + var ours_values = values; │ │1200 + ours_values[0].title = "Drop the legacy configuration loader"; │ │1201 + ours_values[0].tags = "cleanup config breaking"; │ │1202 + ours_values[1].description = "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change."; │ │1203 + ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; │ │1204 + var theirs_values = values; │ │1205 + theirs_values[0].title = "Delete compatibility configuration support"; │ │1206 + theirs_values[0].tags = "config maintenance"; │ │1207 + theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; │ │1208 + theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; │ │1209 + │ │1210 + const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; │ │1211 + const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; │ │1212 + var ours = [_]evt.EventWithId{ │ │1213 + .{ │ │1214 + .id = std.fmt.bytesToHex(patch_ids[1], .lower), │ │1215 + .timestamp = 800, │ │1216 + .author = .{ .name = "alice", .email = "alice@example.test" }, │ │1217 + .event = .{ .patch = ours_values[0] }, │ │1218 + }, │ │1219 + .{ │ │1220 + .id = std.fmt.bytesToHex(patch_ids[2], .lower), │ │1221 + .timestamp = 801, │ │1222 + .author = .{ .name = "alice", .email = "alice@example.test" }, │ │1223 + .event = .{ .patch = ours_values[1] }, │ │1224 + }, │ │1225 + .{ │ │1226 + .id = std.fmt.bytesToHex(patch_ids[3], .lower), │ │1227 + .timestamp = 802, │ │1228 + .author = .{ .name = "alice", .email = "alice@example.test" }, │ │1229 + .event = .{ .patch = ours_values[2] }, │ │1230 + }, │ │1231 + }; │ │1232 + │ │1233 + var theirs = ours; │ │1234 + theirs[0].timestamp = 810; │ │1235 + theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; │ │1236 + theirs[0].event.patch = theirs_values[0]; │ │1237 + theirs[1].timestamp = 811; │ │1238 + theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; │ │1239 + theirs[1].event.patch = theirs_values[1]; │ │1240 + theirs[2].timestamp = 812; │ │1241 + theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; │ │1242 + theirs[2].event.patch = theirs_values[2]; │ │1243 + │ │1244 + try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); │ │1245 + var json: std.Io.Writer.Allocating = .init(allocator); │ │1246 + defer json.deinit(); │ │1247 + for (theirs, 0..) |event, i| { │ │1248 + json.clearRetainingCapacity(); │ │1249 + try std.json.Stringify.value(event, .{}, &json.writer); │ │1250 + const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); │ │1251 + defer allocator.free(author); │ │1252 + _ = try target_repo.commitAtRef(io, allocator, .{ │ │1253 + .author = author, │ │1254 + .message = json.written(), │ │1255 + .timestamp = event.timestamp, │ │1256 + .parent_oids = if (i == 0) &.{seed_tip} else null, │ │1257 + }, null, other_ref); │ │1258 + } │ │1259 + { │ │1260 + var to_events = try target_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │1261 + defer to_events.deinit(); │ │1262 + } │ │1263 + { │ │1264 + var merge = try target_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); │ │1265 + defer merge.deinit(); │ │1266 + if (merge.result != .success) return error.MergeFailed; │ │1267 + } │ │1268 + { │ │1269 + var to_master = try target_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); │ │1270 + defer to_master.deinit(); │ │1271 + } │ │1272 + try target_repo.removeBranch(io, .{ .name = other_ref.name }); │ │1273 + try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); │ │1274 +} │ │1275 + │ │1276 // recursively copy the contents of src_dir into dest_dir │ │1277 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { │ │1278 var iter = src_dir.iterate(); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 .user => blk: { │ │64 const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │65 break :blk switch (route) { │ │66 - .user_repos => |u| .{ .user = try User.init(arena, haxy_moment, u.name, u.start) }, │ │67 - .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name, 0) }, │ │66 + .user_repos => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, u.start, 0) }, │ │67 + .user_forks => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, 0, u.start) }, │ │68 + .user_settings, .user_auth => |name| .{ .user = try User.init(arena, session, haxy_moment, name, 0, 0) },│ │69 else => return error.UnexpectedRoute, │ │70 }; │ │71 }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │72 .repo => switch (route) { │ │72 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_discussions, .repo_events, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) }, │ │73 + .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) },│ │74 else => return error.UnexpectedRoute, │ │75 }, │ │76 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │ 94 home_settings, │ │ 95 home_auth, │ │ 96 user_repos: struct { name: Array(evt.User.name_max_len), start: usize = 0 },│ │ 97 + user_forks: struct { name: Array(evt.User.name_max_len), start: usize = 0 },│ │ 98 user_settings: Array(evt.User.name_max_len), │ │ 99 user_auth: Array(evt.User.name_max_len), │ │100 repo_files: RepoFilesRoute, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │124 // the comment shown in the detail pane (empty = the issue thread).│ │125 comment: Array(evt.event_id_size * 2) = .{}, │ │126 }, │ │127 + repo_patches: struct { │ │128 + name: Array(repo_route_max_len), │ │129 + tag: Array(patch_tag_route_max_len) = .{}, │ │130 + selected: Array(evt.event_id_size * 2) = .{}, │ │131 + theirs: Array(theirs_route_max_len) = .{}, │ │132 + view: PatchesView = .open, │ │133 + comments_start: usize = 0, │ │134 + comment: Array(evt.event_id_size * 2) = .{}, │ │135 + }, │ │136 repo_discussions: struct { │ │137 name: Array(repo_route_max_len), │ │138 tag: Array(discussion_tag_route_max_len) = .{}, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 │ │152 pub const IssuesView = enum { open, closed, tags, new, edit, description, new_comment, edit_comment, remove, conflicts, resolve }; │ │153 │ │154 + pub const PatchesView = enum { open, closed, merged, tags, new, drafts, edit, description, new_comment, edit_comment, remove, conflicts, resolve };│ │155 + │ │156 pub const DiscussionsView = enum { recent, tags, new, edit, description, new_comment, edit_comment, remove }; │ │157 │ │158 pub const EventsView = enum { active, removed }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │244 const line_seg = @tagName(Params.ParamKey.line) ++ ":"; │ │245 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":"; │ │246 const issue_seg = "issue:"; │ │247 + const patch_seg = "patch:"; │ │248 const discuss_seg = "discuss:"; │ │249 const comment_seg = "comment:"; │ │250 // marks a files or commits route's trailing path: the value is the raw│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │266 │ │267 // a url-encoded tag can grow to three bytes per source byte. │ │268 pub const issue_tag_route_max_len = evt.Issue.tag_max_len * 3; │ │269 + pub const patch_tag_route_max_len = evt.Patch.tag_max_len * 3; │ │270 pub const discussion_tag_route_max_len = evt.Discussion.tag_max_len * 3; │ │271 │ │272 // caps the resolve view's theirs: field list (a longer one doesn't route).│ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │409 return .{ .repo_issues = .{ │ │410 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │411 .tag = Array(issue_tag_route_max_len).from(tag) orelse return null, │ │412 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │413 + .view = switch (status) { │ │414 + .open => .open, │ │415 + .closed => .closed, │ │416 + }, │ │417 + } }; │ │418 + } │ │419 + │ │420 + pub fn repoPatchesRoute(identity: []const u8, status: evt.Patch.Status, tag: []const u8, selected: []const u8) ?RoutablePage {│ │421 + return .{ .repo_patches = .{ │ │422 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │423 + .tag = Array(patch_tag_route_max_len).from(tag) orelse return null, │ │424 .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │425 .view = switch (status) { │ │426 .open => .open, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯