╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────╮ │ 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 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │213 .base_oid = try aa.dupe(u8, base_oid), │ │214 .commit_count = if (repo_kind == .xit and location == .repo) blk: { │ │215 if (base_oid.len == 0) break :blk repo.commitCount(io, gpa, .{ .oid = &resolved.oid }) catch null; │ │163 - var moment = repo.core.latestMoment() catch break :blk null; │ │164 - const state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } };│ │216 break :blk evt.PatchRev.commitCount(repo_kind, repo_opts, state, io, gpa, base_oid, &resolved.oid) catch null; │ │217 } else null, │ │218 .commits = try aa.dupe(Commit, buf[0..count]), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │219 .next_start = next_start, │ │220 .content = try pageContent(aa, content), │ │221 + .search = query, │ │222 + .search_available = search_available, │ │223 + }; │ │224 +} │ │225 + │ │226 +// one commit's row: its message, author and stats. the diff window is rendered │ │227 +// afterward, once the log iterator is closed. │ │228 +fn commitEntry( │ │229 + comptime repo_kind: rp.RepoKind, │ │230 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │231 + arena: *std.heap.ArenaAllocator, │ │232 + repo: *rp.Repo(repo_kind, repo_opts), │ │233 + io: std.Io, │ │234 + gpa: std.mem.Allocator, │ │235 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │236 + commit_object: *obj.Object(repo_kind, repo_opts), │ │237 + full_message: bool, │ │238 +) !Commit { │ │239 + const aa = arena.allocator(); │ │240 + const md = commit_object.content.commit.metadata; │ │241 + const text, const truncated = try readMessage(repo_kind, repo_opts, aa, commit_object, full_message); │ │242 + return .{ │ │243 + .oid = try aa.dupe(u8, &commit_object.oid), │ │244 + .date = try formatDate(aa, md.timestamp), │ │245 + .message = text, │ │246 + .message_truncated = truncated, │ │247 + .author = try ui.Author.init(admin_moment, arena, md.author orelse ""), │ │248 + .stats = if (repo_kind == .xit) try repo.commitStats(io, gpa, .{ .oid = &commit_object.oid }) else null,│ │249 }; │ │250 } │ │251 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯