~ubuntu-branches/ubuntu/oneiric/bzr-rebase/oneiric

« back to all changes in this revision

Viewing changes to rebase.py

  • Committer: Bazaar Package Importer
  • Author(s): Jelmer Vernooij
  • Date: 2009-03-10 14:24:42 UTC
  • mfrom: (1.2.1 upstream) (5.1.1 jaunty)
  • Revision ID: james.westby@ubuntu.com-20090310142442-8i2q0hbwo29uqcmg
Tags: 0.4.4-1
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
"""Rebase."""
17
17
 
18
18
from bzrlib.config import Config
19
 
from bzrlib.errors import (BzrError, NoSuchFile, UnknownFormatError, 
20
 
                           NoCommonAncestor, UnrelatedBranches)
 
19
from bzrlib.errors import (BzrError, NoSuchFile, UnknownFormatError,
 
20
                           NoCommonAncestor, NoSuchId, UnrelatedBranches)
21
21
from bzrlib.generate_ids import gen_revision_id
 
22
from bzrlib.graph import FrozenHeadsCache
22
23
from bzrlib.merge import Merger
23
24
from bzrlib import osutils
24
25
from bzrlib.revision import NULL_REVISION
25
26
from bzrlib.trace import mutter
 
27
from bzrlib.tsort import topo_sort
26
28
import bzrlib.ui as ui
27
29
 
28
30
from maptree import MapTree, map_file_ids
40
42
    :return: boolean
41
43
    """
42
44
    try:
43
 
        return wt._control_files.get(REBASE_PLAN_FILENAME).read() != ''
 
45
        return wt._transport.get_bytes(REBASE_PLAN_FILENAME) != ''
44
46
    except NoSuchFile:
45
47
        return False
46
48
 
51
53
    :param wt: Working Tree for which to write the plan.
52
54
    :return: Tuple with last revision info and replace map.
53
55
    """
54
 
    text = wt._control_files.get(REBASE_PLAN_FILENAME).read()
 
56
    text = wt._transport.get_bytes(REBASE_PLAN_FILENAME)
55
57
    if text == '':
56
58
        raise NoSuchFile(REBASE_PLAN_FILENAME)
57
59
    return unmarshall_rebase_plan(text)
63
65
    :param wt: Working Tree for which to write the plan.
64
66
    :param replace_map: Replace map (old revid -> (new revid, new parents))
65
67
    """
66
 
    wt._control_files.put_utf8(REBASE_PLAN_FILENAME, 
67
 
            marshall_rebase_plan(wt.branch.last_revision_info(), replace_map))
 
68
    content = marshall_rebase_plan(wt.branch.last_revision_info(), replace_map)
 
69
    assert type(content) == str
 
70
    wt._transport.put_bytes(REBASE_PLAN_FILENAME, content)
68
71
 
69
72
 
70
73
def remove_rebase_plan(wt):
72
75
 
73
76
    :param wt: Working Tree for which to remove the plan.
74
77
    """
75
 
    wt._control_files.put_utf8(REBASE_PLAN_FILENAME, '')
 
78
    wt._transport.put_bytes(REBASE_PLAN_FILENAME, '')
76
79
 
77
80
 
78
81
def marshall_rebase_plan(last_rev_info, replace_map):
110
113
            # Skip empty lines
111
114
            continue
112
115
        pts = l.split(" ")
113
 
        replace_map[pts[0]] = (pts[1], pts[2:])
 
116
        replace_map[pts[0]] = (pts[1], tuple(pts[2:]))
114
117
    return (last_revision_info, replace_map)
115
118
 
116
119
 
124
127
    return gen_revision_id(rev.committer, rev.timestamp)
125
128
 
126
129
 
127
 
def generate_simple_plan(history, start_revid, stop_revid, onto_revid, 
128
 
                         onto_ancestry, get_parents, generate_revid,
129
 
                         skip_full_merged=False):
 
130
def generate_simple_plan(todo_set, start_revid, stop_revid, onto_revid, graph,
 
131
    generate_revid, skip_full_merged=False):
130
132
    """Create a simple rebase plan that replays history based 
131
133
    on one revision being replayed on top of another.
132
134
 
133
 
    :param history: Revision history
 
135
    :param todo_set: A set of revisions to rebase. Only the revisions from
 
136
        stop_revid back through the left hand ancestry are rebased; other
 
137
        revisions are ignored (and references to them are preserved).
134
138
    :param start_revid: Id of revision at which to start replaying
135
139
    :param stop_revid: Id of revision until which to stop replaying
136
140
    :param onto_revid: Id of revision on top of which to replay
137
 
    :param onto_ancestry: Ancestry of onto_revid
138
 
    :param get_parents: Function for obtaining the parents of a revision
 
141
    :param graph: Graph object
139
142
    :param generate_revid: Function for generating new revision ids
140
143
    :param skip_full_merged: Skip revisions that merge already merged 
141
144
                             revisions.
142
145
 
143
146
    :return: replace map
144
147
    """
145
 
    assert start_revid is None or start_revid in history
146
 
    assert stop_revid is None or stop_revid in history
 
148
    assert start_revid is None or start_revid in todo_set, \
 
149
        "invalid start revid(%r), todo_set(%r)" % (start_revid, todo_set)
 
150
    assert stop_revid is None or stop_revid in todo_set, "invalid stop_revid"
147
151
    replace_map = {}
148
 
    if start_revid is not None:
149
 
        start_revno = history.index(start_revid)
150
 
    else:
151
 
        start_revno = None
152
 
    if stop_revid is not None:
153
 
        stop_revno = history.index(stop_revid)+1
154
 
    else:
155
 
        stop_revno = None
 
152
    parent_map = graph.get_parent_map(todo_set)
 
153
    order = topo_sort(parent_map)
 
154
    left_most_path = []
 
155
    if stop_revid is None:
 
156
        stop_revid = order[-1]
 
157
    rev = stop_revid
 
158
    while rev in parent_map:
 
159
        left_most_path.append(rev)
 
160
        if rev == start_revid:
 
161
            # manual specified early-stop
 
162
            break
 
163
        rev = parent_map[rev][0]
 
164
    left_most_path.reverse()
 
165
    if start_revid is None:
 
166
        # We need a common base.
 
167
        lca = graph.find_lca(stop_revid, onto_revid)
 
168
        if lca == set([NULL_REVISION]):
 
169
            raise UnrelatedBranches()
156
170
    new_parent = onto_revid
157
 
    for oldrevid in history[start_revno:stop_revno]: 
158
 
        oldparents = list(get_parents(oldrevid))
159
 
        assert isinstance(oldparents, list)
160
 
        assert oldparents == [] or \
161
 
                oldparents[0] == history[history.index(oldrevid)-1]
 
171
    todo = left_most_path
 
172
    heads_cache = FrozenHeadsCache(graph)
 
173
    # XXX: The output replacemap'd parents should get looked up in some manner
 
174
    # by the heads cache? RBC 20080719
 
175
    for oldrevid in todo:
 
176
        oldparents = parent_map[oldrevid]
 
177
        assert isinstance(oldparents, tuple), "not tuple: %r" % oldparents
162
178
        if len(oldparents) > 1:
163
 
            parents = [new_parent] + filter(lambda p: p not in onto_ancestry or p == onto_revid, oldparents[1:]) 
 
179
            additional_parents = heads_cache.heads(oldparents[1:])
 
180
            parents = [new_parent]
 
181
            for parent in parents:
 
182
                if parent in additional_parents and parent not in parents:
 
183
                    # Use as a parent
 
184
                    parent = replace_map.get(parent, (parent,))[0]
 
185
                    parents.append(parent)
 
186
            parents = tuple(parents)
164
187
            if len(parents) == 1 and skip_full_merged:
165
188
                continue
166
189
        else:
167
 
            parents = [new_parent]
 
190
            parents = (new_parent,)
168
191
        newrevid = generate_revid(oldrevid)
169
 
        assert newrevid != oldrevid
 
192
        assert newrevid != oldrevid, "old and newrevid equal (%r)" % newrevid
 
193
        assert isinstance(parents, tuple), "parents not tuple: %r" % parents
170
194
        replace_map[oldrevid] = (newrevid, parents)
171
195
        new_parent = newrevid
172
196
    return replace_map
173
197
 
174
198
 
175
 
def generate_transpose_plan(graph, renames, get_parents, generate_revid):
 
199
def generate_transpose_plan(ancestry, renames, graph, generate_revid):
176
200
    """Create a rebase plan that replaces a bunch of revisions
177
201
    in a revision graph.
178
202
 
179
 
    :param graph: Revision graph in which to operate
 
203
    :param ancestry: Ancestry to consider
180
204
    :param renames: Renames of revision
181
 
    :param get_parents: Function for determining parents
 
205
    :param graph: Graph object
182
206
    :param generate_revid: Function for creating new revision ids
183
207
    """
184
208
    replace_map = {}
185
209
    todo = []
186
210
    children = {}
187
 
    for r in graph:
188
 
        if not children.has_key(r):
189
 
            children[r] = []
190
 
        for p in graph[r]:
 
211
    parent_map = {}
 
212
    for r, ps in ancestry:
 
213
        if not children.has_key(r):
 
214
            children[r] = []
 
215
        if ps is None: # Ghost
 
216
            continue
 
217
        parent_map[r] = ps
 
218
        if not children.has_key(r):
 
219
            children[r] = []
 
220
        for p in ps:
191
221
            if not children.has_key(p):
192
222
                children[p] = []
193
223
            children[p].append(r)
194
224
 
 
225
    parent_map.update(graph.get_parent_map(filter(lambda x: not x in parent_map, renames.values())))
 
226
 
195
227
    # todo contains a list of revisions that need to 
196
228
    # be rewritten
197
 
    for r in renames:
198
 
        replace_map[r] = (renames[r], get_parents(renames[r]))
 
229
    for r, v in renames.items():
 
230
        replace_map[r] = (v, parent_map[v])
199
231
        todo.append(r)
200
232
 
201
233
    total = len(todo)
205
237
    try:
206
238
        while len(todo) > 0:
207
239
            r = todo.pop()
 
240
            processed.add(r)
208
241
            i += 1
209
242
            pb.update('determining dependencies', i, total)
210
243
            # Add entry for them in replace_map
212
245
                if c in renames:
213
246
                    continue
214
247
                if replace_map.has_key(c):
215
 
                    parents = list(replace_map[c][1])
 
248
                    parents = replace_map[c][1]
216
249
                else:
217
 
                    parents = list(graph[c])
218
 
                assert isinstance(parents, list), \
219
 
                        "Expected list of parents, got: %r" % parents
 
250
                    parents = parent_map[c]
 
251
                assert isinstance(parents, tuple), \
 
252
                        "Expected tuple of parents, got: %r" % parents
220
253
                # replace r in parents with replace_map[r][0]
221
254
                if not replace_map[r][0] in parents:
 
255
                    parents = list(parents)
222
256
                    parents[parents.index(r)] = replace_map[r][0]
223
 
                replace_map[c] = (generate_revid(c), parents)
224
 
                assert replace_map[c][0] != c
225
 
            processed.add(r)
226
 
            # Add them to todo[]
227
 
            todo.extend(filter(lambda x: not x in processed, children[r]))
 
257
                    parents = tuple(parents)
 
258
                replace_map[c] = (generate_revid(c), tuple(parents))
 
259
                if replace_map[c][0] == c:
 
260
                    del replace_map[c]
 
261
                elif c not in processed:
 
262
                    todo.append(c)
228
263
    finally:
229
264
        pb.finished()
230
265
 
242
277
    :param repository: Repository that contains the revisions
243
278
    :param replace_map: Replace map
244
279
    """
245
 
    for revid in replace_map:
246
 
        if not repository.has_revision(replace_map[revid][0]):
 
280
    for revid, parent_ids in replace_map.items():
 
281
        assert isinstance(parent_ids, tuple), "replace map parents not tuple"
 
282
        if not repository.has_revision(parent_ids[0]):
247
283
            yield revid
248
284
 
249
285
 
254
290
    :param replace_map: Dictionary with revisions to (optionally) rewrite
255
291
    :param merge_fn: Function for replaying a revision
256
292
    """
257
 
    todo = list(rebase_todo(repository, replace_map))
258
 
    dependencies = {}
259
 
 
260
293
    # Figure out the dependencies
261
 
    for revid in todo:
262
 
        for p in replace_map[revid][1]:
263
 
            if repository.has_revision(p):
264
 
                continue
265
 
            if not dependencies.has_key(p):
266
 
                dependencies[p] = []
267
 
            dependencies[p].append(revid)
268
 
 
 
294
    graph = repository.get_graph()
 
295
    todo = list(graph.iter_topo_order(replace_map.keys()))
269
296
    pb = ui.ui_factory.nested_progress_bar()
270
 
    total = len(todo)
271
 
    i = 0
272
297
    try:
273
 
        while len(todo) > 0:
274
 
            pb.update('rebase revisions', i, total)
275
 
            i += 1
276
 
            revid = todo.pop()
 
298
        for i, revid in enumerate(todo):
 
299
            pb.update('rebase revisions', i, len(todo))
277
300
            (newrevid, newparents) = replace_map[revid]
278
 
            if filter(repository.has_revision, newparents) != newparents:
279
 
                # Not all parents present yet, avoid for now
280
 
                continue
 
301
            assert isinstance(newparents, tuple), "Expected tuple for %r" % newparents
281
302
            if repository.has_revision(newrevid):
282
303
                # Was already converted, no need to worry about it again
283
304
                continue
284
305
            replay_fn(repository, revid, newrevid, newparents)
285
 
            assert repository.has_revision(newrevid)
286
 
            assert list(repository.revision_parents(newrevid)) == newparents, \
287
 
                   "expected parents %r, got %r" % (newparents, 
288
 
                           repository.revision_parents(newrevid))
289
 
            if dependencies.has_key(newrevid):
290
 
                todo.extend(dependencies[newrevid])
291
 
                del dependencies[newrevid]
292
306
    finally:
293
307
        pb.finished()
294
308
        
295
 
    #assert all(map(repository.has_revision, 
296
 
    #           [replace_map[r][0] for r in replace_map]))
297
 
 
298
 
 
299
 
 
300
 
def replay_snapshot(repository, oldrevid, newrevid, new_parents, 
301
 
                    revid_renames, fix_revid=None):
 
309
 
 
310
def replay_snapshot(repository, oldrevid, newrevid, new_parents):
302
311
    """Replay a commit by simply commiting the same snapshot with different 
303
312
    parents.
304
313
 
306
315
    :param oldrevid: Revision id of the revision to copy.
307
316
    :param newrevid: Revision id of the revision to create.
308
317
    :param new_parents: Revision ids of the new parent revisions.
309
 
    :param revid_renames: Revision id renames for texts.
310
318
    """
311
 
    assert isinstance(new_parents, list)
 
319
    assert isinstance(new_parents, tuple), "replay_snapshot: Expected tuple for %r" % new_parents
312
320
    mutter('creating copy %r of %r with new parents %r' % 
313
321
                               (newrevid, oldrevid, new_parents))
314
322
    oldrev = repository.get_revision(oldrevid)
327
335
    try:
328
336
        # Check what new_ie.file_id should be
329
337
        # use old and new parent inventories to generate new_id map
330
 
        fileid_map = map_file_ids(repository, oldrev.parent_ids, new_parents)
331
 
        oldtree = MapTree(repository.revision_tree(oldrevid), fileid_map)
332
 
        total = len(oldtree.inventory)
 
338
        nonghost_oldparents = tuple([p for p in oldrev.parent_ids if repository.has_revision(p)])
 
339
        nonghost_newparents = tuple([p for p in new_parents if repository.has_revision(p)])
 
340
        fileid_map = map_file_ids(repository, nonghost_oldparents, nonghost_newparents)
 
341
        oldtree = repository.revision_tree(oldrevid)
 
342
        mappedtree = MapTree(oldtree, fileid_map)
333
343
        pb = ui.ui_factory.nested_progress_bar()
334
 
        i = 0
335
344
        try:
336
 
            parent_invs = map(repository.get_revision_inventory, new_parents)
337
 
            transact = repository.get_transaction()
338
 
            for path, ie in oldtree.inventory.iter_entries():
339
 
                pb.update('upgrading file', i, total)
340
 
                ie = ie.copy()
 
345
            old_parent_invs = list(repository.iter_inventories(nonghost_oldparents))
 
346
            new_parent_invs = list(repository.iter_inventories(nonghost_newparents))
 
347
            for i, (path, old_ie) in enumerate(mappedtree.inventory.iter_entries()):
 
348
                pb.update('upgrading file', i, len(mappedtree.inventory))
 
349
                ie = old_ie.copy()
341
350
                # Either this file was modified last in this revision, 
342
351
                # in which case it has to be rewritten
343
 
                if fix_revid is not None:
344
 
                    ie.revision = fix_revid(ie.revision)
345
 
                if ie.revision == oldrevid:
346
 
                    if repository.weave_store.get_weave_or_empty(ie.file_id, 
347
 
                            repository.get_transaction()).has_version(newrevid):
 
352
                if old_ie.revision == oldrevid:
 
353
                    if repository.texts.has_key((ie.file_id, newrevid)):
 
354
                        # Use the existing text
348
355
                        ie.revision = newrevid
349
356
                    else:
 
357
                        # Create a new text
350
358
                        ie.revision = None
351
359
                else:
352
360
                    # or it was already there before the commit, in 
353
361
                    # which case the right revision should be used
354
 
                    if revid_renames.has_key(ie.revision):
355
 
                        ie.revision = revid_renames[ie.revision]
356
 
                    # make sure at least one of the new parents contains 
357
 
                    # the ie.file_id, ie.revision combination
358
 
                    if len(filter(lambda inv: ie.file_id in inv and inv[ie.file_id].revision == ie.revision, parent_invs)) == 0:
359
 
                        raise ReplayParentsInconsistent(ie.file_id, ie.revision)
360
 
                i += 1
361
 
                builder.record_entry_contents(ie, parent_invs, path, oldtree,
362
 
                        oldtree.path_content_summary(path))
 
362
                    # one of the old parents had this revision, so find that
 
363
                    # and then use the matching new parent
 
364
                    old_file_id = oldtree.inventory.path2id(path)
 
365
                    assert old_file_id is not None
 
366
                    ie = None
 
367
                    for (old_pinv, new_pinv) in zip(old_parent_invs, new_parent_invs):
 
368
                        if (old_pinv.has_id(old_file_id) and
 
369
                            old_pinv[old_file_id].revision == old_ie.revision):
 
370
                            try:
 
371
                                ie = new_pinv[old_ie.file_id].copy()
 
372
                            except NoSuchId:
 
373
                                raise ReplayParentsInconsistent(old_ie.file_id, old_ie.revision)
 
374
                            break
 
375
                    assert ie is not None
 
376
                builder.record_entry_contents(ie, new_parent_invs, path, mappedtree,
 
377
                        mappedtree.path_content_summary(path))
363
378
        finally:
364
379
            pb.finished()
365
380
        builder.finish_inventory()
375
390
    :param wt: Mutable tree with the changes.
376
391
    :param oldrev: Revision info of new revision to commit.
377
392
    :param newrevid: New revision id."""
378
 
    assert oldrev.revision_id != newrevid
 
393
    assert oldrev.revision_id != newrevid, "Invalid revid %r" % newrevid
379
394
    revprops = dict(oldrev.properties)
380
395
    revprops[REVPROP_REBASE_OF] = oldrev.revision_id
 
396
    committer = wt.branch.get_config().username()
 
397
    author = oldrev.get_apparent_author()
 
398
    if author == committer or 'author' in revprops:
 
399
        author = None
381
400
    wt.commit(message=oldrev.message, timestamp=oldrev.timestamp, 
382
 
              timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid)
 
401
              timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid,
 
402
              committer=committer, author=author)
383
403
    write_active_rebase_revid(wt, None)
384
404
 
385
405
 
386
406
def replay_determine_base(graph, oldrevid, oldparents, newrevid, newparents):
 
407
    """Determine the base for replaying a revision using merge.
 
408
 
 
409
    :param graph: Revision graph.
 
410
    :param oldrevid: Revid of old revision.
 
411
    :param oldparents: List of old parents revids.
 
412
    :param newrevid: Revid of new revision.
 
413
    :param newparents: List of new parents revids.
 
414
    :return: Revision id of the new new revision.
 
415
    """
387
416
    # If this was the first commit, no base is needed
388
417
    if len(oldparents) == 0:
389
418
        return NULL_REVISION
423
452
    oldrev = wt.branch.repository.get_revision(oldrevid)
424
453
    # Make sure there are no conflicts or pending merges/changes 
425
454
    # in the working tree
426
 
    if wt.changes_from(wt.basis_tree()).has_changed():
427
 
        raise BzrError("Working tree has uncommitted changes.")
428
455
    complete_revert(wt, [newparents[0]])
429
 
    assert not wt.changes_from(wt.basis_tree()).has_changed()
 
456
    assert not wt.changes_from(wt.basis_tree()).has_changed(), "Changes in rev"
430
457
 
431
458
    oldtree = repository.revision_tree(oldrevid)
432
459
    write_active_rebase_revid(wt, oldrevid)
433
460
    merger = Merger(wt.branch, this_tree=wt)
434
461
    merger.set_other_revision(oldrevid, wt.branch)
435
 
    base_revid = replay_determine_base(repository.get_graph(), 
 
462
    base_revid = replay_determine_base(repository.get_graph(),
436
463
                                       oldrevid, oldrev.parent_ids,
437
464
                                       newrevid, newparents)
438
 
    mutter('replaying %r as %r with base %r and new parents %r' % 
 
465
    mutter('replaying %r as %r with base %r and new parents %r' %
439
466
           (oldrevid, newrevid, base_revid, newparents))
440
467
    merger.set_base_revision(base_revid, wt.branch)
441
468
    merger.merge_type = merge_type
452
479
    :param map_ids: Whether to try to map between file ids (False for path-based merge)
453
480
    """
454
481
    def replay(repository, oldrevid, newrevid, newparents):
455
 
        assert wt.branch.repository == repository
 
482
        assert wt.branch.repository == repository, "Different repository"
456
483
        return replay_delta_workingtree(wt, oldrevid, newrevid, newparents, 
457
484
                                        merge_type=merge_type)
458
485
    return replay
466
493
    """
467
494
    if revid is None:
468
495
        revid = NULL_REVISION
469
 
    wt._control_files.put_utf8(REBASE_CURRENT_REVID_FILENAME, revid)
 
496
    assert type(revid) == str
 
497
    wt._transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, revid)
470
498
 
471
499
 
472
500
def read_active_rebase_revid(wt):
476
504
    :return: Id of the revision that is being rebased.
477
505
    """
478
506
    try:
479
 
        text = wt._control_files.get(REBASE_CURRENT_REVID_FILENAME).read().rstrip("\n")
 
507
        text = wt._transport.get_bytes(REBASE_CURRENT_REVID_FILENAME).rstrip("\n")
480
508
        if text == NULL_REVISION:
481
509
            return None
482
510
        return text
503
531
            else:
504
532
                os.unlink(abs_path)
505
533
    wt.revert(None, old_tree=newtree, backups=False)
506
 
    assert not wt.changes_from(wt.basis_tree()).has_changed()
 
534
    assert not wt.changes_from(wt.basis_tree()).has_changed(), "Rev changed"
507
535
    wt.set_parent_ids(newparents)
508
536
 
509
537
 
510
538
class ReplaySnapshotError(BzrError):
 
539
    """Raised when replaying a snapshot failed."""
511
540
    _fmt = """Replaying the snapshot failed: %(message)s."""
512
541
 
513
542
    def __init__(self, message):
516
545
 
517
546
 
518
547
class ReplayParentsInconsistent(BzrError):
 
548
    """Raised when parents were inconsistent."""
519
549
    _fmt = """Parents were inconsistent while replaying commit for file id %(fileid)s, revision %(revid)s."""
520
550
 
521
551
    def __init__(self, fileid, revid):