in git-codereview/gofmt.go [106:341]
func runGofmt(flags int) (files []string, stderrText string) {
pwd, err := os.Getwd()
if err != nil {
dief("%v", err)
}
pwd = filepath.Clean(pwd) // convert to host \ syntax
if !strings.HasSuffix(pwd, string(filepath.Separator)) {
pwd += string(filepath.Separator)
}
b := CurrentBranch()
repo := repoRoot()
if !strings.HasSuffix(repo, string(filepath.Separator)) {
repo += string(filepath.Separator)
}
// Find files modified in the index compared to the branchpoint.
// The default of HEAD will only compare against the most recent commit.
// But if we know the origin branch, and this isn't a branch tag move,
// then check all the pending commits.
branchpt := "HEAD"
if b.OriginBranch() != "" {
isBranchTagMove := strings.Contains(cmdOutput("git", "branch", "-r", "--contains", b.FullName()), "origin/")
if !isBranchTagMove {
branchpt = b.Branchpoint()
}
}
indexFiles := addRoot(repo, filter(gofmtRequired, nonBlankLines(cmdOutput("git", "diff", "--name-only", "--diff-filter=ACM", "--cached", branchpt, "--"))))
localFiles := addRoot(repo, filter(gofmtRequired, nonBlankLines(cmdOutput("git", "diff", "--name-only", "--diff-filter=ACM"))))
localFilesMap := stringMap(localFiles)
isUnstaged := func(file string) bool {
return localFilesMap[file]
}
if len(indexFiles) == 0 && ((flags&gofmtCommand) == 0 || len(localFiles) == 0) {
return
}
// Determine which files have unstaged changes and are therefore
// different from their index versions. For those, the index version must
// be copied into a temporary file in the local file system.
needTemp := filter(isUnstaged, indexFiles)
// Map between temporary file name and place in file tree where
// file would be checked out (if not for the unstaged changes).
tempToFile := map[string]string{}
fileToTemp := map[string]string{}
cleanup := func() {} // call before dying (defer won't run)
if len(needTemp) > 0 {
// Ask Git to copy the index versions into temporary files.
// Git stores the temporary files, named .merge_*, in the repo root.
// Unlike the Git commands above, the non-temp file names printed
// here are relative to the current directory, not the repo root.
// git checkout-index --temp is broken on windows. Running this command:
//
// 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
//
// produces this output
//
// .merge_file_a05448 bad-bad-bad2.go
// .merge_file_b05448 bad-bad-broken.go
// .merge_file_c05448 bad-bad-good.go
// .merge_file_d05448 bad-bad2-bad.go
// .merge_file_e05448 bad-bad2-broken.go
// .merge_file_f05448 bad-bad2-good.go
// .merge_file_g05448 bad-broken-bad.go
// .merge_file_h05448 bad-broken-bad2.go
// .merge_file_i05448 bad-broken-good.go
// .merge_file_j05448 bad-good-bad.go
// .merge_file_k05448 bad-good-bad2.go
// .merge_file_l05448 bad-good-broken.go
// .merge_file_m05448 bad2-bad-bad2.go
// .merge_file_n05448 bad2-bad-broken.go
// .merge_file_o05448 bad2-bad-good.go
// .merge_file_p05448 bad2-bad2-bad.go
// .merge_file_q05448 bad2-bad2-broken.go
// .merge_file_r05448 bad2-bad2-good.go
// .merge_file_s05448 bad2-broken-bad.go
// .merge_file_t05448 bad2-broken-bad2.go
// .merge_file_u05448 bad2-broken-good.go
// .merge_file_v05448 bad2-good-bad.go
// .merge_file_w05448 bad2-good-bad2.go
// .merge_file_x05448 bad2-good-broken.go
// .merge_file_y05448 broken-bad-bad2.go
// .merge_file_z05448 broken-bad-broken.go
// error: unable to create file .merge_file_XXXXXX (No error)
// .merge_file_XXXXXX broken-bad-good.go
// error: unable to create file .merge_file_XXXXXX (No error)
// .merge_file_XXXXXX broken-bad2-bad.go
// error: unable to create file .merge_file_XXXXXX (No error)
// .merge_file_XXXXXX broken-bad2-broken.go
// error: unable to create file .merge_file_XXXXXX (No error)
// .merge_file_XXXXXX broken-bad2-good.go
//
// so limit the number of file arguments to 25.
for len(needTemp) > 0 {
n := len(needTemp)
if n > 25 {
n = 25
}
args := []string{"checkout-index", "--temp", "--"}
args = append(args, needTemp[:n]...)
// Until Git 2.3.0, git checkout-index --temp is broken if not run in the repo root.
// Work around by running in the repo root.
// http://article.gmane.org/gmane.comp.version-control.git/261739
// https://github.com/git/git/commit/74c4de5
for _, line := range nonBlankLines(cmdOutputDir(repo, "git", args...)) {
i := strings.Index(line, "\t")
if i < 0 {
continue
}
temp, file := line[:i], line[i+1:]
temp = filepath.Join(repo, temp)
file = filepath.Join(repo, file)
tempToFile[temp] = file
fileToTemp[file] = temp
}
needTemp = needTemp[n:]
}
cleanup = func() {
for temp := range tempToFile {
os.Remove(temp)
}
tempToFile = nil
}
defer cleanup()
}
dief := func(format string, args ...interface{}) {
cleanup()
dief(format, args...) // calling top-level dief function
}
// Run gofmt to find out which files need reformatting;
// if gofmtWrite is set, reformat them in place.
// For references to local files, remove leading pwd if present
// to make relative to current directory.
// Temp files and local-only files stay as absolute paths for easy matching in output.
args := []string{"-l"}
if flags&gofmtWrite != 0 {
args = append(args, "-w")
}
for _, file := range indexFiles {
if isUnstaged(file) {
args = append(args, fileToTemp[file])
} else {
args = append(args, strings.TrimPrefix(file, pwd))
}
}
if flags&gofmtCommand != 0 {
for _, file := range localFiles {
args = append(args, file)
}
}
if *verbose > 1 {
fmt.Fprintln(stderr(), commandString("gofmt", args))
}
cmd := exec.Command("gofmt", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if stderr.Len() == 0 && err != nil {
// Error but no stderr: usually can't find gofmt.
dief("invoking gofmt: %v", err)
}
// Build file list.
files = lines(stdout.String())
// Restage files that need to be restaged.
if flags&gofmtWrite != 0 {
add := []string{"add"}
write := []string{"hash-object", "-w", "--"}
updateIndex := []string{}
for _, file := range files {
if real := tempToFile[file]; real != "" {
write = append(write, file)
updateIndex = append(updateIndex, strings.TrimPrefix(real, repo))
} else if !isUnstaged(file) {
add = append(add, file)
}
}
if len(add) > 1 {
run("git", add...)
}
if len(updateIndex) > 0 {
hashes := nonBlankLines(cmdOutput("git", write...))
if len(hashes) != len(write)-3 {
dief("git hash-object -w did not write expected number of objects")
}
var buf bytes.Buffer
for i, name := range updateIndex {
fmt.Fprintf(&buf, "100644 %s\t%s\n", hashes[i], name)
}
verbosef("git update-index --index-info")
cmd := exec.Command("git", "update-index", "--index-info")
cmd.Stdin = &buf
out, err := cmd.CombinedOutput()
if err != nil {
dief("git update-index: %v\n%s", err, out)
}
}
}
// Remap temp files back to original names for caller.
for i, file := range files {
if real := tempToFile[file]; real != "" {
if flags&gofmtCommand != 0 {
real += " (staged)"
}
files[i] = strings.TrimPrefix(real, pwd)
} else if isUnstaged(file) {
files[i] = strings.TrimPrefix(file+" (unstaged)", pwd)
}
}
// Rewrite temp names in stderr, and shorten local file names.
// No suffix added for local file names (see comment above).
text := "\n" + stderr.String()
for temp, file := range tempToFile {
if flags&gofmtCommand != 0 {
file += " (staged)"
}
text = strings.Replace(text, "\n"+temp+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
}
for _, file := range localFiles {
text = strings.Replace(text, "\n"+file+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
}
text = text[1:]
sort.Strings(files)
return files, text
}