╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#1b99cc2983bd6d1cf064bcf29656571f806c86d0",│ │ 7 - .hash = "xit-0.0.0-Ub3nDLvmTwGnsCaUgT3UwHN0PcHJuWnH_POiriug8wjq", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#0f04f0024679935e194aa6277818fda7cc0b98a8",│ │ 7 + .hash = "xit-0.0.0-Ub3nDFrsTwEk0OjVuEiEZrNeub1CsbB7MeMz4Vc839D-", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 +const builtin = @import("builtin"); │ │3 const evt = @import("../../event.zig");│ │4 const ui = @import("../../ui.zig"); │ │5 const xit = @import("xit"); │ ╰──────────────────────────────────────────╯ ╭────────────────────────────────╮ │ 6 const rp = xit.repo; │ │ 7 +const df = xit.diff; │ │ 8 const xitui = xit.xitui; │ │ 9 const wgt = xitui.widget; │ │10 const layout = xitui.layout;│ ╰────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │12 const Grid = xitui.grid.Grid; │ │13 const Focus = xitui.focus.Focus; │ │14 │ │13 -const Oid = [xit.hash.hexLen(.sha1)]u8; │ │14 - │ │15 // how many commits a page shows before a "next" link appears. │ │16 const page_size = 20; │ │17 +// max diff lines rendered per chunk (the initial render and each "load more")│ │18 +const diff_budget = 100; │ │19 │ │18 -// one commit on the current page. │ │20 +pub const Hunk = struct { │ │21 + lines: []const []const u8, │ │22 +}; │ │23 + │ │24 +// one commit on the current page, with its diff against its first parent │ │25 +// pre-rendered up to the budget so the web client can show it without a repo.│ │26 pub const Commit = struct { │ │27 oid: []const u8, │ │28 date: []const u8, // "YYYY-MM-DD" │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │29 message: []const u8, // first line only │ │30 + hunks: []const Hunk, │ │31 + // how many hunks `hunks` covers; the resume cursor for "load more". │ │32 + shown_hunks: usize, │ │33 + // true when the diff was truncated at the budget and more hunks remain.│ │34 + has_more: bool, │ │35 }; │ │36 │ │37 // "owner/name", so the view can build /repo/owner/name/commits/<oid> links.│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │39 commits: []const Commit, │ │40 // the first oid of the next page, or null when this is the last page. │ │41 next_start: ?[]const u8, │ │42 +// the on-disk repo directory name (hex event id), for reopening to render more│ │43 +// diff hunks when "load more" is activated in the TUI. │ │44 +repo_dir_hex: []const u8, │ │45 │ │46 const Self = @This(); │ │47 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │53 start_oid: []const u8, │ │54 ) !Self { │ │55 const aa = arena.allocator(); │ │41 - const empty: Self = .{ .identity = try aa.dupe(u8, identity), .commits = &.{}, .next_start = null };│ │56 + const hex = std.fmt.bytesToHex(event_id.*, .lower); │ │57 + const empty: Self = .{ │ │58 + .identity = try aa.dupe(u8, identity), │ │59 + .commits = &.{}, │ │60 + .next_start = null, │ │61 + .repo_dir_hex = try aa.dupe(u8, &hex), │ │62 + }; │ │63 │ │64 // no filesystem (wasm) or nowhere to look: empty listing. the wasm path │ │65 // never calls init anyway — it rebuilds from the serialized snapshot. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │66 const io = session.io orelse return empty; │ │67 const repos_dir = session.repos_dir orelse return empty; │ │68 │ │48 - // the repo's working copy lives at <repos_dir>/<hex event id>. │ │49 - const hex = std.fmt.bytesToHex(event_id.*, .lower); │ │69 const repo_path = try std.fs.path.join(aa, &.{ repos_dir, &hex }); │ │70 │ │71 // walk the log with the arena's backing allocator (transient; the commits│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │75 defer repo.deinit(io, gpa); │ │76 │ │77 // page 1 walks from HEAD (null); a later page starts at its first oid. │ │59 - var start_arr: [1]Oid = undefined; │ │60 - const start_oids: ?[]const Oid = if (start_oid.len == 0) null else blk: { │ │78 + var start_arr: [1][xit.hash.hexLen(.sha1)]u8 = undefined; │ │79 + const start_oids: ?[]const [xit.hash.hexLen(.sha1)]u8 = if (start_oid.len == 0) null else blk: {│ │80 if (start_oid.len != start_arr[0].len) return empty; // malformed oid │ │81 @memcpy(&start_arr[0], start_oid); │ │82 break :blk start_arr[0..1]; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 83 }; │ │ 84 │ │ 66 - var iter = repo.log(io, gpa, start_oids) catch return empty; │ │ 67 - defer iter.deinit(); │ │ 68 - │ │ 69 - // collect this page's commits, plus a peek at the one after it (its oid is │ │ 70 - // the next page's start). │ │ 85 + // collect this page's commit metadata, plus a peek at the one after it (its │ │ 86 + // oid is the next page's start). the diff for each is rendered afterward, so │ │ 87 + // the log iterator is closed before opening per-commit diff iterators. │ │ 88 var buf: [page_size]Commit = undefined; │ │ 89 + var oids: [page_size][xit.hash.hexLen(.sha1)]u8 = undefined; │ │ 90 var count: usize = 0; │ │ 91 var next_start: ?[]const u8 = null; │ │ 74 - while (try iter.next(gpa)) |commit_object| { │ │ 75 - defer commit_object.deinit(); │ │ 76 - if (count == page_size) { │ │ 77 - next_start = try aa.dupe(u8, &commit_object.oid); │ │ 78 - break; │ │ 92 + { │ │ 93 + var iter = repo.log(io, gpa, start_oids) catch return empty; │ │ 94 + defer iter.deinit(); │ │ 95 + while (try iter.next(gpa)) |commit_object| { │ │ 96 + defer commit_object.deinit(); │ │ 97 + if (count == page_size) { │ │ 98 + next_start = try aa.dupe(u8, &commit_object.oid); │ │ 99 + break; │ │100 + } │ │101 + const md = commit_object.content.commit.metadata; │ │102 + @memcpy(&oids[count], &commit_object.oid); │ │103 + buf[count] = .{ │ │104 + .oid = try aa.dupe(u8, &commit_object.oid), │ │105 + .date = try formatDate(aa, md.timestamp), │ │106 + .message = try aa.dupe(u8, firstLine(md.message orelse "")), │ │107 + .hunks = &.{}, │ │108 + .shown_hunks = 0, │ │109 + .has_more = false, │ │110 + }; │ │111 + count += 1; │ │112 + } │ │113 + } │ │114 + │ │115 + // render each commit's diff (best effort: a failed diff leaves it empty). │ │116 + // the diff machinery isn't wasm-clean and the wasm client never runs it (it │ │117 + // renders from the snapshot), so gate it out of the wasm build. │ │118 + if (!builtin.cpu.arch.isWasm()) { │ │119 + for (buf[0..count], oids[0..count]) |*commit, oid| { │ │120 + const rendered = renderCommitDiff(io, gpa, aa, &repo, oid, 0, diff_budget) catch│ │121 + RenderedDiff{ .hunks = &.{}, .shown_hunks = 0, .has_more = false }; │ │122 + commit.hunks = rendered.hunks; │ │123 + commit.shown_hunks = rendered.shown_hunks; │ │124 + commit.has_more = rendered.has_more; │ │125 } │ │ 80 - const md = commit_object.content.commit.metadata; │ │ 81 - buf[count] = .{ │ │ 82 - .oid = try aa.dupe(u8, &commit_object.oid), │ │ 83 - .date = try formatDate(aa, md.timestamp), │ │ 84 - .message = try aa.dupe(u8, firstLine(md.message orelse "")), │ │ 85 - }; │ │ 86 - count += 1; │ │126 } │ │127 │ │128 return .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯