╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │331 try stack.children.put(allocator, files_view.getFocus().id, .{ .repo_files = files_view }); │ │332 } │ │333 │ │320 - // commits — the current page of the commit log. │ │321 - { │ │322 - var commits_view = try Commits.View.init(allocator, &data.commits, session); │ │323 - errdefer commits_view.deinit(allocator); │ │324 - try stack.children.put(allocator, commits_view.getFocus().id, .{ .repo_commits = commits_view }); │ │334 + switch (data.changes) { │ │335 + .diff => |*diff| { │ │336 + var diff_view = try Diff.View.init(allocator, diff, session); │ │337 + errdefer diff_view.deinit(allocator); │ │338 + try stack.children.put(allocator, diff_view.getFocus().id, .{ .diff_view = diff_view }); │ │339 + }, │ │340 + .commits => |*commits| { │ │341 + var commits_view = try Commits.View.init(allocator, commits, session); │ │342 + errdefer commits_view.deinit(allocator); │ │343 + try stack.children.put(allocator, commits_view.getFocus().id, .{ .repo_commits = commits_view });│ │344 + }, │ │345 } │ │346 │ │347 // refs — the repo's branches and tags. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────╮ │ 4 const ui = @import("../../ui.zig"); │ │ 5 const xit = @import("xit"); │ │ 6 const rp = xit.repo; │ │ 7 -pub const Diff = @import("Diff.zig");│ │ 7 +const Diff = @import("Diff.zig"); │ │ 8 const xitui = xit.xitui; │ │ 9 const wgt = xitui.widget; │ │10 const layout = xitui.layout; │ ╰─────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Diff.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 +const evt = @import("../../event.zig");│ │3 const ui = @import("../../ui.zig"); │ │4 const xit = @import("xit"); │ │5 const rp = xit.repo; │ ╰──────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │29 │ │30 pub const Route = union(enum) { │ │31 commit: struct { location: ui.RoutablePage.RepoLocation, oid: []const u8, base_oid: []const u8 = "" }, │ │32 + repo: struct { identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8, base_oid: []const u8 },│ │33 + patchrev: struct { identity: []const u8, id: []const u8 }, │ │34 fork: struct { identity: []const u8, id: []const u8 }, │ │35 │ │33 - fn link(self: Route, arena: *std.heap.ArenaAllocator, start: usize, path: []const u8) ![]const u8 { │ │34 - const route = switch (self) { │ │36 + pub fn page(self: Route, start: usize, path: []const u8) ?ui.RoutablePage { │ │37 + return switch (self) { │ │38 .commit => |c| c.location.commitsRoute(.object, c.oid, start, path, c.base_oid), │ │39 + .repo => |r| ui.RoutablePage.repoDiffRoute(r.identity, r.ref_or_oid, r.value, start, path, r.base_oid), │ │40 + .patchrev => |r| ui.RoutablePage.repoPatchRevDiffRoute(r.identity, r.id, start, path), │ │41 .fork => |f| ui.RoutablePage.forkDiffRoute(f.identity, f.id, start, path), │ │37 - } orelse return error.RouteTooLong; │ │42 + }; │ │43 + } │ │44 + │ │45 + fn link(self: Route, arena: *std.heap.ArenaAllocator, start: usize, path: []const u8) ![]const u8 { │ │46 + const route = self.page(start, path) orelse return error.RouteTooLong; │ │47 return std.fmt.allocPrint(arena.allocator(), "a:{s}", .{try route.toUrl(arena)}); │ │48 } │ │49 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 50 │ │ 42 -// render a window of the net diff between two commits │ │ 51 +pub fn init( │ │ 52 + comptime repo_kind: rp.RepoKind, │ │ 53 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 54 + arena: *std.heap.ArenaAllocator, │ │ 55 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 56 + io: std.Io, │ │ 57 + gpa: std.mem.Allocator, │ │ 58 + route: ui.RoutablePage.RepoDiffRoute, │ │ 59 +) !Self { │ │ 60 + const aa = arena.allocator(); │ │ 61 + const oid_len = comptime xit.hash.hexLen(repo_opts.hash); │ │ 62 + const identity = try aa.dupe(u8, route.name.slice()); │ │ 63 + const base_oid, const head_oid, const diff_route = if (route.patchrev_id.len != 0) blk: { │ │ 64 + const id = evt.parseEventId(route.patchrev_id.slice()) catch return error.NotFound; │ │ 65 + const record = (try evt.readFromRepo(evt.PatchRev, repo_kind, repo_opts, io, gpa, arena, repo, &id)) orelse return error.NotFound; │ │ 66 + if (record.removed) return error.NotFound; │ │ 67 + if (record.base_tree_oid.len != oid_len or record.head_tree_oid.len != oid_len) return error.NotFound; │ │ 68 + break :blk .{ │ │ 69 + record.base_tree_oid[0..oid_len].*, │ │ 70 + record.head_tree_oid[0..oid_len].*, │ │ 71 + Route{ .patchrev = .{ .identity = identity, .id = try aa.dupe(u8, route.patchrev_id.slice()) } }, │ │ 72 + }; │ │ 73 + } else blk: { │ │ 74 + const resolved = (try ui.ResolvedRefOrOid(repo_kind, repo_opts).init(repo, io, aa, route.ref_or_oid, route.value.slice())) orelse return error.NotFound; │ │ 75 + evt.PatchRev.validateOid(repo_opts.hash, &resolved.oid) catch return error.NotFound; │ │ 76 + evt.PatchRev.validateOid(repo_opts.hash, route.base_oid.slice()) catch return error.NotFound; │ │ 77 + │ │ 78 + var moment = try repo.core.latestMoment(); │ │ 79 + const state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │ 80 + // peel annotated tags before comparing their trees │ │ 81 + var tip = try xit.object.Object(repo_kind, repo_opts).initCommit(state, io, gpa, &resolved.oid); │ │ 82 + defer tip.deinit(); │ │ 83 + break :blk .{ │ │ 84 + route.base_oid.slice()[0..oid_len].*, │ │ 85 + tip.oid, │ │ 86 + Route{ .repo = .{ │ │ 87 + .identity = identity, │ │ 88 + .ref_or_oid = resolved.ref_or_oid, │ │ 89 + .value = resolved.value, │ │ 90 + .base_oid = try aa.dupe(u8, route.base_oid.slice()), │ │ 91 + } }, │ │ 92 + }; │ │ 93 + }; │ │ 94 + const path = try aa.dupe(u8, route.path.slice()); │ │ 95 + return .{ │ │ 96 + .route = diff_route, │ │ 97 + .path = path, │ │ 98 + .window = try render(repo_kind, repo_opts, io, gpa, aa, repo, if (std.mem.allEqual(u8, &base_oid, '0')) null else &base_oid, head_oid, route.start, path),│ │ 99 + }; │ │100 +} │ │101 + │ │102 +// render a window of the net diff between two commits or trees │ │103 pub fn render( │ │104 comptime repo_kind: rp.RepoKind, │ │105 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │46 // didn't name one), so the view's directory links stay pinned to it. │ │47 ref_or_oid: ui.RoutablePage.RefOrOid, │ │48 ref_or_oid_value: []const u8, │ │49 +patchrev_id: ?[]const u8 = null, │ │50 // the directory being viewed, relative to the repo root ("" at the root).│ │51 dir: []const u8, │ │52 // a file within `dir` to start selected (so its contents show and the url│ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │58 header: Header, │ │59 │ │60 const Self = @This(); │ │61 + │ │62 +pub fn initPatchRev( │ │63 + comptime repo_kind: rp.RepoKind, │ │64 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │65 + arena: *std.heap.ArenaAllocator, │ │66 + repo: *rp.Repo(repo_kind, repo_opts), │ │67 + io: std.Io, │ │68 + gpa: std.mem.Allocator, │ │69 + location: ui.RoutablePage.RepoLocation, │ │70 + id_hex: []const u8, │ │71 + path: []const u8, │ │72 + line: usize, │ │73 +) !Self { │ │74 + const id = evt.parseEventId(id_hex) catch return error.NotFound; │ │75 + const record = (try evt.readFromRepo(evt.PatchRev, repo_kind, repo_opts, io, gpa, arena, repo, &id)) orelse return error.NotFound;│ │76 + if (record.removed) return error.NotFound; │ │77 + var data = try init(repo_kind, repo_opts, arena, repo, io, gpa, location, .object, record.head_tree_oid, path, line); │ │78 + data.patchrev_id = try arena.allocator().dupe(u8, id_hex); │ │79 + data.header.content = try std.fmt.allocPrint(arena.allocator(), "patchrev {s}", .{id_hex}); │ │80 + return data; │ │81 +} │ │82 + │ │83 +pub fn filesRoute(self: *const Self, path: []const u8, line: usize) ?ui.RoutablePage { │ │84 + if (self.patchrev_id) |id| return switch (self.location) { │ │85 + .repo => |identity| ui.RoutablePage.repoPatchRevFilesRoute(identity, id, path, line), │ │86 + .fork => null, │ │87 + }; │ │88 + return self.location.filesRoute(self.ref_or_oid, self.ref_or_oid_value, path, line); │ │89 +} │ │90 │ │91 // build the listing for an opened repo. generic over the repo's backend and │ │92 // hash kind so the tree/diff types it threads through match the repo's opts. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 ) !Self { │ │108 const aa = arena.allocator(); │ │109 │ │ 80 - // resolve the requested ref (or the default branch) to the commit oid whose │ │110 + // resolve the requested ref (or the default branch) to the object oid whose │ │111 // tree we list. a ref the route named explicitly that doesn't resolve is a │ │112 // bad url (NotFound -> 404); the default-branch path falls through to empty. │ │113 const resolved = (try ui.ResolvedRefOrOid(repo_kind, repo_opts).init(repo, io, aa, requested_ref_or_oid, requested_value)) orelse {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │115 return emptyResult(aa, location, .branch, requested_value, path); │ │116 }; │ │117 │ │ 88 - // read just the viewed directory of that commit's tree. building the │ │118 + // read just the viewed directory of that tree or commit. building the │ │119 // read-only state mirrors what repo.status does internally, but for an │ │120 // arbitrary commit rather than HEAD. a path that doesn't exist in the tree│ │121 // is a bad url (404). │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │592 fn addNavLink(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, entry: Entry, target_start: usize) !void { │ │593 const page_arena = self.session.page_arena; │ │594 const path = try childDir(page_arena.allocator(), self.data.dir, entry.name); │ │565 - const route = self.data.location.filesRoute(self.data.ref_or_oid, self.data.ref_or_oid_value, path, target_start) orelse return error.RouteTooLong;│ │595 + const route = self.data.filesRoute(path, target_start) orelse return error.RouteTooLong; │ │596 const link = try std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{try route.toUrl(page_arena)}); │ │597 var tb = try wgt.TextBox.init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │598 errdefer tb.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯