/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Alexander Belchenko
  • Date: 2006-06-29 08:41:31 UTC
  • mto: (1860.1.1 win32.installer)
  • mto: This revision was merged to the branch mainline in revision 1906.
  • Revision ID: bialix@ukr.net-20060629084131-3ea4d44e3204e36f
win32 installer for bzr.dev.0.9

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
 
1
# Copyright (C) 2004 - 2006 Aaron Bentley
2
2
# <aaron.bentley@utoronto.ca>
3
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
 
 
18
 
from __future__ import absolute_import
19
 
 
20
 
from .errors import (
21
 
    BzrError,
22
 
    )
23
 
 
24
 
import os
25
 
import re
26
 
 
27
 
 
28
 
binary_files_re = b'Binary files (.*) and (.*) differ\n'
29
 
 
30
 
 
31
 
class PatchSyntax(BzrError):
32
 
    """Base class for patch syntax errors."""
33
 
 
34
 
 
35
 
class BinaryFiles(BzrError):
36
 
 
37
 
    _fmt = 'Binary files section encountered.'
38
 
 
39
 
    def __init__(self, orig_name, mod_name):
40
 
        self.orig_name = orig_name
41
 
        self.mod_name = mod_name
 
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)
42
22
 
43
23
 
44
24
class MalformedPatchHeader(PatchSyntax):
45
 
 
46
 
    _fmt = "Malformed patch header.  %(desc)s\n%(line)r"
47
 
 
48
 
    def __init__(self, desc, line):
49
 
        self.desc = desc
50
 
        self.line = line
 
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)
51
38
 
52
39
 
53
40
class MalformedLine(PatchSyntax):
54
 
 
55
 
    _fmt = "Malformed line.  %(desc)s\n%(line)r"
56
 
 
57
41
    def __init__(self, desc, line):
58
42
        self.desc = desc
59
43
        self.line = line
60
 
 
61
 
 
62
 
class PatchConflict(BzrError):
63
 
 
64
 
    _fmt = ('Text contents mismatch at line %(line_no)d.  Original has '
65
 
            '"%(orig_line)s", but patch says it should be "%(patch_line)s"')
66
 
 
 
44
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
45
        PatchSyntax.__init__(self, msg)
 
46
 
 
47
 
 
48
class PatchConflict(Exception):
67
49
    def __init__(self, line_no, orig_line, patch_line):
68
 
        self.line_no = line_no
69
 
        self.orig_line = orig_line.rstrip('\n')
70
 
        self.patch_line = patch_line.rstrip('\n')
71
 
 
72
 
 
73
 
class MalformedHunkHeader(PatchSyntax):
74
 
 
75
 
    _fmt = "Malformed hunk header.  %(desc)s\n%(line)r"
76
 
 
77
 
    def __init__(self, desc, line):
78
 
        self.desc = desc
79
 
        self.line = 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)
80
55
 
81
56
 
82
57
def get_patch_names(iter_lines):
83
 
    line = next(iter_lines)
84
58
    try:
85
 
        match = re.match(binary_files_re, line)
86
 
        if match is not None:
87
 
            raise BinaryFiles(match.group(1), match.group(2))
88
 
        if not line.startswith(b"--- "):
 
59
        line = iter_lines.next()
 
60
        if not line.startswith("--- "):
89
61
            raise MalformedPatchHeader("No orig name", line)
90
62
        else:
91
 
            orig_name = line[4:].rstrip(b"\n")
 
63
            orig_name = line[4:].rstrip("\n")
92
64
    except StopIteration:
93
65
        raise MalformedPatchHeader("No orig line", "")
94
66
    try:
95
 
        line = next(iter_lines)
96
 
        if not line.startswith(b"+++ "):
 
67
        line = iter_lines.next()
 
68
        if not line.startswith("+++ "):
97
69
            raise PatchSyntax("No mod name")
98
70
        else:
99
 
            mod_name = line[4:].rstrip(b"\n")
 
71
            mod_name = line[4:].rstrip("\n")
100
72
    except StopIteration:
101
73
        raise MalformedPatchHeader("No mod line", "")
102
74
    return (orig_name, mod_name)
110
82
    :return: the position and range, as a tuple
111
83
    :rtype: (int, int)
112
84
    """
113
 
    tmp = textrange.split(b',')
 
85
    tmp = textrange.split(',')
114
86
    if len(tmp) == 1:
115
87
        pos = tmp[0]
116
 
        range = b"1"
 
88
        range = "1"
117
89
    else:
118
90
        (pos, range) = tmp
119
91
    pos = int(pos)
120
92
    range = int(range)
121
93
    return (pos, range)
122
94
 
123
 
 
 
95
 
124
96
def hunk_from_header(line):
125
 
    import re
126
 
    matches = re.match(br'\@\@ ([^@]*) \@\@( (.*))?\n', line)
127
 
    if matches is None:
128
 
        raise MalformedHunkHeader("Does not match format.", line)
 
97
    if not line.startswith("@@") or not line.endswith("@@\n") \
 
98
        or not len(line) > 4:
 
99
        raise MalformedHunkHeader("Does not start and end with @@.", line)
129
100
    try:
130
 
        (orig, mod) = matches.group(1).split(b" ")
131
 
    except (ValueError, IndexError) as e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
132
103
        raise MalformedHunkHeader(str(e), line)
133
 
    if not orig.startswith(b'-') or not mod.startswith(b'+'):
 
104
    if not orig.startswith('-') or not mod.startswith('+'):
134
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
135
106
    try:
136
107
        (orig_pos, orig_range) = parse_range(orig[1:])
137
108
        (mod_pos, mod_range) = parse_range(mod[1:])
138
 
    except (ValueError, IndexError) as e:
 
109
    except Exception, e:
139
110
        raise MalformedHunkHeader(str(e), line)
140
111
    if mod_range < 0 or orig_range < 0:
141
112
        raise MalformedHunkHeader("Hunk range is negative", line)
142
 
    tail = matches.group(3)
143
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
144
 
 
145
 
 
146
 
class HunkLine(object):
147
 
 
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
 
114
 
 
115
 
 
116
class HunkLine:
148
117
    def __init__(self, contents):
149
118
        self.contents = contents
150
119
 
151
120
    def get_str(self, leadchar):
152
 
        if self.contents == b"\n" and leadchar == b" " and False:
153
 
            return b"\n"
154
 
        if not self.contents.endswith(b'\n'):
155
 
            terminator = b'\n' + NO_NL
 
121
        if self.contents == "\n" and leadchar == " " and False:
 
122
            return "\n"
 
123
        if not self.contents.endswith('\n'):
 
124
            terminator = '\n' + NO_NL
156
125
        else:
157
 
            terminator = b''
 
126
            terminator = ''
158
127
        return leadchar + self.contents + terminator
159
128
 
160
 
    def as_bytes(self):
161
 
        raise NotImplementedError
162
 
 
163
129
 
164
130
class ContextLine(HunkLine):
165
 
 
166
131
    def __init__(self, contents):
167
132
        HunkLine.__init__(self, contents)
168
133
 
169
 
    def as_bytes(self):
170
 
        return self.get_str(b" ")
 
134
    def __str__(self):
 
135
        return self.get_str(" ")
171
136
 
172
137
 
173
138
class InsertLine(HunkLine):
174
139
    def __init__(self, contents):
175
140
        HunkLine.__init__(self, contents)
176
141
 
177
 
    def as_bytes(self):
178
 
        return self.get_str(b"+")
 
142
    def __str__(self):
 
143
        return self.get_str("+")
179
144
 
180
145
 
181
146
class RemoveLine(HunkLine):
182
147
    def __init__(self, contents):
183
148
        HunkLine.__init__(self, contents)
184
149
 
185
 
    def as_bytes(self):
186
 
        return self.get_str(b"-")
187
 
 
188
 
 
189
 
NO_NL = b'\\ No newline at end of file\n'
190
 
__pychecker__ = "no-returnvalues"
191
 
 
 
150
    def __str__(self):
 
151
        return self.get_str("-")
 
152
 
 
153
NO_NL = '\\ No newline at end of file\n'
 
154
__pychecker__="no-returnvalues"
192
155
 
193
156
def parse_line(line):
194
 
    if line.startswith(b"\n"):
 
157
    if line.startswith("\n"):
195
158
        return ContextLine(line)
196
 
    elif line.startswith(b" "):
 
159
    elif line.startswith(" "):
197
160
        return ContextLine(line[1:])
198
 
    elif line.startswith(b"+"):
 
161
    elif line.startswith("+"):
199
162
        return InsertLine(line[1:])
200
 
    elif line.startswith(b"-"):
 
163
    elif line.startswith("-"):
201
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
202
167
    else:
203
168
        raise MalformedLine("Unknown line type", line)
204
 
 
205
 
 
206
 
__pychecker__ = ""
207
 
 
208
 
 
209
 
class Hunk(object):
210
 
 
211
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
 
169
__pychecker__=""
 
170
 
 
171
 
 
172
class Hunk:
 
173
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
212
174
        self.orig_pos = orig_pos
213
175
        self.orig_range = orig_range
214
176
        self.mod_pos = mod_pos
215
177
        self.mod_range = mod_range
216
 
        self.tail = tail
217
178
        self.lines = []
218
179
 
219
180
    def get_header(self):
220
 
        if self.tail is None:
221
 
            tail_str = b''
222
 
        else:
223
 
            tail_str = b' ' + self.tail
224
 
        return b"@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
225
 
                                                      self.orig_range),
226
 
                                       self.range_str(self.mod_pos,
227
 
                                                      self.mod_range),
228
 
                                       tail_str)
 
181
        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
 
182
                                                   self.orig_range),
 
183
                                    self.range_str(self.mod_pos, 
 
184
                                                   self.mod_range))
229
185
 
230
186
    def range_str(self, pos, range):
231
187
        """Return a file range, special-casing for 1-line files.
237
193
        :return: a string in the format 1,4 except when range == pos == 1
238
194
        """
239
195
        if range == 1:
240
 
            return b"%i" % pos
 
196
            return "%i" % pos
241
197
        else:
242
 
            return b"%i,%i" % (pos, range)
 
198
            return "%i,%i" % (pos, range)
243
199
 
244
 
    def as_bytes(self):
 
200
    def __str__(self):
245
201
        lines = [self.get_header()]
246
202
        for line in self.lines:
247
 
            lines.append(line.as_bytes())
248
 
        return b"".join(lines)
249
 
 
250
 
    __bytes__ = as_bytes
 
203
            lines.append(str(line))
 
204
        return "".join(lines)
251
205
 
252
206
    def shift_to_mod(self, pos):
253
 
        if pos < self.orig_pos - 1:
 
207
        if pos < self.orig_pos-1:
254
208
            return 0
255
 
        elif pos > self.orig_pos + self.orig_range:
 
209
        elif pos > self.orig_pos+self.orig_range:
256
210
            return self.mod_range - self.orig_range
257
211
        else:
258
212
            return self.shift_to_mod_lines(pos)
259
213
 
260
214
    def shift_to_mod_lines(self, pos):
261
 
        position = self.orig_pos - 1
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
 
216
        position = self.orig_pos-1
262
217
        shift = 0
263
218
        for line in self.lines:
264
219
            if isinstance(line, InsertLine):
275
230
        return shift
276
231
 
277
232
 
278
 
def iter_hunks(iter_lines, allow_dirty=False):
279
 
    '''
280
 
    :arg iter_lines: iterable of lines to parse for hunks
281
 
    :kwarg allow_dirty: If True, when we encounter something that is not
282
 
        a hunk header when we're looking for one, assume the rest of the lines
283
 
        are not part of the patch (comments or other junk).  Default False
284
 
    '''
 
233
def iter_hunks(iter_lines):
285
234
    hunk = None
286
235
    for line in iter_lines:
287
 
        if line == b"\n":
 
236
        if line == "\n":
288
237
            if hunk is not None:
289
238
                yield hunk
290
239
                hunk = None
291
240
            continue
292
241
        if hunk is not None:
293
242
            yield hunk
294
 
        try:
295
 
            hunk = hunk_from_header(line)
296
 
        except MalformedHunkHeader:
297
 
            if allow_dirty:
298
 
                # If the line isn't a hunk header, then we've reached the end
299
 
                # of this patch and there's "junk" at the end.  Ignore the
300
 
                # rest of this patch.
301
 
                return
302
 
            raise
 
243
        hunk = hunk_from_header(line)
303
244
        orig_size = 0
304
245
        mod_size = 0
305
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
306
 
            hunk_line = parse_line(next(iter_lines))
 
247
            hunk_line = parse_line(iter_lines.next())
307
248
            hunk.lines.append(hunk_line)
308
249
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
309
250
                orig_size += 1
313
254
        yield hunk
314
255
 
315
256
 
316
 
class BinaryPatch(object):
317
 
 
 
257
class Patch:
318
258
    def __init__(self, oldname, newname):
319
259
        self.oldname = oldname
320
260
        self.newname = newname
321
 
 
322
 
    def as_bytes(self):
323
 
        return b'Binary files %s and %s differ\n' % (self.oldname, self.newname)
324
 
 
325
 
 
326
 
class Patch(BinaryPatch):
327
 
 
328
 
    def __init__(self, oldname, newname):
329
 
        BinaryPatch.__init__(self, oldname, newname)
330
261
        self.hunks = []
331
262
 
332
 
    def as_bytes(self):
333
 
        ret = self.get_header()
334
 
        ret += b"".join([h.as_bytes() for h in self.hunks])
 
263
    def __str__(self):
 
264
        ret = self.get_header() 
 
265
        ret += "".join([str(h) for h in self.hunks])
335
266
        return ret
336
267
 
337
268
    def get_header(self):
338
 
        return b"--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
339
270
 
340
 
    def stats_values(self):
341
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
342
273
        removes = 0
343
274
        inserts = 0
344
275
        for hunk in self.hunks:
345
276
            for line in hunk.lines:
346
277
                if isinstance(line, InsertLine):
347
 
                    inserts += 1
 
278
                     inserts+=1;
348
279
                elif isinstance(line, RemoveLine):
349
 
                    removes += 1
350
 
        return (inserts, removes, len(self.hunks))
351
 
 
352
 
    def stats_str(self):
353
 
        """Return a string of patch statistics"""
 
280
                     removes+=1;
354
281
        return "%i inserts, %i removes in %i hunks" % \
355
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
356
283
 
357
284
    def pos_in_mod(self, position):
358
285
        newpos = position
362
289
                return None
363
290
            newpos += shift
364
291
        return newpos
365
 
 
 
292
            
366
293
    def iter_inserted(self):
367
294
        """Iteraties through inserted lines
368
 
 
 
295
        
369
296
        :return: Pair of line number, line
370
297
        :rtype: iterator of (int, InsertLine)
371
298
        """
372
299
        for hunk in self.hunks:
373
 
            pos = hunk.mod_pos - 1
 
300
            pos = hunk.mod_pos - 1;
374
301
            for line in hunk.lines:
375
302
                if isinstance(line, InsertLine):
376
303
                    yield (pos, line)
379
306
                    pos += 1
380
307
 
381
308
 
382
 
def parse_patch(iter_lines, allow_dirty=False):
383
 
    '''
384
 
    :arg iter_lines: iterable of lines to parse
385
 
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
386
 
        Default False
387
 
    '''
388
 
    iter_lines = iter_lines_handle_nl(iter_lines)
389
 
    try:
390
 
        (orig_name, mod_name) = get_patch_names(iter_lines)
391
 
    except BinaryFiles as e:
392
 
        return BinaryPatch(e.orig_name, e.mod_name)
393
 
    else:
394
 
        patch = Patch(orig_name, mod_name)
395
 
        for hunk in iter_hunks(iter_lines, allow_dirty):
396
 
            patch.hunks.append(hunk)
397
 
        return patch
398
 
 
399
 
 
400
 
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
401
 
    '''
402
 
    :arg iter_lines: iterable of lines to parse for patches
403
 
    :kwarg allow_dirty: If True, allow comments and other non-patch text
404
 
        before the first patch.  Note that the algorithm here can only find
405
 
        such text before any patches have been found.  Comments after the
406
 
        first patch are stripped away in iter_hunks() if it is also passed
407
 
        allow_dirty=True.  Default False.
408
 
    '''
409
 
    # FIXME: Docstring is not quite true.  We allow certain comments no
410
 
    # matter what, If they startwith '===', '***', or '#' Someone should
411
 
    # reexamine this logic and decide if we should include those in
412
 
    # allow_dirty or restrict those to only being before the patch is found
413
 
    # (as allow_dirty does).
414
 
    regex = re.compile(binary_files_re)
 
309
def parse_patch(iter_lines):
 
310
    (orig_name, mod_name) = get_patch_names(iter_lines)
 
311
    patch = Patch(orig_name, mod_name)
 
312
    for hunk in iter_hunks(iter_lines):
 
313
        patch.hunks.append(hunk)
 
314
    return patch
 
315
 
 
316
 
 
317
def iter_file_patch(iter_lines):
415
318
    saved_lines = []
416
 
    dirty_head = []
417
 
    orig_range = 0
418
 
    beginning = True
419
 
 
420
319
    for line in iter_lines:
421
 
        if line.startswith(b'=== '):
 
320
        if line.startswith('=== ') or line.startswith('*** '):
 
321
            continue
 
322
        if line.startswith('#'):
 
323
            continue
 
324
        elif line.startswith('--- '):
422
325
            if len(saved_lines) > 0:
423
 
                if keep_dirty and len(dirty_head) > 0:
424
 
                    yield {'saved_lines': saved_lines,
425
 
                           'dirty_head': dirty_head}
426
 
                    dirty_head = []
427
 
                else:
428
 
                    yield saved_lines
429
 
                saved_lines = []
430
 
            dirty_head.append(line)
431
 
            continue
432
 
        if line.startswith(b'*** '):
433
 
            continue
434
 
        if line.startswith(b'#'):
435
 
            continue
436
 
        elif orig_range > 0:
437
 
            if line.startswith(b'-') or line.startswith(b' '):
438
 
                orig_range -= 1
439
 
        elif line.startswith(b'--- ') or regex.match(line):
440
 
            if allow_dirty and beginning:
441
 
                # Patches can have "junk" at the beginning
442
 
                # Stripping junk from the end of patches is handled when we
443
 
                # parse the patch
444
 
                beginning = False
445
 
            elif len(saved_lines) > 0:
446
 
                if keep_dirty and len(dirty_head) > 0:
447
 
                    yield {'saved_lines': saved_lines,
448
 
                           'dirty_head': dirty_head}
449
 
                    dirty_head = []
450
 
                else:
451
 
                    yield saved_lines
 
326
                yield saved_lines
452
327
            saved_lines = []
453
 
        elif line.startswith(b'@@'):
454
 
            hunk = hunk_from_header(line)
455
 
            orig_range = hunk.orig_range
456
328
        saved_lines.append(line)
457
329
    if len(saved_lines) > 0:
458
 
        if keep_dirty and len(dirty_head) > 0:
459
 
            yield {'saved_lines': saved_lines,
460
 
                   'dirty_head': dirty_head}
461
 
        else:
462
 
            yield saved_lines
 
330
        yield saved_lines
463
331
 
464
332
 
465
333
def iter_lines_handle_nl(iter_lines):
472
340
    last_line = None
473
341
    for line in iter_lines:
474
342
        if line == NO_NL:
475
 
            if not last_line.endswith(b'\n'):
476
 
                raise AssertionError()
 
343
            assert last_line.endswith('\n')
477
344
            last_line = last_line[:-1]
478
345
            line = None
479
346
        if last_line is not None:
483
350
        yield last_line
484
351
 
485
352
 
486
 
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
487
 
    '''
488
 
    :arg iter_lines: iterable of lines to parse for patches
489
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
490
 
        selected places.  This includes comments before and after a patch
491
 
        for instance.  Default False.
492
 
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
493
 
        Default False.
494
 
    '''
495
 
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
496
 
        if 'dirty_head' in patch_lines:
497
 
            yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
498
 
                    'dirty_head': patch_lines['dirty_head']})
499
 
        else:
500
 
            yield parse_patch(patch_lines, allow_dirty)
 
353
def parse_patches(iter_lines):
 
354
    iter_lines = iter_lines_handle_nl(iter_lines)
 
355
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
501
356
 
502
357
 
503
358
def difference_index(atext, btext):
515
370
        length = len(btext)
516
371
    for i in range(length):
517
372
        if atext[i] != btext[i]:
518
 
            return i
 
373
            return i;
519
374
    return None
520
375
 
521
376
 
523
378
    """Iterate through a series of lines with a patch applied.
524
379
    This handles a single file, and does exact, not fuzzy patching.
525
380
    """
526
 
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
381
    if orig_lines is not None:
 
382
        orig_lines = orig_lines.__iter__()
 
383
    seen_patch = []
 
384
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
527
385
    get_patch_names(patch_lines)
528
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
529
 
 
530
 
 
531
 
def iter_patched_from_hunks(orig_lines, hunks):
532
 
    """Iterate through a series of lines with a patch applied.
533
 
    This handles a single file, and does exact, not fuzzy patching.
534
 
 
535
 
    :param orig_lines: The unpatched lines.
536
 
    :param hunks: An iterable of Hunk instances.
537
 
    """
538
 
    seen_patch = []
539
386
    line_no = 1
540
 
    if orig_lines is not None:
541
 
        orig_lines = iter(orig_lines)
542
 
    for hunk in hunks:
 
387
    for hunk in iter_hunks(patch_lines):
543
388
        while line_no < hunk.orig_pos:
544
 
            orig_line = next(orig_lines)
 
389
            orig_line = orig_lines.next()
545
390
            yield orig_line
546
391
            line_no += 1
547
392
        for hunk_line in hunk.lines:
548
 
            seen_patch.append(hunk_line.contents)
 
393
            seen_patch.append(str(hunk_line))
549
394
            if isinstance(hunk_line, InsertLine):
550
395
                yield hunk_line.contents
551
396
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
552
 
                orig_line = next(orig_lines)
 
397
                orig_line = orig_lines.next()
553
398
                if orig_line != hunk_line.contents:
554
 
                    raise PatchConflict(line_no, orig_line,
555
 
                                        b''.join(seen_patch))
 
399
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
556
400
                if isinstance(hunk_line, ContextLine):
557
401
                    yield orig_line
558
402
                else:
559
 
                    if not isinstance(hunk_line, RemoveLine):
560
 
                        raise AssertionError(hunk_line)
 
403
                    assert isinstance(hunk_line, RemoveLine)
561
404
                line_no += 1
562
405
    if orig_lines is not None:
563
406
        for line in orig_lines:
564
407
            yield line
565
 
 
566
 
 
567
 
def apply_patches(tt, patches, prefix=1):
568
 
    """Apply patches to a TreeTransform.
569
 
 
570
 
    :param tt: TreeTransform instance
571
 
    :param patches: List of patches
572
 
    :param prefix: Number leading path segments to strip
573
 
    """
574
 
    def strip_prefix(p):
575
 
        return '/'.join(p.split('/')[1:])
576
 
 
577
 
    from breezy.bzr.generate_ids import gen_file_id
578
 
    # TODO(jelmer): Extract and set mode
579
 
    for patch in patches:
580
 
        if patch.oldname == b'/dev/null':
581
 
            trans_id = None
582
 
            orig_contents = b''
583
 
        else:
584
 
            oldname = strip_prefix(patch.oldname.decode())
585
 
            trans_id = tt.trans_id_tree_path(oldname)
586
 
            orig_contents = tt._tree.get_file_text(oldname)
587
 
            tt.delete_contents(trans_id)
588
 
 
589
 
        if patch.newname != b'/dev/null':
590
 
            newname = strip_prefix(patch.newname.decode())
591
 
            new_contents = iter_patched_from_hunks(
592
 
                orig_contents.splitlines(True), patch.hunks)
593
 
            if trans_id is None:
594
 
                parts = os.path.split(newname)
595
 
                trans_id = tt.root
596
 
                for part in parts[1:-1]:
597
 
                    trans_id = tt.new_directory(part, trans_id)
598
 
                tt.new_file(
599
 
                    parts[-1], trans_id, new_contents,
600
 
                    file_id=gen_file_id(newname))
601
 
            else:
602
 
                tt.create_file(new_contents, trans_id)
603
 
 
604
 
 
605
 
class AppliedPatches(object):
606
 
    """Context that provides access to a tree with patches applied.
607
 
    """
608
 
 
609
 
    def __init__(self, tree, patches, prefix=1):
610
 
        self.tree = tree
611
 
        self.patches = patches
612
 
        self.prefix = prefix
613
 
 
614
 
    def __enter__(self):
615
 
        from .transform import TransformPreview
616
 
        self._tt = TransformPreview(self.tree)
617
 
        apply_patches(self._tt, self.patches, prefix=self.prefix)
618
 
        return self._tt.get_preview_tree()
619
 
 
620
 
    def __exit__(self, exc_type, exc_value, exc_tb):
621
 
        self._tt.finalize()
622
 
        return False