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  `