| 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 rename contains the implementation of the 'gorename' command |
| 6 | // whose main function is in golang.org/x/tools/cmd/gorename. |
| 7 | // See the Usage constant for the command documentation. |
| 8 | package rename // import "golang.org/x/tools/refactor/rename" |
| 9 | |
| 10 | import ( |
| 11 | "bytes" |
| 12 | "errors" |
| 13 | "fmt" |
| 14 | "go/ast" |
| 15 | "go/build" |
| 16 | "go/format" |
| 17 | "go/parser" |
| 18 | "go/token" |
| 19 | "go/types" |
| 20 | exec "golang.org/x/sys/execabs" |
| 21 | "io" |
| 22 | "io/ioutil" |
| 23 | "log" |
| 24 | "os" |
| 25 | "path" |
| 26 | "regexp" |
| 27 | "sort" |
| 28 | "strconv" |
| 29 | "strings" |
| 30 | |
| 31 | "golang.org/x/tools/go/loader" |
| 32 | "golang.org/x/tools/go/types/typeutil" |
| 33 | "golang.org/x/tools/refactor/importgraph" |
| 34 | "golang.org/x/tools/refactor/satisfy" |
| 35 | ) |
| 36 | |
| 37 | const Usage = `gorename: precise type-safe renaming of identifiers in Go source code. |
| 38 | |
| 39 | Usage: |
| 40 | |
| 41 | gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force] |
| 42 | |
| 43 | You must specify the object (named entity) to rename using the -offset |
| 44 | or -from flag. Exactly one must be specified. |
| 45 | |
| 46 | Flags: |
| 47 | |
| 48 | -offset specifies the filename and byte offset of an identifier to rename. |
| 49 | This form is intended for use by text editors. |
| 50 | |
| 51 | -from specifies the object to rename using a query notation; |
| 52 | This form is intended for interactive use at the command line. |
| 53 | A legal -from query has one of the following forms: |
| 54 | |
| 55 | "encoding/json".Decoder.Decode method of package-level named type |
| 56 | (*"encoding/json".Decoder).Decode ditto, alternative syntax |
| 57 | "encoding/json".Decoder.buf field of package-level named struct type |
| 58 | "encoding/json".HTMLEscape package member (const, func, var, type) |
| 59 | "encoding/json".Decoder.Decode::x local object x within a method |
| 60 | "encoding/json".HTMLEscape::x local object x within a function |
| 61 | "encoding/json"::x object x anywhere within a package |
| 62 | json.go::x object x within file json.go |
| 63 | |
| 64 | Double-quotes must be escaped when writing a shell command. |
| 65 | Quotes may be omitted for single-segment import paths such as "fmt". |
| 66 | |
| 67 | For methods, the parens and '*' on the receiver type are both |
| 68 | optional. |
| 69 | |
| 70 | It is an error if one of the ::x queries matches multiple |
| 71 | objects. |
| 72 | |
| 73 | -to the new name. |
| 74 | |
| 75 | -force causes the renaming to proceed even if conflicts were reported. |
| 76 | The resulting program may be ill-formed, or experience a change |
| 77 | in behaviour. |
| 78 | |
| 79 | WARNING: this flag may even cause the renaming tool to crash. |
| 80 | (In due course this bug will be fixed by moving certain |
| 81 | analyses into the type-checker.) |
| 82 | |
| 83 | -d display diffs instead of rewriting files |
| 84 | |
| 85 | -v enables verbose logging. |
| 86 | |
| 87 | gorename automatically computes the set of packages that might be |
| 88 | affected. For a local renaming, this is just the package specified by |
| 89 | -from or -offset, but for a potentially exported name, gorename scans |
| 90 | the workspace ($GOROOT and $GOPATH). |
| 91 | |
| 92 | gorename rejects renamings of concrete methods that would change the |
| 93 | assignability relation between types and interfaces. If the interface |
| 94 | change was intentional, initiate the renaming at the interface method. |
| 95 | |
| 96 | gorename rejects any renaming that would create a conflict at the point |
| 97 | of declaration, or a reference conflict (ambiguity or shadowing), or |
| 98 | anything else that could cause the resulting program not to compile. |
| 99 | |
| 100 | |
| 101 | Examples: |
| 102 | |
| 103 | $ gorename -offset file.go:#123 -to foo |
| 104 | |
| 105 | Rename the object whose identifier is at byte offset 123 within file file.go. |
| 106 | |
| 107 | $ gorename -from '"bytes".Buffer.Len' -to Size |
| 108 | |
| 109 | Rename the "Len" method of the *bytes.Buffer type to "Size". |
| 110 | ` |
| 111 | |
| 112 | // ---- TODO ---- |
| 113 | |
| 114 | // Correctness: |
| 115 | // - handle dot imports correctly |
| 116 | // - document limitations (reflection, 'implements' algorithm). |
| 117 | // - sketch a proof of exhaustiveness. |
| 118 | |
| 119 | // Features: |
| 120 | // - support running on packages specified as *.go files on the command line |
| 121 | // - support running on programs containing errors (loader.Config.AllowErrors) |
| 122 | // - allow users to specify a scope other than "global" (to avoid being |
| 123 | // stuck by neglected packages in $GOPATH that don't build). |
| 124 | // - support renaming the package clause (no object) |
| 125 | // - support renaming an import path (no ident or object) |
| 126 | // (requires filesystem + SCM updates). |
| 127 | // - detect and reject edits to autogenerated files (cgo, protobufs) |
| 128 | // and optionally $GOROOT packages. |
| 129 | // - report all conflicts, or at least all qualitatively distinct ones. |
| 130 | // Sometimes we stop to avoid redundancy, but |
| 131 | // it may give a disproportionate sense of safety in -force mode. |
| 132 | // - support renaming all instances of a pattern, e.g. |
| 133 | // all receiver vars of a given type, |
| 134 | // all local variables of a given type, |
| 135 | // all PkgNames for a given package. |
| 136 | // - emit JSON output for other editors and tools. |
| 137 | |
| 138 | var ( |
| 139 | // Force enables patching of the source files even if conflicts were reported. |
| 140 | // The resulting program may be ill-formed. |
| 141 | // It may even cause gorename to crash. TODO(adonovan): fix that. |
| 142 | Force bool |
| 143 | |
| 144 | // Diff causes the tool to display diffs instead of rewriting files. |
| 145 | Diff bool |
| 146 | |
| 147 | // DiffCmd specifies the diff command used by the -d feature. |
| 148 | // (The command must accept a -u flag and two filename arguments.) |
| 149 | DiffCmd = "diff" |
| 150 | |
| 151 | // ConflictError is returned by Main when it aborts the renaming due to conflicts. |
| 152 | // (It is distinguished because the interesting errors are the conflicts themselves.) |
| 153 | ConflictError = errors.New("renaming aborted due to conflicts") |
| 154 | |
| 155 | // Verbose enables extra logging. |
| 156 | Verbose bool |
| 157 | ) |
| 158 | |
| 159 | var stdout io.Writer = os.Stdout |
| 160 | |
| 161 | type renamer struct { |
| 162 | iprog *loader.Program |
| 163 | objsToUpdate map[types.Object]bool |
| 164 | hadConflicts bool |
| 165 | from, to string |
| 166 | satisfyConstraints map[satisfy.Constraint]bool |
| 167 | packages map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect |
| 168 | msets typeutil.MethodSetCache |
| 169 | changeMethods bool |
| 170 | } |
| 171 | |
| 172 | var reportError = func(posn token.Position, message string) { |
| 173 | fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message) |
| 174 | } |
| 175 | |
| 176 | // importName renames imports of fromPath within the package specified by info. |
| 177 | // If fromName is not empty, importName renames only imports as fromName. |
| 178 | // If the renaming would lead to a conflict, the file is left unchanged. |
| 179 | func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error { |
| 180 | if fromName == to { |
| 181 | return nil // no-op (e.g. rename x/foo to y/foo) |
| 182 | } |
| 183 | for _, f := range info.Files { |
| 184 | var from types.Object |
| 185 | for _, imp := range f.Imports { |
| 186 | importPath, _ := strconv.Unquote(imp.Path.Value) |
| 187 | importName := path.Base(importPath) |
| 188 | if imp.Name != nil { |
| 189 | importName = imp.Name.Name |
| 190 | } |
| 191 | if importPath == fromPath && (fromName == "" || importName == fromName) { |
| 192 | from = info.Implicits[imp] |
| 193 | break |
| 194 | } |
| 195 | } |
| 196 | if from == nil { |
| 197 | continue |
| 198 | } |
| 199 | r := renamer{ |
| 200 | iprog: iprog, |
| 201 | objsToUpdate: make(map[types.Object]bool), |
| 202 | to: to, |
| 203 | packages: map[*types.Package]*loader.PackageInfo{info.Pkg: info}, |
| 204 | } |
| 205 | r.check(from) |
| 206 | if r.hadConflicts { |
| 207 | reportError(iprog.Fset.Position(f.Imports[0].Pos()), |
| 208 | "skipping update of this file") |
| 209 | continue // ignore errors; leave the existing name |
| 210 | } |
| 211 | if err := r.update(); err != nil { |
| 212 | return err |
| 213 | } |
| 214 | } |
| 215 | return nil |
| 216 | } |
| 217 | |
| 218 | func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error { |
| 219 | // -- Parse the -from or -offset specifier ---------------------------- |
| 220 | |
| 221 | if (offsetFlag == "") == (fromFlag == "") { |
| 222 | return fmt.Errorf("exactly one of the -from and -offset flags must be specified") |
| 223 | } |
| 224 | |
| 225 | if !isValidIdentifier(to) { |
| 226 | return fmt.Errorf("-to %q: not a valid identifier", to) |
| 227 | } |
| 228 | |
| 229 | if Diff { |
| 230 | defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile) |
| 231 | writeFile = diff |
| 232 | } |
| 233 | |
| 234 | var spec *spec |
| 235 | var err error |
| 236 | if fromFlag != "" { |
| 237 | spec, err = parseFromFlag(ctxt, fromFlag) |
| 238 | } else { |
| 239 | spec, err = parseOffsetFlag(ctxt, offsetFlag) |
| 240 | } |
| 241 | if err != nil { |
| 242 | return err |
| 243 | } |
| 244 | |
| 245 | if spec.fromName == to { |
| 246 | return fmt.Errorf("the old and new names are the same: %s", to) |
| 247 | } |
| 248 | |
| 249 | // -- Load the program consisting of the initial package ------------- |
| 250 | |
| 251 | iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true}) |
| 252 | if err != nil { |
| 253 | return err |
| 254 | } |
| 255 | |
| 256 | fromObjects, err := findFromObjects(iprog, spec) |
| 257 | if err != nil { |
| 258 | return err |
| 259 | } |
| 260 | |
| 261 | // -- Load a larger program, for global renamings --------------------- |
| 262 | |
| 263 | if requiresGlobalRename(fromObjects, to) { |
| 264 | // For a local refactoring, we needn't load more |
| 265 | // packages, but if the renaming affects the package's |
| 266 | // API, we we must load all packages that depend on the |
| 267 | // package defining the object, plus their tests. |
| 268 | |
| 269 | if Verbose { |
| 270 | log.Print("Potentially global renaming; scanning workspace...") |
| 271 | } |
| 272 | |
| 273 | // Scan the workspace and build the import graph. |
| 274 | _, rev, errors := importgraph.Build(ctxt) |
| 275 | if len(errors) > 0 { |
| 276 | // With a large GOPATH tree, errors are inevitable. |
| 277 | // Report them but proceed. |
| 278 | fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n") |
| 279 | for path, err := range errors { |
| 280 | fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err) |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | // Enumerate the set of potentially affected packages. |
| 285 | affectedPackages := make(map[string]bool) |
| 286 | for _, obj := range fromObjects { |
| 287 | // External test packages are never imported, |
| 288 | // so they will never appear in the graph. |
| 289 | for path := range rev.Search(obj.Pkg().Path()) { |
| 290 | affectedPackages[path] = true |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | // TODO(adonovan): allow the user to specify the scope, |
| 295 | // or -ignore patterns? Computing the scope when we |
| 296 | // don't (yet) support inputs containing errors can make |
| 297 | // the tool rather brittle. |
| 298 | |
| 299 | // Re-load the larger program. |
| 300 | iprog, err = loadProgram(ctxt, affectedPackages) |
| 301 | if err != nil { |
| 302 | return err |
| 303 | } |
| 304 | |
| 305 | fromObjects, err = findFromObjects(iprog, spec) |
| 306 | if err != nil { |
| 307 | return err |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | // -- Do the renaming ------------------------------------------------- |
| 312 | |
| 313 | r := renamer{ |
| 314 | iprog: iprog, |
| 315 | objsToUpdate: make(map[types.Object]bool), |
| 316 | from: spec.fromName, |
| 317 | to: to, |
| 318 | packages: make(map[*types.Package]*loader.PackageInfo), |
| 319 | } |
| 320 | |
| 321 | // A renaming initiated at an interface method indicates the |
| 322 | // intention to rename abstract and concrete methods as needed |
| 323 | // to preserve assignability. |
| 324 | for _, obj := range fromObjects { |
| 325 | if obj, ok := obj.(*types.Func); ok { |
| 326 | recv := obj.Type().(*types.Signature).Recv() |
| 327 | if recv != nil && isInterface(recv.Type().Underlying()) { |
| 328 | r.changeMethods = true |
| 329 | break |
| 330 | } |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | // Only the initially imported packages (iprog.Imported) and |
| 335 | // their external tests (iprog.Created) should be inspected or |
| 336 | // modified, as only they have type-checked functions bodies. |
| 337 | // The rest are just dependencies, needed only for package-level |
| 338 | // type information. |
| 339 | for _, info := range iprog.Imported { |
| 340 | r.packages[info.Pkg] = info |
| 341 | } |
| 342 | for _, info := range iprog.Created { // (tests) |
| 343 | r.packages[info.Pkg] = info |
| 344 | } |
| 345 | |
| 346 | for _, from := range fromObjects { |
| 347 | r.check(from) |
| 348 | } |
| 349 | if r.hadConflicts && !Force { |
| 350 | return ConflictError |
| 351 | } |
| 352 | return r.update() |
| 353 | } |
| 354 | |
| 355 | // loadProgram loads the specified set of packages (plus their tests) |
| 356 | // and all their dependencies, from source, through the specified build |
| 357 | // context. Only packages in pkgs will have their functions bodies typechecked. |
| 358 | func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) { |
| 359 | conf := loader.Config{ |
| 360 | Build: ctxt, |
| 361 | ParserMode: parser.ParseComments, |
| 362 | |
| 363 | // TODO(adonovan): enable this. Requires making a lot of code more robust! |
| 364 | AllowErrors: false, |
| 365 | } |
| 366 | // Optimization: don't type-check the bodies of functions in our |
| 367 | // dependencies, since we only need exported package members. |
| 368 | conf.TypeCheckFuncBodies = func(p string) bool { |
| 369 | return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")] |
| 370 | } |
| 371 | |
| 372 | if Verbose { |
| 373 | var list []string |
| 374 | for pkg := range pkgs { |
| 375 | list = append(list, pkg) |
| 376 | } |
| 377 | sort.Strings(list) |
| 378 | for _, pkg := range list { |
| 379 | log.Printf("Loading package: %s", pkg) |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | for pkg := range pkgs { |
| 384 | conf.ImportWithTests(pkg) |
| 385 | } |
| 386 | |
| 387 | // Ideally we would just return conf.Load() here, but go/types |
| 388 | // reports certain "soft" errors that gc does not (Go issue 14596). |
| 389 | // As a workaround, we set AllowErrors=true and then duplicate |
| 390 | // the loader's error checking but allow soft errors. |
| 391 | // It would be nice if the loader API permitted "AllowErrors: soft". |
| 392 | conf.AllowErrors = true |
| 393 | prog, err := conf.Load() |
| 394 | if err != nil { |
| 395 | return nil, err |
| 396 | } |
| 397 | |
| 398 | var errpkgs []string |
| 399 | // Report hard errors in indirectly imported packages. |
| 400 | for _, info := range prog.AllPackages { |
| 401 | if containsHardErrors(info.Errors) { |
| 402 | errpkgs = append(errpkgs, info.Pkg.Path()) |
| 403 | } |
| 404 | } |
| 405 | if errpkgs != nil { |
| 406 | var more string |
| 407 | if len(errpkgs) > 3 { |
| 408 | more = fmt.Sprintf(" and %d more", len(errpkgs)-3) |
| 409 | errpkgs = errpkgs[:3] |
| 410 | } |
| 411 | return nil, fmt.Errorf("couldn't load packages due to errors: %s%s", |
| 412 | strings.Join(errpkgs, ", "), more) |
| 413 | } |
| 414 | return prog, nil |
| 415 | } |
| 416 | |
| 417 | func containsHardErrors(errors []error) bool { |
| 418 | for _, err := range errors { |
| 419 | if err, ok := err.(types.Error); ok && err.Soft { |
| 420 | continue |
| 421 | } |
| 422 | return true |
| 423 | } |
| 424 | return false |
| 425 | } |
| 426 | |
| 427 | // requiresGlobalRename reports whether this renaming could potentially |
| 428 | // affect other packages in the Go workspace. |
| 429 | func requiresGlobalRename(fromObjects []types.Object, to string) bool { |
| 430 | var tfm bool |
| 431 | for _, from := range fromObjects { |
| 432 | if from.Exported() { |
| 433 | return true |
| 434 | } |
| 435 | switch objectKind(from) { |
| 436 | case "type", "field", "method": |
| 437 | tfm = true |
| 438 | } |
| 439 | } |
| 440 | if ast.IsExported(to) && tfm { |
| 441 | // A global renaming may be necessary even if we're |
| 442 | // exporting a previous unexported name, since if it's |
| 443 | // the name of a type, field or method, this could |
| 444 | // change selections in other packages. |
| 445 | // (We include "type" in this list because a type |
| 446 | // used as an embedded struct field entails a field |
| 447 | // renaming.) |
| 448 | return true |
| 449 | } |
| 450 | return false |
| 451 | } |
| 452 | |
| 453 | // update updates the input files. |
| 454 | func (r *renamer) update() error { |
| 455 | // We use token.File, not filename, since a file may appear to |
| 456 | // belong to multiple packages and be parsed more than once. |
| 457 | // token.File captures this distinction; filename does not. |
| 458 | |
| 459 | var nidents int |
| 460 | var filesToUpdate = make(map[*token.File]bool) |
| 461 | docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) |
| 462 | for _, info := range r.packages { |
| 463 | // Mutate the ASTs and note the filenames. |
| 464 | for id, obj := range info.Defs { |
| 465 | if r.objsToUpdate[obj] { |
| 466 | nidents++ |
| 467 | id.Name = r.to |
| 468 | filesToUpdate[r.iprog.Fset.File(id.Pos())] = true |
| 469 | // Perform the rename in doc comments too. |
| 470 | if doc := r.docComment(id); doc != nil { |
| 471 | for _, comment := range doc.List { |
| 472 | comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to) |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | for id, obj := range info.Uses { |
| 479 | if r.objsToUpdate[obj] { |
| 480 | nidents++ |
| 481 | id.Name = r.to |
| 482 | filesToUpdate[r.iprog.Fset.File(id.Pos())] = true |
| 483 | } |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | // Renaming not supported if cgo files are affected. |
| 488 | var generatedFileNames []string |
| 489 | for _, info := range r.packages { |
| 490 | for _, f := range info.Files { |
| 491 | tokenFile := r.iprog.Fset.File(f.Pos()) |
| 492 | if filesToUpdate[tokenFile] && generated(f, tokenFile) { |
| 493 | generatedFileNames = append(generatedFileNames, tokenFile.Name()) |
| 494 | } |
| 495 | } |
| 496 | } |
| 497 | if !Force && len(generatedFileNames) > 0 { |
| 498 | return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames) |
| 499 | } |
| 500 | |
| 501 | // Write affected files. |
| 502 | var nerrs, npkgs int |
| 503 | for _, info := range r.packages { |
| 504 | first := true |
| 505 | for _, f := range info.Files { |
| 506 | tokenFile := r.iprog.Fset.File(f.Pos()) |
| 507 | if filesToUpdate[tokenFile] { |
| 508 | if first { |
| 509 | npkgs++ |
| 510 | first = false |
| 511 | if Verbose { |
| 512 | log.Printf("Updating package %s", info.Pkg.Path()) |
| 513 | } |
| 514 | } |
| 515 | |
| 516 | filename := tokenFile.Name() |
| 517 | var buf bytes.Buffer |
| 518 | if err := format.Node(&buf, r.iprog.Fset, f); err != nil { |
| 519 | log.Printf("failed to pretty-print syntax tree: %v", err) |
| 520 | nerrs++ |
| 521 | continue |
| 522 | } |
| 523 | if err := writeFile(filename, buf.Bytes()); err != nil { |
| 524 | log.Print(err) |
| 525 | nerrs++ |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | } |
| 530 | if !Diff { |
| 531 | fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n", |
| 532 | nidents, plural(nidents), |
| 533 | len(filesToUpdate), plural(len(filesToUpdate)), |
| 534 | npkgs, plural(npkgs)) |
| 535 | } |
| 536 | if nerrs > 0 { |
| 537 | return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs)) |
| 538 | } |
| 539 | return nil |
| 540 | } |
| 541 | |
| 542 | // docComment returns the doc for an identifier. |
| 543 | func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup { |
| 544 | _, nodes, _ := r.iprog.PathEnclosingInterval(id.Pos(), id.End()) |
| 545 | for _, node := range nodes { |
| 546 | switch decl := node.(type) { |
| 547 | case *ast.FuncDecl: |
| 548 | return decl.Doc |
| 549 | case *ast.Field: |
| 550 | return decl.Doc |
| 551 | case *ast.GenDecl: |
| 552 | return decl.Doc |
| 553 | // For {Type,Value}Spec, if the doc on the spec is absent, |
| 554 | // search for the enclosing GenDecl |
| 555 | case *ast.TypeSpec: |
| 556 | if decl.Doc != nil { |
| 557 | return decl.Doc |
| 558 | } |
| 559 | case *ast.ValueSpec: |
| 560 | if decl.Doc != nil { |
| 561 | return decl.Doc |
| 562 | } |
| 563 | case *ast.Ident: |
| 564 | default: |
| 565 | return nil |
| 566 | } |
| 567 | } |
| 568 | return nil |
| 569 | } |
| 570 | |
| 571 | func plural(n int) string { |
| 572 | if n != 1 { |
| 573 | return "s" |
| 574 | } |
| 575 | return "" |
| 576 | } |
| 577 | |
| 578 | // writeFile is a seam for testing and for the -d flag. |
| 579 | var writeFile = reallyWriteFile |
| 580 | |
| 581 | func reallyWriteFile(filename string, content []byte) error { |
| 582 | return ioutil.WriteFile(filename, content, 0644) |
| 583 | } |
| 584 | |
| 585 | func diff(filename string, content []byte) error { |
| 586 | renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid()) |
| 587 | if err := ioutil.WriteFile(renamed, content, 0644); err != nil { |
| 588 | return err |
| 589 | } |
| 590 | defer os.Remove(renamed) |
| 591 | |
| 592 | diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput() |
| 593 | if len(diff) > 0 { |
| 594 | // diff exits with a non-zero status when the files don't match. |
| 595 | // Ignore that failure as long as we get output. |
| 596 | stdout.Write(diff) |
| 597 | return nil |
| 598 | } |
| 599 | if err != nil { |
| 600 | return fmt.Errorf("computing diff: %v", err) |
| 601 | } |
| 602 | return nil |
| 603 | } |
| 604 |
Members