~bzr-pqm/bzr/bzr.dev

757 by Martin Pool
- add john's changeset plugin
1
#!/usr/bin/env python
2
"""\
3
Just some work for generating a changeset.
4
"""
5
6
import bzrlib, bzrlib.errors
7
8
import common
9
10
from bzrlib.inventory import ROOT_ID
11
12
try:
13
    set
14
except NameError:
15
    from sets import Set as set
16
17
def _canonicalize_revision(branch, revno):
18
    """Turn some sort of revision information into a single
19
    set of from-to revision ids.
20
21
    A revision id can be None if there is no associated revison.
22
23
    :return: (old, new)
24
    """
25
    # This is a little clumsy because revision parsing may return
26
    # a single entry, or a list
27
    if revno is None:
28
        new = branch.last_patch()
29
    else:
30
        new = branch.lookup_revision(revno)
31
32
    if new is None:
33
        raise BzrCommandError('Cannot generate a changset with no commits in tree.')
34
35
    old = branch.get_revision(new).precursor
36
37
    return old, new
38
39
def _get_trees(branch, revisions):
40
    """Get the old and new trees based on revision.
41
    """
42
    from bzrlib.tree import EmptyTree
43
    if revisions[0] is None:
44
        if hasattr(branch, 'get_root_id'): # Watch out for trees with labeled ROOT ids
45
            old_tree = EmptyTree(branch.get_root_id) 
46
        else:
47
            old_tree = EmptyTree()
48
    else:
49
        old_tree = branch.revision_tree(revisions[0])
50
51
    if revisions[1] is None:
52
        # This is for the future, once we support rollup revisions
53
        # Or working tree revisions
54
        new_tree = branch.working_tree()
55
    else:
56
        new_tree = branch.revision_tree(revisions[1])
57
    return old_tree, new_tree
58
59
def _fake_working_revision(branch):
60
    """Fake a Revision object for the working tree.
61
    
62
    This is for the future, to support changesets against the working tree.
63
    """
64
    from bzrlib.revision import Revision
65
    import time
66
    from bzrlib.osutils import local_time_offset, \
67
            username
68
69
    precursor = branch.last_patch()
70
    precursor_sha1 = branch.get_revision_sha1(precursor)
71
72
    return Revision(timestamp=time.time(),
73
            timezone=local_time_offset(),
74
            committer=username(),
75
            precursor=precursor,
76
            precursor_sha1=precursor_sha1)
77
78
79
class MetaInfoHeader(object):
80
    """Maintain all of the header information about this
81
    changeset.
82
    """
83
84
    def __init__(self, branch, revisions, delta,
85
            full_remove=True, full_rename=False,
86
            external_diff_options = None,
87
            new_tree=None, old_tree=None,
88
            old_label = '', new_label = ''):
89
        """
90
        :param full_remove: Include the full-text for a delete
91
        :param full_rename: Include an add+delete patch for a rename
92
        """
93
        self.branch = branch
94
        self.delta = delta
95
        self.full_remove=full_remove
96
        self.full_rename=full_rename
97
        self.external_diff_options = external_diff_options
98
        self.old_label = old_label
99
        self.new_label = new_label
100
        self.old_tree = old_tree
101
        self.new_tree = new_tree
102
        self.to_file = None
103
        self.revno = None
104
        self.precursor_revno = None
105
106
        self._get_revision_list(revisions)
107
108
    def _get_revision_list(self, revisions):
109
        """This generates the list of all revisions from->to.
110
111
        This is for the future, when we support having a rollup changeset.
112
        For now, the list should only be one long.
113
        """
114
        old_revno = None
115
        new_revno = None
116
        rh = self.branch.revision_history()
117
        for revno, rev in enumerate(rh):
118
            if rev == revisions[0]:
119
                old_revno = revno
120
            if rev == revisions[1]:
121
                new_revno = revno
122
123
        self.revision_list = []
124
        if old_revno is None:
125
            self.base_revision = None # Effectively the EmptyTree()
126
            old_revno = -1
127
        else:
128
            self.base_revision = self.branch.get_revision(rh[old_revno])
129
        if new_revno is None:
130
            # For the future, when we support working tree changesets.
131
            for rev_id in rh[old_revno+1:]:
132
                self.revision_list.append(self.branch.get_revision(rev_id))
133
            self.revision_list.append(_fake_working_revision(self.branch))
134
        else:
135
            for rev_id in rh[old_revno+1:new_revno+1]:
136
                self.revision_list.append(self.branch.get_revision(rev_id))
137
        self.precursor_revno = old_revno
138
        self.revno = new_revno
139
140
    def _write(self, txt, key=None):
141
        if key:
142
            self.to_file.write('# %s: %s\n' % (key, txt))
143
        else:
144
            self.to_file.write('# %s\n' % (txt,))
145
146
    def write_meta_info(self, to_file):
147
        """Write out the meta-info portion to the supplied file.
148
149
        :param to_file: Write out the meta information to the supplied
150
                        file
151
        """
152
        self.to_file = to_file
153
154
        self._write_header()
155
        self._write_diffs()
156
        self._write_footer()
157
158
    def _write_header(self):
159
        """Write the stuff that comes before the patches."""
160
        from bzrlib.osutils import username, format_date
161
        write = self._write
162
163
        for line in common.get_header():
164
            write(line)
165
166
        # This grabs the current username, what we really want is the
167
        # username from the actual patches.
168
        #write(username(), key='committer')
169
        assert len(self.revision_list) == 1
170
        rev = self.revision_list[0]
171
        write(rev.committer, key='committer')
172
        write(format_date(rev.timestamp, offset=rev.timezone), key='date')
173
        write(str(self.revno), key='revno')
174
        if rev.message:
175
            self.to_file.write('# message:\n')
176
            for line in rev.message.split('\n'):
177
                self.to_file.write('#    %s\n' % line)
178
        write(rev.revision_id, key='revision')
179
180
        if self.base_revision:
181
            write(self.base_revision.revision_id, key='precursor')
182
            write(str(self.precursor_revno), key='precursor revno')
183
184
185
        write('')
186
        self.to_file.write('\n')
187
188
    def _write_footer(self):
189
        """Write the stuff that comes after the patches.
190
191
        This is meant to be more meta-information, which people probably don't want
192
        to read, but which is required for proper bzr operation.
193
        """
194
        write = self._write
195
196
        write('BEGIN BZR FOOTER')
197
198
        assert len(self.revision_list) == 1 # We only handle single revision entries
199
        rev = self.revision_list[0]
200
        write(self.branch.get_revision_sha1(rev.revision_id),
201
                key='revision sha1')
202
        if self.base_revision:
203
            rev_id = self.base_revision.revision_id
204
            write(self.branch.get_revision_sha1(rev_id),
205
                    key='precursor sha1')
206
207
        write('%.9f' % rev.timestamp, key='timestamp')
208
        write(str(rev.timezone), key='timezone')
209
210
        self._write_ids()
211
212
        write('END BZR FOOTER')
213
214
    def _write_revisions(self):
215
        """Not used. Used for writing multiple revisions."""
216
        first = True
217
        for rev in self.revision_list:
218
            if rev.revision_id is not None:
219
                if first:
220
                    self._write('revisions:')
221
                    first = False
222
                self._write(' '*4 + rev.revision_id + '\t' + self.branch.get_revision_sha1(rev.revision_id))
223
224
225
    def _write_ids(self):
226
        if hasattr(self.branch, 'get_root_id'):
227
            root_id = self.branch.get_root_id()
228
        else:
229
            root_id = ROOT_ID
230
231
        old_ids = set()
232
        new_ids = set()
233
234
        for path, file_id, kind in self.delta.removed:
235
            old_ids.add(file_id)
236
        for path, file_id, kind in self.delta.added:
237
            new_ids.add(file_id)
238
        for old_path, new_path, file_id, kind, text_modified in self.delta.renamed:
239
            old_ids.add(file_id)
240
            new_ids.add(file_id)
241
        for path, file_id, kind in self.delta.modified:
242
            new_ids.add(file_id)
243
244
        self._write(root_id, key='tree root id')
245
246
        def write_ids(tree, id_set, name):
247
            if len(id_set) > 0:
248
                self.to_file.write('# %s ids:\n' % name)
249
            seen_ids = set([root_id])
250
            while len(id_set) > 0:
251
                file_id = id_set.pop()
252
                if file_id in seen_ids:
253
                    continue
254
                seen_ids.add(file_id)
255
                ie = tree.inventory[file_id]
256
                if ie.parent_id not in seen_ids:
257
                    id_set.add(ie.parent_id)
258
                path = tree.inventory.id2path(file_id)
259
                self.to_file.write('#    %s\t%s\t%s\n'
260
                        % (path.encode('utf8'), file_id.encode('utf8'),
261
                            ie.parent_id.encode('utf8')))
262
        write_ids(self.new_tree, new_ids, 'file')
263
        write_ids(self.old_tree, old_ids, 'old file')
264
265
    def _write_diffs(self):
266
        """Write out the specific diffs"""
267
        from bzrlib.diff import internal_diff, external_diff
268
        DEVNULL = '/dev/null'
269
270
        if self.external_diff_options:
271
            assert isinstance(self.external_diff_options, basestring)
272
            opts = self.external_diff_options.split()
273
            def diff_file(olab, olines, nlab, nlines, to_file):
274
                external_diff(olab, olines, nlab, nlines, to_file, opts)
275
        else:
276
            diff_file = internal_diff
277
278
        for path, file_id, kind in self.delta.removed:
279
            print >>self.to_file, '*** removed %s %r' % (kind, path)
280
            if kind == 'file' and self.full_remove:
281
                diff_file(self.old_label + path,
282
                          self.old_tree.get_file(file_id).readlines(),
283
                          DEVNULL, 
284
                          [],
285
                          self.to_file)
286
    
287
        for path, file_id, kind in self.delta.added:
288
            print >>self.to_file, '*** added %s %r' % (kind, path)
289
            if kind == 'file':
290
                diff_file(DEVNULL,
291
                          [],
292
                          self.new_label + path,
293
                          self.new_tree.get_file(file_id).readlines(),
294
                          self.to_file)
295
    
296
        for old_path, new_path, file_id, kind, text_modified in self.delta.renamed:
297
            print >>self.to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
298
            if self.full_rename and kind == 'file':
299
                diff_file(self.old_label + old_path,
300
                          self.old_tree.get_file(file_id).readlines(),
301
                          DEVNULL, 
302
                          [],
303
                          self.to_file)
304
                diff_file(DEVNULL,
305
                          [],
306
                          self.new_label + new_path,
307
                          self.new_tree.get_file(file_id).readlines(),
308
                          self.to_file)
309
            elif text_modified:
310
                    diff_file(self.old_label + old_path,
311
                              self.old_tree.get_file(file_id).readlines(),
312
                              self.new_label + new_path,
313
                              self.new_tree.get_file(file_id).readlines(),
314
                              self.to_file)
315
    
316
        for path, file_id, kind in self.delta.modified:
317
            print >>self.to_file, '*** modified %s %r' % (kind, path)
318
            if kind == 'file':
319
                diff_file(self.old_label + path,
320
                          self.old_tree.get_file(file_id).readlines(),
321
                          self.new_label + path,
322
                          self.new_tree.get_file(file_id).readlines(),
323
                          self.to_file)
324
325
def show_changeset(branch, revision=None, specific_files=None,
326
        external_diff_options=None, to_file=None,
327
        include_full_diff=False):
328
    from bzrlib.diff import compare_trees
329
330
    if to_file is None:
331
        import sys
332
        to_file = sys.stdout
333
    revisions = _canonicalize_revision(branch, revision)
334
335
    old_tree, new_tree = _get_trees(branch, revisions)
336
337
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
338
                          specific_files=specific_files)
339
340
    meta = MetaInfoHeader(branch, revisions, delta,
341
            external_diff_options=external_diff_options,
342
            old_tree=old_tree, new_tree=new_tree)
343
    meta.write_meta_info(to_file)
344
345