╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Patches.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
449 selected_id: []const u8, 450 ) !Window { 451 const user_id = session.data.user_id orelse return .empty; 452 if (user_id.len != evt.event_id_size) return .empty; 453 const repos_dir = session.repos_dir orelse return .empty; 454 const io = session.io orelse return .empty; 455 const key = evt.Fork.draftKey(repo_id, user_id); 456 const drafts_cursor = try admin_moment.getCursor(hash.hashInt(evt.admin_repo_opts.hash, evt.Fork.repo_user_to_draft_id_set_key)) orelse return .empty; 457 const drafts = try evt.AdminDB.HashMap(.read_only).init(drafts_cursor); 458 const set_cursor = try drafts.getCursor(hash.hashInt(evt.admin_repo_opts.hash, &key)) orelse return .empty; 459 const set = try evt.AdminDB.SortedSet(.read_only).init(set_cursor); 460 const aa = arena.allocator(); 461 462 var root_key: ?[]const u8 = null; 463 if (selected_id.len != 0) { 464 const id = evt.parseEventId(selected_id) catch return error.NotFound; 465 if (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, arena, &id)) |record| { 466 if (!record.removed and record.event.stage == .draft) { 467 const order_key = try aa.dupe(u8, &evt.orderKeyDesc(record.created_order, &id)); 468 if (!try set.contains(order_key)) return error.NotFound; 469 root_key = order_key; 470 } 471 } 472 } 473 474 var prev_id: ?[]const u8 = null; 475 var iter = if (root_key) |root| blk: { 476 const rank = try set.rank(root); 477 if (rank > 0 and rank <= page_size) { 478 prev_id = ""; 479 } else if (rank > page_size) { 480 const pair = try set.getIndexKeyValuePair(@intCast(rank - page_size)) orelse return error.NotFound; 481 var previous: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 482 _ = try pair.key_cursor.readBytes(&previous); 483 const hex = std.fmt.bytesToHex(previous[@sizeOf(u64)..].*, .lower); 484 prev_id = try aa.dupe(u8, &hex); 485 } 486 break :blk try set.iteratorFrom(root); 487 } else try set.iteratorFromIndex(0); 488 489 var items: std.ArrayList(PatchWithId) = .empty; 490 var next_id: ?[]const u8 = null; 491 while (try iter.next()) |cursor| { 492 const id = try evt.readOrderKeyId(evt.AdminDB, cursor); 493 const id_hex = std.fmt.bytesToHex(id, .lower); 494 if (items.items.len == page_size) { 495 next_id = try aa.dupe(u8, &id_hex); 496 break; 497 } 498 const path = try fork.forkPath(aa, repos_dir, &id_hex); 499 var fork_repo = rp.Repo(.xit, .{}).open(io, arena.child_allocator, .{ .path = path, .require_repo_root = true }) catch continue; 500 defer fork_repo.deinit(io, arena.child_allocator); 501 const moment = evt.currentMoment(.{}, &fork_repo) catch continue; 502 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, arena, &id)) orelse continue; 503 var revision_ready = false; 504 if (patch.event.revision) |revision| { 505 const revision_id = evt.parseEventId(&revision.id) catch continue; 506 if (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, arena, &revision_id)) |record| 507 revision_ready = revision.matches(record); 508 } 509 try items.append(aa, .{ 510 .id = try aa.dupe(u8, &id_hex), 511 .record = patch, 512 .author = try ui.Author.initFromEmail(admin_moment, arena, patch.author_email), 513 .draft = true, 514 .revision_ready = revision_ready, 515 }); 516 } 517 return .{ 518 .items = items.items, 519 .prev_id = prev_id, 520 .next_id = next_id, 521 .count = @intCast(try set.count()), 522 }; 523 } 524 525 pub const View = Threads.View(.patch, Self); 526 527 pub const Header = Threads.Header; 528 529 // tabs switching between the patches page's views 530 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 531 var header = try Header.init(allocator); 532 errdefer header.deinit(allocator); 533 const aa = session.page_arena.allocator(); 534 535 // a list tab per status, labeled with its listing's patch count 536 for ([_]evt.Patch.Status{ .open, .closed, .merged }, 0..) |status, index| { 537 const route = ui.RoutablePage.repoPatchesRoute(data.identity, status, data.tag, "") orelse return error.RouteTooLong; 538 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 539 var label_buf: [64]u8 = undefined; 540 const label = try std.fmt.bufPrint(&label_buf, "{s} ({d})", .{ @tagName(status), data.window(status).count }); 541 try header.addTab(allocator, label, link, index); 542 } 543 544 // tags tab, labeled with the active tag filter 545 { 546 const tags_route = ui.RoutablePage.repoThreadTagsRoute(.patch, data.identity, data.tag) orelse return error.RouteTooLong; 547 const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.toUrl(session.page_arena)}); 548 const label = if (data.tag.len == 0) "tags" else blk: { 549 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 550 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); 551 }; 552 try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); 553 } 554 555 // new-patch tab; an edit or resolve url shows its tab in this place 556 if (session.local == null) { 557 const route = switch (data.view) { 558 .edit => ui.RoutablePage.repoThreadEditRoute(.patch, data.identity, data.selected_id) orelse return error.RouteTooLong, 559 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 560 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 561 .remove => ui.RoutablePage.repoThreadRemoveRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 562 .resolve => ui.RoutablePage.repoPatchesResolveRoute(data.identity, data.selected_id, data.theirs_picks) orelse return error.RouteTooLong, 563 else => ui.RoutablePage.repoThreadNewRoute(.patch, data.identity) orelse return error.RouteTooLong, 564 }; 565 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 566 const label: []const u8 = switch (data.view) { 567 .edit => "edit", 568 .new_comment => "reply", 569 .edit_comment => "edit", 570 .remove => "remove", 571 .resolve => "resolve", 572 else => "new", 573 }; 574 try header.addTab(allocator, label, link, View.viewIndex(.new)); 575 } 576 577 // drafts belong to the logged-in user and are not available locally 578 if (session.local == null) { 579 const route = ui.RoutablePage.repoPatchesDraftsRoute(data.identity) orelse return error.RouteTooLong; 580 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 581 var label_buf: [64]u8 = undefined; 582 const label = try std.fmt.bufPrint(&label_buf, "drafts ({d})", .{data.drafts.count}); 583 try header.addTab(allocator, label, link, View.viewIndex(.drafts)); 584 } 585 586 // conflicts tab, labeled with the conflict count 587 if (data.conflicts.count > 0) { 588 const route = ui.RoutablePage.repoPatchesConflictsRoute(data.identity, "") orelse return error.RouteTooLong; 589 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 590 var label_buf: [64]u8 = undefined; 591 const label = try std.fmt.bufPrint(&label_buf, "conflicts ({d})", .{data.conflicts.count}); 592 try header.addTab(allocator, label, link, View.viewIndex(.conflicts)); 593 } 594 595 header.select(View.viewIndex(data.view)); 596 return header; 597 } 598