summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/diff.zig47
1 files changed, 46 insertions, 1 deletions
diff --git a/src/diff.zig b/src/diff.zig
index d129c222..9ab1bb23 100644
--- a/src/diff.zig
+++ b/src/diff.zig
@@ -161,12 +161,41 @@ fn isDevNull(path: []const u8) bool {
/// A `---`/`+++` path with what follows it cut: `diff -u`'s tab and
/// timestamp, and git's quotes round a name it had to escape.
fn cutPath(rest: []const u8) []const u8 {
- var name = rest[0 .. std.mem.indexOfScalar(u8, rest, '\t') orelse rest.len];
+ var name = rest[0 .. std.mem.indexOfScalar(u8, rest, '\t') orelse stampAt(rest) orelse rest.len];
name = std.mem.trimEnd(u8, name, " \r");
if (name.len >= 2 and name[0] == '"' and name[name.len - 1] == '"') name = name[1 .. name.len - 1];
return name;
}
+/// Where `diff -u`'s timestamp starts when a terminal turned the tab before
+/// it into blanks (a command pane's output): the start of the blanks that
+/// run up to one. A tab may become a single blank, so one is enough; the
+/// stamp itself is strict, so a name with blanks in it is not cut.
+fn stampAt(rest: []const u8) ?usize {
+ var i: usize = 0;
+ while (std.mem.indexOfScalarPos(u8, rest, i, ' ')) |blank| {
+ var end = blank;
+ while (end < rest.len and rest[end] == ' ') end += 1;
+ if (isStamp(rest[end..])) return blank;
+ i = end;
+ }
+ return null;
+}
+
+/// `2026-09-30 10:00:00...` (GNU diff) or `Wed Sep 30 10:00:00 2026` (ctime).
+fn isStamp(t: []const u8) bool {
+ const d = std.ascii.isDigit;
+ if (t.len >= 19 and d(t[0]) and d(t[1]) and d(t[2]) and d(t[3]) and t[4] == '-' and d(t[5]) and d(t[6]) and t[7] == '-' and
+ d(t[8]) and d(t[9]) and t[10] == ' ' and d(t[11]) and d(t[12]) and t[13] == ':' and d(t[14]) and d(t[15]) and t[16] == ':' and d(t[17]) and d(t[18]))
+ return true;
+ if (t.len < 24 or t[3] != ' ' or t[7] != ' ') return false;
+ for ([_][]const u8{ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }) |day| if (std.mem.eql(u8, t[0..3], day)) {
+ const clock = t[11..19];
+ return (d(t[8]) or t[8] == ' ') and d(t[9]) and t[10] == ' ' and d(clock[0]) and d(clock[1]) and clock[2] == ':' and d(clock[3]) and d(clock[4]) and clock[5] == ':';
+ };
+ return false;
+}
+
/// `diff --git a/x b/y`: the b side. A name with a space in it is split at
/// the last ` b/`, which is right unless the old name has one too.
fn gitPath(line: []const u8) []const u8 {
@@ -437,6 +466,22 @@ test "diff plain diff -u output: no a/ b/, timestamps after the names" {
try testing.expectEqualStrings("x.zig", stripSide("b/x.zig"));
}
+test "diff -u in a command pane, its tabs turned to blanks by the terminal, names its files without the stamps" {
+ // As a terminal shows `diff -u old/x.zig new/x.zig`: each tab gone to
+ // the blanks up to the next tab stop, one of them a single blank.
+ const lines = splitLines("--- old/x.zig 2026-09-30 10:00:00.000000000 +0000\n" ++
+ "+++ new/a b.zig 2026-09-30 11:00:00.000000000 +0000\n" ++
+ "@@ -3 +3,2 @@\n" ++
+ "-const a = 1;\n" ++
+ "+const a = 2;\n" ++
+ "+const b = 3;\n");
+ try testing.expect(looksLikeDiff(&lines));
+ try testing.expectEqualStrings("new/a b.zig", targetAt(&lines, 5, false).?.path);
+ try testing.expectEqualStrings("x.zig", cutPath("x.zig Wed Sep 30 10:00:00 2026"));
+ // A name with blanks and no stamp after them is whole.
+ try testing.expectEqualStrings("my file 2026 notes.txt", cutPath("my file 2026 notes.txt"));
+}
+
test "diff a --- line opens the new name when the +++ under it names another" {
const lines = splitLines(
\\diff --git a/old name.zig b/new name.zig