char/sorcery

static-files based git repo viewer

git clone https://git.t4t.associates/char/sorcery

Charlotte Somsorcery-ssh: refactor3356bff

main
5.6 KiB133 linesraw
1const std = @import("std");
2const refresh = @import("refresh.zig");
3
4pub const Service = enum { @"git-upload-pack", @"git-receive-pack" };
5
6/// Run `service` on a repository beneath HOME, creating it on first push.
7pub fn serve(
8    allocator: std.mem.Allocator,
9    io: std.Io,
10    environ: *const std.process.Environ.Map,
11    service: Service,
12    path: []const u8,
13) !void {
14    const home = environ.get("HOME") orelse std.process.fatal("HOME not set", .{});
15    const repo_path = try expandPath(allocator, home, path);
16    defer allocator.free(repo_path);
17    if (!isUnder(repo_path, home)) std.process.fatal("repo path must be inside HOME: {s}", .{path});
18
19    if (service == .@"git-upload-pack") {
20        return std.process.replace(io, .{
21            .argv = &.{ "git-upload-pack", repo_path },
22        });
23    }
24
25    var created_repo = false;
26    std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch {
27        std.log.info("auto-initializing bare repo at {s}", .{repo_path});
28        std.Io.Dir.createDirPath(.cwd(), io, repo_path) catch |err|
29            std.process.fatal("failed to create repo directory: {}", .{err});
30        const result = try std.process.run(allocator, io, .{
31            .argv = &.{ "git", "init", "--bare", repo_path },
32        });
33        defer allocator.free(result.stdout);
34        defer allocator.free(result.stderr);
35        if (result.term != .exited or result.term.exited != 0) {
36            std.process.fatal("git init --bare failed: {s}", .{result.stderr});
37        }
38        created_repo = true;
39    };
40
41    var receive_pack = try std.process.spawn(io, .{
42        .argv = &.{ "git-receive-pack", repo_path },
43    });
44    const term = try receive_pack.wait(io);
45    if (term != .exited) std.process.exit(1);
46    if (term.exited != 0) std.process.exit(term.exited);
47
48    if (created_repo) try fixHead(allocator, io, repo_path);
49    refresh.request(io, environ, repo_path);
50}
51
52/// Whether `path` is strictly below `root`; `root` itself does not count.
53fn isUnder(path: []const u8, root: []const u8) bool {
54    if (std.mem.eql(u8, root, &.{std.fs.path.sep})) {
55        return path.len > 1 and path[0] == std.fs.path.sep;
56    }
57    return std.mem.startsWith(u8, path, root) and
58        path.len > root.len and path[root.len] == std.fs.path.sep;
59}
60
61fn fixHead(allocator: std.mem.Allocator, io: std.Io, repo_path: []const u8) !void {
62    const head_result = try std.process.run(allocator, io, .{
63        .argv = &.{ "git", "--git-dir", repo_path, "rev-parse", "--verify", "--quiet", "HEAD" },
64    });
65    defer allocator.free(head_result.stdout);
66    defer allocator.free(head_result.stderr);
67    if (head_result.term == .exited and head_result.term.exited == 0) return;
68    if (head_result.term != .exited or head_result.term.exited != 1) {
69        std.log.err("failed to resolve repository HEAD: {s}", .{head_result.stderr});
70        return error.GitCommandFailed;
71    }
72
73    const refs_result = try std.process.run(allocator, io, .{
74        .argv = &.{ "git", "--git-dir", repo_path, "for-each-ref", "--format=%(refname)", "refs/heads/" },
75    });
76    defer allocator.free(refs_result.stdout);
77    defer allocator.free(refs_result.stderr);
78    if (refs_result.term != .exited or refs_result.term.exited != 0) {
79        std.log.err("failed to list repository branches: {s}", .{refs_result.stderr});
80        return error.GitCommandFailed;
81    }
82
83    var refs = std.mem.tokenizeScalar(u8, refs_result.stdout, '\n');
84    var sole_ref: ?[]const u8 = null;
85    var main_ref: ?[]const u8 = null;
86    var ref_count: usize = 0;
87    while (refs.next()) |ref| {
88        sole_ref = ref;
89        ref_count += 1;
90        if (std.mem.eql(u8, ref, "refs/heads/main")) main_ref = ref;
91    }
92
93    const head_ref = main_ref orelse if (ref_count == 1) sole_ref else null;
94    if (head_ref) |ref| {
95        const set_head_result = try std.process.run(allocator, io, .{
96            .argv = &.{ "git", "--git-dir", repo_path, "symbolic-ref", "HEAD", ref },
97        });
98        defer allocator.free(set_head_result.stdout);
99        defer allocator.free(set_head_result.stderr);
100        if (set_head_result.term != .exited or set_head_result.term.exited != 0) {
101            std.log.err("failed to set repository HEAD: {s}", .{set_head_result.stderr});
102            return error.GitCommandFailed;
103        }
104    } else if (ref_count > 1) {
105        std.log.warn("HEAD remains unresolved because the repository has multiple branches and no main branch", .{});
106    }
107}
108
109// im pretty sure this is what git expand user path does. wordexp from posix is overkill
110fn expandPath(allocator: std.mem.Allocator, home: []const u8, path: []const u8) ![]const u8 {
111    if (std.mem.startsWith(u8, path, "~/")) {
112        return std.fs.path.resolve(allocator, &.{ home, path[2..] });
113    } else if (std.mem.eql(u8, path, "~")) {
114        return allocator.dupe(u8, home);
115    } else {
116        return std.fs.path.resolve(allocator, &.{ home, path });
117    }
118}
119
120test "pushed paths normalise into HOME or are rejected" {
121    const home = "/home/git";
122    for ([_][]const u8{ "x", "~/x", "/home/git/x", "~/a/../x", "/home/git/../git/x" }) |path| {
123        const expanded = try expandPath(std.testing.allocator, home, path);
124        defer std.testing.allocator.free(expanded);
125        try std.testing.expectEqualStrings("/home/git/x", expanded);
126        try std.testing.expect(isUnder(expanded, home));
127    }
128    for ([_][]const u8{ "~", ".", "../x", "~/../x", "/etc/passwd", "/home/gitolite/x", "/home" }) |path| {
129        const expanded = try expandPath(std.testing.allocator, home, path);
130        defer std.testing.allocator.free(expanded);
131        try std.testing.expect(!isUnder(expanded, home));
132    }
133}