╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
228 oid_refspec, 229 }; 230 231 const is_ssh = switch (transport_def) { 232 .file => false, 233 .wire => |wire_kind| .ssh == wire_kind, 234 }; 235 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 236 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 237 238 try client_repo.fetch( 239 io, 240 allocator, 241 "origin", 242 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 243 .command = ssh_cmd_maybe, 244 } } }, 245 ); 246 247 // update the working dir 248 try client_repo.restore(io, allocator, "."); 249 250 // make sure fetch was successful 251 { 252 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 253 defer hello_txt.close(io); 254 255 try std.testing.expect(null != try client_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 256 try std.testing.expect(null != try client_repo.readRef(io, .{ .kind = .head, .name = "foo" })); 257 258 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 259 try std.testing.expectEqualStrings(&commit1, &oid_master); 260 } 261 262 // make another commit 263 const commit2 = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "goodbye.txt", .content = "goodbye, world!" }} }, .{ .message = "goodbye" }); 264 265 try client_repo.fetch( 266 io, 267 allocator, 268 "origin", 269 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 270 .command = ssh_cmd_maybe, 271 } } }, 272 ); 273 274 // update the working dir 275 try client_repo.restore(io, allocator, "."); 276 277 // make sure fetch was successful 278 { 279 const goodbye_txt = try temp.dir.openFile(io, "client/goodbye.txt", .{}); 280 defer goodbye_txt.close(io); 281 282 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 283 try std.testing.expectEqualStrings(&commit2, &oid_master); 284 } 285 } 286 287 fn testPush( 288 comptime repo_kind: rp.RepoKind, 289 comptime transport_def: net.TransportDefinition, 290 comptime hash_kind: hash.HashKind, 291 comptime port: u16, 292 io: std.Io, 293 allocator: std.mem.Allocator, 294 ) !void { 295 // create the temp dir 296 var temp = std.testing.tmpDir(.{}); 297 defer temp.cleanup(); 298 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 299 defer allocator.free(temp_path); 300 301 // init server 302 var server_process = try runServer(io, allocator, temp_path, port); 303 defer _ = server_process.kill(io); 304 305 // register the repo under admin and locate its on-disk directory 306 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 307 defer allocator.free(server_path); 308 309 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 310 defer server_repo.deinit(io, allocator); 311 312 // add config 313 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 314 315 // export server repo 316 { 317 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 318 defer export_file.close(io); 319 } 320 321 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 322 defer allocator.free(client_path); 323 324 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 325 defer client_repo.deinit(io, allocator); 326 327 // make a commit 328 const commit1 = blk: { 329 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 330 defer hello_txt.close(io); 331 try hello_txt.writeStreamingAll(io, "hello, world!"); 332 try client_repo.add(io, allocator, &.{"hello.txt"}); 333 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 334 }; 335 336 // add a tag 337 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 338 339 // add remote 340 { 341 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 342 defer allocator.free(remote_url); 343 344 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 345 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 346 } 347 348 const refspecs = &.{ 349 "refs/tags/1.0.0:refs/tags/1.0.0", 350 }; 351 352 const is_ssh = switch (transport_def) { 353 .file => false, 354 .wire => |wire_kind| .ssh == wire_kind, 355 }; 356 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 357 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 358 359 try client_repo.push( 360 io, 361 allocator, 362 "origin", 363 "master", 364 false, 365 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 366 .command = ssh_cmd_maybe, 367 } } }, 368 ); 369 370 // make sure push was successful 371 { 372 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 373 374 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 375 try std.testing.expectEqualStrings(&commit1, &oid_master); 376 } 377 378 // make a commit on the server 379 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world from the server!" }} }, .{ .message = "new commit from the server" }); 380 381 // make another commit 382 const commit2 = blk: { 383 const goodbye_txt = try client_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 384 defer goodbye_txt.close(io); 385 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 386 try client_repo.add(io, allocator, &.{"goodbye.txt"}); 387 break :blk try client_repo.commit(io, allocator, .{ .message = "goodbye" }); 388 }; 389 390 // can't push because server has commit not found locally 391 try std.testing.expectError(error.RemoteRefContainsCommitsNotFoundLocally, client_repo.push( 392 io, 393 allocator, 394 "origin", 395 "master", 396 false, 397 .{ .wire = .{ .ssh = .{ 398 .command = ssh_cmd_maybe, 399 } } }, 400 )); 401 402 // make a commit on the server with no parents, thus creating an incompatible git history 403 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world from the server again!" }} }, .{ .message = "new git history on the server", .parent_oids = &.{} }); 404 405 // can't push because commit doesn't exist locally 406 try std.testing.expectError(error.RemoteRefContainsCommitsNotFoundLocally, client_repo.push( 407 io, 408 allocator, 409 "origin", 410 "master", 411 false, 412 .{ .wire = .{ .ssh = .{ 413 .command = ssh_cmd_maybe, 414 } } }, 415 )); 416 417 // retrieve the commit object 418 try client_repo.fetch( 419 io, 420 allocator, 421 "origin", 422 .{ .wire = .{ .ssh = .{ 423 .command = ssh_cmd_maybe, 424 } } }, 425 ); 426 427 // can't push because server's history is incompatible 428 try std.testing.expectError(error.RemoteRefContainsIncompatibleHistory, client_repo.push( 429 io, 430 allocator, 431 "origin", 432 "master", 433 false, 434 .{ .wire = .{ .ssh = .{ 435 .command = ssh_cmd_maybe, 436 } } }, 437 )); 438 439 // test denyNonFastForwards 440 { 441 // set denyNonFastForwards on server 442 try server_repo.addConfig(io, allocator, .{ .name = "receive.denynonfastforwards", .value = "true" }); 443 444 // save the server's current master ref 445 const oid_before_denied_push = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 446 447 // force push should be rejected by server due to denyNonFastForwards 448 try std.testing.expectError(error.RemoteRejectedRef, client_repo.push( 449 io, 450 allocator, 451 "origin", 452 "master", 453 true, 454 .{ .wire = .{ .ssh = .{ 455 .command = ssh_cmd_maybe, 456 } } }, 457 )); 458 459 // verify the server ref was not updated (push was denied) 460 { 461 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 462 try std.testing.expectEqualStrings(&oid_before_denied_push, &oid_master); 463 } 464 465 // remove denyNonFastForwards from server 466 try server_repo.removeConfig(io, allocator, .{ .name = "receive.denynonfastforwards" }); 467 } 468 469 // force push 470 try client_repo.push( 471 io, 472 allocator, 473 "origin", 474 "master", 475 true, 476 .{ .wire = .{ .ssh = .{ 477 .command = ssh_cmd_maybe, 478 } } }, 479 ); 480 481 // make sure push was successful 482 { 483 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 484 try std.testing.expectEqualStrings(&commit2, &oid_master); 485 } 486 487 // remove the remote tag 488 try client_repo.push( 489 io, 490 allocator, 491 "origin", 492 ":refs/tags/1.0.0", 493 false, 494 .{ .wire = .{ .ssh = .{ 495 .command = ssh_cmd_maybe, 496 } } }, 497 ); 498 499 // make sure push was successful 500 try std.testing.expect(null == try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 501 } 502 503 fn testPushFork( 504 comptime hash_kind: hash.HashKind, 505 comptime port: u16, 506 io: std.Io, 507 allocator: std.mem.Allocator, 508 ) !void { 509 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind }; 510 511 var temp = std.testing.tmpDir(.{}); 512 defer temp.cleanup(); 513 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 514 defer allocator.free(temp_path); 515 516 var server_process = try runServer(io, allocator, temp_path, port); 517 defer _ = server_process.kill(io); 518 519 const target_path = (try repoOnDiskPath(io, allocator, temp_path, "target", true)).?; 520 defer allocator.free(target_path); 521 const base_oid = blk: { 522 var target_repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .bare = true, .path = target_path }); 523 defer target_repo.deinit(io, allocator); 524 break :blk try commitServer(&target_repo, io, allocator, .{ .files = &.{.{ .path = "main.txt", .content = "base contents\n" }} }, .{ .author = "admin <admin@example.test>", .message = "initial code" }); 525 }; 526 527 const other_path = (try repoOnDiskPath(io, allocator, temp_path, "other", true)).?; 528 defer allocator.free(other_path); 529 530 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 531 defer allocator.free(admin_path); 532 const repos_dir = try std.fs.path.join(allocator, &.{ temp_path, "repos" }); 533 defer allocator.free(repos_dir); 534 535 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 536 const fork_id = evt.EventWithId.randomId(prng.random()); 537 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); 538 var user_id: [evt.event_id_size]u8 = undefined; 539 var repo_id: [evt.event_id_size]u8 = undefined; 540 const draft_path = blk: { 541 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 542 defer admin.deinit(io, allocator); 543 var arena = std.heap.ArenaAllocator.init(allocator); 544 defer arena.deinit(); 545 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 546 const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, "admin", "target")) orelse return error.NotFound; 547 repo_id = target.event_id; 548 @memcpy(&user_id, target.repo.event.user_id); 549 break :blk try fork.create(repo_opts, io, allocator, repos_dir, &admin, .{ 550 .id = fork_id_hex, 551 .user_id = user_id, 552 .repo_id = repo_id, 553 .title = "add a feature", 554 .description = "a draft patch", 555 .tags = "enhancement", 556 .target_branch = "master", 557 .author = .{ .name = "admin", .email = "admin@example.test" }, 558 .timestamp = 1, 559 }); 560 }; 561 defer allocator.free(draft_path); 562 563 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 564 defer allocator.free(client_path); 565 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 566 defer allocator.free(ssh_cmd); 567 var client = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 568 io, 569 allocator, 570 "git@localhost:admin/target", 571 client_path, 572 client_path, 573 null, 574 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 575 ); 576 defer client.deinit(io, allocator); 577 578 const source_oid = blk: { 579 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 580 defer file.close(io); 581 try file.writeStreamingAll(io, "fork contents\n"); 582 try client.add(io, allocator, &.{"feature.txt"}); 583 break :blk try client.commit(io, allocator, .{ .message = "add feature" }); 584 }; 585 586 const remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 587 defer allocator.free(remote_url); 588 try client.addRemote(io, allocator, .{ .name = "patch", .value = remote_url }); 589 try client.push(io, allocator, "patch", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 590 591 var first_revision_id: [evt.event_id_size]u8 = undefined; 592 { 593 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 594 defer draft.deinit(io, allocator); 595 try std.testing.expect(try draft.isBare(io, allocator)); 596 var arena = std.heap.ArenaAllocator.init(allocator); 597 defer arena.deinit(); 598 const moment = try evt.currentMoment(repo_opts, &draft); 599 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 600 try std.testing.expect(!patch.removed); 601 try std.testing.expectEqualStrings(&source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 602 const selected = patch.event.revision orelse return error.NotFound; 603 try std.testing.expectEqualStrings("master", patch.event.target_branch); 604 try std.testing.expectEqualStrings(&source_oid, selected.source_oid); 605 try std.testing.expectEqual(@as(?u64, 1), selected.commit_count); 606 first_revision_id = try evt.parseEventId(&selected.id); 607 const revision = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &first_revision_id)) orelse return error.NotFound; 608 try std.testing.expectEqualStrings(&source_oid, revision.event.source_oid); 609 } 610 611 // 612 // publish, then push another revision 613 // 614 615 { 616 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 617 defer admin.deinit(io, allocator); 618 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 619 defer target.deinit(io, allocator); 620 try target.addBranch(io, .{ .name = "feature" }); 621 try pch.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{ 622 .id = fork_id_hex, 623 .user_id = user_id, 624 .repo_id = repo_id, 625 .author = .{ .name = "admin", .email = "admin@example.test" }, 626 .timestamp = 2, 627 }); 628 var arena = std.heap.ArenaAllocator.init(allocator); 629 defer arena.deinit(); 630 const moment = try evt.currentMoment(repo_opts, &target); 631 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 632 try evt.Patch.update(.server, .xit, repo_opts, io, allocator, &target, &fork_id, .{ .fields = .{ 633 .title = "edited feature", 634 .description = patch.event.description, 635 .tags = patch.event.tags, 636 .target_branch = "feature", 637 } }, .{ .name = "admin", .email = "admin@example.test" }); 638 } 639 640 const feature_remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 641 defer allocator.free(feature_remote_url); 642 try client.addRemote(io, allocator, .{ .name = "patch-to-feature", .value = feature_remote_url }); 643 644 const second_source_oid = blk: { 645 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 646 defer file.close(io); 647 try file.writeStreamingAll(io, "updated fork contents\n"); 648 try client.add(io, allocator, &.{"feature.txt"}); 649 break :blk try client.commit(io, allocator, .{ .message = "revise feature" }); 650 }; 651 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 652 653 var second_revision_id: [evt.event_id_size]u8 = undefined; 654 var second_patch_oid: [hash.hexLen(hash_kind)]u8 = undefined; 655 { 656 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 657 defer draft.deinit(io, allocator); 658 var arena = std.heap.ArenaAllocator.init(allocator); 659 defer arena.deinit(); 660 const moment = try evt.currentMoment(repo_opts, &draft); 661 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 662 try std.testing.expect(patch.removed); 663 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 664 second_revision_id = newest.id; 665 @memcpy(&second_patch_oid, newest.record.patch_oid); 666 try std.testing.expect(!std.mem.eql(u8, &first_revision_id, &second_revision_id)); 667 try std.testing.expectEqualStrings(&second_source_oid, newest.record.event.source_oid); 668 try std.testing.expectEqualStrings("edited feature", newest.record.event.message); 669 } 670 { 671 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 672 defer target.deinit(io, allocator); 673 var arena = std.heap.ArenaAllocator.init(allocator); 674 defer arena.deinit(); 675 const moment = try evt.currentMoment(repo_opts, &target); 676 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 677 try std.testing.expectEqualStrings("edited feature", patch.event.title); 678 const selected = patch.event.revision orelse return error.NotFound; 679 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(second_revision_id, .lower), &selected.id); 680 try std.testing.expectEqualStrings(&second_source_oid, selected.source_oid); 681 try std.testing.expectEqual(@as(?u64, 2), selected.commit_count); 682 try std.testing.expectEqualStrings("feature", patch.event.target_branch); 683 } 684 685 // 686 // reject extra refs without applying the patch update 687 // 688 689 const third_source_oid = blk: { 690 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 691 defer file.close(io); 692 try file.writeStreamingAll(io, "rejected fork contents\n"); 693 try client.add(io, allocator, &.{"feature.txt"}); 694 break :blk try client.commit(io, allocator, .{ .message = "rejected revision" }); 695 }; 696 const extra_ref_rejected = if (client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ 697 .refspecs = &.{"refs/heads/master:refs/heads/extra"}, 698 .wire = .{ .ssh = .{ .command = ssh_cmd } }, 699 })) |_| false else |_| true; 700 try std.testing.expect(extra_ref_rejected); 701 { 702 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 703 defer draft.deinit(io, allocator); 704 try std.testing.expectEqualStrings(&second_source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 705 try std.testing.expectEqual(null, try draft.readRef(io, .{ .kind = .head, .name = "extra" })); 706 var arena = std.heap.ArenaAllocator.init(allocator); 707 defer arena.deinit(); 708 const moment = try evt.currentMoment(repo_opts, &draft); 709 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 710 try std.testing.expectEqualSlices(u8, &second_revision_id, &newest.id); 711 } 712 713 // 714 // a removed target patch stays removed after another push 715 // 716 717 { 718 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 719 defer target.deinit(io, allocator); 720 try evt.remove(.server, .repo, .xit, repo_opts, io, allocator, &target, &fork_id, .patch, .{ .name = "admin", .email = "admin@example.test" }); 721 } 722 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 723 { 724 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 725 defer target.deinit(io, allocator); 726 var arena = std.heap.ArenaAllocator.init(allocator); 727 defer arena.deinit(); 728 const moment = try evt.currentMoment(repo_opts, &target); 729 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 730 try std.testing.expect(patch.removed); 731 } 732 733 // 734 // restore the patch, merge a descendant of its source oid, and push the merge 735 // 736 737 { 738 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 739 defer target.deinit(io, allocator); 740 var arena = std.heap.ArenaAllocator.init(allocator); 741 defer arena.deinit(); 742 const moment = try evt.currentMoment(repo_opts, &target); 743 const removed = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 744 try evt.consume(.server, .repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 745 .id = fork_id_hex, 746 .timestamp = 4, 747 .author = .{ .name = "admin", .email = "admin@example.test" }, 748 .event = .{ .patch = removed.event }, 749 }}); 750 } 751 const merge_parents = [_][hash.hexLen(hash_kind)]u8{ base_oid, third_source_oid }; 752 const merge_oid = try client.commit(io, allocator, .{ .message = "merge patch", .parent_oids = &merge_parents }); 753 // later commits in the push must not be mistaken for the merge itself 754 _ = try client.commit(io, allocator, .{ .message = "after merge", .allow_empty = true }); 755 try client.push(io, allocator, "origin", "master:feature", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 756 { 757 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 758 defer target.deinit(io, allocator); 759 var arena = std.heap.ArenaAllocator.init(allocator); 760 defer arena.deinit(); 761 const moment = try evt.currentMoment(repo_opts, &target); 762 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 763 try std.testing.expectEqual(.merged, patch.event.status.kind()); 764 const merged_status = switch (patch.event.status) { 765 .merged => |value| value, 766 else => return error.InvalidPatch, 767 }; 768 try std.testing.expectEqual(.source, merged_status.revision); 769 const result_id = try evt.parseEventId(&merged_status.patchrev_id); 770 const result = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &result_id)) orelse return error.NotFound; 771 try std.testing.expectEqualStrings(&base_oid, result.event.base_oid); 772 try std.testing.expectEqualStrings(&merge_oid, result.event.source_oid); 773 const imported = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &second_revision_id)) orelse return error.NotFound; 774 try std.testing.expectEqualStrings(&second_patch_oid, imported.patch_oid); 775 } 776 777 const other_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/other/patch:{s}", .{&fork_id_hex}); 778 defer allocator.free(other_url); 779 try client.addRemote(io, allocator, .{ .name = "other", .value = other_url }); 780 // early rejections need no input. give ssh eof so windows openssh 781 // doesn't hang closing stdin before forwarding the error. 782 const reject_ssh_cmd = try std.fmt.allocPrint(allocator, "{s} -n", .{ssh_cmd}); 783 defer allocator.free(reject_ssh_cmd); 784 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "other", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 785 786 // 787 // the fork remains cloneable after the target repo is removed 788 // 789 790 { 791 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 792 defer admin.deinit(io, allocator); 793 try evt.remove(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, &repo_id, .repo, .{ .name = "admin", .email = "admin@example.test" }); 794 } 795 const clone_path = try std.fs.path.join(allocator, &.{ temp_path, "fork-clone" }); 796 defer allocator.free(clone_path); 797 var fork_clone = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 798 io, 799 allocator, 800 remote_url, 801 clone_path, 802 clone_path, 803 null, 804 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 805 ); 806 defer fork_clone.deinit(io, allocator); 807 try std.testing.expectEqualStrings(&third_source_oid, &(try fork_clone.readRef(io, fork.ref) orelse return error.NotFound)); 808 809 { 810 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 811 defer admin.deinit(io, allocator); 812 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 813 .{ 814 .id = std.fmt.bytesToHex(repo_id, .lower), 815 .timestamp = 2, 816 .author = .{ .name = "admin", .email = "admin@example.test" }, 817 .event = .{ .repo = .{ 818 .user_id = &user_id, 819 .name = "target", 820 .description = "", 821 } }, 822 }, 823 .{ 824 .id = std.fmt.bytesToHex(user_id, .lower), 825 .timestamp = 2, 826 .author = .{ .name = "admin", .email = "admin@example.test" }, 827 .event = .{ .user = .{ 828 .name = "admin", 829 .email = "admin@example.test", 830 .password_hash = "", 831 .ssh_keys = "", 832 } }, 833 }, 834 }); 835 } 836 837 const denied_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 838 defer allocator.free(denied_url); 839 try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); 840 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "denied", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 841 } 842 843 fn testPushCreatesMissingRepo( 844 comptime repo_kind: rp.RepoKind, 845 comptime transport_def: net.TransportDefinition, 846 comptime port: u16, 847 io: std.Io, 848 allocator: std.mem.Allocator, 849 ) !void { 850 var temp = std.testing.tmpDir(.{}); 851 defer temp.cleanup(); 852 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 853 defer allocator.free(temp_path); 854 855 var server_process = try runServer(io, allocator, temp_path, port); 856 defer _ = server_process.kill(io); 857 858 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 859 defer allocator.free(client_path); 860 861 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 862 defer client_repo.deinit(io, allocator); 863 864 const commit1 = blk: { 865 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 866 defer hello_txt.close(io); 867 try hello_txt.writeStreamingAll(io, "hello, world!"); 868 try client_repo.add(io, allocator, &.{"hello.txt"}); 869 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 870 }; 871 872 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 873 874 // the repo is not pre-registered; the push should mint the repo event under 875 // admin and create the repo on disk 876 { 877 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 878 defer allocator.free(remote_url); 879 880 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 881 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 882 } 883 884 const is_ssh = switch (transport_def) { 885 .file => false, 886 .wire => |wire_kind| .ssh == wire_kind, 887 }; 888 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 889 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 890 891 try client_repo.push( 892 io, 893 allocator, 894 "origin", 895 "master", 896 false, 897 .{ .refspecs = &.{"refs/tags/1.0.0:refs/tags/1.0.0"}, .wire = .{ .ssh = .{ 898 .command = ssh_cmd_maybe, 899 } } }, 900 ); 901 902 // the push registered admin/server; resolve its on-disk directory 903 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", false)).?; 904 defer allocator.free(server_path); 905 906 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).open(io, allocator, .{ .path = server_path }); 907 defer server_repo.deinit(io, allocator); 908 try std.testing.expect(try server_repo.isBare(io, allocator)); 909 910 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 911 912 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 913 try std.testing.expectEqualStrings(&commit1, &oid_master); 914 } 915 916 fn testPushEvents( 917 comptime repo_kind: rp.RepoKind, 918 comptime transport_def: net.TransportDefinition, 919 comptime hash_kind: hash.HashKind, 920 comptime port: u16, 921 io: std.Io, 922 allocator: std.mem.Allocator, 923 ) !void { 924 // create the temp dir 925 var temp = std.testing.tmpDir(.{}); 926 defer temp.cleanup(); 927 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 928 defer allocator.free(temp_path); 929 930 // init server 931 var server_process = try runServer(io, allocator, temp_path, port); 932 defer _ = server_process.kill(io); 933 934 // register the repo under admin and locate its on-disk directory 935 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 936 defer allocator.free(server_path); 937 938 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 939 const ServerRepo = rp.Repo(.xit, repo_opts); 940 var server_repo = try ServerRepo.init(io, allocator, .{ .bare = true, .path = server_path }); 941 defer server_repo.deinit(io, allocator); 942 943 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 944 defer allocator.free(client_path); 945 946 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 947 defer client_repo.deinit(io, allocator); 948 949 // add remote 950 { 951 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 952 defer allocator.free(remote_url); 953 954 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 955 } 956 957 const is_ssh = switch (transport_def) { 958 .file => false, 959 .wire => |wire_kind| .ssh == wire_kind, 960 }; 961 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 962 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 963 964 var arena = std.heap.ArenaAllocator.init(allocator); 965 defer arena.deinit(); 966 967 // 968 // define test events 969 // 970 971 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 972 const issue_event_id = evt.EventWithId.randomId(prng.random()); 973 974 const events_to_push = [_]evt.EventWithId{ 975 .{ 976 .id = std.fmt.bytesToHex(issue_event_id, .lower), 977 .author = .{ .name = "haxy", .email = "user@haxy" }, 978 .event = .{ 979 .issue = .{ 980 .title = "Login form clears password on validation error", 981 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 982 .tags = "bug priority-high ui", 983 }, 984 }, 985 }, 986 }; 987 988 // commit the issue on the client and push it to the server 989 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 990 try client_repo.push( 991 io, 992 allocator, 993 "origin", 994 "haxy/events", 995 false, 996 .{ .wire = .{ .ssh = .{ 997 .command = ssh_cmd_maybe, 998 } } }, 999 ); 1000 1001 // the push consumed the issue into the server repo's db 1002 { 1003 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1004 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1005 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1006 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1007 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1008 1009 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1010 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.tags, issue.event.tags); 1011 } 1012 1013 // 1014 // edit the issue 1015 // 1016 1017 // this event edits the previous one because it has the same id 1018 const events_to_push2 = [_]evt.EventWithId{ 1019 .{ 1020 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1021 .author = .{ .name = "haxy", .email = "user@haxy" }, 1022 .event = .{ 1023 .issue = .{ 1024 .title = "Login form clears password on validation error", 1025 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1026 .tags = "bug priority-low ui", 1027 }, 1028 }, 1029 }, 1030 }; 1031 1032 // commit the edit on the client and push it to the server 1033 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1034 try client_repo.push( 1035 io, 1036 allocator, 1037 "origin", 1038 "haxy/events", 1039 false, 1040 .{ .wire = .{ .ssh = .{ 1041 .command = ssh_cmd_maybe, 1042 } } }, 1043 ); 1044 1045 // the push consumed the edit into the server repo's db 1046 { 1047 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1048 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1049 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1050 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1051 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1052 1053 // the description and tags were correctly edited 1054 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1055 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.tags, issue.event.tags); 1056 } 1057 } 1058 1059 fn testClone( 1060 comptime repo_kind: rp.RepoKind, 1061 comptime transport_def: net.TransportDefinition, 1062 comptime shell_out_to_git: bool, 1063 comptime hash_kind: hash.HashKind, 1064 comptime port: u16, 1065 io: std.Io, 1066 allocator: std.mem.Allocator, 1067 ) !void { 1068 // create the temp dir 1069 var temp = std.testing.tmpDir(.{}); 1070 defer temp.cleanup(); 1071 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1072 defer allocator.free(temp_path); 1073 1074 // init server 1075 var server_process = try runServer(io, allocator, temp_path, port); 1076 defer _ = server_process.kill(io); 1077 1078 // register the repo under admin and locate its on-disk directory 1079 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1080 defer allocator.free(server_path); 1081 1082 // init server repo with default branch name as main 1083 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1084 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" }); 1085 defer server_repo.deinit(io, allocator); 1086 1087 if (shell_out_to_git) { 1088 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1089 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1090 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1091 } 1092 1093 // make a commit 1094 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world!" }} }, .{ .message = "let there be light" }); 1095 1096 // tag first commit 1097 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1098 1099 // make a commit 1100 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "goodbye.txt", .content = "goodbye, world!" }} }, .{ .message = "add goodbye file" }); 1101 1102 // export server repo 1103 { 1104 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1105 defer export_file.close(io); 1106 } 1107 1108 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1109 defer allocator.free(client_path); 1110 1111 // get remote url 1112 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1113 defer allocator.free(remote_url); 1114 1115 const is_ssh = switch (transport_def) { 1116 .file => false, 1117 .wire => |wire_kind| .ssh == wire_kind, 1118 }; 1119 1120 if (shell_out_to_git) { 1121 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1122 defer allocator.free(ssh_cmd); 1123 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1124 defer allocator.free(ssh_config_arg); 1125 1126 { 1127 var process = try std.process.spawn(io, .{ 1128 .argv = if (is_ssh) 1129 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--depth", "1", remote_url, "client" } 1130 else 1131 &.{ "git", "clone", "--quiet", "--depth", "1", remote_url, "client" }, 1132 .cwd = .{ .path = temp_path }, 1133 .stdin = .ignore, 1134 .stdout = .ignore, 1135 .stderr = .inherit, 1136 }); 1137 defer process.kill(io); 1138 const term = try process.wait(io); 1139 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1140 } 1141 1142 // make sure shallow clone was successful 1143 { 1144 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1145 hello_txt.close(io); 1146 } 1147 1148 // make a third commit on the server 1149 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1150 1151 // pull --unshallow to deepen the clone and get the new commit 1152 { 1153 var process = try std.process.spawn(io, .{ 1154 .argv = if (is_ssh) 1155 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "--unshallow" } 1156 else 1157 &.{ "git", "pull", "--quiet", "--unshallow" }, 1158 .cwd = .{ .path = client_path }, 1159 .stdin = .ignore, 1160 .stdout = .ignore, 1161 .stderr = .inherit, 1162 }); 1163 defer process.kill(io); 1164 const term = try process.wait(io); 1165 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1166 } 1167 1168 // make sure unshallow pull was successful 1169 { 1170 const extra_txt = try temp.dir.openFile(io, "client/extra.txt", .{}); 1171 extra_txt.close(io); 1172 } 1173 1174 // delete client and clone again with --shallow-since 1175 try temp.dir.deleteTree(io, "client"); 1176 1177 { 1178 var process = try std.process.spawn(io, .{ 1179 .argv = if (is_ssh) 1180 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" } 1181 else 1182 &.{ "git", "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" }, 1183 .cwd = .{ .path = temp_path }, 1184 .stdin = .ignore, 1185 .stdout = .ignore, 1186 .stderr = .inherit, 1187 }); 1188 defer process.kill(io); 1189 const term = try process.wait(io); 1190 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1191 } 1192 1193 // make sure shallow clone was successful 1194 { 1195 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1196 hello_txt.close(io); 1197 } 1198 1199 // delete client and clone again with --shallow-exclude 1200 try temp.dir.deleteTree(io, "client"); 1201 1202 { 1203 var process = try std.process.spawn(io, .{ 1204 .argv = if (is_ssh) 1205 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" } 1206 else 1207 &.{ "git", "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" }, 1208 .cwd = .{ .path = temp_path }, 1209 .stdin = .ignore, 1210 .stdout = .ignore, 1211 .stderr = .inherit, 1212 }); 1213 defer process.kill(io); 1214 const term = try process.wait(io); 1215 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1216 } 1217 1218 // make sure shallow clone was successful 1219 { 1220 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1221 hello_txt.close(io); 1222 } 1223 1224 // delete client and clone again with --filter=blob:none 1225 try temp.dir.deleteTree(io, "client"); 1226 1227 { 1228 var process = try std.process.spawn(io, .{ 1229 .argv = if (is_ssh) 1230 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=blob:none", remote_url, "client" } 1231 else 1232 &.{ "git", "clone", "--quiet", "--filter=blob:none", remote_url, "client" }, 1233 .cwd = .{ .path = temp_path }, 1234 .stdin = .ignore, 1235 .stdout = .ignore, 1236 .stderr = .inherit, 1237 }); 1238 defer process.kill(io); 1239 const term = try process.wait(io); 1240 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1241 } 1242 1243 // make sure partial clone was successful 1244 { 1245 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1246 hello_txt.close(io); 1247 } 1248 1249 // delete client and clone again with --filter=tree:0 1250 try temp.dir.deleteTree(io, "client"); 1251 1252 { 1253 var process = try std.process.spawn(io, .{ 1254 .argv = if (is_ssh) 1255 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=tree:0", remote_url, "client" } 1256 else 1257 &.{ "git", "clone", "--quiet", "--filter=tree:0", remote_url, "client" }, 1258 .cwd = .{ .path = temp_path }, 1259 .stdin = .ignore, 1260 .stdout = .ignore, 1261 .stderr = .inherit, 1262 }); 1263 defer process.kill(io); 1264 const term = try process.wait(io); 1265 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1266 } 1267 1268 // make sure treeless clone was successful 1269 { 1270 const goodbye_txt = try temp.dir.openFile(io, "client/goodbye.txt", .{}); 1271 goodbye_txt.close(io); 1272 } 1273 } else { 1274 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1275 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1276 1277 // clone repo 1278 var clone_result = try rp.AnyRepo(repo_kind, .{ .is_test = true }).clone( 1279 io, 1280 allocator, 1281 remote_url, 1282 temp_path, 1283 client_path, 1284 null, 1285 .{ .transport = .{ .wire = .{ .ssh = .{ 1286 .command = ssh_cmd_maybe, 1287 } } } }, 1288 ); 1289 defer clone_result.deinit(io, allocator); 1290 const client_repo = &@field(clone_result, @tagName(hash_kind)); 1291 1292 // make sure HEAD points to the right default branch 1293 var current_branch_buffer = [_]u8{0} ** rf.MAX_REF_CONTENT_SIZE; 1294 const head = try client_repo.head(io, &current_branch_buffer); 1295 try std.testing.expectEqualStrings("main", head.ref.name); 1296 1297 // make sure clone was successful 1298 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1299 defer hello_txt.close(io); 1300 } 1301 } 1302 1303 fn testFetchLarge( 1304 comptime repo_kind: rp.RepoKind, 1305 comptime transport_def: net.TransportDefinition, 1306 comptime shell_out_to_git: bool, 1307 comptime hash_kind: hash.HashKind, 1308 comptime port: u16, 1309 io: std.Io, 1310 allocator: std.mem.Allocator, 1311 ) !void { 1312 // create the temp dir 1313 var temp = std.testing.tmpDir(.{}); 1314 defer temp.cleanup(); 1315 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1316 defer allocator.free(temp_path); 1317 1318 // init server 1319 var server_process = try runServer(io, allocator, temp_path, port); 1320 defer _ = server_process.kill(io); 1321 1322 // register the repo under admin and locate its on-disk directory 1323 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1324 defer allocator.free(server_path); 1325 1326 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1327 defer server_repo.deinit(io, allocator); 1328 1329 // make a commit with files from the current repo 1330 const commit1 = try commitServer(&server_repo, io, allocator, .{ .dirs = &.{"src"} }, .{ .message = "let there be light" }); 1331 1332 // export server repo 1333 { 1334 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1335 defer export_file.close(io); 1336 } 1337 1338 if (shell_out_to_git) { 1339 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1340 } 1341 1342 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1343 defer allocator.free(client_path); 1344 1345 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1346 defer client_repo.deinit(io, allocator); 1347 1348 // add remote 1349 { 1350 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1351 defer allocator.free(remote_url); 1352 1353 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1354 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1355 } 1356 1357 const is_ssh = switch (transport_def) { 1358 .file => false, 1359 .wire => |wire_kind| .ssh == wire_kind, 1360 }; 1361 1362 if (shell_out_to_git) { 1363 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1364 defer allocator.free(ssh_cmd); 1365 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1366 defer allocator.free(ssh_config_arg); 1367 1368 { 1369 var process = try std.process.spawn(io, .{ 1370 .argv = if (is_ssh) 1371 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "origin", "master" } 1372 else 1373 &.{ "git", "pull", "--quiet", "origin", "master" }, 1374 .cwd = .{ .path = client_path }, 1375 .stdin = .ignore, 1376 .stdout = .ignore, 1377 .stderr = .inherit, 1378 }); 1379 defer process.kill(io); 1380 const term = try process.wait(io); 1381 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1382 } 1383 1384 // make sure pull was successful 1385 { 1386 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1387 try std.testing.expectEqualStrings(&commit1, &oid_master); 1388 } 1389 1390 // make another commit on the server 1391 const commit2 = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1392 1393 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1394 { 1395 var process = try std.process.spawn(io, .{ 1396 .argv = if (is_ssh) 1397 &.{ "git", "-c", ssh_config_arg, "fetch", "--quiet", "origin", "master" } 1398 else 1399 &.{ "git", "fetch", "--quiet", "origin", "master" }, 1400 .cwd = .{ .path = client_path }, 1401 .stdin = .ignore, 1402 .stdout = .ignore, 1403 .stderr = .inherit, 1404 }); 1405 defer process.kill(io); 1406 const term = try process.wait(io); 1407 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1408 } 1409 1410 // make sure fetch with want-ref was successful 1411 { 1412 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1413 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1414 } 1415 } else { 1416 const refspecs = &.{ 1417 "+refs/heads/master:refs/heads/master", 1418 }; 1419 1420 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1421 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1422 1423 try client_repo.fetch( 1424 io, 1425 allocator, 1426 "origin", 1427 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1428 .command = ssh_cmd_maybe, 1429 } } }, 1430 ); 1431 1432 // update the working dir 1433 try client_repo.restore(io, allocator, "."); 1434 } 1435 1436 // make sure fetch was successful 1437 { 1438 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1439 try std.testing.expectEqualStrings(&commit1, &oid_master); 1440 } 1441 } 1442 1443 fn testPushLarge( 1444 comptime repo_kind: rp.RepoKind, 1445 comptime transport_def: net.TransportDefinition, 1446 comptime shell_out_to_git: bool, 1447 comptime hash_kind: hash.HashKind, 1448 comptime port: u16, 1449 io: std.Io, 1450 allocator: std.mem.Allocator, 1451 ) !void { 1452 // create the temp dir 1453 var temp = std.testing.tmpDir(.{}); 1454 defer temp.cleanup(); 1455 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1456 defer allocator.free(temp_path); 1457 1458 // init server 1459 var server_process = try runServer(io, allocator, temp_path, port); 1460 defer _ = server_process.kill(io); 1461 1462 // register the repo under admin and locate its on-disk directory 1463 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1464 defer allocator.free(server_path); 1465 1466 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1467 defer server_repo.deinit(io, allocator); 1468 1469 // add config 1470 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1471 1472 // export server repo 1473 { 1474 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1475 defer export_file.close(io); 1476 } 1477 1478 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1479 defer allocator.free(client_path); 1480 1481 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1482 defer client_repo.deinit(io, allocator); 1483 1484 var client_dir = try temp.dir.openDir(io, "client", .{}); 1485 defer client_dir.close(io); 1486 1487 { 1488 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1489 defer hello_txt.close(io); 1490 try hello_txt.writeStreamingAll(io, "hello, world!"); 1491 try client_repo.add(io, allocator, &.{"hello.txt"}); 1492 } 1493 1494 // copy files from current repo into client dir 1495 const cwd = std.Io.Dir.cwd(); 1496 for (&[_][]const u8{"src"}) |dir_name| { 1497 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1498 defer src_repo_dir.close(io); 1499 1500 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1501 defer dest_repo_dir.close(io); 1502 1503 try copyDir(io, src_repo_dir, dest_repo_dir); 1504 1505 try client_repo.add(io, allocator, &.{dir_name}); 1506 } 1507 1508 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1509 1510 // change the files so git will send them as delta objects 1511 for (&[_][]const u8{"src"}) |dir_name| { 1512 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1513 defer dest_repo_dir.close(io); 1514 1515 { 1516 var iter = dest_repo_dir.iterate(); 1517 while (try iter.next(io)) |entry| { 1518 switch (entry.kind) { 1519 .file => { 1520 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1521 defer file.close(io); 1522 var writer = file.writer(io, &.{}); 1523 try writer.interface.writeAll("EDIT"); 1524 }, 1525 else => {}, 1526 } 1527 } 1528 } 1529 1530 try client_repo.add(io, allocator, &.{dir_name}); 1531 } 1532 1533 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1534 1535 // add remote 1536 { 1537 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1538 defer allocator.free(remote_url); 1539 1540 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1541 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1542 } 1543 1544 const is_ssh = switch (transport_def) { 1545 .file => false, 1546 .wire => |wire_kind| .ssh == wire_kind, 1547 }; 1548 1549 if (shell_out_to_git) { 1550 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1551 defer allocator.free(ssh_cmd); 1552 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1553 defer allocator.free(ssh_config_arg); 1554 1555 // shell out to git so it will send delta objects 1556 var process = try std.process.spawn(io, .{ 1557 .argv = if (is_ssh) 1558 &.{ "git", "-c", ssh_config_arg, "push", "--quiet", "origin", "master" } 1559 else 1560 &.{ "git", "push", "--quiet", "origin", "master" }, 1561 .cwd = .{ .path = client_path }, 1562 .stdin = .ignore, 1563 .stdout = .ignore, 1564 .stderr = .inherit, 1565 }); 1566 defer process.kill(io); 1567 const term = try process.wait(io); 1568 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1569 } else { 1570 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1571 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1572 1573 try client_repo.push( 1574 io, 1575 allocator, 1576 "origin", 1577 "master", 1578 false, 1579 .{ .wire = .{ .ssh = .{ 1580 .command = ssh_cmd_maybe, 1581 } } }, 1582 ); 1583 } 1584 1585 // make sure push was successful 1586 { 1587 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1588 try std.testing.expectEqualStrings(&commit2, &oid_master); 1589 1590 try std.testing.expectError(error.FileNotFound, server_repo.core.work_dir.openFile(io, "hello.txt", .{})); 1591 } 1592 } 1593 1594 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1595 var iter = src_dir.iterate(); 1596 while (try iter.next(io)) |entry| { 1597 switch (entry.kind) { 1598 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1599 .directory => { 1600 try dest_dir.createDirPath(io, entry.name); 1601 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1602 defer dest_entry_dir.close(io); 1603 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1604 defer src_entry_dir.close(io); 1605 try copyDir(io, src_entry_dir, dest_entry_dir); 1606 }, 1607 else => {}, 1608 } 1609 } 1610 } 1611 1612 // the dev SSH public key matching the private key written by runServer 1613 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1614 1615 // create the admin event repo with a user holding the dev SSH public key 1616 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_path: []const u8) !void { 1617 const admin_repo_path = try std.fs.path.join(allocator, &.{ data_path, "admin" }); 1618 defer allocator.free(admin_repo_path); 1619 1620 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .bare = true, .path = admin_repo_path }); 1621 defer repo.deinit(io, allocator); 1622 1623 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1624 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1625 1626 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1627 const user_id = evt.EventWithId.randomId(prng.random()); 1628 1629 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1630 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1631 1632 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1633 .id = std.fmt.bytesToHex(user_id, .lower), 1634 .author = .{ .name = "admin", .email = "admin@example.test" }, 1635 .event = .{ .user = .{ 1636 .name = "admin", 1637 .email = "admin@example.test", 1638 .password_hash = password_hash, 1639 .ssh_keys = admin_ssh_key, 1640 } }, 1641 }}); 1642 } 1643 1644 // resolve admin/<repo_name> to its on-disk directory under <data_dir>/repos via 1645 // the event store 1646 fn repoOnDiskPath( 1647 io: std.Io, 1648 allocator: std.mem.Allocator, 1649 data_path: []const u8, 1650 repo_name: []const u8, 1651 create: bool, 1652 ) !?[]u8 { 1653 const admin_repo_path = try std.fs.path.join(allocator, &.{ data_path, "admin" }); 1654 defer allocator.free(admin_repo_path); 1655 1656 const event_id_hex = (try evt.resolveOrCreateRepo( 1657 io, 1658 allocator, 1659 admin_repo_path, 1660 "admin", 1661 repo_name, 1662 if (create) .{ .read_access = .public } else null, 1663 )) orelse return null; 1664 return try std.fs.path.join(allocator, &.{ data_path, "repos", &event_id_hex }); 1665 } 1666 1667 // build the remote URL addressing admin/<repo_name> over the given transport 1668 fn remoteUrl( 1669 comptime transport_def: net.TransportDefinition, 1670 allocator: std.mem.Allocator, 1671 repo_name: []const u8, 1672 port: u16, 1673 ) ![]u8 { 1674 return switch (transport_def) { 1675 .file => unreachable, 1676 .wire => |wire_kind| switch (wire_kind) { 1677 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/admin/{s}", .{ port, repo_name }), 1678 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/admin/{s}", .{ port, repo_name }), 1679 .ssh => try std.fmt.allocPrint(allocator, "git@localhost:admin/{s}", .{repo_name}), 1680 }, 1681 }; 1682 } 1683 1684 // the ssh command the haxy git client should invoke, or null when not using 1685 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1686 // and skip writing to known_hosts. 1687 fn sshCommand( 1688 is_ssh: bool, 1689 allocator: std.mem.Allocator, 1690 temp_path: []const u8, 1691 port: u16, 1692 ) !?[]u8 { 1693 if (!is_ssh) return null; 1694 1695 const priv_key_path = try std.fs.path.join(allocator, &.{ temp_path, "key" }); 1696 defer allocator.free(priv_key_path); 1697 1698 if (builtin.os.tag == .windows) std.mem.replaceScalar(u8, priv_key_path, '\\', '/'); 1699 const null_path = if (builtin.os.tag == .windows) "NUL" else "/dev/null"; 1700 return try std.fmt.allocPrint(allocator, "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 }); 1701 } 1702 1703 const ServerFiles = union(enum) { 1704 files: []const struct { path: []const u8, content: []const u8 }, 1705 dirs: []const []const u8, 1706 }; 1707 1708 /// commit files without writing the server's index or worktree. 1709 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 { 1710 const R = @TypeOf(repo.*); 1711 const opts = repo.self_repo_opts; 1712 const Index = xit.index.Index(.xit, opts); 1713 var result: [xit.hash.hexLen(opts.hash)]u8 = undefined; 1714 const Ctx = struct { 1715 core: *R.Core, 1716 io: std.Io, 1717 allocator: std.mem.Allocator, 1718 files: ServerFiles, 1719 metadata: xit.object.CommitMetadata(opts.hash), 1720 result: *[xit.hash.hexLen(opts.hash)]u8, 1721 1722 fn addFile(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8, content: []const u8) !void { 1723 var reader = std.Io.Reader.fixed(content); 1724 var oid: [xit.hash.byteLen(opts.hash)]u8 = undefined; 1725 try xit.object.writeObject(.xit, opts, state, ctx.io, ctx.allocator, &reader, .{ .kind = .blob, .size = content.len }, &oid); 1726 const path_parts = try xit.fs.splitPath(ctx.allocator, path); 1727 defer ctx.allocator.free(path_parts); 1728 try index.addTreeEntryFile(&.{ .oid = oid, .mode = @bitCast(@as(u32, 0o100644)) }, path_parts, content.len, 0); 1729 } 1730 1731 fn addDir(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8) anyerror!void { 1732 var dir = try std.Io.Dir.cwd().openDir(ctx.io, path, .{ .iterate = true }); 1733 defer dir.close(ctx.io); 1734 var entries = dir.iterate(); 1735 while (try entries.next(ctx.io)) |entry| { 1736 const child = try std.fs.path.join(ctx.allocator, &.{ path, entry.name }); 1737 defer ctx.allocator.free(child); 1738 switch (entry.kind) { 1739 .directory => try ctx.addDir(state, index, child), 1740 .file => { 1741 const content = try dir.readFileAlloc(ctx.io, entry.name, ctx.allocator, .unlimited); 1742 defer ctx.allocator.free(content); 1743 try ctx.addFile(state, index, child, content); 1744 }, 1745 else => {}, 1746 } 1747 } 1748 } 1749 1750 pub fn run(ctx: @This(), cursor: *R.DB.Cursor(.read_write)) !void { 1751 var moment = try R.DB.HashMap(.read_write).init(cursor.*); 1752 const state = R.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1753 const head_oid = try rf.readHeadRecurMaybe(.xit, opts, state.readOnly(), ctx.io); 1754 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); 1755 defer index.deinit(); 1756 switch (ctx.files) { 1757 .files => |updates| for (updates) |file| try ctx.addFile(state, &index, file.path, file.content), 1758 .dirs => |dirs| for (dirs) |dir| try ctx.addDir(state, &index, dir), 1759 } 1760 var tree = try xit.object.Tree.initFromIndex(.xit, opts, state, ctx.io, ctx.allocator, &index); 1761 defer tree.deinit(); 1762 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 1763 const head = try rf.readHead(.xit, opts, state.readOnly(), ctx.io, &head_buffer) orelse return error.HeadNotFound; 1764 ctx.result.* = try xit.object.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, ctx.metadata, &tree, head.ref); 1765 try xit.undo.writeMessage(opts, state, .{ .commit = ctx.metadata }); 1766 } 1767 }; 1768 try repo.core.db_file.lock(io, .exclusive); 1769 defer repo.core.db_file.unlock(io); 1770 const history = try R.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1771 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 1772 .core = &repo.core, 1773 .io = io, 1774 .allocator = allocator, 1775 .files = files, 1776 .metadata = metadata, 1777 .result = &result, 1778 }); 1779 return result; 1780 } 1781