Something went wrong. Try again.
A history browser for git, a replacement for gitk, drawn with Vulkan on Wayland.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! grebe: a history browser for git.//!//! `grebe [revision arguments] [-- paths]` takes what `gitk` takes, and//! hands it to git as it is.
const std = @import("std");const builtin = @import("builtin");const build_options = @import("build_options");
const aerie = @import("aerie");
const App = @import("App.zig");const l10n = @import("l10n.zig");const load_config = @import("config/load.zig");
/// Where grebe looks for the repository, in place of the current/// directory: what a file manager opens a folder with.const repository_option = "--repository=";
pub fn main(init: std.process.Init) !u8 { const gpa = init.gpa; const io = init.io; const arena = init.arena.allocator(); const args = try init.minimal.args.toSlice(arena);
// What goes wrong before the window opens, said when grebe stops. var failure: std.Io.Writer.Allocating = .init(arena); const stderr = &failure.writer; defer report(io, arena, failure.written());
try l10n.init(gpa, init.environ_map, null); defer l10n.deinit();
// `--repository=DIR` is grebe's, and goes no further; everything else // is git's. var git_args: std.ArrayList([]const u8) = .empty; var repository: ?[]const u8 = null; var past_options = false; for (args[1..]) |a| { if (!past_options and std.mem.startsWith(u8, a, repository_option)) { repository = a[repository_option.len..]; continue; } if (std.mem.eql(u8, a, "--")) past_options = true; try git_args.append(arena, a); }
for (git_args.items) |a| { if (std.mem.eql(u8, a, "--")) break; if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { var stdout_buf: [1024]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &stdout_buf); try stdout_file.interface.print("{s}\n", .{l10n.text("usage")}); try stdout_file.interface.flush(); return 0; } if (std.mem.eql(u8, a, "--version")) { var stdout_buf: [64]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &stdout_buf); try stdout_file.interface.print("grebe {s}\n", .{build_options.version}); try stdout_file.interface.flush(); return 0; } }
// Before anything else runs: git, and every thread, look from there. if (repository) |dir| std.process.setCurrentPath(io, dir) catch |err| { try l10n.write(stderr, "cli-repository-failed", &.{ l10n.str("path", dir), l10n.str("error", @errorName(err)) }); try stderr.writeByte('\n'); return 1; };
// A repository, first: said on the terminal it was started from, as // git says it, rather than in a window with nothing to show. const top = std.process.run(arena, io, .{ .argv = &.{ "git", "rev-parse", "--show-toplevel" }, }) catch |err| { try l10n.write(stderr, "cli-git-failed", &.{l10n.str("error", @errorName(err))}); try stderr.writeByte('\n'); return 1; }; const ok = switch (top.term) { .exited => |code| code == 0, else => false, }; if (!ok) { try stderr.writeAll(top.stderr); return 1; } const top_path = std.mem.trimEnd(u8, top.stdout, "\n"); const name = std.fs.path.basename(top_path);
// The desktop's settings, for the configuration to see, and then the // configuration: a mistake in it is said on the terminal, as git's // are. const desktop = aerie.desktop.Watcher.start(gpa, io, init.environ_map); var desktop_owned = desktop != null; defer if (desktop_owned) desktop.?.destroy(); var settings: aerie.desktop.Settings = .init(gpa); var seen: u64 = 0; if (desktop) |d| if (try d.takeIfNewer(gpa, 0)) |latest| { settings.deinit(); settings = latest.settings; seen = latest.generation; }; var problem: load_config.Problem = .{}; var loaded = load_config.load(gpa, io, init.environ_map, top_path, settings, &problem) catch |err| switch (err) { error.OutOfMemory => return err, else => { try l10n.write(stderr, "cli-config-failed", &.{l10n.str("problem", try std.fmt.allocPrint(arena, "{f}", .{&problem}))}); try stderr.writeByte('\n'); return 1; }, }; var loaded_owned = true; defer if (loaded_owned) loaded.deinit(gpa); if (loaded.config().language) |language| try l10n.init(gpa, init.environ_map, language); const title = try l10n.format(arena, "window-title", &.{l10n.str("repository", name)});
// The app takes the configuration and the settings, whatever happens. loaded_owned = false; desktop_owned = false; const app = App.create(gpa, io, init.environ_map, .{ .args = git_args.items, .title = title, .snapshot = init.environ_map.get("GREBE_SNAPSHOT"), .config = loaded, .desktop = desktop, .desktop_seen = seen, }) catch |err| { try l10n.write(stderr, "cli-failed", &.{l10n.str("error", @errorName(err))}); try stderr.writeByte('\n'); return 1; }; defer app.destroy(); try app.run(); return 0;}
/// Says why grebe stopped before its window opened: on the terminal, and/// on Windows in a message box as well, since one started from Explorer/// has no terminal to say it on.fn report(io: std.Io, arena: std.mem.Allocator, text: []const u8) void { if (text.len == 0) return; var buf: [1024]u8 = undefined; var w = std.Io.File.stderr().writer(io, &buf); w.interface.writeAll(text) catch {}; w.interface.flush() catch {}; if (builtin.os.tag == .windows and !builtin.is_test) { const win32 = @import("win32").everything; const message = std.unicode.utf8ToUtf16LeAllocZ(arena, std.mem.trimEnd(u8, text, "\r\n")) catch return; _ = win32.MessageBoxW(null, message, std.unicode.utf8ToUtf16LeStringLiteral("grebe"), .{ .ICONHAND = 1 }); }}
test { _ = @import("App.zig"); _ = @import("l10n.zig"); _ = @import("Clock.zig"); _ = @import("history/History.zig"); _ = @import("history/Loader.zig"); _ = @import("history/git/LogParser.zig"); _ = @import("history/git/Relate.zig"); _ = @import("history/RelationsCache.zig"); _ = @import("history/git/Log.zig"); _ = @import("history/git/Show.zig"); _ = @import("history/git/ShowParser.zig"); _ = @import("history/git/Local.zig"); _ = @import("history/git/reference.zig"); _ = @import("history/git/Refs.zig"); _ = @import("ui/Dialog.zig"); _ = @import("ui/Menu.zig"); _ = @import("find/Query.zig"); _ = @import("history/git/Find.zig"); _ = @import("ui/TextField.zig"); _ = @import("ui/CommitList.zig"); _ = @import("graph/Layout.zig"); _ = @import("ui/Document.zig"); _ = @import("ui/Panes.zig"); _ = @import("ui/theme.zig"); _ = @import("ui/Scrollbar.zig"); _ = @import("testing/fixture.zig"); _ = @import("config/Config.zig"); _ = @import("config/load.zig"); _ = @import("input/bindings.zig");}