╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │76 user_repos: struct { name: Array(evt.User.name_max_len), start: usize = 0 },│ │77 user_settings: Array(evt.User.name_max_len), │ │78 user_auth: Array(evt.User.name_max_len), │ │79 - repo_files: Array(repo_route_max_len), │ │80 - repo_commits: Array(repo_route_max_len), │ │79 + repo_files: RepoFilesRoute, │ │80 + repo_commits: RepoCommitsRoute, │ │81 repo_refs: struct { │ │82 name: Array(repo_route_max_len), │ │83 kind: RefKind = .branch, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │188 // pair means the default branch's files root. a local session's routes │ │189 // elide the identity: "" at the bare root, else starting at the tail's "/". │ │190 pub const repo_route_max_len = 1024; │ │191 + pub const repo_identity_max_len = evt.User.name_max_len + 1 + evt.Repo.name_max_len;│ │192 │ │193 // true when a repo route's stored string elides its identity (local mode). │ │194 // unambiguous because a full string always starts with its owner segment. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │202 // caps a url-encoded ref name in a route (a longer name doesn't route). │ │203 pub const ref_route_max_len = 512; │ │204 │ │204 - // the parts of a `.repo_files` route. all slices point into the route's │ │205 - // stored string. ref_kind is null (and ref_value/dir empty) for the bare│ │206 - // default-branch root; dir is "" at a ref's root. │ │207 - pub const RepoFiles = struct { │ │205 + // the identity stored by every repo route. Local routes elide it. │ │206 + pub const RepoIdentity = struct { │ │207 identity: []const u8, // "owner/name" │ │208 owner: []const u8, │ │209 name: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │211 - start: usize, // the content window's first line, 0 = the first window│ │212 - ref_kind: ?RefOrOid, │ │213 - ref_value: []const u8, │ │214 - dir: []const u8, │ │210 │ │216 - // parse a `.repo_files` route's stored string. a malformed tail reads│ │217 - // as the bare default-branch root. │ │218 - pub fn parse(s: []const u8) ?RepoFiles { │ │219 - var result: RepoFiles = .{ │ │211 + pub fn parse(s: []const u8) ?RepoIdentity { │ │212 + var result: RepoIdentity = .{ │ │213 .identity = "", │ │214 .owner = "", │ │215 .name = "", │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │223 - .start = 0, │ │224 - .ref_kind = null, │ │225 - .ref_value = "", │ │226 - .dir = "", │ │216 }; │ │228 - var rest: []const u8 = undefined; │ │217 if (identityElided(s)) { │ │230 - rest = if (s.len == 0) "" else s[1..]; │ │218 + return result; │ │219 } else { │ │220 var id_segments = std.mem.splitScalar(u8, s, '/'); │ │221 const owner = id_segments.next() orelse return null;│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │224 result.identity = s[0 .. owner.len + 1 + name.len]; │ │225 result.owner = owner; │ │226 result.name = name; │ │239 - rest = id_segments.rest(); │ │227 } │ │241 - var segments = std.mem.splitScalar(u8, rest, '/'); │ │242 - if (!std.mem.eql(u8, segments.next() orelse return result, files_seg)) return result;│ │243 - var params = Params{}; │ │244 - params.scanPairs(&segments) catch return result; │ │245 - result.start = params.start() orelse return result; │ │246 - const ref = (params.ref() catch return result) orelse return result; │ │247 - result.ref_kind = ref.kind; │ │248 - result.ref_value = ref.value; │ │249 - result.dir = pathValue(segments.rest()) orelse return result; │ │228 return result; │ │229 } │ │230 + }; │ │231 + │ │232 + pub const RepoFilesRoute = struct { │ │233 + name: Array(repo_identity_max_len), │ │234 + ref_kind: ?RefOrOid = null, │ │235 + ref_value: Array(ref_route_max_len) = .{}, │ │236 + path: Array(repo_route_max_len) = .{}, │ │237 + start: usize = 0, │ │238 + }; │ │239 + │ │240 + pub const RepoCommitsRoute = struct { │ │241 + name: Array(repo_identity_max_len), │ │242 + ref_or_oid: ?RefOrOid = null, │ │243 + value: Array(ref_route_max_len) = .{}, │ │244 + start: usize = 0, │ │245 }; │ │246 │ │247 // build a `.repo_files` route (a null ref_kind = the bare default-branch │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │248 // root). null if the result doesn't fit the inline name. │ │249 pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefOrOid, ref_value: []const u8, dir: []const u8, start: usize) ?RoutablePage {│ │257 - if (ref_kind == null and start == 0) return .{ .repo_files = Array(repo_route_max_len).from(identity) orelse return null }; │ │258 - var buf: [repo_route_max_len]u8 = undefined; │ │259 - var writer: std.Io.Writer = .fixed(&buf); │ │260 - writer.print("{s}/" ++ files_seg, .{identity}) catch return null; │ │261 - if (ref_kind) |kind| writer.print("/{s}:{s}", .{ @tagName(kind), ref_value }) catch return null; │ │262 - if (start != 0) writer.print("/" ++ start_seg ++ "{d}", .{start}) catch return null; │ │263 - if (dir.len != 0) writer.print("/" ++ path_seg ++ "{s}", .{dir}) catch return null; │ │264 - return .{ .repo_files = Array(repo_route_max_len).from(writer.buffered()) orelse return null }; │ │250 + if (ref_kind == null and (ref_value.len != 0 or dir.len != 0)) return null; │ │251 + if (ref_kind != null and ref_value.len == 0 and dir.len != 0) return null; │ │252 + return .{ .repo_files = .{ │ │253 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │254 + .ref_kind = ref_kind, │ │255 + .ref_value = Array(ref_route_max_len).from(ref_value) orelse return null, │ │256 + .path = Array(repo_route_max_len).from(dir) orelse return null, │ │257 + .start = start, │ │258 + } }; │ │259 } │ │260 │ │267 - // the ref/oid a `.repo_commits` route's stored string walks from (null = │ │268 - // the default branch), plus the diff window start. the value slices into `s`. │ │269 - pub const CommitsRef = struct { ref_or_oid: ?RefOrOid, value: []const u8, start: usize }; │ │270 - pub fn repoCommitsRef(s: []const u8) CommitsRef { │ │271 - var result = CommitsRef{ .ref_or_oid = null, .value = "", .start = 0 }; │ │272 - if (s.len == 0) return result; │ │273 - var segments = std.mem.splitScalar(u8, s, '/'); │ │274 - if (identityElided(s)) { │ │275 - _ = segments.next(); // the empty segment before the tail │ │276 - } else { │ │277 - _ = segments.next(); // owner │ │278 - _ = segments.next(); // name │ │279 - } │ │280 - if (!std.mem.eql(u8, segments.next() orelse "", commits_seg)) return result; │ │281 - var params = Params{}; │ │282 - params.scanPairs(&segments) catch return result; │ │283 - result.start = params.start() orelse return result; │ │284 - const ref = (params.ref() catch return result) orelse return result; │ │285 - result.ref_or_oid = ref.kind; │ │286 - result.value = ref.value; │ │287 - return result; │ │288 - } │ │289 - │ │261 // build a `.repo_commits` route (a null ref_or_oid = the default branch). │ │262 // always carries the `commits` marker so the bare route doesn't collide │ │263 // with the files root. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 pub fn repoCommitsRoute(identity: []const u8, ref_or_oid: ?RefOrOid, value: []const u8, start: usize) ?RoutablePage {│ │294 - var buf: [repo_route_max_len]u8 = undefined; │ │295 - var writer: std.Io.Writer = .fixed(&buf); │ │296 - writer.print("{s}/" ++ commits_seg, .{identity}) catch return null; │ │297 - if (ref_or_oid) |kind| writer.print("/{s}:{s}", .{ @tagName(kind), value }) catch return null; │ │298 - if (start != 0) writer.print("/" ++ start_seg ++ "{d}", .{start}) catch return null; │ │299 - return .{ .repo_commits = Array(repo_route_max_len).from(writer.buffered()) orelse return null }; │ │265 + if (ref_or_oid == null and value.len != 0) return null; │ │266 + return .{ .repo_commits = .{ │ │267 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │268 + .ref_or_oid = ref_or_oid, │ │269 + .value = Array(ref_route_max_len).from(value) orelse return null, │ │270 + .start = start, │ │271 + } }; │ │272 } │ │273 │ │274 // build a `.repo_refs` route windowing `kind`'s column from the │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │338 }; │ │339 } │ │340 │ │369 - pub fn urlAlloc(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │341 + pub fn toUrl(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │342 return switch (self) { │ │343 .home_users => |start| if (start == 0) @as([]const u8, "/users") else try std.fmt.allocPrint(arena.allocator(), "/users/" ++ start_seg ++ "{d}", .{start}),│ │344 .home_repos => |start| if (start == 0) @as([]const u8, "/repos") else try std.fmt.allocPrint(arena.allocator(), "/repos/" ++ start_seg ++ "{d}", .{start}),│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │350 try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/repos/" ++ start_seg ++ "{d}", .{ u.name.slice(), u.start }),│ │351 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}), │ │352 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │381 - .repo_files => |name| try repoUrlPath(arena, name.slice()), │ │382 - .repo_commits => |name| try repoUrlPath(arena, name.slice()), │ │353 + .repo_files => |f| blk: { │ │354 + const prefix = try repoUrlPrefix(arena, f.name.slice()); │ │355 + if (f.ref_kind == null and f.start == 0) break :blk if (prefix.len == 0) "/" else prefix; │ │356 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │357 + try out.writer.print("{s}/" ++ files_seg, .{prefix}); │ │358 + if (f.ref_kind) |kind| if (f.ref_value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), f.ref_value.slice() });│ │359 + if (f.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{f.start}); │ │360 + if (f.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{f.path.slice()}); │ │361 + break :blk out.written(); │ │362 + }, │ │363 + .repo_commits => |c| blk: { │ │364 + const prefix = try repoUrlPrefix(arena, c.name.slice()); │ │365 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │366 + try out.writer.print("{s}/" ++ commits_seg, .{prefix}); │ │367 + if (c.ref_or_oid) |kind| if (c.value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), c.value.slice() }); │ │368 + if (c.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{c.start}); │ │369 + break :blk out.written(); │ │370 + }, │ │371 .repo_refs => |r| blk: { │ │372 const prefix = try repoUrlPrefix(arena, r.name.slice()); │ │373 break :blk if (r.from.len == 0) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯