╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │124 dir: []const u8, │ │125 │ │126 // parse a `.repo_files` route's stored string ("owner/name" or │ │127 - // "owner/name/files/<refkind>/<refvalue>[/<dir>]"). │ │127 + // "owner/name/files/<refkind>/<refvalue>[/<dir>]"). a malformed tail│ │128 + // reads as the bare default-branch root. │ │129 pub fn parse(s: []const u8) ?RepoFiles { │ │129 - const s1 = std.mem.indexOfScalar(u8, s, '/') orelse return null; │ │130 - const owner = s[0..s1]; │ │131 - const after = s[s1 + 1 ..]; │ │132 - const s2 = std.mem.indexOfScalar(u8, after, '/'); │ │133 - const name = if (s2) |i| after[0..i] else after; │ │130 + var segments = std.mem.splitScalar(u8, s, '/'); │ │131 + const owner = segments.next() orelse return null; │ │132 + const name = segments.next() orelse return null; │ │133 if (owner.len == 0 or name.len == 0) return null; │ │134 var result: RepoFiles = .{ │ │136 - .identity = s[0 .. s1 + 1 + name.len], │ │135 + .identity = s[0 .. owner.len + 1 + name.len], │ │136 .owner = owner, │ │137 .name = name, │ │138 .ref_kind = null, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │139 .ref_value = "", │ │140 .dir = "", │ │141 }; │ │143 - // the tail after "owner/name/". absent (or a bare "files") = default root. │ │144 - const tail = if (s2) |i| after[i + 1 ..] else return result; │ │145 - if (std.mem.eql(u8, tail, files_seg)) return result; │ │146 - if (!std.mem.startsWith(u8, tail, files_seg ++ "/")) return result; │ │147 - // "<refkind>/<refvalue>[/<dir>]" │ │148 - const ref_part = tail[files_seg.len + 1 ..]; │ │149 - const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return result; │ │150 - const kind = RefOrOid.fromSeg(ref_part[0..k1]) orelse return result; │ │151 - const after_kind = ref_part[k1 + 1 ..]; │ │152 - const k2 = std.mem.indexOfScalar(u8, after_kind, '/'); │ │153 - const value = if (k2) |i| after_kind[0..i] else after_kind; │ │142 + if (!std.mem.eql(u8, segments.next() orelse return result, files_seg)) return result; │ │143 + const kind = RefOrOid.fromSeg(segments.next() orelse return result) orelse return result;│ │144 + const value = segments.next() orelse return result; │ │145 if (value.len == 0) return result; │ │146 result.ref_kind = kind; │ │147 result.ref_value = value; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │157 - result.dir = if (k2) |i| after_kind[i + 1 ..] else "";│ │148 + result.dir = segments.rest(); │ │149 return result; │ │150 } │ │151 }; │ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 // returned value slices into `s`. │ │171 pub const CommitsRef = struct { ref_or_oid: ?RefOrOid, value: []const u8 }; │ │172 pub fn repoCommitsRef(s: []const u8) CommitsRef { │ │182 - const marker = "/" ++ commits_seg ++ "/"; │ │183 - const i = std.mem.indexOf(u8, s, marker) orelse return .{ .ref_or_oid = null, .value = "" }; │ │184 - const ref_part = s[i + marker.len ..]; // "<refkind>/<refvalue>" │ │185 - const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return .{ .ref_or_oid = null, .value = "" };│ │186 - const kind = RefOrOid.fromSeg(ref_part[0..k1]) orelse return .{ .ref_or_oid = null, .value = "" }; │ │187 - const value = ref_part[k1 + 1 ..]; │ │188 - if (value.len == 0) return .{ .ref_or_oid = null, .value = "" }; │ │173 + const bare = CommitsRef{ .ref_or_oid = null, .value = "" }; │ │174 + var segments = std.mem.splitScalar(u8, s, '/'); │ │175 + _ = segments.next(); // owner │ │176 + _ = segments.next(); // name │ │177 + if (!std.mem.eql(u8, segments.next() orelse "", commits_seg)) return bare; │ │178 + const kind = RefOrOid.fromSeg(segments.next() orelse "") orelse return bare; │ │179 + const value = segments.rest(); │ │180 + if (value.len == 0) return bare; │ │181 return .{ .ref_or_oid = kind, .value = value }; │ │182 } │ │183 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │289 // /user/<name>[/repos|/settings|/auth] │ │290 if (std.mem.startsWith(u8, path, user_segment)) { │ │291 const rest = path[user_segment.len..]; │ │300 - const slash = std.mem.indexOfScalar(u8, rest, '/'); │ │301 - const name = if (slash) |s| rest[0..s] else rest; │ │292 + var segments = std.mem.splitScalar(u8, rest, '/'); │ │293 + const name = segments.next() orelse return null; │ │294 if (name.len == 0) return null; │ │295 const parsed = Array(evt.User.name_max_len).from(name) orelse return null; // name too long │ │304 - if (slash) |s| { │ │305 - const sub = rest[s + 1 ..]; │ │296 + if (segments.peek() != null) { │ │297 + const sub = segments.rest(); │ │298 if (std.mem.eql(u8, sub, "repos")) return .{ .user_repos = .{ .name = parsed, .after = after } };│ │299 if (std.mem.eql(u8, sub, "settings")) return .{ .user_settings = parsed }; │ │300 if (std.mem.eql(u8, sub, "auth")) return .{ .user_auth = parsed }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │308 // "username/reponame/files/<dir>"; settings/auth store just the pair. │ │309 if (std.mem.startsWith(u8, path, repo_segment)) { │ │310 const rest = path[repo_segment.len..]; │ │319 - const user_slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null; │ │320 - const after_user = rest[user_slash + 1 ..]; │ │321 - const repo_slash = std.mem.indexOfScalar(u8, after_user, '/'); │ │322 - const repo_name = if (repo_slash) |s| after_user[0..s] else after_user; │ │311 + var rest_segments = std.mem.splitScalar(u8, rest, '/'); │ │312 + const owner = rest_segments.next() orelse return null; │ │313 + const repo_name = rest_segments.next() orelse return null; │ │314 // reject an empty username or reponame │ │324 - if (user_slash == 0 or repo_name.len == 0) return null; │ │325 - const pair = rest[0 .. user_slash + 1 + repo_name.len]; // "username/reponame"│ │326 - if (repo_slash) |s| { │ │327 - const sub = after_user[s + 1 ..]; │ │315 + if (owner.len == 0 or repo_name.len == 0) return null; │ │316 + const pair = rest[0 .. owner.len + 1 + repo_name.len]; // "username/reponame" │ │317 + if (rest_segments.peek() != null) { │ │318 + const sub = rest_segments.rest(); │ │319 if (std.mem.eql(u8, sub, "refs")) return .{ .repo_refs = .{ │ │320 .name = Array(repo_route_max_len).from(pair) orelse return null, │ │321 .kind = refsKind(query), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │342 } │ │343 if (std.mem.eql(u8, sub, "settings")) return .{ .repo_settings = Array(repo_route_max_len).from(pair) orelse return null }; │ │344 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null }; │ │354 - if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, null, "", "", after); // trailing /files == default-branch root│ │355 - if (std.mem.startsWith(u8, sub, files_seg ++ "/")) { │ │356 - // "files/<refkind>/<refvalue>[/<dir>]" │ │357 - const ref_part = sub[files_seg.len + 1 ..]; │ │358 - const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return null; │ │359 - const kind = RefOrOid.fromSeg(ref_part[0..k1]) orelse return null; │ │360 - const after_kind = ref_part[k1 + 1 ..]; │ │361 - const k2 = std.mem.indexOfScalar(u8, after_kind, '/'); │ │362 - const value = if (k2) |i| after_kind[0..i] else after_kind; │ │345 + if (std.mem.eql(u8, sub, files_seg) or std.mem.startsWith(u8, sub, files_seg ++ "/")) { │ │346 + // "files[/<refkind>/<refvalue>[/<dir>]]" (bare = default-branch root) │ │347 + var segments = std.mem.splitScalar(u8, sub, '/'); │ │348 + _ = segments.next(); // "files" │ │349 + const kind_segment = segments.next() orelse return repoFilesRoute(pair, null, "", "", after); │ │350 + const kind = RefOrOid.fromSeg(kind_segment) orelse return null; │ │351 + const value = segments.next() orelse return null; │ │352 if (value.len == 0) return null; │ │364 - const dir = if (k2) |i| after_kind[i + 1 ..] else ""; │ │365 - return repoFilesRoute(pair, kind, value, dir, after); │ │353 + return repoFilesRoute(pair, kind, value, segments.rest(), after); │ │354 } │ │367 - if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, null, "", after); // trailing /commits == default branch │ │368 - if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) { │ │369 - // "commits/<refkind>/<refvalue>" │ │370 - const ref_part = sub[commits_seg.len + 1 ..]; │ │371 - const k1 = std.mem.indexOfScalar(u8, ref_part, '/') orelse return null; │ │372 - const kind = RefOrOid.fromSeg(ref_part[0..k1]) orelse return null; │ │373 - const value = ref_part[k1 + 1 ..]; │ │355 + if (std.mem.eql(u8, sub, commits_seg) or std.mem.startsWith(u8, sub, commits_seg ++ "/")) { │ │356 + // "commits[/<refkind>/<refvalue>]" (bare = default branch) │ │357 + var segments = std.mem.splitScalar(u8, sub, '/'); │ │358 + _ = segments.next(); // "commits" │ │359 + const kind_segment = segments.next() orelse return repoCommitsRoute(pair, null, "", after); │ │360 + const kind = RefOrOid.fromSeg(kind_segment) orelse return null; │ │361 + const value = segments.rest(); │ │362 if (value.len == 0) return null; │ │363 return repoCommitsRoute(pair, kind, value, after); │ │364 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯