~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Martin Pool
  • Date: 2005-05-10 06:07:16 UTC
  • Revision ID: mbp@sourcefrog.net-20050510060716-0f939ce3ddea5d15
- New command update-stat-cache for testing
- work-cache always stored with unix newlines and in ascii

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004 - 2006, 2008 Aaron Bentley, Canonical Ltd
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
 
 
18
 
 
19
 
class PatchSyntax(Exception):
20
 
    def __init__(self, msg):
21
 
        Exception.__init__(self, msg)
22
 
 
23
 
 
24
 
class MalformedPatchHeader(PatchSyntax):
25
 
    def __init__(self, desc, line):
26
 
        self.desc = desc
27
 
        self.line = line
28
 
        msg = "Malformed patch header.  %s\n%r" % (self.desc, self.line)
29
 
        PatchSyntax.__init__(self, msg)
30
 
 
31
 
 
32
 
class MalformedHunkHeader(PatchSyntax):
33
 
    def __init__(self, desc, line):
34
 
        self.desc = desc
35
 
        self.line = line
36
 
        msg = "Malformed hunk header.  %s\n%r" % (self.desc, self.line)
37
 
        PatchSyntax.__init__(self, msg)
38
 
 
39
 
 
40
 
class MalformedLine(PatchSyntax):
41
 
    def __init__(self, desc, line):
42
 
        self.desc = desc
43
 
        self.line = line
44
 
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
45
 
        PatchSyntax.__init__(self, msg)
46
 
 
47
 
 
48
 
class PatchConflict(Exception):
49
 
    def __init__(self, line_no, orig_line, patch_line):
50
 
        orig = orig_line.rstrip('\n')
51
 
        patch = str(patch_line).rstrip('\n')
52
 
        msg = 'Text contents mismatch at line %d.  Original has "%s",'\
53
 
            ' but patch says it should be "%s"' % (line_no, orig, patch)
54
 
        Exception.__init__(self, msg)
55
 
 
56
 
 
57
 
def get_patch_names(iter_lines):
58
 
    try:
59
 
        line = iter_lines.next()
60
 
        if not line.startswith("--- "):
61
 
            raise MalformedPatchHeader("No orig name", line)
62
 
        else:
63
 
            orig_name = line[4:].rstrip("\n")
64
 
    except StopIteration:
65
 
        raise MalformedPatchHeader("No orig line", "")
66
 
    try:
67
 
        line = iter_lines.next()
68
 
        if not line.startswith("+++ "):
69
 
            raise PatchSyntax("No mod name")
70
 
        else:
71
 
            mod_name = line[4:].rstrip("\n")
72
 
    except StopIteration:
73
 
        raise MalformedPatchHeader("No mod line", "")
74
 
    return (orig_name, mod_name)
75
 
 
76
 
 
77
 
def parse_range(textrange):
78
 
    """Parse a patch range, handling the "1" special-case
79
 
 
80
 
    :param textrange: The text to parse
81
 
    :type textrange: str
82
 
    :return: the position and range, as a tuple
83
 
    :rtype: (int, int)
84
 
    """
85
 
    tmp = textrange.split(',')
86
 
    if len(tmp) == 1:
87
 
        pos = tmp[0]
88
 
        range = "1"
89
 
    else:
90
 
        (pos, range) = tmp
91
 
    pos = int(pos)
92
 
    range = int(range)
93
 
    return (pos, range)
94
 
 
95
 
 
96
 
def hunk_from_header(line):
97
 
    import re
98
 
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
99
 
    if matches is None:
100
 
        raise MalformedHunkHeader("Does not match format.", line)
101
 
    try:
102
 
        (orig, mod) = matches.group(1).split(" ")
103
 
    except (ValueError, IndexError), e:
104
 
        raise MalformedHunkHeader(str(e), line)
105
 
    if not orig.startswith('-') or not mod.startswith('+'):
106
 
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
107
 
    try:
108
 
        (orig_pos, orig_range) = parse_range(orig[1:])
109
 
        (mod_pos, mod_range) = parse_range(mod[1:])
110
 
    except (ValueError, IndexError), e:
111
 
        raise MalformedHunkHeader(str(e), line)
112
 
    if mod_range < 0 or orig_range < 0:
113
 
        raise MalformedHunkHeader("Hunk range is negative", line)
114
 
    tail = matches.group(3)
115
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
116
 
 
117
 
 
118
 
class HunkLine:
119
 
    def __init__(self, contents):
120
 
        self.contents = contents
121
 
 
122
 
    def get_str(self, leadchar):
123
 
        if self.contents == "\n" and leadchar == " " and False:
124
 
            return "\n"
125
 
        if not self.contents.endswith('\n'):
126
 
            terminator = '\n' + NO_NL
127
 
        else:
128
 
            terminator = ''
129
 
        return leadchar + self.contents + terminator
130
 
 
131
 
 
132
 
class ContextLine(HunkLine):
133
 
    def __init__(self, contents):
134
 
        HunkLine.__init__(self, contents)
135
 
 
136
 
    def __str__(self):
137
 
        return self.get_str(" ")
138
 
 
139
 
 
140
 
class InsertLine(HunkLine):
141
 
    def __init__(self, contents):
142
 
        HunkLine.__init__(self, contents)
143
 
 
144
 
    def __str__(self):
145
 
        return self.get_str("+")
146
 
 
147
 
 
148
 
class RemoveLine(HunkLine):
149
 
    def __init__(self, contents):
150
 
        HunkLine.__init__(self, contents)
151
 
 
152
 
    def __str__(self):
153
 
        return self.get_str("-")
154
 
 
155
 
NO_NL = '\\ No newline at end of file\n'
156
 
__pychecker__="no-returnvalues"
157
 
 
158
 
def parse_line(line):
159
 
    if line.startswith("\n"):
160
 
        return ContextLine(line)
161
 
    elif line.startswith(" "):
162
 
        return ContextLine(line[1:])
163
 
    elif line.startswith("+"):
164
 
        return InsertLine(line[1:])
165
 
    elif line.startswith("-"):
166
 
        return RemoveLine(line[1:])
167
 
    else:
168
 
        raise MalformedLine("Unknown line type", line)
169
 
__pychecker__=""
170
 
 
171
 
 
172
 
class Hunk:
173
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
 
        self.orig_pos = orig_pos
175
 
        self.orig_range = orig_range
176
 
        self.mod_pos = mod_pos
177
 
        self.mod_range = mod_range
178
 
        self.tail = tail
179
 
        self.lines = []
180
 
 
181
 
    def get_header(self):
182
 
        if self.tail is None:
183
 
            tail_str = ''
184
 
        else:
185
 
            tail_str = ' ' + self.tail
186
 
        return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
187
 
                                                     self.orig_range),
188
 
                                      self.range_str(self.mod_pos,
189
 
                                                     self.mod_range),
190
 
                                      tail_str)
191
 
 
192
 
    def range_str(self, pos, range):
193
 
        """Return a file range, special-casing for 1-line files.
194
 
 
195
 
        :param pos: The position in the file
196
 
        :type pos: int
197
 
        :range: The range in the file
198
 
        :type range: int
199
 
        :return: a string in the format 1,4 except when range == pos == 1
200
 
        """
201
 
        if range == 1:
202
 
            return "%i" % pos
203
 
        else:
204
 
            return "%i,%i" % (pos, range)
205
 
 
206
 
    def __str__(self):
207
 
        lines = [self.get_header()]
208
 
        for line in self.lines:
209
 
            lines.append(str(line))
210
 
        return "".join(lines)
211
 
 
212
 
    def shift_to_mod(self, pos):
213
 
        if pos < self.orig_pos-1:
214
 
            return 0
215
 
        elif pos > self.orig_pos+self.orig_range:
216
 
            return self.mod_range - self.orig_range
217
 
        else:
218
 
            return self.shift_to_mod_lines(pos)
219
 
 
220
 
    def shift_to_mod_lines(self, pos):
221
 
        position = self.orig_pos-1
222
 
        shift = 0
223
 
        for line in self.lines:
224
 
            if isinstance(line, InsertLine):
225
 
                shift += 1
226
 
            elif isinstance(line, RemoveLine):
227
 
                if position == pos:
228
 
                    return None
229
 
                shift -= 1
230
 
                position += 1
231
 
            elif isinstance(line, ContextLine):
232
 
                position += 1
233
 
            if position > pos:
234
 
                break
235
 
        return shift
236
 
 
237
 
 
238
 
def iter_hunks(iter_lines):
239
 
    hunk = None
240
 
    for line in iter_lines:
241
 
        if line == NO_NL and hunk is not None:
242
 
            hunk.lines.append(NO_NL)
243
 
            continue
244
 
        if line == "\n":
245
 
            if hunk is not None:
246
 
                yield hunk
247
 
                hunk = None
248
 
            continue
249
 
        if hunk is not None:
250
 
            yield hunk
251
 
        hunk = hunk_from_header(line)
252
 
        orig_size = 0
253
 
        mod_size = 0
254
 
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
255
 
            hunk_line = parse_line(iter_lines.next())
256
 
            hunk.lines.append(hunk_line)
257
 
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
258
 
                orig_size += 1
259
 
            if isinstance(hunk_line, (InsertLine, ContextLine)):
260
 
                mod_size += 1
261
 
    if hunk is not None:
262
 
        yield hunk
263
 
 
264
 
 
265
 
class Patch:
266
 
    def __init__(self, oldname, newname):
267
 
        self.oldname = oldname
268
 
        self.newname = newname
269
 
        self.hunks = []
270
 
 
271
 
    def __str__(self):
272
 
        ret = self.get_header() 
273
 
        ret += "".join([str(h) for h in self.hunks])
274
 
        return ret
275
 
 
276
 
    def get_header(self):
277
 
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
278
 
 
279
 
    def stats_str(self):
280
 
        """Return a string of patch statistics"""
281
 
        removes = 0
282
 
        inserts = 0
283
 
        for hunk in self.hunks:
284
 
            for line in hunk.lines:
285
 
                if isinstance(line, InsertLine):
286
 
                     inserts+=1;
287
 
                elif isinstance(line, RemoveLine):
288
 
                     removes+=1;
289
 
        return "%i inserts, %i removes in %i hunks" % \
290
 
            (inserts, removes, len(self.hunks))
291
 
 
292
 
    def pos_in_mod(self, position):
293
 
        newpos = position
294
 
        for hunk in self.hunks:
295
 
            shift = hunk.shift_to_mod(position)
296
 
            if shift is None:
297
 
                return None
298
 
            newpos += shift
299
 
        return newpos
300
 
            
301
 
    def iter_inserted(self):
302
 
        """Iteraties through inserted lines
303
 
        
304
 
        :return: Pair of line number, line
305
 
        :rtype: iterator of (int, InsertLine)
306
 
        """
307
 
        for hunk in self.hunks:
308
 
            pos = hunk.mod_pos - 1;
309
 
            for line in hunk.lines:
310
 
                if isinstance(line, InsertLine):
311
 
                    yield (pos, line)
312
 
                    pos += 1
313
 
                if isinstance(line, ContextLine):
314
 
                    pos += 1
315
 
 
316
 
 
317
 
def parse_patch(iter_lines):
318
 
    (orig_name, mod_name) = get_patch_names(iter_lines)
319
 
    patch = Patch(orig_name, mod_name)
320
 
    for hunk in iter_hunks(iter_lines):
321
 
        patch.hunks.append(hunk)
322
 
    return patch
323
 
 
324
 
 
325
 
def iter_file_patch(iter_lines):
326
 
    saved_lines = []
327
 
    orig_range = 0
328
 
    for line in iter_lines:
329
 
        if line.startswith('=== ') or line.startswith('*** '):
330
 
            continue
331
 
        if line.startswith('#'):
332
 
            continue
333
 
        elif orig_range > 0:
334
 
            if line.startswith('-') or line.startswith(' '):
335
 
                orig_range -= 1
336
 
        elif line.startswith('--- '):
337
 
            if len(saved_lines) > 0:
338
 
                yield saved_lines
339
 
            saved_lines = []
340
 
        elif line.startswith('@@'):
341
 
            hunk = hunk_from_header(line)
342
 
            orig_range = hunk.orig_range
343
 
        saved_lines.append(line)
344
 
    if len(saved_lines) > 0:
345
 
        yield saved_lines
346
 
 
347
 
 
348
 
def iter_lines_handle_nl(iter_lines):
349
 
    """
350
 
    Iterates through lines, ensuring that lines that originally had no
351
 
    terminating \n are produced without one.  This transformation may be
352
 
    applied at any point up until hunk line parsing, and is safe to apply
353
 
    repeatedly.
354
 
    """
355
 
    last_line = None
356
 
    for line in iter_lines:
357
 
        if line == NO_NL:
358
 
            if not last_line.endswith('\n'):
359
 
                raise AssertionError()
360
 
            last_line = last_line[:-1]
361
 
            line = None
362
 
        if last_line is not None:
363
 
            yield last_line
364
 
        last_line = line
365
 
    if last_line is not None:
366
 
        yield last_line
367
 
 
368
 
 
369
 
def parse_patches(iter_lines):
370
 
    iter_lines = iter_lines_handle_nl(iter_lines)
371
 
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
372
 
 
373
 
 
374
 
def difference_index(atext, btext):
375
 
    """Find the indext of the first character that differs between two texts
376
 
 
377
 
    :param atext: The first text
378
 
    :type atext: str
379
 
    :param btext: The second text
380
 
    :type str: str
381
 
    :return: The index, or None if there are no differences within the range
382
 
    :rtype: int or NoneType
383
 
    """
384
 
    length = len(atext)
385
 
    if len(btext) < length:
386
 
        length = len(btext)
387
 
    for i in range(length):
388
 
        if atext[i] != btext[i]:
389
 
            return i;
390
 
    return None
391
 
 
392
 
 
393
 
def iter_patched(orig_lines, patch_lines):
394
 
    """Iterate through a series of lines with a patch applied.
395
 
    This handles a single file, and does exact, not fuzzy patching.
396
 
    """
397
 
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
398
 
    get_patch_names(patch_lines)
399
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
400
 
 
401
 
 
402
 
def iter_patched_from_hunks(orig_lines, hunks):
403
 
    """Iterate through a series of lines with a patch applied.
404
 
    This handles a single file, and does exact, not fuzzy patching.
405
 
 
406
 
    :param orig_lines: The unpatched lines.
407
 
    :param hunks: An iterable of Hunk instances.
408
 
    """
409
 
    seen_patch = []
410
 
    line_no = 1
411
 
    if orig_lines is not None:
412
 
        orig_lines = iter(orig_lines)
413
 
    for hunk in hunks:
414
 
        while line_no < hunk.orig_pos:
415
 
            orig_line = orig_lines.next()
416
 
            yield orig_line
417
 
            line_no += 1
418
 
        for hunk_line in hunk.lines:
419
 
            seen_patch.append(str(hunk_line))
420
 
            if isinstance(hunk_line, InsertLine):
421
 
                yield hunk_line.contents
422
 
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
423
 
                orig_line = orig_lines.next()
424
 
                if orig_line != hunk_line.contents:
425
 
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
426
 
                if isinstance(hunk_line, ContextLine):
427
 
                    yield orig_line
428
 
                else:
429
 
                    if not isinstance(hunk_line, RemoveLine):
430
 
                        raise AssertionError(hunk_line)
431
 
                line_no += 1
432
 
    if orig_lines is not None:
433
 
        for line in orig_lines:
434
 
            yield line