1
# Copyright (C) 2007 by Jelmer Vernooij <jelmer@samba.org>
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Bazaar command-line subcommands."""
19
from ...commands import (
23
from ...errors import (
30
from ...option import (
33
from ...trace import (
37
from ...i18n import gettext
40
def finish_rebase(state, wt, replace_map, replayer):
45
# Start executing plan from current Branch.last_revision()
46
rebase(wt.branch.repository, replace_map, replayer)
47
except ConflictsInTree:
48
raise CommandError(gettext(
49
"A conflict occurred replaying a commit."
50
" Resolve the conflict and run 'brz rebase-continue' or "
51
"run 'brz rebase-abort'."))
56
class cmd_rebase(Command):
59
Rebasing is the process of taking a branch and modifying the history so
60
that it appears to start from a different point. This can be useful
61
to clean up the history before submitting your changes. The tree at the
62
end of the process will be the same as if you had merged the other branch,
63
but the history will be different.
65
The command takes the location of another branch on to which the branch in
66
the specified directory (by default, the current working directory)
67
will be rebased. If a branch is not specified then the parent branch
68
is used, and this is usually the desired result.
70
The first step identifies the revisions that are in the current branch that
71
are not in the parent branch. The current branch is then set to be at the
72
same revision as the target branch, and each revision is replayed on top
73
of the branch. At the end of the process it will appear as though your
74
current branch was branched off the current last revision of the target.
76
Each revision that is replayed may cause conflicts in the tree. If this
77
happens the command will stop and allow you to fix them up. Resolve the
78
commits as you would for a merge, and then run 'brz resolve' to marked
79
them as resolved. Once you have resolved all the conflicts you should
80
run 'brz rebase-continue' to continue the rebase operation.
82
If conflicts are encountered and you decide that you do not wish to continue
83
you can run 'brz rebase-abort'.
85
The '--onto' option allows you to specify a different revision in the
86
target branch to start at when replaying the revisions. This means that
87
you can change the point at which the current branch will appear to be
88
branched from when the operation completes.
90
takes_args = ['upstream_location?']
92
'revision', 'merge-type', 'verbose',
95
help="Show what would be done, but don't actually do anything."),
97
'always-rebase-merges',
98
help="Don't skip revisions that merge already present revisions."),
101
help="Rebase pending merges onto local branch."),
103
'onto', help='Different revision to replay onto.',
108
help="Branch to replay onto, rather than the one containing the working directory.",
113
def run(self, upstream_location=None, onto=None, revision=None,
114
merge_type=None, verbose=False, dry_run=False,
115
always_rebase_merges=False, pending_merges=False,
117
from ...branch import Branch
118
from ...revisionspec import RevisionSpec
119
from ...workingtree import WorkingTree
120
from .rebase import (
121
generate_simple_plan,
124
WorkingTreeRevisionRewriter,
125
regenerate_default_revid,
128
if revision is not None and pending_merges:
129
raise CommandError(gettext(
130
"--revision and --pending-merges are mutually exclusive"))
132
wt = WorkingTree.open_containing(directory)[0]
135
state = RebaseState1(wt)
136
if upstream_location is None:
138
upstream_location = directory
140
upstream_location = wt.branch.get_parent()
141
if upstream_location is None:
142
raise CommandError(gettext("No upstream branch specified."))
143
note(gettext("Rebasing on %s"), upstream_location)
144
upstream = Branch.open_containing(upstream_location)[0]
145
upstream_repository = upstream.repository
146
upstream_revision = upstream.last_revision()
147
# Abort if there already is a plan file
149
raise CommandError(gettext(
150
"A rebase operation was interrupted. "
151
"Continue using 'brz rebase-continue' or abort using 'brz "
156
if revision is not None:
157
if len(revision) == 1:
158
if revision[0] is not None:
159
stop_revid = revision[0].as_revision_id(wt.branch)
160
elif len(revision) == 2:
161
if revision[0] is not None:
162
start_revid = revision[0].as_revision_id(wt.branch)
163
if revision[1] is not None:
164
stop_revid = revision[1].as_revision_id(wt.branch)
166
raise CommandError(gettext(
167
"--revision takes only one or two arguments"))
170
wt_parents = wt.get_parent_ids()
171
if len(wt_parents) in (0, 1):
172
raise CommandError(gettext("No pending merges present."))
173
elif len(wt_parents) > 2:
174
raise CommandError(gettext(
175
"Rebasing more than one pending merge not supported"))
176
stop_revid = wt_parents[1]
177
assert stop_revid is not None, "stop revid invalid"
179
# Check for changes in the working tree.
180
if (not pending_merges and
181
wt.basis_tree().changes_from(wt).has_changed()):
182
raise UncommittedChanges(wt)
184
# Pull required revisions
185
wt.branch.repository.fetch(upstream_repository, upstream_revision)
187
onto = upstream.last_revision()
189
rev_spec = RevisionSpec.from_string(onto)
190
onto = rev_spec.as_revision_id(upstream)
192
wt.branch.repository.fetch(upstream_repository, revision_id=onto)
194
if stop_revid is None:
195
stop_revid = wt.branch.last_revision()
196
repo_graph = wt.branch.repository.get_graph()
197
our_new, onto_unique = repo_graph.find_difference(stop_revid, onto)
199
if start_revid is None:
201
self.outf.write(gettext("No revisions to rebase.\n"))
204
self.outf.write(gettext(
205
"Base branch is descendant of current "
206
"branch. Pulling instead.\n"))
208
wt.pull(upstream, stop_revision=onto)
210
# else: include extra revisions needed to make start_revid mean
214
replace_map = generate_simple_plan(
215
our_new, start_revid, stop_revid, onto, repo_graph,
216
lambda revid, ps: regenerate_default_revid(
217
wt.branch.repository, revid),
218
not always_rebase_merges)
220
if verbose or dry_run:
221
todo = list(rebase_todo(wt.branch.repository, replace_map))
222
note(gettext('%d revisions will be rebased:') % len(todo))
228
state.write_plan(replace_map)
230
replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
232
finish_rebase(state, wt, replace_map, replayer)
237
class cmd_rebase_abort(Command):
238
"""Abort an interrupted rebase."""
244
help="Branch to replay onto, rather than the one containing the working directory.",
248
def run(self, directory="."):
249
from .rebase import (
253
from ...workingtree import WorkingTree
254
wt = WorkingTree.open_containing(directory)[0]
257
state = RebaseState1(wt)
258
# Read plan file and set last revision
260
last_rev_info = state.read_plan()[0]
262
raise CommandError("No rebase to abort")
263
complete_revert(wt, [last_rev_info[1]])
269
class cmd_rebase_continue(Command):
270
"""Continue an interrupted rebase after resolving conflicts."""
272
'merge-type', Option(
275
help="Branch to replay onto, rather than the one containing the working directory.",
279
def run(self, merge_type=None, directory="."):
280
from .rebase import (
282
WorkingTreeRevisionRewriter,
284
from ...workingtree import WorkingTree
285
wt = WorkingTree.open_containing(directory)[0]
288
state = RebaseState1(wt)
289
replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
290
# Abort if there are any conflicts
291
if len(wt.conflicts()) != 0:
292
raise CommandError(gettext(
293
"There are still conflicts present. "
294
"Resolve the conflicts and then run "
295
"'brz resolve' and try again."))
298
replace_map = state.read_plan()[1]
300
raise CommandError(gettext("No rebase to continue"))
301
oldrevid = state.read_active_revid()
302
if oldrevid is not None:
303
oldrev = wt.branch.repository.get_revision(oldrevid)
304
replayer.commit_rebase(oldrev, replace_map[oldrevid][0])
305
finish_rebase(state, wt, replace_map, replayer)
310
class cmd_rebase_todo(Command):
311
"""Print list of revisions that still need to be replayed as part of the
312
current rebase operation.
320
help="Branch to replay onto, rather than the one containing the working directory.",
323
def run(self, directory="."):
324
from .rebase import (
328
from ...workingtree import WorkingTree
329
wt = WorkingTree.open_containing(directory)[0]
331
state = RebaseState1(wt)
333
replace_map = state.read_plan()[1]
335
raise CommandError(gettext("No rebase in progress"))
336
currentrevid = state.read_active_revid()
337
if currentrevid is not None:
338
note(gettext("Currently replaying: %s") % currentrevid)
339
for revid in rebase_todo(wt.branch.repository, replace_map):
340
note(gettext("{0} -> {1}").format(revid, replace_map[revid][0]))
343
class cmd_replay(Command):
344
"""Replay commits from another branch on top of this one.
349
'revision', 'merge-type',
353
help="Branch to replay onto, rather than the one containing the working directory.",
355
takes_args = ['location']
358
def run(self, location, revision=None, merge_type=None, directory="."):
359
from ...branch import Branch
360
from ...workingtree import WorkingTree
362
from .rebase import (
364
regenerate_default_revid,
365
WorkingTreeRevisionRewriter,
368
from_branch = Branch.open_containing(location)[0]
370
if revision is not None:
371
if len(revision) == 1:
372
if revision[0] is not None:
373
todo = [revision[0].as_revision_id(from_branch)]
374
elif len(revision) == 2:
375
from_revno, from_revid = revision[0].in_history(from_branch)
376
to_revno, to_revid = revision[1].in_history(from_branch)
378
to_revno = from_branch.revno()
380
for revno in range(from_revno, to_revno + 1):
381
todo.append(from_branch.get_rev_id(revno))
383
raise CommandError(gettext(
384
"--revision takes only one or two arguments"))
386
raise CommandError(gettext("--revision is mandatory"))
388
wt = WorkingTree.open(directory)
391
state = RebaseState1(wt)
392
replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
393
pb = ui.ui_factory.nested_progress_bar()
396
pb.update(gettext("replaying commits"), todo.index(revid), len(todo))
397
wt.branch.repository.fetch(from_branch.repository, revid)
398
newrevid = regenerate_default_revid(wt.branch.repository, revid)
399
replayer(revid, newrevid, [wt.last_revision()])
406
class cmd_pseudonyms(Command):
407
"""Show a list of 'pseudonym' revisions.
409
Pseudonym revisions are revisions that are roughly the same revision,
410
usually because they were converted from the same revision in a
411
foreign version control system.
414
takes_args = ['repository?']
417
def run(self, repository=None):
418
from ...controldir import ControlDir
419
dir, _ = ControlDir.open_containing(repository)
420
r = dir.find_repository()
421
from .pseudonyms import find_pseudonyms
422
for pseudonyms in find_pseudonyms(r, r.all_revision_ids()):
423
self.outf.write(", ".join(pseudonyms) + "\n")
426
class cmd_rebase_foreign(Command):
427
"""Rebase revisions based on a branch created with a different import tool.
429
This will change the identity of revisions whose parents
430
were mapped from revisions in the other version control system.
432
You are recommended to run "brz check" in the local repository
433
after running this command.
435
takes_args = ['new_base?']
438
Option("idmap-file", help="Write map with old and new revision ids.",
443
help="Branch to replay onto, rather than the one containing the working directory.",
447
def run(self, new_base=None, verbose=False, idmap_file=None, directory="."):
451
from ...branch import Branch
452
from ...workingtree import WorkingTree
453
from .pseudonyms import (
455
generate_rebase_map_from_pseudonyms,
458
from .upgrade import (
459
create_deterministic_revid,
462
from ...foreign import (
463
update_workingtree_fileids,
467
wt_to = WorkingTree.open(directory)
468
branch_to = wt_to.branch
469
except NoWorkingTree:
471
branch_to = Branch.open(directory)
473
stored_loc = branch_to.get_parent()
475
if stored_loc is None:
476
raise CommandError(gettext(
477
"No pull location known or specified."))
479
display_url = urlutils.unescape_for_display(
480
stored_loc, self.outf.encoding)
481
self.outf.write(gettext("Using saved location: %s\n") % display_url)
482
new_base = Branch.open(stored_loc)
484
new_base = Branch.open(new_base)
486
branch_to.repository.fetch(
487
new_base.repository, revision_id=branch_to.last_revision())
489
pseudonyms = pseudonyms_as_dict(find_pseudonyms(
490
branch_to.repository, branch_to.repository.all_revision_ids()))
492
def generate_rebase_map(revision_id):
493
return generate_rebase_map_from_pseudonyms(
494
pseudonyms, branch_to.repository.get_ancestry(revision_id),
495
branch_to.repository.get_ancestry(new_base.last_revision()))
496
def determine_new_revid(old_revid, new_parents):
497
return create_deterministic_revid(old_revid, new_parents)
498
branch_to.lock_write()
500
graph = branch_to.repository.get_graph()
501
renames = upgrade_branch(
502
branch_to, generate_rebase_map,
503
determine_new_revid, allow_changes=True,
505
if wt_to is not None:
506
basis_tree = wt_to.basis_tree()
507
basis_tree.lock_read()
509
update_workingtree_fileids(wt_to, basis_tree)
516
note(gettext("Nothing to do."))
518
if idmap_file is not None:
519
f = open(idmap_file, 'w')
521
for oldid, newid in renames.iteritems():
522
f.write("%s\t%s\n" % (oldid, newid))
526
if wt_to is not None:
527
wt_to.set_last_revision(branch_to.last_revision())