╭ progress.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const ssh = @import("./serve_ssh_protocol.zig"); 5 6 // reporting is optional and best effort: a failed report must not fail the work 7 pub fn report(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, progress_ctx_maybe: ?repo_opts.ProgressCtx, event: rp.ProgressEvent) void { 8 if (repo_opts.ProgressCtx != void) { 9 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, event) catch {}; 10 } 11 } 12 13 // a push's or a clone's progress, sent on the pack stream's sideband 14 pub const Sideband = struct { 15 // a push reports through its deferred response, which knows whether the 16 // client wanted sideband at all. a clone has none and writes band 2 itself. 17 response: ?*xit.net_server_receive_pack.Response = null, 18 writer: *std.Io.Writer, 19 // the client to probe, absent when nothing is listening 20 sess: ?*ssh.SessionCtx = null, 21 // set once it has hung up, so the phase in flight can stop too 22 gone: bool = false, 23 label: []const u8 = "Processing commits", 24 count: usize = 0, 25 total: usize = 0, 26 // the last reported step, so a line is sent only when it changes 27 step: usize = 0, 28 29 // how many items a phase with no total advances between reports 30 const report_every = 1000; 31 32 // the phases a client is told about, listed so a new kind has to be 33 // decided on rather than inherit whatever label was last set 34 fn reported(kind: rp.ProgressKind) bool { 35 return switch (kind) { 36 .writing_object_from_pack, .writing_object, .checking_object, .walking_commit, .enumerating_object, .compressing_object, .writing_patch => true, 37 .sending_bytes, .receiving_bytes => false, 38 }; 39 } 40 41 pub fn run(self: *Sideband, _: std.Io, event: rp.ProgressEvent) !void { 42 switch (event) { 43 .start => |start| { 44 if (!reported(start.kind)) return; 45 // a pack phase carries no label of its own; the rest send a 46 // .text first 47 switch (start.kind) { 48 .writing_object_from_pack => self.label = "Unpacking objects", 49 .writing_object => self.label = "Copying objects", 50 .checking_object => self.label = "Checking connectivity", 51 .walking_commit => self.label = "Walking commits", 52 .enumerating_object => self.label = "Enumerating objects", 53 .compressing_object => self.label = "Compressing objects", 54 else => {}, 55 } 56 self.total = start.estimated_total_items; 57 self.count = 0; 58 self.step = 0; 59 }, 60 .complete_one => |kind| { 61 if (!reported(kind)) return; 62 self.count += 1; 63 }, 64 .end => |kind| { 65 if (!reported(kind)) return; 66 }, 67 // each phase names itself before reporting its count 68 .text => |text| { 69 self.label = text; 70 return; 71 }, 72 else => return, 73 } 74 // a phase that knows no total counts instead, and stays quiet until it 75 // has something to count 76 if (self.total == 0 and self.count == 0) return; 77 78 // every report flushes, so one is sent per percent, or per block of 79 // items when there is no total to turn into one 80 const step: usize = if (self.total > 0) @intCast(@as(u128, self.count) * 100 / self.total) else self.count / report_every; 81 if (event == .complete_one and step == self.step) return; 82 self.step = step; 83 84 // reporting is the only moment a transaction looks up from its work 85 if (self.sess) |sess| if (sess.peerGone()) { 86 self.gone = true; 87 return error.ClientGone; 88 }; 89 90 var buffer: [128]u8 = undefined; 91 const suffix = if (event == .end) "\n" else "\r"; 92 const text = if (self.total > 0) 93 try std.fmt.bufPrint(&buffer, "{s}: {d}% ({d}/{d}){s}", .{ self.label, step, self.count, self.total, suffix }) 94 else 95 try std.fmt.bufPrint(&buffer, "{s}: {d}{s}", .{ self.label, self.count, suffix }); 96 if (self.response) |response| { 97 try response.progress(self.writer, text); 98 } else { 99 try xit.net_server_pkt.sendSideband(self.writer, 2, text); 100 try self.writer.flush(); 101 } 102 } 103 }; 104 105 // whether the client the work is reporting to has hung up. only a sideband 106 // reporter probes, so any other context never cancels. 107 pub fn cancelled(comptime repo_opts: rp.RepoOpts(.xit), progress_ctx_maybe: ?repo_opts.ProgressCtx) bool { 108 if (repo_opts.ProgressCtx != *Sideband) return false; 109 const progress_ctx = progress_ctx_maybe orelse return false; 110 return progress_ctx.gone; 111 } 112