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