move git out of contributions

This commit is contained in:
Emil Lerch 2026-04-21 13:13:49 -07:00
parent b717c9fa3d
commit f134e701d0
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -21,6 +21,7 @@
const std = @import("std");
const zfin = @import("../root.zig");
const cli = @import("common.zig");
const git = @import("../git.zig");
const fmt = cli.fmt;
const Date = zfin.Date;
const Lot = zfin.Lot;
@ -44,7 +45,7 @@ pub fn run(
const arena = arena_state.allocator();
// 1. Figure out the git repo and the portfolio's path inside it.
const repo = findGitRepo(arena, portfolio_path) catch |err| {
const repo = git.findRepo(arena, portfolio_path) catch |err| {
switch (err) {
error.NotInGitRepo => try cli.stderrPrint("Error: contributions requires portfolio.srf to be in a git repo.\n"),
error.GitUnavailable => try cli.stderrPrint("Error: could not run 'git'. Is git installed and on PATH?\n"),
@ -54,7 +55,7 @@ pub fn run(
};
// 2. Decide which snapshots to compare.
const status = try pathStatus(arena, repo.root, repo.rel_path);
const status = try git.pathStatus(arena, repo.root, repo.rel_path);
if (status == .untracked) {
try cli.stderrPrint("Error: portfolio.srf is not tracked in git. Add and commit it first.\n");
return;
@ -63,7 +64,7 @@ pub fn run(
// 3. Pull both snapshots.
const before = if (dirty)
gitShow(arena, repo.root, "HEAD", repo.rel_path) catch |err| {
git.show(arena, repo.root, "HEAD", repo.rel_path) catch |err| {
switch (err) {
error.PathMissingInRev => try cli.stderrPrint("Error: portfolio.srf not present at HEAD.\n"),
else => try cli.stderrPrint("Error reading HEAD:portfolio.srf from git.\n"),
@ -71,7 +72,7 @@ pub fn run(
return;
}
else
gitShow(arena, repo.root, "HEAD~1", repo.rel_path) catch |err| {
git.show(arena, repo.root, "HEAD~1", repo.rel_path) catch |err| {
switch (err) {
error.PathMissingInRev => try cli.stderrPrint("Error: portfolio.srf not present at HEAD~1.\n"),
error.UnknownRevision => try cli.stderrPrint("Error: no prior commit to compare against (HEAD~1 does not exist).\n"),
@ -86,7 +87,7 @@ pub fn run(
return;
}
else
gitShow(arena, repo.root, "HEAD", repo.rel_path) catch {
git.show(arena, repo.root, "HEAD", repo.rel_path) catch {
try cli.stderrPrint("Error reading HEAD:portfolio.srf from git.\n");
return;
};
@ -141,108 +142,10 @@ pub fn run(
}
// Git discovery / invocation
const RepoInfo = struct {
/// Absolute path to the repo root.
root: []const u8,
/// Relative path from root to the portfolio file (using '/'-style separators).
rel_path: []const u8,
};
fn findGitRepo(allocator: std.mem.Allocator, portfolio_path: []const u8) !RepoInfo {
// Resolve the portfolio file's directory.
const abs_path = try std.fs.cwd().realpathAlloc(allocator, portfolio_path);
const dir = std.fs.path.dirname(abs_path) orelse "/";
// git -C <dir> rev-parse --show-toplevel
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "git", "-C", dir, "rev-parse", "--show-toplevel" },
.max_output_bytes = 64 * 1024,
}) catch {
return error.GitUnavailable;
};
switch (result.term) {
.Exited => |code| if (code != 0) return error.NotInGitRepo,
else => return error.NotInGitRepo,
}
const root_raw = std.mem.trim(u8, result.stdout, " \t\r\n");
const root = try allocator.dupe(u8, root_raw);
// Relative path from root to the portfolio file.
const rel_raw = if (std.mem.startsWith(u8, abs_path, root) and abs_path.len > root.len)
std.mem.trimLeft(u8, abs_path[root.len..], "/")
else
std.fs.path.basename(abs_path);
const rel = try allocator.dupe(u8, rel_raw);
return .{ .root = root, .rel_path = rel };
}
/// Inspect the portfolio file's git status. Only the portfolio file is
/// considered (via pathspec); untracked files elsewhere in the repo are
/// ignored. Returns one of:
/// - `.modified`: tracked and has unstaged and/or staged changes
/// - `.clean`: tracked and matches HEAD (no uncommitted changes)
/// - `.untracked`: not yet added to the repo (no HEAD version to diff against)
const PathStatus = enum { modified, clean, untracked };
fn pathStatus(allocator: std.mem.Allocator, root: []const u8, rel_path: []const u8) !PathStatus {
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "git", "-C", root, "status", "--porcelain", "--", rel_path },
.max_output_bytes = 64 * 1024,
}) catch return error.GitUnavailable;
switch (result.term) {
.Exited => |code| if (code != 0) return error.GitStatusFailed,
else => return error.GitStatusFailed,
}
const trimmed = std.mem.trim(u8, result.stdout, " \t\r\n");
if (trimmed.len == 0) return .clean;
// Porcelain format: "XY <path>" where X is index status, Y is worktree status.
// "??" means untracked. Anything else with at least one non-space is a modification.
if (trimmed.len >= 2 and trimmed[0] == '?' and trimmed[1] == '?') return .untracked;
return .modified;
}
/// Run `git show <rev>:<path>` and return the stdout bytes.
fn gitShow(allocator: std.mem.Allocator, root: []const u8, rev: []const u8, rel_path: []const u8) ![]const u8 {
// Build "<rev>:<rel_path>".
const spec = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ rev, rel_path });
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "git", "-C", root, "show", spec },
.max_output_bytes = 32 * 1024 * 1024,
}) catch return error.GitUnavailable;
switch (result.term) {
.Exited => |code| {
if (code != 0) {
// Distinguish "no such revision" from "path missing".
if (std.mem.indexOf(u8, result.stderr, "unknown revision") != null or
std.mem.indexOf(u8, result.stderr, "bad revision") != null or
std.mem.indexOf(u8, result.stderr, "ambiguous argument") != null)
{
return error.UnknownRevision;
}
if (std.mem.indexOf(u8, result.stderr, "does not exist") != null or
std.mem.indexOf(u8, result.stderr, "exists on disk, but not in") != null)
{
return error.PathMissingInRev;
}
return error.GitShowFailed;
}
},
else => return error.GitShowFailed,
}
return result.stdout;
}
//
// Git plumbing lives in `src/git.zig` (shared with future snapshot
// features). This module only classifies which revisions to diff and
// how to interpret the result.
// Diff algorithm