~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to contrib/plugins/changeset/read_changeset.py

  • Committer: Martin Pool
  • Date: 2005-06-22 09:08:43 UTC
  • Revision ID: mbp@sourcefrog.net-20050622090843-78fe9c62da9ed167
- add john's changeset plugin

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
7
import common
 
8
 
 
9
class BadChangeset(Exception): pass
 
10
class MalformedHeader(BadChangeset): pass
 
11
class MalformedPatches(BadChangeset): pass
 
12
class MalformedFooter(BadChangeset): pass
 
13
 
 
14
def _unescape(name):
 
15
    """Now we want to find the filename effected.
 
16
    Unfortunately the filename is written out as
 
17
    repr(filename), which means that it surrounds
 
18
    the name with quotes which may be single or double
 
19
    (single is preferred unless there is a single quote in
 
20
    the filename). And some characters will be escaped.
 
21
 
 
22
    TODO:   There has to be some pythonic way of undo-ing the
 
23
            representation of a string rather than using eval.
 
24
    """
 
25
    delimiter = name[0]
 
26
    if name[-1] != delimiter:
 
27
        raise BadChangeset('Could not properly parse the'
 
28
                ' filename: %r' % name)
 
29
    # We need to handle escaped hexadecimals too.
 
30
    return name[1:-1].replace('\"', '"').replace("\'", "'")
 
31
 
 
32
class ChangesetInfo(object):
 
33
    """This is the intermediate class that gets filled out as
 
34
    the file is read.
 
35
    """
 
36
    def __init__(self):
 
37
        self.committer = None
 
38
        self.date = None
 
39
        self.message = None
 
40
        self.revno = None
 
41
        self.revision = None
 
42
        self.revision_sha1 = None
 
43
        self.precursor = None
 
44
        self.precursor_sha1 = None
 
45
        self.precursor_revno = None
 
46
 
 
47
        self.timestamp = None
 
48
        self.timezone = None
 
49
 
 
50
        self.tree_root_id = None
 
51
        self.file_ids = None
 
52
        self.old_file_ids = None
 
53
 
 
54
        self.actions = [] #this is the list of things that happened
 
55
        self.id2path = {} # A mapping from file id to path name
 
56
        self.path2id = {} # The reverse mapping
 
57
        self.id2parent = {} # A mapping from a given id to it's parent id
 
58
 
 
59
        self.old_id2path = {}
 
60
        self.old_path2id = {}
 
61
        self.old_id2parent = {}
 
62
 
 
63
    def __str__(self):
 
64
        import pprint
 
65
        return pprint.pformat(self.__dict__)
 
66
 
 
67
    def create_maps(self):
 
68
        """Go through the individual id sections, and generate the 
 
69
        id2path and path2id maps.
 
70
        """
 
71
        # Rather than use an empty path, the changeset code seems 
 
72
        # to like to use "./." for the tree root.
 
73
        self.id2path[self.tree_root_id] = './.'
 
74
        self.path2id['./.'] = self.tree_root_id
 
75
        self.id2parent[self.tree_root_id] = bzrlib.changeset.NULL_ID
 
76
        self.old_id2path = self.id2path.copy()
 
77
        self.old_path2id = self.path2id.copy()
 
78
        self.old_id2parent = self.id2parent.copy()
 
79
 
 
80
        if self.file_ids:
 
81
            for info in self.file_ids:
 
82
                path, f_id, parent_id = info.split('\t')
 
83
                self.id2path[f_id] = path
 
84
                self.path2id[path] = f_id
 
85
                self.id2parent[f_id] = parent_id
 
86
        if self.old_file_ids:
 
87
            for info in self.old_file_ids:
 
88
                path, f_id, parent_id = info.split('\t')
 
89
                self.old_id2path[f_id] = path
 
90
                self.old_path2id[path] = f_id
 
91
                self.old_id2parent[f_id] = parent_id
 
92
 
 
93
    def get_changeset(self):
 
94
        """Create a changeset from the data contained within."""
 
95
        from bzrlib.changeset import Changeset, ChangesetEntry, \
 
96
            PatchApply, ReplaceContents
 
97
        cset = Changeset()
 
98
        
 
99
        entry = ChangesetEntry(self.tree_root_id, 
 
100
                bzrlib.changeset.NULL_ID, './.')
 
101
        cset.add_entry(entry)
 
102
        for info, lines in self.actions:
 
103
            parts = info.split(' ')
 
104
            action = parts[0]
 
105
            kind = parts[1]
 
106
            extra = ' '.join(parts[2:])
 
107
            if action == 'renamed':
 
108
                old_path, new_path = extra.split(' => ')
 
109
                old_path = _unescape(old_path)
 
110
                new_path = _unescape(new_path)
 
111
 
 
112
                new_id = self.path2id[new_path]
 
113
                old_id = self.old_path2id[old_path]
 
114
                assert old_id == new_id
 
115
 
 
116
                new_parent = self.id2parent[new_id]
 
117
                old_parent = self.old_id2parent[old_id]
 
118
 
 
119
                entry = ChangesetEntry(old_id, old_parent, old_path)
 
120
                entry.new_path = new_path
 
121
                entry.new_parent = new_parent
 
122
                if lines:
 
123
                    entry.contents_change = PatchApply(''.join(lines))
 
124
            elif action == 'removed':
 
125
                old_path = _unescape(extra)
 
126
                old_id = self.old_path2id[old_path]
 
127
                old_parent = self.old_id2parent[old_id]
 
128
                entry = ChangesetEntry(old_id, old_parent, old_path)
 
129
                entry.new_path = None
 
130
                entry.new_parent = None
 
131
                if lines:
 
132
                    # Technically a removed should be a ReplaceContents()
 
133
                    # Where you need to have the old contents
 
134
                    # But at most we have a remove style patch.
 
135
                    #entry.contents_change = ReplaceContents()
 
136
                    pass
 
137
            elif action == 'added':
 
138
                new_path = _unescape(extra)
 
139
                new_id = self.path2id[new_path]
 
140
                new_parent = self.id2parent[new_id]
 
141
                entry = ChangesetEntry(new_id, new_parent, new_path)
 
142
                entry.path = None
 
143
                entry.parent = None
 
144
                if lines:
 
145
                    # Technically an added should be a ReplaceContents()
 
146
                    # Where you need to have the old contents
 
147
                    # But at most we have an add style patch.
 
148
                    #entry.contents_change = ReplaceContents()
 
149
                    entry.contents_change = PatchApply(''.join(lines))
 
150
            elif action == 'modified':
 
151
                new_path = _unescape(extra)
 
152
                new_id = self.path2id[new_path]
 
153
                new_parent = self.id2parent[new_id]
 
154
                entry = ChangesetEntry(new_id, new_parent, new_path)
 
155
                entry.path = None
 
156
                entry.parent = None
 
157
                if lines:
 
158
                    # Technically an added should be a ReplaceContents()
 
159
                    # Where you need to have the old contents
 
160
                    # But at most we have an add style patch.
 
161
                    #entry.contents_change = ReplaceContents()
 
162
                    entry.contents_change = PatchApply(''.join(lines))
 
163
            else:
 
164
                raise BadChangeset('Unrecognized action: %r' % action)
 
165
            cset.add_entry(entry)
 
166
        return cset
 
167
 
 
168
class ChangesetReader(object):
 
169
    """This class reads in a changeset from a file, and returns
 
170
    a Changeset object, which can then be applied against a tree.
 
171
    """
 
172
    def __init__(self, from_file):
 
173
        """Read in the changeset from the file.
 
174
 
 
175
        :param from_file: A file-like object (must have iterator support).
 
176
        """
 
177
        object.__init__(self)
 
178
        self.from_file = from_file
 
179
        
 
180
        self.info = ChangesetInfo()
 
181
        # We put the actual inventory ids in the footer, so that the patch
 
182
        # is easier to read for humans.
 
183
        # Unfortunately, that means we need to read everything before we
 
184
        # can create a proper changeset.
 
185
        self._read_header()
 
186
        next_line = self._read_patches()
 
187
        if next_line is not None:
 
188
            self._read_footer(next_line)
 
189
 
 
190
    def get_info(self):
 
191
        """Create the actual changeset object.
 
192
        """
 
193
        self.info.create_maps()
 
194
        return self.info
 
195
 
 
196
    def _read_header(self):
 
197
        """Read the bzr header"""
 
198
        header = common.get_header()
 
199
        for head_line, line in zip(header, self.from_file):
 
200
            if (line[:2] != '# '
 
201
                    or line[-1] != '\n'
 
202
                    or line[2:-1] != head_line):
 
203
                raise MalformedHeader('Did not read the opening'
 
204
                    ' header information.')
 
205
 
 
206
        for line in self.from_file:
 
207
            if self._handle_info_line(line) is not None:
 
208
                break
 
209
 
 
210
    def _handle_info_line(self, line, in_footer=False):
 
211
        """Handle reading a single line.
 
212
 
 
213
        This may call itself, in the case that we read_multi,
 
214
        and then had a dangling line on the end.
 
215
        """
 
216
        # The bzr header is terminated with a blank line
 
217
        # which does not start with #
 
218
        next_line = None
 
219
        if line[:1] == '\n':
 
220
            return 'break'
 
221
        if line[:2] != '# ':
 
222
            raise MalformedHeader('Opening bzr header did not start with #')
 
223
 
 
224
        line = line[2:-1] # Remove the '# '
 
225
        if not line:
 
226
            return # Ignore blank lines
 
227
 
 
228
        if in_footer and line in ('BEGIN BZR FOOTER', 'END BZR FOOTER'):
 
229
            return
 
230
 
 
231
        loc = line.find(': ')
 
232
        if loc != -1:
 
233
            key = line[:loc]
 
234
            value = line[loc+2:]
 
235
            if not value:
 
236
                value, next_line = self._read_many()
 
237
        else:
 
238
            if line[-1:] == ':':
 
239
                key = line[:-1]
 
240
                value, next_line = self._read_many()
 
241
            else:
 
242
                raise MalformedHeader('While looking for key: value pairs,'
 
243
                        ' did not find the colon %r' % (line))
 
244
 
 
245
        key = key.replace(' ', '_')
 
246
        if hasattr(self.info, key):
 
247
            if getattr(self.info, key) is None:
 
248
                setattr(self.info, key, value)
 
249
            else:
 
250
                raise MalformedHeader('Duplicated Key: %s' % key)
 
251
        else:
 
252
            # What do we do with a key we don't recognize
 
253
            raise MalformedHeader('Unknown Key: %s' % key)
 
254
        
 
255
        if next_line:
 
256
            self._handle_info_line(next_line, in_footer=in_footer)
 
257
 
 
258
    def _read_many(self):
 
259
        """If a line ends with no entry, that means that it should be
 
260
        followed with multiple lines of values.
 
261
 
 
262
        This detects the end of the list, because it will be a line that
 
263
        does not start with '#    '. Because it has to read that extra
 
264
        line, it returns the tuple: (values, next_line)
 
265
        """
 
266
        values = []
 
267
        for line in self.from_file:
 
268
            if line[:5] != '#    ':
 
269
                return values, line
 
270
            values.append(line[5:-1])
 
271
        return values, None
 
272
 
 
273
    def _read_one_patch(self, first_line=None):
 
274
        """Read in one patch, return the complete patch, along with
 
275
        the next line.
 
276
 
 
277
        :return: action, lines, next_line, do_continue
 
278
        """
 
279
        first = True
 
280
        action = None
 
281
 
 
282
        def parse_firstline(line):
 
283
            if line[:1] == '#':
 
284
                return None
 
285
            if line[:3] != '***':
 
286
                raise MalformedPatches('The first line of all patches'
 
287
                    ' should be a bzr meta line "***"')
 
288
            return line[4:-1]
 
289
 
 
290
        if first_line is not None:
 
291
            action = parse_firstline(first_line)
 
292
            first = False
 
293
            if action is None:
 
294
                return None, [], first_line, False
 
295
 
 
296
        lines = []
 
297
        for line in self.from_file:
 
298
            if first:
 
299
                action = parse_firstline(line)
 
300
                first = False
 
301
                if action is None:
 
302
                    return None, [], line, False
 
303
            else:
 
304
                if line[:3] == '***':
 
305
                    return action, lines, line, True
 
306
                elif line[:1] == '#':
 
307
                    return action, lines, line, False
 
308
                lines.append(line)
 
309
        return action, lines, None, False
 
310
            
 
311
    def _read_patches(self):
 
312
        next_line = None
 
313
        do_continue = True
 
314
        while do_continue:
 
315
            action, lines, next_line, do_continue = \
 
316
                    self._read_one_patch(next_line)
 
317
            if action is not None:
 
318
                self.info.actions.append((action, lines))
 
319
        return next_line
 
320
 
 
321
    def _read_footer(self, first_line=None):
 
322
        """Read the rest of the meta information.
 
323
 
 
324
        :param first_line:  The previous step iterates past what it
 
325
                            can handle. That extra line is given here.
 
326
        """
 
327
        if first_line is not None:
 
328
            if self._handle_info_line(first_line, in_footer=True) is not None:
 
329
                return
 
330
        for line in self.from_file:
 
331
            if self._handle_info_line(line, in_footer=True) is not None:
 
332
                break
 
333
 
 
334
 
 
335
def read_changeset(from_file):
 
336
    """Read in a changeset from a filelike object (must have "readline" support), and
 
337
    parse it into a Changeset object.
 
338
    """
 
339
    cr = ChangesetReader(from_file)
 
340
    info = cr.get_info()
 
341
    return info
 
342