╭ changed ──────╮ │src/command.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────╮ │216 }, │ │217 }, │ │218 help: ?CommandKind, │ │219 + local, │ │220 cli: Command, │ │221 │ │222 pub fn init(cmd_args: *CommandArgs) !CommandDispatch {│ ╰───────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │228 .command = switch (dispatch) { │ │229 .invalid => return dispatch, // if there was already an error, return it instead│ │230 .help => |cmd_kind_maybe| cmd_kind_maybe, │ │231 + .local => null, │ │232 .cli => |command| command, │ │233 }, │ │234 .value = cmd_args.unused_args.keys()[0], │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │252 } │ │253 } else if (cmd_args.command_name) |command_name| { │ │254 return .{ .invalid = .{ .command = command_name } };│ │253 - } else { │ │255 + } else if (show_help) { │ │256 return .{ .help = null }; │ │257 + } else { │ │258 + return .local; │ │259 } │ │260 } │ │261 }; │ ╰─────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/main.zig│ ╰────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │3 const rp = xit.repo; │ │4 const cmd = @import("./command.zig"); │ │5 const srv = @import("./serve.zig"); │ │6 +const ui = @import("./ui.zig"); │ │7 │ │8 // cook the terminal before a panic/segfault trace is printed, so the trace│ │9 // isn't mangled by raw mode and the alternate buffer │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 74 }, │ │ 75 }, │ │ 76 .help => |cmd_kind_maybe| try cmd.printHelp(cmd_kind_maybe, run_opts.out), │ │ 77 + .local => { │ │ 78 + // session-lifetime allocations (the repo path, session state) │ │ 79 + var session_arena = std.heap.ArenaAllocator.init(allocator); │ │ 80 + defer session_arena.deinit(); │ │ 81 + │ │ 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, │ │ 95 + } │ │ 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 }; │ │108 + }; │ │109 + │ │110 + // every local route internally carries this synthetic │ │111 + // "owner/name"; urls elide it. │ │112 + const identity = try std.fmt.allocPrint(session_arena.allocator(), "local/{s}", .{std.fs.path.basename(local.path)});│ │113 + const route = ui.RoutablePage.repoFilesRoute(identity, null, "", "", 0) orelse { │ │114 + try run_opts.err.print("repo directory name is too long\n", .{}); │ │115 + return error.HandledError; │ │116 + }; │ │117 + │ │118 + var session = ui.Session{ │ │119 + .arena = &session_arena, │ │120 + .page_arena = &session_arena, │ │121 + .io = io, │ │122 + .local = local, │ │123 + .is_terminal = true, │ │124 + .data = .{ .current_page = route }, │ │125 + }; │ │126 + │ │127 + try ui.run(io, allocator, &session, null); │ │128 + }, │ │129 .cli => |cli_cmd| switch (cli_cmd) { │ │130 .serve => |options| try srv.run(repo_kind, any_repo_opts, io, allocator, cwd_path, options, run_opts.err, {}), │ │131 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │31 repo: Repo, │ │32 │ │33 pub fn init(arena: *std.heap.ArenaAllocator, session: *Session, route: RoutablePage) !Page { │ │34 - const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │34 + // the repo page can build without a moment in local mode; the home and │ │35 + // user pages always read from the admin db. │ │36 return switch (route.parent()) { │ │36 - .home => .{ .home = try Home.init(arena, haxy_moment, switch (route) { │ │37 + .home => .{ .home = try Home.init(arena, session.haxy_moment orelse return error.NoMoment, switch (route) {│ │38 .home_users => |after| after, │ │39 else => 0, │ │40 }, switch (route) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │42 else => 0, │ │43 }) }, │ │44 .user => switch (route) { │ │44 - .user_repos => |u| .{ .user = try User.init(arena, haxy_moment, u.name, u.after) }, │ │45 - .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name, 0) }, │ │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, │ │48 }, │ │49 .repo => switch (route) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │269 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │270 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │271 }; │ │272 + } │ │273 + │ │274 + // the url with its leading "/repo/<owner>/<name>" elided, for local mode │ │275 + // (every url there points at the one local repo). non-repo routes serialize │ │276 + // in full. │ │277 + pub fn urlAllocLocal(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │278 + const full = try self.urlAlloc(arena); │ │279 + const name_str: []const u8 = switch (self) { │ │280 + .repo_files => |f| f.name.slice(), │ │281 + .repo_commits => |c| c.name.slice(), │ │282 + .repo_refs => |r| r.name.slice(), │ │283 + .repo_issues => |i| i.name.slice(), │ │284 + .repo_settings, .repo_auth => |n| n.slice(), │ │285 + else => return full, │ │286 + }; │ │287 + const identity = (RepoFiles.parse(name_str) orelse return full).identity; │ │288 + const rest = full[repo_segment.len + identity.len ..]; │ │289 + if (rest.len == 0) return "/"; │ │290 + if (rest[0] == '/') return rest; │ │291 + return try std.fmt.allocPrint(arena.allocator(), "/{s}", .{rest}); │ │292 } │ │293 │ │294 pub fn parent(self: RoutablePage) PageKind { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │584 return out.written(); │ │585 } │ │586 │ │566 -// a resolved ref/oid for a repo opened with `repo_opts` │ │567 -pub fn ResolvedRefOrOid(comptime repo_opts: rp.RepoOpts(.xit)) type { │ │587 +// a resolved ref/oid for a repo opened with `repo_kind`/`repo_opts` │ │588 +pub fn ResolvedRefOrOid(comptime repo_kind: rp.RepoKind, comptime repo_opts: rp.RepoOpts(repo_kind)) type {│ │589 return struct { │ │590 pub const hex_len = hash.hexLen(repo_opts.hash); │ │591 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │605 // the url-encoded form (as it appears in the route). null when it doesn't│ │606 // resolve (an unknown branch/tag, or a malformed/unknown oid). │ │607 pub fn init( │ │587 - repo: *rp.Repo(.xit, repo_opts), │ │608 + repo: *rp.Repo(repo_kind, repo_opts), │ │609 io: std.Io, │ │610 aa: std.mem.Allocator, │ │611 requested_ref_or_oid: ?RoutablePage.RefOrOid, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯