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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git merge-base`: merge bases found as `git merge-base` finds//! them, by the library's `merge_base`.//!//! zig-git merge-base [-a | --all] <commit> <commit>...//! zig-git merge-base [-a | --all] --octopus <commit>...//! zig-git merge-base --is-ancestor <commit> <commit>//! zig-git merge-base --independent <commit>...//! zig-git merge-base --fork-point <ref> [<commit>]//!//! Its messages and exit statuses are git's: 1 when there is no merge//! base, or the answer to `--is-ancestor` or `--fork-point` is no, 128//! for a name that is not a commit, and 129 for a command used wrongly.
const std = @import("std");const Io = std.Io;
const git = @import("git");const ObjectId = git.ObjectId;const Diagnostics = git.Diagnostics;const tool = @import("zig_git.zig");const Context = tool.Context;const discovery = @import("discovery.zig");const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const history = @import("history.zig");
/// What is said where git prints its own usage; the exit status is git's.pub const usage = \\usage: zig-git merge-base [-a | --all] <commit> <commit>... \\ or: zig-git merge-base [-a | --all] --octopus <commit>... \\ or: zig-git merge-base --is-ancestor <commit> <commit> \\ or: zig-git merge-base --independent <commit>... \\ or: zig-git merge-base --fork-point <ref> [<commit>] \\;
const Mode = enum { bases, octopus, independent, is_ancestor, fork_point };
/// Runs `merge-base` with `args`, the arguments after its name.pub fn run(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { const gpa = ctx.gpa; var all = false; var mode: Mode = .bases; var modes: std.ArrayList(Mode) = .empty; defer modes.deinit(gpa); var operands: std.ArrayList([]const u8) = .empty; defer operands.deinit(gpa); var only_operands = false; for (args) |arg| { if (!only_operands and arg.len > 1 and arg[0] == '-') { if (std.mem.eql(u8, arg, "--")) { only_operands = true; } else if (std.mem.eql(u8, arg, "-a") or std.mem.eql(u8, arg, "--all")) { all = true; } else if (std.mem.eql(u8, arg, "--no-all")) { all = false; } else if (std.mem.eql(u8, arg, "--octopus")) { try modes.append(gpa, .octopus); } else if (std.mem.eql(u8, arg, "--independent")) { try modes.append(gpa, .independent); } else if (std.mem.eql(u8, arg, "--is-ancestor")) { try modes.append(gpa, .is_ancestor); } else if (std.mem.eql(u8, arg, "--fork-point")) { try modes.append(gpa, .fork_point); } else { try stderr.writeAll(usage); return error.UsageQuiet; } continue; } try operands.append(gpa, arg); } // The last of the modes given wins, as git's options do; two different // ones are an error. for (modes.items) |m| { if (mode != .bases and mode != m) { try stderr.print("error: options '--{s}' and '--{s}' cannot be used together\n", .{ modeName(m), modeName(mode) }); try stderr.writeAll(usage); return error.UsageQuiet; } mode = m; } if (mode == .is_ancestor and all) return tool.fatalMessage(stderr, "options '--is-ancestor' and '--all' cannot be used together", .{}); switch (mode) { .bases => if (operands.items.len < 2) { try stderr.writeAll(usage); return error.UsageQuiet; }, .is_ancestor => { if (operands.items.len < 2) { try stderr.writeAll(usage); return error.UsageQuiet; } if (operands.items.len != 2) return tool.fatalMessage(stderr, "--is-ancestor takes exactly two commits", .{}); }, .fork_point => if (operands.items.len < 1 or operands.items.len > 2) { try stderr.writeAll(usage); return error.UsageQuiet; }, .octopus, .independent => if (operands.items.len == 0) return error.No, }
var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); const settings = try object_names.settings(&repo.config, stderr); var revs = try revisions.Revisions.init(ctx, &repo, settings, .cat_file, stderr); defer revs.deinit(); revs.resolver.options.search_refs = true; var h = try history.History.open(ctx, &repo, stderr); defer h.close(); const store = &h.store;
var commits: std.ArrayList(git.Commits.Index) = .empty; defer commits.deinit(gpa); var fork_tips: std.ArrayList(git.Commits.Index) = .empty; defer fork_tips.deinit(gpa); if (mode == .fork_point) { const ref_text = operands.items[0]; var diag: Diagnostics = .{}; const full = revs.resolver.fullName(ref_text, &diag) catch .none; revs.resolver.clearMessages(); const name = switch (full) { .name => |n| n, else => return tool.fatalMessage(stderr, "No such ref: '{s}'", .{ref_text}), }; const commit_text = if (operands.items.len == 2) operands.items[1] else "HEAD"; const found = revs.resolver.resolve(commit_text, &diag) catch return tool.fatalMessage(stderr, "Not a valid object name: '{s}'", .{commit_text}); try revs.flushMessages(stderr, 1, false); const c = try revs.resolver.peelToCommit(found.id) orelse return tool.fatalMessage(stderr, "Not a valid object name: '{s}'", .{commit_text}); try commits.append(gpa, try store.name(c)); // The ref's value, then every value its reflog records. const refs = repo.refs(); var res = refs.resolve(ctx.io, gpa, name, &diag) catch return tool.fatalMessage(stderr, "No such ref: '{s}'", .{ref_text}); defer res.deinit(gpa); switch (res.outcome) { .direct => |d| if (try revs.resolver.peelToCommit(d.id)) |tip| try fork_tips.append(gpa, try store.name(tip)), else => {}, } var log = refs.reflog(ctx.io, gpa, name, &diag) catch null; if (log) |*l| { defer l.deinit(); var k = l.entries.len; while (k > 0) { k -= 1; const e = l.entries[k]; if (std.mem.allEqual(u8, e.new.slice(), 0)) continue; const tip = (revs.resolver.peelToCommit(e.new) catch null) orelse continue; const ti = try store.name(tip); if (std.mem.findScalar(git.Commits.Index, fork_tips.items, ti) == null) try fork_tips.append(gpa, ti); } } } else { for (operands.items) |text| { var diag: Diagnostics = .{}; const found = revs.resolver.resolve(text, &diag) catch |err| { try revs.flushMessages(stderr, 1, false); if (git.Resolver.severity(err) == .unsupported) try revisions.stopAt(err, &diag, false, false, stderr); return tool.fatalMessage(stderr, "Not a valid object name {s}", .{text}); }; try revs.flushMessages(stderr, 1, false); const c = try revs.resolver.peelToCommit(found.id) orelse { if (found.kind) |kind| try stderr.print("error: object {f} is a {s}, not a commit\n", .{ found.id, kind.name() }) else try stderr.print("error: Could not read {f}\n", .{found.id}); return tool.fatalMessage(stderr, "Not a valid commit name {s}", .{text}); }; try commits.append(gpa, try store.name(c)); } }
var diag: Diagnostics = .{}; const fail = struct { fn f(hh: *history.History, err: anyerror, d: *const Diagnostics, w: *Io.Writer) error{Fatal} { hh.report(w) catch {}; return hh.fail(err, d, null, w); } }.f; switch (mode) { .is_ancestor => { const yes = git.merge_base.isAncestor(gpa, store, commits.items[0], commits.items[1], &diag) catch |err| return fail(&h, err, &diag, stderr); try h.report(stderr); if (!yes) return error.No; }, .independent => { const n = git.merge_base.independent(gpa, store, commits.items, &diag) catch |err| return fail(&h, err, &diag, stderr); try h.report(stderr); for (commits.items[0..n]) |c| try stdout.print("{f}\n", .{store.id(c)}); }, .octopus => { const found = git.merge_base.octopus(gpa, store, commits.items, all, &diag) catch |err| return fail(&h, err, &diag, stderr); defer gpa.free(found); try h.report(stderr); if (found.len == 0) return error.No; for (found) |c| try stdout.print("{f}\n", .{store.id(c)}); }, .bases => { const found = git.merge_base.bases(gpa, store, commits.items[0], commits.items[1..], all, &diag) catch |err| return fail(&h, err, &diag, stderr); defer gpa.free(found); try h.report(stderr); if (found.len == 0) return error.No; for (found) |c| try stdout.print("{f}\n", .{store.id(c)}); }, .fork_point => { const found = git.merge_base.forkPoint(gpa, store, commits.items[0], fork_tips.items, &diag) catch |err| return fail(&h, err, &diag, stderr); try h.report(stderr); const c = found orelse return error.No; try stdout.print("{f}\n", .{store.id(c)}); }, }}
fn modeName(m: Mode) []const u8 { return switch (m) { .octopus => "octopus", .independent => "independent", .is_ancestor => "is-ancestor", .fork_point => "fork-point", .bases => "", };}