/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
1
#!/usr/bin/env python
2
"""\
3
Read in a changeset output, and process it into a Changeset object.
4
"""
5
6
import bzrlib, bzrlib.changeset
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
7
import pprint
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
8
import common
9
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
10
from bzrlib.trace import mutter
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
11
from bzrlib.errors import BzrError
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
12
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
13
class BadChangeset(Exception): pass
14
class MalformedHeader(BadChangeset): pass
15
class MalformedPatches(BadChangeset): pass
16
class MalformedFooter(BadChangeset): pass
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
17
0.5.11 by John Arbash Meinel
Working on properly representing renames.
18
def _unescape(name):
19
    """Now we want to find the filename effected.
20
    Unfortunately the filename is written out as
21
    repr(filename), which means that it surrounds
22
    the name with quotes which may be single or double
23
    (single is preferred unless there is a single quote in
24
    the filename). And some characters will be escaped.
25
26
    TODO:   There has to be some pythonic way of undo-ing the
27
            representation of a string rather than using eval.
28
    """
29
    delimiter = name[0]
30
    if name[-1] != delimiter:
31
        raise BadChangeset('Could not properly parse the'
32
                ' filename: %r' % name)
33
    # We need to handle escaped hexadecimals too.
34
    return name[1:-1].replace('\"', '"').replace("\'", "'")
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
35
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
36
class RevisionInfo(object):
37
    """Gets filled out for each revision object that is read.
38
    """
39
    def __init__(self, rev_id):
40
        self.rev_id = rev_id
41
        self.sha1 = None
42
        self.committer = None
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
43
        self.date = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
44
        self.timestamp = None
45
        self.timezone = None
46
        self.inventory_id = None
47
        self.inventory_sha1 = None
48
49
        self.parents = None
50
        self.message = None
51
52
    def __str__(self):
53
        return pprint.pformat(self.__dict__)
54
0.5.37 by John Arbash Meinel
Made read_changeset able to spit out 'Revision' entities.
55
    def as_revision(self):
56
        from bzrlib.revision import Revision, RevisionReference
57
        rev = Revision(revision_id=self.rev_id,
58
            committer=self.committer,
59
            timestamp=float(self.timestamp),
60
            timezone=int(self.timezone),
61
            inventory_id=self.inventory_id,
62
            inventory_sha1=self.inventory_sha1,
63
            message='\n'.join(self.message))
64
0.5.59 by John Arbash Meinel
Several fixes for handling the case where you are doing a changeset against revno=0 (Null base)
65
        if self.parents:
66
            for parent in self.parents:
67
                rev_id, sha1 = parent.split('\t')
68
                rev.parents.append(RevisionReference(rev_id, sha1))
0.5.37 by John Arbash Meinel
Made read_changeset able to spit out 'Revision' entities.
69
70
        return rev
71
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
72
class ChangesetInfo(object):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
73
    """This contains the meta information. Stuff that allows you to
74
    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.
75
    """
76
    def __init__(self):
77
        self.committer = None
78
        self.date = None
0.5.17 by John Arbash Meinel
adding apply-changset, plus more meta information.
79
        self.message = None
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
80
        self.base = None
81
        self.base_sha1 = None
82
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
83
        # A list of RevisionInfo objects
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
84
        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.
85
86
        self.actions = []
87
88
        # The next entries are created during complete_info() and
89
        # other post-read functions.
90
91
        # A list of real Revision objects
92
        self.real_revisions = []
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
93
        self.text_ids = {} # file_id => text_id
94
95
        self.timestamp = None
96
        self.timezone = None
0.5.15 by John Arbash Meinel
Created an apply-changeset function, and modified output for better parsing.
97
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
98
    def __str__(self):
99
        return pprint.pformat(self.__dict__)
100
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
101
    def complete_info(self):
102
        """This makes sure that all information is properly
103
        split up, based on the assumptions that can be made
104
        when information is missing.
105
        """
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.
106
        # Put in all of the guessable information.
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
107
        if not self.timestamp and self.date:
108
            self.timestamp, self.timezone = common.unpack_highres_date(self.date)
109
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.
110
        self.real_revisions = []
0.5.39 by John Arbash Meinel
(broken) Working on changing the processing to use a ChangesetTree.
111
        for rev in self.revisions:
0.5.60 by John Arbash Meinel
read_changeset now parses the date: subheader of revisions correctly.
112
            if rev.timestamp is None:
113
                if rev.date is not None:
114
                    rev.timestamp, rev.timezone = \
115
                            common.unpack_highres_date(rev.date)
116
                else:
117
                    rev.timestamp = self.timestamp
118
                    rev.timezone = self.timezone
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
119
            if rev.message is None and self.message:
120
                rev.message = self.message
121
            if rev.committer is None and self.committer:
122
                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.
123
            if rev.inventory_id is None:
124
                rev.inventory_id = rev.rev_id
125
            self.real_revisions.append(rev.as_revision())
126
127
        if self.base is None:
128
            # When we don't have a base, then the real base
129
            # is the first parent of the first revision listed
130
            rev = self.real_revisions[0]
0.5.59 by John Arbash Meinel
Several fixes for handling the case where you are doing a changeset against revno=0 (Null base)
131
            if len(rev.parents) == 0:
132
                # There is no base listed, and
133
                # the lowest revision doesn't have a parent
134
                # so this is probably against the empty tree
135
                # and thus base truly is None
136
                self.base = None
137
                self.base_sha1 = None
138
            else:
139
                self.base = rev.parents[0].revision_id
140
                # In general, if self.base is None, self.base_sha1 should
141
                # also be None
142
                if self.base_sha1 is not None:
143
                    assert self.base_sha1 == rev.parents[0].revision_sha1
144
                self.base_sha1 = rev.parents[0].revision_sha1
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.
145
146
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
147
148
class ChangesetReader(object):
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
149
    """This class reads in a changeset from a file, and returns
150
    a Changeset object, which can then be applied against a tree.
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
151
    """
152
    def __init__(self, from_file):
153
        """Read in the changeset from the file.
154
155
        :param from_file: A file-like object (must have iterator support).
156
        """
157
        object.__init__(self)
158
        self.from_file = from_file
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
159
        self._next_line = None
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
160
        
161
        self.info = ChangesetInfo()
162
        # We put the actual inventory ids in the footer, so that the patch
163
        # is easier to read for humans.
164
        # Unfortunately, that means we need to read everything before we
165
        # can create a proper changeset.
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
166
        self._read()
167
        self._validate()
168
169
    def _read(self):
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
170
        self._read_header()
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
171
        self._read_patches()
172
        self._read_footer()
173
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
174
    def _validate(self):
175
        """Make sure that the information read in makes sense
176
        and passes appropriate checksums.
177
        """
178
        # Fill in all the missing blanks for the revisions
179
        # and generate the real_revisions list.
180
        self.info.complete_info()
181
        self._validate_revisions()
182
183
    def _validate_revisions(self):
184
        """Make sure all revision entries match their checksum."""
185
        from bzrlib.xml import pack_xml
186
        from cStringIO import StringIO
187
        from bzrlib.osutils import sha_file
188
189
        # This is a mapping from each revision id to it's sha hash
190
        rev_to_sha1 = {}
191
192
        for rev, rev_info in zip(self.info.real_revisions, self.info.revisions):
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
193
            assert rev.revision_id == rev_info.rev_id
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
194
            sio = StringIO()
195
            pack_xml(rev, sio)
196
            sio.seek(0)
197
            sha1 = sha_file(sio)
198
            if sha1 != rev_info.sha1:
199
                raise BzrError('Revision checksum mismatch.'
200
                    ' For rev_id {%s} supplied sha1 (%s) != measured (%s)'
201
                    % (rev.revision_id, rev_info.sha1, sha1))
202
            if rev_to_sha1.has_key(rev.revision_id):
203
                raise BzrError('Revision {%s} given twice in the list'
204
                        % (rev.revision_id))
205
            rev_to_sha1[rev.revision_id] = sha1
206
207
        # Now that we've checked all the sha1 sums, we can make sure that
208
        # at least for the small list we have, all of the references are
209
        # valid.
210
        for rev in self.info.real_revisions:
211
            for parent in rev.parents:
212
                if parent.revision_id in rev_to_sha1:
213
                    if parent.revision_sha1 != rev_to_sha1[parent.revision_id]:
214
                        raise BzrError('Parent revision checksum mismatch.'
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
215
                                ' A parent was referenced with an'
216
                                ' incorrect checksum'
0.5.62 by John Arbash Meinel
Doing some internal validation before allowing processing to continue, additional checks at the command level.
217
                                ': {%r} %s != %s' % (parent.revision_id,
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
218
                                            parent.revision_sha1,
219
                                            rev_to_sha1[parent.revision_id]))
220
221
    def _validate_references_from_branch(self, branch):
222
        """Now that we have a branch which should have some of the
223
        revisions we care about, go through and validate all of them
224
        that we can.
225
        """
226
        rev_to_sha = {}
227
        def add_sha(rev_id, sha1):
228
            if rev_id is None:
229
                if sha1 is not None:
230
                    raise BzrError('A Null revision should always'
231
                        'have a null sha1 hash')
232
                return
233
            if rev_id in rev_to_sha:
234
                # This really should have been validated as part
235
                # of _validate_revisions but lets do it again
236
                if sha1 != rev_to_sha[rev_id]:
237
                    raise BzrError('** Revision %r referenced with 2 different'
238
                            ' sha hashes %s != %s' % (rev_id,
239
                                sha1, rev_to_sha[rev_id]))
240
            else:
241
                rev_to_sha[rev_id] = sha1
242
243
        add_sha(self.info.base, self.info.base_sha1)
244
        # All of the contained revisions were checked
245
        # in _validate_revisions
246
        checked = {}
247
        for rev_info in self.info.revisions:
248
            checked[rev_info.rev_id] = True
249
            add_sha(rev_info.rev_id, rev_info.sha1)
250
                
251
        for rev in self.info.real_revisions:
252
            for parent in rev.parents:
253
                add_sha(parent.revision_id, parent.revision_sha1)
254
255
        missing = {}
256
        for rev_id, sha1 in rev_to_sha.iteritems():
257
            if rev_id in branch.revision_store:
258
                local_sha1 = branch.get_revision_sha1(rev_id)
259
                if sha1 != local_sha1:
260
                    raise BzrError('sha1 mismatch. For revision_id {%s}' 
261
                            'local: %s, cset: %s' % (rev_id, local_sha1, sha1))
262
            elif rev_id not in checked:
263
                missing[rev_id] = sha1
264
265
        if len(missing) > 0:
266
            # I don't know if this is an error yet
267
            from bzrlib.trace import warning
268
            warning('Not all revision hashes could be validated.'
269
                    ' Unable validate %d hashes' % len(missing))
270
271
    def _validate_inventory(self, branch, tree):
272
        """At this point we should have generated the ChangesetTree,
273
        so build up an inventory, and make sure the hashes match.
274
        """
275
        
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
276
    def get_info_and_tree(self, branch):
277
        """Return the meta information, and a Changeset tree which can
278
        be used to populate the local stores and working tree, respectively.
279
        """
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
280
        self._validate_references_from_branch(branch)
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
281
        tree = ChangesetTree(branch.revision_tree(self.info.base))
282
        self._update_tree(tree)
283
0.5.63 by John Arbash Meinel
Moving the validation into part of the reading.
284
        self._validate_inventory(branch, tree)
285
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.
286
        return self.info, tree
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
287
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
288
    def _next(self):
289
        """yield the next line, but secretly
290
        keep 1 extra line for peeking.
291
        """
292
        for line in self.from_file:
293
            last = self._next_line
294
            self._next_line = line
295
            if last is not None:
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
296
                #mutter('yielding line: %r' % last)
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
297
                yield last
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.
298
        last = self._next_line
299
        self._next_line = None
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
300
        #mutter('yielding line: %r' % last)
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.
301
        yield last
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
302
303
    def _read_header(self):
304
        """Read the bzr header"""
305
        header = common.get_header()
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
306
        found = False
307
        for line in self._next():
308
            if found:
309
                if (line[:2] != '# ' or line[-1:] != '\n'
310
                        or line[2:-1] != header[0]):
311
                    raise MalformedHeader('Found a header, but it'
312
                        ' was improperly formatted')
313
                header.pop(0) # We read this line.
314
                if not header:
315
                    break # We found everything.
316
            elif (line[:1] == '#' and line[-1:] == '\n'):
317
                line = line[1:-1].strip()
318
                if line[:len(common.header_str)] == common.header_str:
319
                    if line == header[0]:
320
                        found = True
321
                    else:
322
                        raise MalformedHeader('Found what looks like'
323
                                ' a header, but did not match')
324
                    header.pop(0)
325
        else:
326
            raise MalformedHeader('Did not find an opening header')
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
327
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
328
        for line in self._next():
329
            # The bzr header is terminated with a blank line
330
            # which does not start with '#'
331
            if line == '\n':
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
332
                break
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
333
            self._handle_next(line)
334
335
    def _read_next_entry(self, line, indent=1):
336
        """Read in a key-value pair
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
337
        """
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
338
        if line[:1] != '#':
339
            raise MalformedHeader('Bzr header did not start with #')
340
        line = line[1:-1] # Remove the '#' and '\n'
341
        if line[:indent] == ' '*indent:
342
            line = line[indent:]
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
343
        if not line:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
344
            return None, None# Ignore blank lines
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
345
346
        loc = line.find(': ')
347
        if loc != -1:
348
            key = line[:loc]
349
            value = line[loc+2:]
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
350
            if not value:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
351
                value = self._read_many(indent=indent+3)
352
        elif line[-1:] == ':':
353
            key = line[:-1]
354
            value = self._read_many(indent=indent+3)
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
355
        else:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
356
            raise MalformedHeader('While looking for key: value pairs,'
357
                    ' did not find the colon %r' % (line))
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
358
359
        key = key.replace(' ', '_')
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
360
        #mutter('found %s: %s' % (key, value))
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
361
        return key, value
362
363
    def _handle_next(self, line):
364
        key, value = self._read_next_entry(line, indent=1)
365
        if key is None:
366
            return
367
368
        if key == 'revision':
369
            self._read_revision(value)
370
        elif hasattr(self.info, key):
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
371
            if getattr(self.info, key) is None:
372
                setattr(self.info, key, value)
373
            else:
374
                raise MalformedHeader('Duplicated Key: %s' % key)
375
        else:
376
            # What do we do with a key we don't recognize
377
            raise MalformedHeader('Unknown Key: %s' % key)
378
        
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
379
    def _read_many(self, indent):
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
380
        """If a line ends with no entry, that means that it should be
381
        followed with multiple lines of values.
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
382
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
383
        This detects the end of the list, because it will be a line that
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
384
        does not start properly indented.
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
385
        """
386
        values = []
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
387
        start = '#' + (' '*indent)
388
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.
389
        if self._next_line is None or self._next_line[:len(start)] != start:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
390
            return values
391
392
        for line in self._next():
393
            values.append(line[len(start):-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.
394
            if self._next_line is None or self._next_line[:len(start)] != start:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
395
                break
396
        return values
397
398
    def _read_one_patch(self):
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
399
        """Read in one patch, return the complete patch, along with
400
        the next line.
401
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
402
        :return: action, lines, do_continue
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
403
        """
0.5.57 by John Arbash Meinel
Simplified the header, only output base if it is not the expected one.
404
        #mutter('_read_one_patch: %r' % self._next_line)
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
405
        # Peek and see if there are no patches
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.
406
        if self._next_line is None or self._next_line[:1] == '#':
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
407
            return None, [], False
408
409
        line = self._next().next()
410
        if line[:3] != '***':
411
            raise MalformedPatches('The first line of all patches'
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.
412
                ' should be a bzr meta line "***"'
413
                ': %r' % line)
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
414
        action = line[4:-1]
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
415
0.5.59 by John Arbash Meinel
Several fixes for handling the case where you are doing a changeset against revno=0 (Null base)
416
        if self._next_line is None or self._next_line[:1] == '#':
417
            return action, [], False
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
418
        lines = []
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
419
        for line in self._next():
420
            lines.append(line)
421
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.
422
            if self._next_line is not None and self._next_line[:3] == '***':
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
423
                return action, lines, True
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.
424
            elif self._next_line is None or self._next_line[:1] == '#':
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
425
                return action, lines, False
426
        return action, lines, False
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
427
            
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
428
    def _read_patches(self):
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
429
        do_continue = True
430
        while do_continue:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
431
            action, lines, do_continue = self._read_one_patch()
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
432
            if action is not None:
433
                self.info.actions.append((action, lines))
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
434
435
    def _read_revision(self, rev_id):
436
        """Revision entries have extra information associated.
437
        """
438
        rev_info = RevisionInfo(rev_id)
439
        start = '#    '
440
        for line in self._next():
441
            key,value = self._read_next_entry(line, indent=4)
442
            #if key is None:
443
            #    continue
444
            if hasattr(rev_info, key):
445
                if getattr(rev_info, key) is None:
446
                    setattr(rev_info, key, value)
447
                else:
448
                    raise MalformedHeader('Duplicated Key: %s' % key)
449
            else:
450
                # What do we do with a key we don't recognize
451
                raise MalformedHeader('Unknown Key: %s' % key)
452
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.
453
            if self._next_line is None or self._next_line[:len(start)] != start:
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
454
                break
455
456
        self.info.revisions.append(rev_info)
457
458
    def _read_footer(self):
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
459
        """Read the rest of the meta information.
460
0.5.9 by John Arbash Meinel
Now adding the patch information to the ChangesetInfo
461
        :param first_line:  The previous step iterates past what it
462
                            can handle. That extra line is given here.
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
463
        """
0.5.36 by John Arbash Meinel
Updated so that read_changeset is able to parse the output
464
        for line in self._next():
465
            self._handle_next(line)
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.
466
            if self._next_line is None or self._next_line[:1] != '#':
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
467
                break
468
469
    def _update_tree(self, tree):
470
        """This fills out a ChangesetTree based on the information
471
        that was read in.
472
473
        :param tree: A ChangesetTree to update with the new information.
474
        """
475
        from common import decode
476
477
        def get_text_id(info, file_id):
478
            if info is not None:
479
                if info[:8] != 'text-id:':
480
                    raise BzrError("Text ids should be prefixed with 'text-id:'"
481
                        ': %r' % info)
482
                text_id = decode(info[8:])
483
            elif self.info.text_ids.has_key(file_id):
484
                return self.info.text_ids[file_id]
485
            else:
486
                # If text_id was not explicitly supplied
487
                # then it should be whatever we would guess it to be
488
                # based on the base revision, and what we know about
489
                # the target revision
490
                text_id = common.guess_text_id(tree.base_tree, 
491
                        file_id, self.info.base, True)
492
            if (self.info.text_ids.has_key(file_id)
493
                    and self.info.text_ids[file_id] != text_id):
494
                raise BzrError('Mismatched text_ids for file_id {%s}'
495
                        ': %s != %s' % (file_id,
496
                                        self.info.text_ids[file_id],
497
                                        text_id))
498
            # The Info object makes more sense for where
499
            # to store something like text_id, since it is
500
            # what will be used to generate stored inventory
501
            # entries.
502
            # The problem is that we are parsing the
503
            # ChangesetTree right now, we really modifying
504
            # the ChangesetInfo object
505
            self.info.text_ids[file_id] = text_id
506
            return text_id
507
508
        def renamed(kind, extra, lines):
509
            info = extra.split('\t')
510
            if len(info) < 2:
511
                raise BzrError('renamed action lines need both a from and to'
512
                        ': %r' % extra)
513
            old_path = decode(info[0])
514
            if info[1][:3] == '=> ':
515
                new_path = decode(info[1][3:])
516
            else:
517
                new_path = decode(info[1][3:])
518
519
            file_id = tree.path2id(new_path)
520
            if len(info) > 2:
521
                text_id = get_text_id(info[2], file_id)
522
            else:
523
                text_id = get_text_id(None, file_id)
524
            tree.note_rename(old_path, new_path)
525
            if lines:
526
                tree.note_patch(new_path, lines)
527
528
        def removed(kind, extra, lines):
529
            info = extra.split('\t')
530
            if len(info) > 1:
531
                # TODO: in the future we might allow file ids to be
532
                # given for removed entries
533
                raise BzrError('removed action lines should only have the path'
534
                        ': %r' % extra)
535
            path = decode(info[0])
536
            tree.note_deletion(path)
537
538
        def added(kind, extra, lines):
539
            info = extra.split('\t')
540
            if len(info) <= 1:
541
                raise BzrError('add action lines require the path and file id'
542
                        ': %r' % extra)
543
            elif len(info) > 3:
544
                raise BzrError('add action lines have fewer than 3 entries.'
545
                        ': %r' % extra)
546
            path = decode(info[0])
0.5.59 by John Arbash Meinel
Several fixes for handling the case where you are doing a changeset against revno=0 (Null base)
547
            if info[1][:8] != 'file-id:':
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
548
                raise BzrError('The file-id should follow the path for an add'
549
                        ': %r' % extra)
550
            file_id = decode(info[1][8:])
551
552
            if len(info) > 2:
553
                text_id = get_text_id(info[2], file_id)
554
            else:
555
                text_id = get_text_id(None, file_id)
556
            tree.note_id(file_id, path)
557
            tree.note_patch(path, lines)
558
559
        def modified(kind, extra, lines):
560
            info = extra.split('\t')
561
            if len(info) < 1:
562
                raise BzrError('modified action lines have at least'
563
                        'the path in them: %r' % extra)
564
            path = decode(info[0])
565
566
            file_id = tree.path2id(path)
567
            if len(info) > 1:
568
                text_id = get_text_id(info[1], file_id)
569
            else:
570
                text_id = get_text_id(None, file_id)
571
            tree.note_patch(path, lines)
572
            
573
574
        valid_actions = {
575
            'renamed':renamed,
576
            'removed':removed,
577
            'added':added,
578
            'modified':modified
579
        }
580
        for action_line, lines in self.info.actions:
581
            first = action_line.find(' ')
582
            if first == -1:
583
                raise BzrError('Bogus action line'
584
                        ' (no opening space): %r' % action_line)
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.
585
            second = action_line.find(' ', first+1)
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
586
            if second == -1:
587
                raise BzrError('Bogus action line'
588
                        ' (missing second space): %r' % action_line)
589
            action = action_line[:first]
590
            kind = action_line[first+1:second]
591
            if kind not in ('file', 'directory'):
592
                raise BzrError('Bogus action line'
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.
593
                        ' (invalid object kind %r): %r' % (kind, action_line))
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
594
            extra = action_line[second+1:]
595
596
            if action not in valid_actions:
597
                raise BzrError('Bogus action line'
598
                        ' (unrecognized action): %r' % action_line)
599
            valid_actions[action](kind, extra, lines)
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
600
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.
601
def read_changeset(from_file, branch):
602
    """Read in a changeset from a iterable object (such as a file object)
603
604
    :param from_file: A file-like object to read the changeset information.
605
    :param branch: This will be used to build the changeset tree, it needs
606
                   to contain the base of the changeset. (Which you probably
607
                   won't know about until after the changeset is parsed.)
0.5.7 by John Arbash Meinel
Added a bunch more information about changesets. Can now read back in all of the meta information.
608
    """
609
    cr = ChangesetReader(from_file)
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.
610
    return cr.get_info_and_tree(branch)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
611
612
class ChangesetTree:
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
613
    def __init__(self, base_tree=None):
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
614
        self.base_tree = base_tree
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
615
        self._renamed = {} # Mapping from old_path => new_path
616
        self._renamed_r = {} # new_path => old_path
617
        self._new_id = {} # new_path => new_id
618
        self._new_id_r = {} # new_id => new_path
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
619
        self.patches = {}
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
620
        self.deleted = []
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
621
        self.contents_by_id = True
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
622
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
623
    def __str__(self):
624
        return pprint.pformat(self.__dict__)
625
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
626
    def note_rename(self, old_path, new_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
627
        """A file/directory has been renamed from old_path => new_path"""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
628
        assert not self._renamed.has_key(old_path)
629
        assert not self._renamed_r.has_key(new_path)
630
        self._renamed[new_path] = old_path
631
        self._renamed_r[old_path] = new_path
632
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
633
    def note_id(self, new_id, new_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
634
        """Files that don't exist in base need a new id."""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
635
        self._new_id[new_path] = new_id
636
        self._new_id_r[new_id] = new_path
637
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
638
    def note_patch(self, new_path, patch):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
639
        """There is a patch for a given filename."""
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
640
        self.patches[new_path] = patch
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
641
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
642
    def note_deletion(self, old_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
643
        """The file at old_path has been deleted."""
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
644
        self.deleted.append(old_path)
645
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
646
    def old_path(self, new_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
647
        """Get the old_path (path in the base_tree) for the file at new_path"""
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
648
        import os.path
649
        old_path = self._renamed.get(new_path)
650
        if old_path is not None:
651
            return old_path
652
        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.
653
        # dirname is not '' doesn't work, because
654
        # dirname may be a unicode entry, and is
655
        # requires the objects to be identical
656
        if dirname != '':
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
657
            old_dir = self.old_path(dirname)
658
            if old_dir is None:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
659
                old_path = None
660
            else:
661
                old_path = os.path.join(old_dir, basename)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
662
        else:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
663
            old_path = new_path
664
        #If the new path wasn't in renamed, the old one shouldn't be in
665
        #renamed_r
666
        if self._renamed_r.has_key(old_path):
667
            return None
668
        return old_path 
669
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
670
671
    def new_path(self, old_path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
672
        """Get the new_path (path in the target_tree) for the file at old_path
673
        in the base tree.
674
        """
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
675
        import os.path
676
        new_path = self._renamed_r.get(old_path)
677
        if new_path is not None:
678
            return new_path
679
        if self._renamed.has_key(new_path):
680
            return None
681
        dirname,basename = os.path.split(old_path)
682
        if dirname is not '':
683
            new_dir = self.new_path(dirname)
684
            if new_dir is None:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
685
                new_path = None
686
            else:
687
                new_path = os.path.join(new_dir, basename)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
688
        else:
0.5.42 by aaron.bentley at utoronto
Improved rename handling
689
            new_path = old_path
690
        #If the old path wasn't in renamed, the new one shouldn't be in
691
        #renamed_r
692
        if self._renamed.has_key(new_path):
693
            return None
694
        return new_path 
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
695
696
    def path2id(self, path):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
697
        """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
698
        file_id = self._new_id.get(path)
699
        if file_id is not None:
700
            return file_id
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
701
        old_path = self.old_path(path)
702
        if old_path is None:
703
            return None
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
704
        if old_path in self.deleted:
705
            return None
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.
706
        return self.base_tree.inventory.path2id(old_path)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
707
708
    def id2path(self, file_id):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
709
        """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
710
        path = self._new_id_r.get(file_id)
711
        if path is not None:
712
            return path
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
713
        old_path = self.base_tree.id2path(file_id)
714
        if old_path is None:
715
            return None
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
716
        if old_path in self.deleted:
717
            return None
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
718
        return self.new_path(old_path)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
719
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
720
    def old_contents_id(self, file_id):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
721
        """Return the id in the base_tree for the given file_id,
722
        or None if the file did not exist in base.
723
724
        FIXME:  Something doesn't seem right here. It seems like this function
725
                should always either return None or file_id. Even if
726
                you are doing the by-path lookup, you are doing a
727
                id2path lookup, just to do the reverse path2id lookup.
728
        """
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
729
        if self.contents_by_id:
730
            if self.base_tree.has_id(file_id):
731
                return file_id
732
            else:
733
                return None
734
        new_path = self.id2path(file_id)
735
        return self.base_tree.path2id(new_path)
736
        
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
737
    def get_file(self, file_id):
0.5.55 by John Arbash Meinel
Lots of updates. Using a minimized annotations for changesets.
738
        """Return a file-like object containing the new contents of the
739
        file given by file_id.
740
741
        TODO:   It might be nice if this actually generated an entry
742
                in the text-store, so that the file contents would
743
                then be cached.
744
        """
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
745
        base_id = self.old_contents_id(file_id)
0.5.50 by aaron.bentley at utoronto
Evaluate patches against file paths, not file ids
746
        if base_id is not None:
747
            patch_original = self.base_tree.get_file(base_id)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
748
        else:
749
            patch_original = None
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
750
        file_patch = self.patches.get(self.id2path(file_id))
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
751
        if file_patch is None:
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
752
            return patch_original
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
753
        return patched_file(file_patch, patch_original)
754
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
755
    def __iter__(self):
756
        for file_id in self._new_id_r.iterkeys():
757
            yield file_id
758
        for file_id in self.base_tree:
759
            if self.id2path(file_id) is None:
760
                continue
761
            yield file_id
762
763
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
764
def patched_file(file_patch, original):
765
    from bzrlib.patch import patch
766
    from tempfile import mkdtemp
767
    from shutil import rmtree
768
    from StringIO import StringIO
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
769
    from bzrlib.osutils import pumpfile
770
    import os.path
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
771
    temp_dir = mkdtemp()
772
    try:
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
773
        original_path = os.path.join(temp_dir, "originalfile")
774
        temp_original = file(original_path, "wb")
775
        if original is not None:
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
776
            pumpfile(original, temp_original)
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
777
        temp_original.close()
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
778
        patched_path = os.path.join(temp_dir, "patchfile")
0.5.47 by aaron.bentley at utoronto
Added safety check to patch call
779
        assert patch(file_patch, original_path, patched_path) == 0
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
780
        result = StringIO()
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
781
        temp_patched = file(patched_path, "rb")
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
782
        pumpfile(temp_patched, result)
783
        temp_patched.close()
784
        result.seek(0,0)
785
786
    finally:
787
        rmtree(temp_dir)
788
789
    return result
790
791
def test():
792
    import unittest
793
    from StringIO import StringIO
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
794
    from bzrlib.diff import internal_diff
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
795
    class MockTree(object):
796
        def __init__(self):
797
            object.__init__(self)
798
            self.paths = {}
799
            self.ids = {}
800
            self.contents = {}
801
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
802
        def __iter__(self):
803
            return self.paths.iterkeys()
804
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
805
        def add_dir(self, file_id, path):
806
            self.paths[file_id] = path
807
            self.ids[path] = file_id
808
        
809
        def add_file(self, file_id, path, contents):
810
            self.add_dir(file_id, path)
811
            self.contents[file_id] = contents
812
813
        def path2id(self, path):
814
            return self.ids.get(path)
815
816
        def id2path(self, file_id):
817
            return self.paths.get(file_id)
818
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
819
        def has_id(self, file_id):
820
            return self.id2path(file_id) is not None
821
0.5.46 by aaron.bentley at utoronto
Got file gets working
822
        def get_file(self, file_id):
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
823
            result = StringIO()
824
            result.write(self.contents[file_id])
825
            result.seek(0,0)
826
            return result
827
828
    class CTreeTester(unittest.TestCase):
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
829
830
        def make_tree_1(self):
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
831
            mtree = MockTree()
832
            mtree.add_dir("a", "grandparent")
833
            mtree.add_dir("b", "grandparent/parent")
0.5.50 by aaron.bentley at utoronto
Evaluate patches against file paths, not file ids
834
            mtree.add_file("c", "grandparent/parent/file", "Hello\n")
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
835
            mtree.add_dir("d", "grandparent/alt_parent")
836
            return ChangesetTree(mtree), mtree
837
            
0.5.45 by aaron.bentley at utoronto
fixed method names
838
        def test_renames(self):
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
839
            """Ensure that file renames have the proper effect on children"""
840
            ctree = self.make_tree_1()[0]
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
841
            self.assertEqual(ctree.old_path("grandparent"), "grandparent")
842
            self.assertEqual(ctree.old_path("grandparent/parent"), "grandparent/parent")
843
            self.assertEqual(ctree.old_path("grandparent/parent/file"),
844
                "grandparent/parent/file")
845
846
            self.assertEqual(ctree.id2path("a"), "grandparent")
847
            self.assertEqual(ctree.id2path("b"), "grandparent/parent")
848
            self.assertEqual(ctree.id2path("c"), "grandparent/parent/file")
849
850
            self.assertEqual(ctree.path2id("grandparent"), "a")
851
            self.assertEqual(ctree.path2id("grandparent/parent"), "b")
852
            self.assertEqual(ctree.path2id("grandparent/parent/file"), "c")
853
854
            self.assertEqual(ctree.path2id("grandparent2"), None)
855
            self.assertEqual(ctree.path2id("grandparent2/parent"), None)
856
            self.assertEqual(ctree.path2id("grandparent2/parent/file"), None)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
857
858
            ctree.note_rename("grandparent", "grandparent2")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
859
            self.assertEqual(ctree.old_path("grandparent"), None)
860
            self.assertEqual(ctree.old_path("grandparent/parent"), None)
861
            self.assertEqual(ctree.old_path("grandparent/parent/file"), None)
862
863
            self.assertEqual(ctree.id2path("a"), "grandparent2")
864
            self.assertEqual(ctree.id2path("b"), "grandparent2/parent")
865
            self.assertEqual(ctree.id2path("c"), "grandparent2/parent/file")
866
867
            self.assertEqual(ctree.path2id("grandparent2"), "a")
868
            self.assertEqual(ctree.path2id("grandparent2/parent"), "b")
869
            self.assertEqual(ctree.path2id("grandparent2/parent/file"), "c")
870
871
            self.assertEqual(ctree.path2id("grandparent"), None)
872
            self.assertEqual(ctree.path2id("grandparent/parent"), None)
873
            self.assertEqual(ctree.path2id("grandparent/parent/file"), None)
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
874
875
            ctree.note_rename("grandparent/parent", "grandparent2/parent2")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
876
            self.assertEqual(ctree.id2path("a"), "grandparent2")
877
            self.assertEqual(ctree.id2path("b"), "grandparent2/parent2")
878
            self.assertEqual(ctree.id2path("c"), "grandparent2/parent2/file")
879
880
            self.assertEqual(ctree.path2id("grandparent2"), "a")
881
            self.assertEqual(ctree.path2id("grandparent2/parent2"), "b")
882
            self.assertEqual(ctree.path2id("grandparent2/parent2/file"), "c")
883
884
            self.assertEqual(ctree.path2id("grandparent2/parent"), None)
885
            self.assertEqual(ctree.path2id("grandparent2/parent/file"), None)
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
886
887
            ctree.note_rename("grandparent/parent/file", 
888
                              "grandparent2/parent2/file2")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
889
            self.assertEqual(ctree.id2path("a"), "grandparent2")
890
            self.assertEqual(ctree.id2path("b"), "grandparent2/parent2")
891
            self.assertEqual(ctree.id2path("c"), "grandparent2/parent2/file2")
892
893
            self.assertEqual(ctree.path2id("grandparent2"), "a")
894
            self.assertEqual(ctree.path2id("grandparent2/parent2"), "b")
895
            self.assertEqual(ctree.path2id("grandparent2/parent2/file2"), "c")
896
897
            self.assertEqual(ctree.path2id("grandparent2/parent2/file"), None)
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
898
0.5.45 by aaron.bentley at utoronto
fixed method names
899
        def test_moves(self):
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
900
            """Ensure that file moves have the proper effect on children"""
901
            ctree = self.make_tree_1()[0]
902
            ctree.note_rename("grandparent/parent/file", 
903
                              "grandparent/alt_parent/file")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
904
            self.assertEqual(ctree.id2path("c"), "grandparent/alt_parent/file")
905
            self.assertEqual(ctree.path2id("grandparent/alt_parent/file"), "c")
906
            self.assertEqual(ctree.path2id("grandparent/parent/file"), None)
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
907
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
908
        def unified_diff(self, old, new):
909
            out = StringIO()
910
            internal_diff("old", old, "new", new, out)
911
            out.seek(0,0)
912
            return out.read()
913
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
914
        def make_tree_2(self):
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
915
            ctree = self.make_tree_1()[0]
916
            ctree.note_rename("grandparent/parent/file", 
917
                              "grandparent/alt_parent/file")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
918
            self.assertEqual(ctree.id2path("e"), None)
919
            self.assertEqual(ctree.path2id("grandparent/parent/file"), None)
0.5.43 by aaron.bentley at utoronto
Handled moves and adds properly
920
            ctree.note_id("e", "grandparent/parent/file")
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
921
            return ctree
922
923
        def test_adds(self):
924
            """File/inventory adds"""
925
            ctree = self.make_tree_2()
926
            add_patch = self.unified_diff([], ["Extra cheese\n"])
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
927
            ctree.note_patch("grandparent/parent/file", add_patch)
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
928
            self.adds_test(ctree)
0.5.44 by aaron.bentley at utoronto
Got get_file working for new files
929
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
930
        def adds_test(self, ctree):
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
931
            self.assertEqual(ctree.id2path("e"), "grandparent/parent/file")
932
            self.assertEqual(ctree.path2id("grandparent/parent/file"), "e")
933
            self.assertEqual(ctree.get_file("e").read(), "Extra cheese\n")
0.5.45 by aaron.bentley at utoronto
fixed method names
934
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
935
        def test_adds2(self):
936
            """File/inventory adds, with patch-compatibile renames"""
937
            ctree = self.make_tree_2()
938
            ctree.contents_by_id = False
939
            add_patch = self.unified_diff(["Hello\n"], ["Extra cheese\n"])
940
            ctree.note_patch("grandparent/parent/file", add_patch)
941
            self.adds_test(ctree)
942
943
        def make_tree_3(self):
0.5.46 by aaron.bentley at utoronto
Got file gets working
944
            ctree, mtree = self.make_tree_1()
945
            mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
946
            ctree.note_rename("grandparent/parent/file", 
947
                              "grandparent/alt_parent/file")
948
            ctree.note_rename("grandparent/parent/topping", 
949
                              "grandparent/alt_parent/stopping")
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
950
            return ctree
951
952
        def get_file_test(self, ctree):
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
953
            self.assertEqual(ctree.get_file("e").read(), "Lemon\n")
954
            self.assertEqual(ctree.get_file("c").read(), "Hello\n")
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
955
956
        def test_get_file(self):
957
            """Get file contents"""
958
            ctree = self.make_tree_3()
959
            mod_patch = self.unified_diff(["Anchovies\n"], ["Lemon\n"])
960
            ctree.note_patch("grandparent/alt_parent/stopping", mod_patch)
961
            self.get_file_test(ctree)
962
963
        def test_get_file2(self):
964
            """Get file contents, with patch-compatibile renames"""
965
            ctree = self.make_tree_3()
966
            ctree.contents_by_id = False
0.5.50 by aaron.bentley at utoronto
Evaluate patches against file paths, not file ids
967
            mod_patch = self.unified_diff([], ["Lemon\n"])
0.5.46 by aaron.bentley at utoronto
Got file gets working
968
            ctree.note_patch("grandparent/alt_parent/stopping", mod_patch)
0.5.50 by aaron.bentley at utoronto
Evaluate patches against file paths, not file ids
969
            mod_patch = self.unified_diff([], ["Hello\n"])
970
            ctree.note_patch("grandparent/alt_parent/file", mod_patch)
0.5.52 by aaron.bentley at utoronto
Make contents-addressing configurable
971
            self.get_file_test(ctree)
0.5.46 by aaron.bentley at utoronto
Got file gets working
972
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
973
        def test_delete(self):
974
            "Deletion by changeset"
975
            ctree = self.make_tree_1()[0]
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
976
            self.assertEqual(ctree.get_file("c").read(), "Hello\n")
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
977
            ctree.note_deletion("grandparent/parent/file")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
978
            self.assertEqual(ctree.id2path("c"), None)
979
            self.assertEqual(ctree.path2id("grandparent/parent/file"), None)
0.5.48 by aaron.bentley at utoronto
Implemented deletion for ChangesetTrees
980
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
981
        def sorted_ids(self, tree):
982
            ids = list(tree)
983
            ids.sort()
984
            return ids
985
986
        def test_iteration(self):
987
            """Ensure that iteration through ids works properly"""
988
            ctree = self.make_tree_1()[0]
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
989
            self.assertEqual(self.sorted_ids(ctree), ['a', 'b', 'c', 'd'])
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
990
            ctree.note_deletion("grandparent/parent/file")
991
            ctree.note_id("e", "grandparent/alt_parent/fool")
0.5.54 by John Arbash Meinel
Changed all test code into using self.assertEquals instead of assert
992
            self.assertEqual(self.sorted_ids(ctree), ['a', 'b', 'd', 'e'])
0.5.49 by aaron.bentley at utoronto
Implemented iteration over ids
993
            
994
0.5.45 by aaron.bentley at utoronto
fixed method names
995
    patchesTestSuite = unittest.makeSuite(CTreeTester,'test_')
0.5.41 by aaron.bentley at utoronto
Added non-working ChangesetTree
996
    runner = unittest.TextTestRunner()
997
    runner.run(patchesTestSuite)
998