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