github.com/rsc/tmp@v0.0.0-20240517235954-6deaab19748b/patch/patch.go (about)

     1  // Copyright 2009 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 patch implements parsing and execution of the textual and
     6  // binary patch descriptions used by version control tools such as
     7  // CVS, Git, Mercurial, and Subversion.
     8  package patch
     9  
    10  import (
    11  	"bytes"
    12  	"path"
    13  	"strings"
    14  )
    15  
    16  // A Set represents a set of patches to be applied as a single atomic unit.
    17  // Patch sets are often preceded by a descriptive header.
    18  type Set struct {
    19  	Header string // free-form text
    20  	File   []*File
    21  }
    22  
    23  // A File represents a collection of changes to be made to a single file.
    24  type File struct {
    25  	Verb             Verb
    26  	Src              string // source for Verb == Copy, Verb == Rename
    27  	Dst              string
    28  	OldMode, NewMode int // 0 indicates not used
    29  	Diff                 // changes to data; == NoDiff if operation does not edit file
    30  }
    31  
    32  // A Verb is an action performed on a file.
    33  type Verb string
    34  
    35  const (
    36  	Add    Verb = "add"
    37  	Copy   Verb = "copy"
    38  	Delete Verb = "delete"
    39  	Edit   Verb = "edit"
    40  	Rename Verb = "rename"
    41  )
    42  
    43  // A Diff is any object that describes changes to transform
    44  // an old byte stream to a new one.
    45  type Diff interface {
    46  	// Apply applies the changes listed in the diff
    47  	// to the string s, returning the new version of the string.
    48  	// Note that the string s need not be a text string.
    49  	Apply(old []byte) (new []byte, err error)
    50  }
    51  
    52  // NoDiff is a no-op Diff implementation: it passes the
    53  // old data through unchanged.
    54  var NoDiff Diff = noDiffType(0)
    55  
    56  type noDiffType int
    57  
    58  func (noDiffType) Apply(old []byte) ([]byte, error) {
    59  	return old, nil
    60  }
    61  
    62  // A SyntaxError represents a syntax error encountered while parsing a patch.
    63  type SyntaxError string
    64  
    65  func (e SyntaxError) Error() string { return string(e) }
    66  
    67  var newline = []byte{'\n'}
    68  
    69  // Parse patches the patch text to create a patch Set.
    70  // The patch text typically comprises a textual header and a sequence
    71  // of file patches, as would be generated by CVS, Subversion,
    72  // Mercurial, or Git.
    73  func Parse(text []byte) (*Set, error) {
    74  	// Split text into files.
    75  	// CVS and Subversion begin new files with
    76  	//	Index: file name.
    77  	//	==================
    78  	//	diff -u blah blah
    79  	//
    80  	// Mercurial and Git use
    81  	//	diff [--git] a/file/path b/file/path.
    82  	//
    83  	// First look for Index: lines.  If none, fall back on diff lines.
    84  	text, files := sections(text, "Index: ")
    85  	if len(files) == 0 {
    86  		text, files = sections(text, "diff ")
    87  	}
    88  
    89  	set := &Set{string(text), make([]*File, len(files))}
    90  
    91  	// Parse file header and then
    92  	// parse files into patch chunks.
    93  	// Each chunk begins with @@.
    94  	for i, raw := range files {
    95  		p := new(File)
    96  		set.File[i] = p
    97  
    98  		// First line of hdr is the Index: that
    99  		// begins the section.  After that is the file name.
   100  		s, raw, _ := getLine(raw, 1)
   101  		if hasPrefix(s, "Index: ") {
   102  			p.Dst = string(bytes.TrimSpace(s[7:]))
   103  			goto HaveName
   104  		} else if hasPrefix(s, "diff ") {
   105  			str := string(bytes.TrimSpace(s))
   106  			i := strings.LastIndex(str, " b/")
   107  			if i >= 0 {
   108  				p.Dst = str[i+3:]
   109  				goto HaveName
   110  			}
   111  		}
   112  		return nil, SyntaxError("unexpected patch header line: " + string(s))
   113  	HaveName:
   114  		p.Dst = path.Clean(p.Dst)
   115  		if strings.HasPrefix(p.Dst, "../") || strings.HasPrefix(p.Dst, "/") {
   116  			return nil, SyntaxError("invalid path: " + p.Dst)
   117  		}
   118  
   119  		// Parse header lines giving file information:
   120  		//	new file mode %o	- file created
   121  		//	deleted file mode %o	- file deleted
   122  		//	old file mode %o	- file mode changed
   123  		//	new file mode %o	- file mode changed
   124  		//	rename from %s	- file renamed from other file
   125  		//	rename to %s
   126  		//	copy from %s		- file copied from other file
   127  		//	copy to %s
   128  		p.Verb = Edit
   129  		for len(raw) > 0 {
   130  			oldraw := raw
   131  			var l []byte
   132  			l, raw, _ = getLine(raw, 1)
   133  			l = bytes.TrimSpace(l)
   134  			if m, s, ok := atoi(l, "new file mode ", 8); ok && len(s) == 0 {
   135  				p.NewMode = m
   136  				p.Verb = Add
   137  				continue
   138  			}
   139  			if m, s, ok := atoi(l, "deleted file mode ", 8); ok && len(s) == 0 {
   140  				p.OldMode = m
   141  				p.Verb = Delete
   142  				p.Src = p.Dst
   143  				p.Dst = ""
   144  				continue
   145  			}
   146  			if m, s, ok := atoi(l, "old file mode ", 8); ok && len(s) == 0 {
   147  				// usually implies p.Verb = "rename" or "copy"
   148  				// but we'll get that from the rename or copy line.
   149  				p.OldMode = m
   150  				continue
   151  			}
   152  			if m, s, ok := atoi(l, "old mode ", 8); ok && len(s) == 0 {
   153  				p.OldMode = m
   154  				continue
   155  			}
   156  			if m, s, ok := atoi(l, "new mode ", 8); ok && len(s) == 0 {
   157  				p.NewMode = m
   158  				continue
   159  			}
   160  			if _, ok := skip(l, "similarity index "); ok {
   161  				continue
   162  			}
   163  			if s, ok := skip(l, "rename from "); ok && len(s) > 0 {
   164  				p.Src = string(s)
   165  				p.Verb = Rename
   166  				continue
   167  			}
   168  			if s, ok := skip(l, "rename to "); ok && len(s) > 0 {
   169  				p.Verb = Rename
   170  				continue
   171  			}
   172  			if s, ok := skip(l, "copy from "); ok && len(s) > 0 {
   173  				p.Src = string(s)
   174  				p.Verb = Copy
   175  				continue
   176  			}
   177  			if s, ok := skip(l, "copy to "); ok && len(s) > 0 {
   178  				p.Verb = Copy
   179  				continue
   180  			}
   181  			if s, ok := skip(l, "Binary file "); ok && len(s) > 0 {
   182  				// Hg prints
   183  				//	Binary file foo has changed
   184  				// when deleting a binary file.
   185  				continue
   186  			}
   187  			if s, ok := skip(l, "RCS file: "); ok && len(s) > 0 {
   188  				// CVS prints
   189  				//	RCS file: /cvs/plan9/bin/yesterday,v
   190  				//	retrieving revision 1.1
   191  				// for each file.
   192  				continue
   193  			}
   194  			if s, ok := skip(l, "retrieving revision "); ok && len(s) > 0 {
   195  				// CVS prints
   196  				//	RCS file: /cvs/plan9/bin/yesterday,v
   197  				//	retrieving revision 1.1
   198  				// for each file.
   199  				continue
   200  			}
   201  			if hasPrefix(l, "===") || hasPrefix(l, "---") || hasPrefix(l, "+++") || hasPrefix(l, "diff ") {
   202  				continue
   203  			}
   204  			if hasPrefix(l, "@@ -") {
   205  				diff, err := ParseTextDiff(oldraw)
   206  				if err != nil {
   207  					return nil, err
   208  				}
   209  				p.Diff = diff
   210  				break
   211  			}
   212  			if hasPrefix(l, "GIT binary patch") || (hasPrefix(l, "index ") && !hasPrefix(raw, "--- ")) {
   213  				diff, err := ParseGitBinary(oldraw)
   214  				if err != nil {
   215  					return nil, err
   216  				}
   217  				p.Diff = diff
   218  				break
   219  			}
   220  			if hasPrefix(l, "index ") {
   221  				continue
   222  			}
   223  			if len(l) == 0 {
   224  				continue
   225  			}
   226  			return nil, SyntaxError("unexpected patch header line: " + string(l))
   227  		}
   228  		if p.Diff == nil {
   229  			p.Diff = NoDiff
   230  		}
   231  		if p.Verb == Edit {
   232  			p.Src = p.Dst
   233  		}
   234  	}
   235  
   236  	return set, nil
   237  }
   238  
   239  // getLine returns the first n lines of data and the remainder.
   240  // If data has no newline, getLine returns data, nil, false
   241  func getLine(data []byte, n int) (first []byte, rest []byte, ok bool) {
   242  	rest = data
   243  	ok = true
   244  	for ; n > 0; n-- {
   245  		nl := bytes.Index(rest, newline)
   246  		if nl < 0 {
   247  			rest = nil
   248  			ok = false
   249  			break
   250  		}
   251  		rest = rest[nl+1:]
   252  	}
   253  	first = data[0 : len(data)-len(rest)]
   254  	return
   255  }
   256  
   257  // sections returns a collection of file sections,
   258  // each of which begins with a line satisfying prefix.
   259  // text before the first instance of such a line is
   260  // returned separately.
   261  func sections(text []byte, prefix string) ([]byte, [][]byte) {
   262  	n := 0
   263  	for b := text; ; {
   264  		if hasPrefix(b, prefix) {
   265  			n++
   266  		}
   267  		nl := bytes.Index(b, newline)
   268  		if nl < 0 {
   269  			break
   270  		}
   271  		b = b[nl+1:]
   272  	}
   273  
   274  	sect := make([][]byte, n+1)
   275  	n = 0
   276  	for b := text; ; {
   277  		if hasPrefix(b, prefix) {
   278  			sect[n] = text[0 : len(text)-len(b)]
   279  			n++
   280  			text = b
   281  		}
   282  		nl := bytes.Index(b, newline)
   283  		if nl < 0 {
   284  			sect[n] = text
   285  			break
   286  		}
   287  		b = b[nl+1:]
   288  	}
   289  	return sect[0], sect[1:]
   290  }
   291  
   292  // if s begins with the prefix t, skip returns
   293  // s with that prefix removed and ok == true.
   294  func skip(s []byte, t string) (ss []byte, ok bool) {
   295  	if len(s) < len(t) || string(s[0:len(t)]) != t {
   296  		return nil, false
   297  	}
   298  	return s[len(t):], true
   299  }
   300  
   301  // if s begins with the prefix t and then is a sequence
   302  // of digits in the given base, atoi returns the number
   303  // represented by the digits and s with the
   304  // prefix and the digits removed.
   305  func atoi(s []byte, t string, base int) (n int, ss []byte, ok bool) {
   306  	if s, ok = skip(s, t); !ok {
   307  		return
   308  	}
   309  	var i int
   310  	for i = 0; i < len(s) && '0' <= s[i] && s[i] <= byte('0'+base-1); i++ {
   311  		n = n*base + int(s[i]-'0')
   312  	}
   313  	if i == 0 {
   314  		return
   315  	}
   316  	return n, s[i:], true
   317  }
   318  
   319  // hasPrefix returns true if s begins with t.
   320  func hasPrefix(s []byte, t string) bool {
   321  	_, ok := skip(s, t)
   322  	return ok
   323  }
   324  
   325  // splitLines returns the result of splitting s into lines.
   326  // The \n on each line is preserved.
   327  func splitLines(s []byte) [][]byte { return bytes.SplitAfter(s, newline) }