From 44e5ad763794a438ecfd50c8b7f6ea760ea82da5 Mon Sep 17 00:00:00 2001 From: tsne Date: Wed, 1 Jan 2025 14:58:28 +0100 Subject: initial commit --- src/git.zig | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/git.zig (limited to 'src/git.zig') diff --git a/src/git.zig b/src/git.zig new file mode 100644 index 0000000..82e19b0 --- /dev/null +++ b/src/git.zig @@ -0,0 +1,179 @@ +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; +} -- cgit v1.2.3