diff options
author | Russ Cox <rsc@golang.org> | 2009-11-04 17:55:06 -0800 |
---|---|---|
committer | Russ Cox <rsc@golang.org> | 2009-11-04 17:55:06 -0800 |
commit | ce9b3deaa4fa22e9d5580df05e9762b988793baa (patch) | |
tree | eacfeb41baaf6752deba91909b8f96d90dea476e /src/pkg/patch/textdiff.go | |
parent | 782c3f76d1c7af511daf0d44ba29737f3e1de655 (diff) | |
download | golang-ce9b3deaa4fa22e9d5580df05e9762b988793baa.tar.gz |
package patch
R=r
http://go/go-review/1018043
Diffstat (limited to 'src/pkg/patch/textdiff.go')
-rw-r--r-- | src/pkg/patch/textdiff.go | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/src/pkg/patch/textdiff.go b/src/pkg/patch/textdiff.go new file mode 100644 index 000000000..db8527682 --- /dev/null +++ b/src/pkg/patch/textdiff.go @@ -0,0 +1,171 @@ +package patch + +import ( + "bytes"; + "os"; +) + +type TextDiff []TextChunk + +// A TextChunk specifies an edit to a section of a file: +// the text beginning at Line, which should be exactly Old, +// is to be replaced with New. +type TextChunk struct { + Line int; + Old []byte; + New []byte; +} + +func ParseTextDiff(raw []byte) (TextDiff, os.Error) { + // Copy raw so it is safe to keep references to slices. + _, chunks := sections(raw, "@@ -"); + delta := 0; + diff := make(TextDiff, len(chunks)); + for i, raw := range chunks { + c := &diff[i]; + + // Parse start line: @@ -oldLine,oldCount +newLine,newCount @@ junk + chunk := splitLines(raw); + chunkHeader := chunk[0]; + var ok bool; + var oldLine, oldCount, newLine, newCount int; + s := chunkHeader; + if oldLine, s, ok = atoi(s, "@@ -", 10); !ok { + ErrChunkHdr: + return nil, SyntaxError("unexpected chunk header line: " + string(chunkHeader)); + } + if len(s) == 0 || s[0] != ',' { + oldCount = 1; + } else if oldCount, s, ok = atoi(s, ",", 10); !ok { + goto ErrChunkHdr; + } + if newLine, s, ok = atoi(s, " +", 10); !ok { + goto ErrChunkHdr; + } + if len(s) == 0 || s[0] != ',' { + newCount = 1; + } else if newCount, s, ok = atoi(s, ",", 10); !ok { + goto ErrChunkHdr; + } + if !hasPrefix(s, " @@") { + goto ErrChunkHdr; + } + + // Special case: for created or deleted files, the empty half + // is given as starting at line 0. Translate to line 1. + if oldCount == 0 && oldLine == 0 { + oldLine = 1; + } + if newCount == 0 && newLine == 0 { + newLine = 1; + } + + // Count lines in text + var dropOldNL, dropNewNL bool; + var nold, nnew int; + var lastch byte; + chunk = chunk[1:len(chunk)]; + for _, l := range chunk { + if nold == oldCount && nnew == newCount && (len(l) == 0 || l[0] != '\\') { + if len(bytes.TrimSpace(l)) != 0 { + return nil, SyntaxError("too many chunk lines"); + } + continue; + } + if len(l) == 0 { + return nil, SyntaxError("empty chunk line"); + } + switch l[0] { + case '+': + nnew++; + case '-': + nold++; + case ' ': + nnew++; + nold++; + case '\\': + if _, ok := skip(l, "\\ No newline at end of file"); ok { + switch lastch { + case '-': + dropOldNL = true; + case '+': + dropNewNL = true; + case ' ': + dropOldNL = true; + dropNewNL = true; + default: + return nil, SyntaxError("message `\\ No newline at end of file' out of context"); + } + break; + } + fallthrough; + default: + return nil, SyntaxError("unexpected chunk line: " + string(l)); + } + lastch = l[0]; + } + + // Does it match the header? + if nold != oldCount || nnew != newCount { + return nil, SyntaxError("chunk header does not match line count: " + string(chunkHeader)); + } + if oldLine+delta != newLine { + return nil, SyntaxError("chunk delta is out of sync with previous chunks"); + } + delta += nnew-nold; + c.Line = oldLine; + + var old, new bytes.Buffer; + nold = 0; + nnew = 0; + for _, l := range chunk { + if nold == oldCount && nnew == newCount { + break; + } + ch, l := l[0], l[1:len(l)]; + if ch == '\\' { + continue; + } + if ch != '+' { + old.Write(l); + nold++; + } + if ch != '-' { + new.Write(l); + nnew++; + } + } + c.Old = old.Bytes(); + c.New = new.Bytes(); + if dropOldNL { + c.Old = c.Old[0 : len(c.Old)-1]; + } + if dropNewNL { + c.New = c.New[0 : len(c.New)-1]; + } + } + return diff, nil; +} + +var ErrPatchFailure = os.NewError("patch did not apply cleanly") + +// Apply applies the changes listed in the diff +// to the data, returning the new version. +func (d TextDiff) Apply(data []byte) ([]byte, os.Error) { + var buf bytes.Buffer; + line := 1; + for _, c := range d { + var ok bool; + var prefix []byte; + prefix, data, ok = getLine(data, c.Line - line); + if !ok || !bytes.HasPrefix(data, c.Old) { + return nil, ErrPatchFailure; + } + buf.Write(prefix); + data = data[len(c.Old):len(data)]; + buf.Write(c.New); + line = c.Line + bytes.Count(c.Old, newline); + } + buf.Write(data); + return buf.Bytes(), nil; +} |