╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/search_commit.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │257 const diff = try commitDiff(repo_opts, state.readOnly(), io, allocator, diff_arena.allocator(), if (base) |*base_oid| base_oid else null, &item.oid, progress_ctx_maybe);│ │258 │ │259 // the walk is done, so the messages left to read are the total │ │259 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing commits" }); │ │260 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = diff.removed.len + diff.added.len } }); │ │260 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing commits" }); │ │261 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = diff.removed.len + diff.added.len } }); │ │262 for (diff.removed) |commit| { │ │262 - if (serve_common.progressCancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; │ │263 - defer serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); │ │263 + if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; │ │264 + defer progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); │ │265 const key = try readPosting(repo_opts, state.readOnly(), io, allocator, &message, commit); │ │266 try srch.remove(DB, version, allocator, &key, message.items); │ │267 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │268 for (diff.added) |commit| { │ │268 - if (serve_common.progressCancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; │ │269 - defer serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch });│ │269 + if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; │ │270 + defer progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); │ │271 const key = try readPosting(repo_opts, state.readOnly(), io, allocator, &message, commit); │ │272 try srch.add(DB, version, allocator, &key, message.items); │ │273 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │273 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch });│ │274 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ │275 │ │276 try versions.append(aa, .{ .oid = item.oid, .timestamp = item.timestamp }); │ │277 // the next version copies this one's slot, so everything │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │399 } else { │ │400 // a whole history has no count until it is walked, so the walk reports │ │401 // the commits it has reached so far │ │401 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .text = "Walking commits" }); │ │402 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 0 } });│ │402 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Walking commits" }); │ │403 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 0 } }); │ │404 var iter = try obj.ObjectIterator(.xit, repo_opts).init(state, io, allocator, .{ .kind = .commit }); │ │405 defer iter.deinit(); │ │406 try iter.include(tip); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │407 while (try iter.next(allocator)) |object| { │ │408 defer object.deinit(); │ │409 try added.append(aa, .{ .oid = object.oid, .timestamp = object.content.commit.metadata.timestamp });│ │409 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch });│ │410 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); │ │411 } │ │411 - serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ │412 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ │413 } │ │414 │ │415 return .{ .removed = removed.items, .added = added.items }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/serve_common.zig│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 1 const std = @import("std"); │ │ 2 const evt = @import("./event.zig"); │ │ 3 -const xit = @import("xit"); │ │ 4 -const rp = xit.repo; │ │ 5 -const ssh = @import("./serve_ssh_protocol.zig"); │ │ 6 - │ │ 7 -// progress is optional and best effort: a failed report must not fail the work │ │ 8 -pub fn reportProgress(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, progress_ctx_maybe: ?repo_opts.ProgressCtx, event: rp.ProgressEvent) void {│ │ 9 - if (repo_opts.ProgressCtx != void) { │ │ 10 - if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, event) catch {}; │ │ 11 - } │ │ 12 -} │ │ 13 - │ │ 14 -// progress for a push or a clone, sent on the pack stream's sideband │ │ 15 -pub const SidebandProgress = struct { │ │ 16 - // a push reports through its deferred response, which knows whether the │ │ 17 - // client wanted sideband at all. a clone has none and writes band 2 itself. │ │ 18 - response: ?*xit.net_server_receive_pack.Response = null, │ │ 19 - writer: *std.Io.Writer, │ │ 20 - // the client to probe, absent when nothing is listening │ │ 21 - sess: ?*ssh.SessionCtx = null, │ │ 22 - // set once it has hung up, so the phase in flight can stop too │ │ 23 - gone: bool = false, │ │ 24 - label: []const u8 = "Processing commits", │ │ 25 - count: usize = 0, │ │ 26 - total: usize = 0, │ │ 27 - // the last reported step, so a line is sent only when it changes │ │ 28 - step: usize = 0, │ │ 29 - │ │ 30 - // how many items a phase with no total advances between reports │ │ 31 - const report_every = 1000; │ │ 32 - │ │ 33 - // the phases a client is told about, listed so a new kind has to be │ │ 34 - // decided on rather than inherit whatever label was last set │ │ 35 - fn reported(kind: rp.ProgressKind) bool { │ │ 36 - return switch (kind) { │ │ 37 - .writing_object_from_pack, .enumerating_object, .compressing_object, .writing_patch => true, │ │ 38 - .writing_object, .sending_bytes, .receiving_bytes => false, │ │ 39 - }; │ │ 40 - } │ │ 41 - │ │ 42 - pub fn run(self: *SidebandProgress, _: std.Io, event: rp.ProgressEvent) !void { │ │ 43 - switch (event) { │ │ 44 - .start => |start| { │ │ 45 - if (!reported(start.kind)) return; │ │ 46 - // a pack phase carries no label of its own; the rest send a │ │ 47 - // .text first │ │ 48 - switch (start.kind) { │ │ 49 - .writing_object_from_pack => self.label = "Unpacking objects", │ │ 50 - .enumerating_object => self.label = "Enumerating objects", │ │ 51 - .compressing_object => self.label = "Compressing objects", │ │ 52 - else => {}, │ │ 53 - } │ │ 54 - self.total = start.estimated_total_items; │ │ 55 - self.count = 0; │ │ 56 - self.step = 0; │ │ 57 - }, │ │ 58 - .complete_one => |kind| { │ │ 59 - if (!reported(kind)) return; │ │ 60 - self.count += 1; │ │ 61 - }, │ │ 62 - .end => |kind| { │ │ 63 - if (!reported(kind)) return; │ │ 64 - }, │ │ 65 - // each phase names itself before reporting its count │ │ 66 - .text => |text| { │ │ 67 - self.label = text; │ │ 68 - return; │ │ 69 - }, │ │ 70 - else => return, │ │ 71 - } │ │ 72 - // a phase that knows no total counts instead, and stays quiet until it │ │ 73 - // has something to count │ │ 74 - if (self.total == 0 and self.count == 0) return; │ │ 75 - │ │ 76 - // every report flushes, so one is sent per percent, or per block of │ │ 77 - // items when there is no total to turn into one │ │ 78 - const step: usize = if (self.total > 0) @intCast(@as(u128, self.count) * 100 / self.total) else self.count / report_every; │ │ 79 - if (event == .complete_one and step == self.step) return; │ │ 80 - self.step = step; │ │ 81 - │ │ 82 - // reporting is the only moment a transaction looks up from its work │ │ 83 - if (self.sess) |sess| if (sess.peerGone()) { │ │ 84 - self.gone = true; │ │ 85 - return error.ClientGone; │ │ 86 - }; │ │ 3 +const rp = @import("xit").repo; │ │ 4 │ │ 88 - var buffer: [128]u8 = undefined; │ │ 89 - const suffix = if (event == .end) "\n" else "\r"; │ │ 90 - const text = if (self.total > 0) │ │ 91 - try std.fmt.bufPrint(&buffer, "{s}: {d}% ({d}/{d}){s}", .{ self.label, step, self.count, self.total, suffix }) │ │ 92 - else │ │ 93 - try std.fmt.bufPrint(&buffer, "{s}: {d}{s}", .{ self.label, self.count, suffix }); │ │ 94 - if (self.response) |response| { │ │ 95 - try response.progress(self.writer, text); │ │ 96 - } else { │ │ 97 - try xit.net_server_pkt.sendSideband(self.writer, 2, text); │ │ 98 - try self.writer.flush(); │ │ 99 - } │ │100 - } │ │101 -}; │ │102 - │ │103 -// whether the client the work is reporting to has hung up. only a sideband │ │104 -// reporter probes, so any other context never cancels. │ │105 -pub fn progressCancelled(comptime repo_opts: rp.RepoOpts(.xit), progress_ctx_maybe: ?repo_opts.ProgressCtx) bool { │ │106 - if (repo_opts.ProgressCtx != *SidebandProgress) return false; │ │107 - const progress_ctx = progress_ctx_maybe orelse return false; │ │108 - return progress_ctx.gone; │ │109 -} │ │110 - │ │ 5 // the outcome of resolving a requested repo path to its on-disk directory. │ │ 6 // the http and ssh paths each map the cases to their own error responses. │ │ 7 pub const RepoPath = union(enum) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │10 const evt = @import("./event.zig"); │ │11 const serve_common = @import("./serve_common.zig"); │ │12 const fork = @import("./fork.zig"); │ │13 +const progress = @import("./progress.zig"); │ │14 │ │15 // listener resource limits. the watchdog gives peers a hard deadline to start │ │16 // a session, times out non-interactive sessions that stall while we're blocked│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │24 │ │25 var active_connections: std.atomic.Value(u32) = .init(0); │ │26 │ │26 -const any_repo_opts: rp.AnyRepoOpts(.xit) = .{ .ProgressCtx = *serve_common.SidebandProgress };│ │27 +const any_repo_opts: rp.AnyRepoOpts(.xit) = .{ .ProgressCtx = *progress.Sideband }; │ │28 │ │29 pub const SessionHandler = struct { │ │30 admin_repo_path: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │443 return writeError(sess, "patch draft not found"); │ │444 defer draft.deinit(io, allocator); │ │445 if (service == .upload_pack) { │ │445 - var progress = serve_common.SidebandProgress{ .writer = writer, .sess = sess }; │ │446 + var sideband = progress.Sideband{ .writer = writer, .sess = sess }; │ │447 switch (draft) { │ │447 - inline else => |*repo| try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &progress), │ │448 + inline else => |*repo| try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband), │ │449 } │ │450 } else { │ │451 const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, owner_repo.owner, owner_repo.name)) orelse│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │526 ) !void { │ │527 switch (service) { │ │528 .upload_pack => { │ │528 - var progress = serve_common.SidebandProgress{ .writer = writer, .sess = sess }; │ │529 - try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &progress); │ │529 + var sideband = progress.Sideband{ .writer = writer, .sess = sess }; │ │530 + try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband); │ │531 }, │ │532 .receive_pack => try push.receivePackAndConsume(repo_opts, io, allocator, repo, reader, writer, .{ .protocol_version = protocol_version }, author, repo_root_path, error_writer, sess),│ │533 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯