Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/rexdiff
/.pi/
/dist/
*.test
*.out
12 changes: 6 additions & 6 deletions cmd/rexdiff/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ end

-- runs_rexdiff returns true if the terminal block id runs rexdiff. rexdiff
-- reports app=rexdiff to Rex with OSC 7501. Rex keeps an idle status after
-- its program stops, so the block counts only while the process that
-- its program stops. Thus the block counts only while the process that
-- reported the status is in the foreground.
local function runs_rexdiff(id)
for _, r in ipairs(call("program_status", id).records or {}) do
Expand All @@ -92,8 +92,8 @@ local function reviewers()
end

-- check fails the action if rexdiff does not accept config, the settings
-- as a JSON object. Here the user sees the error. If rexdiff stops at once
-- in a new pane, the pane closes and the error is lost.
-- as a JSON object. The user sees an error of the action. If rexdiff stops
-- at once in a new pane, the pane closes and the user does not see the error.
local function check(config)
local r, err = rex.exec({ BIN, "--check", "--config", config })
if r == nil then error(err, 0) end
Expand Down Expand Up @@ -213,7 +213,7 @@ func splitLua(path, content string) (settings, code string, err error) {
}

// newLua returns the content of rexdiff.lua with the code for bin. It
// keeps the settings of old, the content that the file has now. If old is
// keeps the settings of old, the current content of the file. If old is
// empty, it uses the default settings.
func newLua(path, old, bin string) (string, error) {
if old == "" {
Expand All @@ -236,8 +236,8 @@ func readFile(path string) (string, error) {
}

// executable returns the path of the rexdiff binary, for the Rex action. It
// keeps a symlink, such as the one from Homebrew. The target of the link
// changes at each upgrade.
// keeps a symlink, such as the one from Homebrew, because the target of the
// link changes at each upgrade.
func executable() (string, error) {
bin, err := os.Executable()
if err != nil {
Expand Down
20 changes: 11 additions & 9 deletions cmd/rexdiff/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func fakeRexDir(t *testing.T, log string) string {
}

// TestUpgrade checks that upgrade replaces the code of rexdiff.lua if it
// differs from the code of the binary, and keeps the settings.
// differs from the code of the binary. It keeps the settings.
func TestUpgrade(t *testing.T) {
dir := t.TempDir()
reloads := filepath.Join(dir, "reloads")
Expand Down Expand Up @@ -51,7 +51,7 @@ func TestUpgrade(t *testing.T) {
t.Fatalf("upgrade without rexdiff.lua = %q, want no notice", n)
}
if _, err := os.Stat(lua); err == nil {
t.Fatal("upgrade made a rexdiff.lua")
t.Fatal("upgrade without rexdiff.lua: the file exists, want no file")
}

// If the file has old code for the same binary, upgrade replaces the
Expand All @@ -62,7 +62,8 @@ func TestUpgrade(t *testing.T) {
t.Fatalf("upgrade = %q, want no notice", n)
}
if got := read(); got != settings+luaCode(bin) {
t.Errorf("rexdiff.lua after upgrade:\n%s\nwant the settings and the new code", got)
t.Errorf("rexdiff.lua after upgrade got:\n%s\nwant:\n%s",
got, settings+luaCode(bin))
}
if b, _ := os.ReadFile(reloads); string(b) != "config reload\n" {
t.Errorf("rex calls = %q, want one config reload", b)
Expand All @@ -89,23 +90,24 @@ func TestUpgrade(t *testing.T) {
// tells what is wrong and what to do.
noMark := strings.Replace(settings+old, codeMark+"\n", "", 1)
if !strings.Contains(noMark, luaQuote(bin)) {
t.Fatal("the test file does not run bin")
t.Fatalf("the test file has no %s, want it", luaQuote(bin))
}
write(noMark)
n := upgrade(ctx, bin)
for _, want := range []string{"cannot upgrade rexdiff.lua", lua, codeMark,
"cannot tell your settings from its code", "Put that line back"} {
if !strings.Contains(n, want) {
t.Errorf("notice %q does not have %q", n, want)
t.Errorf("notice = %q, want %q in it", n, want)
}
}
if read() != noMark {
t.Error("upgrade changed rexdiff.lua without the line above the code")
if got := read(); got != noMark {
t.Errorf("rexdiff.lua without the line above the code, after upgrade "+
"got:\n%s\nwant:\n%s", got, noMark)
}
}

// TestLuaQuote checks that a path that Go would escape stays the same in
// Lua 5.1, which knows only a few escapes.
// TestLuaQuote checks that a path that Go escapes stays the same in Lua 5.1.
// Lua 5.1 knows only a few escapes.
func TestLuaQuote(t *testing.T) {
got := luaQuote("/a\u200bb/\"x\"\\y\nz")
want := "\"/a\u200bb/\\\"x\\\"\\\\y\\nz\""
Expand Down
15 changes: 9 additions & 6 deletions cmd/rexdiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
}
defer closeLog()

// In Rex, upgrade replaces the code of the action if it differs from
// the code of this rexdiff.
// In Rex, upgrade replaces the code of the action if that code differs
// from the code of this rexdiff.
client := rex.FromEnv()
var notice string
bin, binErr := executable()
Expand Down Expand Up @@ -121,7 +121,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
if err = os.Setenv(resumeEnv, resume); err == nil {
err = update.Restart(bin, args) // returns only if it fails
}
fmt.Print("\x1b[?1049l\x1b[?25h") // leave the screen that Run kept, show the cursor
// Run keeps the alternate screen for the restart. Leave that screen and
// show the cursor.
fmt.Print("\x1b[?1049l\x1b[?25h")
return fmt.Errorf("start the new rexdiff: %w; your comments are saved in %s", err, resume)
}

Expand All @@ -130,12 +132,13 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
// flag, so that the arguments of the user do not change.
const resumeEnv = "REXDIFF_RESUME"

// resumeFile returns the file in resumeEnv, or "", and unsets the variable,
// resumeFile returns the file in resumeEnv, or "". It unsets the variable,
// so that the programs that rexdiff starts, such as an editor, do not get
// it.
// the variable.
func resumeFile() string {
path := os.Getenv(resumeEnv)
_ = os.Unsetenv(resumeEnv) // fails only for a bad name, and resumeEnv is good
// Unsetenv fails only for a bad name. resumeEnv is a good name.
_ = os.Unsetenv(resumeEnv)
return path
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/rexdiff/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func TestMain(m *testing.M) {
})
}

// fakeRex writes its arguments to $WORK/rex.log, so that a script can see
// what rexdiff asked Rex to do. It fails if $REX_FAIL is set.
// fakeRex writes its arguments to $WORK/rex.log. A script reads the log to
// see what rexdiff asks Rex to do. fakeRex fails if $REX_FAIL is set.
func fakeRex() {
log := filepath.Join(os.Getenv("WORK"), "rex.log")
f, err := os.OpenFile(log, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
Expand Down
20 changes: 20 additions & 0 deletions diff/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package diff

import (
"strings"
"testing"
)

// TestCacheKeepsFile checks that RenderFile uses the colours that the cache
// keeps for the same file. RenderFile colours a changed file again.
func TestCacheKeepsFile(t *testing.T) {
var c Cache
RenderFile("a.go", "x := 1\n", Options{Cache: &c})
c.file.out = "kept"
if got := RenderFile("a.go", "x := 1\n", Options{Cache: &c}).Lines[0]; !strings.Contains(got, "kept") {
t.Errorf("the same file shows %q, want the kept colours", got)
}
if got := RenderFile("a.go", "x := 2\n", Options{Cache: &c}).Lines[0]; strings.Contains(got, "kept") {
t.Errorf("a changed file shows %q, want new colours", got)
}
}
16 changes: 9 additions & 7 deletions diff/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"fmt"
"slices"
"strings"

"github.com/rednafi/rexdiff/internal/termtext"
)

// Comment is a review note on a range of lines on one side of a file. Use
// [NewComment] to make one. A comment keeps the text of its lines, for
// [Comments.Relocate], and its diff rows, for [Comments.Review].
type Comment struct {
ID int // set by [Comments.Add]
ID int // [Comments.Add] sets it
Path string
Side Side
// Start and End are the first and the last line of the range. They
Expand Down Expand Up @@ -55,10 +57,10 @@ func NewComment(path string, s Side, rows []Row, old, new string) Comment {
}

// Location returns the path and the lines of c, for example "a.go:3-4".
// A comment on the old side has " (removed)" at the end. The path is
// quoted with [Quote].
// A comment on the old side has " (removed)" at the end. [termtext.Quote]
// quotes the path.
func (c *Comment) Location() string {
loc := fmt.Sprintf("%s:%d", Quote(c.Path), c.Start)
loc := fmt.Sprintf("%s:%d", termtext.Quote(c.Path), c.Start)
if c.End != c.Start {
loc += fmt.Sprintf("-%d", c.End)
}
Expand All @@ -78,7 +80,7 @@ type Comments struct {
// Len returns the number of comments.
func (cs *Comments) Len() int { return len(cs.items) }

// Add adds a copy of c with a new ID, and returns the copy.
// Add adds c with a new ID, and returns the comment in the set.
func (cs *Comments) Add(c Comment) *Comment {
cs.last++
c.ID = cs.last
Expand Down Expand Up @@ -149,8 +151,8 @@ func (cs *Comments) Count(path string) int {
// one. If the version does not have the text, the comment stays and
// becomes stale.
func (cs *Comments) Relocate(path, old, new string) {
// The lines of each side. Relocate splits a side only if a comment
// needs it.
// lines has the lines of each side. Relocate splits a side only if a
// comment needs that side.
var lines [2][]string
for _, c := range cs.items {
if c.Path != path || len(c.code) == 0 {
Expand Down
46 changes: 26 additions & 20 deletions diff/comment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ func TestReviewMatchesHerdr(t *testing.T) {
"c.go:1-2 (stale: the code has changed since)\n" +
" ctx\n-\told[31m\n+\tnew\nfix]7501;state=idle this"
if got != want {
t.Errorf("Review:\n%s\nwant:\n%s", got, want)
t.Errorf("Review got:\n%s\nwant:\n%s", got, want)
}
if got := new(diff.Comments).Review(""); got != "" {
t.Errorf("Review of nothing = %q", got)
t.Errorf("Review with no comments = %q, want \"\"", got)
}
}

Expand All @@ -80,7 +80,8 @@ func TestReviewContinuedRows(t *testing.T) {
// The comment has no diff lines, so Review writes the lines of its range.
want := "a.go:1\n+" + strings.TrimSuffix(new, "\n") + "\nlong"
if got := review(c); len(cont) == 0 || got != want {
t.Errorf("%d continued rows, Review:\n%s\nwant:\n%s", len(cont), got, want)
t.Errorf("%d continued rows, Review got:\n%s\nwant:\n%s",
len(cont), got, want)
}
}

Expand All @@ -91,27 +92,30 @@ func TestComments(t *testing.T) {
b := cs.Add(diff.Comment{Path: "a.go", Side: diff.SideOld, Start: 4, End: 4})
cs.Add(diff.Comment{Path: "b.go", Side: diff.SideNew, Start: 1, End: 1})
if cs.Len() != 3 || cs.Count("a.go") != 2 || a.ID == b.ID {
t.Fatalf("Len = %d, Count(a.go) = %d, IDs %d %d",
t.Fatalf("Len = %d, Count(a.go) = %d, IDs %d %d; "+
"want 3, 2, two different IDs",
cs.Len(), cs.Count("a.go"), a.ID, b.ID)
}
if c := cs.At("a.go", diff.SideNew, 3); c != a {
t.Errorf("At(a.go, New, 3) = %v", c)
t.Errorf("At(a.go, New, 3) = %v, want %v", c, a)
}
if c := cs.At("a.go", diff.SideNew, 5); c != nil {
t.Errorf("At(a.go, New, 5) = %v, want nil", c)
}
if got := cs.EndingAt("a.go", diff.SideOld, 4); len(got) != 1 || got[0] != b {
t.Errorf("EndingAt(a.go, Old, 4) = %v", got)
t.Errorf("EndingAt(a.go, Old, 4) = %v, want [%v]", got, b)
}
if cs.Find(b.ID) != b {
t.Error("Find did not find b")
t.Errorf("Find(%d) = %v, want %v", b.ID, cs.Find(b.ID), b)
}
cs.Remove(b.ID)
if cs.Find(b.ID) != nil || cs.Len() != 2 {
t.Error("Remove did not remove b")
t.Errorf("after Remove(%d): Find = %v, Len = %d; want nil, 2",
b.ID, cs.Find(b.ID), cs.Len())
}
if c := cs.Add(diff.Comment{}); c.ID == a.ID || c.ID == b.ID {
t.Error("Add reused an ID")
t.Errorf("Add gives ID %d, want an ID other than %d and %d",
c.ID, a.ID, b.ID)
}
}

Expand All @@ -136,7 +140,7 @@ func TestNewComment(t *testing.T) {
}
want += "\n a\n-b\n-c\n+B\n+C\n d\n"
if got := review(c); got != want {
t.Errorf("split %v, side %d: Review:\n%s\nwant:\n%s",
t.Errorf("split %v, side %d: Review got:\n%s\nwant:\n%s",
split, s, got, want)
}
// The comment follows the lines of its side.
Expand All @@ -161,7 +165,8 @@ func TestNewComment(t *testing.T) {
t.Errorf("split %v, the new lines: %+v, want lines 2-3", split, got)
}
if cs.Relocate("x.txt", "", "B\nC\n"); got.Start != 1 || got.Stale {
t.Errorf("split %v, the new lines did not follow B and C: %+v", split, got)
t.Errorf("split %v, the new lines after a move: %+v, want line 1",
split, got)
}
}
// A hunk header has no line.
Expand All @@ -183,26 +188,27 @@ func TestRelocateFollowsMovedCode(t *testing.T) {
// Each comment looks in the version of its side.
cs.Relocate("a.go", "x\nx\nx\nb\n", "new\nnew\na\nb\nc\n")
if c.Start != 4 || c.End != 5 || c.Stale {
t.Errorf("moved: %+v", c)
t.Errorf("after a move: %+v, want lines 4-5", c)
}
if other.Start != 4 || other.Stale {
t.Errorf("the comment on the old side did not follow the old version: %+v", other)
t.Errorf("the comment on the old side after a move: %+v, want line 4",
other)
}
cs.Relocate("a.go", "b\n", "a\nB\nc\n")
if !c.Stale || c.Start != 4 {
t.Errorf("not stale: %+v", c)
t.Errorf("after an edit: %+v, want stale at line 4", c)
}
cs.Relocate("a.go", "b\n", "b\nc\nx\nx\nx\nb\nc\n")
if c.Stale || c.Start != 6 {
t.Errorf("did not go to the nearest copy: %+v", c)
t.Errorf("with two copies: %+v, want line 6", c)
}
// The end of a file is not an empty line, and CRLF is the same as LF.
// The end of a file is not an empty line. CRLF is the same as LF.
blank := cs.Add(commentOn("b.go", diff.SideNew, "x\n\n", 2, 2))
if cs.Relocate("b.go", "", "x\n"); !blank.Stale {
t.Errorf("a comment on a deleted blank line is not stale: %+v", blank)
t.Errorf("a comment on a deleted blank line: %+v, want stale", blank)
}
if cs.Relocate("b.go", "", "x\r\n\r\n"); blank.Stale || blank.Start != 2 {
t.Errorf("a comment on a CRLF blank line: %+v", blank)
t.Errorf("a comment on a CRLF blank line: %+v, want line 2", blank)
}
}

Expand All @@ -216,7 +222,7 @@ func TestSnippetIsTheRealDiff(t *testing.T) {
d := diff.Render("x.go", old, new, diff.Options{Split: split})
got := strings.Join(snippet(d.Rows, old, new), "\n")
if got != want {
t.Errorf("split %v:\n%s\nwant:\n%s", split, got, want)
t.Errorf("split %v: snippet got:\n%s\nwant:\n%s", split, got, want)
}
}

Expand All @@ -235,6 +241,6 @@ func TestSnippetIsTheRealDiff(t *testing.T) {
out, _ := git.Output()
_, body, _ := strings.Cut(string(out), "@@\n")
if strings.TrimSuffix(body, "\n") != want {
t.Errorf("git diff:\n%s\nwant:\n%s", body, want)
t.Errorf("git diff got:\n%s\nwant:\n%s", body, want)
}
}
Loading
Loading