package gitread import ( "context" "os" "os/exec" "path/filepath" "testing" "github.com/barerepo/server/internal/gitx" "strings" ) // buildRepo makes a real repository, because only git's actual output exercises the parser. func buildRepo(t *testing.T) string { t.Helper() if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } dir := t.TempDir() run := func(args ...string) { t.Helper() cmd := exec.Command(gitx.Bin, args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=lisa", "GIT_AUTHOR_EMAIL=m@x", "GIT_COMMITTER_NAME=lisa", "GIT_COMMITTER_EMAIL=m@x") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } write := func(name, body string) { t.Helper() if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } run("init", "-q", "-b", "master") write("config.go", "package main\n\nfunc Load() {}\n") run("add", "-A") run("commit", "-qm", "first") write("config.go", "package main\n\nfunc Load() error {\n\treturn nil\n}\n") write("irc/conn.go", "package irc\n") run("add", "-A") run("commit", "-qm", "second commit\n\nwith a body that explains it.") return dir } func TestLog(t *testing.T) { dir := buildRepo(t) commits, err := Log(context.Background(), dir, "HEAD", "", 10) if err != nil { t.Fatal(err) } if len(commits) != 2 { t.Fatalf("got %d commits, want 2", len(commits)) } // The log page draws no diff, so the hunks below come from the commit page's read. shown, err := Show(context.Background(), dir, commits[0].SHA) if err != nil || shown == nil { t.Fatalf("Show(%s) = %v, %v", commits[0].SHA, shown, err) } // Newest first, per chapter 24. c := commits[0] if c.Subject != "second commit" { t.Errorf("Subject = %q", c.Subject) } if c.Body != "with a body that explains it." { t.Errorf("Body = %q", c.Body) } if c.Author != "lisa" { t.Errorf("Author = %q", c.Author) } if c.When.IsZero() { t.Error("When is zero") } if len(c.Short) == 0 || len(c.SHA) != 40 { t.Errorf("SHA = %q, Short = %q", c.SHA, c.Short) } if len(c.Files) != 2 { t.Fatalf("got %d files, want 2: %+v", len(c.Files), c.Files) } // git orders the diff by path, so config.go comes before irc/conn.go. if c.Files[0].Path != "config.go" || c.Files[1].Path != "irc/conn.go" { t.Errorf("paths = %q, %q", c.Files[0].Path, c.Files[1].Path) } if c.Add == 0 || c.Del == 0 { t.Errorf("Add = %d, Del = %d, want both non-zero", c.Add, c.Del) } // The hunk must survive with its header and its line kinds intact. f := shown.Files[0] if len(f.Hunks) == 0 { t.Fatal("config.go has no hunks") } h := f.Hunks[0] if h.Header[:2] != "@@" { t.Errorf("hunk header = %q", h.Header) } var kinds string for _, l := range h.Lines { kinds += string(l.Kind) } for _, want := range []byte{' ', '+', '-'} { if !contains(kinds, want) { t.Errorf("no %q lines in %q", want, kinds) } } // The first commit added a file, so it has no deletions. if commits[1].Del != 0 { t.Errorf("the first commit reports %d deletions", commits[1].Del) } } // A revision that is really an argument must never reach git. func TestLogRefusesFlags(t *testing.T) { dir := buildRepo(t) for _, rev := range []string{"--upload-pack=/bin/sh", "-x", "master; id", "HEAD@{0}"} { got, err := Log(context.Background(), dir, rev, "", 10) if err != nil || got != nil { t.Errorf("Log(%q) = %v, %v; want nil, nil", rev, got, err) } } } func contains(s string, b byte) bool { for i := 0; i < len(s); i++ { if s[i] == b { return true } } return false } func TestOpenAndBlame(t *testing.T) { dir := buildRepo(t) f, err := Open(context.Background(), dir, "HEAD", "config.go") if err != nil || f == nil { t.Fatalf("Open: %v %v", f, err) } if len(f.Lines) != 5 { t.Fatalf("got %d lines, want 5: %+v", len(f.Lines), f.Lines) } if f.Binary || f.TooBig { t.Errorf("Binary = %v, TooBig = %v", f.Binary, f.TooBig) } if f.Lines[0].Text != "package main" { t.Errorf("line 1 = %q", f.Lines[0].Text) } if f.Lines[0].Number != 1 || f.Lines[4].Number != 5 { t.Error("line numbers are wrong") } // Blame in the gutter on every line, always. Chapter 24. for _, l := range f.Lines { if l.Short == "" || l.Author != "lisa" || l.When.IsZero() { t.Errorf("line %d has no blame: %+v", l.Number, l) break } } if f, err := Open(context.Background(), dir, "HEAD", "nope.go"); err != nil || f != nil { t.Errorf("a missing file returned %v, %v", f, err) } for _, bad := range []string{"../../etc/passwd", "-rf", "a/../../b"} { if f, _ := Open(context.Background(), dir, "HEAD", bad); f != nil { t.Errorf("Open(%q) returned a file", bad) } } } func TestOpenDetectsBinary(t *testing.T) { dir := buildRepo(t) if err := os.WriteFile(filepath.Join(dir, "blob.bin"), append([]byte("gif89a"), 0, 1, 2, 3), 0o644); err != nil { t.Fatal(err) } cmd := exec.Command(gitx.Bin, "add", "-A") cmd.Dir = dir cmd.Run() cmd = exec.Command(gitx.Bin, "-c", "user.email=m@x", "-c", "user.name=lisa", "commit", "-qm", "binary") cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("commit: %v\n%s", err, out) } f, err := Open(context.Background(), dir, "HEAD", "blob.bin") if err != nil || f == nil { t.Fatalf("Open: %v %v", f, err) } if !f.Binary { t.Error("a file with a null byte was not detected as binary") } if len(f.Lines) != 0 { t.Error("binary content was rendered into lines") } } func TestCompare(t *testing.T) { dir := buildRepo(t) c, err := Compare(context.Background(), dir, "HEAD~1", "HEAD") if err != nil { t.Fatal(err) } if c.Missing != "" { t.Fatalf("Missing = %q", c.Missing) } if c.Commits != 1 { t.Errorf("Commits = %d, want 1", c.Commits) } if len(c.Files) != 2 { t.Errorf("got %d files, want 2", len(c.Files)) } if c.Add == 0 { t.Error("Add = 0") } if c.Conflict { t.Error("a fast-forward comparison reported conflicts") } // A side that does not exist is named, not swallowed. c, err = Compare(context.Background(), dir, "HEAD", "nosuchref") if err != nil { t.Fatal(err) } if c.Missing != "nosuchref" { t.Errorf("Missing = %q, want nosuchref", c.Missing) } // And a side that is really an argument never reaches git. c, _ = Compare(context.Background(), dir, "HEAD", "--upload-pack=/bin/sh") if c.Missing == "" { t.Error("a flag was accepted as a revision") } } // Chapter 35.3 comments by line number, so it has to survive the parser as the new-side one. func TestLineNumbers(t *testing.T) { dir := buildRepo(t) commits, err := Log(context.Background(), dir, "HEAD", "", 1) if err != nil { t.Fatal(err) } // Line numbers live in hunks, and hunks are what the commit page reads. shown, err := Show(context.Background(), dir, commits[0].SHA) if err != nil || shown == nil { t.Fatalf("Show(%s) = %v, %v", commits[0].SHA, shown, err) } var f FileDiff for _, c := range shown.Files { if c.Path == "config.go" { f = c } } if len(f.Hunks) == 0 { t.Fatal("config.go has no hunks") } seen := map[int]bool{} for _, h := range f.Hunks { start := newStartOf(h.Header) if start <= 0 { t.Fatalf("hunk header %q has no new-side start", h.Header) } want := start for _, l := range h.Lines { switch l.Kind { case '-': if l.New != 0 { t.Errorf("a removed line carries new-side number %d", l.New) } default: if l.New != want { t.Errorf("line %q numbered %d, want %d", l.Text, l.New, want) } if seen[l.New] { t.Errorf("line number %d appears twice", l.New) } seen[l.New] = true want++ } } } } func TestNewStartOf(t *testing.T) { for header, want := range map[string]int{ "@@ -41,7 +41,10 @@ func Load": 41, "@@ -1 +1,2 @@": 1, "@@ -0,0 +1,5 @@": 1, "@@ -88,23 +88,0 @@": 88, "not a header": 0, } { if got := newStartOf(header); got != want { t.Errorf("newStartOf(%q) = %d, want %d", header, got, want) } } } // A readme is not the landing page, but a repository that has one should be able to say so. func TestReadmeFindsTheRootOneAndNothingElse(t *testing.T) { ctx := context.Background() dir := buildRepo(t) head := headOf(t, dir) // buildRepo writes no readme, so this repository has none to find. if got := Readme(ctx, dir, head); got != "" { t.Errorf("found a readme %q in a repository with none", got) } for _, name := range []string{"README.md", "readme.txt", "README"} { dir := buildRepo(t) commitFile(t, dir, name, "hello") if got := Readme(ctx, dir, headOf(t, dir)); got != name { t.Errorf("Readme with %s = %q", name, got) } } // A readme below the root is somebody's documentation folder, not the repository's readme. deep := buildRepo(t) commitFile(t, deep, "docs/README.md", "hello") if got := Readme(ctx, deep, headOf(t, deep)); got != "" { t.Errorf("a nested readme was taken as the repository's: %q", got) } } // The compare fields are free text per chapter 24, so this is what exists to type, not a dropdown. func TestRefsListsBranchesTagsAndProposals(t *testing.T) { dir := buildRepo(t) gitRun(t, dir, "branch", "topic") gitRun(t, dir, "tag", "v0.1.0") gitRun(t, dir, "update-ref", "refs/proposals/47", "HEAD") kinds := map[string]string{} for _, ref := range Refs(context.Background(), dir) { kinds[ref.Name] = ref.Kind } want := map[string]string{ "master": "branch", "topic": "branch", "v0.1.0": "tag", // A proposal keeps its full name, because that is what the compare page asks you to type. "refs/proposals/47": "proposal", } for name, kind := range want { if kinds[name] != kind { t.Errorf("%s came back as %q, want %q", name, kinds[name], kind) } } // Notes are storage and not somewhere to compare against. for name := range kinds { if strings.HasPrefix(name, "refs/notes/") { t.Errorf("a notes ref was offered to compare against: %s", name) } } } func gitRun(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command(gitx.Bin, args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=lisa", "GIT_AUTHOR_EMAIL=m@x", "GIT_COMMITTER_NAME=lisa", "GIT_COMMITTER_EMAIL=m@x") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } func headOf(t *testing.T, dir string) string { t.Helper() out, err := gitx.Run(context.Background(), dir, "rev-parse", "HEAD") if err != nil { t.Fatal(err) } return strings.TrimSpace(out) } func commitFile(t *testing.T, dir, name, body string) { t.Helper() if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } gitRun(t, dir, "add", name) gitRun(t, dir, "commit", "-qm", "add "+name) }