const std = @import("std"); const assert = std.debug.assert; const fs = @import("fs.zig"); const shell = @import("shell.zig"); const cli = @import("cli.zig"); const Env = @import("env.zig"); const fatal = cli.fatal; pub const Commit = struct { hash: []const u8, timestamp: u64, pub fn init_empty() Commit { return .{ .hash = "", .timestamp = 0 }; } pub fn parse(version_str: []const u8) !Commit { const s = std.mem.trim(u8, version_str, " \t"); if (s.len == 0) return .init_empty(); const pos = std.mem.indexOfScalar(u8, s, '@') orelse return error.invalid_commit; return .{ .hash = s[0..pos], .timestamp = std.fmt.parseInt(u64, s[pos + 1 ..], 10) catch return error.invalid_commit, }; } pub fn format(self: Commit, w: *std.io.Writer) std.io.Writer.Error!void { if (self.hash.len > 0) { try w.print("{s}@{d}", .{ self.hash, self.timestamp }); } } }; pub const Repo = struct { path: fs.Path, url: []const u8, branch: []const u8, pub fn clone(self: Repo, env: Env) !void { const dir = try fs.dir_info(self.path); if (!dir.exists or dir.empty) { var branch_buf: [std.fs.max_name_bytes]u8 = undefined; const branch_param = branch_buf[0 .. 9 + self.branch.len]; @memcpy(branch_param[0..9], "--branch="); @memcpy(branch_param[9..], self.branch); const res = shell.exec(.{ .env = env, .argv = &.{ "git", "clone", "--quiet", "--depth=1", branch_param, self.url, self.path.name() }, }); switch (res) { .success => |out| env.dealloc(out), .failure => |out| { env.err("failed to clone {s}\n", .{self.url}); fatal("{s}", .{out}); }, } } } pub fn reset(self: Repo, env: Env) void { var res = shell.exec(.{ .env = env, .argv = &.{ "git", "reset", "--hard" }, .cwd = self.path, }); switch (res) { .success => |out| env.dealloc(out), .failure => |out| { env.err("failed to reset repository: {s}\n", .{self.path.name()}); fatal("{s}", .{out}); }, } res = shell.exec(.{ .env = env, .argv = &.{ "git", "clean", "-fdx" }, .cwd = self.path, }); switch (res) { .success => |out| env.dealloc(out), .failure => |out| { env.err("failed to clean repository: {s}\n", .{self.path.name()}); fatal("{s}", .{out}); }, } } pub fn update(self: Repo, env: Env) !Commit { const dir = try fs.dir_info(self.path); if (!dir.exists or dir.empty) { try self.clone(env); } else { var res = shell.exec(.{ .env = env, .argv = &.{ "git", "fetch", "--quiet", "--depth=1", "origin", self.branch }, .cwd = self.path, }); switch (res) { .success => |out| env.dealloc(out), .failure => |out| { env.err("failed to fetch branch {s}: {s}\n", .{ self.branch, self.url }); fatal("{s}", .{out}); }, } const branch_spec: fs.Path = .join(.{ "origin", self.branch }); res = shell.exec(.{ .env = env, .argv = &.{ "git", "reset", "--hard", branch_spec.name() }, .cwd = self.path, }); switch (res) { .success => |out| env.dealloc(out), .failure => |out| { env.err("failed to reset branch {s}: {s}\n", .{ self.branch, self.path.name() }); fatal("{s}", .{out}); }, } } // We leak `out` intentionally here. const out = self.must_run(env, &.{ "git", "show", "--no-patch", "--format=format:%H %at", self.branch }); const space = std.mem.indexOfScalar(u8, out, ' ') orelse { fatal("unexpected output for 'git show'", .{}); }; return .{ .hash = out[0..space], .timestamp = ts: { var timestamp: u64 = 0; for (out[space + 1 ..]) |c| { if (c < '0' or c > '9') { fatal("unexpected timestamp in 'git show'", .{}); } timestamp *= 10; timestamp += @intCast(c - '0'); } break :ts timestamp; }, }; } fn must_run(self: Repo, env: Env, argv: []const []const u8) []const u8 { const res = shell.exec(.{ .env = env, .argv = argv, .cwd = self.path, }); switch (res) { .success => |out| return out, .failure => |out| { var errmsg = out; if (std.mem.indexOfScalar(u8, errmsg, '\n')) |eol| { errmsg = errmsg[0..eol]; if (errmsg.len > 0 and errmsg[errmsg.len - 1] == '\r') { errmsg = errmsg[0 .. errmsg.len - 1]; } } fatal("{s}", .{errmsg}); }, } } }; pub fn is_valid_branch_name(name: []const u8) bool { var last_c: u8 = 0; for (name) |c| { switch (c) { 0...31 => return false, ':', '?', '[', '\\', '^', '~', ' ', '*' => return false, '.' => if (last_c == '.') return false, '{' => if (last_c == '@') return false, else => {}, } last_c = c; } return true; }