╭ changed ╮ │README.md│ ╰─────────╯ ╭──────────────────────────────────────────────────────────╮ │59 echo "hello" > hello.txt │ │60 git add hello.txt │ │61 git commit -m "let there be light" │ │62 -git remote add origin http://localhost:8080/test │ │62 +git remote add origin http://localhost:8080/admin/test│ │63 git push origin HEAD:master │ │64 ``` │ │65 │ ╰──────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 -After that, you'll see your repo in `temp-try/server/repos/test`. It's MAGIC! │ │66 +After that, the repo will be in `temp-try/server/repos` at a directory named by its repo id. │ │67 │ │68 *"C'mon Alex! You always dreamt about going on a big adventure! Let this be our first!" -- Lunar: Silver Star Story*│ │69 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/command.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │198 if (cmd_args.get("--data-dir")) |val_maybe| { │ │199 options.data_dir = val_maybe orelse return error.DataDirNeedsValue;│ │200 } │ │201 + if (cmd_args.contains("--test")) { │ │202 + options.is_test = true; │ │203 + } │ │204 │ │205 return .{ .serve = options }; │ │206 }, │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │366 } │ │367 │ │368 try consume(repo_opts, io, allocator, repo, ref); │ │369 +} │ │370 + │ │371 +// split a pushed "<owner>/<repo>" path into its two components, or null if it │ │372 +// isn't exactly two non-empty segments. │ │373 +pub fn parseOwnerRepoPath(path: []const u8) ?struct { owner: []const u8, name: []const u8 } { │ │374 + var it = std.mem.splitScalar(u8, path, '/'); │ │375 + const owner = it.next() orelse return null; │ │376 + const name = it.next() orelse return null; │ │377 + if (it.next() != null) return null; │ │378 + if (owner.len == 0 or name.len == 0) return null; │ │379 + │ │380 + return .{ .owner = owner, .name = name }; │ │381 +} │ │382 + │ │383 +// resolve a pushed `<owner>/<repo>` to the hex event id that names its on-disk │ │384 +// directory under the repos dir │ │385 +pub fn resolveOrCreateRepo( │ │386 + io: std.Io, │ │387 + allocator: std.mem.Allocator, │ │388 + admin_repo_path: []const u8, │ │389 + owner_name: []const u8, │ │390 + repo_name: []const u8, │ │391 + create_if_missing: bool, │ │392 +) !?[event_id_size * 2]u8 { │ │393 + var repo = rp.Repo(.xit, admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) { │ │394 + error.RepoNotFound => return null, │ │395 + else => |e| return e, │ │396 + }; │ │397 + defer repo.deinit(io, allocator); │ │398 + │ │399 + var arena = std.heap.ArenaAllocator.init(allocator); │ │400 + defer arena.deinit(); │ │401 + │ │402 + const moment = try currentMoment(admin_repo_opts, &repo); │ │403 + │ │404 + // an already-registered repo reuses its event id, so a re-push (or a clone) │ │405 + // lands in the same repo │ │406 + if (try Repo.readByOwnerAndName(AdminDB, admin_repo_opts.hash, moment, &arena, owner_name, repo_name)) |found| { │ │407 + return std.fmt.bytesToHex(found.event_id, .lower); │ │408 + } │ │409 + │ │410 + if (!create_if_missing) return null; │ │411 + │ │412 + // the new repo is owned by the named user, read from the name index; an │ │413 + // unknown owner can't own a repo, so the push is rejected │ │414 + const name_to_user_id_cursor = try moment.getCursor(hash.hashInt(admin_repo_opts.hash, "name->user-id")) orelse return null;│ │415 + const name_to_user_id = try AdminDB.HashMap(.read_only).init(name_to_user_id_cursor); │ │416 + const owner_id_cursor = try name_to_user_id.getCursor(hash.hashInt(admin_repo_opts.hash, owner_name)) orelse return null; │ │417 + var owner_user_id: [event_id_size]u8 = undefined; │ │418 + _ = try owner_id_cursor.readBytes(&owner_user_id); │ │419 + │ │420 + var id_bytes: [event_id_size]u8 = undefined; │ │421 + io.random(&id_bytes); │ │422 + const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │423 + │ │424 + try commitAndConsume(admin_repo_opts, io, allocator, &repo, events_ref, &[_]EventWithId{.{ │ │425 + .id = event_id_hex, │ │426 + .event = .{ .repo = .{ │ │427 + .user_id = &owner_user_id, │ │428 + .name = repo_name, │ │429 + .description = "", │ │430 + .enable_issue = true, │ │431 + } }, │ │432 + }}); │ │433 + │ │434 + return event_id_hex; │ │435 } │ │436 │ │437 // │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │3 const rp = xit.repo; │ │4 const hash = xit.hash; │ │5 const ui = @import("./ui.zig"); │ │6 +const evt = @import("./event.zig"); │ │7 const web = @import("./web.zig"); │ │8 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig");│ │9 const serve_ssh = @import("./serve_ssh.zig"); │ ╰──────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │13 ssh_listen: []const u8 = "127.0.0.1:8022", │ │14 wui_listen: []const u8 = "127.0.0.1:8000", │ │15 data_dir: []const u8 = ".", │ │16 + // test mode, used by the networking tests. it serves repos directly by │ │17 + // their on-disk path instead of resolving them as <owner>/<repo> through │ │18 + // the event store, giving the plain git-server behavior those tests rely on.│ │19 + is_test: bool = false, │ │20 }; │ │21 │ │22 const ListenAddress = struct { │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │92 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); │ │93 try err.flush(); │ │94 │ │90 - runHttpListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, &http_server, &tasks, err); │ │95 + runHttpListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, options.is_test, &http_server, &tasks, err);│ │96 │ │97 try err.print("serving SSH on {s}\n", .{options.ssh_listen}); │ │98 try err.flush(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 const ssh_session_handler = serve_ssh.SessionHandler{ │ │101 .admin_repo_path = admin_repo_path, │ │102 .repo_root_path = repo_root_path, │ │103 + .is_test = options.is_test, │ │104 }; │ │105 runSshListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err);│ │106 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │122 io: std.Io, │ │123 allocator: std.mem.Allocator, │ │124 repo_root_path: []const u8, │ │125 + admin_repo_path: []const u8, │ │126 + is_test: bool, │ │127 net_server: *std.Io.net.Server,│ │128 tasks: *std.Io.Group, │ │129 err: *std.Io.Writer, │ ╰────────────────────────────────────────╯ ╭────────────────────────────────────────────╮ │132 io: std.Io, │ │133 allocator: std.mem.Allocator, │ │134 repo_root_path: []const u8, │ │135 + admin_repo_path: []const u8, │ │136 + is_test: bool, │ │137 net_server: *std.Io.net.Server,│ │138 tasks: *std.Io.Group, │ │139 err: *std.Io.Writer, │ ╰────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯