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 }