Skip to content

Commit 1a1e3d0

Browse files
fieldingclaude
andcommitted
Extract shared fmt module, expand tests to 47, clean up codebase
Refactor: - New src/fmt.zig: shared diff callbacks (compactCallback, humanCallback), stat summary (writeStat), and date formatting (epochToDate). Eliminates ~200 lines of duplication between diff.zig and show.zig. - Fixes subtle color guard inconsistency in show.zig's human callback (bare if vs if/else blocks). - Remove dead code: unused last_file field, unused color constants (blue, magenta, white, bg_red, bg_green), redundant default_count. - Clean up cli.zig: remove unused writer param from openRepo, hoist repo open above command dispatch. New tests (47 total, up from 34): - show --stat: delete-only commit, additions-only commit, totals consistency - Merge commits: log and show on merge - Multi-file diffs: file listing and count - Subdirectory: status/log/diff from within a subdirectory - Usage: no args, --help, help all show usage text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb70b3e commit 1a1e3d0

7 files changed

Lines changed: 344 additions & 426 deletions

File tree

src/cli.zig

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,16 @@ pub fn run() !void {
9595
try git.init();
9696
defer git.deinit();
9797

98-
// Native commands — optimized output via libgit2
98+
var repo = try openRepo();
99+
defer repo.deinit();
100+
99101
if (std.mem.eql(u8, cmd, "status") or std.mem.eql(u8, cmd, "s")) {
100-
var repo = try openRepo(w);
101-
defer repo.deinit();
102102
try status.run(repo.repo, human, w);
103103
} else if (std.mem.eql(u8, cmd, "log") or std.mem.eql(u8, cmd, "l")) {
104-
var repo = try openRepo(w);
105-
defer repo.deinit();
106104
try log.run(repo.repo, human, count, w);
107105
} else if (std.mem.eql(u8, cmd, "diff") or std.mem.eql(u8, cmd, "d")) {
108-
var repo = try openRepo(w);
109-
defer repo.deinit();
110106
try diff.run(repo.repo, human, staged, w);
111107
} else if (std.mem.eql(u8, cmd, "show")) {
112-
var repo = try openRepo(w);
113-
defer repo.deinit();
114108
// Find revision arg (first non-flag arg after "show")
115109
var rev: ?[]const u8 = null;
116110
for (args[2..]) |arg| {
@@ -125,7 +119,7 @@ pub fn run() !void {
125119
try w.flush();
126120
}
127121

128-
fn openRepo(_: *std.Io.Writer) !git.Repository {
122+
fn openRepo() !git.Repository {
129123
return git.Repository.openFromCwd() catch {
130124
std.debug.print("fatal: not a git repository\n", .{});
131125
std.process.exit(128);

src/cmd/diff.zig

Lines changed: 8 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const std = @import("std");
22
const git = @import("../git.zig");
33
const c = git.c;
44
const color = @import("../color.zig");
5+
const fmt = @import("../fmt.zig");
56

67
const Writer = std.Io.Writer;
78

@@ -15,6 +16,7 @@ pub fn run(repo: *c.git_repository, human: bool, staged: bool, w: *Writer) !void
1516
var diff_result: ?*c.git_diff = null;
1617

1718
if (staged) {
19+
// Diff HEAD to index. HEAD may not exist yet (initial commit).
1820
var head: ?*c.git_object = null;
1921
_ = c.git_revparse_single(&head, repo, "HEAD");
2022
defer if (head != null) c.git_object_free(head);
@@ -41,194 +43,19 @@ pub fn run(repo: *c.git_repository, human: bool, staged: bool, w: *Writer) !void
4143
if (human) {
4244
try printHuman(diff_result, w);
4345
} else {
44-
var ctx = PrintCtx{ .writer = w, .last_file = null };
45-
_ = c.git_diff_print(diff_result, c.GIT_DIFF_FORMAT_PATCH, compactCallback, @ptrCast(&ctx));
46+
var ctx = fmt.CompactCtx{ .writer = w };
47+
_ = c.git_diff_print(diff_result, c.GIT_DIFF_FORMAT_PATCH, fmt.compactCallback, @ptrCast(&ctx));
4648
}
4749
}
4850

49-
// --- Compact (agent) output ---
50-
51-
const PrintCtx = struct {
52-
writer: *Writer,
53-
last_file: ?[*:0]const u8,
54-
};
55-
56-
fn compactCallback(
57-
delta: [*c]const c.git_diff_delta,
58-
_: [*c]const c.git_diff_hunk,
59-
line: [*c]const c.git_diff_line,
60-
payload: ?*anyopaque,
61-
) callconv(.c) c_int {
62-
const ctx: *PrintCtx = @ptrCast(@alignCast(payload));
63-
const content = line.*.content;
64-
const len = line.*.content_len;
65-
66-
if (len == 0 or content == null) return 0;
67-
68-
const slice = content[0..len];
69-
const w = ctx.writer;
70-
71-
switch (line.*.origin) {
72-
'+', '-', ' ' => {
73-
w.writeByte(@intCast(line.*.origin)) catch return -1;
74-
w.writeAll(slice) catch return -1;
75-
},
76-
'H' => {
77-
// Strip trailing context text after closing @@
78-
if (std.mem.indexOf(u8, slice, " @@")) |pos| {
79-
w.writeAll(slice[0 .. pos + 3]) catch return -1;
80-
w.writeByte('\n') catch return -1;
81-
} else {
82-
w.writeAll(slice) catch return -1;
83-
}
84-
},
85-
'F' => {
86-
if (delta != null) {
87-
const path = delta.*.new_file.path;
88-
if (path != null) {
89-
if (slice.len >= 4 and std.mem.eql(u8, slice[0..4], "diff")) {
90-
w.writeAll("--- ") catch return -1;
91-
w.writeAll(std.mem.span(path)) catch return -1;
92-
w.writeByte('\n') catch return -1;
93-
}
94-
}
95-
}
96-
},
97-
else => {},
98-
}
99-
100-
return 0;
101-
}
102-
103-
// --- Human output ---
104-
10551
fn printHuman(diff_result: ?*c.git_diff, w: *Writer) !void {
10652
const use_color = color.isTty();
107-
108-
// Print per-file stat + patch
10953
const num_deltas = c.git_diff_num_deltas(diff_result);
11054
if (num_deltas == 0) return;
11155

112-
// Collect stats first
113-
var stats: ?*c.git_diff_stats = null;
114-
try git.check(c.git_diff_get_stats(&stats, diff_result));
115-
defer c.git_diff_stats_free(stats);
116-
117-
const total_files = c.git_diff_stats_files_changed(stats);
118-
const total_add = c.git_diff_stats_insertions(stats);
119-
const total_del = c.git_diff_stats_deletions(stats);
120-
121-
// Summary line
122-
if (use_color) {
123-
try w.print("{s}{d} file{s}{s}, ", .{ color.bold, total_files, if (total_files != 1) "s" else "", color.reset });
124-
try w.print("{s}+{d}{s} ", .{ color.bright_green, total_add, color.reset });
125-
try w.print("{s}-{d}{s}\n\n", .{ color.bright_red, total_del, color.reset });
126-
} else {
127-
try w.print("{d} file{s}, +{d} -{d}\n\n", .{ total_files, if (total_files != 1) "s" else "", total_add, total_del });
128-
}
129-
130-
// Print each file's patch
131-
var ctx = HumanCtx{ .writer = w, .use_color = use_color, .current_file = null };
132-
_ = c.git_diff_print(diff_result, c.GIT_DIFF_FORMAT_PATCH, humanCallback, @ptrCast(&ctx));
133-
}
134-
135-
const HumanCtx = struct {
136-
writer: *Writer,
137-
use_color: bool,
138-
current_file: ?[*:0]const u8,
139-
};
140-
141-
fn humanCallback(
142-
delta: [*c]const c.git_diff_delta,
143-
_: [*c]const c.git_diff_hunk,
144-
line: [*c]const c.git_diff_line,
145-
payload: ?*anyopaque,
146-
) callconv(.c) c_int {
147-
const ctx: *HumanCtx = @ptrCast(@alignCast(payload));
148-
const content = line.*.content;
149-
const len = line.*.content_len;
150-
151-
if (len == 0 or content == null) return 0;
152-
153-
const slice = content[0..len];
154-
const w = ctx.writer;
155-
156-
switch (line.*.origin) {
157-
'F' => {
158-
// File header - print a clean colored separator on first line only
159-
if (delta != null) {
160-
const path = delta.*.new_file.path;
161-
if (path != null and slice.len >= 4 and std.mem.eql(u8, slice[0..4], "diff")) {
162-
// Check if this is a new file vs the last one we printed
163-
const new_path = path;
164-
if (ctx.current_file == null or ctx.current_file != new_path) {
165-
if (ctx.current_file != null) {
166-
// Separator between files
167-
w.writeByte('\n') catch return -1;
168-
}
169-
ctx.current_file = new_path;
170-
171-
const path_str = std.mem.span(path);
172-
173-
if (ctx.use_color) {
174-
// Bold filename
175-
w.writeAll(color.bold) catch return -1;
176-
w.writeAll(path_str) catch return -1;
177-
w.writeAll(color.reset) catch return -1;
178-
w.writeByte('\n') catch return -1;
179-
} else {
180-
w.writeAll(path_str) catch return -1;
181-
w.writeByte('\n') catch return -1;
182-
}
183-
}
184-
}
185-
}
186-
},
187-
'H' => {
188-
// Hunk header
189-
if (ctx.use_color) {
190-
w.writeAll(color.cyan) catch return -1;
191-
w.writeAll(slice) catch return -1;
192-
w.writeAll(color.reset) catch return -1;
193-
} else {
194-
w.writeAll(slice) catch return -1;
195-
}
196-
},
197-
'+' => {
198-
if (ctx.use_color) {
199-
w.writeAll(color.bright_green) catch return -1;
200-
w.writeByte('+') catch return -1;
201-
w.writeAll(slice) catch return -1;
202-
w.writeAll(color.reset) catch return -1;
203-
} else {
204-
w.writeByte('+') catch return -1;
205-
w.writeAll(slice) catch return -1;
206-
}
207-
},
208-
'-' => {
209-
if (ctx.use_color) {
210-
w.writeAll(color.bright_red) catch return -1;
211-
w.writeByte('-') catch return -1;
212-
w.writeAll(slice) catch return -1;
213-
w.writeAll(color.reset) catch return -1;
214-
} else {
215-
w.writeByte('-') catch return -1;
216-
w.writeAll(slice) catch return -1;
217-
}
218-
},
219-
' ' => {
220-
if (ctx.use_color) {
221-
w.writeAll(color.dim) catch return -1;
222-
w.writeByte(' ') catch return -1;
223-
w.writeAll(slice) catch return -1;
224-
w.writeAll(color.reset) catch return -1;
225-
} else {
226-
w.writeByte(' ') catch return -1;
227-
w.writeAll(slice) catch return -1;
228-
}
229-
},
230-
else => {},
231-
}
56+
try fmt.writeStat(diff_result, use_color, w);
57+
try w.writeByte('\n');
23258

233-
return 0;
59+
var ctx = fmt.HumanCtx{ .writer = w, .use_color = use_color, .current_file = null };
60+
_ = c.git_diff_print(diff_result, c.GIT_DIFF_FORMAT_PATCH, fmt.humanCallback, @ptrCast(&ctx));
23461
}

src/cmd/log.zig

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const std = @import("std");
22
const git = @import("../git.zig");
33
const c = git.c;
4-
const clr = @import("../color.zig");
4+
const color = @import("../color.zig");
5+
const fmt = @import("../fmt.zig");
56

67
const Writer = std.Io.Writer;
7-
const default_count: usize = 20;
88

99
pub fn run(repo: *c.git_repository, human: bool, count: usize, w: *Writer) !void {
1010
var walk: ?*c.git_revwalk = null;
@@ -16,9 +16,8 @@ pub fn run(repo: *c.git_repository, human: bool, count: usize, w: *Writer) !void
1616

1717
var oid: c.git_oid = undefined;
1818
var shown: usize = 0;
19-
const max = if (count == 0) default_count else count;
2019

21-
while (shown < max) {
20+
while (shown < count) {
2221
const err = c.git_revwalk_next(&oid, walk);
2322
if (err < 0) break;
2423

@@ -33,33 +32,16 @@ pub fn run(repo: *c.git_repository, human: bool, count: usize, w: *Writer) !void
3332
const short_hash = oid_buf[0..7];
3433

3534
if (human) {
36-
const use_color = clr.isTty();
37-
const time = c.git_commit_time(commit);
38-
const ts: u64 = @intCast(time);
39-
const epoch = std.time.epoch.EpochSeconds{ .secs = ts };
40-
const day = epoch.getEpochDay();
41-
const yd = day.calculateYearDay();
42-
const md = yd.calculateMonthDay();
35+
const use_color = color.isTty();
36+
const d = fmt.epochToDate(c.git_commit_time(commit));
4337
if (use_color) {
4438
try w.print("{s}{s}{s} {s}{d}-{d:0>2}-{d:0>2}{s} {s}\n", .{
45-
clr.yellow,
46-
short_hash,
47-
clr.reset,
48-
clr.dim,
49-
yd.year,
50-
@intFromEnum(md.month),
51-
md.day_index + 1,
52-
clr.reset,
39+
color.yellow, short_hash, color.reset,
40+
color.dim, d.year, d.month, d.day, color.reset,
5341
summary,
5442
});
5543
} else {
56-
try w.print("{s} {d}-{d:0>2}-{d:0>2} {s}\n", .{
57-
short_hash,
58-
yd.year,
59-
@intFromEnum(md.month),
60-
md.day_index + 1,
61-
summary,
62-
});
44+
try w.print("{s} {d}-{d:0>2}-{d:0>2} {s}\n", .{ short_hash, d.year, d.month, d.day, summary });
6345
}
6446
} else {
6547
try w.print("{s} {s}\n", .{ short_hash, summary });

0 commit comments

Comments
 (0)