const std = @import("std"); const Build = std.Build; pub const Options = struct { /// Length of the short hash (default: 7, git's default) hash_length: u6 = 7, /// String to append when working tree is dirty dirty_flag: []const u8 = "*", }; /// Get git version information by reading .git files directly pub fn getVersion(b: *Build, options: Options) []const u8 { const allocator = b.allocator; // Find build root by looking for build.zig const build_root = findBuildRoot(allocator) catch return "unknown"; defer allocator.free(build_root); // Read .git/HEAD relative to build root const head_path = std.fmt.allocPrint(allocator, "{s}/.git/HEAD", .{build_root}) catch return "unknown"; defer allocator.free(head_path); const head_data = std.fs.cwd().readFileAlloc(allocator, head_path, 1024) catch { return "not under version control"; }; defer allocator.free(head_data); const head_trimmed = std.mem.trim(u8, head_data, &std.ascii.whitespace); // Parse HEAD - either "ref: refs/heads/branch" or direct hash const hash_owned = if (std.mem.startsWith(u8, head_trimmed, "ref: ")) blk: { const ref_path_rel = std.mem.trimLeft(u8, head_trimmed[5..], &std.ascii.whitespace); const ref_file = std.fmt.allocPrint(allocator, "{s}/.git/{s}", .{ build_root, ref_path_rel }) catch return "unknown"; defer allocator.free(ref_file); const ref_fd = std.fs.openFileAbsolute(ref_file, .{}) catch return "unknown"; defer ref_fd.close(); var ref_buf: [1024]u8 = undefined; const bytes_read = ref_fd.readAll(&ref_buf) catch return "unknown"; const ref_data = ref_buf[0..bytes_read]; const ref_trimmed = std.mem.trim(u8, ref_data, &std.ascii.whitespace); break :blk allocator.dupe(u8, ref_trimmed) catch return "unknown"; } else allocator.dupe(u8, head_trimmed) catch return "unknown"; defer allocator.free(hash_owned); // Truncate to short hash const short_hash = if (hash_owned.len > options.hash_length) hash_owned[0..options.hash_length] else hash_owned; // Check if dirty using simple heuristic: // If any .zig files are newer than .git/index, mark as dirty const is_dirty = isDirty(allocator, build_root) catch return "unknown"; if (is_dirty) { return std.fmt.allocPrint(allocator, "{s}{s}", .{ short_hash, options.dirty_flag }) catch return "unknown"; } return allocator.dupe(u8, short_hash) catch return "unknown"; } fn findBuildRoot(allocator: std.mem.Allocator) ![]const u8 { var buf: [std.fs.max_path_bytes]u8 = undefined; const start_cwd = try std.fs.cwd().realpath(".", &buf); var cwd: []const u8 = start_cwd; while (true) { // Check if build.zig exists in current directory var dir = std.fs.openDirAbsolute(cwd, .{}) catch break; defer dir.close(); dir.access("build.zig", .{}) catch { // build.zig not found, try parent const parent = std.fs.path.dirname(cwd) orelse break; if (std.mem.eql(u8, parent, cwd)) break; // Reached root cwd = parent; continue; }; return allocator.dupe(u8, cwd); } return error.BuildRootNotFound; } fn isDirty(allocator: std.mem.Allocator, build_root: []const u8) !bool { // Get .git/index mtime const index_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".git", "index" }); defer allocator.free(index_path); const index_stat = std.fs.cwd().statFile(index_path) catch return error.CannotDetermineDirty; const index_mtime = index_stat.mtime; // Read .gitignore const ignore_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".gitignore" }); defer allocator.free(ignore_path); const ignore_data = std.fs.cwd().readFileAlloc(allocator, ignore_path, 1024 * 1024) catch try allocator.dupe(u8, ""); defer allocator.free(ignore_data); // Walk source files in build root and check if any are newer var dir = std.fs.openDirAbsolute(build_root, .{ .iterate = true }) catch return error.CannotDetermineDirty; defer dir.close(); var walker = dir.walk(allocator) catch return error.CannotDetermineDirty; defer walker.deinit(); while (walker.next() catch return error.CannotDetermineDirty) |entry| { if (entry.kind != .file) continue; // Always ignore .git/ if (std.mem.startsWith(u8, entry.path, ".git/")) continue; // Check if path matches any ignore pattern var lines = std.mem.splitScalar(u8, ignore_data, '\n'); var ignored = false; while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); if (trimmed.len == 0) continue; if (trimmed[0] == '#') continue; if (isIgnored(entry.path, trimmed)) { ignored = true; break; } } if (ignored) continue; const stat = entry.dir.statFile(entry.basename) catch continue; if (stat.mtime > index_mtime) { return true; } } return false; } /// Handles checking a gitignore style pattern to determine if a path should /// be ignored. Mostly, globs are not handled, but an extension-like pattern /// is common and recognized by this function, so things like '*.swp' will work /// as expected fn isIgnored(path: []const u8, pattern: []const u8) bool { // TODO: Handle other glob patterns (?, [], *prefix, suffix*) // Handle *.extension pattern if (std.mem.startsWith(u8, pattern, "*.")) return std.mem.endsWith(u8, path, pattern[1..]); // Pattern ending with / matches directory prefix if (std.mem.endsWith(u8, pattern, "/")) return std.mem.startsWith(u8, path, pattern); // Pattern starting with / matches from root if (std.mem.startsWith(u8, pattern, "/")) return std.mem.eql(u8, path, pattern[1..]); // Otherwise match as substring (file/dir name anywhere in path) return std.mem.indexOf(u8, path, pattern) != null; }