aboutsummaryrefslogtreecommitdiff
path: root/src/git.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/git.zig')
-rw-r--r--src/git.zig179
1 files changed, 179 insertions, 0 deletions
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;
+}