╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────╮ │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| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯