╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │541 │ │542 // the concrete ref/oid (a null request resolves to the default branch) │ │543 ref_or_oid: RoutablePage.RefOrOid, │ │544 - // its branch/tag name or oid string, duped into the allocator passed in │ │544 + // its branch/tag name or oid, url-encoded (ref names may contain '/', so the│ │545 + // route layer — which splits on '/' — stores them encoded). duped into `aa`.│ │546 value: []const u8, │ │547 // the commit oid it points at │ │548 oid: [hex_len]u8, │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │549 │ │550 // resolve a requested ref/oid (null = the repo's default branch) to a │ │550 - // concrete ref/oid plus the commit oid it points at. null when it doesn't │ │551 - // resolve (an unknown branch/tag, or a malformed/unknown oid). │ │551 + // concrete ref/oid plus the commit oid it points at. `requested_value` is the│ │552 + // url-encoded form (as it appears in the route). null when it doesn't resolve│ │553 + // (an unknown branch/tag, or a malformed/unknown oid). │ │554 pub fn init( │ │555 repo: *rp.Repo(.xit, .{}), │ │556 io: std.Io, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │559 requested_value: []const u8, │ │560 ) !?ResolvedRefOrOid { │ │561 var ref_or_oid: RoutablePage.RefOrOid = requested_ref_or_oid orelse .branch;│ │560 - var value: []const u8 = requested_value; │ │562 + // the decoded ref name / oid to look up. a named ref arrives url-encoded. │ │563 + var value: []const u8 = if (requested_ref_or_oid == null) │ │564 + requested_value │ │565 + else │ │566 + std.Uri.percentDecodeInPlace(try aa.dupe(u8, requested_value)); │ │567 // no ref named: fall back to HEAD's branch (or its oid when detached). │ │568 if (requested_ref_or_oid == null) { │ │569 var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │590 oid = (repo.readRef(io, .{ .kind = ref_kind, .name = value }) catch null) orelse return null;│ │591 }, │ │592 } │ │587 - // dupe so the value outlives the stack buffer head() may have filled. │ │588 - return .{ .ref_or_oid = ref_or_oid, .value = try aa.dupe(u8, value), .oid = oid }; │ │593 + // store url-encoded so the route layer can hold the value verbatim. │ │594 + return .{ .ref_or_oid = ref_or_oid, .value = try urlEncode(aa, value), .oid = oid }; │ │595 + } │ │596 + │ │597 + fn isUnreserved(c: u8) bool { │ │598 + return std.ascii.isAlphanumeric(c) or c == '-' or c == '.' or c == '_' or c == '~'; │ │599 } │ │600 + │ │601 + // percent-encode a ref name for use as a single url path segment. │ │602 + fn urlEncode(aa: std.mem.Allocator, raw: []const u8) ![]const u8 { │ │603 + var out: std.Io.Writer.Allocating = .init(aa); │ │604 + try std.Uri.Component.percentEncode(&out.writer, raw, isUnreserved); │ │605 + return out.written(); │ │606 + } │ │607 }; │ │608 │ │609 // per-connection mutable state. each SSH session / web session / local TUI │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯