╭ changed ───────────╮ │src/serve_common.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const evt = @import("./event.zig"); │ │3 -const rp = @import("xit").repo; │ │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 {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │10 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, event) catch {}; │ │11 } │ │12 } │ │13 + │ │14 +// progress for a push, sent on the pack stream's sideband │ │15 +pub const PushProgress = struct { │ │16 + response: *xit.net_server_receive_pack.Response, │ │17 + writer: *std.Io.Writer, │ │18 + // the client to probe, absent when nothing is listening │ │19 + sess: ?*ssh.SessionCtx = null, │ │20 + // set once it has hung up, so the phase in flight can stop too │ │21 + gone: bool = false, │ │22 + label: []const u8 = "Processing commits", │ │23 + count: usize = 0, │ │24 + total: usize = 0, │ │25 + // the last reported step, so a line is sent only when it changes │ │26 + step: usize = 0, │ │27 │ │28 + // how many items a phase with no total advances between reports │ │29 + const report_every = 1000; │ │30 + │ │31 + // the phases a push reports: the objects unpacked from the pack it │ │32 + // received, then everything counted after it │ │33 + fn reported(kind: rp.ProgressKind) bool { │ │34 + return kind == .writing_object_from_pack or kind == .writing_patch; │ │35 + } │ │36 + │ │37 + pub fn run(self: *PushProgress, _: std.Io, event: rp.ProgressEvent) !void { │ │38 + switch (event) { │ │39 + .start => |start| { │ │40 + if (!reported(start.kind)) return; │ │41 + // the unpack phase sends no label of its own │ │42 + if (start.kind == .writing_object_from_pack) self.label = "Unpacking objects"; │ │43 + self.total = start.estimated_total_items; │ │44 + self.count = 0; │ │45 + self.step = 0; │ │46 + }, │ │47 + .complete_one => |kind| { │ │48 + if (!reported(kind)) return; │ │49 + self.count += 1; │ │50 + }, │ │51 + .end => |kind| { │ │52 + if (!reported(kind)) return; │ │53 + }, │ │54 + // each phase names itself before reporting its count │ │55 + .text => |text| { │ │56 + self.label = text; │ │57 + return; │ │58 + }, │ │59 + else => return, │ │60 + } │ │61 + // a phase that knows no total counts instead, and stays quiet until it │ │62 + // has something to count │ │63 + if (self.total == 0 and self.count == 0) return; │ │64 + │ │65 + // every report flushes, so one is sent per percent, or per block of │ │66 + // items when there is no total to turn into one │ │67 + const step: usize = if (self.total > 0) @intCast(@as(u128, self.count) * 100 / self.total) else self.count / report_every;│ │68 + if (event == .complete_one and step == self.step) return; │ │69 + self.step = step; │ │70 + │ │71 + // reporting is the only moment a transaction looks up from its work │ │72 + if (self.sess) |sess| if (sess.peerGone()) { │ │73 + self.gone = true; │ │74 + return error.ClientGone; │ │75 + }; │ │76 + │ │77 + var buffer: [128]u8 = undefined; │ │78 + const suffix = if (event == .end) "\n" else "\r"; │ │79 + const text = if (self.total > 0) │ │80 + try std.fmt.bufPrint(&buffer, "{s}: {d}% ({d}/{d}){s}", .{ self.label, step, self.count, self.total, suffix }) │ │81 + else │ │82 + try std.fmt.bufPrint(&buffer, "{s}: {d}{s}", .{ self.label, self.count, suffix }); │ │83 + try self.response.progress(self.writer, text); │ │84 + } │ │85 +}; │ │86 + │ │87 +// whether the client the work is reporting to has hung up. only a sideband │ │88 +// reporter probes, so any other context never cancels. │ │89 +pub fn progressCancelled(comptime repo_opts: rp.RepoOpts(.xit), progress_ctx_maybe: ?repo_opts.ProgressCtx) bool { │ │90 + if (repo_opts.ProgressCtx != *SidebandProgress) return false; │ │91 + const progress_ctx = progress_ctx_maybe orelse return false; │ │92 + return progress_ctx.gone; │ │93 +} │ │94 + │ │95 // the outcome of resolving a requested repo path to its on-disk directory. │ │96 // the http and ssh paths each map the cases to their own error responses. │ │97 pub const RepoPath = union(enum) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯