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