╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │418 │ │419 var self = View{ .box = box }; │ │420 // a page opens on its tabs, except that search results open in the │ │413 - // files tab's search box. │ │414 - self.getFocus().child_id = box.children.keys()[if (data.files.find != null) stack_index else header_index]; │ │421 + // tab's own search box. │ │422 + const results = data.files.find != null or (data.changes == .commits and data.changes.commits.search != null);│ │423 + self.getFocus().child_id = box.children.keys()[if (results) stack_index else header_index]; │ │424 return self; │ │425 } │ │426 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │471 .repo_header => { │ │472 if (stack.getSelected()) |selected_widget| switch (selected_widget.*) {│ │473 .repo_files => |*v| if (v.focusHeader(root_focus)) return, │ │465 - .repo_commits => |*v| if (v.focusCloneUrl(root_focus)) return, │ │474 + .repo_commits => |*v| if (v.focusHeader(root_focus)) return, │ │475 else => {}, │ │476 }; │ │477 index = stack_index; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────╮ │ 5 const xit = @import("xit"); │ │ 6 const rp = xit.repo; │ │ 7 const Diff = @import("Diff.zig"); │ │ 8 +const obj = xit.object; │ │ 9 +const cms = @import("../../search_commit.zig");│ │10 +const srch = @import("../../search.zig"); │ │11 const xitui = xit.xitui; │ │12 const wgt = xitui.widget; │ │13 const layout = xitui.layout; │ ╰───────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │48 next_start: ?[]const u8, │ │49 // what the pane shows for the commit the log walks from. │ │50 content: Content = .{ .diff = .{} }, │ │51 +// the decoded query this list holds the results of, or null for a plain log.│ │52 +search: ?[]const u8 = null, │ │53 +// whether the viewed ref has a commit index, so the search box shows and a │ │54 +// search: route resolves. travels in the page json for the wasm view. │ │55 +search_available: bool = false, │ │56 │ │57 const Self = @This(); │ │58 │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │86 requested_value: []const u8, │ │87 content: ui.RoutablePage.RepoCommitsRoute.Content, │ │88 base_oid: []const u8, │ │89 + search: []const u8, │ │90 + from: []const u8, │ │91 ) !Self { │ │92 const aa = arena.allocator(); │ │93 const hex_len = ui.ResolvedRefOrOid(repo_kind, repo_opts).hex_len;│ ╰──────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 if (base_oid.len != 0) { │ │ 95 evt.PatchRev.validateOid(repo_opts.hash, base_oid) catch return error.NotFound; │ │ 96 + } │ │ 97 + if (from.len != 0) { │ │ 98 + evt.PatchRev.validateOid(repo_opts.hash, from) catch return error.NotFound; │ │ 99 } │ │100 + const query: ?[]const u8 = if (search.len == 0) null else std.Uri.percentDecodeInPlace(try aa.dupe(u8, search));│ │101 │ │102 // the walk root's message replaces its diff, so it's read whole and its │ │103 // hunks aren't rendered. the window and the filter are its diff's. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │118 if (requested_ref_or_oid != null) return error.NotFound; │ │119 return emptyResult(aa, location, .branch, requested_value, content, base_oid); │ │120 }; │ │121 + │ │122 + var moment = repo.core.latestMoment() catch return emptyResult(aa, location, resolved.ref_or_oid, resolved.value, content, base_oid);│ │123 + const state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │124 │ │125 // collect this page's commit metadata, plus a peek at the one after it (its │ │126 // oid is the next page's start). the diff for each is rendered afterward, so │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │129 var oids: [page_size][hex_len]u8 = undefined; │ │130 var count: usize = 0; │ │131 var next_start: ?[]const u8 = null; │ │115 - { │ │116 - var iter = repo.log(io, gpa, .{ .start_oids = &.{resolved.oid}, .first_parent = true }) catch return emptyResult(aa, location, resolved.ref_or_oid, resolved.value, content, base_oid);│ │132 + var search_available = false; │ │133 + var searched = false; │ │134 + │ │135 + // the index covers a repository's branch and tag tips, so a fork or an │ │136 + // object view gets no search box and refuses a search: url. nor does a list │ │137 + // bounded by a base, whose version also covers the commits before it. │ │138 + if (comptime repo_kind == .xit) index: { │ │139 + if (location != .repo or resolved.ref_or_oid == .object or base_oid.len != 0) break :index; │ │140 + // versions are keyed by commit, so an annotated tag is peeled first │ │141 + var tip = obj.Object(.xit, repo_opts).initCommit(state, io, gpa, &resolved.oid) catch break :index; │ │142 + defer tip.deinit(); │ │143 + resolved.oid = tip.oid; │ │144 + const index = (try cms.lookup(repo_opts, moment, &resolved.oid)) orelse break :index; │ │145 + search_available = true; │ │146 + const text = query orelse break :index; │ │147 + │ │148 + var results = try srch.Query(rp.Repo(.xit, repo_opts).DB).init(index, aa, text); │ │149 + if (from.len != 0) { │ │150 + var start = obj.Object(.xit, repo_opts).initCommit(state, io, gpa, from[0..hex_len]) catch return error.NotFound; │ │151 + defer start.deinit(); │ │152 + const key = try cms.docKey(repo_opts.hash, start.content.commit.metadata.timestamp, &start.oid); │ │153 + results.seek(&key); │ │154 + } │ │155 + while (try results.next()) |key| { │ │156 + const oid = try cms.keyOid(repo_opts.hash, key); │ │157 + if (count == page_size) { │ │158 + next_start = try aa.dupe(u8, &oid); │ │159 + break; │ │160 + } │ │161 + // a result whose object is gone is skipped rather than shown │ │162 + var commit_object = obj.Object(.xit, repo_opts).init(state, io, gpa, &oid) catch continue; │ │163 + defer commit_object.deinit(); │ │164 + if (commit_object.content != .commit) continue; │ │165 + @memcpy(&oids[count], &oid); │ │166 + buf[count] = try commitEntry(repo_kind, repo_opts, arena, repo, io, gpa, admin_moment, &commit_object, root_message and count == 0); │ │167 + count += 1; │ │168 + } │ │169 + searched = true; │ │170 + } │ │171 + if (query != null and !searched) return error.NotFound; │ │172 + │ │173 + if (!searched) { │ │174 + // paging keeps the ref and starts the walk at the url's commit │ │175 + var start_oid = resolved.oid; │ │176 + if (from.len != 0) @memcpy(&start_oid, from); │ │177 + │ │178 + var iter = repo.log(io, gpa, .{ .start_oids = &.{start_oid}, .first_parent = true }) catch return emptyResult(aa, location, resolved.ref_or_oid, resolved.value, content, base_oid); │ │179 defer iter.deinit(); │ │180 while (try iter.next(gpa)) |commit_object| { │ │181 defer commit_object.deinit(); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │182 // count from the commit itself when the tip is an annotated tag │ │121 - if (count == 0) resolved.oid = commit_object.oid; │ │183 + if (count == 0 and from.len == 0) resolved.oid = commit_object.oid;│ │184 // the base is a stopping point, not an ancestry exclusion │ │185 if (std.mem.eql(u8, &commit_object.oid, base_oid)) break; │ │186 if (count == page_size) { │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 next_start = try aa.dupe(u8, &commit_object.oid); │ │188 break; │ │189 } │ │128 - const md = commit_object.content.commit.metadata; │ │190 @memcpy(&oids[count], &commit_object.oid); │ │130 - const text, const truncated = try readMessage(repo_kind, repo_opts, aa, commit_object, root_message and count == 0); │ │131 - buf[count] = .{ │ │132 - .oid = try aa.dupe(u8, &commit_object.oid), │ │133 - .date = try formatDate(aa, md.timestamp), │ │134 - .message = text, │ │135 - .message_truncated = truncated, │ │136 - .author = try ui.Author.init(admin_moment, arena, md.author orelse ""), │ │137 - .stats = if (repo_kind == .xit) try repo.commitStats(io, gpa, .{ .oid = &commit_object.oid }) else null, │ │138 - }; │ │191 + buf[count] = try commitEntry(repo_kind, repo_opts, arena, repo, io, gpa, admin_moment, commit_object, root_message and count == 0);│ │192 count += 1; │ │193 } │ │194 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯