╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 66 // a repo's path, inside its owner's dir │ │ 67 pub fn repoPath(allocator: std.mem.Allocator, users_dir: []const u8, owner_id: []const u8, repo_id: []const u8) ![]u8 { │ │ 68 return try std.fs.path.join(allocator, &.{ users_dir, &std.fmt.bytesToHex(owner_id[0..event_id_size].*, .lower), "repos", &std.fmt.bytesToHex(repo_id[0..event_id_size].*, .lower) });│ │ 69 +} │ │ 70 + │ │ 71 +// the ids a repo path names, or null for a path `repoPath` didn't build │ │ 72 +pub fn parseRepoPath(users_dir: []const u8, path: []const u8) ?RepoLocation { │ │ 73 + const repos_dir = std.fs.path.dirname(path) orelse return null; │ │ 74 + if (!std.mem.eql(u8, std.fs.path.basename(repos_dir), "repos")) return null; │ │ 75 + const owner_dir = std.fs.path.dirname(repos_dir) orelse return null; │ │ 76 + if (!std.mem.eql(u8, std.fs.path.dirname(owner_dir) orelse return null, users_dir)) return null; │ │ 77 + return .{ │ │ 78 + .owner_id = parseEventId(std.fs.path.basename(owner_dir)) catch return null, │ │ 79 + .repo_id = parseEventId(std.fs.path.basename(path)) catch return null, │ │ 80 + }; │ │ 81 } │ │ 82 + │ │ 83 +// the owner's repos newest activity first, as order keys │ │ 84 +pub const recent_repo_id_set_key = "haxy/recent-repo-id-set"; │ │ 85 +// each repo's activity time in that set │ │ 86 +const repo_activity_time_key = "haxy/repo-id->activity-time"; │ │ 87 + │ │ 88 +// a repo's activity time is rewritten at most once per window │ │ 89 +pub const repo_activity_window_secs = 5 * 60; │ │ 90 │ │ 91 +// move a repo to `timestamp` in its owner's activity index, unless it moved within the window │ │ 92 +pub fn bumpRepoActivity( │ │ 93 + io: std.Io, │ │ 94 + allocator: std.mem.Allocator, │ │ 95 + users_dir: []const u8, │ │ 96 + owner_id: *const [event_id_size]u8, │ │ 97 + repo_id: *const [event_id_size]u8, │ │ 98 + timestamp: u64, │ │ 99 +) !void { │ │100 + const hash_kind = user_repo_opts.hash; │ │101 + var user_repo = (try openUserRepo(io, allocator, users_dir, owner_id)) orelse return; │ │102 + defer user_repo.deinit(io, allocator); │ │103 + │ │104 + // a recent bump skips the transaction │ │105 + if (try readActivityTime(try user_repo.core.latestMoment(), repo_id)) |time| { │ │106 + if (timestamp -| time < repo_activity_window_secs) return; │ │107 + } │ │108 + │ │109 + const Ctx = struct { │ │110 + repo_id: *const [event_id_size]u8, │ │111 + timestamp: u64, │ │112 + │ │113 + pub fn run(ctx: @This(), cursor: *UserDB.Cursor(.read_write)) !void { │ │114 + const moment = try UserDB.HashMap(.read_write).init(cursor.*); │ │115 + // reread so a concurrent bump can't leave its key behind │ │116 + const old_time = try readActivityTime(moment.readOnly(), ctx.repo_id); │ │117 + if (old_time) |time| { │ │118 + if (ctx.timestamp -| time < repo_activity_window_secs) return error.CancelTransaction; │ │119 + } │ │120 + const recent = try UserDB.SortedSet(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, recent_repo_id_set_key))); │ │121 + if (old_time) |time| _ = try recent.remove(&orderKeyDesc(time, ctx.repo_id)); │ │122 + try recent.put(&orderKeyDesc(ctx.timestamp, ctx.repo_id)); │ │123 + const times = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, repo_activity_time_key))); │ │124 + try times.put(hash.hashInt(hash_kind, ctx.repo_id), .{ .uint = ctx.timestamp }); │ │125 + } │ │126 + }; │ │127 + │ │128 + try user_repo.core.db_file.lock(io, .exclusive); │ │129 + defer user_repo.core.db_file.unlock(io); │ │130 + │ │131 + const history = try UserDB.ArrayList(.read_write).init(user_repo.core.db.rootCursor()); │ │132 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo_id = repo_id, .timestamp = timestamp }) catch |err| switch (err) { │ │133 + error.CancelTransaction => {}, │ │134 + else => |e| return e, │ │135 + }; │ │136 +} │ │137 + │ │138 +// bump the repo at `repo_path` to now, logging rather than returning a failure │ │139 +pub fn bumpRepoActivityAt(io: std.Io, allocator: std.mem.Allocator, users_dir: []const u8, repo_path: []const u8) void { │ │140 + const location = parseRepoPath(users_dir, repo_path) orelse return; │ │141 + const now: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │142 + bumpRepoActivity(io, allocator, users_dir, &location.owner_id, &location.repo_id, now) catch |err| { │ │143 + std.log.warn("failed to record repo activity: {s}", .{@errorName(err)}); │ │144 + }; │ │145 +} │ │146 + │ │147 +fn readActivityTime(moment: UserDB.HashMap(.read_only), repo_id: *const [event_id_size]u8) !?u64 { │ │148 + const times_cursor = try moment.getCursor(hash.hashInt(user_repo_opts.hash, repo_activity_time_key)) orelse return null; │ │149 + const times = try UserDB.HashMap(.read_only).init(times_cursor); │ │150 + const time_cursor = try times.getCursor(hash.hashInt(user_repo_opts.hash, repo_id)) orelse return null; │ │151 + return try time_cursor.readUint(); │ │152 +} │ │153 + │ │154 // open a user's repo, or null when it's missing │ │155 pub fn openUserRepo(io: std.Io, allocator: std.mem.Allocator, users_dir: []const u8, user_id: *const [event_id_size]u8) !?rp.Repo(.xit, user_repo_opts) { │ │156 const path = try userRepoPath(allocator, users_dir, user_id); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │627 // explicit patch writes already saved their checks in the event transaction │ │628 if (role == .repo and repo_kind == .xit and host == .server and events.len == 0) { │ │629 pch.refreshOpenMergeability(repo_opts, io, allocator, repo, host.server.users_dir, null, null);│ │630 + } │ │631 + │ │632 + if (role == .repo and host == .server and events.len > 0) { │ │633 + bumpRepoActivityAt(io, allocator, host.server.users_dir, repo.core.work_path); │ │634 } │ │635 } │ │636 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1864 io.random(&id_bytes); │ │1865 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │1866 │ │1867 + const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │1868 try consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, user_repo_opts, io, allocator, &user_repo, events_ref, &[_]EventWithId{.{│ │1869 .id = event_id_hex, │ │1782 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1870 + .timestamp = timestamp, │ │1871 .author = .{ .name = owner.event.name, .email = owner.event.email }, │ │1872 .event = .{ .repo = .{ │ │1873 .user_id = &owner_user_id, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1876 .read_access = create_options.read_access, │ │1877 } }, │ │1878 }}); │ │1879 + bumpRepoActivity(io, allocator, users_dir, &owner_user_id, &id_bytes, timestamp) catch |err| {│ │1880 + std.log.warn("failed to record repo activity: {s}", .{@errorName(err)}); │ │1881 + }; │ │1882 │ │1883 return .{ .owner_id = owner_user_id, .repo_id = id_bytes }; │ │1884 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │ 95 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo = repo, .io = io, .arena = &arena, .id = id, .patch = patch, .expected_patch = expected_patch, .author = author, .checks = &checks, .first_parent = first_parent });│ │ 96 } │ │ 97 + if (host == .server) evt.bumpRepoActivityAt(io, allocator, host.server.users_dir, repo.core.work_path); │ │ 98 } │ │ 99 } │ │100 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 998 else => |e| return e, │ │ 999 }; │ │1000 } │ │1001 + evt.bumpRepoActivityAt(io, allocator, users_dir, target_repo.core.work_path); │ │1002 │ │1003 // remove the consumed draft from the fork │ │1004 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1236 .timestamp = input.timestamp, │ │1237 }); │ │1238 } │ │1239 + evt.bumpRepoActivityAt(io, allocator, users_dir, target_repo.core.work_path); │ │1240 │ │1241 // refresh other patches affected by the updated target branch. │ │1242 refreshOpenMergeability(repo_opts, io, allocator, target_repo, users_dir, null, null);│ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/push.zig│ ╰────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │210 if (err == error.CancelTransaction) return; │ │211 return err; │ │212 }; │ │213 + evt.bumpRepoActivityAt(io, allocator, users_dir, repo.core.work_path); │ │214 │ │215 pch.refreshBranches(.{ .server = .{ .users_dir = users_dir } }, .xit, repo_opts, io, allocator, repo, updates.items.items, &sideband) catch |err| {│ │216 serve_common.logError(io, error_writer, "failed to refresh branch patches: {s}\n", .{@errorName(err)}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 if (owned.items.len > 0) try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, owned.items);│ │266 } │ │267 │ │268 + // the creation timestamps order the repos tabs │ │269 + for (repo_data, &repo_event_ids, repo_events) |r, *id_bytes, event| { │ │270 + try evt.bumpRepoActivity(io, allocator, users_dir, &user_ids[r.user_index], id_bytes, event.timestamp); │ │271 + } │ │272 + │ │273 // every repo gets the same generated history, so build it once into a │ │274 // template repo and copy that to each repo's location below rather than │ │275 // redoing the expensive commit work for every repo. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 const user_moment = if (user_repo_maybe) |*user_repo| try evt.userMoment(user_repo) else null; │ │71 │ │72 const search: ?[]const u8 = if (repos_search.len == 0) null else std.Uri.percentDecodeInPlace(try arena.allocator().dupe(u8, repos_search));│ │73 - const prefix = search orelse ""; │ │73 │ │74 var repos: std.ArrayList(evt.Repo.Record) = .empty; │ │75 var repos_next_start: ?usize = null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯