╭ changed ───╮ │src/push.zig│ ╰────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 85 io: std.Io, │ │ 86 allocator: std.mem.Allocator, │ │ 87 author: ?evt.CommitAuthor, │ │ 88 + updates: []const xit.net_server_receive_pack.AppliedRefUpdate, │ │ 89 ) !void { │ │ 89 - var author_json: std.json.ObjectMap = if (author) |user| │ │ 90 - try .init(allocator, &.{ "username", "email" }, &.{ .{ .string = user.name }, .{ .string = user.email } }) │ │ 90 + // what the payload may take up: the record holds a timestamp and the │ │ 91 + // action kind in the same fixed buffer │ │ 92 + const payload_max_len = repo_opts.max_read_size - @sizeOf(i64) - undo_action.len - 1; │ │ 93 + │ │ 94 + var arena = std.heap.ArenaAllocator.init(allocator); │ │ 95 + defer arena.deinit(); │ │ 96 + const aa = arena.allocator(); │ │ 97 + │ │ 98 + const author_json: std.json.ObjectMap = if (author) |user| │ │ 99 + try .init(aa, &.{ "username", "email" }, &.{ .{ .string = user.name }, .{ .string = user.email } }) │ │100 else │ │101 .empty; │ │ 93 - defer author_json.deinit(allocator); │ │ 94 - var payload: std.json.ObjectMap = try .init(allocator, &.{"author"}, &.{if (author != null) .{ .object = author_json } else .null}); │ │ 95 - defer payload.deinit(allocator); │ │102 + var payload: std.json.ObjectMap = try .init(aa, &.{"author"}, &.{if (author != null) .{ .object = author_json } else .null}); │ │103 + │ │104 + // where each ref landed, so the undo tab can say what the push did to it. │ │105 + // an oid of all zeros means the ref has no side there, which is how a │ │106 + // create and a remove are told apart │ │107 + var refs: std.json.Array = .init(aa); │ │108 + for (updates) |update| { │ │109 + const entry: std.json.ObjectMap = try .init(aa, &.{ "name", "old", "new" }, &.{ │ │110 + .{ .string = update.ref_name }, │ │111 + .{ .string = if (zeroOid(update.old_oid)) "" else update.old_oid }, │ │112 + .{ .string = if (zeroOid(update.new_oid)) "" else update.new_oid }, │ │113 + }); │ │114 + try refs.append(.{ .object = entry }); │ │115 + try payload.put(aa, "refs", .{ .array = refs }); │ │116 + │ │117 + // the record is written into one fixed buffer, so a ref that no │ │118 + // longer fits is left out rather than failing the push │ │119 + if (try payloadLen(payload) > payload_max_len) { │ │120 + _ = refs.pop(); │ │121 + break; │ │122 + } │ │123 + } │ │124 + │ │125 + // the array is stored by value, so the one the payload holds still counts │ │126 + // the entry that was popped until it is replaced here │ │127 + try payload.put(aa, "refs", .{ .array = refs }); │ │128 + │ │129 try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action, .payload = payload } });│ │130 +} │ │131 + │ │132 +// the oid a ref update sends when it has no side: all zeros │ │133 +fn zeroOid(oid: []const u8) bool { │ │134 + for (oid) |char| if (char != '0') return false; │ │135 + return true; │ │136 } │ │137 │ │138 +// how long the record's payload would be, to keep it within the buffer │ │139 +fn payloadLen(payload: std.json.ObjectMap) !u64 { │ │140 + var discarding = std.Io.Writer.Discarding.init(&.{}); │ │141 + try std.json.Stringify.value(std.json.Value{ .object = payload }, .{}, &discarding.writer); │ │142 + return discarding.fullCount(); │ │143 +} │ │144 + │ │145 // serve a receive-pack and consume any events it pushed to the events branch, │ │146 // all in one transaction: the push and the views derived from it commit │ │147 // atomically, and a failed consume cancels the push │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │206 _ = try find.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator); │ │207 _ = try srch_cmmt.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.updates.items.items);│ │208 │ │163 - try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author); │ │209 + try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, ctx.updates.items.items); │ │210 } │ │211 }; │ │212 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │378 try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); │ │379 if (!try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction;│ │380 } │ │335 - try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author); │ │381 + // a fork push tracks no ref updates, so it records no ranges │ │382 + try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, &.{}); │ │383 } │ │384 }; │ │385 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯