github.com/golang/review@v0.0.0-20190122205339-266ee1edf5c3/git-codereview/hook_test.go (about)

     1  // Copyright 2014 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"testing"
    16  )
    17  
    18  func TestHookCommitMsgGerrit(t *testing.T) {
    19  	gt := newGitTest(t)
    20  	gt.enableGerrit(t)
    21  	defer gt.done()
    22  
    23  	// Check that hook adds Change-Id.
    24  	write(t, gt.client+"/msg.txt", "Test message.\n")
    25  	testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
    26  	data := read(t, gt.client+"/msg.txt")
    27  	if !bytes.Contains(data, []byte("\n\nChange-Id: ")) {
    28  		t.Fatalf("after hook-invoke commit-msg, missing Change-Id:\n%s", data)
    29  	}
    30  
    31  	// Check that hook is no-op when Change-Id is already present.
    32  	testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
    33  	data1 := read(t, gt.client+"/msg.txt")
    34  	if !bytes.Equal(data, data1) {
    35  		t.Fatalf("second hook-invoke commit-msg changed Change-Id:\nbefore:\n%s\n\nafter:\n%s", data, data1)
    36  	}
    37  
    38  	// Check that hook rejects multiple Change-Ids.
    39  	write(t, gt.client+"/msgdouble.txt", string(data)+string(data))
    40  	testMainDied(t, "hook-invoke", "commit-msg", gt.client+"/msgdouble.txt")
    41  	const multiple = "git-codereview: multiple Change-Id lines\n"
    42  	if got := testStderr.String(); got != multiple {
    43  		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, multiple)
    44  	}
    45  }
    46  
    47  func TestHookCommitMsg(t *testing.T) {
    48  	gt := newGitTest(t)
    49  	defer gt.done()
    50  
    51  	// Check that hook fails when message is empty.
    52  	write(t, gt.client+"/empty.txt", "\n\n# just a file with\n# comments\n")
    53  	testMainDied(t, "hook-invoke", "commit-msg", gt.client+"/empty.txt")
    54  	const empty = "git-codereview: empty commit message\n"
    55  	if got := testStderr.String(); got != empty {
    56  		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, empty)
    57  	}
    58  
    59  	// Check that hook inserts a blank line after the first line as needed.
    60  	rewrites := []struct {
    61  		in   string
    62  		want string
    63  	}{
    64  		{in: "all: gofmt", want: "all: gofmt"},
    65  		{in: "all: gofmt\n", want: "all: gofmt\n"},
    66  		{in: "all: gofmt\nahhh", want: "all: gofmt\n\nahhh"},
    67  		{in: "all: gofmt\n\nahhh", want: "all: gofmt\n\nahhh"},
    68  		{in: "all: gofmt\n\n\nahhh", want: "all: gofmt\n\n\nahhh"},
    69  		// Issue 16376
    70  		{
    71  			in:   "all: gofmt\n# ------------------------ >8 ------------------------\ndiff",
    72  			want: "all: gofmt\n",
    73  		},
    74  	}
    75  	for _, tt := range rewrites {
    76  		write(t, gt.client+"/in.txt", tt.in)
    77  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/in.txt")
    78  		write(t, gt.client+"/want.txt", tt.want)
    79  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/want.txt")
    80  		got, err := ioutil.ReadFile(gt.client + "/in.txt")
    81  		if err != nil {
    82  			t.Fatal(err)
    83  		}
    84  		want, err := ioutil.ReadFile(gt.client + "/want.txt")
    85  		if err != nil {
    86  			t.Fatal(err)
    87  		}
    88  
    89  		if !bytes.Equal(got, want) {
    90  			t.Fatalf("failed to rewrite:\n%s\n\ngot:\n\n%s\n\nwant:\n\n%s\n", tt.in, got, want)
    91  		}
    92  	}
    93  }
    94  
    95  func TestHookCommitMsgIssueRepoRewrite(t *testing.T) {
    96  	gt := newGitTest(t)
    97  	defer gt.done()
    98  
    99  	msgs := []string{
   100  		// If there's no config, don't rewrite issue references.
   101  		"math/big: catch all the rats\n\nFixes #99999, at least for now\n",
   102  		// Fix the fix-message, even without config
   103  		"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n",
   104  		"math/big: catch all the rats\n\nFixes issue 99999, at least for now\n",
   105  		// Don't forget to write back if Change-Id already exists
   106  	}
   107  	for _, msg := range msgs {
   108  		write(t, gt.client+"/msg.txt", msg)
   109  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   110  		got := read(t, gt.client+"/msg.txt")
   111  		const want = "math/big: catch all the rats\n\nFixes #99999, at least for now\n"
   112  		if string(got) != want {
   113  			t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s\nlen %d and %d", got, want, len(got), len(want))
   114  		}
   115  	}
   116  
   117  	// Add issuerepo config, clear any previous config.
   118  	write(t, gt.client+"/codereview.cfg", "issuerepo: golang/go")
   119  	cachedConfig = nil
   120  
   121  	// Check for the rewrite
   122  	msgs = []string{
   123  		"math/big: catch all the rats\n\nFixes #99999, at least for now\n",
   124  		"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n",
   125  		"math/big: catch all the rats\n\nFixes issue 99999, at least for now\n",
   126  		"math/big: catch all the rats\n\nFixes issue golang/go#99999, at least for now\n",
   127  	}
   128  	for _, msg := range msgs {
   129  		write(t, gt.client+"/msg.txt", msg)
   130  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   131  		got := read(t, gt.client+"/msg.txt")
   132  		const want = "math/big: catch all the rats\n\nFixes golang/go#99999, at least for now\n"
   133  		if string(got) != want {
   134  			t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s", got, want)
   135  		}
   136  	}
   137  
   138  	// Reset config state
   139  	cachedConfig = nil
   140  }
   141  
   142  func TestHookCommitMsgBranchPrefix(t *testing.T) {
   143  	testHookCommitMsgBranchPrefix(t, false)
   144  	testHookCommitMsgBranchPrefix(t, true)
   145  }
   146  
   147  func testHookCommitMsgBranchPrefix(t *testing.T, gerrit bool) {
   148  	t.Logf("gerrit=%v", gerrit)
   149  
   150  	gt := newGitTest(t)
   151  	if gerrit {
   152  		gt.enableGerrit(t)
   153  	}
   154  	defer gt.done()
   155  
   156  	checkPrefix := func(prefix string) {
   157  		write(t, gt.client+"/msg.txt", "Test message.\n")
   158  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   159  		data, err := ioutil.ReadFile(gt.client + "/msg.txt")
   160  		if err != nil {
   161  			t.Fatal(err)
   162  		}
   163  		if !bytes.HasPrefix(data, []byte(prefix)) {
   164  			t.Errorf("after hook-invoke commit-msg on %s, want prefix %q:\n%s", CurrentBranch().Name, prefix, data)
   165  		}
   166  
   167  		if i := strings.Index(prefix, "]"); i >= 0 {
   168  			prefix := prefix[:i+1]
   169  			for _, magic := range []string{"fixup!", "squash!"} {
   170  				write(t, gt.client+"/msg.txt", magic+" Test message.\n")
   171  				testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   172  				data, err := ioutil.ReadFile(gt.client + "/msg.txt")
   173  				if err != nil {
   174  					t.Fatal(err)
   175  				}
   176  				if bytes.HasPrefix(data, []byte(prefix)) {
   177  					t.Errorf("after hook-invoke commit-msg on %s with %s, found incorrect prefix %q:\n%s", CurrentBranch().Name, magic, prefix, data)
   178  				}
   179  			}
   180  		}
   181  	}
   182  
   183  	// Create server branch and switch to server branch on client.
   184  	// Test that commit hook adds prefix.
   185  	trun(t, gt.server, "git", "checkout", "-b", "dev.cc")
   186  	trun(t, gt.client, "git", "fetch", "-q")
   187  	testMain(t, "change", "dev.cc")
   188  	if gerrit {
   189  		checkPrefix("[dev.cc] Test message.\n")
   190  	} else {
   191  		checkPrefix("Test message.\n")
   192  	}
   193  
   194  	// Work branch with server branch as upstream.
   195  	testMain(t, "change", "ccwork")
   196  	if gerrit {
   197  		checkPrefix("[dev.cc] Test message.\n")
   198  	} else {
   199  		checkPrefix("Test message.\n")
   200  	}
   201  
   202  	// Master has no prefix.
   203  	testMain(t, "change", "master")
   204  	checkPrefix("Test message.\n")
   205  
   206  	// Work branch from master has no prefix.
   207  	testMain(t, "change", "work")
   208  	checkPrefix("Test message.\n")
   209  }
   210  
   211  func TestHookPreCommit(t *testing.T) {
   212  	gt := newGitTest(t)
   213  	defer gt.done()
   214  
   215  	// Write out a non-Go file.
   216  	testMain(t, "change", "mybranch")
   217  	write(t, gt.client+"/msg.txt", "A test message.")
   218  	trun(t, gt.client, "git", "add", "msg.txt")
   219  	testMain(t, "hook-invoke", "pre-commit") // should be no-op
   220  
   221  	if err := os.MkdirAll(gt.client+"/test/bench", 0755); err != nil {
   222  		t.Fatal(err)
   223  	}
   224  	write(t, gt.client+"/bad.go", badGo)
   225  	write(t, gt.client+"/good.go", goodGo)
   226  	write(t, gt.client+"/test/bad.go", badGo)
   227  	write(t, gt.client+"/test/good.go", goodGo)
   228  	write(t, gt.client+"/test/bench/bad.go", badGo)
   229  	write(t, gt.client+"/test/bench/good.go", goodGo)
   230  	trun(t, gt.client, "git", "add", ".")
   231  
   232  	testMainDied(t, "hook-invoke", "pre-commit")
   233  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
   234  		"bad.go", "!good.go", fromSlash("!test/bad"), fromSlash("test/bench/bad.go"))
   235  
   236  	write(t, gt.client+"/broken.go", brokenGo)
   237  	trun(t, gt.client, "git", "add", "broken.go")
   238  	testMainDied(t, "hook-invoke", "pre-commit")
   239  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
   240  		"bad.go", "!good.go", fromSlash("!test/bad"), fromSlash("test/bench/bad.go"),
   241  		"gofmt reported errors:", "broken.go")
   242  }
   243  
   244  func TestHookChangeGofmt(t *testing.T) {
   245  	// During git change, we run the gofmt check before invoking commit,
   246  	// so we should not see the line about 'git commit' failing.
   247  	// That is, the failure should come from git change, not from
   248  	// the commit hook.
   249  	gt := newGitTest(t)
   250  	defer gt.done()
   251  	gt.work(t)
   252  
   253  	// Write out a non-Go file.
   254  	write(t, gt.client+"/bad.go", badGo)
   255  	trun(t, gt.client, "git", "add", ".")
   256  
   257  	t.Logf("invoking commit hook explicitly")
   258  	testMainDied(t, "hook-invoke", "pre-commit")
   259  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go")
   260  
   261  	t.Logf("change without hook installed")
   262  	testCommitMsg = "foo: msg"
   263  	testMainDied(t, "change")
   264  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
   265  
   266  	t.Logf("change with hook installed")
   267  	restore := testInstallHook(t, gt)
   268  	defer restore()
   269  	testCommitMsg = "foo: msg"
   270  	testMainDied(t, "change")
   271  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
   272  }
   273  
   274  func TestHookPreCommitDetachedHead(t *testing.T) {
   275  	// If we're in detached head mode, something special is going on,
   276  	// like git rebase. We disable the gofmt-checking precommit hook,
   277  	// since we expect it would just get in the way at that point.
   278  	// (It also used to crash.)
   279  
   280  	gt := newGitTest(t)
   281  	defer gt.done()
   282  	gt.work(t)
   283  
   284  	write(t, gt.client+"/bad.go", badGo)
   285  	trun(t, gt.client, "git", "add", ".")
   286  	trun(t, gt.client, "git", "checkout", "HEAD^0")
   287  
   288  	testMainDied(t, "hook-invoke", "pre-commit")
   289  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go")
   290  
   291  	/*
   292  		OLD TEST, back when we disabled gofmt in detached head,
   293  		in case we go back to that:
   294  
   295  		// If we're in detached head mode, something special is going on,
   296  		// like git rebase. We disable the gofmt-checking precommit hook,
   297  		// since we expect it would just get in the way at that point.
   298  		// (It also used to crash.)
   299  
   300  		gt := newGitTest(t)
   301  		defer gt.done()
   302  		gt.work(t)
   303  
   304  		write(t, gt.client+"/bad.go", badGo)
   305  		trun(t, gt.client, "git", "add", ".")
   306  		trun(t, gt.client, "git", "checkout", "HEAD^0")
   307  
   308  		testMain(t, "hook-invoke", "pre-commit")
   309  		testNoStdout(t)
   310  		testNoStderr(t)
   311  	*/
   312  }
   313  
   314  func TestHookPreCommitEnv(t *testing.T) {
   315  	// If $GIT_GOFMT_HOOK == "off", gofmt hook should not complain.
   316  
   317  	gt := newGitTest(t)
   318  	defer gt.done()
   319  	gt.work(t)
   320  
   321  	write(t, gt.client+"/bad.go", badGo)
   322  	trun(t, gt.client, "git", "add", ".")
   323  	os.Setenv("GIT_GOFMT_HOOK", "off")
   324  	defer os.Unsetenv("GIT_GOFMT_HOOK")
   325  
   326  	testMain(t, "hook-invoke", "pre-commit")
   327  	testNoStdout(t)
   328  	testPrintedStderr(t, "git-codereview pre-commit gofmt hook disabled by $GIT_GOFMT_HOOK=off")
   329  }
   330  
   331  func TestHookPreCommitUnstaged(t *testing.T) {
   332  	gt := newGitTest(t)
   333  	defer gt.done()
   334  	gt.work(t)
   335  
   336  	write(t, gt.client+"/bad.go", badGo)
   337  	write(t, gt.client+"/good.go", goodGo)
   338  
   339  	// The pre-commit hook is being asked about files in the index.
   340  	// Make sure it is not looking at files in the working tree (current directory) instead.
   341  	// There are three possible kinds of file: good, bad (misformatted), and broken (syntax error).
   342  	// There are also three possible places files live: the most recent commit, the index,
   343  	// and the working tree. We write a sequence of files that cover all possible
   344  	// combination of kinds of file in the various places. For example,
   345  	// good-bad-broken.go is a good file in the most recent commit,
   346  	// a bad file in the index, and a broken file in the working tree.
   347  	// After creating these files, we check that the gofmt hook reports
   348  	// about the index only.
   349  
   350  	const N = 3
   351  	name := []string{"good", "bad", "broken"}
   352  	content := []string{goodGo, badGo, brokenGo}
   353  	var wantErr []string
   354  	var allFiles []string
   355  	writeFiles := func(n int) {
   356  		allFiles = nil
   357  		wantErr = nil
   358  		for i := 0; i < N*N*N; i++ {
   359  			// determine n'th digit of 3-digit base-N value i
   360  			j := i
   361  			for k := 0; k < (3 - 1 - n); k++ {
   362  				j /= N
   363  			}
   364  			file := fmt.Sprintf("%s-%s-%s.go", name[i/N/N], name[(i/N)%N], name[i%N])
   365  			allFiles = append(allFiles, file)
   366  			write(t, gt.client+"/"+file, content[j%N])
   367  
   368  			switch {
   369  			case strings.Contains(file, "-bad-"):
   370  				wantErr = append(wantErr, "\t"+file+"\n")
   371  			case strings.Contains(file, "-broken-"):
   372  				wantErr = append(wantErr, "\t"+file+":")
   373  			default:
   374  				wantErr = append(wantErr, "!"+file)
   375  			}
   376  		}
   377  	}
   378  
   379  	// committed files
   380  	writeFiles(0)
   381  	trun(t, gt.client, "git", "add", ".")
   382  	trun(t, gt.client, "git", "commit", "-m", "msg")
   383  
   384  	// staged files
   385  	writeFiles(1)
   386  	trun(t, gt.client, "git", "add", ".")
   387  
   388  	// unstaged files
   389  	writeFiles(2)
   390  
   391  	wantErr = append(wantErr, "gofmt reported errors", "gofmt needs to format these files")
   392  
   393  	testMainDied(t, "hook-invoke", "pre-commit")
   394  	testPrintedStderr(t, wantErr...)
   395  }
   396  
   397  func TestHooks(t *testing.T) {
   398  	gt := newGitTest(t)
   399  	defer gt.done()
   400  
   401  	gt.removeStubHooks()
   402  	testMain(t, "hooks") // install hooks
   403  
   404  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   405  	if err != nil {
   406  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   407  	}
   408  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   409  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   410  	}
   411  }
   412  
   413  var worktreeRE = regexp.MustCompile(`\sworktree\s`)
   414  
   415  func mustHaveWorktree(t *testing.T) {
   416  	commands := trun(t, "", "git", "help", "-a")
   417  	if !worktreeRE.MatchString(commands) {
   418  		t.Skip("git doesn't support worktree")
   419  	}
   420  }
   421  
   422  func TestHooksInWorktree(t *testing.T) {
   423  	gt := newGitTest(t)
   424  	defer gt.done()
   425  
   426  	mustHaveWorktree(t)
   427  
   428  	trun(t, gt.client, "git", "worktree", "add", "../worktree")
   429  	chdir(t, filepath.Join("..", "worktree"))
   430  
   431  	gt.removeStubHooks()
   432  	testMain(t, "hooks") // install hooks
   433  
   434  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   435  	if err != nil {
   436  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   437  	}
   438  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   439  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   440  	}
   441  }
   442  
   443  func TestHooksInSubdir(t *testing.T) {
   444  	gt := newGitTest(t)
   445  	defer gt.done()
   446  
   447  	gt.removeStubHooks()
   448  	if err := os.MkdirAll(gt.client+"/test", 0755); err != nil {
   449  		t.Fatal(err)
   450  	}
   451  	chdir(t, gt.client+"/test")
   452  
   453  	testMain(t, "hooks") // install hooks
   454  
   455  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   456  	if err != nil {
   457  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   458  	}
   459  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   460  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   461  	}
   462  }
   463  
   464  func TestHooksOverwriteOldCommitMsg(t *testing.T) {
   465  	gt := newGitTest(t)
   466  	defer gt.done()
   467  
   468  	write(t, gt.client+"/.git/hooks/commit-msg", oldCommitMsgHook)
   469  	testMain(t, "hooks") // install hooks
   470  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   471  	if err != nil {
   472  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   473  	}
   474  	if string(data) == oldCommitMsgHook {
   475  		t.Fatalf("hooks left old commit-msg hook in place")
   476  	}
   477  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   478  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   479  	}
   480  }
   481  
   482  func testInstallHook(t *testing.T, gt *gitTest) (restore func()) {
   483  	trun(t, gt.pwd, "go", "build", "-o", gt.client+"/git-codereview")
   484  	path := os.Getenv("PATH")
   485  	os.Setenv("PATH", gt.client+string(filepath.ListSeparator)+path)
   486  	gt.removeStubHooks()
   487  	testMain(t, "hooks") // install hooks
   488  
   489  	return func() {
   490  		os.Setenv("PATH", path)
   491  	}
   492  }
   493  
   494  func TestHookCommitMsgFromGit(t *testing.T) {
   495  	gt := newGitTest(t)
   496  	gt.enableGerrit(t)
   497  	defer gt.done()
   498  
   499  	restore := testInstallHook(t, gt)
   500  	defer restore()
   501  
   502  	testMain(t, "change", "mybranch")
   503  	write(t, gt.client+"/file", "more data")
   504  	trun(t, gt.client, "git", "add", "file")
   505  	trun(t, gt.client, "git", "commit", "-m", "mymsg")
   506  
   507  	log := trun(t, gt.client, "git", "log", "-n", "1")
   508  	if !strings.Contains(log, "mymsg") {
   509  		t.Fatalf("did not find mymsg in git log output:\n%s", log)
   510  	}
   511  	// The 4 spaces are because git indents the commit message proper.
   512  	if !strings.Contains(log, "\n    \n    Change-Id:") {
   513  		t.Fatalf("did not find Change-Id in git log output:\n%s", log)
   514  	}
   515  }