Improve toolbar review layout.
This commit is contained in:
+52
-6
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user