github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/git-codereview/gofmt.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  	"flag"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  )
    17  
    18  var gofmtList bool
    19  
    20  func cmdGofmt(args []string) {
    21  	flags.BoolVar(&gofmtList, "l", false, "list files that need to be formatted")
    22  	flags.Parse(args)
    23  	if len(flag.Args()) > 0 {
    24  		fmt.Fprintf(stderr(), "Usage: %s gofmt %s [-l]\n", os.Args[0], globalFlags)
    25  		os.Exit(2)
    26  	}
    27  
    28  	f := gofmtCommand
    29  	if !gofmtList {
    30  		f |= gofmtWrite
    31  	}
    32  
    33  	files, stderr := runGofmt(f)
    34  	if gofmtList {
    35  		w := stdout()
    36  		for _, file := range files {
    37  			fmt.Fprintf(w, "%s\n", file)
    38  		}
    39  	}
    40  	if stderr != "" {
    41  		dief("gofmt reported errors:\n\t%s", strings.Replace(strings.TrimSpace(stderr), "\n", "\n\t", -1))
    42  	}
    43  }
    44  
    45  const (
    46  	gofmtPreCommit = 1 << iota
    47  	gofmtCommand
    48  	gofmtWrite
    49  )
    50  
    51  // runGofmt runs the external gofmt command over modified files.
    52  //
    53  // The definition of "modified files" depends on the bit flags.
    54  // If gofmtPreCommit is set, then runGofmt considers *.go files that
    55  // differ between the index (staging area) and the branchpoint
    56  // (the latest commit before the branch diverged from upstream).
    57  // If gofmtCommand is set, then runGofmt considers all those files
    58  // in addition to files with unstaged modifications.
    59  // It never considers untracked files.
    60  //
    61  // As a special case for the main repo (but applied everywhere)
    62  // *.go files under a top-level test directory are excluded from the
    63  // formatting requirement, except run.go and those in test/bench/.
    64  //
    65  // If gofmtWrite is set (only with gofmtCommand, meaning this is 'git gofmt'),
    66  // runGofmt replaces the original files with their formatted equivalents.
    67  // Git makes this difficult. In general the file in the working tree
    68  // (the local file system) can have unstaged changes that make it different
    69  // from the equivalent file in the index. To help pass the precommit hook,
    70  // 'git gofmt'  must make it easy to update the files in the index.
    71  // One option is to run gofmt on all the files of the same name in the
    72  // working tree and leave it to the user to sort out what should be staged
    73  // back into the index. Another is to refuse to reformat files for which
    74  // different versions exist in the index vs the working tree. Both of these
    75  // options are unsatisfying: they foist busy work onto the user,
    76  // and it's exactly the kind of busy work that a program is best for.
    77  // Instead, when runGofmt finds files in the index that need
    78  // reformatting, it reformats them there, bypassing the working tree.
    79  // It also reformats files in the working tree that need reformatting.
    80  // For both, only files modified since the branchpoint are considered.
    81  // The result should be that both index and working tree get formatted
    82  // correctly and diffs between the two remain meaningful (free of
    83  // formatting distractions). Modifying files in the index directly may
    84  // surprise Git users, but it seems the best of a set of bad choices, and
    85  // of course those users can choose not to use 'git gofmt'.
    86  // This is a bit more work than the other git commands do, which is
    87  // a little worrying, but the choice being made has the nice property
    88  // that if 'git gofmt' is interrupted, a second 'git gofmt' will put things into
    89  // the same state the first would have.
    90  //
    91  // runGofmt returns a list of files that need (or needed) reformatting.
    92  // If gofmtPreCommit is set, the names always refer to files in the index.
    93  // If gofmtCommand is set, then a name without a suffix (see below)
    94  // refers to both the copy in the index and the copy in the working tree
    95  // and implies that the two copies are identical. Otherwise, in the case
    96  // that the index and working tree differ, the file name will have an explicit
    97  // " (staged)" or " (unstaged)" suffix saying which is meant.
    98  //
    99  // runGofmt also returns any standard error output from gofmt,
   100  // usually indicating syntax errors in the Go source files.
   101  // If gofmtCommand is set, syntax errors in index files that do not match
   102  // the working tree show a " (staged)" suffix after the file name.
   103  // The errors never use the " (unstaged)" suffix, in order to keep
   104  // references to the local file system in the standard file:line form.
   105  func runGofmt(flags int) (files []string, stderrText string) {
   106  	pwd, err := os.Getwd()
   107  	if err != nil {
   108  		dief("%v", err)
   109  	}
   110  	pwd = filepath.Clean(pwd) // convert to host \ syntax
   111  	if !strings.HasSuffix(pwd, string(filepath.Separator)) {
   112  		pwd += string(filepath.Separator)
   113  	}
   114  
   115  	b := CurrentBranch()
   116  	repo := repoRoot()
   117  	if !strings.HasSuffix(repo, string(filepath.Separator)) {
   118  		repo += string(filepath.Separator)
   119  	}
   120  
   121  	// Find files modified in the index compared to the branchpoint.
   122  	branchpt := b.Branchpoint()
   123  	if strings.Contains(cmdOutput("git", "branch", "-r", "--contains", b.FullName()), "origin/") {
   124  		// This is a branch tag move, not an actual change.
   125  		// Use HEAD as branch point, so nothing will appear changed.
   126  		// We don't want to think about gofmt on already published
   127  		// commits.
   128  		branchpt = "HEAD"
   129  	}
   130  	indexFiles := addRoot(repo, filter(gofmtRequired, nonBlankLines(cmdOutput("git", "diff", "--name-only", "--diff-filter=ACM", "--cached", branchpt, "--"))))
   131  	localFiles := addRoot(repo, filter(gofmtRequired, nonBlankLines(cmdOutput("git", "diff", "--name-only", "--diff-filter=ACM"))))
   132  	localFilesMap := stringMap(localFiles)
   133  	isUnstaged := func(file string) bool {
   134  		return localFilesMap[file]
   135  	}
   136  
   137  	if len(indexFiles) == 0 && ((flags&gofmtCommand) == 0 || len(localFiles) == 0) {
   138  		return
   139  	}
   140  
   141  	// Determine which files have unstaged changes and are therefore
   142  	// different from their index versions. For those, the index version must
   143  	// be copied into a temporary file in the local file system.
   144  	needTemp := filter(isUnstaged, indexFiles)
   145  
   146  	// Map between temporary file name and place in file tree where
   147  	// file would be checked out (if not for the unstaged changes).
   148  	tempToFile := map[string]string{}
   149  	fileToTemp := map[string]string{}
   150  	cleanup := func() {} // call before dying (defer won't run)
   151  	if len(needTemp) > 0 {
   152  		// Ask Git to copy the index versions into temporary files.
   153  		// Git stores the temporary files, named .merge_*, in the repo root.
   154  		// Unlike the Git commands above, the non-temp file names printed
   155  		// here are relative to the current directory, not the repo root.
   156  
   157  		// git checkout-index --temp is broken on windows. Running this command:
   158  		//
   159  		// git checkout-index --temp -- bad-bad-bad2.go bad-bad-broken.go bad-bad-good.go bad-bad2-bad.go bad-bad2-broken.go bad-bad2-good.go bad-broken-bad.go bad-broken-bad2.go bad-broken-good.go bad-good-bad.go bad-good-bad2.go bad-good-broken.go bad2-bad-bad2.go bad2-bad-broken.go bad2-bad-good.go bad2-bad2-bad.go bad2-bad2-broken.go bad2-bad2-good.go bad2-broken-bad.go bad2-broken-bad2.go bad2-broken-good.go bad2-good-bad.go bad2-good-bad2.go bad2-good-broken.go broken-bad-bad2.go broken-bad-broken.go broken-bad-good.go broken-bad2-bad.go broken-bad2-broken.go broken-bad2-good.go
   160  		//
   161  		// produces this output
   162  		//
   163  		// .merge_file_a05448      bad-bad-bad2.go
   164  		// .merge_file_b05448      bad-bad-broken.go
   165  		// .merge_file_c05448      bad-bad-good.go
   166  		// .merge_file_d05448      bad-bad2-bad.go
   167  		// .merge_file_e05448      bad-bad2-broken.go
   168  		// .merge_file_f05448      bad-bad2-good.go
   169  		// .merge_file_g05448      bad-broken-bad.go
   170  		// .merge_file_h05448      bad-broken-bad2.go
   171  		// .merge_file_i05448      bad-broken-good.go
   172  		// .merge_file_j05448      bad-good-bad.go
   173  		// .merge_file_k05448      bad-good-bad2.go
   174  		// .merge_file_l05448      bad-good-broken.go
   175  		// .merge_file_m05448      bad2-bad-bad2.go
   176  		// .merge_file_n05448      bad2-bad-broken.go
   177  		// .merge_file_o05448      bad2-bad-good.go
   178  		// .merge_file_p05448      bad2-bad2-bad.go
   179  		// .merge_file_q05448      bad2-bad2-broken.go
   180  		// .merge_file_r05448      bad2-bad2-good.go
   181  		// .merge_file_s05448      bad2-broken-bad.go
   182  		// .merge_file_t05448      bad2-broken-bad2.go
   183  		// .merge_file_u05448      bad2-broken-good.go
   184  		// .merge_file_v05448      bad2-good-bad.go
   185  		// .merge_file_w05448      bad2-good-bad2.go
   186  		// .merge_file_x05448      bad2-good-broken.go
   187  		// .merge_file_y05448      broken-bad-bad2.go
   188  		// .merge_file_z05448      broken-bad-broken.go
   189  		// error: unable to create file .merge_file_XXXXXX (No error)
   190  		// .merge_file_XXXXXX      broken-bad-good.go
   191  		// error: unable to create file .merge_file_XXXXXX (No error)
   192  		// .merge_file_XXXXXX      broken-bad2-bad.go
   193  		// error: unable to create file .merge_file_XXXXXX (No error)
   194  		// .merge_file_XXXXXX      broken-bad2-broken.go
   195  		// error: unable to create file .merge_file_XXXXXX (No error)
   196  		// .merge_file_XXXXXX      broken-bad2-good.go
   197  		//
   198  		// so limit the number of file arguments to 25.
   199  		for len(needTemp) > 0 {
   200  			n := len(needTemp)
   201  			if n > 25 {
   202  				n = 25
   203  			}
   204  			args := []string{"checkout-index", "--temp", "--"}
   205  			args = append(args, needTemp[:n]...)
   206  			// Until Git 2.3.0, git checkout-index --temp is broken if not run in the repo root.
   207  			// Work around by running in the repo root.
   208  			// http://article.gmane.org/gmane.comp.version-control.git/261739
   209  			// https://github.com/git/git/commit/74c4de5
   210  			for _, line := range nonBlankLines(cmdOutputDir(repo, "git", args...)) {
   211  				i := strings.Index(line, "\t")
   212  				if i < 0 {
   213  					continue
   214  				}
   215  				temp, file := line[:i], line[i+1:]
   216  				temp = filepath.Join(repo, temp)
   217  				file = filepath.Join(repo, file)
   218  				tempToFile[temp] = file
   219  				fileToTemp[file] = temp
   220  			}
   221  			needTemp = needTemp[n:]
   222  		}
   223  		cleanup = func() {
   224  			for temp := range tempToFile {
   225  				os.Remove(temp)
   226  			}
   227  			tempToFile = nil
   228  		}
   229  		defer cleanup()
   230  	}
   231  	dief := func(format string, args ...interface{}) {
   232  		cleanup()
   233  		dief(format, args...) // calling top-level dief function
   234  	}
   235  
   236  	// Run gofmt to find out which files need reformatting;
   237  	// if gofmtWrite is set, reformat them in place.
   238  	// For references to local files, remove leading pwd if present
   239  	// to make relative to current directory.
   240  	// Temp files and local-only files stay as absolute paths for easy matching in output.
   241  	args := []string{"-l"}
   242  	if flags&gofmtWrite != 0 {
   243  		args = append(args, "-w")
   244  	}
   245  	for _, file := range indexFiles {
   246  		if isUnstaged(file) {
   247  			args = append(args, fileToTemp[file])
   248  		} else {
   249  			args = append(args, strings.TrimPrefix(file, pwd))
   250  		}
   251  	}
   252  	if flags&gofmtCommand != 0 {
   253  		for _, file := range localFiles {
   254  			args = append(args, file)
   255  		}
   256  	}
   257  
   258  	if *verbose > 1 {
   259  		fmt.Fprintln(stderr(), commandString("gofmt", args))
   260  	}
   261  	cmd := exec.Command("gofmt", args...)
   262  	var stdout, stderr bytes.Buffer
   263  	cmd.Stdout = &stdout
   264  	cmd.Stderr = &stderr
   265  	err = cmd.Run()
   266  
   267  	if stderr.Len() == 0 && err != nil {
   268  		// Error but no stderr: usually can't find gofmt.
   269  		dief("invoking gofmt: %v", err)
   270  	}
   271  
   272  	// Build file list.
   273  	files = lines(stdout.String())
   274  
   275  	// Restage files that need to be restaged.
   276  	if flags&gofmtWrite != 0 {
   277  		add := []string{"add"}
   278  		write := []string{"hash-object", "-w", "--"}
   279  		updateIndex := []string{}
   280  		for _, file := range files {
   281  			if real := tempToFile[file]; real != "" {
   282  				write = append(write, file)
   283  				updateIndex = append(updateIndex, strings.TrimPrefix(real, repo))
   284  			} else if !isUnstaged(file) {
   285  				add = append(add, file)
   286  			}
   287  		}
   288  		if len(add) > 1 {
   289  			run("git", add...)
   290  		}
   291  		if len(updateIndex) > 0 {
   292  			hashes := nonBlankLines(cmdOutput("git", write...))
   293  			if len(hashes) != len(write)-3 {
   294  				dief("git hash-object -w did not write expected number of objects")
   295  			}
   296  			var buf bytes.Buffer
   297  			for i, name := range updateIndex {
   298  				fmt.Fprintf(&buf, "100644 %s\t%s\n", hashes[i], name)
   299  			}
   300  			verbosef("git update-index --index-info")
   301  			cmd := exec.Command("git", "update-index", "--index-info")
   302  			cmd.Stdin = &buf
   303  			out, err := cmd.CombinedOutput()
   304  			if err != nil {
   305  				dief("git update-index: %v\n%s", err, out)
   306  			}
   307  		}
   308  	}
   309  
   310  	// Remap temp files back to original names for caller.
   311  	for i, file := range files {
   312  		if real := tempToFile[file]; real != "" {
   313  			if flags&gofmtCommand != 0 {
   314  				real += " (staged)"
   315  			}
   316  			files[i] = strings.TrimPrefix(real, pwd)
   317  		} else if isUnstaged(file) {
   318  			files[i] = strings.TrimPrefix(file+" (unstaged)", pwd)
   319  		}
   320  	}
   321  
   322  	// Rewrite temp names in stderr, and shorten local file names.
   323  	// No suffix added for local file names (see comment above).
   324  	text := "\n" + stderr.String()
   325  	for temp, file := range tempToFile {
   326  		if flags&gofmtCommand != 0 {
   327  			file += " (staged)"
   328  		}
   329  		text = strings.Replace(text, "\n"+temp+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
   330  	}
   331  	for _, file := range localFiles {
   332  		text = strings.Replace(text, "\n"+file+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
   333  	}
   334  	text = text[1:]
   335  
   336  	sort.Strings(files)
   337  	return files, text
   338  }
   339  
   340  // gofmtRequired reports whether the specified file should be checked
   341  // for gofmt'dness by the pre-commit hook.
   342  // The file name is relative to the repo root.
   343  func gofmtRequired(file string) bool {
   344  	// TODO: Consider putting this policy into codereview.cfg.
   345  	if !strings.HasSuffix(file, ".go") {
   346  		return false
   347  	}
   348  	if !strings.HasPrefix(file, "test/") {
   349  		return true
   350  	}
   351  	return strings.HasPrefix(file, "test/bench/") || file == "test/run.go"
   352  }
   353  
   354  // stringMap returns a map m such that m[s] == true if s was in the original list.
   355  func stringMap(list []string) map[string]bool {
   356  	m := map[string]bool{}
   357  	for _, x := range list {
   358  		m[x] = true
   359  	}
   360  	return m
   361  }
   362  
   363  // filter returns the elements in list satisfying f.
   364  func filter(f func(string) bool, list []string) []string {
   365  	var out []string
   366  	for _, x := range list {
   367  		if f(x) {
   368  			out = append(out, x)
   369  		}
   370  	}
   371  	return out
   372  }
   373  
   374  func addRoot(root string, list []string) []string {
   375  	var out []string
   376  	for _, x := range list {
   377  		out = append(out, filepath.Join(root, x))
   378  	}
   379  	return out
   380  }