Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Decorations compared with git: `--decorate` in every mode and format,//! `%d`, `%D` and `%(decorate:...)`, the ref filters and the settings//! that choose them, their colors, `--source` and `%S`, and//! `--simplify-by-decoration`, over generated histories with lightweight//! and annotated tags, tags of tags, tags of trees and blobs, branches,//! remote-tracking refs and refs of other kinds, under SHA-1 and SHA-256.
const std = @import("std");
const tool = @import("zig_git.zig");const Repo = tool.Repo;const walk_tests = @import("walk_tests.zig");
const testing = std.testing;
fn same(repo: *Repo, args: []const []const u8) !void { try tool.expectSameAsGitHow(repo, args, .{});}
/// A generated history with every kind of ref a decoration can come from.fn makeRefs(repo: *Repo, seed: u64) !void { try walk_tests.makeHistory(repo, seed, 70); const tags = try repo.output(&.{ "tag", "-l" }); defer repo.gpa.free(tags); var it = std.mem.tokenizeScalar(u8, tags, '\n'); const first = it.next().?; try repo.git(&.{ "tag", "-a", "-m", "of a tag", "tag-of-tag", first }); try repo.git(&.{ "tag", "tree-tag", "main~2^{tree}" }); try repo.git(&.{ "tag", "-a", "-m", "blob", "blob-tag", "main:f1" }); try repo.git(&.{ "update-ref", "refs/stash", "b2" }); try repo.git(&.{ "update-ref", "refs/notes/commits", "b3~1" }); try repo.git(&.{ "update-ref", "refs/other/x", "main~1" }); try repo.git(&.{ "branch", "nested/b", "main~3" }); try repo.git(&.{ "branch", "same", "main" }); try repo.git(&.{ "update-ref", "refs/remotes/origin/HEAD", "main" }); try repo.git(&.{ "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main" }); try repo.git(&.{ "update-ref", "refs/remotes/origin/main", "main~1" });}
const decorating = [_][]const []const u8{ &.{ "log", "--decorate", "--oneline", "--all" }, &.{ "log", "--decorate=full", "--oneline", "--all" }, &.{ "log", "--decorate=short", "--all" }, &.{ "log", "--decorate=no", "--oneline", "--all" }, &.{ "log", "--decorate=auto", "--oneline", "--all" }, &.{ "log", "--no-decorate", "--format=%h%d", "--all" }, &.{ "log", "--format=%h [%d] [%D]", "--all" }, &.{ "log", "--format=%(decorate)|%(decorate:prefix=<,suffix=>,separator=;,pointer=>>,tag=T:)|%(decorate:prefix=,suffix=)", "--all" }, &.{ "log", "--format=%(decorate:prefix=[%x2C%x29],suffix=]%n)%(decorate:prefix)%(decorate:,x)%(decorate:bogus=1)", "--all" }, &.{ "log", "--format=<%(decorate:prefix=[%x00x,suffix=]%x41,tag=T%x00Z)>", "--all" }, &.{ "log", "--format=%h%+d%-D% d", "--all" }, &.{ "log", "--decorate", "--pretty=medium", "--parents", "-5" }, &.{ "log", "--decorate", "--pretty=fuller", "--all" }, &.{ "log", "--decorate", "--pretty=raw", "-3" }, &.{ "log", "--decorate", "--pretty=email", "-3" }, &.{ "log", "--decorate", "--pretty=reference", "-3" }, &.{ "log", "--decorate", "--left-right", "--boundary", "--oneline", "b1...main" }, &.{ "log", "--color=always", "--decorate", "--oneline", "--all" }, &.{ "log", "--color=always", "--format=%d|%D|%C(auto)%d|%C(auto)%(decorate:prefix=,suffix=,tag=,pointer=)", "--all" }, &.{ "-c", "color.decorate.branch=red", "-c", "color.decorate.HEAD=bold blue", "-c", "color.decorate.tag=green", "-c", "color.decorate.remoteBranch=cyan", "-c", "color.decorate.stash=magenta", "log", "--color=always", "--oneline", "--decorate", "--clear-decorations", "--all" }, &.{ "-c", "color.decorate.branch=bogus", "log", "-1" }, &.{ "-c", "log.decorate=short", "log", "--oneline", "--all" }, &.{ "-c", "log.decorate=full", "log", "--oneline", "--all" }, &.{ "-c", "log.decorate=true", "log", "--oneline", "-3" }, &.{ "-c", "log.decorate=false", "log", "--oneline", "-3" }, &.{ "-c", "log.decorate=short", "log", "--no-decorate", "--oneline", "-3" }, &.{ "-c", "log.initialDecorationSet=all", "log", "--decorate", "--oneline", "--all" }, &.{ "-c", "log.excludeDecoration=refs/tags", "log", "--decorate", "--oneline", "--all" }, &.{ "-c", "log.excludeDecoration=refs/tags", "log", "--decorate", "--oneline", "--all", "--decorate-refs=refs/tags/t*" }, &.{ "log", "--decorate", "--oneline", "--all", "--clear-decorations" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs=refs/heads", "--clear-decorations" }, &.{ "log", "--decorate", "--oneline", "--all", "--clear-decorations", "--decorate-refs=refs/heads" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs=refs/heads", "--decorate-refs-exclude=refs/heads/b1" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs", "tags/*" }, &.{ "rev-list", "--format=%h %d %D", "--all" }, &.{ "show", "-s", "--decorate", "tag-of-tag", "main" }, &.{ "show", "-s", "--decorate", "--oneline", "main~1" }, &.{ "log", "--format=%h [%S]", "-3" }, &.{ "rev-list", "--format=%h [%S]", "-3", "main" }, &.{ "show", "-s", "--format=%h [%S] [%d]", "main", "main~1", "tag-of-tag" }, &.{ "log", "--source", "--oneline", "--decorate", "--all" }, &.{ "log", "--source", "-2" }, &.{ "log", "--source", "--format=%h %S", "b1~2..main", "b3" }, &.{ "log", "--source", "--format=%h %S", "--branches", "--tags" }, &.{ "log", "--source", "--format=%h %S", "--remotes", "--glob=refs/s*" }, &.{ "log", "--simplify-by-decoration", "--format=%h %p %d", "--all" }, &.{ "log", "--simplify-by-decoration", "--format=%h %p %d", "--topo-order", "main", "b2" }, &.{ "log", "--simplify-by-decoration", "--format=%h %p %d", "--decorate-refs=refs/tags", "--all" }, &.{ "log", "--simplify-by-decoration", "--parents", "--oneline", "--decorate", "--clear-decorations", "--all" }, &.{ "rev-list", "--simplify-by-decoration", "--parents", "--all" }, &.{ "rev-list", "--simplify-by-decoration", "--reverse", "main" },};
const patterns = [_][]const u8{ "heads", "refs/heads", "refs/heads/", "refs/heads/*", "b1", "refs/tags/t*", "tags/*", "refs/remotes", "origin/*", "HEAD", "refs/other", "refs/*", "*", "refs", "**", "", "stash", "heads/nested" };
test "decorations are found, filtered and written as git writes them" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeRefs(&repo, 3); for (decorating) |args| try same(&repo, args); for (patterns) |p| { const include = try std.fmt.allocPrint(testing.allocator, "--decorate-refs={s}", .{p}); defer testing.allocator.free(include); const exclude = try std.fmt.allocPrint(testing.allocator, "--decorate-refs-exclude={s}", .{p}); defer testing.allocator.free(exclude); try same(&repo, &.{ "log", "--decorate", "--oneline", "--all", include }); try same(&repo, &.{ "log", "--decorate", "--oneline", "--all", exclude }); } // Detached, HEAD stands alone. try repo.git(&.{ "checkout", "-q", "--detach", "main~1" }); try same(&repo, &.{ "log", "--decorate", "--oneline", "--all" }); try same(&repo, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs-exclude=HEAD" }); try repo.git(&.{ "checkout", "-q", "main" }); try same(&repo, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs-exclude=refs/heads/main" });}
/// The tags after `commit` and before it, as grebe finds them: each/// tag's commit, of those that contain it (or that it contains), but not/// those on the commit itself, once each, and space-separated.fn tagCommits(repo: *Repo, flag: []const u8, commit: []const u8, out: *std.ArrayList([]u8)) !void { const gpa = repo.gpa; const filter = try std.fmt.allocPrint(gpa, "{s}{s}", .{ flag, commit }); defer gpa.free(filter); const listed = try repo.output(&.{ "for-each-ref", "--format=%(if)%(*objectname)%(then)%(*objectname)%(else)%(objectname)%(end) %(refname:short)", filter, "refs/tags/" }); defer gpa.free(listed); var lines = std.mem.tokenizeScalar(u8, listed, '\n'); while (lines.next()) |line| { const id = line[0 .. std.mem.findScalar(u8, line, ' ') orelse line.len]; if (std.mem.eql(u8, id, commit)) continue; const dup = for (out.items) |x| { if (std.mem.eql(u8, x, id)) break true; } else false; if (!dup) try out.append(gpa, try gpa.dupe(u8, id)); }}
test "the nearest tags after a commit are found as grebe asks git for them" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeRefs(&repo, 11); // More tags, so that most commits have some after them. const ids_out = try repo.output(&.{ "rev-list", "--all" }); defer gpa.free(ids_out); var ids: std.ArrayList([]const u8) = .empty; defer ids.deinit(gpa); var it = std.mem.tokenizeScalar(u8, ids_out, '\n'); while (it.next()) |id| try ids.append(gpa, id); for (ids.items, 0..) |id, k| if (k % 7 == 3) { var name: [16]u8 = undefined; try repo.git(&.{ "tag", try std.fmt.bufPrint(&name, "extra{d}", .{k}), id }); }; for ([_]bool{ false, true }) |graph| { if (graph) try repo.git(&.{ "commit-graph", "write", "--reachable" }); for (ids.items, 0..) |x, k| { if (k % 3 != 0) continue; var after: std.ArrayList([]u8) = .empty; defer { for (after.items) |a| gpa.free(a); after.deinit(gpa); } try tagCommits(&repo, "--contains=", x, &after); const contains = try std.fmt.allocPrint(gpa, "--contains={s}", .{x}); defer gpa.free(contains); try same(&repo, &.{ "for-each-ref", "--format=%(refname:short)", contains, "refs/heads/" }); if (after.items.len != 0) { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "-c", "log.showSignature=false", "log", "--simplify-by-decoration", "--decorate-refs=refs/tags/", "--ancestry-path", "--format=%H %P" }); for (after.items) |a| try args.append(gpa, a); const not = try std.fmt.allocPrint(gpa, "^{s}", .{x}); defer gpa.free(not); try args.appendSlice(gpa, &.{ not, "--" }); try same(&repo, args.items); } var before: std.ArrayList([]u8) = .empty; defer { for (before.items) |b| gpa.free(b); before.deinit(gpa); } try tagCommits(&repo, "--merged=", x, &before); if (before.items.len != 0) { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "merge-base", "--independent" }); for (before.items) |b| try args.append(gpa, b); try same(&repo, args.items); } } } // Without --ancestry-path, an excluded commit is refused. const ours = try tool.runOurs(&repo, &.{ "log", "--simplify-by-decoration", "--format=%H", "main", "^main~3" }, .{}); defer ours.deinit(gpa); try testing.expectEqual(128, ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "not supported") != null);}
test "grafted and replaced commits are decorated as git decorates them" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeRefs(&repo, 5); try repo.git(&.{ "replace", "--graft", "main~2", "main~4" }); for ([_][]const []const u8{ &.{ "log", "--decorate", "--oneline", "--all" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs=refs/heads" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs-exclude=refs/replace" }, &.{ "--no-replace-objects", "log", "--decorate", "--oneline", "--all" }, &.{ "log", "--color=always", "--decorate", "--oneline", "-5" }, &.{ "-c", "color.decorate.grafted=red", "log", "--color=always", "--decorate", "--oneline", "-5" }, }) |args| try same(&repo, args);
const url = try std.fmt.allocPrint(gpa, "file://{s}", .{repo.path}); defer gpa.free(url); try repo.git(&.{ "clone", "-q", "--depth", "2", "--no-single-branch", url, "shallow" }); for ([_][]const []const u8{ &.{ "log", "--decorate", "--oneline", "--all" }, &.{ "log", "--decorate", "--oneline", "--all", "--decorate-refs=refs/heads" }, &.{ "log", "--color=always", "--format=%C(auto)%d", "--all" }, }) |args| try tool.expectSameAsGitHow(&repo, args, .{ .cwd = "shallow" });}