╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │193 │ │194 // the "key:value" segments of a url tail, collected in any order │ │195 const Params = struct { │ │196 - const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find }; │ │196 + const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from };│ │197 │ │198 // a branch:/tag:/object: param; the value stays url-encoded │ │199 const RefParam = struct { kind: RefOrOid, value: []const u8 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │272 const start_seg = @tagName(Params.ParamKey.start) ++ ":"; │ │273 const line_seg = @tagName(Params.ParamKey.line) ++ ":"; │ │274 const find_seg = @tagName(Params.ParamKey.find) ++ ":"; │ │275 + const search_seg = @tagName(Params.ParamKey.search) ++ ":";│ │276 + const from_seg = @tagName(Params.ParamKey.from) ++ ":"; │ │277 const base_seg = @tagName(Params.ParamKey.base) ++ ":"; │ │278 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":";│ │279 const issue_seg = "issue:"; │ ╰────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │311 // caps the files tab's url-encoded search term. │ │312 pub const find_route_max_len = 100 * 3; │ │313 │ │314 + // caps the commits tab's url-encoded search query. │ │315 + pub const search_route_max_len = 100 * 3; │ │316 + │ │317 // the identity stored by every repo route. Local routes elide it.│ │318 pub const RepoIdentity = struct { │ │319 identity: []const u8, // "owner/name" │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │369 ref_or_oid: ?RefOrOid = null, │ │370 value: Array(ref_route_max_len) = .{}, │ │371 base_oid: Array(ref_route_max_len) = .{}, │ │372 + // the url-encoded query this page holds the results of ("" = the plain│ │373 + // log). only a branch or tag can carry one. │ │374 + search: Array(search_route_max_len) = .{}, │ │375 + // the commit the list starts at ("" = the ref's own commit). │ │376 + from: Array(ref_route_max_len) = .{}, │ │377 // what the pane shows for the commit the log walks from. │ │378 content: Content = .{ .diff = .{} }, │ │379 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │540 │ │541 // carry a search term on a files route, url-encoding it into the route. │ │542 // null when it doesn't fit. │ │533 - pub fn withFind(self: RoutablePage, search: []const u8) ?RoutablePage { │ │534 - var find: Array(find_route_max_len) = .{}; │ │535 - var writer: std.Io.Writer = .fixed(&find.bytes); │ │536 - std.Uri.Component.percentEncode(&writer, search, isUnreserved) catch return null;│ │537 - find.len = @intCast(writer.buffered().len); │ │543 + pub fn withFind(self: RoutablePage, file_name: []const u8) ?RoutablePage { │ │544 + const find = encodeParam(find_route_max_len, file_name) orelse return null; │ │545 var route = self; │ │546 switch (route) { │ │547 .repo_files => |*f| f.find = find, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │548 .fork_files => |*f| f.find = find, │ │549 + else => return null, │ │550 + } │ │551 + return route; │ │552 + } │ │553 + │ │554 + // carry a search query on a commits route, url-encoding it into the route. │ │555 + pub fn withSearch(self: RoutablePage, query: []const u8) ?RoutablePage { │ │556 + const search = encodeParam(search_route_max_len, query) orelse return null;│ │557 + var route = self; │ │558 + switch (route) { │ │559 + .repo_commits => |*c| c.search = search, │ │560 else => return null, │ │561 } │ │562 return route; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │563 } │ │564 │ │565 + // root a commits route's list at `oid` rather than the ref's own commit. │ │566 + pub fn withFrom(self: RoutablePage, oid: []const u8) ?RoutablePage { │ │567 + var route = self; │ │568 + switch (route) { │ │569 + .repo_commits => |*c| c.from = Array(ref_route_max_len).from(oid) orelse return null, │ │570 + else => return null, │ │571 + } │ │572 + return route; │ │573 + } │ │574 + │ │575 + fn encodeParam(comptime max_len: usize, value: []const u8) ?Array(max_len) { │ │576 + var encoded: Array(max_len) = .{}; │ │577 + var writer: std.Io.Writer = .fixed(&encoded.bytes); │ │578 + std.Uri.Component.percentEncode(&writer, value, isUnreserved) catch return null; │ │579 + encoded.len = @intCast(writer.buffered().len); │ │580 + return encoded; │ │581 + } │ │582 + │ │583 pub fn repoPatchRevFilesRoute(identity: []const u8, id: []const u8, path: []const u8, line: usize) ?RoutablePage {│ │584 _ = evt.parseEventId(id) catch return null; │ │585 return .{ .repo_files = .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │970 try out.writer.print("{s}/" ++ commits_seg, .{prefix}); │ │971 if (c.ref_or_oid) |kind| if (c.value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), c.value.slice() });│ │972 if (c.base_oid.len != 0) try out.writer.print("/" ++ base_seg ++ "{s}", .{c.base_oid.slice()}); │ │973 + if (c.search.len != 0) try out.writer.print("/" ++ search_seg ++ "{s}", .{c.search.slice()}); │ │974 + if (c.from.len != 0) try out.writer.print("/" ++ from_seg ++ "{s}", .{c.from.slice()}); │ │975 switch (c.content) { │ │976 .diff => |d| { │ │977 if (d.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{d.start}); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1526 } │ │1527 if (std.mem.eql(u8, tab, commits_seg)) { │ │1528 params.scanPairs(&segments) catch return null; │ │1491 - if (!params.only(&.{ .start, .branch, .tag, .object, .base })) return null; │ │1529 + if (!params.only(&.{ .start, .branch, .tag, .object, .base, .search, .from })) return null; │ │1530 const base_oid = params.values.get(.base) orelse ""; │ │1531 + const search = params.values.get(.search) orelse ""; │ │1532 + const from = params.values.get(.from) orelse ""; │ │1533 const start = params.start() orelse return null; │ │1534 const ref = (params.ref() catch return null) orelse { │ │1535 + if (search.len != 0 or from.len != 0) return null; │ │1536 const file = pathValue(segments.rest()) orelse return null; │ │1537 return if (file.len == 0) repoCommitsRoute(pair, null, "", start, "", base_oid) else null;│ │1538 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1539 + // the index covers branch and tag tips, so only they can be │ │1540 + // searched or paged by commit; an object roots the list itself │ │1541 + if ((search.len != 0 or from.len != 0) and ref.kind == .object) return null; │ │1542 // the message page has no diff window or file filter │ │1499 - if (std.mem.eql(u8, segments.rest(), message_seg)) │ │1500 - return if (start == 0) repoCommitMessageRoute(pair, ref.kind, ref.value, base_oid) else null; │ │1501 - const file = pathValue(segments.rest()) orelse return null; │ │1502 - return repoCommitsRoute(pair, ref.kind, ref.value, start, file, base_oid); │ │1543 + var route = if (std.mem.eql(u8, segments.rest(), message_seg)) │ │1544 + (if (start == 0) repoCommitMessageRoute(pair, ref.kind, ref.value, base_oid) else null) orelse return null │ │1545 + else │ │1546 + repoCommitsRoute(pair, ref.kind, ref.value, start, pathValue(segments.rest()) orelse return null, base_oid) orelse return null;│ │1547 + if (search.len != 0) route.repo_commits.search = Array(search_route_max_len).from(search) orelse return null; │ │1548 + if (from.len != 0) route.repo_commits.from = Array(ref_route_max_len).from(from) orelse return null; │ │1549 + return route; │ │1550 } │ │1551 if (std.mem.eql(u8, tab, "diff")) { │ │1552 params.scanPairs(&segments) catch return null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯