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