╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
999 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1000 const issue_event_id = evt.EventWithId.randomId(prng.random()); 1001 1002 const events_to_push = [_]evt.EventWithId{ 1003 .{ 1004 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1005 .author = .{ .name = "haxy", .email = "user@haxy" }, 1006 .event = .{ 1007 .issue = .{ 1008 .title = "Login form clears password on validation error", 1009 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 1010 .tags = "bug priority-high ui", 1011 }, 1012 }, 1013 }, 1014 }; 1015 1016 // commit the issue on the client and push it to the server 1017 try evt.consume(.repo, repo_kind, .{ .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 1018 try client_repo.push( 1019 io, 1020 allocator, 1021 "origin", 1022 "haxy/events", 1023 false, 1024 .{ .wire = .{ .ssh = .{ 1025 .command = ssh_cmd_maybe, 1026 } } }, 1027 ); 1028 1029 // the push consumed the issue into the server repo's db 1030 { 1031 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1032 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1033 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1034 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1035 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1036 1037 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1038 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.tags, issue.event.tags); 1039 } 1040 1041 // 1042 // edit the issue 1043 // 1044 1045 // this event edits the previous one because it has the same id 1046 const events_to_push2 = [_]evt.EventWithId{ 1047 .{ 1048 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1049 .author = .{ .name = "haxy", .email = "user@haxy" }, 1050 .event = .{ 1051 .issue = .{ 1052 .title = "Login form clears password on validation error", 1053 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1054 .tags = "bug priority-low ui", 1055 }, 1056 }, 1057 }, 1058 }; 1059 1060 // commit the edit on the client and push it to the server 1061 try evt.consume(.repo, repo_kind, .{ .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1062 try client_repo.push( 1063 io, 1064 allocator, 1065 "origin", 1066 "haxy/events", 1067 false, 1068 .{ .wire = .{ .ssh = .{ 1069 .command = ssh_cmd_maybe, 1070 } } }, 1071 ); 1072 1073 // the push consumed the edit into the server repo's db 1074 { 1075 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1076 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1077 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1078 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1079 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1080 1081 // the description and tags were correctly edited 1082 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1083 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.tags, issue.event.tags); 1084 } 1085 } 1086 1087 fn testClone( 1088 comptime repo_kind: rp.RepoKind, 1089 comptime transport_def: net.TransportDefinition, 1090 comptime shell_out_to_git: bool, 1091 comptime port: u16, 1092 io: std.Io, 1093 allocator: std.mem.Allocator, 1094 ) !void { 1095 const temp_dir_name = "temp-testnet-clone"; 1096 1097 // create the temp dir 1098 const cwd = std.Io.Dir.cwd(); 1099 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1100 if (temp_dir_or_err) |*temp_dir| { 1101 temp_dir.close(io); 1102 try cwd.deleteTree(io, temp_dir_name); 1103 } else |_| {} 1104 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1105 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1106 defer temp_dir.close(io); 1107 1108 // init server 1109 var server_process = try runServer(io, allocator, temp_dir_name, port); 1110 defer _ = server_process.kill(io); 1111 1112 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1113 defer allocator.free(cwd_path); 1114 1115 const temp_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1116 defer allocator.free(temp_path); 1117 1118 // register the repo under admin and locate its on-disk directory 1119 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1120 defer allocator.free(server_path); 1121 1122 // init server repo with default branch name as main 1123 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1124 var server_repo = try rp.Repo(.xit, .{ .is_test = !shell_out_to_git }).init(io, allocator, .{ .path = server_path, .create_default_branch = "main" }); 1125 defer server_repo.deinit(io, allocator); 1126 1127 if (shell_out_to_git) { 1128 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1129 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1130 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1131 } 1132 1133 // make a commit 1134 { 1135 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1136 defer hello_txt.close(io); 1137 try hello_txt.writeStreamingAll(io, "hello, world!"); 1138 try server_repo.add(io, allocator, &.{"hello.txt"}); 1139 _ = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 1140 } 1141 1142 // tag first commit 1143 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1144 1145 // make a commit 1146 { 1147 const goodbye_txt = try server_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 1148 defer goodbye_txt.close(io); 1149 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 1150 try server_repo.add(io, allocator, &.{"goodbye.txt"}); 1151 _ = try server_repo.commit(io, allocator, .{ .message = "add goodbye file" }); 1152 } 1153 1154 // export server repo 1155 { 1156 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1157 defer export_file.close(io); 1158 } 1159 1160 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1161 defer allocator.free(client_path); 1162 1163 // get remote url 1164 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1165 defer allocator.free(remote_url); 1166 1167 const is_ssh = switch (transport_def) { 1168 .file => false, 1169 .wire => |wire_kind| .ssh == wire_kind, 1170 }; 1171 1172 if (shell_out_to_git) { 1173 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1174 defer allocator.free(priv_key_path); 1175 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1176 defer allocator.free(ssh_config_arg); 1177 1178 { 1179 var process = try std.process.spawn(io, .{ 1180 .argv = if (is_ssh) 1181 &.{ "git", "-c", ssh_config_arg, "clone", "--depth", "1", remote_url, "client" } 1182 else 1183 &.{ "git", "clone", "--depth", "1", remote_url, "client" }, 1184 .cwd = .{ .path = temp_path }, 1185 .stdin = .ignore, 1186 .stdout = .ignore, 1187 .stderr = .ignore, 1188 }); 1189 const term = try process.wait(io); 1190 if (term != .exited or term.exited != 0) { 1191 return error.GitCommandFailed; 1192 } 1193 } 1194 1195 // make sure shallow clone was successful 1196 { 1197 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1198 hello_txt.close(io); 1199 } 1200 1201 // make a third commit on the server 1202 { 1203 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1204 defer extra_txt.close(io); 1205 try extra_txt.writeStreamingAll(io, "extra content"); 1206 try server_repo.add(io, allocator, &.{"extra.txt"}); 1207 _ = try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1208 } 1209 1210 // pull --unshallow to deepen the clone and get the new commit 1211 { 1212 var process = try std.process.spawn(io, .{ 1213 .argv = if (is_ssh) 1214 &.{ "git", "-c", ssh_config_arg, "pull", "--unshallow" } 1215 else 1216 &.{ "git", "pull", "--unshallow" }, 1217 .cwd = .{ .path = client_path }, 1218 .stdin = .ignore, 1219 .stdout = .ignore, 1220 .stderr = .ignore, 1221 }); 1222 const term = try process.wait(io); 1223 if (term != .exited or term.exited != 0) { 1224 return error.GitCommandFailed; 1225 } 1226 } 1227 1228 // make sure unshallow pull was successful 1229 { 1230 const extra_txt = try temp_dir.openFile(io, "client/extra.txt", .{}); 1231 extra_txt.close(io); 1232 } 1233 1234 // delete client and clone again with --shallow-since 1235 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1236 1237 { 1238 var process = try std.process.spawn(io, .{ 1239 .argv = if (is_ssh) 1240 &.{ "git", "-c", ssh_config_arg, "clone", "--shallow-since=2000-01-01", remote_url, "client" } 1241 else 1242 &.{ "git", "clone", "--shallow-since=2000-01-01", remote_url, "client" }, 1243 .cwd = .{ .path = temp_path }, 1244 .stdin = .ignore, 1245 .stdout = .ignore, 1246 .stderr = .ignore, 1247 }); 1248 const term = try process.wait(io); 1249 if (term != .exited or term.exited != 0) { 1250 return error.GitCommandFailed; 1251 } 1252 } 1253 1254 // make sure shallow clone was successful 1255 { 1256 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1257 hello_txt.close(io); 1258 } 1259 1260 // delete client and clone again with --shallow-exclude 1261 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1262 1263 { 1264 var process = try std.process.spawn(io, .{ 1265 .argv = if (is_ssh) 1266 &.{ "git", "-c", ssh_config_arg, "clone", "--shallow-exclude=v1", remote_url, "client" } 1267 else 1268 &.{ "git", "clone", "--shallow-exclude=v1", remote_url, "client" }, 1269 .cwd = .{ .path = temp_path }, 1270 .stdin = .ignore, 1271 .stdout = .ignore, 1272 .stderr = .ignore, 1273 }); 1274 const term = try process.wait(io); 1275 if (term != .exited or term.exited != 0) { 1276 return error.GitCommandFailed; 1277 } 1278 } 1279 1280 // make sure shallow clone was successful 1281 { 1282 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1283 hello_txt.close(io); 1284 } 1285 1286 // delete client and clone again with --filter=blob:none 1287 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1288 1289 { 1290 var process = try std.process.spawn(io, .{ 1291 .argv = if (is_ssh) 1292 &.{ "git", "-c", ssh_config_arg, "clone", "--filter=blob:none", remote_url, "client" } 1293 else 1294 &.{ "git", "clone", "--filter=blob:none", remote_url, "client" }, 1295 .cwd = .{ .path = temp_path }, 1296 .stdin = .ignore, 1297 .stdout = .ignore, 1298 .stderr = .ignore, 1299 }); 1300 const term = try process.wait(io); 1301 if (term != .exited or term.exited != 0) { 1302 return error.GitCommandFailed; 1303 } 1304 } 1305 1306 // make sure partial clone was successful 1307 { 1308 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1309 hello_txt.close(io); 1310 } 1311 1312 // delete client and clone again with --filter=tree:0 1313 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1314 1315 { 1316 var process = try std.process.spawn(io, .{ 1317 .argv = if (is_ssh) 1318 &.{ "git", "-c", ssh_config_arg, "clone", "--filter=tree:0", remote_url, "client" } 1319 else 1320 &.{ "git", "clone", "--filter=tree:0", remote_url, "client" }, 1321 .cwd = .{ .path = temp_path }, 1322 .stdin = .ignore, 1323 .stdout = .ignore, 1324 .stderr = .ignore, 1325 }); 1326 const term = try process.wait(io); 1327 if (term != .exited or term.exited != 0) { 1328 return error.GitCommandFailed; 1329 } 1330 } 1331 1332 // make sure treeless clone was successful 1333 { 1334 const goodbye_txt = try temp_dir.openFile(io, "client/goodbye.txt", .{}); 1335 goodbye_txt.close(io); 1336 } 1337 } else { 1338 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1339 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1340 1341 // clone repo 1342 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).clone( 1343 io, 1344 allocator, 1345 remote_url, 1346 temp_path, 1347 client_path, 1348 null, 1349 .{ .wire = .{ .ssh = .{ 1350 .command = ssh_cmd_maybe, 1351 } } }, 1352 ); 1353 defer client_repo.deinit(io, allocator); 1354 1355 // make sure HEAD points to the right default branch 1356 var current_branch_buffer = [_]u8{0} ** rf.MAX_REF_CONTENT_SIZE; 1357 const head = try client_repo.head(io, &current_branch_buffer); 1358 try std.testing.expectEqualStrings("main", head.ref.name); 1359 1360 // make sure clone was successful 1361 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1362 defer hello_txt.close(io); 1363 } 1364 } 1365 1366 fn testFetchLarge( 1367 comptime repo_kind: rp.RepoKind, 1368 comptime transport_def: net.TransportDefinition, 1369 comptime shell_out_to_git: bool, 1370 comptime port: u16, 1371 io: std.Io, 1372 allocator: std.mem.Allocator, 1373 ) !void { 1374 const temp_dir_name = "temp-testnet-fetch-large"; 1375 1376 // create the temp dir 1377 const cwd = std.Io.Dir.cwd(); 1378 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1379 if (temp_dir_or_err) |*temp_dir| { 1380 temp_dir.close(io); 1381 try cwd.deleteTree(io, temp_dir_name); 1382 } else |_| {} 1383 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1384 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1385 defer temp_dir.close(io); 1386 1387 // init server 1388 var server_process = try runServer(io, allocator, temp_dir_name, port); 1389 defer _ = server_process.kill(io); 1390 1391 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1392 defer allocator.free(cwd_path); 1393 1394 // register the repo under admin and locate its on-disk directory 1395 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1396 defer allocator.free(server_path); 1397 1398 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1399 defer server_repo.deinit(io, allocator); 1400 1401 var server_dir = try cwd.openDir(io, server_path, .{}); 1402 defer server_dir.close(io); 1403 1404 // copy files from current repo into server dir 1405 for (&[_][]const u8{"src"}) |dir_name| { 1406 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1407 defer src_repo_dir.close(io); 1408 1409 var dest_repo_dir = try server_dir.createDirPathOpen(io, dir_name, .{}); 1410 defer dest_repo_dir.close(io); 1411 1412 try copyDir(io, src_repo_dir, dest_repo_dir); 1413 1414 try server_repo.add(io, allocator, &.{dir_name}); 1415 } 1416 1417 // make a commit 1418 const commit1 = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 1419 1420 // export server repo 1421 { 1422 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1423 defer export_file.close(io); 1424 } 1425 1426 if (shell_out_to_git) { 1427 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1428 } 1429 1430 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1431 defer allocator.free(client_path); 1432 1433 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1434 defer client_repo.deinit(io, allocator); 1435 1436 // add remote 1437 { 1438 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1439 defer allocator.free(remote_url); 1440 1441 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1442 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1443 } 1444 1445 const is_ssh = switch (transport_def) { 1446 .file => false, 1447 .wire => |wire_kind| .ssh == wire_kind, 1448 }; 1449 1450 if (shell_out_to_git) { 1451 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1452 defer allocator.free(priv_key_path); 1453 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1454 defer allocator.free(ssh_config_arg); 1455 1456 { 1457 var process = try std.process.spawn(io, .{ 1458 .argv = if (is_ssh) 1459 &.{ "git", "-c", ssh_config_arg, "pull", "origin", "master" } 1460 else 1461 &.{ "git", "pull", "origin", "master" }, 1462 .cwd = .{ .path = client_path }, 1463 .stdin = .ignore, 1464 .stdout = .ignore, 1465 .stderr = .ignore, 1466 }); 1467 const term = try process.wait(io); 1468 if (term != .exited or term.exited != 0) { 1469 return error.GitCommandFailed; 1470 } 1471 } 1472 1473 // make sure pull was successful 1474 { 1475 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1476 try std.testing.expectEqualStrings(&commit1, &oid_master); 1477 } 1478 1479 // make another commit on the server 1480 const commit2 = blk: { 1481 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1482 defer extra_txt.close(io); 1483 try extra_txt.writeStreamingAll(io, "extra content"); 1484 try server_repo.add(io, allocator, &.{"extra.txt"}); 1485 break :blk try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1486 }; 1487 1488 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1489 { 1490 var process = try std.process.spawn(io, .{ 1491 .argv = if (is_ssh) 1492 &.{ "git", "-c", ssh_config_arg, "fetch", "origin", "master" } 1493 else 1494 &.{ "git", "fetch", "origin", "master" }, 1495 .cwd = .{ .path = client_path }, 1496 .stdin = .ignore, 1497 .stdout = .ignore, 1498 .stderr = .ignore, 1499 }); 1500 const term = try process.wait(io); 1501 if (term != .exited or term.exited != 0) { 1502 return error.GitCommandFailed; 1503 } 1504 } 1505 1506 // make sure fetch with want-ref was successful 1507 { 1508 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1509 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1510 } 1511 } else { 1512 const refspecs = &.{ 1513 "+refs/heads/master:refs/heads/master", 1514 }; 1515 1516 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1517 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1518 1519 try client_repo.fetch( 1520 io, 1521 allocator, 1522 "origin", 1523 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1524 .command = ssh_cmd_maybe, 1525 } } }, 1526 ); 1527 1528 // update the working dir 1529 try client_repo.restore(io, allocator, "."); 1530 } 1531 1532 // make sure fetch was successful 1533 { 1534 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1535 try std.testing.expectEqualStrings(&commit1, &oid_master); 1536 } 1537 } 1538 1539 fn testPushLarge( 1540 comptime repo_kind: rp.RepoKind, 1541 comptime transport_def: net.TransportDefinition, 1542 comptime shell_out_to_git: bool, 1543 comptime port: u16, 1544 io: std.Io, 1545 allocator: std.mem.Allocator, 1546 ) !void { 1547 const temp_dir_name = "temp-testnet-push-large"; 1548 1549 // create the temp dir 1550 const cwd = std.Io.Dir.cwd(); 1551 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1552 if (temp_dir_or_err) |*temp_dir| { 1553 temp_dir.close(io); 1554 try cwd.deleteTree(io, temp_dir_name); 1555 } else |_| {} 1556 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1557 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1558 defer temp_dir.close(io); 1559 1560 // init server 1561 var server_process = try runServer(io, allocator, temp_dir_name, port); 1562 defer _ = server_process.kill(io); 1563 1564 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1565 defer allocator.free(cwd_path); 1566 1567 // register the repo under admin and locate its on-disk directory 1568 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1569 defer allocator.free(server_path); 1570 1571 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1572 defer server_repo.deinit(io, allocator); 1573 1574 // add config 1575 try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1576 try server_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 1577 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1578 1579 // export server repo 1580 { 1581 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1582 defer export_file.close(io); 1583 } 1584 1585 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1586 defer allocator.free(client_path); 1587 1588 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1589 defer client_repo.deinit(io, allocator); 1590 1591 var client_dir = try cwd.openDir(io, client_path, .{}); 1592 defer client_dir.close(io); 1593 1594 { 1595 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1596 defer hello_txt.close(io); 1597 try hello_txt.writeStreamingAll(io, "hello, world!"); 1598 try client_repo.add(io, allocator, &.{"hello.txt"}); 1599 } 1600 1601 // copy files from current repo into client dir 1602 for (&[_][]const u8{"src"}) |dir_name| { 1603 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1604 defer src_repo_dir.close(io); 1605 1606 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1607 defer dest_repo_dir.close(io); 1608 1609 try copyDir(io, src_repo_dir, dest_repo_dir); 1610 1611 try client_repo.add(io, allocator, &.{dir_name}); 1612 } 1613 1614 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1615 1616 // change the files so git will send them as delta objects 1617 for (&[_][]const u8{"src"}) |dir_name| { 1618 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1619 defer dest_repo_dir.close(io); 1620 1621 { 1622 var iter = dest_repo_dir.iterate(); 1623 while (try iter.next(io)) |entry| { 1624 switch (entry.kind) { 1625 .file => { 1626 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1627 defer file.close(io); 1628 var writer = file.writer(io, &.{}); 1629 try writer.interface.writeAll("EDIT"); 1630 }, 1631 else => {}, 1632 } 1633 } 1634 } 1635 1636 try client_repo.add(io, allocator, &.{dir_name}); 1637 } 1638 1639 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1640 1641 // add remote 1642 { 1643 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1644 defer allocator.free(remote_url); 1645 1646 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1647 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1648 } 1649 1650 const is_ssh = switch (transport_def) { 1651 .file => false, 1652 .wire => |wire_kind| .ssh == wire_kind, 1653 }; 1654 1655 if (shell_out_to_git) { 1656 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1657 defer allocator.free(priv_key_path); 1658 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1659 defer allocator.free(ssh_config_arg); 1660 1661 // shell out to git so it will send delta objects 1662 var process = try std.process.spawn(io, .{ 1663 .argv = if (is_ssh) 1664 &.{ "git", "-c", ssh_config_arg, "push", "origin", "master" } 1665 else 1666 &.{ "git", "push", "origin", "master" }, 1667 .cwd = .{ .path = client_path }, 1668 .stdin = .ignore, 1669 .stdout = .ignore, 1670 .stderr = .ignore, 1671 }); 1672 const term = try process.wait(io); 1673 if (term != .exited or term.exited != 0) { 1674 return error.GitCommandFailed; 1675 } 1676 } else { 1677 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1678 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1679 1680 try client_repo.push( 1681 io, 1682 allocator, 1683 "origin", 1684 "master", 1685 false, 1686 .{ .wire = .{ .ssh = .{ 1687 .command = ssh_cmd_maybe, 1688 } } }, 1689 ); 1690 } 1691 1692 // make sure push was successful 1693 { 1694 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1695 try std.testing.expectEqualStrings(&commit2, &oid_master); 1696 1697 const hello_txt = try server_repo.core.work_dir.openFile(io, "hello.txt", .{}); 1698 defer hello_txt.close(io); 1699 } 1700 } 1701 1702 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1703 var iter = src_dir.iterate(); 1704 while (try iter.next(io)) |entry| { 1705 switch (entry.kind) { 1706 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1707 .directory => { 1708 try dest_dir.createDirPath(io, entry.name); 1709 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1710 defer dest_entry_dir.close(io); 1711 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1712 defer src_entry_dir.close(io); 1713 try copyDir(io, src_entry_dir, dest_entry_dir); 1714 }, 1715 else => {}, 1716 } 1717 } 1718 } 1719 1720 // the dev SSH public key matching the private key written by runServer 1721 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1722 1723 // create the admin event repo with a user holding the dev SSH public key 1724 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_dir_name: []const u8) !void { 1725 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1726 defer allocator.free(cwd_path); 1727 1728 const admin_repo_path = try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "admin" }); 1729 defer allocator.free(admin_repo_path); 1730 1731 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_repo_path }); 1732 defer repo.deinit(io, allocator); 1733 1734 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1735 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1736 1737 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1738 const user_id = evt.EventWithId.randomId(prng.random()); 1739 1740 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1741 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1742 1743 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1744 .id = std.fmt.bytesToHex(user_id, .lower), 1745 .author = .{ .name = "admin", .email = "admin@example.test" }, 1746 .event = .{ .user = .{ 1747 .name = "admin", 1748 .email = "admin@example.test", 1749 .password_hash = password_hash, 1750 .ssh_keys = admin_ssh_key, 1751 } }, 1752 }}); 1753 } 1754 1755 // resolve admin/<repo_name> to its on-disk directory under <data_dir>/repos via 1756 // the event store 1757 fn repoOnDiskPath( 1758 io: std.Io, 1759 allocator: std.mem.Allocator, 1760 data_dir_name: []const u8, 1761 repo_name: []const u8, 1762 create: bool, 1763 ) !?[]u8 { 1764 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1765 defer allocator.free(cwd_path); 1766 1767 const admin_repo_path = try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "admin" }); 1768 defer allocator.free(admin_repo_path); 1769 1770 const event_id_hex = (try evt.resolveOrCreateRepo( 1771 io, 1772 allocator, 1773 admin_repo_path, 1774 "admin", 1775 repo_name, 1776 if (create) .{ .read_access = .public } else null, 1777 )) orelse return null; 1778 return try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "repos", &event_id_hex }); 1779 } 1780 1781 // build the remote URL addressing admin/<repo_name> over the given transport 1782 fn remoteUrl( 1783 comptime transport_def: net.TransportDefinition, 1784 allocator: std.mem.Allocator, 1785 repo_name: []const u8, 1786 port: u16, 1787 ) ![]u8 { 1788 return switch (transport_def) { 1789 .file => unreachable, 1790 .wire => |wire_kind| switch (wire_kind) { 1791 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/admin/{s}", .{ port, repo_name }), 1792 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/admin/{s}", .{ port, repo_name }), 1793 .ssh => try std.fmt.allocPrint(allocator, "git@localhost:admin/{s}", .{repo_name}), 1794 }, 1795 }; 1796 } 1797 1798 // the ssh command the haxy git client should invoke, or null when not using 1799 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1800 // and skip writing to known_hosts. 1801 fn sshCommand( 1802 is_ssh: bool, 1803 allocator: std.mem.Allocator, 1804 cwd_path: []const u8, 1805 temp_dir_name: []const u8, 1806 port: u16, 1807 ) !?[]u8 { 1808 if (!is_ssh) return null; 1809 1810 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1811 defer allocator.free(priv_key_path); 1812 1813 return try std.fmt.allocPrint(allocator, "ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o IdentitiesOnly=yes -o IdentityFile=\"{s}\"", .{ port + 1, priv_key_path }); 1814 } 1815