╭ changed ───╮ │src/main.zig│ ╰────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 82 // probe for a git repo first, then a xit repo. only the work path │ │ 83 // and backend kind are kept; pages re-open the repo on each build. │ │ 84 const local: ui.RepoSource = blk: { │ │ 85 - if (rp.AnyRepo(.git, .{}).open(io, allocator, .{ .path = cwd_path })) |repo| { │ │ 86 - var git_repo = repo; │ │ 87 - defer git_repo.deinit(io, allocator); │ │ 88 - const work_path = switch (git_repo) { │ │ 89 - inline else => |*r| r.core.work_path, │ │ 90 - }; │ │ 91 - break :blk .{ .path = try session_arena.allocator().dupe(u8, work_path), .repo_kind = .git }; │ │ 92 - } else |err| switch (err) { │ │ 93 - error.RepoNotFound => {}, │ │ 94 - else => |e| return e, │ │ 85 + inline for ([_]rp.RepoKind{ .git, .xit }) |probe_kind| { │ │ 86 + if (rp.AnyRepo(probe_kind, .{}).open(io, allocator, .{ .path = cwd_path })) |repo| { │ │ 87 + var opened = repo; │ │ 88 + defer opened.deinit(io, allocator); │ │ 89 + const work_path = switch (opened) { │ │ 90 + inline else => |*r| r.core.work_path, │ │ 91 + }; │ │ 92 + break :blk .{ .path = try session_arena.allocator().dupe(u8, work_path), .repo_kind = probe_kind };│ │ 93 + } else |err| switch (err) { │ │ 94 + error.RepoNotFound => {}, │ │ 95 + else => |e| return e, │ │ 96 + } │ │ 97 } │ │ 96 - var xit_repo = rp.AnyRepo(.xit, .{}).open(io, allocator, .{ .path = cwd_path }) catch |err| switch (err) { │ │ 97 - error.RepoNotFound => { │ │ 98 - try run_opts.err.print("no git or xit repo found in the current directory\n", .{}); │ │ 99 - return error.HandledError; │ │100 - }, │ │101 - else => |e| return e, │ │102 - }; │ │103 - defer xit_repo.deinit(io, allocator); │ │104 - const work_path = switch (xit_repo) { │ │105 - inline else => |*r| r.core.work_path, │ │106 - }; │ │107 - break :blk .{ .path = try session_arena.allocator().dupe(u8, work_path), .repo_kind = .xit }; │ │ 98 + try run_opts.err.print("no git or xit repo found in the current directory\n", .{}); │ │ 99 + return error.HandledError; │ │100 }; │ │101 │ │102 var session = ui.Session{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │41 .home_repos => |after| after, │ │42 else => 0, │ │43 }) }, │ │44 - .user => switch (route) { │ │45 - .user_repos => |u| .{ .user = try User.init(arena, session.haxy_moment orelse return error.NoMoment, u.name, u.after) }, │ │46 - .user_settings, .user_auth => |name| .{ .user = try User.init(arena, session.haxy_moment orelse return error.NoMoment, name, 0) }, │ │47 - else => return error.UnexpectedRoute, │ │44 + .user => blk: { │ │45 + const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │46 + break :blk switch (route) { │ │47 + .user_repos => |u| .{ .user = try User.init(arena, haxy_moment, u.name, u.after) }, │ │48 + .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name, 0) }, │ │49 + else => return error.UnexpectedRoute, │ │50 + }; │ │51 }, │ │52 .repo => switch (route) { │ │53 .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │115 // straight at the tail's separator ("/files/..."). │ │116 pub const repo_route_max_len = 1024; │ │117 │ │115 - // true when a repo route's stored string elides its identity (local mode) │ │118 + // true when a repo route's stored string elides its identity (local mode).│ │119 + // unambiguous because a full string always starts with its owner segment. │ │120 fn identityElided(s: []const u8) bool { │ │121 return s.len == 0 or s[0] == '/'; │ │122 } │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────╮ │2 const evt = @import("../event.zig");│ │3 const ui = @import("../ui.zig"); │ │4 const xit = @import("xit"); │ │5 +const rp = xit.repo; │ │6 const xitui = xit.xitui; │ │7 const wgt = xitui.widget; │ │8 const layout = xitui.layout; │ ╰───────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │155 } │ │156 } │ │157 │ │157 - // build files and commits first so their resolved ref (the default branch │ │158 - // when the route named none) can canonicalize each tab's mirror url to the │ │159 - // explicit ref it's viewing rather than leaving it bare. both resolve the │ │160 - // same requested ref, so they end up viewing the same one. │ │161 - const files = try Files.init(arena, session, source, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after); │ │162 - const commits = try Commits.init(arena, session, source, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); │ │158 + // open the on-disk repo once and read every tab's data from it. files and │ │159 + // commits resolve the same requested ref (the default branch when the │ │160 + // route named none), so they end up viewing the same one and either's │ │161 + // resolved ref can canonicalize the tab mirror urls below. no filesystem │ │162 + // (wasm), nowhere to look, or a failed open: empty tabs. │ │163 + const files, const commits, const refs, const issues = blk: { │ │164 + read: { │ │165 + const io = session.io orelse break :read; │ │166 + const src = source orelse break :read; │ │167 + const gpa = arena.child_allocator; │ │168 + switch (src.repo_kind) { │ │169 + inline else => |repo_kind| { │ │170 + var any_repo = rp.AnyRepo(repo_kind, .{}).open(io, gpa, .{ .path = src.path }) catch break :read; │ │171 + defer any_repo.deinit(io, gpa); │ │172 + switch (any_repo) { │ │173 + inline else => |*opened| break :blk .{ │ │174 + try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after),│ │175 + try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after), │ │176 + try Refs.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, rf.identity, refs_kind, refs_after), │ │177 + try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, rf.identity, issues_tag, issues_selected), │ │178 + }, │ │179 + } │ │180 + }, │ │181 + } │ │182 + } │ │183 + const aa = arena.allocator(); │ │184 + break :blk .{ │ │185 + try Files.emptyResult(aa, rf.identity, requested_ref_or_oid orelse .branch, requested_ref_value, files_dir), │ │186 + try Commits.emptyResult(aa, rf.identity, requested_ref_or_oid orelse .branch, requested_ref_value), │ │187 + try Refs.emptyResult(arena, rf.identity, refs_kind, refs_after), │ │188 + try Issues.emptyResult(aa, rf.identity, issues_tag, issues_selected), │ │189 + }; │ │190 + }; │ │191 │ │192 // each tab mirror carries this page's route for that tab; tabs not targeted │ │193 // by the incoming route fall back to their root/first-page route. the files │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │207 .repo = repo, │ │208 .files = files, │ │209 .commits = commits, │ │182 - .refs = try Refs.init(arena, session, source, rf.identity, refs_kind, refs_after), │ │183 - .issues = try Issues.init(arena, session, source, rf.identity, issues_tag, issues_selected),│ │210 + .refs = refs, │ │211 + .issues = issues, │ │212 .settings = Settings.init(), │ │213 .auth = Auth.init(), │ │214 .quit = Quit.init(), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │53 sub_header: SubHeader, │ │54 │ │55 const Self = @This(); │ │56 - │ │57 -pub fn init( │ │58 - arena: *std.heap.ArenaAllocator, │ │59 - session: *ui.Session, │ │60 - source_maybe: ?ui.RepoSource, │ │61 - identity: []const u8, │ │62 - requested_ref_or_oid: ?ui.RoutablePage.RefOrOid, │ │63 - requested_value: []const u8, │ │64 - // the hunk index the selected commit's (the first one, the walk root) diff │ │65 - // window starts at; 0 is the first window. moved by "next"/"previous". │ │66 - after: usize, │ │67 -) !Self { │ │68 - const aa = arena.allocator(); │ │56 │ │70 - // no filesystem (wasm) or nowhere to look: empty listing. the wasm path │ │71 - // never calls init anyway — it rebuilds from the serialized snapshot. │ │72 - const io = session.io orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value); │ │73 - const source = source_maybe orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value); │ │74 - │ │75 - // walk the log with the arena's backing allocator (transient; the commits │ │76 - // we keep are duped into the page arena so they outlive it). AnyRepo opens │ │77 - // both sha1 and sha256 repos. │ │78 - const gpa = arena.child_allocator; │ │79 - switch (source.repo_kind) { │ │80 - inline else => |repo_kind| { │ │81 - var any_repo = rp.AnyRepo(repo_kind, .{}).open(io, gpa, .{ .path = source.path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value);│ │82 - defer any_repo.deinit(io, gpa); │ │83 - │ │84 - return switch (any_repo) { │ │85 - inline else => |*repo| collect(repo_kind, repo.self_repo_opts, arena, repo, io, gpa, identity, requested_ref_or_oid, requested_value, after), │ │86 - }; │ │87 - }, │ │88 - } │ │89 -} │ │90 - │ │57 // walk the log for an opened repo. generic over the repo's backend and hash │ │58 // kind so the oid buffers and diff types it threads through match the repo's │ │93 -// opts. │ │94 -fn collect( │ │59 +// opts. `after` is the hunk index the selected commit's (the first one, the │ │60 +// walk root) diff window starts at; 0 is the first window, moved by │ │61 +// "next"/"previous". walks with the arena's backing allocator (transient; the │ │62 +// commits we keep are duped into the page arena so they outlive it). │ │63 +pub fn init( │ │64 comptime repo_kind: rp.RepoKind, │ │65 comptime repo_opts: rp.RepoOpts(repo_kind), │ │66 arena: *std.heap.ArenaAllocator, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 } │ │145 │ │146 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │178 -fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Self { │ │147 +pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Self {│ │148 return .{ │ │149 .identity = try aa.dupe(u8, identity), │ │150 .ref_or_oid = ref_or_oid, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 67 │ │ 68 const Self = @This(); │ │ 69 │ │ 70 -pub fn init( │ │ 71 - arena: *std.heap.ArenaAllocator, │ │ 72 - session: *ui.Session, │ │ 73 - source_maybe: ?ui.RepoSource, │ │ 74 - identity: []const u8, │ │ 75 - requested_ref_or_oid: ?ui.RoutablePage.RefOrOid, │ │ 76 - requested_value: []const u8, │ │ 77 - path: []const u8, │ │ 78 - // the line offset the selected file's content window starts at (0 = first). │ │ 79 - after: usize, │ │ 80 -) !Self { │ │ 81 - const aa = arena.allocator(); │ │ 82 - │ │ 83 - // no filesystem (wasm) or nowhere to look: empty listing pinned to whatever │ │ 84 - // ref the route asked for. the wasm path never calls init anyway — it │ │ 85 - // rebuilds from the serialized snapshot. │ │ 86 - const io = session.io orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path); │ │ 87 - const source = source_maybe orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path); │ │ 88 - │ │ 89 - // open + read the committed file list with the arena's backing allocator │ │ 90 - // (transient; freed before init returns); the listing is built into the │ │ 91 - // page arena so it outlives them. AnyRepo opens both sha1 and sha256 repos. │ │ 92 - const gpa = arena.child_allocator; │ │ 93 - switch (source.repo_kind) { │ │ 94 - inline else => |repo_kind| { │ │ 95 - var any_repo = rp.AnyRepo(repo_kind, .{}).open(io, gpa, .{ .path = source.path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path);│ │ 96 - defer any_repo.deinit(io, gpa); │ │ 97 - │ │ 98 - return switch (any_repo) { │ │ 99 - inline else => |*repo| listing(repo_kind, repo.self_repo_opts, arena, repo, io, gpa, identity, requested_ref_or_oid, requested_value, path, after), │ │100 - }; │ │101 - }, │ │102 - } │ │103 -} │ │104 - │ │ 70 // build the listing for an opened repo. generic over the repo's backend and │ │ 71 // hash kind so the tree/diff types it threads through match the repo's opts. │ │107 -fn listing( │ │ 72 +// reads with the arena's backing allocator (transient); the listing is built │ │ 73 +// into the page arena so it outlives the repo handle. │ │ 74 +pub fn init( │ │ 75 comptime repo_kind: rp.RepoKind, │ │ 76 comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 77 arena: *std.heap.ArenaAllocator, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │221 } │ │222 │ │223 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │257 -fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, ref_or_oid_value: []const u8, dir: []const u8) !Self { │ │224 +pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, ref_or_oid_value: []const u8, dir: []const u8) !Self {│ │225 return .{ │ │226 .identity = try aa.dupe(u8, identity), │ │227 .ref_or_oid = ref_or_oid, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯