╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#33d6feef019b1cbef6ef4a564437a05f53d66831",│ │ 7 - .hash = "xit-0.0.0-Ub3nDKXiTwGfl548rVnm4yhEw9MqGslV5qfV7ufEzyl5", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#54f8100b0384633cb6c808bd2489220800d0c3b0",│ │ 7 + .hash = "xit-0.0.0-Ub3nDKXiTwGzogIxqPU6NrVccrxsaSYFu6OAolvrXLvd", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │202 │ │203 try repo_i.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); │ │204 _ = try repo_i.commit(io, allocator, .{ .message = "let there be light" }); │ │205 + │ │206 + // a batch of empty commits so the commits tab has more than one page │ │207 + // to paginate through. stepped timestamps vary the date column. │ │208 + const base_ts: u64 = 1_700_000_000; // 2023-11-14 │ │209 + var c: usize = 0; │ │210 + while (c < 30) : (c += 1) { │ │211 + const message = try std.fmt.allocPrint(arena.allocator(), "empty commit {d}", .{c + 1}); │ │212 + _ = try repo_i.commit(io, allocator, .{ .message = message, .allow_empty = true, .timestamp = base_ts + c * std.time.s_per_day });│ │213 + } │ │214 } │ │215 │ │216 break :blk try ui.Session.init(&session_arena, &repo, .{}); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │36 else => return error.UnexpectedRoute, │ │37 }, │ │38 .repo => switch (route) { │ │39 - .repo, .repo_settings, .repo_auth => |name| .{ .repo = try Repo.init(arena, session, name) }, │ │39 + .repo, .repo_commits, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ │40 else => return error.UnexpectedRoute, │ │41 }, │ │42 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │63 user_settings: Array(evt.User.name_max_len),│ │64 user_auth: Array(evt.User.name_max_len), │ │65 repo: Array(repo_route_max_len), │ │66 + repo_commits: Array(repo_route_max_len), │ │67 repo_settings: Array(repo_route_max_len), │ │68 repo_auth: Array(repo_route_max_len), │ │69 │ ╰────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │72 const user_segment = "/user/"; │ │73 const repo_segment = "/repo/"; │ │74 const files_seg = "files"; │ │75 + const commits_seg = "commits"; │ │76 │ │77 // a repo route is "username/reponame", optionally followed by│ │78 // "/files/<dir>" for a directory in the files tab │ ╰──────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │110 var buf: [repo_route_max_len]u8 = undefined; │ │111 const s = std.fmt.bufPrint(&buf, "{s}/" ++ files_seg ++ "/{s}", .{ identity, dir }) catch return null; │ │112 return .{ .repo = Array(repo_route_max_len).from(s) orelse return null }; │ │113 + } │ │114 + │ │115 + // the start oid of a `.repo_commits` route's stored string ("" = page 1, │ │116 + // which the commits tab walks from HEAD). │ │117 + pub fn repoCommitsStart(s: []const u8) []const u8 { │ │118 + const marker = "/" ++ commits_seg ++ "/"; │ │119 + const i = std.mem.indexOf(u8, s, marker) orelse return ""; │ │120 + return s[i + marker.len ..]; │ │121 + } │ │122 + │ │123 + // build a `.repo_commits` route for "owner/name" (identity) at `start_oid` │ │124 + // ("" = the first page). always carries the `commits` marker so page 1 │ │125 + // ("owner/name/commits") doesn't collide with the files root. │ │126 + pub fn repoCommitsRoute(identity: []const u8, start_oid: []const u8) ?RoutablePage { │ │127 + var buf: [repo_route_max_len]u8 = undefined; │ │128 + const s = if (start_oid.len == 0) │ │129 + std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg, .{identity}) catch return null │ │130 + else │ │131 + std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg ++ "/{s}", .{ identity, start_oid }) catch return null;│ │132 + return .{ .repo_commits = Array(repo_route_max_len).from(s) orelse return null }; │ │133 } │ │134 │ │135 // an inline, owned array of data. keeping it in the route (rather than a │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │160 .home_settings => "/settings", │ │161 .home_auth => "/auth", │ │162 .user, .user_settings, .user_auth => @compileError("user routes are dynamic; use urlAlloc"), │ │141 - .repo, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"), │ │163 + .repo, .repo_commits, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"),│ │164 }; │ │165 } │ │166 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}),│ │171 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │172 .repo => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │173 + // the stored string already carries the "commits[/oid]" suffix. │ │174 + .repo_commits => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │175 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │176 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │177 inline else => |_, tag| url(tag), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │182 return switch (self) { │ │183 .home_users, .home_repos, .home_settings, .home_auth => .home,│ │184 .user, .user_settings, .user_auth => .user, │ │161 - .repo, .repo_settings, .repo_auth => .repo, │ │185 + .repo, .repo_commits, .repo_settings, .repo_auth => .repo, │ │186 }; │ │187 } │ │188 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │226 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null };│ │227 if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, ""); // trailing /files == root │ │228 if (std.mem.startsWith(u8, sub, files_seg ++ "/")) return repoFilesRoute(pair, sub[files_seg.len + 1 ..]); │ │229 + if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, ""); // trailing /commits == page 1 │ │230 + if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) return repoCommitsRoute(pair, sub[commits_seg.len + 1 ..]); │ │231 return null; // unknown sub-path │ │232 } │ │233 return repoFilesRoute(pair, ""); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯