diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/CHANGELOG.md | 8 | ||||
| -rw-r--r-- | src/diff.zig | 208 | ||||
| -rw-r--r-- | src/look.zig | 76 |
3 files changed, 223 insertions, 69 deletions
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 881df10c..69b737ca 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +- A look on a diff names a file as the repository does: git's `a/`/`b/` + (or mnemonic `c/`, `i/`, `w/`) is dropped only where both paths of the + `diff --git` line carry one, a `--no-prefix` diff's real `a/` is kept, a + plain `diff -u`'s names are as written, and a missing file is said by + that name (`Look: src/x.zig: no such file here`). + ## 0.0.10 - A 9P write never moves dot's end: scrolling written text into view moves diff --git a/src/diff.zig b/src/diff.zig index 4962cddc..e2b4afd4 100644 --- a/src/diff.zig +++ b/src/diff.zig @@ -39,13 +39,17 @@ pub const Line = struct { }; pub const Walk = struct { - /// The section's paths, as written, borrowed from the lines walked: - /// `a/` and `b/` kept, timestamps cut. + /// The section's paths, borrowed from the lines walked: timestamps + /// cut, and in a git section git's own side prefix (`strip`) dropped, + /// so they are the repository's names. old_path: []const u8 = "", new_path: []const u8 = "", - /// `diff --git a/x b/y`'s b side, for a section with no `+++` yet + /// `diff --git a/x b/y`'s new side, for a section with no `+++` yet /// (a mode change, a rename, a binary file). git_path: []const u8 = "", + /// The section's `diff --git` line has git's side prefixes on both its + /// paths (gitHeader), so its `---`/`+++` paths have them too. + strip: bool = false, old_left: usize = 0, new_left: usize = 0, old_at: usize = 0, @@ -75,6 +79,13 @@ pub const Walk = struct { return isDevNull(w.new_path); } + /// A `---`/`+++` path as the repository names it: git's side prefix + /// dropped in a section whose `diff --git` line has them. + pub fn side(w: *const Walk, path_raw: []const u8) []const u8 { + if (w.strip and !isDevNull(path_raw)) return prefixed(path_raw) orelse path_raw; + return path_raw; + } + pub fn step(w: *Walk, line_raw: []const u8) Line { const line = std.mem.trimEnd(u8, line_raw, "\r"); const got = w.stepInner(line); @@ -120,18 +131,21 @@ pub const Walk = struct { return .{ .kind = .no_newline, .old = w.old_at, .new = w.new_at }; if (std.mem.startsWith(u8, line, "diff ")) { w.* = .{ .continued_rows = w.continued_rows }; - w.git_path = gitPath(line); + if (gitHeader(line)) |h| { + w.git_path = h.new; + w.strip = h.strip; + } return .{ .kind = .meta }; } if (std.mem.startsWith(u8, line, "--- ")) { // A second `---` with no `+++` between starts another section // (plain `diff -u` output has no `diff` line of its own). - w.* = .{ .continued_rows = w.continued_rows, .git_path = w.git_path }; - w.old_path = cutPath(line[4..]); + w.* = .{ .continued_rows = w.continued_rows, .git_path = w.git_path, .strip = w.strip }; + w.old_path = w.side(cutPath(line[4..])); return .{ .kind = .old_path }; } if (std.mem.startsWith(u8, line, "+++ ")) { - w.new_path = cutPath(line[4..]); + w.new_path = w.side(cutPath(line[4..])); return .{ .kind = .new_path }; } if (parseHunk(line)) |h| { @@ -196,13 +210,50 @@ fn isStamp(t: []const u8) bool { 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 { - if (!std.mem.startsWith(u8, line, "diff --git ")) return ""; +/// The one-letter side prefixes git writes: `a/`/`b/`, and with +/// `diff.mnemonicPrefix` `c/` (commit), `i/` (index), `w/` (work tree) and +/// `o/` (object). +const side_letters = "abciow"; + +/// `path` past a side prefix, or null when it has none. +fn prefixed(path: []const u8) ?[]const u8 { + if (path.len > 2 and path[1] == '/' and std.mem.indexOfScalar(u8, side_letters, path[0]) != null) return path[2..]; + return null; +} + +pub const GitHeader = struct { + /// The new side, its prefix dropped when `strip`. + new: []const u8, + /// Both paths carry a side prefix, and different ones, as git writes + /// them (`a/x b/x`, `c/x w/x`, `i/x w/x`). `--no-prefix` writes none, + /// so `a/x a/x` there is a real directory `a`, kept. + strip: bool, +}; + +/// `diff --git <old> <new>`, split where both sides are prefixed (the +/// names equal, else the last such split), else where the halves are +/// equal (`--no-prefix`), else at the last blank. A name with a blank in +/// it splits wrongly only in a rename, and only on a matching prefix. +pub fn gitHeader(line_raw: []const u8) ?GitHeader { + const line = std.mem.trimEnd(u8, line_raw, "\r "); + if (!std.mem.startsWith(u8, line, "diff --git ")) return null; const rest = line["diff --git ".len..]; - const at = std.mem.lastIndexOf(u8, rest, " b/") orelse return ""; - return std.mem.trimEnd(u8, rest[at + 1 ..], " \r"); + var split: ?usize = null; + var at: usize = 0; + while (std.mem.indexOfScalarPos(u8, rest, at, ' ')) |blank| : (at = blank + 1) { + const old = rest[0..blank]; + const new = rest[blank + 1 ..]; + const o = prefixed(old) orelse continue; + const n = prefixed(new) orelse continue; + if (old[0] == new[0]) continue; + split = blank; + if (std.mem.eql(u8, o, n)) break; + } + if (split) |blank| return .{ .new = rest[blank + 3 ..], .strip = true }; + if (rest.len % 2 == 1 and rest[rest.len / 2] == ' ' and std.mem.eql(u8, rest[0 .. rest.len / 2], rest[rest.len / 2 + 1 ..])) + return .{ .new = rest[rest.len / 2 + 1 ..], .strip = false }; + const blank = std.mem.lastIndexOfScalar(u8, rest, ' ') orelse return .{ .new = rest, .strip = false }; + return .{ .new = rest[blank + 1 ..], .strip = false }; } pub const HunkHeader = struct { old_start: usize, old_count: usize, new_start: usize, new_count: usize }; @@ -276,7 +327,7 @@ pub fn looksLikeDiff(lines: []const []const u8) bool { pub const Target = struct { /// The line's own kind. kind: Kind, - /// As written in the diff, `a/`/`b/` kept. + /// The repository's name in a git section (Walk.side), else as written. path: []const u8, /// From 1; 0 for the file alone (a `---`/`+++` line). line: usize, @@ -284,6 +335,19 @@ pub const Target = struct { old_side: bool = false, }; +/// Whether the section `row` is in deletes its file by a line still ahead +/// of it (`deleted file mode`, `+++ /dev/null`): its `diff --git` and `---` +/// lines come first, and are about a file this diff deletes all the same. +fn deletesAhead(lines: []const []const u8, row: usize) bool { + for (lines[row + 1 ..]) |raw| { + const l = std.mem.trimEnd(u8, raw, "\r"); + if (std.mem.startsWith(u8, l, "diff ") or std.mem.startsWith(u8, l, "@@")) return false; + if (std.mem.startsWith(u8, l, "deleted file mode") or std.mem.eql(u8, l, "+++ /dev/null") or + (std.mem.startsWith(u8, l, "+++ /dev/null") and l.len > 13 and (l[13] == '\t' or l[13] == ' '))) return true; + } + return false; +} + /// What a look at `lines[row]` opens, or null for a line that is no part /// of a file section (a commit message, `index ...`). The whole line is /// the look's: a click on a `+++`, `---` or `@@` marker is one on its path @@ -299,19 +363,6 @@ pub const Target = struct { /// from the end of the file. In a section that deletes its file there is /// no new file, so a removed or context line opens the old one at its old /// line (it is there while the diff is not applied). -/// Whether the section `row` is in deletes its file by a line still ahead -/// of it (`deleted file mode`, `+++ /dev/null`): its `diff --git` and `---` -/// lines come first, and are about a file this diff deletes all the same. -fn deletesAhead(lines: []const []const u8, row: usize) bool { - for (lines[row + 1 ..]) |raw| { - const l = std.mem.trimEnd(u8, raw, "\r"); - if (std.mem.startsWith(u8, l, "diff ") or std.mem.startsWith(u8, l, "@@")) return false; - if (std.mem.startsWith(u8, l, "deleted file mode") or std.mem.eql(u8, l, "+++ /dev/null") or - (std.mem.startsWith(u8, l, "+++ /dev/null") and l.len > 13 and (l[13] == '\t' or l[13] == ' '))) return true; - } - return false; -} - pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Target { if (row >= lines.len) return null; var w: Walk = .{ .continued_rows = continued_rows }; @@ -327,14 +378,14 @@ pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Ta return null; }, .old_path, .new_path => { - const own = cutPath(line[4..]); + const own = w.side(cutPath(line[4..])); // `---` names the old file, but the new one on the `+++` under // it wins when they differ: a new file (`--- /dev/null`), a // rename, `diff -u old/x new/x`. The old name is the one // likely gone. if (got.kind == .old_path and row + 1 < lines.len and std.mem.startsWith(u8, lines[row + 1], "+++ ")) { - const new = cutPath(std.mem.trimEnd(u8, lines[row + 1], "\r")[4..]); - if (!isDevNull(new) and (isDevNull(own) or !std.mem.eql(u8, stripSide(new), stripSide(own)))) + const new = w.side(cutPath(std.mem.trimEnd(u8, lines[row + 1], "\r")[4..])); + if (!isDevNull(new) and (isDevNull(own) or !std.mem.eql(u8, new, own))) return .{ .kind = got.kind, .path = new, .line = 0 }; } if (!isDevNull(own)) return .{ .kind = got.kind, .path = own, .line = 0, .old_side = got.kind == .old_path and (w.deleted() or deletesAhead(lines, row)) }; @@ -358,12 +409,6 @@ pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Ta } } -/// `a/x` and `b/x` as git writes them: the name in the repository. -pub fn stripSide(path: []const u8) []const u8 { - if (path.len > 2 and (path[0] == 'a' or path[0] == 'b') and path[1] == '/') return path[2..]; - return path; -} - // ---- tests ---- const testing = std.testing; @@ -408,7 +453,7 @@ test "diff every line of a section that deletes its file is about a file this di for (lines, 0..) |line, row| { if (row < 15) continue; // the deleting section: `diff --git a/old.py` on const t = targetAt(&lines, row, false) orelse continue; - try testing.expectEqualStrings("old.py", stripSide(t.path)); + try testing.expectEqualStrings("old.py", t.path); try testing.expect(t.old_side); _ = line; rows += 1; @@ -440,25 +485,25 @@ test "diff a look on each kind of line names the new file and its line" { try testing.expectEqual(line, t.line); } }; - try expect.at(&lines, 0, "b/src/a.zig", 0); // diff --git + try expect.at(&lines, 0, "src/a.zig", 0); // diff --git try testing.expect(targetAt(&lines, 1, false) == null); // index - try expect.at(&lines, 2, "a/src/a.zig", 0); // --- - try expect.at(&lines, 3, "b/src/a.zig", 0); // +++ - try expect.at(&lines, 4, "b/src/a.zig", 10); // @@ - try expect.at(&lines, 5, "b/src/a.zig", 10); // context - try expect.at(&lines, 6, "b/src/a.zig", 11); // removed: the line now there - try expect.at(&lines, 7, "b/src/a.zig", 11); // added - try expect.at(&lines, 8, "b/src/a.zig", 12); - try expect.at(&lines, 9, "b/src/a.zig", 13); + try expect.at(&lines, 2, "src/a.zig", 0); // --- + try expect.at(&lines, 3, "src/a.zig", 0); // +++ + try expect.at(&lines, 4, "src/a.zig", 10); // @@ + try expect.at(&lines, 5, "src/a.zig", 10); // context + try expect.at(&lines, 6, "src/a.zig", 11); // removed: the line now there + try expect.at(&lines, 7, "src/a.zig", 11); // added + try expect.at(&lines, 8, "src/a.zig", 12); + try expect.at(&lines, 9, "src/a.zig", 13); // `--- x` is a removed line, gone from past the hunk's new lines. - try expect.at(&lines, 10, "b/src/a.zig", 13); - try expect.at(&lines, 11, "b/src/a.zig", 41); - try expect.at(&lines, 12, "b/src/a.zig", 41); - try expect.at(&lines, 13, "b/src/a.zig", 42); - try expect.at(&lines, 14, "b/src/a.zig", 42); + try expect.at(&lines, 10, "src/a.zig", 13); + try expect.at(&lines, 11, "src/a.zig", 41); + try expect.at(&lines, 12, "src/a.zig", 41); + try expect.at(&lines, 13, "src/a.zig", 42); + try expect.at(&lines, 14, "src/a.zig", 42); // A removed line of a deleted file: its old line in the old file. const gone = targetAt(&lines, 21, false).?; - try testing.expectEqualStrings("a/old.py", gone.path); + try testing.expectEqualStrings("old.py", gone.path); try testing.expectEqual(@as(usize, 2), gone.line); try testing.expect(gone.old_side); try testing.expect(targetAt(&lines, 1, false) == null); @@ -490,8 +535,6 @@ test "diff plain diff -u output: no a/ b/, timestamps after the names" { try testing.expectEqual(@as(usize, 4), t.line); // `--- old/x.zig` with `+++ new/x.zig` under it: the new file. try testing.expectEqualStrings("new/x.zig", targetAt(&lines, 0, false).?.path); - try testing.expectEqualStrings("new/x.zig", stripSide("new/x.zig")); - 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" { @@ -523,9 +566,9 @@ test "diff a --- line opens the new name when the +++ under it names another" { \\+b \\ ); - try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 0, false).?.path); - try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 4, false).?.path); - try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 5, false).?.path); + try testing.expectEqualStrings("new name.zig", targetAt(&lines, 0, false).?.path); + try testing.expectEqualStrings("new name.zig", targetAt(&lines, 4, false).?.path); + try testing.expectEqualStrings("new name.zig", targetAt(&lines, 5, false).?.path); try testing.expect(targetAt(&lines, 2, false) == null); } @@ -569,3 +612,54 @@ test "diff a terminal's wrapped row stays in its line" { for (lines[0..5]) |line| last = w.step(line).kind; try testing.expectEqual(Kind.added, last); } + +test "diff git's side prefixes: dropped when both sides carry different ones, else kept" { + const cases = [_]struct { header: []const u8, new: []const u8, strip: bool }{ + .{ .header = "diff --git a/src/x.zig b/src/x.zig", .new = "src/x.zig", .strip = true }, + // diff.mnemonicPrefix: commit, index, work tree. + .{ .header = "diff --git c/src/x.zig w/src/x.zig", .new = "src/x.zig", .strip = true }, + .{ .header = "diff --git i/src/x.zig w/src/x.zig", .new = "src/x.zig", .strip = true }, + .{ .header = "diff --git a/old name.zig b/new name.zig", .new = "new name.zig", .strip = true }, + // --no-prefix: no prefix at all, and a real top-level `a/` kept. + .{ .header = "diff --git src/x.zig src/x.zig", .new = "src/x.zig", .strip = false }, + .{ .header = "diff --git a/x.zig a/x.zig", .new = "a/x.zig", .strip = false }, + .{ .header = "diff --git a/b x a/b x", .new = "a/b x", .strip = false }, + }; + for (cases) |c| { + const h = gitHeader(c.header).?; + try testing.expectEqualStrings(c.new, h.new); + try testing.expectEqual(c.strip, h.strip); + } + try testing.expect(gitHeader("diff -u old new") == null); +} + +fn pathsOf(comptime text: []const u8) ![3][]const u8 { + const lines = splitLines(text); + var w: Walk = .{}; + for (lines) |line| _ = w.step(line); + const t = targetAt(&lines, 3, false) orelse return error.NoTarget; + return .{ w.old_path, w.new_path, t.path }; +} + +test "diff a section's paths are the repository's names under every prefix git writes, and as written elsewhere" { + const expect = struct { + fn paths(got: [3][]const u8, old: []const u8, new: []const u8) !void { + try testing.expectEqualStrings(old, got[0]); + try testing.expectEqualStrings(new, got[1]); + try testing.expectEqualStrings(new, got[2]); + } + }; + try expect.paths(try pathsOf("diff --git a/src/x.zig b/src/x.zig\n--- a/src/x.zig\n+++ b/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig"); + try expect.paths(try pathsOf("diff --git c/src/x.zig w/src/x.zig\n--- c/src/x.zig\n+++ w/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig"); + try expect.paths(try pathsOf("diff --git i/src/x.zig w/src/x.zig\n--- i/src/x.zig\n+++ w/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig"); + // --no-prefix, in a repository with a real directory `a`. + try expect.paths(try pathsOf("diff --git a/x.zig a/x.zig\n--- a/x.zig\n+++ a/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "a/x.zig", "a/x.zig"); + try expect.paths(try pathsOf("diff --git src/x.zig src/x.zig\n--- src/x.zig\n+++ src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig"); + // Plain diff -u never strips, even a name that looks prefixed. + try expect.paths(try pathsOf("--- old/x.zig\t2026-09-30 10:00:00 +0000\n+++ new/x.zig\t2026-09-30 11:00:00 +0000\n@@ -1 +1 @@\n-a\n"), "old/x.zig", "new/x.zig"); + try expect.paths(try pathsOf("--- a/x.zig\n+++ b/x.zig\n@@ -1 +1 @@\n-a\n"), "a/x.zig", "b/x.zig"); + // /dev/null stays itself. + const made = try pathsOf("diff --git a/n.zig b/n.zig\n--- /dev/null\n+++ b/n.zig\n@@ -0,0 +1 @@\n+a\n"); + try testing.expectEqualStrings("/dev/null", made[0]); + try testing.expectEqualStrings("n.zig", made[1]); +} diff --git a/src/look.zig b/src/look.zig index e22f5705..afb5b760 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1527,9 +1527,11 @@ const DiffLook = union(enum) { none, done, at: []const u8 }; /// line names (diff.targetAt): the new file at a `@@` line's first new /// line, at a context or added line's own line, and at a removed line's /// place, the new line now standing where it was. `---`/`+++` open their -/// file. The path is the repository's (walking up from the diff's -/// directory to a `.git` or `.jj`), `a/` and `b/` dropped, else the -/// directory's own. A file that is not there opens nothing and says so. +/// file. The path is the repository's name (diff.Walk.side: git's side +/// prefixes dropped by git's rules, a plain diff's names as written), +/// found from the repository root (walking up from the diff's directory to +/// a `.git` or `.jj`), else the directory. A file that is not there opens +/// nothing and says so, by that name. /// /// Which line: the one a click just gave (`clicked`: exec.pointerOperand /// expanded to it and kept its row), else, for a look written over 9P, a @@ -1573,8 +1575,7 @@ fn diffLook(p: *Pardes, id: usize, pane: *Pane, operand: []const u8, clicked: bo } /// The file a diff names, from `base` (the diff's directory): tried in the -/// repository `base` is in, then in `base`, as git writes it (`b/x`, the -/// repository's `x`) and as written. +/// repository `base` is in, then in `base`. fn diffFile(base: []const u8, path: []const u8, out: *[4096]u8) ?[]const u8 { if (comptime !platform_has_fs) return null; if (path.len == 0) return null; @@ -1593,11 +1594,9 @@ fn diffFile(base: []const u8, path: []const u8, out: *[4096]u8) ?[]const u8 { } else null; for ([_]?[]const u8{ root, base }) |maybe| { const at = maybe orelse continue; - for ([_][]const u8{ diff.stripSide(path), path }) |name| { - var joined: [4096]u8 = undefined; - const full = std.fmt.bufPrint(&joined, "{s}/{s}", .{ at, name }) catch continue; - if (fs.resolveOs(full, out)) |found| if (!found.dir) return found.path; - } + var joined: [4096]u8 = undefined; + const full = std.fmt.bufPrint(&joined, "{s}/{s}", .{ at, path }) catch continue; + if (fs.resolveOs(full, out)) |found| if (!found.dir) return found.path; } return null; } @@ -2175,10 +2174,10 @@ test "Look on a diff line of a file that is not here says so and opens nothing" var after: usize = 0; for (p.panes) |slot| after += @intFromBool(slot != null); try std.testing.expectEqual(before, after); - try std.testing.expectEqualStrings("Look: b/src/elsewhere.zig: no such file here", pane.msg[0..pane.msg_len]); + try std.testing.expectEqualStrings("Look: src/elsewhere.zig: no such file here", pane.msg[0..pane.msg_len]); DiffFixture.click(p, pane, 11); try std.testing.expect(p.look_missed); - try std.testing.expectEqualStrings("Look: a/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); + try std.testing.expectEqualStrings("Look: old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); } test "Look on plain diff -u output: names as written, timestamps cut, next to the diff" { @@ -2303,3 +2302,56 @@ test "Look outside a diff pane expands and looks as ever" { const operand = exec.pointerOperand(p, pane, pointed.sel); try std.testing.expect(operand.text == null or !std.mem.eql(u8, operand.text.?, "+const a = 1;")); } + +test "Look on a diff under each git prefix opens the repository's file, and a real a/ directory under --no-prefix" { + if (!platform_has_fs) return; + const Case = struct { text: []const u8, file: []const u8 }; + const cases = [_]Case{ + .{ .text = "diff --git a/src/x.zig b/src/x.zig\n--- a/src/x.zig\n+++ b/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" }, + .{ .text = "diff --git c/src/x.zig w/src/x.zig\n--- c/src/x.zig\n+++ w/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" }, + .{ .text = "diff --git i/src/x.zig w/src/x.zig\n--- i/src/x.zig\n+++ w/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" }, + // --no-prefix: `a/x.zig` is the file x.zig in the directory a, and + // a `x.zig` at the top is never taken for it. + .{ .text = "diff --git a/x.zig a/x.zig\n--- a/x.zig\n+++ a/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/a/x.zig" }, + }; + for (cases) |c| { + var fx: DiffFixture = undefined; + try fx.init(true, &.{ .{ "src/x.zig", "uno\ntwo\n" }, .{ "a/x.zig", "uno\ntwo\n" }, .{ "x.zig", "top\n" } }, c.text); + defer fx.tmp.cleanup(); + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + const pane = try fx.open(p, c.text); + DiffFixture.click(p, pane, 6); // `two`, a context line: new line 2 + try std.testing.expect(!p.look_missed); + try expectAt(p, c.file, 2); + DiffFixture.click(p, pane, 1); // `---` + try std.testing.expect(!p.look_missed); + try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, c.file)); + } +} + +test "Look on a diff says a missing or deleted file by its repository name, a plain diff's as written" { + if (!platform_has_fs) return; + const git_text = "diff --git c/src/gone.zig w/src/gone.zig\n--- c/src/gone.zig\n+++ w/src/gone.zig\n@@ -1 +1 @@\n-a\n+b\n" ++ + "diff --git a/lib/old.zig b/lib/old.zig\ndeleted file mode 100644\n--- a/lib/old.zig\n+++ /dev/null\n@@ -1 +0,0 @@\n-a\n"; + const plain_text = "--- a/x.zig\t2026-09-30 10:00:00 +0000\n+++ b/x.zig\t2026-09-30 11:00:00 +0000\n@@ -1 +1 @@\n-a\n+b\n"; + var fx: DiffFixture = undefined; + try fx.init(true, &.{.{ "x.zig", "b\n" }}, git_text); + defer fx.tmp.cleanup(); + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + var pane = try fx.open(p, git_text); + DiffFixture.click(p, pane, 5); + try std.testing.expect(p.look_missed); + try std.testing.expectEqualStrings("Look: src/gone.zig: no such file here", pane.msg[0..pane.msg_len]); + DiffFixture.click(p, pane, 11); + try std.testing.expectEqualStrings("Look: lib/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); + DiffFixture.click(p, pane, 6); // `diff --git` of the deleted file + try std.testing.expectEqualStrings("Look: lib/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); + // A plain diff's `b/x.zig` is a path as written: there is no b/ here, + // and the x.zig at the top is not it. + pane = try fx.open(p, plain_text); + DiffFixture.click(p, pane, 4); + try std.testing.expect(p.look_missed); + try std.testing.expectEqualStrings("Look: b/x.zig: no such file here", pane.msg[0..pane.msg_len]); +} |
