╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │631 .author = try commitAuthor(moment, arena, &holder.id),│ │632 .protected_branches = if (owner) names else &.{}, │ │633 .locked_branches = if (owner) &.{} else names, │ │634 + .owner = owner, │ │635 } }; │ │636 }, │ │637 }, │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │282 try std.testing.expectEqualStrings("base.txt", diff_before.window.hunks[0].path orelse return error.MissingPath); │ │283 _ = try repo.commitAtRef(io, allocator, .{ .message = "rewrite history", .parent_oids = &.{} }, null, .{ .kind = .head, .name = "master" });│ │284 try repo.removeBranch(io, .{ .name = "feature" }); │ │285 - _ = try repo.garbageCollect(io, allocator, .{}); │ │285 + _ = try repo.garbageCollect(io, allocator, .{}, null); │ │286 { │ │287 var moment = try repo.core.latestMoment(); │ │288 const state = rp.Repo(.xit, opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │790 const roots = try evt.PatchRev.gcRoots(Repo.DB, repo_opts.hash, allocator, stacked_moment); │ │791 defer allocator.free(roots); │ │792 try std.testing.expectEqual(4, roots.len); │ │793 - _ = try target.garbageCollect(io, allocator, .{ .extra_roots = roots }); │ │793 + _ = try target.garbageCollect(io, allocator, .{ .extra_roots = roots }, null); │ │794 │ │795 var after_gc_moment = try target.core.latestMoment(); │ │796 const after_gc_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &after_gc_moment } };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭────────────────────────────────────────────────────────────╮ │79 const allocator = std.testing.allocator; │ │80 try testPushFork(.sha1, 3801, io, allocator); │ │81 try testPushFork(.sha256, 3803, io, allocator); │ │82 +} │ │83 + │ │84 +test "push clears undo history" { │ │85 + const io = std.testing.io; │ │86 + const allocator = std.testing.allocator; │ │87 + try testPushClearUndo(.sha1, 3901, io, allocator); │ │88 + try testPushClearUndo(.sha256, 3903, io, allocator);│ │89 } │ │90 │ │91 fn runServer( │ ╰────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1613 try std.testing.expectError(error.FileNotFound, server_repo.core.work_dir.openFile(io, "hello.txt", .{})); │ │1614 } │ │1615 } │ │1616 + │ │1617 +fn testPushClearUndo( │ │1618 + comptime hash_kind: hash.HashKind, │ │1619 + comptime port: u16, │ │1620 + io: std.Io, │ │1621 + allocator: std.mem.Allocator, │ │1622 +) !void { │ │1623 + const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; │ │1624 + │ │1625 + // create the temp dir │ │1626 + var temp = std.testing.tmpDir(.{}); │ │1627 + defer temp.cleanup(); │ │1628 + const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); │ │1629 + defer allocator.free(temp_path); │ │1630 + │ │1631 + // init server │ │1632 + var server_process = try runServer(io, allocator, temp_path, port); │ │1633 + defer _ = server_process.kill(io); │ │1634 + │ │1635 + // register the repo under admin and locate its on-disk directory │ │1636 + const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; │ │1637 + defer allocator.free(server_path); │ │1638 + { │ │1639 + var server_repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .bare = true, .path = server_path }); │ │1640 + defer server_repo.deinit(io, allocator); │ │1641 + } │ │1642 + │ │1643 + // a git client with one commit to push │ │1644 + const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); │ │1645 + defer allocator.free(client_path); │ │1646 + const commit = blk: { │ │1647 + var client_repo = try rp.Repo(.git, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); │ │1648 + defer client_repo.deinit(io, allocator); │ │1649 + const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); │ │1650 + defer hello_txt.close(io); │ │1651 + try hello_txt.writeStreamingAll(io, "hello, world!"); │ │1652 + try client_repo.add(io, allocator, &.{"hello.txt"}); │ │1653 + const remote_url = try remoteUrl(.{ .wire = .ssh }, allocator, "testrepo", port); │ │1654 + defer allocator.free(remote_url); │ │1655 + try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); │ │1656 + break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); │ │1657 + }; │ │1658 │ │1659 + const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; │ │1660 + defer allocator.free(ssh_cmd); │ │1661 + const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); │ │1662 + defer allocator.free(ssh_config_arg); │ │1663 + │ │1664 + { │ │1665 + const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master"}); │ │1666 + defer allocator.free(result.stderr); │ │1667 + try std.testing.expect(result.ok); │ │1668 + } │ │1669 + const pushed_count = try historyCount(repo_opts, io, allocator, server_path); │ │1670 + try std.testing.expect(pushed_count > 1); │ │1671 + │ │1672 + // the clear ref can only be deleted │ │1673 + { │ │1674 + const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master:refs/haxy/undo"}); │ │1675 + defer allocator.free(result.stderr); │ │1676 + try std.testing.expect(!result.ok); │ │1677 + try std.testing.expect(std.mem.indexOf(u8, result.stderr, "can only be deleted") != null); │ │1678 + } │ │1679 + │ │1680 + // and only alone, refusing the whole push │ │1681 + { │ │1682 + const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{ "master:refs/heads/other", ":refs/haxy/undo" }); │ │1683 + defer allocator.free(result.stderr); │ │1684 + try std.testing.expect(!result.ok); │ │1685 + try std.testing.expect(std.mem.indexOf(u8, result.stderr, "must be deleted alone") != null); │ │1686 + } │ │1687 + │ │1688 + // a refused push leaves no transaction behind │ │1689 + try std.testing.expectEqual(pushed_count, try historyCount(repo_opts, io, allocator, server_path)); │ │1690 + │ │1691 + { │ │1692 + const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{":refs/haxy/undo"}); │ │1693 + defer allocator.free(result.stderr); │ │1694 + try std.testing.expect(result.ok); │ │1695 + try std.testing.expect(std.mem.indexOf(u8, result.stderr, "Compacting database") != null); │ │1696 + } │ │1697 + │ │1698 + // one transaction is left, and it still holds the pushed branch │ │1699 + try std.testing.expectEqual(1, try historyCount(repo_opts, io, allocator, server_path)); │ │1700 + { │ │1701 + var server_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = server_path }); │ │1702 + defer server_repo.deinit(io, allocator); │ │1703 + try std.testing.expectEqualStrings(&commit, &(try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?); │ │1704 + try std.testing.expectEqual(null, try server_repo.readRef(io, .{ .kind = .head, .name = "other" })); │ │1705 + } │ │1706 +} │ │1707 + │ │1708 +// whether a git push from the client succeeded, and what it printed. progress │ │1709 +// is forced on, since git hides it when stderr isn't a terminal. │ │1710 +fn gitPush(io: std.Io, allocator: std.mem.Allocator, client_path: []const u8, ssh_config_arg: []const u8, refspecs: []const []const u8) !struct { ok: bool, stderr: []u8 } {│ │1711 + var argv: std.ArrayList([]const u8) = .empty; │ │1712 + defer argv.deinit(allocator); │ │1713 + try argv.appendSlice(allocator, &.{ "git", "-c", ssh_config_arg, "push", "--progress", "origin" }); │ │1714 + try argv.appendSlice(allocator, refspecs); │ │1715 + const result = try std.process.run(allocator, io, .{ .argv = argv.items, .cwd = .{ .path = client_path } }); │ │1716 + allocator.free(result.stdout); │ │1717 + return .{ .ok = result.term == .exited and result.term.exited == 0, .stderr = result.stderr }; │ │1718 +} │ │1719 + │ │1720 +// the number of transactions in a repo's history. opened afresh each time, │ │1721 +// since gc replaces the database file. │ │1722 +fn historyCount(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, allocator: std.mem.Allocator, path: []const u8) !u64 { │ │1723 + var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }); │ │1724 + defer repo.deinit(io, allocator); │ │1725 + const history = try rp.Repo(.xit, repo_opts).DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); │ │1726 + return try history.count(); │ │1727 +} │ │1728 + │ │1729 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { │ │1730 var iter = src_dir.iterate(); │ │1731 while (try iter.next(io)) |entry| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/test/undo.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │137 try std.testing.expectEqual(null, session.next_page); │ │138 } │ │139 │ │140 -test "local git has no undo and local xit can undo and refresh" {│ │140 +test "local mode has no undo" { │ │141 inline for ([_]xit.repo.RepoKind{ .git, .xit }) |kind| { │ │142 var temp = std.testing.tmpDir(.{}); │ │143 defer temp.cleanup(); │ ╰──────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 { │ │148 var repo = try xit.repo.Repo(kind, .{}).init(io, allocator, .{ .path = path }); │ │149 defer repo.deinit(io, allocator); │ │150 - try repo.addConfig(io, allocator, .{ .name = "undo.value", .value = "first" }); │ │150 } │ │151 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .io = io, .local = .{ .path = path, .repo_kind = kind }, .data = .{ .host_kind = .local } };│ │153 - const files = ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?; │ │154 - const data = try ui.Repo.init(&arena, &session, files); │ │155 - try std.testing.expectEqual(kind == .xit, data.undo != null); │ │156 - if (kind == .git) { │ │157 - try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?)); │ │158 - try std.testing.expectError(error.NotFound, Undo.execute(io, allocator, session.local.?, 1)); │ │159 - } else { │ │160 - Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 999).?.repo_undo); │ │161 - try std.testing.expectEqualStrings("InvalidHistoryIndex", session.data.undo_failure.?); │ │162 - try std.testing.expectEqual(null, session.next_page); │ │163 - Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 1).?.repo_undo); │ │164 - try std.testing.expectEqual(null, session.data.undo_failure); │ │165 - const refreshed = try ui.Repo.init(&arena, &session, session.next_page.?); │ │166 - try std.testing.expectEqualStrings("undo", refreshed.undo.?.items[0].action); │ │167 - } │ │152 + const data = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?); │ │153 + try std.testing.expect(data.undo == null); │ │154 + try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?)); │ │155 } │ │156 } │ │157 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │174 defer temp.cleanup(); │ │175 var arena = std.heap.ArenaAllocator.init(allocator); │ │176 defer arena.deinit(); │ │190 - const path = try temp.dir.realPathFileAlloc(io, ".", arena.allocator()); │ │177 + const aa = arena.allocator(); │ │178 + │ │179 + // a server with one user who owns the repo │ │180 + const base = try temp.dir.realPathFileAlloc(io, ".", aa); │ │181 + const admin_path = try std.Io.Dir.path.join(aa, &.{ base, "admin" }); │ │182 + const users_dir = try std.Io.Dir.path.join(aa, &.{ base, "users" }); │ │183 + var admin = try xit.repo.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); │ │184 + defer admin.deinit(io, allocator); │ │185 + const owner: [evt.event_id_size]u8 = @splat(1); │ │186 + const repo_id: [evt.event_id_size]u8 = @splat(4); │ │187 + const author: evt.CommitAuthor = .{ .name = "test", .email = "test@example.test" }; │ │188 + try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ │ │189 + .{ .id = std.fmt.bytesToHex(owner, .lower), .author = author, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = "unused" } } }, │ │190 + }); │ │191 + var user_repo = try evt.initUserRepo(io, allocator, try evt.userRepoPath(aa, users_dir, &owner)); │ │192 + defer user_repo.deinit(io, allocator); │ │193 + try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{ │ │194 + .{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(owner, .lower), .name = "demo", .description = "", .read_access = .public } } },│ │195 + }); │ │196 + const path = try evt.repoPath(aa, users_dir, &owner, &repo_id); │ │197 const Repo = xit.repo.Repo(.xit, .{}); │ │198 var repo = try Repo.init(io, allocator, .{ .path = path }); │ │199 defer repo.deinit(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 const history = try Repo.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │209 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Corrupt{}); │ │210 │ │205 - var session = ui.Session{ .arena = &arena, .page_arena = &arena, .io = io, .local = .{ .path = path, .repo_kind = .xit }, .data = .{ .host_kind = .local } };│ │211 + var session = try ui.Session.init(&arena, &admin, .{ .user_id = &owner }); │ │212 + session.io = io; │ │213 + session.users_dir = users_dir; │ │214 // tabs switch in-page, so every tab carries the undo data and its failure │ │207 - const files = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?); │ │215 + const files = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoFilesRoute("alice:demo", null, "", "", 0).?); │ │216 try std.testing.expectEqual(0, files.undo.?.items.len); │ │217 try std.testing.expectEqualStrings("InvalidUndoRecord", files.undo.?.failure.?); │ │210 - const undo = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?); │ │218 + const undo = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("alice:demo", null).?); │ │219 try std.testing.expectEqualStrings("InvalidUndoRecord", undo.undo.?.failure.?); │ │220 │ │221 var root = ui.Widget{ .repo_undo = try Undo.View.init(allocator, &undo.undo.?, &session) }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │911 const moment = try evt.currentMoment(.{}, &template_repo); │ │912 const roots = try evt.PatchRev.gcRoots(rp.Repo(.xit, .{}).DB, .sha1, allocator, moment);│ │913 defer allocator.free(roots); │ │914 - _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); │ │914 + _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }, null); │ │915 } │ │916 } │ │917 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯