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.
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.
17 // A Set represents a set of patches to be applied as a single atomic unit.
18 // Patch sets are often preceded by a descriptive header.
20 Header
string // free-form text
24 // A File represents a collection of changes to be made to a single file.
27 Src
string // source for Verb == Copy, Verb == Rename
29 OldMode
, NewMode
int // 0 indicates not used
30 Diff
// changes to data; == NoDiff if operation does not edit file
33 // A Verb is an action performed on a file.
39 Delete Verb
= "delete"
41 Rename Verb
= "rename"
44 // A Diff is any object that describes changes to transform
45 // an old byte stream to a new one.
47 // Apply applies the changes listed in the diff
48 // to the string s, returning the new version of the string.
49 // Note that the string s need not be a text string.
50 Apply(old
[]byte) (new []byte, err os
.Error
)
53 // NoDiff is a no-op Diff implementation: it passes the
54 // old data through unchanged.
55 var NoDiff Diff
= noDiffType(0)
59 func (noDiffType
) Apply(old
[]byte) ([]byte, os
.Error
) {
63 // A SyntaxError represents a syntax error encountered while parsing a patch.
64 type SyntaxError
string
66 func (e SyntaxError
) String() string { return string(e
) }
68 var newline
= []byte{'\n'}
70 // Parse patches the patch text to create a patch Set.
71 // The patch text typically comprises a textual header and a sequence
72 // of file patches, as would be generated by CVS, Subversion,
74 func Parse(text
[]byte) (*Set
, os
.Error
) {
75 // Split text into files.
76 // CVS and Subversion begin new files with
81 // Mercurial and Git use
82 // diff [--git] a/file/path b/file/path.
84 // First look for Index: lines. If none, fall back on diff lines.
85 text
, files
:= sections(text
, "Index: ")
87 text
, files
= sections(text
, "diff ")
90 set
:= &Set
{string(text
), make([]*File
, len(files
))}
92 // Parse file header and then
93 // parse files into patch chunks.
94 // Each chunk begins with @@.
95 for i
, raw
:= range files
{
99 // First line of hdr is the Index: that
100 // begins the section. After that is the file name.
101 s
, raw
, _
:= getLine(raw
, 1)
102 if hasPrefix(s
, "Index: ") {
103 p
.Dst
= string(bytes
.TrimSpace(s
[7:]))
105 } else if hasPrefix(s
, "diff ") {
106 str
:= string(bytes
.TrimSpace(s
))
107 i
:= strings
.LastIndex(str
, " b/")
113 return nil, SyntaxError("unexpected patch header line: " + string(s
))
115 p
.Dst
= path
.Clean(p
.Dst
)
116 if strings
.HasPrefix(p
.Dst
, "../") || strings
.HasPrefix(p
.Dst
, "/") {
117 return nil, SyntaxError("invalid path: " + p
.Dst
)
120 // Parse header lines giving file information:
121 // new file mode %o - file created
122 // deleted file mode %o - file deleted
123 // old file mode %o - file mode changed
124 // new file mode %o - file mode changed
125 // rename from %s - file renamed from other file
127 // copy from %s - file copied from other file
133 l
, raw
, _
= getLine(raw
, 1)
134 l
= bytes
.TrimSpace(l
)
135 if m
, s
, ok
:= atoi(l
, "new file mode ", 8); ok
&& len(s
) == 0 {
140 if m
, s
, ok
:= atoi(l
, "deleted file mode ", 8); ok
&& len(s
) == 0 {
147 if m
, s
, ok
:= atoi(l
, "old file mode ", 8); ok
&& len(s
) == 0 {
148 // usually implies p.Verb = "rename" or "copy"
149 // but we'll get that from the rename or copy line.
153 if m
, s
, ok
:= atoi(l
, "old mode ", 8); ok
&& len(s
) == 0 {
157 if m
, s
, ok
:= atoi(l
, "new mode ", 8); ok
&& len(s
) == 0 {
161 if s
, ok
:= skip(l
, "rename from "); ok
&& len(s
) > 0 {
166 if s
, ok
:= skip(l
, "rename to "); ok
&& len(s
) > 0 {
170 if s
, ok
:= skip(l
, "copy from "); ok
&& len(s
) > 0 {
175 if s
, ok
:= skip(l
, "copy to "); ok
&& len(s
) > 0 {
179 if s
, ok
:= skip(l
, "Binary file "); ok
&& len(s
) > 0 {
181 // Binary file foo has changed
182 // when deleting a binary file.
185 if s
, ok
:= skip(l
, "RCS file: "); ok
&& len(s
) > 0 {
187 // RCS file: /cvs/plan9/bin/yesterday,v
188 // retrieving revision 1.1
192 if s
, ok
:= skip(l
, "retrieving revision "); ok
&& len(s
) > 0 {
194 // RCS file: /cvs/plan9/bin/yesterday,v
195 // retrieving revision 1.1
199 if hasPrefix(l
, "===") ||
hasPrefix(l
, "---") ||
hasPrefix(l
, "+++") ||
hasPrefix(l
, "diff ") {
202 if hasPrefix(l
, "@@ -") {
203 diff
, err
:= ParseTextDiff(oldraw
)
210 if hasPrefix(l
, "GIT binary patch") ||
(hasPrefix(l
, "index ") && !hasPrefix(raw
, "--- ")) {
211 diff
, err
:= ParseGitBinary(oldraw
)
218 if hasPrefix(l
, "index ") {
221 return nil, SyntaxError("unexpected patch header line: " + string(l
))
234 // getLine returns the first n lines of data and the remainder.
235 // If data has no newline, getLine returns data, nil, false
236 func getLine(data
[]byte, n
int) (first
[]byte, rest
[]byte, ok
bool) {
240 nl
:= bytes
.Index(rest
, newline
)
248 first
= data
[0 : len(data
)-len(rest
)]
252 // sections returns a collection of file sections,
253 // each of which begins with a line satisfying prefix.
254 // text before the first instance of such a line is
255 // returned separately.
256 func sections(text
[]byte, prefix
string) ([]byte, [][]byte) {
259 if hasPrefix(b
, prefix
) {
262 nl
:= bytes
.Index(b
, newline
)
269 sect
:= make([][]byte, n
+1)
272 if hasPrefix(b
, prefix
) {
273 sect
[n
] = text
[0 : len(text
)-len(b
)]
277 nl
:= bytes
.Index(b
, newline
)
284 return sect
[0], sect
[1:]
287 // if s begins with the prefix t, skip returns
288 // s with that prefix removed and ok == true.
289 func skip(s
[]byte, t
string) (ss
[]byte, ok
bool) {
290 if len(s
) < len(t
) ||
string(s
[0:len(t
)]) != t
{
293 return s
[len(t
):], true
296 // if s begins with the prefix t and then is a sequence
297 // of digits in the given base, atoi returns the number
298 // represented by the digits and s with the
299 // prefix and the digits removed.
300 func atoi(s
[]byte, t
string, base
int) (n
int, ss
[]byte, ok
bool) {
301 if s
, ok
= skip(s
, t
); !ok
{
305 for i
= 0; i
< len(s
) && '0' <= s
[i
] && s
[i
] <= byte('0'+base
-1); i
++ {
306 n
= n
*base
+ int(s
[i
]-'0')
311 return n
, s
[i
:], true
314 // hasPrefix returns true if s begins with t.
315 func hasPrefix(s
[]byte, t
string) bool {
320 // splitLines returns the result of splitting s into lines.
321 // The \n on each line is preserved.
322 func splitLines(s
[]byte) [][]byte { return bytes
.SplitAfter(s
, newline
, -1) }