github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/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  	"strings"
    14  	"testing"
    15  )
    16  
    17  const lenChangeId = len("\n\nChange-Id: I") + 2*20
    18  
    19  func TestHookCommitMsg(t *testing.T) {
    20  	gt := newGitTest(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  	// Check that hook fails when message is empty.
    47  	write(t, gt.client+"/empty.txt", "\n\n# just a file with\n# comments\n")
    48  	testMainDied(t, "hook-invoke", "commit-msg", gt.client+"/empty.txt")
    49  	const empty = "git-codereview: empty commit message\n"
    50  	if got := testStderr.String(); got != empty {
    51  		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, empty)
    52  	}
    53  
    54  	// Check that hook inserts a blank line after the first line as needed.
    55  	rewrites := []struct {
    56  		in   string
    57  		want string
    58  	}{
    59  		{in: "all: gofmt", want: "all: gofmt"},
    60  		{in: "all: gofmt\n", want: "all: gofmt\n"},
    61  		{in: "all: gofmt\nahhh", want: "all: gofmt\n\nahhh"},
    62  		{in: "all: gofmt\n\nahhh", want: "all: gofmt\n\nahhh"},
    63  		{in: "all: gofmt\n\n\nahhh", want: "all: gofmt\n\n\nahhh"},
    64  	}
    65  	for _, tt := range rewrites {
    66  		write(t, gt.client+"/in.txt", tt.in)
    67  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/in.txt")
    68  		write(t, gt.client+"/want.txt", tt.want)
    69  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/want.txt")
    70  		got, err := ioutil.ReadFile(gt.client + "/in.txt")
    71  		if err != nil {
    72  			t.Fatal(err)
    73  		}
    74  		want, err := ioutil.ReadFile(gt.client + "/want.txt")
    75  		if err != nil {
    76  			t.Fatal(err)
    77  		}
    78  
    79  		// pull off the Change-Id that got appended
    80  		got = got[:len(got)-lenChangeId]
    81  		want = want[:len(want)-lenChangeId]
    82  		if !bytes.Equal(got, want) {
    83  			t.Fatalf("failed to rewrite:\n%s\n\ngot:\n\n%s\n\nwant:\n\n%s\n", tt.in, got, want)
    84  		}
    85  	}
    86  }
    87  
    88  func TestHookCommitMsgIssueRepoRewrite(t *testing.T) {
    89  	gt := newGitTest(t)
    90  	defer gt.done()
    91  
    92  	msgs := []string{
    93  		// If there's no config, don't rewrite issue references.
    94  		"math/big: catch all the rats\n\nFixes #99999, at least for now\n",
    95  		// Fix the fix-message, even without config
    96  		"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n",
    97  		"math/big: catch all the rats\n\nFixes issue 99999, at least for now\n",
    98  		// Don't forget to write back if Change-Id already exists
    99  		"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n\nChange-Id: Ie77358867e38cf976a0688b6e2f80525dae3891e\n",
   100  	}
   101  	for _, msg := range msgs {
   102  		write(t, gt.client+"/msg.txt", msg)
   103  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   104  		got := read(t, gt.client+"/msg.txt")
   105  		got = got[:len(got)-lenChangeId]
   106  		const want = "math/big: catch all the rats\n\nFixes #99999, at least for now\n"
   107  		if string(got) != want {
   108  			t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s\nlen %d and %d", got, want, len(got), len(want))
   109  		}
   110  	}
   111  
   112  	// Add issuerepo config.
   113  	write(t, gt.client+"/codereview.cfg", "issuerepo: golang/go")
   114  	trun(t, gt.client, "git", "add", "codereview.cfg")
   115  	trun(t, gt.client, "git", "commit", "-m", "add issuerepo codereview config")
   116  
   117  	// Look in master rather than origin/master for the config
   118  	savedConfigRef := configRef
   119  	configRef = "master:codereview.cfg"
   120  	cachedConfig = nil
   121  
   122  	// Check for the rewrite
   123  	msgs = []string{
   124  		"math/big: catch all the rats\n\nFixes #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 99999, at least for now\n",
   127  		"math/big: catch all the rats\n\nFixes issue golang/go#99999, at least for now\n",
   128  		"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n\nChange-Id: Ie77358867e38cf976a0688b6e2f80525dae3891e\n",
   129  	}
   130  	for _, msg := range msgs {
   131  		write(t, gt.client+"/msg.txt", msg)
   132  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   133  		got := read(t, gt.client+"/msg.txt")
   134  		got = got[:len(got)-lenChangeId]
   135  		const want = "math/big: catch all the rats\n\nFixes golang/go#99999, at least for now\n"
   136  		if string(got) != want {
   137  			t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s", got, want)
   138  		}
   139  	}
   140  
   141  	// Reset config state
   142  	configRef = savedConfigRef
   143  	cachedConfig = nil
   144  }
   145  
   146  func TestHookCommitMsgBranchPrefix(t *testing.T) {
   147  	gt := newGitTest(t)
   148  	defer gt.done()
   149  
   150  	checkPrefix := func(prefix string) {
   151  		write(t, gt.client+"/msg.txt", "Test message.\n")
   152  		testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   153  		data, err := ioutil.ReadFile(gt.client + "/msg.txt")
   154  		if err != nil {
   155  			t.Fatal(err)
   156  		}
   157  		if !bytes.HasPrefix(data, []byte(prefix)) {
   158  			t.Errorf("after hook-invoke commit-msg on %s, want prefix %q:\n%s", CurrentBranch().Name, prefix, data)
   159  		}
   160  
   161  		if i := strings.Index(prefix, "]"); i >= 0 {
   162  			prefix := prefix[:i+1]
   163  			for _, magic := range []string{"fixup!", "squash!"} {
   164  				write(t, gt.client+"/msg.txt", magic+" Test message.\n")
   165  				testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
   166  				data, err := ioutil.ReadFile(gt.client + "/msg.txt")
   167  				if err != nil {
   168  					t.Fatal(err)
   169  				}
   170  				if bytes.HasPrefix(data, []byte(prefix)) {
   171  					t.Errorf("after hook-invoke commit-msg on %s with %s, found incorrect prefix %q:\n%s", CurrentBranch().Name, magic, prefix, data)
   172  				}
   173  			}
   174  		}
   175  	}
   176  
   177  	// Create server branch and switch to server branch on client.
   178  	// Test that commit hook adds prefix.
   179  	trun(t, gt.server, "git", "checkout", "-b", "dev.cc")
   180  	trun(t, gt.client, "git", "fetch", "-q")
   181  	testMain(t, "change", "dev.cc")
   182  	checkPrefix("[dev.cc] Test message.\n")
   183  
   184  	// Work branch with server branch as upstream.
   185  	testMain(t, "change", "ccwork")
   186  	checkPrefix("[dev.cc] Test message.\n")
   187  
   188  	// Master has no prefix.
   189  	testMain(t, "change", "master")
   190  	checkPrefix("Test message.\n")
   191  
   192  	// Work branch from master has no prefix.
   193  	testMain(t, "change", "work")
   194  	checkPrefix("Test message.\n")
   195  }
   196  
   197  func TestHookPreCommit(t *testing.T) {
   198  	gt := newGitTest(t)
   199  	defer gt.done()
   200  
   201  	// Write out a non-Go file.
   202  	testMain(t, "change", "mybranch")
   203  	write(t, gt.client+"/msg.txt", "A test message.")
   204  	trun(t, gt.client, "git", "add", "msg.txt")
   205  	testMain(t, "hook-invoke", "pre-commit") // should be no-op
   206  
   207  	if err := os.MkdirAll(gt.client+"/test/bench", 0755); err != nil {
   208  		t.Fatal(err)
   209  	}
   210  	write(t, gt.client+"/bad.go", badGo)
   211  	write(t, gt.client+"/good.go", goodGo)
   212  	write(t, gt.client+"/test/bad.go", badGo)
   213  	write(t, gt.client+"/test/good.go", goodGo)
   214  	write(t, gt.client+"/test/bench/bad.go", badGo)
   215  	write(t, gt.client+"/test/bench/good.go", goodGo)
   216  	trun(t, gt.client, "git", "add", ".")
   217  
   218  	testMainDied(t, "hook-invoke", "pre-commit")
   219  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
   220  		"bad.go", "!good.go", fromSlash("!test/bad"), fromSlash("test/bench/bad.go"))
   221  
   222  	write(t, gt.client+"/broken.go", brokenGo)
   223  	trun(t, gt.client, "git", "add", "broken.go")
   224  	testMainDied(t, "hook-invoke", "pre-commit")
   225  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
   226  		"bad.go", "!good.go", fromSlash("!test/bad"), fromSlash("test/bench/bad.go"),
   227  		"gofmt reported errors:", "broken.go")
   228  }
   229  
   230  func TestHookChangeGofmt(t *testing.T) {
   231  	// During git change, we run the gofmt check before invoking commit,
   232  	// so we should not see the line about 'git commit' failing.
   233  	// That is, the failure should come from git change, not from
   234  	// the commit hook.
   235  	gt := newGitTest(t)
   236  	defer gt.done()
   237  	gt.work(t)
   238  
   239  	// Write out a non-Go file.
   240  	write(t, gt.client+"/bad.go", badGo)
   241  	trun(t, gt.client, "git", "add", ".")
   242  
   243  	t.Logf("invoking commit hook explicitly")
   244  	testMainDied(t, "hook-invoke", "pre-commit")
   245  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go")
   246  
   247  	t.Logf("change without hook installed")
   248  	testCommitMsg = "foo: msg"
   249  	testMainDied(t, "change")
   250  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
   251  
   252  	t.Logf("change with hook installed")
   253  	restore := testInstallHook(t, gt)
   254  	defer restore()
   255  	testCommitMsg = "foo: msg"
   256  	testMainDied(t, "change")
   257  	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
   258  }
   259  
   260  func TestHookPreCommitDetachedHead(t *testing.T) {
   261  	// If we're in detached head mode, something special is going on,
   262  	// like git rebase. We disable the gofmt-checking precommit hook,
   263  	// since we expect it would just get in the way at that point.
   264  	// (It also used to crash.)
   265  
   266  	gt := newGitTest(t)
   267  	defer gt.done()
   268  	gt.work(t)
   269  
   270  	write(t, gt.client+"/bad.go", badGo)
   271  	trun(t, gt.client, "git", "add", ".")
   272  	trun(t, gt.client, "git", "checkout", "HEAD^0")
   273  
   274  	testMain(t, "hook-invoke", "pre-commit")
   275  	testNoStdout(t)
   276  	testNoStderr(t)
   277  }
   278  
   279  func TestHookPreCommitEnv(t *testing.T) {
   280  	// If $GIT_GOFMT_HOOK == "off", gofmt hook should not complain.
   281  
   282  	gt := newGitTest(t)
   283  	defer gt.done()
   284  	gt.work(t)
   285  
   286  	write(t, gt.client+"/bad.go", badGo)
   287  	trun(t, gt.client, "git", "add", ".")
   288  	os.Setenv("GIT_GOFMT_HOOK", "off")
   289  	defer os.Unsetenv("GIT_GOFMT_HOOK")
   290  
   291  	testMain(t, "hook-invoke", "pre-commit")
   292  	testNoStdout(t)
   293  	testPrintedStderr(t, "git-gofmt-hook disabled by $GIT_GOFMT_HOOK=off")
   294  }
   295  
   296  func TestHookPreCommitUnstaged(t *testing.T) {
   297  	gt := newGitTest(t)
   298  	defer gt.done()
   299  	gt.work(t)
   300  
   301  	write(t, gt.client+"/bad.go", badGo)
   302  	write(t, gt.client+"/good.go", goodGo)
   303  
   304  	// The pre-commit hook is being asked about files in the index.
   305  	// Make sure it is not looking at files in the working tree (current directory) instead.
   306  	// There are three possible kinds of file: good, bad (misformatted), and broken (syntax error).
   307  	// There are also three possible places files live: the most recent commit, the index,
   308  	// and the working tree. We write a sequence of files that cover all possible
   309  	// combination of kinds of file in the various places. For example,
   310  	// good-bad-broken.go is a good file in the most recent commit,
   311  	// a bad file in the index, and a broken file in the working tree.
   312  	// After creating these files, we check that the gofmt hook reports
   313  	// about the index only.
   314  
   315  	const N = 3
   316  	name := []string{"good", "bad", "broken"}
   317  	content := []string{goodGo, badGo, brokenGo}
   318  	var wantErr []string
   319  	var allFiles []string
   320  	writeFiles := func(n int) {
   321  		allFiles = nil
   322  		wantErr = nil
   323  		for i := 0; i < N*N*N; i++ {
   324  			// determine n'th digit of 3-digit base-N value i
   325  			j := i
   326  			for k := 0; k < (3 - 1 - n); k++ {
   327  				j /= N
   328  			}
   329  			file := fmt.Sprintf("%s-%s-%s.go", name[i/N/N], name[(i/N)%N], name[i%N])
   330  			allFiles = append(allFiles, file)
   331  			write(t, gt.client+"/"+file, content[j%N])
   332  
   333  			switch {
   334  			case strings.Contains(file, "-bad-"):
   335  				wantErr = append(wantErr, "\t"+file+"\n")
   336  			case strings.Contains(file, "-broken-"):
   337  				wantErr = append(wantErr, "\t"+file+":")
   338  			default:
   339  				wantErr = append(wantErr, "!"+file)
   340  			}
   341  		}
   342  	}
   343  
   344  	// committed files
   345  	writeFiles(0)
   346  	trun(t, gt.client, "git", "add", ".")
   347  	trun(t, gt.client, "git", "commit", "-m", "msg")
   348  
   349  	// staged files
   350  	writeFiles(1)
   351  	trun(t, gt.client, "git", "add", ".")
   352  
   353  	// unstaged files
   354  	writeFiles(2)
   355  
   356  	wantErr = append(wantErr, "gofmt reported errors", "gofmt needs to format these files")
   357  
   358  	testMainDied(t, "hook-invoke", "pre-commit")
   359  	testPrintedStderr(t, wantErr...)
   360  }
   361  
   362  func TestHooks(t *testing.T) {
   363  	gt := newGitTest(t)
   364  	defer gt.done()
   365  
   366  	gt.removeStubHooks()
   367  	testMain(t, "hooks") // install hooks
   368  
   369  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   370  	if err != nil {
   371  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   372  	}
   373  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   374  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   375  	}
   376  }
   377  
   378  func TestHooksOverwriteOldCommitMsg(t *testing.T) {
   379  	gt := newGitTest(t)
   380  	defer gt.done()
   381  
   382  	write(t, gt.client+"/.git/hooks/commit-msg", oldCommitMsgHook)
   383  	testMain(t, "hooks") // install hooks
   384  	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
   385  	if err != nil {
   386  		t.Fatalf("hooks did not write commit-msg hook: %v", err)
   387  	}
   388  	if string(data) == oldCommitMsgHook {
   389  		t.Fatalf("hooks left old commit-msg hook in place")
   390  	}
   391  	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
   392  		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
   393  	}
   394  }
   395  
   396  func testInstallHook(t *testing.T, gt *gitTest) (restore func()) {
   397  	trun(t, gt.pwd, "go", "build", "-o", gt.client+"/git-codereview")
   398  	path := os.Getenv("PATH")
   399  	os.Setenv("PATH", gt.client+string(filepath.ListSeparator)+path)
   400  	gt.removeStubHooks()
   401  	testMain(t, "hooks") // install hooks
   402  
   403  	return func() {
   404  		os.Setenv("PATH", path)
   405  	}
   406  }
   407  
   408  func TestHookCommitMsgFromGit(t *testing.T) {
   409  	gt := newGitTest(t)
   410  	defer gt.done()
   411  
   412  	restore := testInstallHook(t, gt)
   413  	defer restore()
   414  
   415  	testMain(t, "change", "mybranch")
   416  	write(t, gt.client+"/file", "more data")
   417  	trun(t, gt.client, "git", "add", "file")
   418  	trun(t, gt.client, "git", "commit", "-m", "mymsg")
   419  
   420  	log := trun(t, gt.client, "git", "log", "-n", "1")
   421  	if !strings.Contains(log, "mymsg") {
   422  		t.Fatalf("did not find mymsg in git log output:\n%s", log)
   423  	}
   424  	// The 4 spaces are because git indents the commit message proper.
   425  	if !strings.Contains(log, "\n    \n    Change-Id:") {
   426  		t.Fatalf("did not find Change-Id in git log output:\n%s", log)
   427  	}
   428  }