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) }