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 }