github.com/nikron/prototool@v1.3.0/internal/diff/diff.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  // Primarily adapted from https://github.com/golang/go/blob/e86168430f0aab8f971763e4b00c2aae7bec55f0/src/cmd/gofmt/gofmt.go.
    22  // Copyright 2009 The Go Authors. All rights reserved.
    23  // Use of this source code is governed by a BSD-style
    24  // license that can be found in the LICENSE file.
    25  
    26  package diff
    27  
    28  import (
    29  	"bytes"
    30  	"fmt"
    31  	"io/ioutil"
    32  	"os"
    33  	"os/exec"
    34  	"path/filepath"
    35  	"runtime"
    36  )
    37  
    38  // Do does a diff between an input and output.
    39  func Do(input []byte, output []byte, filename string) ([]byte, error) {
    40  	f1, err := writeTempFile("", "prototool-diff", input)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	defer func() { _ = os.Remove(f1) }()
    45  
    46  	f2, err := writeTempFile("", "prototool-diff", output)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	defer func() { _ = os.Remove(f2) }()
    51  
    52  	cmd := "diff"
    53  	if runtime.GOOS == "plan9" {
    54  		cmd = "/bin/ape/diff"
    55  	}
    56  
    57  	data, err := exec.Command(cmd, "-u", f1, f2).CombinedOutput()
    58  	if len(data) > 0 {
    59  		// diff exits with a non-zero status when the files don't match.
    60  		// Ignore that failure as long as we get output.
    61  		return replaceTempFilename(data, filename)
    62  	}
    63  	return nil, err
    64  }
    65  
    66  func writeTempFile(dir, prefix string, data []byte) (string, error) {
    67  	file, err := ioutil.TempFile(dir, prefix)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	_, err = file.Write(data)
    72  	if err1 := file.Close(); err == nil {
    73  		err = err1
    74  	}
    75  	if err != nil {
    76  		_ = os.Remove(file.Name())
    77  		return "", err
    78  	}
    79  	return file.Name(), nil
    80  }
    81  
    82  // replaceTempFilename replaces temporary filenames in diff with actual one.
    83  //
    84  //   --- /tmp/gofmt316145376	2017-02-03 19:13:00.280468375 -0500
    85  //   +++ /tmp/gofmt617882815	2017-02-03 19:13:00.280468375 -0500
    86  //   ...
    87  //   ->
    88  //   --- path/to/file.go.orig	2017-02-03 19:13:00.280468375 -0500
    89  //   +++ path/to/file.go	2017-02-03 19:13:00.280468375 -0500
    90  //   ...
    91  func replaceTempFilename(diff []byte, filename string) ([]byte, error) {
    92  	bs := bytes.SplitN(diff, []byte{'\n'}, 3)
    93  	if len(bs) < 3 {
    94  		return nil, fmt.Errorf("got unexpected diff for %s", filename)
    95  	}
    96  	// Preserve timestamps.
    97  	var t0, t1 []byte
    98  	if i := bytes.LastIndexByte(bs[0], '\t'); i != -1 {
    99  		t0 = bs[0][i:]
   100  	}
   101  	if i := bytes.LastIndexByte(bs[1], '\t'); i != -1 {
   102  		t1 = bs[1][i:]
   103  	}
   104  	// Always print filepath with slash separator.
   105  	f := filepath.ToSlash(filename)
   106  	bs[0] = []byte(fmt.Sprintf("--- %s%s", f+".orig", t0))
   107  	bs[1] = []byte(fmt.Sprintf("+++ %s%s", f, t1))
   108  	return bytes.Join(bs, []byte{'\n'}), nil
   109  }