╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
949 950 fn testPushEvents( 951 comptime repo_kind: rp.RepoKind, 952 comptime transport_def: net.TransportDefinition, 953 comptime hash_kind: hash.HashKind, 954 comptime port: u16, 955 io: std.Io, 956 allocator: std.mem.Allocator, 957 ) !void { 958 // create the temp dir 959 var temp = std.testing.tmpDir(.{}); 960 defer temp.cleanup(); 961 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 962 defer allocator.free(temp_path); 963 964 // init server 965 var server_process = try runServer(io, allocator, temp_path, port); 966 defer _ = server_process.kill(io); 967 968 // register the repo under admin and locate its on-disk directory 969 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 970 defer allocator.free(server_path); 971 972 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 973 const ServerRepo = rp.Repo(.xit, repo_opts); 974 var server_repo = try ServerRepo.init(io, allocator, .{ .bare = true, .path = server_path }); 975 defer server_repo.deinit(io, allocator); 976 977 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 978 defer allocator.free(client_path); 979 980 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 981 defer client_repo.deinit(io, allocator); 982 983 // add remote 984 { 985 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 986 defer allocator.free(remote_url); 987 988 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 989 } 990 991 const is_ssh = switch (transport_def) { 992 .file => false, 993 .wire => |wire_kind| .ssh == wire_kind, 994 }; 995 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 996 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 997 998 var arena = std.heap.ArenaAllocator.init(allocator); 999 defer arena.deinit(); 1000 1001 // 1002 // define test events 1003 // 1004 1005 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1006 const issue_event_id = evt.EventWithId.randomId(prng.random()); 1007 1008 const events_to_push = [_]evt.EventWithId{ 1009 .{ 1010 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1011 .author = .{ .name = "haxy", .email = "user@haxy" }, 1012 .event = .{ 1013 .issue = .{ 1014 .title = "Login form clears password on validation error", 1015 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 1016 .labels = "bug priority-high ui", 1017 }, 1018 }, 1019 }, 1020 }; 1021 1022 // commit the issue on the client and push it to the server 1023 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 1024 try client_repo.push( 1025 io, 1026 allocator, 1027 "origin", 1028 "haxy/events", 1029 false, 1030 .{ .wire = .{ .ssh = .{ 1031 .command = ssh_cmd_maybe, 1032 } } }, 1033 ); 1034 1035 // the push consumed the issue into the server repo's db 1036 { 1037 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1038 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1039 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1040 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1041 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1042 1043 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1044 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.labels, issue.event.labels); 1045 } 1046 1047 // 1048 // edit the issue 1049 // 1050 1051 // this event edits the previous one because it has the same id 1052 const events_to_push2 = [_]evt.EventWithId{ 1053 .{ 1054 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1055 .author = .{ .name = "haxy", .email = "user@haxy" }, 1056 .event = .{ 1057 .issue = .{ 1058 .title = "Login form clears password on validation error", 1059 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1060 .labels = "bug priority-low ui", 1061 }, 1062 }, 1063 }, 1064 }; 1065 1066 // commit the edit on the client and push it to the server 1067 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1068 try client_repo.push( 1069 io, 1070 allocator, 1071 "origin", 1072 "haxy/events", 1073 false, 1074 .{ .wire = .{ .ssh = .{ 1075 .command = ssh_cmd_maybe, 1076 } } }, 1077 ); 1078 1079 // the push consumed the edit into the server repo's db 1080 { 1081 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1082 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1083 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1084 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1085 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1086 1087 // the description and labels were correctly edited 1088 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1089 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.labels, issue.event.labels); 1090 } 1091 } 1092 1093 fn testClone( 1094 comptime repo_kind: rp.RepoKind, 1095 comptime transport_def: net.TransportDefinition, 1096 comptime shell_out_to_git: bool, 1097 comptime hash_kind: hash.HashKind, 1098 comptime port: u16, 1099 io: std.Io, 1100 allocator: std.mem.Allocator, 1101 ) !void { 1102 // create the temp dir 1103 var temp = std.testing.tmpDir(.{}); 1104 defer temp.cleanup(); 1105 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1106 defer allocator.free(temp_path); 1107 1108 // init server 1109 var server_process = try runServer(io, allocator, temp_path, port); 1110 defer _ = server_process.kill(io); 1111 1112 // register the repo under admin and locate its on-disk directory 1113 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1114 defer allocator.free(server_path); 1115 1116 // init server repo with default branch name as main 1117 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1118 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = !shell_out_to_git }).init(io, allocator, .{ .bare = true, .path = server_path, .create_default_branch = "main" }); 1119 defer server_repo.deinit(io, allocator); 1120 1121 if (shell_out_to_git) { 1122 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1123 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1124 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1125 } 1126 1127 // make a commit 1128 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world!" }} }, .{ .message = "let there be light" }); 1129 1130 // tag first commit 1131 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1132 1133 // make a commit 1134 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "goodbye.txt", .content = "goodbye, world!" }} }, .{ .message = "add goodbye file" }); 1135 1136 // export server repo 1137 { 1138 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1139 defer export_file.close(io); 1140 } 1141 1142 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1143 defer allocator.free(client_path); 1144 1145 // get remote url 1146 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1147 defer allocator.free(remote_url); 1148 1149 const is_ssh = switch (transport_def) { 1150 .file => false, 1151 .wire => |wire_kind| .ssh == wire_kind, 1152 }; 1153 1154 if (shell_out_to_git) { 1155 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1156 defer allocator.free(ssh_cmd); 1157 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1158 defer allocator.free(ssh_config_arg); 1159 1160 { 1161 var process = try std.process.spawn(io, .{ 1162 .argv = if (is_ssh) 1163 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--depth", "1", remote_url, "client" } 1164 else 1165 &.{ "git", "clone", "--quiet", "--depth", "1", remote_url, "client" }, 1166 .cwd = .{ .path = temp_path }, 1167 .stdin = .ignore, 1168 .stdout = .ignore, 1169 .stderr = .inherit, 1170 }); 1171 defer process.kill(io); 1172 const term = try process.wait(io); 1173 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1174 } 1175 1176 // make sure shallow clone was successful 1177 { 1178 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1179 hello_txt.close(io); 1180 } 1181 1182 // make a third commit on the server 1183 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1184 1185 // pull --unshallow to deepen the clone and get the new commit 1186 { 1187 var process = try std.process.spawn(io, .{ 1188 .argv = if (is_ssh) 1189 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "--unshallow" } 1190 else 1191 &.{ "git", "pull", "--quiet", "--unshallow" }, 1192 .cwd = .{ .path = client_path }, 1193 .stdin = .ignore, 1194 .stdout = .ignore, 1195 .stderr = .inherit, 1196 }); 1197 defer process.kill(io); 1198 const term = try process.wait(io); 1199 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1200 } 1201 1202 // make sure unshallow pull was successful 1203 { 1204 const extra_txt = try temp.dir.openFile(io, "client/extra.txt", .{}); 1205 extra_txt.close(io); 1206 } 1207 1208 // delete client and clone again with --shallow-since 1209 try temp.dir.deleteTree(io, "client"); 1210 1211 { 1212 var process = try std.process.spawn(io, .{ 1213 .argv = if (is_ssh) 1214 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" } 1215 else 1216 &.{ "git", "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" }, 1217 .cwd = .{ .path = temp_path }, 1218 .stdin = .ignore, 1219 .stdout = .ignore, 1220 .stderr = .inherit, 1221 }); 1222 defer process.kill(io); 1223 const term = try process.wait(io); 1224 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1225 } 1226 1227 // make sure shallow clone was successful 1228 { 1229 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1230 hello_txt.close(io); 1231 } 1232 1233 // delete client and clone again with --shallow-exclude 1234 try temp.dir.deleteTree(io, "client"); 1235 1236 { 1237 var process = try std.process.spawn(io, .{ 1238 .argv = if (is_ssh) 1239 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" } 1240 else 1241 &.{ "git", "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" }, 1242 .cwd = .{ .path = temp_path }, 1243 .stdin = .ignore, 1244 .stdout = .ignore, 1245 .stderr = .inherit, 1246 }); 1247 defer process.kill(io); 1248 const term = try process.wait(io); 1249 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1250 } 1251 1252 // make sure shallow clone was successful 1253 { 1254 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1255 hello_txt.close(io); 1256 } 1257 1258 // delete client and clone again with --filter=blob:none 1259 try temp.dir.deleteTree(io, "client"); 1260 1261 { 1262 var process = try std.process.spawn(io, .{ 1263 .argv = if (is_ssh) 1264 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=blob:none", remote_url, "client" } 1265 else 1266 &.{ "git", "clone", "--quiet", "--filter=blob:none", remote_url, "client" }, 1267 .cwd = .{ .path = temp_path }, 1268 .stdin = .ignore, 1269 .stdout = .ignore, 1270 .stderr = .inherit, 1271 }); 1272 defer process.kill(io); 1273 const term = try process.wait(io); 1274 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1275 } 1276 1277 // make sure partial clone was successful 1278 { 1279 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1280 hello_txt.close(io); 1281 } 1282 1283 // delete client and clone again with --filter=tree:0 1284 try temp.dir.deleteTree(io, "client"); 1285 1286 { 1287 var process = try std.process.spawn(io, .{ 1288 .argv = if (is_ssh) 1289 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=tree:0", remote_url, "client" } 1290 else 1291 &.{ "git", "clone", "--quiet", "--filter=tree:0", remote_url, "client" }, 1292 .cwd = .{ .path = temp_path }, 1293 .stdin = .ignore, 1294 .stdout = .ignore, 1295 .stderr = .inherit, 1296 }); 1297 defer process.kill(io); 1298 const term = try process.wait(io); 1299 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1300 } 1301 1302 // make sure treeless clone was successful 1303 { 1304 const goodbye_txt = try temp.dir.openFile(io, "client/goodbye.txt", .{}); 1305 goodbye_txt.close(io); 1306 } 1307 } else { 1308 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1309 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1310 1311 // clone repo 1312 var clone_result = try rp.AnyRepo(repo_kind, .{ .is_test = true }).clone( 1313 io, 1314 allocator, 1315 remote_url, 1316 temp_path, 1317 client_path, 1318 null, 1319 .{ .transport = .{ .wire = .{ .ssh = .{ 1320 .command = ssh_cmd_maybe, 1321 } } } }, 1322 ); 1323 defer clone_result.deinit(io, allocator); 1324 const client_repo = &@field(clone_result, @tagName(hash_kind)); 1325 1326 // make sure HEAD points to the right default branch 1327 var current_branch_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = @splat(0); 1328 const head = try client_repo.head(io, &current_branch_buffer); 1329 try std.testing.expectEqualStrings("main", head.ref.name); 1330 1331 // make sure clone was successful 1332 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1333 defer hello_txt.close(io); 1334 } 1335 } 1336 1337 fn testFetchLarge( 1338 comptime repo_kind: rp.RepoKind, 1339 comptime transport_def: net.TransportDefinition, 1340 comptime shell_out_to_git: bool, 1341 comptime hash_kind: hash.HashKind, 1342 comptime port: u16, 1343 io: std.Io, 1344 allocator: std.mem.Allocator, 1345 ) !void { 1346 // create the temp dir 1347 var temp = std.testing.tmpDir(.{}); 1348 defer temp.cleanup(); 1349 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1350 defer allocator.free(temp_path); 1351 1352 // init server 1353 var server_process = try runServer(io, allocator, temp_path, port); 1354 defer _ = server_process.kill(io); 1355 1356 // register the repo under admin and locate its on-disk directory 1357 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1358 defer allocator.free(server_path); 1359 1360 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1361 defer server_repo.deinit(io, allocator); 1362 1363 // make a commit with files from the current repo 1364 const commit1 = try commitServer(&server_repo, io, allocator, .{ .dirs = &.{"src"} }, .{ .message = "let there be light" }); 1365 1366 // export server repo 1367 { 1368 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1369 defer export_file.close(io); 1370 } 1371 1372 if (shell_out_to_git) { 1373 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1374 } 1375 1376 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1377 defer allocator.free(client_path); 1378 1379 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1380 defer client_repo.deinit(io, allocator); 1381 1382 // add remote 1383 { 1384 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1385 defer allocator.free(remote_url); 1386 1387 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1388 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1389 } 1390 1391 const is_ssh = switch (transport_def) { 1392 .file => false, 1393 .wire => |wire_kind| .ssh == wire_kind, 1394 }; 1395 1396 if (shell_out_to_git) { 1397 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1398 defer allocator.free(ssh_cmd); 1399 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1400 defer allocator.free(ssh_config_arg); 1401 1402 { 1403 var process = try std.process.spawn(io, .{ 1404 .argv = if (is_ssh) 1405 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "origin", "master" } 1406 else 1407 &.{ "git", "pull", "--quiet", "origin", "master" }, 1408 .cwd = .{ .path = client_path }, 1409 .stdin = .ignore, 1410 .stdout = .ignore, 1411 .stderr = .inherit, 1412 }); 1413 defer process.kill(io); 1414 const term = try process.wait(io); 1415 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1416 } 1417 1418 // make sure pull was successful 1419 { 1420 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1421 try std.testing.expectEqualStrings(&commit1, &oid_master); 1422 } 1423 1424 // make another commit on the server 1425 const commit2 = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1426 1427 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1428 { 1429 var process = try std.process.spawn(io, .{ 1430 .argv = if (is_ssh) 1431 &.{ "git", "-c", ssh_config_arg, "fetch", "--quiet", "origin", "master" } 1432 else 1433 &.{ "git", "fetch", "--quiet", "origin", "master" }, 1434 .cwd = .{ .path = client_path }, 1435 .stdin = .ignore, 1436 .stdout = .ignore, 1437 .stderr = .inherit, 1438 }); 1439 defer process.kill(io); 1440 const term = try process.wait(io); 1441 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1442 } 1443 1444 // make sure fetch with want-ref was successful 1445 { 1446 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1447 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1448 } 1449 } else { 1450 const refspecs = &.{ 1451 "+refs/heads/master:refs/heads/master", 1452 }; 1453 1454 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1455 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1456 1457 try client_repo.fetch( 1458 io, 1459 allocator, 1460 "origin", 1461 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1462 .command = ssh_cmd_maybe, 1463 } } }, 1464 ); 1465 1466 // update the working dir 1467 try client_repo.restore(io, allocator, "."); 1468 } 1469 1470 // make sure fetch was successful 1471 { 1472 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1473 try std.testing.expectEqualStrings(&commit1, &oid_master); 1474 } 1475 } 1476 1477 fn testPushLarge( 1478 comptime repo_kind: rp.RepoKind, 1479 comptime transport_def: net.TransportDefinition, 1480 comptime shell_out_to_git: bool, 1481 comptime hash_kind: hash.HashKind, 1482 comptime port: u16, 1483 io: std.Io, 1484 allocator: std.mem.Allocator, 1485 ) !void { 1486 // create the temp dir 1487 var temp = std.testing.tmpDir(.{}); 1488 defer temp.cleanup(); 1489 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1490 defer allocator.free(temp_path); 1491 1492 // init server 1493 var server_process = try runServer(io, allocator, temp_path, port); 1494 defer _ = server_process.kill(io); 1495 1496 // register the repo under admin and locate its on-disk directory 1497 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1498 defer allocator.free(server_path); 1499 1500 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1501 defer server_repo.deinit(io, allocator); 1502 1503 // add config 1504 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1505 1506 // export server repo 1507 { 1508 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1509 defer export_file.close(io); 1510 } 1511 1512 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1513 defer allocator.free(client_path); 1514 1515 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1516 defer client_repo.deinit(io, allocator); 1517 1518 var client_dir = try temp.dir.openDir(io, "client", .{}); 1519 defer client_dir.close(io); 1520 1521 { 1522 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1523 defer hello_txt.close(io); 1524 try hello_txt.writeStreamingAll(io, "hello, world!"); 1525 try client_repo.add(io, allocator, &.{"hello.txt"}); 1526 } 1527 1528 // copy files from current repo into client dir 1529 const cwd = std.Io.Dir.cwd(); 1530 for (&[_][]const u8{"src"}) |dir_name| { 1531 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1532 defer src_repo_dir.close(io); 1533 1534 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1535 defer dest_repo_dir.close(io); 1536 1537 try copyDir(io, src_repo_dir, dest_repo_dir); 1538 1539 try client_repo.add(io, allocator, &.{dir_name}); 1540 } 1541 1542 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1543 1544 // change the files so git will send them as delta objects 1545 for (&[_][]const u8{"src"}) |dir_name| { 1546 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1547 defer dest_repo_dir.close(io); 1548 1549 { 1550 var iter = dest_repo_dir.iterate(); 1551 while (try iter.next(io)) |entry| { 1552 switch (entry.kind) { 1553 .file => { 1554 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1555 defer file.close(io); 1556 var writer = file.writer(io, &.{}); 1557 try writer.interface.writeAll("EDIT"); 1558 }, 1559 else => {}, 1560 } 1561 } 1562 } 1563 1564 try client_repo.add(io, allocator, &.{dir_name}); 1565 } 1566 1567 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1568 1569 // add remote 1570 { 1571 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1572 defer allocator.free(remote_url); 1573 1574 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1575 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1576 } 1577 1578 const is_ssh = switch (transport_def) { 1579 .file => false, 1580 .wire => |wire_kind| .ssh == wire_kind, 1581 }; 1582 1583 if (shell_out_to_git) { 1584 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1585 defer allocator.free(ssh_cmd); 1586 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1587 defer allocator.free(ssh_config_arg); 1588 1589 // shell out to git so it will send delta objects 1590 var process = try std.process.spawn(io, .{ 1591 .argv = if (is_ssh) 1592 &.{ "git", "-c", ssh_config_arg, "push", "--quiet", "origin", "master" } 1593 else 1594 &.{ "git", "push", "--quiet", "origin", "master" }, 1595 .cwd = .{ .path = client_path }, 1596 .stdin = .ignore, 1597 .stdout = .ignore, 1598 .stderr = .inherit, 1599 }); 1600 defer process.kill(io); 1601 const term = try process.wait(io); 1602 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1603 } else { 1604 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1605 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1606 1607 try client_repo.push( 1608 io, 1609 allocator, 1610 "origin", 1611 "master", 1612 false, 1613 .{ .wire = .{ .ssh = .{ 1614 .command = ssh_cmd_maybe, 1615 } } }, 1616 ); 1617 } 1618 1619 // make sure push was successful 1620 { 1621 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1622 try std.testing.expectEqualStrings(&commit2, &oid_master); 1623 1624 try std.testing.expectError(error.FileNotFound, server_repo.core.work_dir.openFile(io, "hello.txt", .{})); 1625 } 1626 } 1627 1628 fn testPushClearUndo( 1629 comptime hash_kind: hash.HashKind, 1630 comptime port: u16, 1631 io: std.Io, 1632 allocator: std.mem.Allocator, 1633 ) !void { 1634 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 1635 1636 // create the temp dir 1637 var temp = std.testing.tmpDir(.{}); 1638 defer temp.cleanup(); 1639 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1640 defer allocator.free(temp_path); 1641 1642 // init server 1643 var server_process = try runServer(io, allocator, temp_path, port); 1644 defer _ = server_process.kill(io); 1645 1646 // register the repo under admin and locate its on-disk directory 1647 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1648 defer allocator.free(server_path); 1649 { 1650 var server_repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .bare = true, .path = server_path }); 1651 defer server_repo.deinit(io, allocator); 1652 } 1653 1654 // a git client with one commit to push 1655 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1656 defer allocator.free(client_path); 1657 const commit = blk: { 1658 var client_repo = try rp.Repo(.git, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1659 defer client_repo.deinit(io, allocator); 1660 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1661 defer hello_txt.close(io); 1662 try hello_txt.writeStreamingAll(io, "hello, world!"); 1663 try client_repo.add(io, allocator, &.{"hello.txt"}); 1664 const remote_url = try remoteUrl(.{ .wire = .ssh }, allocator, "testrepo", port); 1665 defer allocator.free(remote_url); 1666 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1667 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1668 }; 1669 1670 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1671 defer allocator.free(ssh_cmd); 1672 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1673 defer allocator.free(ssh_config_arg); 1674 1675 { 1676 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master"}); 1677 defer allocator.free(result.stderr); 1678 try std.testing.expect(result.ok); 1679 } 1680 const pushed_count = try historyCount(repo_opts, io, allocator, server_path); 1681 try std.testing.expect(pushed_count > 1); 1682 1683 // the clear ref can only be deleted 1684 { 1685 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master:refs/haxy/undo"}); 1686 defer allocator.free(result.stderr); 1687 try std.testing.expect(!result.ok); 1688 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "can only be deleted") != null); 1689 } 1690 1691 // and only alone, refusing the whole push 1692 { 1693 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{ "master:refs/heads/other", ":refs/haxy/undo" }); 1694 defer allocator.free(result.stderr); 1695 try std.testing.expect(!result.ok); 1696 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "must be deleted alone") != null); 1697 } 1698 1699 // a refused push leaves no transaction behind 1700 try std.testing.expectEqual(pushed_count, try historyCount(repo_opts, io, allocator, server_path)); 1701 1702 { 1703 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{":refs/haxy/undo"}); 1704 defer allocator.free(result.stderr); 1705 try std.testing.expect(result.ok); 1706 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "Compacting database") != null); 1707 } 1708 1709 // one transaction is left, and it still holds the pushed branch 1710 try std.testing.expectEqual(1, try historyCount(repo_opts, io, allocator, server_path)); 1711 { 1712 var server_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = server_path }); 1713 defer server_repo.deinit(io, allocator); 1714 try std.testing.expectEqualStrings(&commit, &(try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?); 1715 try std.testing.expectEqual(null, try server_repo.readRef(io, .{ .kind = .head, .name = "other" })); 1716 } 1717 } 1718 1719 // whether a git push from the client succeeded, and what it printed. progress 1720 // is forced on, since git hides it when stderr isn't a terminal. 1721 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 } { 1722 var argv: std.ArrayList([]const u8) = .empty; 1723 defer argv.deinit(allocator); 1724 try argv.appendSlice(allocator, &.{ "git", "-c", ssh_config_arg, "push", "--progress", "origin" }); 1725 try argv.appendSlice(allocator, refspecs); 1726 const result = try std.process.run(allocator, io, .{ .argv = argv.items, .cwd = .{ .path = client_path } }); 1727 allocator.free(result.stdout); 1728 return .{ .ok = result.term == .exited and result.term.exited == 0, .stderr = result.stderr }; 1729 } 1730 1731 // the number of transactions in a repo's history. opened afresh each time, 1732 // since gc replaces the database file. 1733 fn historyCount(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, allocator: std.mem.Allocator, path: []const u8) !u64 { 1734 var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }); 1735 defer repo.deinit(io, allocator); 1736 const history = try rp.Repo(.xit, repo_opts).DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 1737 return try history.count(); 1738 } 1739 1740 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1741 var iter = src_dir.iterate(); 1742 while (try iter.next(io)) |entry| { 1743 switch (entry.kind) { 1744 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1745 .directory => { 1746 try dest_dir.createDirPath(io, entry.name); 1747 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1748 defer dest_entry_dir.close(io); 1749 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1750 defer src_entry_dir.close(io); 1751 try copyDir(io, src_entry_dir, dest_entry_dir); 1752 }, 1753 else => {}, 1754 } 1755 } 1756 } 1757 1758 // the dev SSH public key matching the private key written by runServer 1759 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1760 1761 // create the admin event repo with a user holding the dev SSH public key, plus 1762 // that user's repo 1763 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_path: []const u8) !void { 1764 const admin_repo_path = try std.Io.Dir.path.join(allocator, &.{ data_path, "admin" }); 1765 defer allocator.free(admin_repo_path); 1766 1767 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .bare = true, .path = admin_repo_path }); 1768 defer repo.deinit(io, allocator); 1769 1770 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1771 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1772 1773 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1774 const user_id = evt.EventWithId.randomId(prng.random()); 1775 1776 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1777 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1778 1779 const users_dir = try std.Io.Dir.path.join(allocator, &.{ data_path, "users" }); 1780 defer allocator.free(users_dir); 1781 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1782 .id = std.fmt.bytesToHex(user_id, .lower), 1783 .author = .{ .name = "admin", .email = "admin@example.test" }, 1784 .event = .{ .user = .{ 1785 .name = "admin", 1786 .email = "admin@example.test", 1787 .password_hash = password_hash, 1788 .ssh_keys = admin_ssh_key, 1789 } }, 1790 }}); 1791 1792 const user_repo_path = try evt.userRepoPath(allocator, users_dir, &user_id); 1793 defer allocator.free(user_repo_path); 1794 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 1795 defer user_repo.deinit(io, allocator); 1796 } 1797 1798 // resolve admin:<repo_name> to its on-disk directory under <data_dir>/users via 1799 // the event store 1800 fn repoOnDiskPath( 1801 io: std.Io, 1802 allocator: std.mem.Allocator, 1803 data_path: []const u8, 1804 repo_name: []const u8, 1805 create: bool, 1806 ) !?[]u8 { 1807 const admin_repo_path = try std.Io.Dir.path.join(allocator, &.{ data_path, "admin" }); 1808 defer allocator.free(admin_repo_path); 1809 const users_dir = try std.Io.Dir.path.join(allocator, &.{ data_path, "users" }); 1810 defer allocator.free(users_dir); 1811 1812 const location = (try evt.resolveOrCreateRepo( 1813 io, 1814 allocator, 1815 users_dir, 1816 admin_repo_path, 1817 "admin", 1818 repo_name, 1819 if (create) .{ .read_access = .public } else null, 1820 )) orelse return null; 1821 return try evt.repoPath(allocator, users_dir, &location.owner_id, &location.repo_id); 1822 } 1823 1824 // build the remote URL addressing admin:<repo_name> over the given transport 1825 fn remoteUrl( 1826 comptime transport_def: net.TransportDefinition, 1827 allocator: std.mem.Allocator, 1828 repo_name: []const u8, 1829 port: u16, 1830 ) ![]u8 { 1831 return switch (transport_def) { 1832 .file => unreachable, 1833 .wire => |wire_kind| switch (wire_kind) { 1834 .http => try allocator.print("http://localhost:{}/admin:{s}", .{ port, repo_name }), 1835 .raw => try allocator.print("git://localhost:{}/admin:{s}", .{ port, repo_name }), 1836 .ssh => try allocator.print("git@localhost:admin:{s}", .{repo_name}), 1837 }, 1838 }; 1839 } 1840 1841 // the ssh command the haxy git client should invoke, or null when not using 1842 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1843 // and skip writing to known_hosts. 1844 fn sshCommand( 1845 is_ssh: bool, 1846 allocator: std.mem.Allocator, 1847 temp_path: []const u8, 1848 port: u16, 1849 ) !?[]u8 { 1850 if (!is_ssh) return null; 1851 1852 const priv_key_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "key" }); 1853 defer allocator.free(priv_key_path); 1854 1855 if (builtin.target.os.tag == .windows) std.mem.replaceScalar(u8, priv_key_path, '\\', '/'); 1856 const null_path = if (builtin.target.os.tag == .windows) "NUL" else "/dev/null"; 1857 return try allocator.print("ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile={s} -o LogLevel=ERROR -o IdentitiesOnly=yes -i \"{s}\"", .{ port + 1, null_path, priv_key_path }); 1858 } 1859 1860 const ServerFiles = union(enum) { 1861 files: []const struct { path: []const u8, content: []const u8 }, 1862 dirs: []const []const u8, 1863 }; 1864 1865 /// commit files without writing the server's index or worktree. 1866 fn commitServer(repo: anytype, io: std.Io, allocator: std.mem.Allocator, files: ServerFiles, metadata: xit.object.CommitMetadata(repo.self_repo_opts.hash)) ![xit.hash.hexLen(repo.self_repo_opts.hash)]u8 { 1867 const R = @TypeOf(repo.*); 1868 const opts = repo.self_repo_opts; 1869 const Index = xit.index.Index(.xit, opts); 1870 var result: [xit.hash.hexLen(opts.hash)]u8 = undefined; 1871 const Ctx = struct { 1872 core: *R.Core, 1873 io: std.Io, 1874 allocator: std.mem.Allocator, 1875 files: ServerFiles, 1876 metadata: xit.object.CommitMetadata(opts.hash), 1877 result: *[xit.hash.hexLen(opts.hash)]u8, 1878 1879 fn addFile(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8, content: []const u8) !void { 1880 var reader = std.Io.Reader.fixed(content); 1881 var oid: [xit.hash.byteLen(opts.hash)]u8 = undefined; 1882 try xit.object.writeObject(.xit, opts, state, ctx.io, ctx.allocator, &reader, .{ .kind = .blob, .size = content.len }, &oid); 1883 const path_parts = try xit.fs.splitPath(ctx.allocator, path); 1884 defer ctx.allocator.free(path_parts); 1885 try index.addTreeEntryFile(&.{ .oid = oid, .mode = @bitCast(@as(u32, 0o100644)) }, path_parts, content.len, 0); 1886 } 1887 1888 fn addDir(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8) anyerror!void { 1889 var dir = try std.Io.Dir.cwd().openDir(ctx.io, path, .{ .iterate = true }); 1890 defer dir.close(ctx.io); 1891 var entries = dir.iterate(); 1892 while (try entries.next(ctx.io)) |entry| { 1893 const child = try std.Io.Dir.path.join(ctx.allocator, &.{ path, entry.name }); 1894 defer ctx.allocator.free(child); 1895 switch (entry.kind) { 1896 .directory => try ctx.addDir(state, index, child), 1897 .file => { 1898 const content = try dir.readFileAlloc(ctx.io, entry.name, ctx.allocator, .unlimited); 1899 defer ctx.allocator.free(content); 1900 try ctx.addFile(state, index, child, content); 1901 }, 1902 else => {}, 1903 } 1904 } 1905 } 1906 1907 pub fn run(ctx: @This(), cursor: *R.DB.Cursor(.read_write)) !void { 1908 var moment = try R.DB.HashMap(.read_write).init(cursor.*); 1909 const state = R.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1910 const head_oid = try rf.readHeadRecurMaybe(.xit, opts, state.readOnly(), ctx.io); 1911 var index = if (head_oid) |oid| try Index.initFromCommit(state.readOnly(), ctx.io, ctx.allocator, &oid) else try Index.init(state.readOnly(), ctx.io, ctx.allocator); 1912 defer index.deinit(); 1913 switch (ctx.files) { 1914 .files => |updates| for (updates) |file| try ctx.addFile(state, &index, file.path, file.content), 1915 .dirs => |dirs| for (dirs) |dir| try ctx.addDir(state, &index, dir), 1916 } 1917 var tree = try xit.object.Tree.initFromIndex(.xit, opts, state, ctx.io, ctx.allocator, &index); 1918 defer tree.deinit(); 1919 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 1920 const head = try rf.readHead(.xit, opts, state.readOnly(), ctx.io, &head_buffer) orelse return error.HeadNotFound; 1921 ctx.result.* = try xit.object.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, ctx.metadata, &tree, head.ref); 1922 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .commit = try .init(ctx.metadata.message, ctx.result.*, opts.max_read_size) }); 1923 } 1924 }; 1925 try repo.core.db_file.lock(io, .exclusive); 1926 defer repo.core.db_file.unlock(io); 1927 const history = try R.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1928 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 1929 .core = &repo.core, 1930 .io = io, 1931 .allocator = allocator, 1932 .files = files, 1933 .metadata = metadata, 1934 .result = &result, 1935 }); 1936 return result; 1937 } 1938