~bzr-pqm/bzr/bzr.dev

2052.3.2 by John Arbash Meinel
Change Copyright .. by Canonical to Copyright ... Canonical
1
# Copyright (C) 2006 Canonical Ltd
1711.3.2 by John Arbash Meinel
Add the read_bundle_from_url command, which handles lots of exceptions
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17
"""Read in a bundle stream, and process it into a BundleReader object."""
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
18
1185.82.96 by Aaron Bentley
Got first binary test passing
19
import base64
1185.82.78 by Aaron Bentley
Cleanups
20
from cStringIO import StringIO
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
21
import os
22
import pprint
23
1711.3.2 by John Arbash Meinel
Add the read_bundle_from_url command, which handles lots of exceptions
24
import bzrlib.errors
1185.82.131 by Aaron Bentley
Move BadBundle error (and subclasses) to errors.py
25
from bzrlib.errors import (TestamentMismatch, BzrError, 
1185.82.139 by Aaron Bentley
Raise NotABundle when a non-bundle is supplied
26
                           MalformedHeader, MalformedPatches, NotABundle)
0.5.116 by John Arbash Meinel
Fixed a bug based on the new InventoryEntry separation.
27
from bzrlib.inventory import (Inventory, InventoryEntry,
28
                              InventoryDirectory, InventoryFile,
1731.1.55 by Aaron Bentley
Fix bundle handling
29
                              InventoryLink)
1711.4.19 by John Arbash Meinel
Bundles were still using os.path.join to compute paths rather than osutils.pathjoin
30
from bzrlib.osutils import sha_file, sha_string, pathjoin
1185.82.78 by Aaron Bentley
Cleanups
31
from bzrlib.revision import Revision, NULL_REVISION
1185.82.116 by Aaron Bentley
Introduce StrictTestament, get test failing for the right reasons
32
from bzrlib.testament import StrictTestament
1185.82.78 by Aaron Bentley
Cleanups
33
from bzrlib.trace import mutter, warning
1711.3.2 by John Arbash Meinel
Add the read_bundle_from_url command, which handles lots of exceptions
34
import bzrlib.transport
1185.82.78 by Aaron Bentley
Cleanups
35
from bzrlib.tree import Tree
1711.3.2 by John Arbash Meinel
Add the read_bundle_from_url command, which handles lots of exceptions
36
import bzrlib.urlutils
1185.82.78 by Aaron Bentley
Cleanups
37
from bzrlib.xml5 import serializer_v5
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
38
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
39
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
40
class RevisionInfo(object):
41
    """Gets filled out for each revision object that is read.
42
    """
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
43
    def __init__(self, revision_id):
44
        self.revision_id = revision_id
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
45
        self.sha1 = None
46
        self.committer = None
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
47
        self.date = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
48
        self.timestamp = None
49
        self.timezone = None
50
        self.inventory_sha1 = None
51
1185.82.27 by Aaron Bentley
Fixed most revision attribute handling
52
        self.parent_ids = None
1185.82.74 by Aaron Bentley
Allow custom base for any revision
53
        self.base_id = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
54
        self.message = None
1185.82.27 by Aaron Bentley
Fixed most revision attribute handling
55
        self.properties = None
1185.82.77 by Aaron Bentley
Move tree actions to RevisionInfo
56
        self.tree_actions = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
57
58
    def __str__(self):
59
        return pprint.pformat(self.__dict__)
60
0.5.37 by John Arbash Meinel
Made read_changeset able to spit out 'Revision' entities.
61
    def as_revision(self):
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
62
        rev = Revision(revision_id=self.revision_id,
0.5.37 by John Arbash Meinel
Made read_changeset able to spit out 'Revision' entities.
63
            committer=self.committer,
64
            timestamp=float(self.timestamp),
65
            timezone=int(self.timezone),
66
            inventory_sha1=self.inventory_sha1,
67
            message='\n'.join(self.message))
68
1185.82.28 by Aaron Bentley
Got parent_id handling working
69
        if self.parent_ids:
70
            rev.parent_ids.extend(self.parent_ids)
1185.82.35 by Aaron Bentley
Read revision properties
71
1185.82.59 by Aaron Bentley
Behave properly when there are no properties in a revision
72
        if self.properties:
73
            for property in self.properties:
74
                key_end = property.find(': ')
75
                assert key_end is not None
76
                key = property[:key_end].encode('utf-8')
77
                value = property[key_end+2:].encode('utf-8')
78
                rev.properties[key] = value
1185.82.35 by Aaron Bentley
Read revision properties
79
0.5.37 by John Arbash Meinel
Made read_changeset able to spit out 'Revision' entities.
80
        return rev
81
1185.82.123 by Aaron Bentley
Cleanups to prepare for review
82
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
83
class BundleInfo(object):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
84
    """This contains the meta information. Stuff that allows you to
85
    recreate the revision or inventory XML.
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
86
    """
87
    def __init__(self):
88
        self.committer = None
89
        self.date = None
0.5.17 by John Arbash Meinel
adding apply-changset, plus more meta information.
90
        self.message = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
91
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
92
        # A list of RevisionInfo objects
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
93
        self.revisions = []
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
94
95
        # The next entries are created during complete_info() and
96
        # other post-read functions.
97
98
        # A list of real Revision objects
99
        self.real_revisions = []
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
100
101
        self.timestamp = None
102
        self.timezone = None
0.5.15 by John Arbash Meinel
Created an apply-changeset function, and modified output for better parsing.
103
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
104
    def __str__(self):
105
        return pprint.pformat(self.__dict__)
106
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
107
    def complete_info(self):
108
        """This makes sure that all information is properly
109
        split up, based on the assumptions that can be made
110
        when information is missing.
111
        """
1793.3.8 by John Arbash Meinel
Removed duplicated highres date code.
112
        from bzrlib.bundle.serializer import unpack_highres_date
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
113
        # Put in all of the guessable information.
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
114
        if not self.timestamp and self.date:
0.5.81 by John Arbash Meinel
Cleaning up from pychecker.
115
            self.timestamp, self.timezone = unpack_highres_date(self.date)
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
116
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
117
        self.real_revisions = []
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
118
        for rev in self.revisions:
0.5.60 by John Arbash Meinel
read_changeset now parses the date: subheader of revisions correctly.
119
            if rev.timestamp is None:
120
                if rev.date is not None:
121
                    rev.timestamp, rev.timezone = \
0.5.81 by John Arbash Meinel
Cleaning up from pychecker.
122
                            unpack_highres_date(rev.date)
0.5.60 by John Arbash Meinel
read_changeset now parses the date: subheader of revisions correctly.
123
                else:
124
                    rev.timestamp = self.timestamp
125
                    rev.timezone = self.timezone
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
126
            if rev.message is None and self.message:
127
                rev.message = self.message
128
            if rev.committer is None and self.committer:
129
                rev.committer = self.committer
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
130
            self.real_revisions.append(rev.as_revision())
131
1185.82.48 by Aaron Bentley
Inching closer to supporting multiple revisions per changeset
132
    def get_base(self, revision):
1185.82.74 by Aaron Bentley
Allow custom base for any revision
133
        revision_info = self.get_revision_info(revision.revision_id)
134
        if revision_info.base_id is not None:
135
            if revision_info.base_id == NULL_REVISION:
136
                return None
137
            else:
138
                return revision_info.base_id
1185.82.48 by Aaron Bentley
Inching closer to supporting multiple revisions per changeset
139
        if len(revision.parent_ids) == 0:
140
            # There is no base listed, and
141
            # the lowest revision doesn't have a parent
142
            # so this is probably against the empty tree
143
            # and thus base truly is None
144
            return None
145
        else:
1185.82.73 by Aaron Bentley
Use rightmost parent always
146
            return revision.parent_ids[-1]
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
147
0.5.67 by John Arbash Meinel
Working on apply_changeset
148
    def _get_target(self):
0.5.81 by John Arbash Meinel
Cleaning up from pychecker.
149
        """Return the target revision."""
0.5.67 by John Arbash Meinel
Working on apply_changeset
150
        if len(self.real_revisions) > 0:
1185.82.48 by Aaron Bentley
Inching closer to supporting multiple revisions per changeset
151
            return self.real_revisions[0].revision_id
0.5.67 by John Arbash Meinel
Working on apply_changeset
152
        elif len(self.revisions) > 0:
1185.82.48 by Aaron Bentley
Inching closer to supporting multiple revisions per changeset
153
            return self.revisions[0].revision_id
0.5.67 by John Arbash Meinel
Working on apply_changeset
154
        return None
155
156
    target = property(_get_target, doc='The target revision id')
157
1185.82.49 by Aaron Bentley
SPOT fixes, fix inventory validation
158
    def get_revision(self, revision_id):
159
        for r in self.real_revisions:
160
            if r.revision_id == revision_id:
161
                return r
162
        raise KeyError(revision_id)
163
164
    def get_revision_info(self, revision_id):
165
        for r in self.revisions:
166
            if r.revision_id == revision_id:
167
                return r
168
        raise KeyError(revision_id)
169
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
170
    def revision_tree(self, repository, revision_id, base=None):
171
        revision = self.get_revision(revision_id)
172
        base = self.get_base(revision)
173
        assert base != revision_id
174
        self._validate_references_from_repository(repository)
175
        revision_info = self.get_revision_info(revision_id)
176
        inventory_revision_id = revision_id
177
        bundle_tree = BundleTree(repository.revision_tree(base), 
178
                                  inventory_revision_id)
179
        self._update_tree(bundle_tree, revision_id)
180
181
        inv = bundle_tree.inventory
182
        self._validate_inventory(inv, revision_id)
183
        self._validate_revision(inv, revision_id)
184
185
        return bundle_tree
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
186
1185.82.15 by Aaron Bentley
Disabled validate_revisions (needs info it doesn't have), updated API to repos
187
    def _validate_references_from_repository(self, repository):
188
        """Now that we have a repository which should have some of the
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
189
        revisions we care about, go through and validate all of them
190
        that we can.
191
        """
192
        rev_to_sha = {}
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
193
        inv_to_sha = {}
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
194
        def add_sha(d, revision_id, sha1):
195
            if revision_id is None:
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
196
                if sha1 is not None:
197
                    raise BzrError('A Null revision should always'
198
                        'have a null sha1 hash')
199
                return
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
200
            if revision_id in d:
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
201
                # This really should have been validated as part
202
                # of _validate_revisions but lets do it again
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
203
                if sha1 != d[revision_id]:
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
204
                    raise BzrError('** Revision %r referenced with 2 different'
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
205
                            ' sha hashes %s != %s' % (revision_id,
206
                                sha1, d[revision_id]))
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
207
            else:
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
208
                d[revision_id] = sha1
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
209
210
        # All of the contained revisions were checked
211
        # in _validate_revisions
212
        checked = {}
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
213
        for rev_info in self.revisions:
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
214
            checked[rev_info.revision_id] = True
215
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
216
                
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
217
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
218
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
219
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
220
        count = 0
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
221
        missing = {}
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
222
        for revision_id, sha1 in rev_to_sha.iteritems():
1185.82.15 by Aaron Bentley
Disabled validate_revisions (needs info it doesn't have), updated API to repos
223
            if repository.has_revision(revision_id):
1185.82.121 by Aaron Bentley
Move calculation of Testament sha1s to Testament
224
                testament = StrictTestament.from_revision(repository, 
225
                                                          revision_id)
1910.2.55 by Aaron Bentley
Bundle 0.9 uses Testament 3 strict
226
                local_sha1 = self._testament_sha1_from_revision(repository,
227
                                                                revision_id)
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
228
                if sha1 != local_sha1:
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
229
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
230
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
231
                else:
232
                    count += 1
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
233
            elif revision_id not in checked:
234
                missing[revision_id] = sha1
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
235
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
236
        for inv_id, sha1 in inv_to_sha.iteritems():
1185.82.15 by Aaron Bentley
Disabled validate_revisions (needs info it doesn't have), updated API to repos
237
            if repository.has_revision(inv_id):
1185.82.123 by Aaron Bentley
Cleanups to prepare for review
238
                # Note: branch.get_inventory_sha1() just returns the value that
239
                # is stored in the revision text, and that value may be out
240
                # of date. This is bogus, because that means we aren't
241
                # validating the actual text, just that we wrote and read the
242
                # string. But for now, what the hell.
1185.82.15 by Aaron Bentley
Disabled validate_revisions (needs info it doesn't have), updated API to repos
243
                local_sha1 = repository.get_inventory_sha1(inv_id)
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
244
                if sha1 != local_sha1:
245
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
246
                                   'local: %s, bundle: %s' % 
247
                                   (inv_id, local_sha1, sha1))
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
248
                else:
249
                    count += 1
250
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
251
        if len(missing) > 0:
252
            # I don't know if this is an error yet
253
            warning('Not all revision hashes could be validated.'
254
                    ' Unable validate %d hashes' % len(missing))
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
255
        mutter('Verified %d sha hashes for the bundle.' % count)
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
256
1185.82.49 by Aaron Bentley
SPOT fixes, fix inventory validation
257
    def _validate_inventory(self, inv, revision_id):
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
258
        """At this point we should have generated the BundleTree,
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
259
        so build up an inventory, and make sure the hashes match.
260
        """
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
261
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
262
        assert inv is not None
263
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
264
        # Now we should have a complete inventory entry.
0.5.117 by John Arbash Meinel
Almost there. Just need to track down a few remaining bugs.
265
        s = serializer_v5.write_inventory_to_string(inv)
266
        sha1 = sha_string(s)
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
267
        # Target revision is the last entry in the real_revisions list
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
268
        rev = self.get_revision(revision_id)
1185.82.49 by Aaron Bentley
SPOT fixes, fix inventory validation
269
        assert rev.revision_id == revision_id
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
270
        if sha1 != rev.inventory_sha1:
0.5.117 by John Arbash Meinel
Almost there. Just need to track down a few remaining bugs.
271
            open(',,bogus-inv', 'wb').write(s)
1185.82.61 by Aaron Bentley
Downgrade inventory mismatch to warning (source can be inaccurate)
272
            warning('Inventory sha hash mismatch for revision %s. %s'
273
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
274
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
275
    def _validate_revision(self, inventory, revision_id):
276
        """Make sure all revision entries match their checksum."""
277
278
        # This is a mapping from each revision id to it's sha hash
279
        rev_to_sha1 = {}
280
        
281
        rev = self.get_revision(revision_id)
282
        rev_info = self.get_revision_info(revision_id)
283
        assert rev.revision_id == rev_info.revision_id
284
        assert rev.revision_id == revision_id
1910.2.55 by Aaron Bentley
Bundle 0.9 uses Testament 3 strict
285
        sha1 = self._testament_sha1(rev, inventory)
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
286
        if sha1 != rev_info.sha1:
287
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
1963.2.1 by Robey Pointer
remove usage of has_key()
288
        if rev.revision_id in rev_to_sha1:
1793.2.1 by Aaron Bentley
Move revision_tree into BundleInfo
289
            raise BzrError('Revision {%s} given twice in the list'
290
                    % (rev.revision_id))
291
        rev_to_sha1[rev.revision_id] = sha1
292
293
    def _update_tree(self, bundle_tree, revision_id):
294
        """This fills out a BundleTree based on the information
295
        that was read in.
296
297
        :param bundle_tree: A BundleTree to update with the new information.
298
        """
299
300
        def get_rev_id(last_changed, path, kind):
301
            if last_changed is not None:
302
                changed_revision_id = last_changed.decode('utf-8')
303
            else:
304
                changed_revision_id = revision_id
305
            bundle_tree.note_last_changed(path, changed_revision_id)
306
            return changed_revision_id
307
308
        def extra_info(info, new_path):
309
            last_changed = None
310
            encoding = None
311
            for info_item in info:
312
                try:
313
                    name, value = info_item.split(':', 1)
314
                except ValueError:
315
                    raise 'Value %r has no colon' % info_item
316
                if name == 'last-changed':
317
                    last_changed = value
318
                elif name == 'executable':
319
                    assert value in ('yes', 'no'), value
320
                    val = (value == 'yes')
321
                    bundle_tree.note_executable(new_path, val)
322
                elif name == 'target':
323
                    bundle_tree.note_target(new_path, value)
324
                elif name == 'encoding':
325
                    encoding = value
326
            return last_changed, encoding
327
328
        def do_patch(path, lines, encoding):
329
            if encoding is not None:
330
                assert encoding == 'base64'
331
                patch = base64.decodestring(''.join(lines))
332
            else:
333
                patch =  ''.join(lines)
334
            bundle_tree.note_patch(path, patch)
335
336
        def renamed(kind, extra, lines):
337
            info = extra.split(' // ')
338
            if len(info) < 2:
339
                raise BzrError('renamed action lines need both a from and to'
340
                        ': %r' % extra)
341
            old_path = info[0]
342
            if info[1].startswith('=> '):
343
                new_path = info[1][3:]
344
            else:
345
                new_path = info[1]
346
347
            bundle_tree.note_rename(old_path, new_path)
348
            last_modified, encoding = extra_info(info[2:], new_path)
349
            revision = get_rev_id(last_modified, new_path, kind)
350
            if lines:
351
                do_patch(new_path, lines, encoding)
352
353
        def removed(kind, extra, lines):
354
            info = extra.split(' // ')
355
            if len(info) > 1:
356
                # TODO: in the future we might allow file ids to be
357
                # given for removed entries
358
                raise BzrError('removed action lines should only have the path'
359
                        ': %r' % extra)
360
            path = info[0]
361
            bundle_tree.note_deletion(path)
362
363
        def added(kind, extra, lines):
364
            info = extra.split(' // ')
365
            if len(info) <= 1:
366
                raise BzrError('add action lines require the path and file id'
367
                        ': %r' % extra)
368
            elif len(info) > 5:
369
                raise BzrError('add action lines have fewer than 5 entries.'
370
                        ': %r' % extra)
371
            path = info[0]
372
            if not info[1].startswith('file-id:'):
373
                raise BzrError('The file-id should follow the path for an add'
374
                        ': %r' % extra)
375
            file_id = info[1][8:]
376
377
            bundle_tree.note_id(file_id, path, kind)
378
            # this will be overridden in extra_info if executable is specified.
379
            bundle_tree.note_executable(path, False)
380
            last_changed, encoding = extra_info(info[2:], path)
381
            revision = get_rev_id(last_changed, path, kind)
382
            if kind == 'directory':
383
                return
384
            do_patch(path, lines, encoding)
385
386
        def modified(kind, extra, lines):
387
            info = extra.split(' // ')
388
            if len(info) < 1:
389
                raise BzrError('modified action lines have at least'
390
                        'the path in them: %r' % extra)
391
            path = info[0]
392
393
            last_modified, encoding = extra_info(info[1:], path)
394
            revision = get_rev_id(last_modified, path, kind)
395
            if lines:
396
                do_patch(path, lines, encoding)
397
            
398
        valid_actions = {
399
            'renamed':renamed,
400
            'removed':removed,
401
            'added':added,
402
            'modified':modified
403
        }
404
        for action_line, lines in \
405
            self.get_revision_info(revision_id).tree_actions:
406
            first = action_line.find(' ')
407
            if first == -1:
408
                raise BzrError('Bogus action line'
409
                        ' (no opening space): %r' % action_line)
410
            second = action_line.find(' ', first+1)
411
            if second == -1:
412
                raise BzrError('Bogus action line'
413
                        ' (missing second space): %r' % action_line)
414
            action = action_line[:first]
415
            kind = action_line[first+1:second]
416
            if kind not in ('file', 'directory', 'symlink'):
417
                raise BzrError('Bogus action line'
418
                        ' (invalid object kind %r): %r' % (kind, action_line))
419
            extra = action_line[second+1:]
420
421
            if action not in valid_actions:
422
                raise BzrError('Bogus action line'
423
                        ' (unrecognized action): %r' % action_line)
424
            valid_actions[action](kind, extra, lines)
425
426
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
427
class BundleTree(Tree):
1185.82.16 by Aaron Bentley
Ensured revision ID is stored in ChangesetTree inventories
428
    def __init__(self, base_tree, revision_id):
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
429
        self.base_tree = base_tree
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
430
        self._renamed = {} # Mapping from old_path => new_path
431
        self._renamed_r = {} # new_path => old_path
432
        self._new_id = {} # new_path => new_id
433
        self._new_id_r = {} # new_id => new_path
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
434
        self._kinds = {} # new_id => kind
0.5.118 by John Arbash Meinel
Got most of test_changeset to work. Still needs work for Aaron's test code.
435
        self._last_changed = {} # new_id => revision_id
1185.82.66 by Aaron Bentley
Handle new executable files
436
        self._executable = {} # new_id => executable value
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
437
        self.patches = {}
1185.82.87 by Aaron Bentley
Got symlink adding working
438
        self._targets = {} # new path => new symlink target
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
439
        self.deleted = []
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
440
        self.contents_by_id = True
1185.82.16 by Aaron Bentley
Ensured revision ID is stored in ChangesetTree inventories
441
        self.revision_id = revision_id
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
442
        self._inventory = None
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
443
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
444
    def __str__(self):
445
        return pprint.pformat(self.__dict__)
446
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
447
    def note_rename(self, old_path, new_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
448
        """A file/directory has been renamed from old_path => new_path"""
1963.2.1 by Robey Pointer
remove usage of has_key()
449
        assert new_path not in self._renamed
450
        assert old_path not in self._renamed_r
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
451
        self._renamed[new_path] = old_path
452
        self._renamed_r[old_path] = new_path
453
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
454
    def note_id(self, new_id, new_path, kind='file'):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
455
        """Files that don't exist in base need a new id."""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
456
        self._new_id[new_path] = new_id
457
        self._new_id_r[new_id] = new_path
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
458
        self._kinds[new_id] = kind
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
459
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
460
    def note_last_changed(self, file_id, revision_id):
1963.2.1 by Robey Pointer
remove usage of has_key()
461
        if (file_id in self._last_changed
0.5.118 by John Arbash Meinel
Got most of test_changeset to work. Still needs work for Aaron's test code.
462
                and self._last_changed[file_id] != revision_id):
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
463
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
464
                    ': %s != %s' % (file_id,
0.5.118 by John Arbash Meinel
Got most of test_changeset to work. Still needs work for Aaron's test code.
465
                                    self._last_changed[file_id],
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
466
                                    revision_id))
0.5.118 by John Arbash Meinel
Got most of test_changeset to work. Still needs work for Aaron's test code.
467
        self._last_changed[file_id] = revision_id
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
468
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
469
    def note_patch(self, new_path, patch):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
470
        """There is a patch for a given filename."""
1731.1.55 by Aaron Bentley
Fix bundle handling
471
        self.patches[new_path] = patch
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
472
1185.82.87 by Aaron Bentley
Got symlink adding working
473
    def note_target(self, new_path, target):
474
        """The symlink at the new path has the given target"""
1731.1.55 by Aaron Bentley
Fix bundle handling
475
        self._targets[new_path] = target
1185.82.87 by Aaron Bentley
Got symlink adding working
476
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
477
    def note_deletion(self, old_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
478
        """The file at old_path has been deleted."""
1731.1.55 by Aaron Bentley
Fix bundle handling
479
        self.deleted.append(old_path)
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
480
1185.82.66 by Aaron Bentley
Handle new executable files
481
    def note_executable(self, new_path, executable):
1731.1.55 by Aaron Bentley
Fix bundle handling
482
        self._executable[new_path] = executable
1185.82.66 by Aaron Bentley
Handle new executable files
483
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
484
    def old_path(self, new_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
485
        """Get the old_path (path in the base_tree) for the file at new_path"""
0.6.1 by Aaron Bentley
Fleshed out MockTree, fixed all test failures
486
        assert new_path[:1] not in ('\\', '/')
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
487
        old_path = self._renamed.get(new_path)
488
        if old_path is not None:
489
            return old_path
490
        dirname,basename = os.path.split(new_path)
0.5.56 by John Arbash Meinel
A couple more fixups, it seems actually capable now of writing out a changeset, and reading it back.
491
        # dirname is not '' doesn't work, because
492
        # dirname may be a unicode entry, and is
493
        # requires the objects to be identical
494
        if dirname != '':
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
495
            old_dir = self.old_path(dirname)
496
            if old_dir is None:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
497
                old_path = None
498
            else:
1711.4.19 by John Arbash Meinel
Bundles were still using os.path.join to compute paths rather than osutils.pathjoin
499
                old_path = pathjoin(old_dir, basename)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
500
        else:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
501
            old_path = new_path
502
        #If the new path wasn't in renamed, the old one shouldn't be in
503
        #renamed_r
1963.2.1 by Robey Pointer
remove usage of has_key()
504
        if old_path in self._renamed_r:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
505
            return None
506
        return old_path 
507
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
508
    def new_path(self, old_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
509
        """Get the new_path (path in the target_tree) for the file at old_path
510
        in the base tree.
511
        """
0.6.1 by Aaron Bentley
Fleshed out MockTree, fixed all test failures
512
        assert old_path[:1] not in ('\\', '/')
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
513
        new_path = self._renamed_r.get(old_path)
514
        if new_path is not None:
515
            return new_path
1963.2.1 by Robey Pointer
remove usage of has_key()
516
        if new_path in self._renamed:
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
517
            return None
518
        dirname,basename = os.path.split(old_path)
0.5.81 by John Arbash Meinel
Cleaning up from pychecker.
519
        if dirname != '':
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
520
            new_dir = self.new_path(dirname)
521
            if new_dir is None:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
522
                new_path = None
523
            else:
1711.4.19 by John Arbash Meinel
Bundles were still using os.path.join to compute paths rather than osutils.pathjoin
524
                new_path = pathjoin(new_dir, basename)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
525
        else:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
526
            new_path = old_path
527
        #If the old path wasn't in renamed, the new one shouldn't be in
528
        #renamed_r
1963.2.1 by Robey Pointer
remove usage of has_key()
529
        if new_path in self._renamed:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
530
            return None
531
        return new_path 
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
532
533
    def path2id(self, path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
534
        """Return the id of the file present at path in the target tree."""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
535
        file_id = self._new_id.get(path)
536
        if file_id is not None:
537
            return file_id
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
538
        old_path = self.old_path(path)
539
        if old_path is None:
540
            return None
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
541
        if old_path in self.deleted:
542
            return None
1963.2.6 by Robey Pointer
pychecker is on crack; go back to using 'is None'.
543
        if getattr(self.base_tree, 'path2id', None) is not None:
0.5.66 by John Arbash Meinel
Refactoring, moving test code into test (switching back to assert is None)
544
            return self.base_tree.path2id(old_path)
545
        else:
546
            return self.base_tree.inventory.path2id(old_path)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
547
548
    def id2path(self, file_id):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
549
        """Return the new path in the target tree of the file with id file_id"""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
550
        path = self._new_id_r.get(file_id)
551
        if path is not None:
552
            return path
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
553
        old_path = self.base_tree.id2path(file_id)
554
        if old_path is None:
555
            return None
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
556
        if old_path in self.deleted:
557
            return None
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
558
        return self.new_path(old_path)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
559
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
560
    def old_contents_id(self, file_id):
1185.82.94 by Aaron Bentley
Remove old chatter
561
        """Return the id in the base_tree for the given file_id.
562
        Return None if the file did not exist in base.
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
563
        """
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
564
        if self.contents_by_id:
565
            if self.base_tree.has_id(file_id):
566
                return file_id
567
            else:
568
                return None
569
        new_path = self.id2path(file_id)
570
        return self.base_tree.path2id(new_path)
571
        
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
572
    def get_file(self, file_id):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
573
        """Return a file-like object containing the new contents of the
574
        file given by file_id.
575
576
        TODO:   It might be nice if this actually generated an entry
577
                in the text-store, so that the file contents would
578
                then be cached.
579
        """
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
580
        base_id = self.old_contents_id(file_id)
1910.2.64 by Aaron Bentley
Changes from review
581
        if (base_id is not None and
1910.2.58 by Aaron Bentley
Stop using get_revision_id to make bundles
582
            base_id != self.base_tree.inventory.root.file_id):
0.5.50 by aaron.bentley at utoronto
Evaluate patches against file paths, not file ids
583
            patch_original = self.base_tree.get_file(base_id)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
584
        else:
585
            patch_original = None
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
586
        file_patch = self.patches.get(self.id2path(file_id))
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
587
        if file_patch is None:
1185.82.42 by Aaron Bentley
Handle new directories properly
588
            if (patch_original is None and 
589
                self.get_kind(file_id) == 'directory'):
590
                return StringIO()
591
            assert patch_original is not None, "None: %s" % file_id
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
592
            return patch_original
0.5.94 by Aaron Bentley
Switched to native patch application, added tests for terminating newlines
593
594
        assert not file_patch.startswith('\\'), \
595
            'Malformed patch for %s, %r' % (file_id, file_patch)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
596
        return patched_file(file_patch, patch_original)
597
1185.82.87 by Aaron Bentley
Got symlink adding working
598
    def get_symlink_target(self, file_id):
599
        new_path = self.id2path(file_id)
600
        try:
601
            return self._targets[new_path]
602
        except KeyError:
603
            return self.base_tree.get_symlink_target(file_id)
604
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
605
    def get_kind(self, file_id):
606
        if file_id in self._kinds:
607
            return self._kinds[file_id]
608
        return self.base_tree.inventory[file_id].kind
609
1185.82.66 by Aaron Bentley
Handle new executable files
610
    def is_executable(self, file_id):
1185.82.93 by Aaron Bentley
Code cleanup
611
        path = self.id2path(file_id)
612
        if path in self._executable:
613
            return self._executable[path]
1185.82.66 by Aaron Bentley
Handle new executable files
614
        else:
615
            return self.base_tree.inventory[file_id].executable
616
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
617
    def get_last_changed(self, file_id):
1185.82.95 by Aaron Bentley
Restore path-orientation of ChangesetTree
618
        path = self.id2path(file_id)
619
        if path in self._last_changed:
620
            return self._last_changed[path]
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
621
        return self.base_tree.inventory[file_id].revision
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
622
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
623
    def get_size_and_sha1(self, file_id):
624
        """Return the size and sha1 hash of the given file id.
625
        If the file was not locally modified, this is extracted
626
        from the base_tree. Rather than re-reading the file.
627
        """
628
        new_path = self.id2path(file_id)
629
        if new_path is None:
630
            return None, None
631
        if new_path not in self.patches:
632
            # If the entry does not have a patch, then the
633
            # contents must be the same as in the base_tree
634
            ie = self.base_tree.inventory[file_id]
0.5.69 by John Arbash Meinel
Applying patch from Robey Pointer to clean up apply_changeset.
635
            if ie.text_size is None:
636
                return ie.text_size, ie.text_sha1
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
637
            return int(ie.text_size), ie.text_sha1
0.5.94 by Aaron Bentley
Switched to native patch application, added tests for terminating newlines
638
        fileobj = self.get_file(file_id)
639
        content = fileobj.read()
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
640
        return len(content), sha_string(content)
641
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
642
    def _get_inventory(self):
1185.82.130 by Aaron Bentley
Rename changesets to revision bundles
643
        """Build up the inventory entry for the BundleTree.
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
644
645
        This need to be called before ever accessing self.inventory
646
        """
647
        from os.path import dirname, basename
648
649
        assert self.base_tree is not None
650
        base_inv = self.base_tree.inventory
1731.1.62 by Aaron Bentley
Changes from review comments
651
        inv = Inventory(None, self.revision_id)
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
652
653
        def add_entry(file_id):
654
            path = self.id2path(file_id)
655
            if path is None:
656
                return
1731.1.55 by Aaron Bentley
Fix bundle handling
657
            if path == '':
658
                parent_id = None
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
659
            else:
1731.1.55 by Aaron Bentley
Fix bundle handling
660
                parent_path = dirname(path)
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
661
                parent_id = self.path2id(parent_path)
662
663
            kind = self.get_kind(file_id)
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
664
            revision_id = self.get_last_changed(file_id)
0.5.83 by John Arbash Meinel
Tests pass. Now ChangesetTree has it's own inventory.
665
666
            name = basename(path)
0.5.116 by John Arbash Meinel
Fixed a bug based on the new InventoryEntry separation.
667
            if kind == 'directory':
668
                ie = InventoryDirectory(file_id, name, parent_id)
669
            elif kind == 'file':
670
                ie = InventoryFile(file_id, name, parent_id)
1185.82.66 by Aaron Bentley
Handle new executable files
671
                ie.executable = self.is_executable(file_id)
0.5.116 by John Arbash Meinel
Fixed a bug based on the new InventoryEntry separation.
672
            elif kind == 'symlink':
673
                ie = InventoryLink(file_id, name, parent_id)
1185.82.87 by Aaron Bentley
Got symlink adding working
674
                ie.symlink_target = self.get_symlink_target(file_id)
0.5.115 by John Arbash Meinel
Getting closer to being able to read back the changesets, still broken, though.
675
            ie.revision = revision_id
676
1185.82.88 by Aaron Bentley
Get symlink modification, renames and deletion under test
677
            if kind in ('directory', 'symlink'):
0.5.83 by John Arbash Meinel
Tests pass. Now ChangesetTree has it's own inventory.
678
                ie.text_size, ie.text_sha1 = None, None
679
            else:
680
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
1185.82.88 by Aaron Bentley
Get symlink modification, renames and deletion under test
681
            if (ie.text_size is None) and (kind == 'file'):
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
682
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
683
            inv.add(ie)
684
0.6.1 by Aaron Bentley
Fleshed out MockTree, fixed all test failures
685
        sorted_entries = self.sorted_path_id()
686
        for path, file_id in sorted_entries:
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
687
            add_entry(file_id)
688
689
        return inv
690
691
    # Have to overload the inherited inventory property
692
    # because _get_inventory is only called in the parent.
693
    # Reading the docs, property() objects do not use
694
    # overloading, they use the function as it was defined
695
    # at that instant
696
    inventory = property(_get_inventory)
0.5.64 by John Arbash Meinel
SUCCESS, we now are able to validate the inventory XML.
697
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
698
    def __iter__(self):
0.5.82 by John Arbash Meinel
Lots of changes, changing separators, updating tests, updated ChangesetTree to include text_ids
699
        for path, entry in self.inventory.iter_entries():
0.5.69 by John Arbash Meinel
Applying patch from Robey Pointer to clean up apply_changeset.
700
            yield entry.file_id
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
701
0.6.1 by Aaron Bentley
Fleshed out MockTree, fixed all test failures
702
    def sorted_path_id(self):
703
        paths = []
704
        for result in self._new_id.iteritems():
705
            paths.append(result)
706
        for id in self.base_tree:
707
            path = self.id2path(id)
708
            if path is None:
709
                continue
710
            paths.append((path, id))
711
        paths.sort()
712
        return paths
713
1185.82.123 by Aaron Bentley
Cleanups to prepare for review
714
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
715
def patched_file(file_patch, original):
0.5.94 by Aaron Bentley
Switched to native patch application, added tests for terminating newlines
716
    """Produce a file-like object with the patched version of a text"""
1185.82.13 by Aaron Bentley
Got old changeset tests running
717
    from bzrlib.patches import iter_patched
718
    from bzrlib.iterablefile import IterableFile
0.5.94 by Aaron Bentley
Switched to native patch application, added tests for terminating newlines
719
    if file_patch == "":
720
        return IterableFile(())
1848.1.1 by John Arbash Meinel
fix bug in bundle handling of binary files with just '\r' in them.
721
    # string.splitlines(True) also splits on '\r', but the iter_patched code
722
    # only expects to iterate over '\n' style lines
723
    return IterableFile(iter_patched(original,
724
                StringIO(file_patch).readlines()))