╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────╮ │78 pub const default: RoutablePage = .{ .home_users = 0 };│ │79 │ │80 pub const RefKind = enum { branch, tag }; │ │81 + │ │82 + pub const RefKindOrOid = enum { │ │83 + branch, │ │84 + tag, │ │85 + oid, │ │86 + │ │87 + fn fromSeg(s: []const u8) ?RefKindOrOid { │ │88 + return std.meta.stringToEnum(RefKindOrOid, s); │ │89 + } │ │90 + }; │ │91 │ │92 const user_segment = "/user/"; │ │93 const repo_segment = "/repo/"; │ ╰───────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │ 94 const files_seg = "files"; │ │ 95 const commits_seg = "commits"; │ │ 96 │ │ 87 - // a repo route is "username/reponame", optionally followed by │ │ 88 - // "/files/<dir>" for a directory in the files tab │ │ 97 + // a repo route is "username/reponame", optionally followed by the files tab's│ │ 98 + // "/files/<refkind>/<refvalue>/<dir>" (refkind/refvalue name the ref the tree│ │ 99 + // is read at). the bare "username/reponame" (and "username/reponame/files") │ │100 + // mean the files root of the repo's default branch. │ │101 pub const repo_route_max_len = 1024; │ │102 │ │ 91 - // the parts of a `.repo` files route. owner/name/dir are slices into the │ │ 92 - // route's stored string; dir is "" at the repo root. │ │103 + // the parts of a `.repo` files route. all slices point into the route's │ │104 + // stored string. ref_kind is null (and ref_value/dir empty) for the bare │ │105 + // default-branch root; dir is "" at a ref's root. │ │106 pub const RepoFiles = struct { │ │107 identity: []const u8, // "owner/name" │ │108 owner: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │109 name: []const u8, │ │110 + ref_kind: ?RefKindOrOid, │ │111 + ref_value: []const u8, │ │112 dir: []const u8, │ │113 │ │114 // parse a `.repo` route's stored string ("owner/name" or│ ╰──────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │100 - // "owner/name/files/<dir>"). │ │115 + // "owner/name/files/<refkind>/<refvalue>[/<dir>]"). │ │116 pub fn parse(s: []const u8) ?RepoFiles { │ │117 const s1 = std.mem.indexOfScalar(u8, s, '/') orelse return null;│ │118 const owner = s[0..s1]; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │120 const s2 = std.mem.indexOfScalar(u8, after, '/'); │ │121 const name = if (s2) |i| after[0..i] else after; │ │122 if (owner.len == 0 or name.len == 0) return null; │ │108 - const dir = if (s2) |i| blk: { │ │109 - const tail = after[i + 1 ..]; // "files" or "files/<dir>" │ │110 - break :blk if (std.mem.startsWith(u8, tail, files_seg ++ "/")) tail[files_seg.len + 1 ..] else "";│ │111 - } else ""; │ │112 - return .{ .identity = s[0 .. s1 + 1 + name.len], .owner = owner, .name = name, .dir = dir }; │ │123 + var result: RepoFiles = .{ │ │124 + .identity = s[0 .. s1 + 1 + name.len], │ │125 + .owner = owner, │ │126 + .name = name, │ │127 + .ref_kind = null, │ │128 + .ref_value = "", │ │129 + .dir = "", │ │130 + }; │ │131 + // the tail after "owner/name/". absent (or a bare "files") = default root. │ │132 + const tail = if (s2) |i| after[i + 1 ..] else return result; │ │133 + if (std.mem.eql(u8, tail, files_seg)) return result; │ │134 + if (!std.mem.startsWith(u8, tail, files_seg ++ "/")) return result; │ │135 + // "<refkind>/<refvalue>[/<dir>]" │ │136 + const ref_part = tail[files_seg.len + 1 ..]; │ │137 + const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return result; │ │138 + const kind = RefKindOrOid.fromSeg(ref_part[0..k1]) orelse return result; │ │139 + const after_kind = ref_part[k1 + 1 ..]; │ │140 + const k2 = std.mem.indexOfScalar(u8, after_kind, '/'); │ │141 + const value = if (k2) |i| after_kind[0..i] else after_kind; │ │142 + if (value.len == 0) return result; │ │143 + result.ref_kind = kind; │ │144 + result.ref_value = value; │ │145 + result.dir = if (k2) |i| after_kind[i + 1 ..] else ""; │ │146 + return result; │ │147 } │ │148 }; │ │149 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 - // build a `.repo` files route for "owner/name" (identity) at `dir` ("" = │ │117 - // root), or null if the result doesn't fit the inline name. │ │118 - pub fn repoFilesRoute(identity: []const u8, dir: []const u8) ?RoutablePage { │ │119 - if (dir.len == 0) return .{ .repo = Array(repo_route_max_len).from(identity) orelse return null }; │ │150 + // build a `.repo` files route for "owner/name" (identity) at `dir` ("" = the │ │151 + // ref's root) pinned to `ref_kind`/`ref_value`. a null ref_kind yields the │ │152 + // bare default-branch root. null if the result doesn't fit the inline name. │ │153 + pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefKindOrOid, ref_value: []const u8, dir: []const u8) ?RoutablePage { │ │154 + const kind = ref_kind orelse return .{ .repo = Array(repo_route_max_len).from(identity) orelse return null }; │ │155 var buf: [repo_route_max_len]u8 = undefined; │ │121 - const s = std.fmt.bufPrint(&buf, "{s}/" ++ files_seg ++ "/{s}", .{ identity, dir }) catch return null; │ │156 + const s = if (dir.len == 0) │ │157 + std.fmt.bufPrint(&buf, "{s}/" ++ files_seg ++ "/{s}/{s}", .{ identity, @tagName(kind), ref_value }) catch return null │ │158 + else │ │159 + std.fmt.bufPrint(&buf, "{s}/" ++ files_seg ++ "/{s}/{s}/{s}", .{ identity, @tagName(kind), ref_value, dir }) catch return null;│ │160 return .{ .repo = Array(repo_route_max_len).from(s) orelse return null }; │ │161 } │ │162 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │293 } }; │ │294 if (std.mem.eql(u8, sub, "settings")) return .{ .repo_settings = Array(repo_route_max_len).from(pair) orelse return null };│ │295 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null }; │ │258 - if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, ""); // trailing /files == root │ │259 - if (std.mem.startsWith(u8, sub, files_seg ++ "/")) return repoFilesRoute(pair, sub[files_seg.len + 1 ..]); │ │296 + if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, null, "", ""); // trailing /files == default-branch root │ │297 + if (std.mem.startsWith(u8, sub, files_seg ++ "/")) { │ │298 + // "files/<refkind>/<refvalue>[/<dir>]" │ │299 + const ref_part = sub[files_seg.len + 1 ..]; │ │300 + const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return null; │ │301 + const kind = RefKindOrOid.fromSeg(ref_part[0..k1]) orelse return null; │ │302 + const after_kind = ref_part[k1 + 1 ..]; │ │303 + const k2 = std.mem.indexOfScalar(u8, after_kind, '/'); │ │304 + const value = if (k2) |i| after_kind[0..i] else after_kind; │ │305 + if (value.len == 0) return null; │ │306 + const dir = if (k2) |i| after_kind[i + 1 ..] else ""; │ │307 + return repoFilesRoute(pair, kind, value, dir); │ │308 + } │ │309 if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, "", after); // trailing /commits == page 1 │ │310 if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) return repoCommitsRoute(pair, sub[commits_seg.len + 1 ..], after); │ │311 return null; // unknown sub-path │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │312 } │ │264 - return repoFilesRoute(pair, ""); │ │313 + return repoFilesRoute(pair, null, "", "");│ │314 } │ │315 return null; │ │316 } │ ╰───────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │359 // the content a repo route renders: which repo, plus the files directory and │ │360 // commits page. tab switches keep this fixed (the page already holds every │ │361 // tab's data); a files-directory or commits-page change alters it. │ │313 - const RepoContent = struct { identity: []const u8, files_dir: []const u8, commits_start: []const u8, commits_after: usize, refs_kind: RefKind, refs_after: usize }; │ │362 + const RepoContent = struct { identity: []const u8, files_ref_kind: ?RefKindOrOid, files_ref_value: []const u8, files_dir: []const u8, commits_start: []const u8, commits_after: usize, refs_kind: RefKind, refs_after: usize };│ │363 │ │364 fn repoContent(r: RoutablePage) ?RepoContent { │ │365 const name = switch (r) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │376 .repo_refs => |r2| r2.after, │ │377 else => 0, │ │378 }; │ │379 + const is_files = std.meta.activeTag(r) == .repo; │ │380 return .{ │ │381 .identity = rf.identity, │ │332 - .files_dir = if (std.meta.activeTag(r) == .repo) rf.dir else "", │ │382 + .files_ref_kind = if (is_files) rf.ref_kind else null, │ │383 + .files_ref_value = if (is_files) rf.ref_value else "", │ │384 + .files_dir = if (is_files) rf.dir else "", │ │385 .commits_start = if (std.meta.activeTag(r) == .repo_commits) repoCommitsStart(name) else "",│ │386 .commits_after = switch (r) { │ │387 .repo_commits => |c| c.after, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯