package git import ( "os" "path/filepath" "strings" "testing" "g.fc.run/misc/byre/internal/api" ) func TestChanges(t *testing.T) { dir := t.TempDir() git := func(args ...string) string { t.Helper() out, err := run(dir, args...) if err != nil { t.Fatal(err) } return out } write := func(name, content string) { t.Helper() err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644) if err != nil { t.Fatal(err) } } git("init", "--quiet", "--initial-branch=main") git("config", "user.email", "test@example.com") git("config", "user.name", "test") write(".gitignore", "build/\n") write("keep.txt", "one\ntwo\nthree\n") write("gone.txt", "bye\n") write("old.txt", "a\nb\nc\nd\ne\nf\ng\nh\n") git("add", ".") git("commit", "--quiet", "-m", "base") git("switch", "--quiet", "-c", "task") write("committed.txt", "c\n") git("add", "committed.txt") git("commit", "--quiet", "-m", "task work") write("keep.txt", "one\nTWO\nthree\nfour\n") write("untracked.txt", "u\n") write("blob.bin", "\x00\x01\x02") os.MkdirAll(filepath.Join(dir, "build"), 0o755) write("build/out.txt", "ignored\n") os.Remove(filepath.Join(dir, "gone.txt")) git("mv", "old.txt", "new.txt") indexBefore, err := os.ReadFile(filepath.Join(dir, ".git", "index")) if err != nil { t.Fatal(err) } changes, err := Changes(dir, "main", "") if err != nil { t.Fatal(err) } indexAfter, err := os.ReadFile(filepath.Join(dir, ".git", "index")) if err != nil { t.Fatal(err) } if string(indexBefore) != string(indexAfter) { t.Fatalf("Changes modified the worktree's index") } got := map[string]api.FileChange{} for _, f := range changes.Files { got[f.Path] = f } want := map[string]api.FileChange{ "committed.txt": {Status: api.Added, Additions: 1}, "keep.txt": {Status: api.Modified, Additions: 2, Deletions: 1}, "untracked.txt": {Status: api.Added, Additions: 1}, "blob.bin": {Status: api.Added, Binary: true}, "gone.txt": {Status: api.Deleted, Deletions: 1}, "new.txt": {Status: api.Renamed}, } if len(got) != len(want) { t.Fatalf("got %d files %+v, want %d", len(got), changes.Files, len(want)) } for path, w := range want { g, ok := got[path] if !ok { t.Fatalf("missing %s in %+v", path, changes.Files) } if g.Status != w.Status || g.Additions != w.Additions || g.Deletions != w.Deletions || g.Binary != w.Binary { t.Fatalf("%s: got %+v, want %+v", path, g, w) } } if got["new.txt"].OldPath == nil || *got["new.txt"].OldPath != "old.txt" { t.Fatalf("rename lost its old path: %+v", got["new.txt"]) } head, err := Head(dir) if err != nil { t.Fatal(err) } sinceHead, err := Changes(dir, "main", head) if err != nil { t.Fatal(err) } for _, f := range sinceHead.Files { if f.Path == "committed.txt" { t.Fatalf("changes since HEAD include the already committed file: %+v", sinceHead.Files) } } if len(sinceHead.Files) != len(want)-1 { t.Fatalf("changes since HEAD: got %+v, want every change but committed.txt", sinceHead.Files) } patch, err := FileDiff(dir, changes.Base, changes.Tree, "keep.txt", "") if err != nil { t.Fatal(err) } for _, line := range []string{"-two", "+TWO", "+four"} { if !strings.Contains(patch, "\n"+line+"\n") && !strings.HasSuffix(patch, "\n"+line) { t.Fatalf("patch missing %q:\n%s", line, patch) } } sha, err := CommitAll(dir, "turn work", "Turn Author", "turn@example.com") if err != nil { t.Fatal(err) } if sha == "" || sha == head { t.Fatalf("CommitAll made no commit: %q", sha) } if got := git("log", "-1", "--format=%an <%ae>|%cn <%ce>|%s"); got != "Turn Author |Turn Author |turn work" { t.Fatalf("unexpected commit: %s", got) } if status := git("status", "--porcelain", "--untracked-files=all"); status != "" { t.Fatalf("worktree still dirty after CommitAll:\n%s", status) } again, err := CommitAll(dir, "nothing", "Turn Author", "turn@example.com") if err != nil || again != "" { t.Fatalf("CommitAll on a clean tree: %q %v", again, err) } }