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 }