github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/git-codereview/hook.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 "crypto/rand" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "regexp" 16 "runtime" 17 "strings" 18 ) 19 20 var hookPath = ".git/hooks/" 21 var hookFiles = []string{ 22 "commit-msg", 23 "pre-commit", 24 } 25 26 func installHook() { 27 for _, hookFile := range hookFiles { 28 filename := filepath.Join(repoRoot(), hookPath+hookFile) 29 hookContent := fmt.Sprintf(hookScript, hookFile) 30 31 if data, err := ioutil.ReadFile(filename); err == nil { 32 // Special case: remove old hooks that use 'git-review' 33 oldHookContent := fmt.Sprintf(oldHookScript, hookFile) 34 if string(data) == oldHookContent { 35 verbosef("removing old %v hook", hookFile) 36 os.Remove(filename) 37 } 38 // Special case: remove old commit-msg shell script 39 // in favor of invoking the git-codereview hook 40 // implementation, which will be easier to change in 41 // the future. 42 if hookFile == "commit-msg" && string(data) == oldCommitMsgHook { 43 verbosef("removing old commit-msg hook") 44 os.Remove(filename) 45 } 46 } 47 48 // If hook file exists, assume it is okay. 49 _, err := os.Stat(filename) 50 if err == nil { 51 if *verbose > 0 { 52 data, err := ioutil.ReadFile(filename) 53 if err != nil { 54 verbosef("reading hook: %v", err) 55 } else if string(data) != hookContent { 56 verbosef("unexpected hook content in %s", filename) 57 } 58 } 59 continue 60 } 61 62 if !os.IsNotExist(err) { 63 dief("checking hook: %v", err) 64 } 65 verbosef("installing %s hook", hookFile) 66 if err := ioutil.WriteFile(filename, []byte(hookContent), 0700); err != nil { 67 dief("writing hook: %v", err) 68 } 69 } 70 } 71 72 func repoRoot() string { 73 dir, err := os.Getwd() 74 if err != nil { 75 dief("could not get current directory: %v", err) 76 } 77 rootlen := 1 78 if runtime.GOOS == "windows" { 79 rootlen += len(filepath.VolumeName(dir)) 80 } 81 for { 82 if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { 83 return dir 84 } 85 if len(dir) == rootlen && dir[rootlen-1] == filepath.Separator { 86 dief("git root not found. Rerun from within the Git tree.") 87 } 88 dir = filepath.Dir(dir) 89 } 90 } 91 92 var hookScript = `#!/bin/sh 93 exec git-codereview hook-invoke %s "$@" 94 ` 95 96 var oldHookScript = `#!/bin/sh 97 exec git-review hook-invoke %s "$@" 98 ` 99 100 func cmdHookInvoke(args []string) { 101 flags.Parse(args) 102 args = flags.Args() 103 if len(args) == 0 { 104 dief("usage: git-codereview hook-invoke <hook-name> [args...]") 105 } 106 switch args[0] { 107 case "commit-msg": 108 hookCommitMsg(args[1:]) 109 case "pre-commit": 110 hookPreCommit(args[1:]) 111 } 112 } 113 114 var ( 115 issueRefRE = regexp.MustCompile(`(?P<space>\s)(?P<ref>#\d+\w)`) 116 oldFixesRETemplate = `Fixes +(issue +(%s)?#?)?(?P<issueNum>[0-9]+)` 117 ) 118 119 // hookCommitMsg is installed as the git commit-msg hook. 120 // It adds a Change-Id line to the bottom of the commit message 121 // if there is not one already. 122 func hookCommitMsg(args []string) { 123 if len(args) != 1 { 124 dief("usage: git-codereview hook-invoke commit-msg message.txt\n") 125 } 126 127 b := CurrentBranch() 128 if b.DetachedHead() { 129 // Likely executing rebase or some other internal operation. 130 // Probably a mistake to make commit message changes. 131 return 132 } 133 134 file := args[0] 135 oldData, err := ioutil.ReadFile(file) 136 if err != nil { 137 dief("%v", err) 138 } 139 data := append([]byte{}, oldData...) 140 data = stripComments(data) 141 142 // Empty message not allowed. 143 if len(bytes.TrimSpace(data)) == 0 { 144 dief("empty commit message") 145 } 146 147 // Insert a blank line between first line and subsequent lines if not present. 148 eol := bytes.IndexByte(data, '\n') 149 if eol != -1 && len(data) > eol+1 && data[eol+1] != '\n' { 150 data = append(data, 0) 151 copy(data[eol+1:], data[eol:]) 152 data[eol+1] = '\n' 153 } 154 155 issueRepo := config()["issuerepo"] 156 // Update issue references to point to issue repo, if set. 157 if issueRepo != "" { 158 data = issueRefRE.ReplaceAll(data, []byte("${space}"+issueRepo+"${ref}")) 159 } 160 // TestHookCommitMsgIssueRepoRewrite makes sure the regex is valid 161 oldFixesRE := regexp.MustCompile(fmt.Sprintf(oldFixesRETemplate, regexp.QuoteMeta(issueRepo))) 162 data = oldFixesRE.ReplaceAll(data, []byte("Fixes "+issueRepo+"#${issueNum}")) 163 164 // Complain if two Change-Ids are present. 165 // This can happen during an interactive rebase; 166 // it is easy to forget to remove one of them. 167 nChangeId := bytes.Count(data, []byte("\nChange-Id: ")) 168 if nChangeId > 1 { 169 dief("multiple Change-Id lines") 170 } 171 172 // Add Change-Id to commit message if not present. 173 if nChangeId == 0 { 174 n := len(data) 175 for n > 0 && data[n-1] == '\n' { 176 n-- 177 } 178 var id [20]byte 179 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil { 180 dief("generating Change-Id: %v", err) 181 } 182 data = append(data[:n], fmt.Sprintf("\n\nChange-Id: I%x\n", id[:])...) 183 } 184 185 // Add branch prefix to commit message if not present and not on master 186 // and not a special Git fixup! or squash! commit message. 187 branch := strings.TrimPrefix(b.OriginBranch(), "origin/") 188 if branch != "master" { 189 prefix := "[" + branch + "] " 190 if !bytes.HasPrefix(data, []byte(prefix)) && !isFixup(data) { 191 data = []byte(prefix + string(data)) 192 } 193 } 194 195 // Write back. 196 if !bytes.Equal(data, oldData) { 197 if err := ioutil.WriteFile(file, data, 0666); err != nil { 198 dief("%v", err) 199 } 200 } 201 } 202 203 var ( 204 fixupBang = []byte("fixup!") 205 squashBang = []byte("squash!") 206 ) 207 208 // isFixup reports whether text is a Git fixup! or squash! commit, 209 // which must not have a prefix. 210 func isFixup(text []byte) bool { 211 return bytes.HasPrefix(text, fixupBang) || bytes.HasPrefix(text, squashBang) 212 } 213 214 // stripComments strips lines that begin with "#". 215 func stripComments(in []byte) []byte { 216 return regexp.MustCompile(`(?m)^#.*\n`).ReplaceAll(in, nil) 217 } 218 219 // hookPreCommit is installed as the git pre-commit hook. 220 // It prevents commits to the master branch. 221 // It checks that the Go files added, copied, or modified by 222 // the change are gofmt'd, and if not it prints gofmt instructions 223 // and exits with nonzero status. 224 func hookPreCommit(args []string) { 225 // Prevent commits to master branches. 226 b := CurrentBranch() 227 if b.DetachedHead() { 228 // This is an internal commit such as during git rebase. 229 // Don't die, and don't force gofmt. 230 return 231 } 232 if !b.IsLocalOnly() { 233 dief("cannot commit on %s branch", b.Name) 234 } 235 236 hookGofmt() 237 } 238 239 func hookGofmt() { 240 if os.Getenv("GIT_GOFMT_HOOK") == "off" { 241 fmt.Fprintf(stderr(), "git-gofmt-hook disabled by $GIT_GOFMT_HOOK=off\n") 242 return 243 } 244 245 files, stderr := runGofmt(gofmtPreCommit) 246 247 if stderr != "" { 248 msgf := printf 249 if len(files) == 0 { 250 msgf = dief 251 } 252 msgf("gofmt reported errors:\n\t%s", strings.Replace(strings.TrimSpace(stderr), "\n", "\n\t", -1)) 253 } 254 255 if len(files) == 0 { 256 return 257 } 258 259 dief("gofmt needs to format these files (run 'git gofmt'):\n\t%s", 260 strings.Join(files, "\n\t")) 261 } 262 263 // This is NOT USED ANYMORE. 264 // It is here only for comparing against old commit-hook files. 265 var oldCommitMsgHook = `#!/bin/sh 266 # From Gerrit Code Review 2.2.1 267 # 268 # Part of Gerrit Code Review (http://code.google.com/p/gerrit/) 269 # 270 # Copyright (C) 2009 The Android Open Source Project 271 # 272 # Licensed under the Apache License, Version 2.0 (the "License"); 273 # you may not use this file except in compliance with the License. 274 # You may obtain a copy of the License at 275 # 276 # http://www.apache.org/licenses/LICENSE-2.0 277 # 278 # Unless required by applicable law or agreed to in writing, software 279 # distributed under the License is distributed on an "AS IS" BASIS, 280 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 281 # See the License for the specific language governing permissions and 282 # limitations under the License. 283 # 284 285 CHANGE_ID_AFTER="Bug|Issue" 286 MSG="$1" 287 288 # Check for, and add if missing, a unique Change-Id 289 # 290 add_ChangeId() { 291 clean_message=` + "`" + `sed -e ' 292 /^diff --git a\/.*/{ 293 s/// 294 q 295 } 296 /^Signed-off-by:/d 297 /^#/d 298 ' "$MSG" | git stripspace` + "`" + ` 299 if test -z "$clean_message" 300 then 301 return 302 fi 303 304 if grep -i '^Change-Id:' "$MSG" >/dev/null 305 then 306 return 307 fi 308 309 id=` + "`" + `_gen_ChangeId` + "`" + ` 310 perl -e ' 311 $MSG = shift; 312 $id = shift; 313 $CHANGE_ID_AFTER = shift; 314 315 undef $/; 316 open(I, $MSG); $_ = <I>; close I; 317 s|^diff --git a/.*||ms; 318 s|^#.*$||mg; 319 exit unless $_; 320 321 @message = split /\n/; 322 $haveFooter = 0; 323 $startFooter = @message; 324 for($line = @message - 1; $line >= 0; $line--) { 325 $_ = $message[$line]; 326 327 if (/^[a-zA-Z0-9-]+:/ && !m,^[a-z0-9-]+://,) { 328 $haveFooter++; 329 next; 330 } 331 next if /^[ []/; 332 $startFooter = $line if ($haveFooter && /^\r?$/); 333 last; 334 } 335 336 @footer = @message[$startFooter+1..@message]; 337 @message = @message[0..$startFooter]; 338 push(@footer, "") unless @footer; 339 340 for ($line = 0; $line < @footer; $line++) { 341 $_ = $footer[$line]; 342 next if /^($CHANGE_ID_AFTER):/i; 343 last; 344 } 345 splice(@footer, $line, 0, "Change-Id: I$id"); 346 347 $_ = join("\n", @message, @footer); 348 open(O, ">$MSG"); print O; close O; 349 ' "$MSG" "$id" "$CHANGE_ID_AFTER" 350 } 351 _gen_ChangeIdInput() { 352 echo "tree ` + "`" + `git write-tree` + "`" + `" 353 if parent=` + "`" + `git rev-parse HEAD^0 2>/dev/null` + "`" + ` 354 then 355 echo "parent $parent" 356 fi 357 echo "author ` + "`" + `git var GIT_AUTHOR_IDENT` + "`" + `" 358 echo "committer ` + "`" + `git var GIT_COMMITTER_IDENT` + "`" + `" 359 echo 360 printf '%s' "$clean_message" 361 } 362 _gen_ChangeId() { 363 _gen_ChangeIdInput | 364 git hash-object -t commit --stdin 365 } 366 367 368 add_ChangeId 369 `