Something went wrong. Try again.
A history browser for git, a replacement for gitk, drawn with Vulkan on Wayland.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Repositories for the tests to read, made with git itself in a directory//! of their own and thrown away afterwards.//!//! git runs with nobody's configuration — no global or system file, no//! hooks, no signing — and a fixed author and clock, so a test sees the//! same history on every machine.//!//! The object format is SHA-1, or what `GREBE_TEST_OBJECT_FORMAT` names://! `zig build test` runs every test a second time with it set to `sha256`,//! so that each one that reads a repository reads one of each.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;const testing = std.testing;
pub const Repo = struct { gpa: Allocator, io: Io, tmp: testing.TmpDir, /// Absolute. path: [:0]u8, env: std.process.Environ.Map, /// Commits made so far, for their timestamps. count: u32 = 0,
/// An empty repository whose branch is `main`. Skips the test when /// there is no git to make one with. pub fn init(gpa: Allocator, io: Io) !Repo { var tmp = testing.tmpDir(.{}); errdefer tmp.cleanup(); const path = try tmp.dir.realPathFileAlloc(io, ".", gpa); errdefer gpa.free(path);
var env: std.process.Environ.Map = .init(gpa); errdefer env.deinit(); if (testing.environ.getAlloc(gpa, "PATH")) |p| { defer gpa.free(p); try env.put("PATH", p); } else |_| {} try env.put("HOME", path); try env.put("GIT_CONFIG_NOSYSTEM", "1"); try env.put("GIT_CONFIG_GLOBAL", "/dev/null"); try env.put("GIT_AUTHOR_NAME", "Fixture Author"); try env.put("GIT_AUTHOR_EMAIL", "author@example.com"); try env.put("GIT_COMMITTER_NAME", "Fixture Committer"); try env.put("GIT_COMMITTER_EMAIL", "committer@example.com"); try env.put("TZ", "UTC"); if (testing.environ.getAlloc(gpa, "GREBE_TEST_OBJECT_FORMAT")) |f| { defer gpa.free(f); try env.put("GIT_DEFAULT_HASH", f); } else |_| {}
var r: Repo = .{ .gpa = gpa, .io = io, .tmp = tmp, .path = path, .env = env }; r.git(&.{ "init", "-q", "-b", "main" }) catch |err| switch (err) { error.FileNotFound => return error.SkipZigTest, else => return err, }; return r; }
pub fn deinit(r: *Repo) void { r.env.deinit(); r.gpa.free(r.path); r.tmp.cleanup(); r.* = undefined; }
/// Runs git in the repository, and returns what it wrote, which the /// caller frees; fails unless it succeeds. pub fn output(r: *Repo, args: []const []const u8) ![]u8 { const argv = try r.gpa.alloc([]const u8, args.len + 1); defer r.gpa.free(argv); argv[0] = "git"; @memcpy(argv[1..], args); const result = try std.process.run(r.gpa, r.io, .{ .argv = argv, .cwd = .{ .path = r.path }, .environ_map = &r.env, }); defer r.gpa.free(result.stderr); errdefer r.gpa.free(result.stdout); const ok = switch (result.term) { .exited => |code| code == 0, else => false, }; if (!ok) return error.GitFailed; return result.stdout; }
/// Runs git in the repository, and fails unless it succeeds. pub fn git(r: *Repo, args: []const []const u8) !void { const argv = try r.gpa.alloc([]const u8, args.len + 1); defer r.gpa.free(argv); argv[0] = "git"; @memcpy(argv[1..], args); const result = try std.process.run(r.gpa, r.io, .{ .argv = argv, .cwd = .{ .path = r.path }, .environ_map = &r.env, }); defer r.gpa.free(result.stdout); defer r.gpa.free(result.stderr); const ok = switch (result.term) { .exited => |code| code == 0, else => false, }; if (!ok) { std.debug.print("git {s} failed: {s}\n", .{ args[0], result.stderr }); return error.GitFailed; } }
/// A commit writing `contents` to `path`, with `message`. pub fn commitFile(r: *Repo, path: []const u8, contents: []const u8, message: []const u8) !void { try r.tmp.dir.writeFile(r.io, .{ .sub_path = path, .data = contents }); try r.git(&.{ "add", path }); try r.commit(message); }
/// A commit with `message`, a minute after the last, of whatever is /// staged, or of nothing. pub fn commit(r: *Repo, message: []const u8) !void { r.count += 1; var buf: [32]u8 = undefined; const date = try std.fmt.bufPrint(&buf, "@{d} +0000", .{1_790_000_000 + @as(u64, r.count) * 60}); try r.env.put("GIT_AUTHOR_DATE", date); try r.env.put("GIT_COMMITTER_DATE", date); try r.git(&.{ "commit", "-q", "--allow-empty", "-m", message }); }};
test "the repository's object IDs are as long as its format makes them" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try repo.commit("first"); const head = try repo.output(&.{ "rev-parse", "HEAD" }); defer testing.allocator.free(head); const sha256 = if (testing.environ.getAlloc(testing.allocator, "GREBE_TEST_OBJECT_FORMAT")) |f| blk: { defer testing.allocator.free(f); break :blk std.mem.eql(u8, f, "sha256"); } else |_| false; try testing.expectEqual(@as(usize, if (sha256) 64 else 40), std.mem.trimEnd(u8, head, "\n").len);}