~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to patches-invert.py

Remove patches-invert.py

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
3
 
#
4
 
#    This program is free software; you can redistribute it and/or modify
5
 
#    it under the terms of the GNU General Public License as published by
6
 
#    the Free Software Foundation; either version 2 of the License, or
7
 
#    (at your option) any later version.
8
 
#
9
 
#    This program is distributed in the hope that it will be useful,
10
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
#    GNU General Public License for more details.
13
 
#
14
 
#    You should have received a copy of the GNU General Public License
15
 
#    along with this program; if not, write to the Free Software
16
 
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
import sys
18
 
class PatchSyntax(Exception):
19
 
    def __init__(self, msg):
20
 
        Exception.__init__(self, msg)
21
 
 
22
 
 
23
 
class MalformedPatchHeader(PatchSyntax):
24
 
    def __init__(self, desc, line):
25
 
        self.desc = desc
26
 
        self.line = line
27
 
        msg = "Malformed patch header.  %s\n%s" % (self.desc, self.line)
28
 
        PatchSyntax.__init__(self, msg)
29
 
 
30
 
class MalformedHunkHeader(PatchSyntax):
31
 
    def __init__(self, desc, line):
32
 
        self.desc = desc
33
 
        self.line = line
34
 
        msg = "Malformed hunk header.  %s\n%s" % (self.desc, self.line)
35
 
        PatchSyntax.__init__(self, msg)
36
 
 
37
 
class MalformedLine(PatchSyntax):
38
 
    def __init__(self, desc, line):
39
 
        self.desc = desc
40
 
        self.line = line
41
 
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
42
 
        PatchSyntax.__init__(self, msg)
43
 
 
44
 
def get_patch_names(iter_lines):
45
 
    try:
46
 
        line = iter_lines.next()
47
 
        if line.startswith("*** "):
48
 
            line = iter_lines.next()
49
 
        if not line.startswith("--- "):
50
 
            raise MalformedPatchHeader("No orig name", line)
51
 
        else:
52
 
            orig_name = line[4:].rstrip("\n")
53
 
    except StopIteration:
54
 
        raise MalformedPatchHeader("No orig line", "")
55
 
    try:
56
 
        line = iter_lines.next()
57
 
        if not line.startswith("+++ "):
58
 
            raise PatchSyntax("No mod name")
59
 
        else:
60
 
            mod_name = line[4:].rstrip("\n")
61
 
    except StopIteration:
62
 
        raise MalformedPatchHeader("No mod line", "")
63
 
    return (orig_name, mod_name)
64
 
 
65
 
def parse_range(textrange):
66
 
    """Parse a patch range, handling the "1" special-case
67
 
 
68
 
    :param textrange: The text to parse
69
 
    :type textrange: str
70
 
    :return: the position and range, as a tuple
71
 
    :rtype: (int, int)
72
 
    """
73
 
    tmp = textrange.split(',')
74
 
    if len(tmp) == 1:
75
 
        pos = tmp[0]
76
 
        range = "1"
77
 
    else:
78
 
        (pos, range) = tmp
79
 
    pos = int(pos)
80
 
    range = int(range)
81
 
    return (pos, range)
82
 
 
83
 
 
84
 
def hunk_from_header(line):
85
 
    if not line.startswith("@@") or not line.endswith("@@\n") \
86
 
        or not len(line) > 4:
87
 
        raise MalformedHunkHeader("Does not start and end with @@.", line)
88
 
    try:
89
 
        (orig, mod) = line[3:-4].split(" ")
90
 
    except Exception, e:
91
 
        raise MalformedHunkHeader(str(e), line)
92
 
    if not orig.startswith('-') or not mod.startswith('+'):
93
 
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
94
 
    try:
95
 
        (orig_pos, orig_range) = parse_range(orig[1:])
96
 
        (mod_pos, mod_range) = parse_range(mod[1:])
97
 
    except Exception, e:
98
 
        raise MalformedHunkHeader(str(e), line)
99
 
    if mod_range < 0 or orig_range < 0:
100
 
        raise MalformedHunkHeader("Hunk range is negative", line)
101
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
102
 
 
103
 
 
104
 
class HunkLine:
105
 
    def __init__(self, contents):
106
 
        self.contents = contents
107
 
 
108
 
    def get_str(self, leadchar):
109
 
        if self.contents == "\n" and leadchar == " " and False:
110
 
            return "\n"
111
 
        return leadchar + self.contents
112
 
 
113
 
    def invert(self):
114
 
        return self
115
 
 
116
 
class ContextLine(HunkLine):
117
 
    def __init__(self, contents):
118
 
        HunkLine.__init__(self, contents)
119
 
 
120
 
    def __str__(self):
121
 
        return self.get_str(" ")
122
 
 
123
 
 
124
 
class InsertLine(HunkLine):
125
 
    def __init__(self, contents):
126
 
        HunkLine.__init__(self, contents)
127
 
 
128
 
    def __str__(self):
129
 
        return self.get_str("+")
130
 
 
131
 
    def invert(self):
132
 
        return RemoveLine(self.contents)
133
 
 
134
 
 
135
 
class RemoveLine(HunkLine):
136
 
    def __init__(self, contents):
137
 
        HunkLine.__init__(self, contents)
138
 
 
139
 
    def __str__(self):
140
 
        return self.get_str("-")
141
 
 
142
 
    def invert(self):
143
 
        return InsertLine(self.contents)
144
 
 
145
 
__pychecker__="no-returnvalues"
146
 
def parse_line(line):
147
 
    if line.startswith("\n"):
148
 
        return ContextLine(line)
149
 
    elif line.startswith(" "):
150
 
        return ContextLine(line[1:])
151
 
    elif line.startswith("+"):
152
 
        return InsertLine(line[1:])
153
 
    elif line.startswith("-"):
154
 
        return RemoveLine(line[1:])
155
 
    else:
156
 
        raise MalformedLine("Unknown line type", line)
157
 
__pychecker__=""
158
 
 
159
 
 
160
 
class Hunk:
161
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
162
 
        self.orig_pos = orig_pos
163
 
        self.orig_range = orig_range
164
 
        self.mod_pos = mod_pos
165
 
        self.mod_range = mod_range
166
 
        self.lines = []
167
 
 
168
 
    def get_header(self):
169
 
        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
170
 
                                                   self.orig_range),
171
 
                                    self.range_str(self.mod_pos, 
172
 
                                                   self.mod_range))
173
 
 
174
 
    def range_str(self, pos, range):
175
 
        """Return a file range, special-casing for 1-line files.
176
 
 
177
 
        :param pos: The position in the file
178
 
        :type pos: int
179
 
        :range: The range in the file
180
 
        :type range: int
181
 
        :return: a string in the format 1,4 except when range == pos == 1
182
 
        """
183
 
        if range == 1:
184
 
            return "%i" % pos
185
 
        else:
186
 
            return "%i,%i" % (pos, range)
187
 
 
188
 
    def __str__(self):
189
 
        lines = [self.get_header()]
190
 
        for line in self.lines:
191
 
            lines.append(str(line))
192
 
        return "".join(lines)
193
 
 
194
 
    def shift_to_mod(self, pos):
195
 
        if pos < self.orig_pos-1:
196
 
            return 0
197
 
        elif pos > self.orig_pos+self.orig_range:
198
 
            return self.mod_range - self.orig_range
199
 
        else:
200
 
            return self.shift_to_mod_lines(pos)
201
 
 
202
 
    def shift_to_mod_lines(self, pos):
203
 
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
204
 
        position = self.orig_pos-1
205
 
        shift = 0
206
 
        for line in self.lines:
207
 
            if isinstance(line, InsertLine):
208
 
                shift += 1
209
 
            elif isinstance(line, RemoveLine):
210
 
                if position == pos:
211
 
                    return None
212
 
                shift -= 1
213
 
                position += 1
214
 
            elif isinstance(line, ContextLine):
215
 
                position += 1
216
 
            if position > pos:
217
 
                break
218
 
        return shift
219
 
 
220
 
    def invert(self):
221
 
        clone = Hunk(self.mod_pos, self.mod_range, self.orig_pos, self.orig_range)
222
 
        for line in self.lines:
223
 
            clone.lines.append(line.invert())
224
 
        return clone
225
 
 
226
 
def iter_hunks(iter_lines):
227
 
    hunk = None
228
 
    for line in iter_lines:
229
 
        if line.startswith("@@"):
230
 
            if hunk is not None:
231
 
                yield hunk
232
 
            hunk = hunk_from_header(line)
233
 
        else:
234
 
            hunk.lines.append(parse_line(line))
235
 
 
236
 
    if hunk is not None:
237
 
        yield hunk
238
 
 
239
 
class Patch:
240
 
    def __init__(self, oldname, newname):
241
 
        self.oldname = oldname
242
 
        self.newname = newname
243
 
        self.hunks = []
244
 
 
245
 
    def __str__(self):
246
 
        ret =  "--- %s\n+++ %s\n" % (self.oldname, self.newname) 
247
 
        ret += "".join([str(h) for h in self.hunks])
248
 
        return ret
249
 
 
250
 
    def invert(self):
251
 
        clone = Patch(self.newname, self.oldname)
252
 
        for hunk in self.hunks:
253
 
            clone.hunks.append(hunk.invert())
254
 
        return clone
255
 
 
256
 
    def stats_str(self):
257
 
        """Return a string of patch statistics"""
258
 
        removes = 0
259
 
        inserts = 0
260
 
        for hunk in self.hunks:
261
 
            for line in hunk.lines:
262
 
                if isinstance(line, InsertLine):
263
 
                     inserts+=1;
264
 
                elif isinstance(line, RemoveLine):
265
 
                     removes+=1;
266
 
        return "%i inserts, %i removes in %i hunks" % \
267
 
            (inserts, removes, len(self.hunks))
268
 
 
269
 
    def pos_in_mod(self, position):
270
 
        newpos = position
271
 
        for hunk in self.hunks:
272
 
            shift = hunk.shift_to_mod(position)
273
 
            if shift is None:
274
 
                return None
275
 
            newpos += shift
276
 
        return newpos
277
 
            
278
 
    def iter_inserted(self):
279
 
        """Iteraties through inserted lines
280
 
        
281
 
        :return: Pair of line number, line
282
 
        :rtype: iterator of (int, InsertLine)
283
 
        """
284
 
        for hunk in self.hunks:
285
 
            pos = hunk.mod_pos - 1;
286
 
            for line in hunk.lines:
287
 
                if isinstance(line, InsertLine):
288
 
                    yield (pos, line)
289
 
                    pos += 1
290
 
                if isinstance(line, ContextLine):
291
 
                    pos += 1
292
 
 
293
 
def parse_patch(iter_lines):
294
 
    (orig_name, mod_name) = get_patch_names(iter_lines)
295
 
    patch = Patch(orig_name, mod_name)
296
 
    for hunk in iter_hunks(iter_lines):
297
 
        patch.hunks.append(hunk)
298
 
    return patch
299
 
 
300
 
def parse_patches(iter_lines):
301
 
    def parse(lines):
302
 
        if len(lines) > 0:
303
 
            return [ parse_patch(lines.__iter__()) ]
304
 
        else:
305
 
            return []
306
 
 
307
 
    patches = []
308
 
    saved_lines = []
309
 
    while True:
310
 
        try: line = iter_lines.next()
311
 
        except StopIteration:
312
 
            patches.extend(parse(saved_lines))
313
 
            break
314
 
 
315
 
        if line.startswith('*** '):
316
 
            patches.extend(parse(saved_lines))
317
 
            saved_lines = []
318
 
            continue
319
 
        elif line.startswith('--- ') and len(saved_lines) > 1:
320
 
            patches.extend(parse(saved_lines))
321
 
            saved_lines = [ line ]
322
 
            continue
323
 
 
324
 
        saved_lines.append(line)
325
 
 
326
 
    return patches