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