╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │162 } │ │163 │ │164 var body_reader = std.Io.Reader.fixed(body); │ │165 + const create_if_missing = isReceivePack(handler, suffix, uri.query); │ │166 + const http_backend_options = xit.net_server_http_backend.Options{ │ │167 + .request_method = request_method, │ │168 + .handler = handler, │ │169 + .suffix = suffix, │ │170 + .query_string = if (uri.query) |q| q.percent_encoded else "", │ │171 + .content_type = content_type, │ │172 + .has_remote_user = has_remote_user, │ │173 + .protocol_version = protocol_version, │ │174 + }; │ │175 + │ │176 if (any_repo_opts.hash) |hash_kind| { │ │166 - var repo = try rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }); │ │177 + var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, create_if_missing); │ │178 defer repo.deinit(io, allocator); │ │168 - try runHttpBackend(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator, &body_reader, http_server.out, request_method, handler, suffix, uri.query, content_type, has_remote_user, protocol_version);│ │179 + try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); │ │180 } else { │ │170 - var any_repo = try rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }); │ │181 + var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) { │ │182 + error.RepoNotFound => { │ │183 + if (!create_if_missing) return err; │ │184 + │ │185 + var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); │ │186 + defer repo.deinit(io, allocator); │ │187 + │ │188 + try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); │ │189 + return; │ │190 + }, │ │191 + else => |e| return e, │ │192 + }; │ │193 defer any_repo.deinit(io, allocator); │ │194 + │ │195 switch (any_repo) { │ │196 inline else => |*repo| { │ │174 - try runHttpBackend(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator, &body_reader, http_server.out, request_method, handler, suffix, uri.query, content_type, has_remote_user, protocol_version); │ │197 + try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); │ │198 }, │ │199 } │ │200 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │178 -} │ │179 - │ │180 -fn runHttpBackend( │ │181 - comptime repo_kind: rp.RepoKind, │ │182 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │183 - repo: *rp.Repo(repo_kind, repo_opts), │ │184 - io: std.Io, │ │185 - allocator: std.mem.Allocator, │ │186 - reader: *std.Io.Reader, │ │187 - writer: *std.Io.Writer, │ │188 - request_method: std.http.Method, │ │189 - handler: xit.net_server_http_backend.HandlerKind, │ │190 - suffix: []const u8, │ │191 - query: ?std.Uri.Component, │ │192 - content_type: []const u8, │ │193 - has_remote_user: bool, │ │194 - protocol_version: xit.net_server_common.ProtocolVersion, │ │195 -) !void { │ │196 - try repo.httpBackend(io, allocator, reader, writer, .http, .{ │ │197 - .request_method = request_method, │ │198 - .handler = handler, │ │199 - .suffix = suffix, │ │200 - .query_string = if (query) |q| q.percent_encoded else "", │ │201 - .content_type = content_type, │ │202 - .has_remote_user = has_remote_user, │ │203 - .protocol_version = protocol_version, │ │204 - }); │ │201 } │ │202 │ │203 fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } {│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │277 if (!isSubPath(project_root, repo_path)) return error.Forbidden; │ │278 │ │279 if (any_repo_opts.hash) |hash_kind| { │ │284 - var repo = try rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }); │ │280 + var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, request.service == .receive_pack); │ │281 defer repo.deinit(io, allocator); │ │282 try runSshService(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator, &reader.interface, &writer.interface, request);│ │283 } else { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │288 - var any_repo = try rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }); │ │284 + var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { │ │285 + error.RepoNotFound => { │ │286 + if (request.service != .receive_pack) return open_err; │ │287 + │ │288 + var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); │ │289 + defer repo.deinit(io, allocator); │ │290 + try runSshService(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator, &reader.interface, &writer.interface, request);│ │291 + return; │ │292 + }, │ │293 + else => |e| return e, │ │294 + }; │ │295 defer any_repo.deinit(io, allocator); │ │296 switch (any_repo) { │ │297 inline else => |*repo| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │319 .protocol_version = request.protocol_version, │ │320 }), │ │321 } │ │322 +} │ │323 + │ │324 +fn openRepo( │ │325 + comptime repo_kind: rp.RepoKind, │ │326 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │327 + io: std.Io, │ │328 + allocator: std.mem.Allocator, │ │329 + repo_path: []const u8, │ │330 + create_if_missing: bool, │ │331 +) !rp.Repo(repo_kind, repo_opts) { │ │332 + return rp.Repo(repo_kind, repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) {│ │333 + error.RepoNotFound => { │ │334 + if (!create_if_missing) return open_err; │ │335 + │ │336 + var repo = try rp.Repo(repo_kind, repo_opts).init(io, allocator, .{ .path = repo_path }); │ │337 + errdefer repo.deinit(io, allocator); │ │338 + try repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); │ │339 + try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │340 + return repo; │ │341 + }, │ │342 + else => |e| return e, │ │343 + }; │ │344 } │ │345 │ │346 +fn isReceivePack( │ │347 + handler: xit.net_server_http_backend.HandlerKind, │ │348 + suffix: []const u8, │ │349 + query: ?std.Uri.Component, │ │350 +) bool { │ │351 + return switch (handler) { │ │352 + .run_service => std.mem.eql(u8, suffix, "/git-receive-pack"), │ │353 + .get_info_refs => if (query) |q| │ │354 + std.mem.startsWith(u8, q.percent_encoded, "service=git-receive-pack") │ │355 + else │ │356 + false, │ │357 + }; │ │358 +} │ │359 + │ │360 fn readSshRequest(allocator: std.mem.Allocator, reader: *std.Io.Reader) !SshRequest { │ │361 const magic = try readSshPreludeLine(reader); │ │362 if (!std.mem.eql(u8, magic, "haxy-ssh-helper-v1")) return error.InvalidSshMagic; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/test.zig│ ╰────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │29 } │ │30 } │ │31 │ │32 +test "push creates missing repo under serve" { │ │33 + const io = std.testing.io; │ │34 + const allocator = std.testing.allocator; │ │35 + try testPushCreatesMissingRepo(.xit, .xit, .{ .wire = .http }, 3014, io, allocator); │ │36 + if (.windows != builtin.os.tag) { │ │37 + try testPushCreatesMissingRepo(.xit, .xit, .{ .wire = .ssh }, 3015, io, allocator);│ │38 + } │ │39 +} │ │40 + │ │41 test "clone small" { │ │42 const io = std.testing.io; │ │43 const allocator = std.testing.allocator; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1077 │ │1078 // make sure push was successful │ │1079 try std.testing.expect(null == try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); │ │1080 +} │ │1081 + │ │1082 +fn testPushCreatesMissingRepo( │ │1083 + comptime repo_kind: rp.RepoKind, │ │1084 + comptime server_repo_kind: rp.RepoKind, │ │1085 + comptime transport_def: net.TransportDefinition, │ │1086 + comptime port: u16, │ │1087 + io: std.Io, │ │1088 + allocator: std.mem.Allocator, │ │1089 +) !void { │ │1090 + const temp_dir_name = "temp-testnet-push-create"; │ │1091 + │ │1092 + const cwd = std.Io.Dir.cwd(); │ │1093 + var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); │ │1094 + if (temp_dir_or_err) |*temp_dir| { │ │1095 + temp_dir.close(io); │ │1096 + try cwd.deleteTree(io, temp_dir_name); │ │1097 + } else |_| {} │ │1098 + var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); │ │1099 + defer cwd.deleteTree(io, temp_dir_name) catch {}; │ │1100 + defer temp_dir.close(io); │ │1101 + │ │1102 + var server = try Server(server_repo_kind, transport_def, temp_dir_name, port, true).init(io, allocator); │ │1103 + try server.start(); │ │1104 + defer server.stop(); │ │1105 + │ │1106 + const cwd_path = try std.process.currentPathAlloc(io, allocator); │ │1107 + defer allocator.free(cwd_path); │ │1108 + │ │1109 + const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); │ │1110 + defer allocator.free(server_path); │ │1111 + │ │1112 + const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); │ │1113 + defer allocator.free(client_path); │ │1114 + │ │1115 + var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); │ │1116 + defer client_repo.deinit(io, allocator); │ │1117 + │ │1118 + const commit1 = blk: { │ │1119 + const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); │ │1120 + defer hello_txt.close(io); │ │1121 + try hello_txt.writeStreamingAll(io, "hello, world!"); │ │1122 + try client_repo.add(io, allocator, &.{"hello.txt"}); │ │1123 + break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); │ │1124 + }; │ │1125 + │ │1126 + _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); │ │1127 + │ │1128 + { │ │1129 + if (.windows == builtin.os.tag) { │ │1130 + std.mem.replaceScalar(u8, server_path, '\\', '/'); │ │1131 + } │ │1132 + const separator = if (server_path[0] == '/') "" else "/"; │ │1133 + │ │1134 + const remote_url = switch (transport_def) { │ │1135 + .file => unreachable, │ │1136 + .wire => |wire_kind| switch (wire_kind) { │ │1137 + .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), │ │1138 + .raw => unreachable, │ │1139 + .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), │ │1140 + }, │ │1141 + }; │ │1142 + defer allocator.free(remote_url); │ │1143 + │ │1144 + try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); │ │1145 + try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); │ │1146 + } │ │1147 + │ │1148 + const is_ssh = switch (transport_def) { │ │1149 + .file => false, │ │1150 + .wire => |wire_kind| .ssh == wire_kind, │ │1151 + }; │ │1152 + const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { │ │1153 + const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); │ │1154 + defer allocator.free(known_hosts_path); │ │1155 + │ │1156 + const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); │ │1157 + defer allocator.free(priv_key_path); │ │1158 + │ │1159 + break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path });│ │1160 + } else null; │ │1161 + defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); │ │1162 + │ │1163 + const receive_pack_command = try receivePackCommand(server_repo_kind, is_ssh, true, allocator, cwd_path, port); │ │1164 + defer allocator.free(receive_pack_command); │ │1165 + │ │1166 + try client_repo.push( │ │1167 + io, │ │1168 + allocator, │ │1169 + "origin", │ │1170 + "master", │ │1171 + false, │ │1172 + .{ .refspecs = &.{ "refs/tags/1.0.0:refs/tags/1.0.0" }, .wire = .{ .ssh = .{ │ │1173 + .command = ssh_cmd_maybe, │ │1174 + .receive_pack_command = receive_pack_command, │ │1175 + } } }, │ │1176 + ); │ │1177 + │ │1178 + var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = true }).open(io, allocator, .{ .path = server_path }); │ │1179 + defer server_repo.deinit(io, allocator); │ │1180 + │ │1181 + try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); │ │1182 + │ │1183 + const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; │ │1184 + try std.testing.expectEqualStrings(&commit1, &oid_master); │ │1185 } │ │1186 │ │1187 fn testClone( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯