Improve toolbar review layout.

This commit is contained in:
Greyson Parrelli
2026-08-25 19:51:42 -04:00
parent 44619293c3
commit 0fd0aa44c7
11 changed files with 590 additions and 60 deletions
+52 -6
View File
@@ -608,6 +608,9 @@ fn handleApi(
if (std.mem.eql(u8, path, "diff")) {
return self.handleDiff(a, request, repo, tab, query);
}
if (std.mem.eql(u8, path, "diff/revision")) {
return self.handleRevision(a, request, repo, tab, query);
}
if (std.mem.eql(u8, path, "file")) {
return self.handleFile(a, request, repo, query);
}
@@ -709,6 +712,21 @@ fn clampCopy(buf: []u8, value: []const u8) []const u8 {
return buf[0..n];
}
/// The diff selection a request names, falling back to the one the tab last
/// published for anything it leaves out.
fn queryContext(
a: std.mem.Allocator,
query: []const u8,
tab: Resolved,
) !model.DiffContext {
return .{
.base = try queryValue(a, query, "base") orelse
(if (tab.ctx) |c| c.base else "HEAD"),
.uncommitted = queryFlag(a, query, "uncommitted") catch false,
.commit = try queryValue(a, query, "commit") orelse "",
};
}
fn handleDiff(
self: *Server,
a: std.mem.Allocator,
@@ -717,12 +735,7 @@ fn handleDiff(
tab: Resolved,
query: []const u8,
) !void {
const ctx: model.DiffContext = .{
.base = try queryValue(a, query, "base") orelse
(if (tab.ctx) |c| c.base else "HEAD"),
.uncommitted = queryFlag(a, query, "uncommitted") catch false,
.commit = try queryValue(a, query, "commit") orelse "",
};
const ctx = try queryContext(a, query, tab);
const opts: git.Options = .{
.force = queryFlag(a, query, "force") catch false,
.ignore_whitespace = queryFlag(a, query, "ignoreWhitespace") catch false,
@@ -740,6 +753,39 @@ fn handleDiff(
return writeJson(a, request, .ok, payload);
}
/// A digest of what the selection resolves to *now*, for a page holding the one
/// that came with the diff it is showing. Answering "has this moved?" without
/// shipping the patch again is the whole reason it exists: the page polls this
/// and raises a banner, rather than reloading the diff under whoever is reading
/// it. See `git.revision`.
fn handleRevision(
self: *Server,
a: std.mem.Allocator,
request: *std.http.Server.Request,
repo: git.Repo,
tab: Resolved,
query: []const u8,
) !void {
const ctx = try queryContext(a, query, tab);
const opts: git.Options = .{
// Forced, because the answer has to be comparable with the revision the
// page already has — which, if it got here, is one it was given.
.force = true,
.ignore_whitespace = queryFlag(a, query, "ignoreWhitespace") catch false,
};
const rev = git.revision(repo, a, self.io, ctx, opts) catch |err| return writeError(
a,
request,
if (err == error.BadCommit) .bad_request else .bad_gateway,
switch (err) {
error.BadCommit => "commit is not a sha",
else => "git could not produce that diff — check the base ref",
},
);
return writeJson(a, request, .ok, .{ .revision = rev });
}
fn handleFile(
self: *Server,
a: std.mem.Allocator,
+52
View File
@@ -339,9 +339,61 @@ pub fn diff(
.files = files,
.commits = listed.items,
.moreCommits = listed.more,
.revision = try digest(repo, gpa, io, patch),
};
}
/// A digest of what a selection resolves to right now, for noticing that the
/// diff already on someone's screen has been overtaken.
///
/// Cheap enough to ask for on a timer — the answer is one hash — which is the
/// point: a review pane that reloaded itself every time an agent saved a file
/// would move the diff out from under whoever is reading it, so the page polls
/// this instead and says so.
///
/// The oversize guard is deliberately not applied. This is asked for by a page
/// that already has a patch on screen, so the answer has to be comparable with
/// the `revision` that came with it, guard or no guard.
pub fn revision(
repo: Repo,
gpa: std.mem.Allocator,
io: std.Io,
ctx: model.DiffContext,
opts: Options,
) Error![]const u8 {
try validateCommit(ctx.commit);
const patch = try run(gpa, io, repo.path, try diffArgs(gpa, ctx, opts, &.{
"--no-color",
"--find-renames",
}));
return digest(repo, gpa, io, patch);
}
/// The digest itself, given a patch already in hand: the patch, hashed, plus
/// HEAD.
///
/// Both halves are needed. The patch alone misses work being *committed*, which
/// leaves `base`..worktree byte for byte the same while the commit list beside
/// it grows an entry. HEAD alone misses every edit that hasn't been committed,
/// which is most of what a review watches for.
///
/// Nothing here is adversarial — a collision costs one banner that never
/// appears — so a 64-bit non-cryptographic hash is plenty.
fn digest(
repo: Repo,
gpa: std.mem.Allocator,
io: std.Io,
patch: []const u8,
) Error![]const u8 {
// Best-effort: a repository with no commits has no HEAD to resolve, and the
// patch on its own is still a usable digest.
const head = run(gpa, io, repo.path, &.{ "rev-parse", "HEAD" }) catch "";
var hasher = std.hash.Wyhash.init(0);
hasher.update(trim(head));
hasher.update(patch);
return std.fmt.allocPrint(gpa, "{x}", .{hasher.final()});
}
/// Whether a change set is past what the UI can render at once. Binary files
/// count for no lines, hence the file cap alongside the line one.
fn oversized(files: []const model.DiffFile) bool {
+9
View File
@@ -137,6 +137,15 @@ pub const DiffPayload = struct {
/// `files` is still filled in, so the caller can say how big it is and
/// offer to load it anyway with `force`.
oversized: bool = false,
/// A digest of what this selection resolved to when the patch was produced
/// — see `git.revision`. The page keeps it and asks `GET api/diff/revision`
/// for the current one every so often, which is how it can say the diff on
/// screen has gone stale without reloading it under the reader.
///
/// Empty when the patch was withheld, since there is then nothing on screen
/// to go stale.
revision: []const u8 = "",
};
/// What `GET api/repo` returns for a tab that has a review open.