Merge lp:~jelmer/brz/rebase into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/rebase
Merge into: lp:brz
Diff against target: 3072 lines (+3006/-0)
13 files modified
breezy/plugins/rewrite/README (+59/-0)
breezy/plugins/rewrite/__init__.py (+76/-0)
breezy/plugins/rewrite/commands.py (+529/-0)
breezy/plugins/rewrite/maptree.py (+124/-0)
breezy/plugins/rewrite/pseudonyms.py (+211/-0)
breezy/plugins/rewrite/rebase.py (+604/-0)
breezy/plugins/rewrite/tests/__init__.py (+29/-0)
breezy/plugins/rewrite/tests/test_blackbox.py (+363/-0)
breezy/plugins/rewrite/tests/test_maptree.py (+81/-0)
breezy/plugins/rewrite/tests/test_pseudonyms.py (+39/-0)
breezy/plugins/rewrite/tests/test_rebase.py (+669/-0)
breezy/plugins/rewrite/tests/test_upgrade.py (+32/-0)
breezy/plugins/rewrite/upgrade.py (+190/-0)
To merge this branch: bzr merge lp:~jelmer/brz/rebase
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+374758@code.launchpad.net

Commit message

Port the rewrite plugin to breezy.

Description of the change

Port the rewrite plugin to breezy.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'breezy/plugins/rewrite'
2=== added file 'breezy/plugins/rewrite/README'
3--- breezy/plugins/rewrite/README 1970-01-01 00:00:00 +0000
4+++ breezy/plugins/rewrite/README 2020-01-11 23:46:53 +0000
5@@ -0,0 +1,59 @@
6+This plugin provides various commands for rewriting Bazaar revisions in
7+an automated fashion.
8+
9+It includes a 'bzr rebase' command, in the same fashion of the
10+famous ``git rebase`` command.
11+
12+==========
13+How to use
14+==========
15+Simply run ``bzr rebase``, optionally specifying an upstream branch. If no
16+branch is specified, it will use the current parent branch (which is usually
17+what you want).
18+
19+If the rebase fails halfway through, perhaps because of conflicts
20+replaying a revision, it will abort. When it's aborted, you can either resolve
21+the conflicts (using "bzr resolve") and run ``bzr rebase-continue`` or
22+abort the rebase and undo all changes it's done using ``bzr rebase-abort``.
23+The current state of the rebase can be viewed with the ``bzr rebase-todo``
24+command. See "bzr help rebase" for more details.
25+
26+In the future, I hope it can also support rebasing on top of an unrelated
27+branch.
28+
29+============
30+How it works
31+============
32+The plugin will start off by writing a plan for the rebase, which it
33+will write to .bzr/checkout/rebase-state. If the rebase is interrupted
34+(conflicts that have to be resolved by the user) and
35+needs to be continued or aborted, it will read this file to see what needs
36+(still) needs to be done.
37+
38+The rebase-state file contains the following information:
39+ * last-revision-info when the rebase was started
40+ * map of revisions that need to be replaced. In simple situations,
41+ this would contain the revision on top of which the rebase is taking
42+ place and the revisions that are only on the branch that is being
43+ rebased.
44+
45+ The map is from old revid to a tuple with new revid and new parents.
46+
47+ For example, in the following scenario:
48+
49+ A -> B -> C -> D main
50+ \ -> E -> F next
51+
52+ Where next would be rebased on top of main, the replace map would look
53+ something like this:
54+
55+ E -> (E', [D])
56+ F -> (F', [E'])
57+
58+The rebase plan contains the newly generated revision ids so that it is
59+not necessary to update it after each revision that has been written
60+because the new generated revision ids would have to be known when rewriting
61+their children.
62+
63+The 'rebase-of' revision property of newly created revisions
64+will be set to the revision id of the revision they are a rewrite of.
65
66=== added file 'breezy/plugins/rewrite/__init__.py'
67--- breezy/plugins/rewrite/__init__.py 1970-01-01 00:00:00 +0000
68+++ breezy/plugins/rewrite/__init__.py 2020-01-11 23:46:53 +0000
69@@ -0,0 +1,76 @@
70+# Copyright (C) 2007 by Jelmer Vernooij <jelmer@samba.org>
71+#
72+# This program is free software; you can redistribute it and/or modify
73+# it under the terms of the GNU General Public License as published by
74+# the Free Software Foundation; either version 2 of the License, or
75+# (at your option) any later version.
76+#
77+# This program is distributed in the hope that it will be useful,
78+# but WITHOUT ANY WARRANTY; without even the implied warranty of
79+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
80+# GNU General Public License for more details.
81+#
82+# You should have received a copy of the GNU General Public License
83+# along with this program; if not, write to the Free Software
84+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
85+"""Rebase support.
86+
87+The Bazaar rebase plugin adds support for rebasing branches to Bazaar.
88+It adds the command 'rebase' to Bazaar. When conflicts occur when replaying
89+patches, the user can resolve the conflict and continue the rebase using the
90+'rebase-continue' command or abort using the 'rebase-abort' command.
91+"""
92+
93+from __future__ import absolute_import
94+
95+from ... import errors
96+from ...commands import plugin_cmds
97+
98+from ...bzr.bzrdir import BzrFormat
99+
100+BzrFormat.register_feature(b"rebase-v1")
101+
102+from ...i18n import load_plugin_translations
103+translation = load_plugin_translations("bzr-rewrite")
104+gettext = translation.gettext
105+
106+for cmd in ['rebase', 'rebase_abort', 'rebase_continue', 'rebase_todo',
107+ 'replay', 'pseudonyms', 'rebase_foreign']:
108+ plugin_cmds.register_lazy("cmd_%s" % cmd, [], __name__ + ".commands")
109+
110+
111+def show_rebase_summary(params):
112+ if getattr(params.new_tree, "_format", None) is None:
113+ return
114+ features = getattr(params.new_tree._format, "features", None)
115+ if features is None:
116+ return
117+ if "rebase-v1" not in features:
118+ return
119+ from .rebase import (
120+ RebaseState1,
121+ rebase_todo,
122+ )
123+ state = RebaseState1(params.new_tree)
124+ try:
125+ replace_map = state.read_plan()[1]
126+ except errors.NoSuchFile:
127+ return
128+ todo = list(rebase_todo(params.new_tree.branch.repository, replace_map))
129+ params.to_file.write('Rebase in progress. (%d revisions left)\n' % len(todo))
130+
131+
132+from ...hooks import install_lazy_named_hook
133+
134+install_lazy_named_hook(
135+ 'breezy.status', 'hooks', 'post_status',
136+ show_rebase_summary, 'rewrite status')
137+
138+
139+def load_tests(loader, basic_tests, pattern):
140+ testmod_names = [
141+ 'tests',
142+ ]
143+ basic_tests.addTest(loader.loadTestsFromModuleNames(
144+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
145+ return basic_tests
146
147=== added file 'breezy/plugins/rewrite/commands.py'
148--- breezy/plugins/rewrite/commands.py 1970-01-01 00:00:00 +0000
149+++ breezy/plugins/rewrite/commands.py 2020-01-11 23:46:53 +0000
150@@ -0,0 +1,529 @@
151+# Copyright (C) 2007 by Jelmer Vernooij <jelmer@samba.org>
152+#
153+# This program is free software; you can redistribute it and/or modify
154+# it under the terms of the GNU General Public License as published by
155+# the Free Software Foundation; either version 2 of the License, or
156+# (at your option) any later version.
157+#
158+# This program is distributed in the hope that it will be useful,
159+# but WITHOUT ANY WARRANTY; without even the implied warranty of
160+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
161+# GNU General Public License for more details.
162+#
163+# You should have received a copy of the GNU General Public License
164+# along with this program; if not, write to the Free Software
165+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
166+
167+"""Bazaar command-line subcommands."""
168+
169+from __future__ import absolute_import
170+
171+from ...commands import (
172+ Command,
173+ display_command,
174+ )
175+from ...errors import (
176+ BzrCommandError,
177+ ConflictsInTree,
178+ NoSuchFile,
179+ NoWorkingTree,
180+ UncommittedChanges,
181+ )
182+from ...option import (
183+ Option,
184+ )
185+from ...trace import (
186+ note,
187+ )
188+
189+from ...i18n import gettext
190+
191+
192+def finish_rebase(state, wt, replace_map, replayer):
193+ from .rebase import (
194+ rebase,
195+ )
196+ try:
197+ # Start executing plan from current Branch.last_revision()
198+ rebase(wt.branch.repository, replace_map, replayer)
199+ except ConflictsInTree:
200+ raise BzrCommandError(gettext(
201+ "A conflict occurred replaying a commit."
202+ " Resolve the conflict and run 'brz rebase-continue' or "
203+ "run 'brz rebase-abort'."))
204+ # Remove plan file
205+ state.remove_plan()
206+
207+
208+class cmd_rebase(Command):
209+ """Re-base a branch.
210+
211+ Rebasing is the process of taking a branch and modifying the history so
212+ that it appears to start from a different point. This can be useful
213+ to clean up the history before submitting your changes. The tree at the
214+ end of the process will be the same as if you had merged the other branch,
215+ but the history will be different.
216+
217+ The command takes the location of another branch on to which the branch in
218+ the specified directory (by default, the current working directory)
219+ will be rebased. If a branch is not specified then the parent branch
220+ is used, and this is usually the desired result.
221+
222+ The first step identifies the revisions that are in the current branch that
223+ are not in the parent branch. The current branch is then set to be at the
224+ same revision as the target branch, and each revision is replayed on top
225+ of the branch. At the end of the process it will appear as though your
226+ current branch was branched off the current last revision of the target.
227+
228+ Each revision that is replayed may cause conflicts in the tree. If this
229+ happens the command will stop and allow you to fix them up. Resolve the
230+ commits as you would for a merge, and then run 'brz resolve' to marked
231+ them as resolved. Once you have resolved all the conflicts you should
232+ run 'brz rebase-continue' to continue the rebase operation.
233+
234+ If conflicts are encountered and you decide that you do not wish to continue
235+ you can run 'brz rebase-abort'.
236+
237+ The '--onto' option allows you to specify a different revision in the
238+ target branch to start at when replaying the revisions. This means that
239+ you can change the point at which the current branch will appear to be
240+ branched from when the operation completes.
241+ """
242+ takes_args = ['upstream_location?']
243+ takes_options = [
244+ 'revision', 'merge-type', 'verbose',
245+ Option(
246+ 'dry-run',
247+ help="Show what would be done, but don't actually do anything."),
248+ Option(
249+ 'always-rebase-merges',
250+ help="Don't skip revisions that merge already present revisions."),
251+ Option(
252+ 'pending-merges',
253+ help="Rebase pending merges onto local branch."),
254+ Option(
255+ 'onto', help='Different revision to replay onto.',
256+ type=str),
257+ Option(
258+ 'directory',
259+ short_name='d',
260+ help="Branch to replay onto, rather than the one containing the working directory.",
261+ type=str)
262+ ]
263+
264+ @display_command
265+ def run(self, upstream_location=None, onto=None, revision=None,
266+ merge_type=None, verbose=False, dry_run=False,
267+ always_rebase_merges=False, pending_merges=False,
268+ directory="."):
269+ from ...branch import Branch
270+ from ...revisionspec import RevisionSpec
271+ from ...workingtree import WorkingTree
272+ from .rebase import (
273+ generate_simple_plan,
274+ rebase,
275+ RebaseState1,
276+ WorkingTreeRevisionRewriter,
277+ regenerate_default_revid,
278+ rebase_todo,
279+ )
280+ if revision is not None and pending_merges:
281+ raise BzrCommandError(gettext(
282+ "--revision and --pending-merges are mutually exclusive"))
283+
284+ wt = WorkingTree.open_containing(directory)[0]
285+ wt.lock_write()
286+ try:
287+ state = RebaseState1(wt)
288+ if upstream_location is None:
289+ if pending_merges:
290+ upstream_location = directory
291+ else:
292+ upstream_location = wt.branch.get_parent()
293+ if upstream_location is None:
294+ raise BzrCommandError(gettext("No upstream branch specified."))
295+ note(gettext("Rebasing on %s"), upstream_location)
296+ upstream = Branch.open_containing(upstream_location)[0]
297+ upstream_repository = upstream.repository
298+ upstream_revision = upstream.last_revision()
299+ # Abort if there already is a plan file
300+ if state.has_plan():
301+ raise BzrCommandError(gettext(
302+ "A rebase operation was interrupted. "
303+ "Continue using 'brz rebase-continue' or abort using 'brz "
304+ "rebase-abort'"))
305+
306+ start_revid = None
307+ stop_revid = None
308+ if revision is not None:
309+ if len(revision) == 1:
310+ if revision[0] is not None:
311+ stop_revid = revision[0].as_revision_id(wt.branch)
312+ elif len(revision) == 2:
313+ if revision[0] is not None:
314+ start_revid = revision[0].as_revision_id(wt.branch)
315+ if revision[1] is not None:
316+ stop_revid = revision[1].as_revision_id(wt.branch)
317+ else:
318+ raise BzrCommandError(gettext(
319+ "--revision takes only one or two arguments"))
320+
321+ if pending_merges:
322+ wt_parents = wt.get_parent_ids()
323+ if len(wt_parents) in (0, 1):
324+ raise BzrCommandError(gettext("No pending merges present."))
325+ elif len(wt_parents) > 2:
326+ raise BzrCommandError(gettext(
327+ "Rebasing more than one pending merge not supported"))
328+ stop_revid = wt_parents[1]
329+ assert stop_revid is not None, "stop revid invalid"
330+
331+ # Check for changes in the working tree.
332+ if (not pending_merges and
333+ wt.basis_tree().changes_from(wt).has_changed()):
334+ raise UncommittedChanges(wt)
335+
336+ # Pull required revisions
337+ wt.branch.repository.fetch(upstream_repository, upstream_revision)
338+ if onto is None:
339+ onto = upstream.last_revision()
340+ else:
341+ rev_spec = RevisionSpec.from_string(onto)
342+ onto = rev_spec.as_revision_id(upstream)
343+
344+ wt.branch.repository.fetch(upstream_repository, revision_id=onto)
345+
346+ if stop_revid is None:
347+ stop_revid = wt.branch.last_revision()
348+ repo_graph = wt.branch.repository.get_graph()
349+ our_new, onto_unique = repo_graph.find_difference(stop_revid, onto)
350+
351+ if start_revid is None:
352+ if not onto_unique:
353+ self.outf.write(gettext("No revisions to rebase.\n"))
354+ return
355+ if not our_new:
356+ self.outf.write(gettext(
357+ "Base branch is descendant of current "
358+ "branch. Pulling instead.\n"))
359+ if not dry_run:
360+ wt.pull(upstream, stop_revision=onto)
361+ return
362+ # else: include extra revisions needed to make start_revid mean
363+ # something.
364+
365+ # Create plan
366+ replace_map = generate_simple_plan(
367+ our_new, start_revid, stop_revid, onto, repo_graph,
368+ lambda revid, ps: regenerate_default_revid(
369+ wt.branch.repository, revid),
370+ not always_rebase_merges)
371+
372+ if verbose or dry_run:
373+ todo = list(rebase_todo(wt.branch.repository, replace_map))
374+ note(gettext('%d revisions will be rebased:') % len(todo))
375+ for revid in todo:
376+ note("%s" % revid)
377+
378+ if not dry_run:
379+ # Write plan file
380+ state.write_plan(replace_map)
381+
382+ replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
383+
384+ finish_rebase(state, wt, replace_map, replayer)
385+ finally:
386+ wt.unlock()
387+
388+
389+class cmd_rebase_abort(Command):
390+ """Abort an interrupted rebase."""
391+
392+ takes_options = [
393+ Option(
394+ 'directory',
395+ short_name='d',
396+ help="Branch to replay onto, rather than the one containing the working directory.",
397+ type=str)]
398+
399+ @display_command
400+ def run(self, directory="."):
401+ from .rebase import (
402+ RebaseState1,
403+ complete_revert,
404+ )
405+ from ...workingtree import WorkingTree
406+ wt = WorkingTree.open_containing(directory)[0]
407+ wt.lock_write()
408+ try:
409+ state = RebaseState1(wt)
410+ # Read plan file and set last revision
411+ try:
412+ last_rev_info = state.read_plan()[0]
413+ except NoSuchFile:
414+ raise BzrCommandError("No rebase to abort")
415+ complete_revert(wt, [last_rev_info[1]])
416+ state.remove_plan()
417+ finally:
418+ wt.unlock()
419+
420+
421+class cmd_rebase_continue(Command):
422+ """Continue an interrupted rebase after resolving conflicts."""
423+ takes_options = [
424+ 'merge-type', Option(
425+ 'directory',
426+ short_name='d',
427+ help="Branch to replay onto, rather than the one containing the working directory.",
428+ type=str)]
429+
430+ @display_command
431+ def run(self, merge_type=None, directory="."):
432+ from .rebase import (
433+ RebaseState1,
434+ WorkingTreeRevisionRewriter,
435+ )
436+ from ...workingtree import WorkingTree
437+ wt = WorkingTree.open_containing(directory)[0]
438+ wt.lock_write()
439+ try:
440+ state = RebaseState1(wt)
441+ replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
442+ # Abort if there are any conflicts
443+ if len(wt.conflicts()) != 0:
444+ raise BzrCommandError(gettext(
445+ "There are still conflicts present. "
446+ "Resolve the conflicts and then run "
447+ "'brz resolve' and try again."))
448+ # Read plan file
449+ try:
450+ replace_map = state.read_plan()[1]
451+ except NoSuchFile:
452+ raise BzrCommandError(gettext("No rebase to continue"))
453+ oldrevid = state.read_active_revid()
454+ if oldrevid is not None:
455+ oldrev = wt.branch.repository.get_revision(oldrevid)
456+ replayer.commit_rebase(oldrev, replace_map[oldrevid][0])
457+ finish_rebase(state, wt, replace_map, replayer)
458+ finally:
459+ wt.unlock()
460+
461+
462+class cmd_rebase_todo(Command):
463+ """Print list of revisions that still need to be replayed as part of the
464+ current rebase operation.
465+
466+ """
467+
468+ takes_options = [
469+ Option(
470+ 'directory',
471+ short_name='d',
472+ help="Branch to replay onto, rather than the one containing the working directory.",
473+ type=str)]
474+
475+ def run(self, directory="."):
476+ from .rebase import (
477+ RebaseState1,
478+ rebase_todo,
479+ )
480+ from ...workingtree import WorkingTree
481+ wt = WorkingTree.open_containing(directory)[0]
482+ with wt.lock_read():
483+ state = RebaseState1(wt)
484+ try:
485+ replace_map = state.read_plan()[1]
486+ except NoSuchFile:
487+ raise BzrCommandError(gettext("No rebase in progress"))
488+ currentrevid = state.read_active_revid()
489+ if currentrevid is not None:
490+ note(gettext("Currently replaying: %s") % currentrevid)
491+ for revid in rebase_todo(wt.branch.repository, replace_map):
492+ note(gettext("{0} -> {1}").format(revid, replace_map[revid][0]))
493+
494+
495+class cmd_replay(Command):
496+ """Replay commits from another branch on top of this one.
497+
498+ """
499+
500+ takes_options = [
501+ 'revision', 'merge-type',
502+ Option(
503+ 'directory',
504+ short_name='d',
505+ help="Branch to replay onto, rather than the one containing the working directory.",
506+ type=str)]
507+ takes_args = ['location']
508+ hidden = True
509+
510+ def run(self, location, revision=None, merge_type=None, directory="."):
511+ from ...branch import Branch
512+ from ...workingtree import WorkingTree
513+ from ... import ui
514+ from .rebase import (
515+ RebaseState1,
516+ regenerate_default_revid,
517+ WorkingTreeRevisionRewriter,
518+ )
519+
520+ from_branch = Branch.open_containing(location)[0]
521+
522+ if revision is not None:
523+ if len(revision) == 1:
524+ if revision[0] is not None:
525+ todo = [revision[0].as_revision_id(from_branch)]
526+ elif len(revision) == 2:
527+ from_revno, from_revid = revision[0].in_history(from_branch)
528+ to_revno, to_revid = revision[1].in_history(from_branch)
529+ if to_revid is None:
530+ to_revno = from_branch.revno()
531+ todo = []
532+ for revno in range(from_revno, to_revno + 1):
533+ todo.append(from_branch.get_rev_id(revno))
534+ else:
535+ raise BzrCommandError(gettext(
536+ "--revision takes only one or two arguments"))
537+ else:
538+ raise BzrCommandError(gettext("--revision is mandatory"))
539+
540+ wt = WorkingTree.open(directory)
541+ wt.lock_write()
542+ try:
543+ state = RebaseState1(wt)
544+ replayer = WorkingTreeRevisionRewriter(wt, state, merge_type=merge_type)
545+ pb = ui.ui_factory.nested_progress_bar()
546+ try:
547+ for revid in todo:
548+ pb.update(gettext("replaying commits"), todo.index(revid), len(todo))
549+ wt.branch.repository.fetch(from_branch.repository, revid)
550+ newrevid = regenerate_default_revid(wt.branch.repository, revid)
551+ replayer(revid, newrevid, [wt.last_revision()])
552+ finally:
553+ pb.finished()
554+ finally:
555+ wt.unlock()
556+
557+
558+class cmd_pseudonyms(Command):
559+ """Show a list of 'pseudonym' revisions.
560+
561+ Pseudonym revisions are revisions that are roughly the same revision,
562+ usually because they were converted from the same revision in a
563+ foreign version control system.
564+ """
565+
566+ takes_args = ['repository?']
567+ hidden = True
568+
569+ def run(self, repository=None):
570+ from ...controldir import ControlDir
571+ dir, _ = ControlDir.open_containing(repository)
572+ r = dir.find_repository()
573+ from .pseudonyms import find_pseudonyms
574+ for pseudonyms in find_pseudonyms(r, r.all_revision_ids()):
575+ self.outf.write(", ".join(pseudonyms) + "\n")
576+
577+
578+class cmd_rebase_foreign(Command):
579+ """Rebase revisions based on a branch created with a different import tool.
580+
581+ This will change the identity of revisions whose parents
582+ were mapped from revisions in the other version control system.
583+
584+ You are recommended to run "brz check" in the local repository
585+ after running this command.
586+ """
587+ takes_args = ['new_base?']
588+ takes_options = [
589+ 'verbose',
590+ Option("idmap-file", help="Write map with old and new revision ids.",
591+ type=str),
592+ Option(
593+ 'directory',
594+ short_name='d',
595+ help="Branch to replay onto, rather than the one containing the working directory.",
596+ type=str)
597+ ]
598+
599+ def run(self, new_base=None, verbose=False, idmap_file=None, directory="."):
600+ from ... import (
601+ urlutils,
602+ )
603+ from ...branch import Branch
604+ from ...workingtree import WorkingTree
605+ from .pseudonyms import (
606+ find_pseudonyms,
607+ generate_rebase_map_from_pseudonyms,
608+ pseudonyms_as_dict,
609+ )
610+ from .upgrade import (
611+ create_deterministic_revid,
612+ upgrade_branch,
613+ )
614+ from ...foreign import (
615+ update_workingtree_fileids,
616+ )
617+
618+ try:
619+ wt_to = WorkingTree.open(directory)
620+ branch_to = wt_to.branch
621+ except NoWorkingTree:
622+ wt_to = None
623+ branch_to = Branch.open(directory)
624+
625+ stored_loc = branch_to.get_parent()
626+ if new_base is None:
627+ if stored_loc is None:
628+ raise BzrCommandError(gettext(
629+ "No pull location known or specified."))
630+ else:
631+ display_url = urlutils.unescape_for_display(
632+ stored_loc, self.outf.encoding)
633+ self.outf.write(gettext("Using saved location: %s\n") % display_url)
634+ new_base = Branch.open(stored_loc)
635+ else:
636+ new_base = Branch.open(new_base)
637+
638+ branch_to.repository.fetch(
639+ new_base.repository, revision_id=branch_to.last_revision())
640+
641+ pseudonyms = pseudonyms_as_dict(find_pseudonyms(
642+ branch_to.repository, branch_to.repository.all_revision_ids()))
643+
644+ def generate_rebase_map(revision_id):
645+ return generate_rebase_map_from_pseudonyms(
646+ pseudonyms, branch_to.repository.get_ancestry(revision_id),
647+ branch_to.repository.get_ancestry(new_base.last_revision()))
648+ def determine_new_revid(old_revid, new_parents):
649+ return create_deterministic_revid(old_revid, new_parents)
650+ branch_to.lock_write()
651+ try:
652+ graph = branch_to.repository.get_graph()
653+ renames = upgrade_branch(
654+ branch_to, generate_rebase_map,
655+ determine_new_revid, allow_changes=True,
656+ verbose=verbose)
657+ if wt_to is not None:
658+ basis_tree = wt_to.basis_tree()
659+ basis_tree.lock_read()
660+ try:
661+ update_workingtree_fileids(wt_to, basis_tree)
662+ finally:
663+ basis_tree.unlock()
664+ finally:
665+ branch_to.unlock()
666+
667+ if renames == {}:
668+ note(gettext("Nothing to do."))
669+
670+ if idmap_file is not None:
671+ f = open(idmap_file, 'w')
672+ try:
673+ for oldid, newid in renames.iteritems():
674+ f.write("%s\t%s\n" % (oldid, newid))
675+ finally:
676+ f.close()
677+
678+ if wt_to is not None:
679+ wt_to.set_last_revision(branch_to.last_revision())
680
681=== added file 'breezy/plugins/rewrite/maptree.py'
682--- breezy/plugins/rewrite/maptree.py 1970-01-01 00:00:00 +0000
683+++ breezy/plugins/rewrite/maptree.py 2020-01-11 23:46:53 +0000
684@@ -0,0 +1,124 @@
685+# Copyright (C) 2006-2007 by Jelmer Vernooij
686+#
687+# This program is free software; you can redistribute it and/or modify
688+# it under the terms of the GNU General Public License as published by
689+# the Free Software Foundation; either version 2 of the License, or
690+# (at your option) any later version.
691+#
692+# This program is distributed in the hope that it will be useful,
693+# but WITHOUT ANY WARRANTY; without even the implied warranty of
694+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
695+# GNU General Public License for more details.
696+#
697+# You should have received a copy of the GNU General Public License
698+# along with this program; if not, write to the Free Software
699+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
700+"""Map Tree."""
701+
702+from __future__ import absolute_import
703+
704+def map_file_ids(repository, old_parents, new_parents):
705+ """Try to determine the equivalent file ids in two sets of parents.
706+
707+ :param repository: Repository to use
708+ :param old_parents: List of revision ids of old parents
709+ :param new_parents: List of revision ids of new parents
710+ """
711+ assert len(old_parents) == len(new_parents)
712+ ret = {}
713+ for (oldp, newp) in zip(old_parents, new_parents):
714+ oldtree = repository.revision_tree(oldp)
715+ newtree = repository.revision_tree(newp)
716+ for path, ie in oldtree.iter_entries_by_dir():
717+ file_id = newtree.path2id(path)
718+ if file_id is not None:
719+ ret[ie.file_id] = file_id
720+ return ret
721+
722+
723+class MapTree(object):
724+ """Wrapper around a tree that translates file ids.
725+ """
726+
727+ def __init__(self, oldtree, fileid_map):
728+ """Create a new MapTree.
729+
730+ :param oldtree: Old tree to map to.
731+ :param fileid_map: Map with old -> new file ids.
732+ """
733+ self.oldtree = oldtree
734+ self.map = fileid_map
735+
736+ def old_id(self, file_id):
737+ """Look up the original file id of a file.
738+
739+ :param file_id: New file id
740+ :return: Old file id if mapped, otherwise new file id
741+ """
742+ for x in self.map:
743+ if self.map[x] == file_id:
744+ return x
745+ return file_id
746+
747+ def new_id(self, file_id):
748+ """Look up the new file id of a file.
749+
750+ :param file_id: Old file id
751+ :return: New file id
752+ """
753+ try:
754+ return self.map[file_id]
755+ except KeyError:
756+ return file_id
757+
758+ def get_file_sha1(self, path, file_id=None):
759+ "See Tree.get_file_sha1()."""
760+ return self.oldtree.get_file_sha1(path)
761+
762+ def get_file_with_stat(self, path, file_id=None):
763+ "See Tree.get_file_with_stat()."""
764+ if getattr(self.oldtree, "get_file_with_stat", None) is not None:
765+ return self.oldtree.get_file_with_stat(path=path)
766+ else:
767+ return self.get_file(path), None
768+
769+ def get_file(self, path, file_id=None):
770+ "See Tree.get_file()."""
771+ return self.oldtree.get_file(path)
772+
773+ def is_executable(self, path, file_id=None):
774+ "See Tree.is_executable()."""
775+ return self.oldtree.is_executable(path)
776+
777+ def has_filename(self, filename):
778+ "See Tree.has_filename()."""
779+ return self.oldtree.has_filename(filename)
780+
781+ def path_content_summary(self, path):
782+ "See Tree.path_content_summary()."""
783+ return self.oldtree.path_content_summary(path)
784+
785+ def map_ie(self, ie):
786+ """Fix the references to old file ids in an inventory entry.
787+
788+ :param ie: Inventory entry to map
789+ :return: New inventory entry
790+ """
791+ new_ie = ie.copy()
792+ new_ie.file_id = self.new_id(new_ie.file_id)
793+ new_ie.parent_id = self.new_id(new_ie.parent_id)
794+ return new_ie
795+
796+ def iter_entries_by_dir(self):
797+ """See Tree.iter_entries_by_dir."""
798+ for path, ie in self.oldtree.iter_entries_by_dir():
799+ yield path, self.map_ie(ie)
800+
801+ def path2id(self, path):
802+ file_id = self.oldtree.path2id(path)
803+ if file_id is None:
804+ return None
805+ return self.new_id(file_id)
806+
807+ def id2path(self, file_id):
808+ return self.oldtree.id2path(self.old_id(file_id=file_id))
809
810=== added file 'breezy/plugins/rewrite/pseudonyms.py'
811--- breezy/plugins/rewrite/pseudonyms.py 1970-01-01 00:00:00 +0000
812+++ breezy/plugins/rewrite/pseudonyms.py 2020-01-11 23:46:53 +0000
813@@ -0,0 +1,211 @@
814+# Copyright (C) 2009 by Jelmer Vernooij <jelmer@samba.org>
815+#
816+# This program is free software; you can redistribute it and/or modify
817+# it under the terms of the GNU General Public License as published by
818+# the Free Software Foundation; either version 2 of the License, or
819+# (at your option) any later version.
820+#
821+# This program is distributed in the hope that it will be useful,
822+# but WITHOUT ANY WARRANTY; without even the implied warranty of
823+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
824+# GNU General Public License for more details.
825+#
826+# You should have received a copy of the GNU General Public License
827+# along with this program; if not, write to the Free Software
828+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
829+
830+"""Revision pseudonyms."""
831+
832+from __future__ import absolute_import
833+
834+from collections import defaultdict
835+
836+from breezy import (
837+ errors,
838+ foreign,
839+ ui,
840+ urlutils,
841+ )
842+
843+
844+def parse_git_svn_id(text):
845+ """Parse a git svn id string.
846+
847+ :param text: git svn id
848+ :return: URL, revision number, uuid
849+ """
850+ (head, uuid) = text.rsplit(" ", 1)
851+ (full_url, rev) = head.rsplit("@", 1)
852+ return (full_url.encode("utf-8"), int(rev), uuid.encode("utf-8"))
853+
854+
855+class SubversionBranchUrlFinder(object):
856+
857+ def __init__(self):
858+ self._roots = defaultdict(set)
859+
860+ def find_root(self, uuid, url):
861+ for root in self._roots[uuid]:
862+ if url.startswith(root):
863+ return root
864+ try:
865+ from subvertpy.ra import RemoteAccess
866+ except ImportError:
867+ return None
868+ c = RemoteAccess(url)
869+ root = c.get_repos_root()
870+ self._roots[uuid].add(root)
871+ return root
872+
873+ def find_branch_path(self, uuid, url):
874+ root = self.find_root(uuid, url)
875+ if root is None:
876+ return None
877+ assert url.startswith(root)
878+ return url[len(root):].strip("/")
879+
880+
881+svn_branch_path_finder = SubversionBranchUrlFinder()
882+
883+
884+def _extract_converted_from_revid(rev):
885+ if "converted-from" not in rev.properties:
886+ return
887+
888+ for line in rev.properties.get("converted-from", "").splitlines():
889+ (kind, serialized_foreign_revid) = line.split(" ", 1)
890+ yield (kind, serialized_foreign_revid)
891+
892+
893+def _extract_cscvs(rev):
894+ """Older-style launchpad-cscvs import."""
895+ if "cscvs-svn-branch-path" not in rev.properties:
896+ return
897+ yield (
898+ "svn", "%s:%s:%s" % (
899+ rev.properties["cscvs-svn-repository-uuid"],
900+ rev.properties["cscvs-svn-revision-number"],
901+ urlutils.quote(rev.properties["cscvs-svn-branch-path"].strip("/"))))
902+
903+
904+def _extract_git_svn_id(rev):
905+ if "git-svn-id" not in rev.properties:
906+ return
907+ (full_url, revnum, uuid) = parse_git_svn_id(rev.properties['git-svn-id'])
908+ branch_path = svn_branch_path_finder.find_branch_path(uuid, full_url)
909+ if branch_path is not None:
910+ yield ("svn", "%s:%d:%s" % (uuid, revnum, urlutils.quote(branch_path)))
911+
912+
913+def _extract_foreign_revision(rev):
914+ # Perhaps 'rev' is a foreign revision ?
915+ if getattr(rev, "foreign_revid", None) is not None:
916+ yield ("svn", rev.mapping.vcs.serialize_foreign_revid(rev.foreign_revid))
917+
918+
919+def _extract_foreign_revid(rev):
920+ # Try parsing the revision id
921+ try:
922+ foreign_revid, mapping = \
923+ foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
924+ except errors.InvalidRevisionId:
925+ pass
926+ else:
927+ yield (
928+ mapping.vcs.abbreviation,
929+ mapping.vcs.serialize_foreign_revid(foreign_revid))
930+
931+
932+def _extract_debian_md5sum(rev):
933+ if 'deb-md5' in rev.properties:
934+ yield ("debian-md5sum", rev.properties["deb-md5"])
935+
936+
937+_foreign_revid_extractors = [
938+ _extract_converted_from_revid,
939+ _extract_cscvs,
940+ _extract_git_svn_id,
941+ _extract_foreign_revision,
942+ _extract_foreign_revid,
943+ _extract_debian_md5sum,
944+ ]
945+
946+
947+def extract_foreign_revids(rev):
948+ """Find ids of semi-equivalent revisions in foreign VCS'es.
949+
950+ :param: Bazaar revision object
951+ :return: Set with semi-equivalent revisions.
952+ """
953+ ret = set()
954+ for extractor in _foreign_revid_extractors:
955+ ret.update(extractor(rev))
956+ return ret
957+
958+
959+def find_pseudonyms(repository, revids):
960+ """Find revisions that are pseudonyms of each other.
961+
962+ :param repository: Repository object
963+ :param revids: Sequence of revision ids to check
964+ :return: Iterable over sets of pseudonyms
965+ """
966+ # Where have foreign revids ended up?
967+ conversions = defaultdict(set)
968+ # What are native revids conversions of?
969+ conversion_of = defaultdict(set)
970+ revs = repository.get_revisions(revids)
971+ pb = ui.ui_factory.nested_progress_bar()
972+ try:
973+ for i, rev in enumerate(revs):
974+ pb.update("finding pseudonyms", i, len(revs))
975+ for foreign_revid in extract_foreign_revids(rev):
976+ conversion_of[rev.revision_id].add(foreign_revid)
977+ conversions[foreign_revid].add(rev.revision_id)
978+ finally:
979+ pb.finished()
980+ done = set()
981+ for foreign_revid in conversions.keys():
982+ ret = set()
983+ check = set(conversions[foreign_revid])
984+ while check:
985+ x = check.pop()
986+ extra = set()
987+ for frevid in conversion_of[x]:
988+ extra.update(conversions[frevid])
989+ del conversions[frevid]
990+ del conversion_of[x]
991+ check.update(extra)
992+ ret.add(x)
993+ if len(ret) > 1:
994+ yield ret
995+
996+
997+def pseudonyms_as_dict(l):
998+ """Convert an iterable over pseudonyms to a dictionary.
999+
1000+ :param l: Iterable over sets of pseudonyms
1001+ :return: Dictionary with pseudonyms for each revid.
1002+ """
1003+ ret = {}
1004+ for pns in l:
1005+ for pn in pns:
1006+ ret[pn] = pns - set([pn])
1007+ return ret
1008+
1009+
1010+def generate_rebase_map_from_pseudonyms(pseudonym_dict, existing, desired):
1011+ """Create a rebase map from pseudonyms and existing/desired ancestry.
1012+
1013+ :param pseudonym_dict: Dictionary with pseudonym as returned by
1014+ pseudonyms_as_dict()
1015+ :param existing: Existing ancestry, might need to be rebased
1016+ :param desired: Desired ancestry
1017+ :return: rebase map, as dictionary
1018+ """
1019+ rebase_map = {}
1020+ for revid in existing:
1021+ for pn in pseudonym_dict.get(revid, []):
1022+ if pn in desired:
1023+ rebase_map[revid] = pn
1024+ return rebase_map
1025
1026=== added file 'breezy/plugins/rewrite/rebase.py'
1027--- breezy/plugins/rewrite/rebase.py 1970-01-01 00:00:00 +0000
1028+++ breezy/plugins/rewrite/rebase.py 2020-01-11 23:46:53 +0000
1029@@ -0,0 +1,604 @@
1030+# Copyright (C) 2006-2007 by Jelmer Vernooij
1031+#
1032+# This program is free software; you can redistribute it and/or modify
1033+# it under the terms of the GNU General Public License as published by
1034+# the Free Software Foundation; either version 2 of the License, or
1035+# (at your option) any later version.
1036+#
1037+# This program is distributed in the hope that it will be useful,
1038+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1039+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1040+# GNU General Public License for more details.
1041+#
1042+# You should have received a copy of the GNU General Public License
1043+# along with this program; if not, write to the Free Software
1044+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1045+
1046+"""Rebase."""
1047+
1048+from __future__ import absolute_import
1049+
1050+import os
1051+
1052+from ... import (
1053+ config as _mod_config,
1054+ osutils,
1055+ )
1056+from ...errors import (
1057+ BzrError,
1058+ NoSuchFile,
1059+ UnknownFormatError,
1060+ NoCommonAncestor,
1061+ UnrelatedBranches,
1062+ )
1063+from ...bzr.generate_ids import gen_revision_id
1064+from ...graph import FrozenHeadsCache
1065+from ...merge import Merger
1066+from ...revision import NULL_REVISION
1067+from ...trace import mutter
1068+from ...tsort import topo_sort
1069+from ...tree import TreeChange
1070+from ... import ui
1071+
1072+from .maptree import (
1073+ MapTree,
1074+ map_file_ids,
1075+ )
1076+
1077+REBASE_PLAN_FILENAME = 'rebase-plan'
1078+REBASE_CURRENT_REVID_FILENAME = 'rebase-current'
1079+REBASE_PLAN_VERSION = 1
1080+REVPROP_REBASE_OF = 'rebase-of'
1081+
1082+class RebaseState(object):
1083+
1084+ def has_plan(self):
1085+ """Check whether there is a rebase plan present.
1086+
1087+ :return: boolean
1088+ """
1089+ raise NotImplementedError(self.has_plan)
1090+
1091+ def read_plan(self):
1092+ """Read a rebase plan file.
1093+
1094+ :return: Tuple with last revision info and replace map.
1095+ """
1096+ raise NotImplementedError(self.read_plan)
1097+
1098+ def write_plan(self, replace_map):
1099+ """Write a rebase plan file.
1100+
1101+ :param replace_map: Replace map (old revid -> (new revid, new parents))
1102+ """
1103+ raise NotImplementedError(self.write_plan)
1104+
1105+ def remove_plan(self):
1106+ """Remove a rebase plan file.
1107+ """
1108+ raise NotImplementedError(self.remove_plan)
1109+
1110+ def write_active_revid(self, revid):
1111+ """Write the id of the revision that is currently being rebased.
1112+
1113+ :param revid: Revision id to write
1114+ """
1115+ raise NotImplementedError(self.write_active_revid)
1116+
1117+ def read_active_revid(self):
1118+ """Read the id of the revision that is currently being rebased.
1119+
1120+ :return: Id of the revision that is being rebased.
1121+ """
1122+ raise NotImplementedError(self.read_active_revid)
1123+
1124+
1125+class RebaseState1(RebaseState):
1126+
1127+ def __init__(self, wt):
1128+ self.wt = wt
1129+ self.transport = wt._transport
1130+
1131+ def has_plan(self):
1132+ """See `RebaseState`."""
1133+ try:
1134+ return self.transport.get_bytes(REBASE_PLAN_FILENAME) != b''
1135+ except NoSuchFile:
1136+ return False
1137+
1138+ def read_plan(self):
1139+ """See `RebaseState`."""
1140+ text = self.transport.get_bytes(REBASE_PLAN_FILENAME)
1141+ if text == b'':
1142+ raise NoSuchFile(REBASE_PLAN_FILENAME)
1143+ return unmarshall_rebase_plan(text)
1144+
1145+ def write_plan(self, replace_map):
1146+ """See `RebaseState`."""
1147+ self.wt.update_feature_flags({b"rebase-v1": b"write-required"})
1148+ content = marshall_rebase_plan(
1149+ self.wt.branch.last_revision_info(), replace_map)
1150+ assert isinstance(content, bytes)
1151+ self.transport.put_bytes(REBASE_PLAN_FILENAME, content)
1152+
1153+ def remove_plan(self):
1154+ """See `RebaseState`."""
1155+ self.wt.update_feature_flags({b"rebase-v1": None})
1156+ self.transport.put_bytes(REBASE_PLAN_FILENAME, b'')
1157+
1158+ def write_active_revid(self, revid):
1159+ """See `RebaseState`."""
1160+ if revid is None:
1161+ revid = NULL_REVISION
1162+ assert isinstance(revid, bytes)
1163+ self.transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, revid)
1164+
1165+ def read_active_revid(self):
1166+ """See `RebaseState`."""
1167+ try:
1168+ text = self.transport.get_bytes(REBASE_CURRENT_REVID_FILENAME).rstrip(b"\n")
1169+ if text == NULL_REVISION:
1170+ return None
1171+ return text
1172+ except NoSuchFile:
1173+ return None
1174+
1175+
1176+def marshall_rebase_plan(last_rev_info, replace_map):
1177+ """Marshall a rebase plan.
1178+
1179+ :param last_rev_info: Last revision info tuple.
1180+ :param replace_map: Replace map (old revid -> (new revid, new parents))
1181+ :return: string
1182+ """
1183+ ret = b"# Bazaar rebase plan %d\n" % REBASE_PLAN_VERSION
1184+ ret += b"%d %s\n" % last_rev_info
1185+ for oldrev in replace_map:
1186+ (newrev, newparents) = replace_map[oldrev]
1187+ ret += b"%s %s" % (oldrev, newrev) + \
1188+ b"".join([b" %s" % p for p in newparents]) + b"\n"
1189+ return ret
1190+
1191+
1192+def unmarshall_rebase_plan(text):
1193+ """Unmarshall a rebase plan.
1194+
1195+ :param text: Text to parse
1196+ :return: Tuple with last revision info, replace map.
1197+ """
1198+ lines = text.split(b'\n')
1199+ # Make sure header is there
1200+ if lines[0] != b"# Bazaar rebase plan %d" % REBASE_PLAN_VERSION:
1201+ raise UnknownFormatError(lines[0])
1202+
1203+ pts = lines[1].split(b" ", 1)
1204+ last_revision_info = (int(pts[0]), pts[1])
1205+ replace_map = {}
1206+ for l in lines[2:]:
1207+ if l == b"":
1208+ # Skip empty lines
1209+ continue
1210+ pts = l.split(b" ")
1211+ replace_map[pts[0]] = (pts[1], tuple(pts[2:]))
1212+ return (last_revision_info, replace_map)
1213+
1214+
1215+def regenerate_default_revid(repository, revid):
1216+ """Generate a revision id for the rebase of an existing revision.
1217+
1218+ :param repository: Repository in which the revision is present.
1219+ :param revid: Revision id of the revision that is being rebased.
1220+ :return: new revision id."""
1221+ if revid == NULL_REVISION:
1222+ return NULL_REVISION
1223+ rev = repository.get_revision(revid)
1224+ return gen_revision_id(rev.committer, rev.timestamp)
1225+
1226+
1227+def generate_simple_plan(todo_set, start_revid, stop_revid, onto_revid, graph,
1228+ generate_revid, skip_full_merged=False):
1229+ """Create a simple rebase plan that replays history based
1230+ on one revision being replayed on top of another.
1231+
1232+ :param todo_set: A set of revisions to rebase. Only the revisions
1233+ topologically between stop_revid and start_revid (inclusive) are
1234+ rebased; other revisions are ignored (and references to them are
1235+ preserved).
1236+ :param start_revid: Id of revision at which to start replaying
1237+ :param stop_revid: Id of revision until which to stop replaying
1238+ :param onto_revid: Id of revision on top of which to replay
1239+ :param graph: Graph object
1240+ :param generate_revid: Function for generating new revision ids
1241+ :param skip_full_merged: Skip revisions that merge already merged
1242+ revisions.
1243+
1244+ :return: replace map
1245+ """
1246+ assert start_revid is None or start_revid in todo_set, \
1247+ "invalid start revid(%r), todo_set(%r)" % (start_revid, todo_set)
1248+ assert stop_revid is None or stop_revid in todo_set, "invalid stop_revid"
1249+ replace_map = {}
1250+ parent_map = graph.get_parent_map(todo_set)
1251+ order = topo_sort(parent_map)
1252+ if stop_revid is None:
1253+ stop_revid = order[-1]
1254+ if start_revid is None:
1255+ # We need a common base.
1256+ lca = graph.find_lca(stop_revid, onto_revid)
1257+ if lca == set([NULL_REVISION]):
1258+ raise UnrelatedBranches()
1259+ start_revid = order[0]
1260+ todo = order[order.index(start_revid):order.index(stop_revid) + 1]
1261+ heads_cache = FrozenHeadsCache(graph)
1262+ # XXX: The output replacemap'd parents should get looked up in some manner
1263+ # by the heads cache? RBC 20080719
1264+ for oldrevid in todo:
1265+ oldparents = parent_map[oldrevid]
1266+ assert isinstance(oldparents, tuple), "not tuple: %r" % oldparents
1267+ parents = []
1268+ # Left parent:
1269+ if heads_cache.heads((oldparents[0], onto_revid)) == set((onto_revid,)):
1270+ parents.append(onto_revid)
1271+ elif oldparents[0] in replace_map:
1272+ parents.append(replace_map[oldparents[0]][0])
1273+ else:
1274+ parents.append(onto_revid)
1275+ parents.append(oldparents[0])
1276+ # Other parents:
1277+ if len(oldparents) > 1:
1278+ additional_parents = heads_cache.heads(oldparents[1:])
1279+ for oldparent in oldparents[1:]:
1280+ if oldparent in additional_parents:
1281+ if heads_cache.heads((oldparent, onto_revid)) == set((onto_revid,)):
1282+ pass
1283+ elif oldparent in replace_map:
1284+ newparent = replace_map[oldparent][0]
1285+ if parents[0] == onto_revid:
1286+ parents[0] = newparent
1287+ else:
1288+ parents.append(newparent)
1289+ else:
1290+ parents.append(oldparent)
1291+ if len(parents) == 1 and skip_full_merged:
1292+ continue
1293+ parents = tuple(parents)
1294+ newrevid = generate_revid(oldrevid, parents)
1295+ assert newrevid != oldrevid, "old and newrevid equal (%r)" % newrevid
1296+ assert isinstance(parents, tuple), "parents not tuple: %r" % parents
1297+ replace_map[oldrevid] = (newrevid, parents)
1298+ return replace_map
1299+
1300+
1301+def generate_transpose_plan(ancestry, renames, graph, generate_revid):
1302+ """Create a rebase plan that replaces a bunch of revisions
1303+ in a revision graph.
1304+
1305+ :param ancestry: Ancestry to consider
1306+ :param renames: Renames of revision
1307+ :param graph: Graph object
1308+ :param generate_revid: Function for creating new revision ids
1309+ """
1310+ replace_map = {}
1311+ todo = []
1312+ children = {}
1313+ parent_map = {}
1314+ for r, ps in ancestry:
1315+ if r not in children:
1316+ children[r] = []
1317+ if ps is None: # Ghost
1318+ continue
1319+ parent_map[r] = ps
1320+ if r not in children:
1321+ children[r] = []
1322+ for p in ps:
1323+ if p not in children:
1324+ children[p] = []
1325+ children[p].append(r)
1326+
1327+ parent_map.update(graph.get_parent_map(filter(lambda x: x not in parent_map, renames.values())))
1328+
1329+ # todo contains a list of revisions that need to
1330+ # be rewritten
1331+ for r, v in renames.items():
1332+ replace_map[r] = (v, parent_map[v])
1333+ todo.append(r)
1334+
1335+ total = len(todo)
1336+ processed = set()
1337+ i = 0
1338+ pb = ui.ui_factory.nested_progress_bar()
1339+ try:
1340+ while len(todo) > 0:
1341+ r = todo.pop()
1342+ processed.add(r)
1343+ i += 1
1344+ pb.update('determining dependencies', i, total)
1345+ # Add entry for them in replace_map
1346+ for c in children[r]:
1347+ if c in renames:
1348+ continue
1349+ if c in replace_map:
1350+ parents = replace_map[c][1]
1351+ else:
1352+ parents = parent_map[c]
1353+ assert isinstance(parents, tuple), \
1354+ "Expected tuple of parents, got: %r" % parents
1355+ # replace r in parents with replace_map[r][0]
1356+ if not replace_map[r][0] in parents:
1357+ parents = list(parents)
1358+ parents[parents.index(r)] = replace_map[r][0]
1359+ parents = tuple(parents)
1360+ replace_map[c] = (generate_revid(c, tuple(parents)),
1361+ tuple(parents))
1362+ if replace_map[c][0] == c:
1363+ del replace_map[c]
1364+ elif c not in processed:
1365+ todo.append(c)
1366+ finally:
1367+ pb.finished()
1368+
1369+ # Remove items from the map that already exist
1370+ for revid in renames:
1371+ if revid in replace_map:
1372+ del replace_map[revid]
1373+
1374+ return replace_map
1375+
1376+
1377+def rebase_todo(repository, replace_map):
1378+ """Figure out what revisions still need to be rebased.
1379+
1380+ :param repository: Repository that contains the revisions
1381+ :param replace_map: Replace map
1382+ """
1383+ for revid, parent_ids in replace_map.items():
1384+ assert isinstance(parent_ids, tuple), "replace map parents not tuple"
1385+ if not repository.has_revision(parent_ids[0]):
1386+ yield revid
1387+
1388+
1389+def rebase(repository, replace_map, revision_rewriter):
1390+ """Rebase a working tree according to the specified map.
1391+
1392+ :param repository: Repository that contains the revisions
1393+ :param replace_map: Dictionary with revisions to (optionally) rewrite
1394+ :param merge_fn: Function for replaying a revision
1395+ """
1396+ # Figure out the dependencies
1397+ graph = repository.get_graph()
1398+ todo = list(graph.iter_topo_order(replace_map.keys()))
1399+ pb = ui.ui_factory.nested_progress_bar()
1400+ try:
1401+ for i, revid in enumerate(todo):
1402+ pb.update('rebase revisions', i, len(todo))
1403+ (newrevid, newparents) = replace_map[revid]
1404+ assert isinstance(newparents, tuple), "Expected tuple for %r" % newparents
1405+ if repository.has_revision(newrevid):
1406+ # Was already converted, no need to worry about it again
1407+ continue
1408+ revision_rewriter(revid, newrevid, newparents)
1409+ finally:
1410+ pb.finished()
1411+
1412+
1413+def wrap_iter_changes(old_iter_changes, map_tree):
1414+ for change in old_iter_changes:
1415+ if change.parent_id[0] is not None:
1416+ old_parent = map_tree.new_id(change.parent_id[0])
1417+ else:
1418+ old_parent = change.parent_id[0]
1419+ if change.parent_id[1] is not None:
1420+ new_parent = map_tree.new_id(change.parent_id[1])
1421+ else:
1422+ new_parent = change.parent_id[1]
1423+ yield TreeChange(
1424+ map_tree.new_id(change.file_id), change.path,
1425+ change.changed_content, change.versioned,
1426+ (old_parent, new_parent), change.name, change.kind,
1427+ change.executable)
1428+
1429+
1430+class CommitBuilderRevisionRewriter(object):
1431+ """Revision rewriter that use commit builder.
1432+
1433+ :ivar repository: Repository in which the revision is present.
1434+ """
1435+
1436+ def __init__(self, repository, map_ids=True):
1437+ self.repository = repository
1438+ self.map_ids = map_ids
1439+
1440+ def _get_present_revisions(self, revids):
1441+ return tuple([p for p in revids if self.repository.has_revision(p)])
1442+
1443+ def __call__(self, oldrevid, newrevid, new_parents):
1444+ """Replay a commit by simply commiting the same snapshot with different
1445+ parents.
1446+
1447+ :param oldrevid: Revision id of the revision to copy.
1448+ :param newrevid: Revision id of the revision to create.
1449+ :param new_parents: Revision ids of the new parent revisions.
1450+ """
1451+ assert isinstance(new_parents, tuple), "CommitBuilderRevisionRewriter: Expected tuple for %r" % new_parents
1452+ mutter('creating copy %r of %r with new parents %r',
1453+ newrevid, oldrevid, new_parents)
1454+ oldrev = self.repository.get_revision(oldrevid)
1455+
1456+ revprops = dict(oldrev.properties)
1457+ revprops[REVPROP_REBASE_OF] = oldrevid.decode('utf-8')
1458+
1459+ # Check what new_ie.file_id should be
1460+ # use old and new parent trees to generate new_id map
1461+ nonghost_oldparents = self._get_present_revisions(oldrev.parent_ids)
1462+ nonghost_newparents = self._get_present_revisions(new_parents)
1463+ oldtree = self.repository.revision_tree(oldrevid)
1464+ if self.map_ids:
1465+ fileid_map = map_file_ids(
1466+ self.repository, nonghost_oldparents,
1467+ nonghost_newparents)
1468+ mappedtree = MapTree(oldtree, fileid_map)
1469+ else:
1470+ mappedtree = oldtree
1471+
1472+ try:
1473+ old_base = nonghost_oldparents[0]
1474+ except IndexError:
1475+ old_base = NULL_REVISION
1476+ try:
1477+ new_base = new_parents[0]
1478+ except IndexError:
1479+ new_base = NULL_REVISION
1480+ old_base_tree = self.repository.revision_tree(old_base)
1481+ old_iter_changes = oldtree.iter_changes(old_base_tree)
1482+ iter_changes = wrap_iter_changes(old_iter_changes, mappedtree)
1483+ builder = self.repository.get_commit_builder(
1484+ branch=None, parents=new_parents, committer=oldrev.committer,
1485+ timestamp=oldrev.timestamp, timezone=oldrev.timezone,
1486+ revprops=revprops, revision_id=newrevid,
1487+ config_stack=_mod_config.GlobalStack())
1488+ try:
1489+ for (relpath, fs_hash) in builder.record_iter_changes(
1490+ mappedtree, new_base, iter_changes):
1491+ pass
1492+ builder.finish_inventory()
1493+ return builder.commit(oldrev.message)
1494+ except:
1495+ builder.abort()
1496+ raise
1497+
1498+
1499+class WorkingTreeRevisionRewriter(object):
1500+
1501+ def __init__(self, wt, state, merge_type=None):
1502+ """
1503+ :param wt: Working tree in which to do the replays.
1504+ """
1505+ self.wt = wt
1506+ self.graph = self.wt.branch.repository.get_graph()
1507+ self.state = state
1508+ self.merge_type = merge_type
1509+
1510+ def __call__(self, oldrevid, newrevid, newparents):
1511+ """Replay a commit in a working tree, with a different base.
1512+
1513+ :param oldrevid: Old revision id
1514+ :param newrevid: New revision id
1515+ :param newparents: New parent revision ids
1516+ """
1517+ repository = self.wt.branch.repository
1518+ if self.merge_type is None:
1519+ from ...merge import Merge3Merger
1520+ merge_type = Merge3Merger
1521+ else:
1522+ merge_type = self.merge_type
1523+ oldrev = self.wt.branch.repository.get_revision(oldrevid)
1524+ # Make sure there are no conflicts or pending merges/changes
1525+ # in the working tree
1526+ complete_revert(self.wt, [newparents[0]])
1527+ assert not self.wt.changes_from(self.wt.basis_tree()).has_changed(), "Changes in rev"
1528+
1529+ oldtree = repository.revision_tree(oldrevid)
1530+ self.state.write_active_revid(oldrevid)
1531+ merger = Merger(self.wt.branch, this_tree=self.wt)
1532+ merger.set_other_revision(oldrevid, self.wt.branch)
1533+ base_revid = self.determine_base(
1534+ oldrevid, oldrev.parent_ids, newrevid, newparents)
1535+ mutter('replaying %r as %r with base %r and new parents %r' %
1536+ (oldrevid, newrevid, base_revid, newparents))
1537+ merger.set_base_revision(base_revid, self.wt.branch)
1538+ merger.merge_type = merge_type
1539+ merger.do_merge()
1540+ for newparent in newparents[1:]:
1541+ self.wt.add_pending_merge(newparent)
1542+ self.commit_rebase(oldrev, newrevid)
1543+ self.state.write_active_revid(None)
1544+
1545+ def determine_base(self, oldrevid, oldparents, newrevid, newparents):
1546+ """Determine the base for replaying a revision using merge.
1547+
1548+ :param oldrevid: Revid of old revision.
1549+ :param oldparents: List of old parents revids.
1550+ :param newrevid: Revid of new revision.
1551+ :param newparents: List of new parents revids.
1552+ :return: Revision id of the new new revision.
1553+ """
1554+ # If this was the first commit, no base is needed
1555+ if len(oldparents) == 0:
1556+ return NULL_REVISION
1557+
1558+ # In the case of a "simple" revision with just one parent,
1559+ # that parent should be the base
1560+ if len(oldparents) == 1:
1561+ return oldparents[0]
1562+
1563+ # In case the rhs parent(s) of the origin revision has already been
1564+ # merged in the new branch, use diff between rhs parent and diff from
1565+ # original revision
1566+ if len(newparents) == 1:
1567+ # FIXME: Find oldparents entry that matches newparents[0]
1568+ # and return it
1569+ return oldparents[1]
1570+
1571+ try:
1572+ return self.graph.find_unique_lca(*[oldparents[0], newparents[1]])
1573+ except NoCommonAncestor:
1574+ return oldparents[0]
1575+
1576+ def commit_rebase(self, oldrev, newrevid):
1577+ """Commit a rebase.
1578+
1579+ :param oldrev: Revision info of new revision to commit.
1580+ :param newrevid: New revision id."""
1581+ assert oldrev.revision_id != newrevid, "Invalid revid %r" % newrevid
1582+ revprops = dict(oldrev.properties)
1583+ revprops[REVPROP_REBASE_OF] = oldrev.revision_id.decode('utf-8')
1584+ committer = self.wt.branch.get_config().username()
1585+ authors = oldrev.get_apparent_authors()
1586+ if oldrev.committer == committer:
1587+ # No need to explicitly record the authors if the original
1588+ # committer is rebasing.
1589+ if [oldrev.committer] == authors:
1590+ authors = None
1591+ else:
1592+ if oldrev.committer not in authors:
1593+ authors.append(oldrev.committer)
1594+ if 'author' in revprops:
1595+ del revprops['author']
1596+ if 'authors' in revprops:
1597+ del revprops['authors']
1598+ self.wt.commit(
1599+ message=oldrev.message, timestamp=oldrev.timestamp,
1600+ timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid,
1601+ committer=committer, authors=authors)
1602+
1603+
1604+def complete_revert(wt, newparents):
1605+ """Simple helper that reverts to specified new parents and makes sure none
1606+ of the extra files are left around.
1607+
1608+ :param wt: Working tree to use for rebase
1609+ :param newparents: New parents of the working tree
1610+ """
1611+ newtree = wt.branch.repository.revision_tree(newparents[0])
1612+ delta = wt.changes_from(newtree)
1613+ wt.branch.generate_revision_history(newparents[0])
1614+ wt.set_parent_ids([r for r in newparents[:1] if r != NULL_REVISION])
1615+ for change in delta.added:
1616+ abs_path = wt.abspath(change.path[1])
1617+ if osutils.lexists(abs_path):
1618+ if osutils.isdir(abs_path):
1619+ osutils.rmtree(abs_path)
1620+ else:
1621+ os.unlink(abs_path)
1622+ wt.revert(None, old_tree=newtree, backups=False)
1623+ assert not wt.changes_from(wt.basis_tree()).has_changed(), "Rev changed"
1624+ wt.set_parent_ids([r for r in newparents if r != NULL_REVISION])
1625+
1626+
1627+class ReplaySnapshotError(BzrError):
1628+ """Raised when replaying a snapshot failed."""
1629+ _fmt = """Replaying the snapshot failed: %(msg)s."""
1630+
1631+ def __init__(self, msg):
1632+ BzrError.__init__(self)
1633+ self.msg = msg
1634
1635=== added directory 'breezy/plugins/rewrite/tests'
1636=== added file 'breezy/plugins/rewrite/tests/__init__.py'
1637--- breezy/plugins/rewrite/tests/__init__.py 1970-01-01 00:00:00 +0000
1638+++ breezy/plugins/rewrite/tests/__init__.py 2020-01-11 23:46:53 +0000
1639@@ -0,0 +1,29 @@
1640+# Copyright (C) 2007-2010 by Jelmer Vernooij <jelmer@samba.org>
1641+#
1642+# This program is free software; you can redistribute it and/or modify
1643+# it under the terms of the GNU General Public License as published by
1644+# the Free Software Foundation; either version 2 of the License, or
1645+# (at your option) any later version.
1646+#
1647+# This program is distributed in the hope that it will be useful,
1648+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1649+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1650+# GNU General Public License for more details.
1651+#
1652+# You should have received a copy of the GNU General Public License
1653+# along with this program; if not, write to the Free Software
1654+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1655+
1656+"""Tests for brz-rewrite."""
1657+
1658+
1659+def load_tests(loader, basic_tests, pattern):
1660+ testmod_names = [
1661+ 'test_blackbox',
1662+ 'test_maptree',
1663+ 'test_pseudonyms',
1664+ 'test_rebase',
1665+ 'test_upgrade']
1666+ basic_tests.addTest(loader.loadTestsFromModuleNames(
1667+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
1668+ return basic_tests
1669
1670=== added file 'breezy/plugins/rewrite/tests/test_blackbox.py'
1671--- breezy/plugins/rewrite/tests/test_blackbox.py 1970-01-01 00:00:00 +0000
1672+++ breezy/plugins/rewrite/tests/test_blackbox.py 2020-01-11 23:46:53 +0000
1673@@ -0,0 +1,363 @@
1674+# Copyright (C) 2007 by Jelmer Vernooij <jelmer@samba.org>
1675+#
1676+# This program is free software; you can redistribute it and/or modify
1677+# it under the terms of the GNU General Public License as published by
1678+# the Free Software Foundation; either version 2 of the License, or
1679+# (at your option) any later version.
1680+#
1681+# This program is distributed in the hope that it will be useful,
1682+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1683+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1684+# GNU General Public License for more details.
1685+#
1686+# You should have received a copy of the GNU General Public License
1687+# along with this program; if not, write to the Free Software
1688+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1689+
1690+"""Couple of blackbox tests for the rewrite plugin."""
1691+
1692+import os
1693+
1694+from ....branch import Branch
1695+from ....tests.blackbox import ExternalBase
1696+
1697+class TestRebaseSimple(ExternalBase):
1698+
1699+ def make_file(self, name, contents):
1700+ with open(name, 'w' + ('b' if isinstance(contents, bytes) else '')) as f:
1701+ f.write(contents)
1702+
1703+ def setUp(self):
1704+ super(TestRebaseSimple, self).setUp()
1705+ os.mkdir('main')
1706+ os.chdir('main')
1707+ self.run_bzr('init')
1708+ self.make_file('hello', b"hi world")
1709+ self.run_bzr('add')
1710+ self.run_bzr('commit -m bla')
1711+ self.run_bzr('branch . ../feature')
1712+
1713+ def test_no_upstream_branch(self):
1714+ self.run_bzr_error(['brz: ERROR: No upstream branch specified.\n'],
1715+ 'rebase')
1716+
1717+ def test_notneeded(self):
1718+ os.chdir('../feature')
1719+ self.assertEquals(
1720+ 'No revisions to rebase.\n',
1721+ self.run_bzr('rebase ../main')[0])
1722+
1723+ def test_custom_merge_type(self):
1724+ self.make_file('hello', '42')
1725+ self.run_bzr('commit -m that')
1726+ os.chdir('../feature')
1727+ self.make_file('hoi', "my data")
1728+ self.run_bzr('add')
1729+ self.run_bzr('commit -m this')
1730+ self.assertEquals('', self.run_bzr('rebase --lca ../main')[0])
1731+ self.assertEquals('3\n', self.run_bzr('revno')[0])
1732+
1733+ def test_notneeded_feature_ahead(self):
1734+ os.chdir('../feature')
1735+ self.make_file('barbla', "bloe")
1736+ self.run_bzr('add')
1737+ self.run_bzr('commit -m bloe')
1738+ self.assertEquals(
1739+ 'No revisions to rebase.\n',
1740+ self.run_bzr('rebase ../main')[0])
1741+
1742+ def test_notneeded_main_ahead(self):
1743+ self.make_file('barbla', "bloe")
1744+ self.run_bzr('add')
1745+ self.run_bzr('commit -m bloe')
1746+ os.chdir('../feature')
1747+ self.assertEquals(
1748+ "Base branch is descendant of current branch. Pulling instead.\n",
1749+ self.run_bzr('rebase ../main')[0])
1750+ self.assertEquals(Branch.open("../feature").last_revision_info(),
1751+ Branch.open("../main").last_revision_info())
1752+
1753+ def test_no_pending_merges(self):
1754+ self.run_bzr_error(['brz: ERROR: No pending merges present.\n'],
1755+ ['rebase', '--pending-merges'])
1756+
1757+ def test_pending_merges(self):
1758+ os.chdir('..')
1759+ self.build_tree_contents([('main/hello', '42')])
1760+ self.run_bzr('add', working_dir='main')
1761+ self.run_bzr('commit -m that main')
1762+ self.build_tree_contents([('feature/hoi', 'my data')])
1763+ self.run_bzr('add', working_dir='feature')
1764+ self.run_bzr('commit -m this feature')
1765+ self.assertEqual(
1766+ ('', ' M hello\nAll changes applied successfully.\n'),
1767+ self.run_bzr('merge ../main', working_dir='feature'))
1768+ out, err = self.run_bzr('rebase --pending-merges', working_dir='feature')
1769+ self.assertEqual('', out)
1770+ self.assertContainsRe(err, 'modified hello')
1771+ self.assertEqual(
1772+ ('3\n', ''),
1773+ self.run_bzr('revno', working_dir='feature'))
1774+
1775+ def test_simple_success(self):
1776+ self.make_file('hello', '42')
1777+ self.run_bzr('commit -m that')
1778+ os.chdir('../feature')
1779+ self.make_file('hoi', "my data")
1780+ self.run_bzr('add')
1781+ self.run_bzr('commit -m this')
1782+ self.assertEquals('', self.run_bzr('rebase ../main')[0])
1783+ self.assertEquals('3\n', self.run_bzr('revno')[0])
1784+
1785+ def test_range(self):
1786+ # commit mainline rev 2
1787+ self.make_file('hello', '42')
1788+ self.run_bzr('commit -m that')
1789+ # commit feature rev 2
1790+ os.chdir('../feature')
1791+ self.make_file('hoi', "my data")
1792+ self.run_bzr('add')
1793+ self.run_bzr('commit -m this')
1794+ # commit feature rev 3
1795+ self.make_file('hooi', "your data")
1796+ self.run_bzr('add')
1797+ self.run_bzr('commit -m that')
1798+ # commit feature rev 4
1799+ self.make_file('hoooi', "someone else's data")
1800+ self.run_bzr('add')
1801+ self.run_bzr('commit -m these')
1802+ # pick up just rev 2 and 3 and discard 4 from feature
1803+ self.assertEquals('', self.run_bzr('rebase -r2..3 ../main')[0])
1804+ # our rev 2 is now rev3 and 3 is now rev4:
1805+ self.assertEquals('4\n', self.run_bzr('revno')[0])
1806+ # content added from our old revisions 4 should be gone.
1807+ self.assertPathDoesNotExist('hoooi')
1808+
1809+ def test_range_open_end(self):
1810+ # commit mainline rev 2
1811+ self.make_file('hello', '42')
1812+ self.run_bzr('commit -m that')
1813+ # commit feature rev 2
1814+ os.chdir('../feature')
1815+ self.make_file('hoi', "my data")
1816+ self.run_bzr('add')
1817+ self.run_bzr('commit -m this')
1818+ # commit feature rev 3
1819+ self.make_file('hooi', "your data")
1820+ self.run_bzr('add')
1821+ self.run_bzr('commit -m that')
1822+ # commit feature rev 4
1823+ self.make_file('hoooi', "someone else's data")
1824+ self.run_bzr('add')
1825+ self.run_bzr('commit -m these')
1826+ # rebase only rev 4 onto main
1827+ self.assertEquals('', self.run_bzr('rebase -r4.. ../main')[0])
1828+ # should only get rev 3 (our old 2 and 3 are gone)
1829+ self.assertEquals('3\n', self.run_bzr('revno')[0])
1830+ self.assertPathDoesNotExist('hoi')
1831+ self.assertPathDoesNotExist('hooi')
1832+ branch = Branch.open(".")
1833+ self.assertEquals(
1834+ "these",
1835+ branch.repository.get_revision(branch.last_revision()).message)
1836+ self.assertPathExists('hoooi')
1837+
1838+ def test_conflicting(self):
1839+ # commit mainline rev 2
1840+ self.make_file('hello', '42')
1841+ self.run_bzr('commit -m that')
1842+ # commit feature rev 2 changing hello differently
1843+ os.chdir('../feature')
1844+ self.make_file('hello', "other data")
1845+ self.run_bzr('commit -m this')
1846+ self.run_bzr_error([
1847+ 'Text conflict in hello\n1 conflicts encountered.\nbrz: ERROR: A conflict occurred replaying a commit. Resolve the conflict and run \'brz rebase-continue\' or run \'brz rebase-abort\'.',
1848+ ], ['rebase', '../main'])
1849+
1850+ def test_conflicting_abort(self):
1851+ self.make_file('hello', '42')
1852+ self.run_bzr('commit -m that')
1853+ os.chdir('../feature')
1854+ self.make_file('hello', "other data")
1855+ self.run_bzr('commit -m this')
1856+ old_log = self.run_bzr('log')[0]
1857+ self.run_bzr_error(['Text conflict in hello\n1 conflicts encountered.\nbrz: ERROR: A conflict occurred replaying a commit. Resolve the conflict and run \'brz rebase-continue\' or run \'brz rebase-abort\'.\n'], ['rebase', '../main'])
1858+ self.assertEquals('', self.run_bzr('rebase-abort')[0])
1859+ self.assertEquals(old_log, self.run_bzr('log')[0])
1860+
1861+ def test_conflicting_continue(self):
1862+ self.make_file('hello', '42')
1863+ self.run_bzr('commit -m that')
1864+ os.chdir('../feature')
1865+ self.make_file('hello', "other data")
1866+ self.run_bzr('commit -m this')
1867+ self.run_bzr_error(['Text conflict in hello\n1 conflicts encountered.\nbrz: ERROR: A conflict occurred replaying a commit. Resolve the conflict and run \'brz rebase-continue\' or run \'brz rebase-abort\'.\n'], ['rebase', '../main'])
1868+ self.run_bzr('resolved hello')
1869+ self.assertEquals('', self.run_bzr('rebase-continue')[0])
1870+ self.assertEquals('3\n', self.run_bzr('revno')[0])
1871+
1872+ def test_continue_nothing(self):
1873+ self.run_bzr_error(['brz: ERROR: No rebase to continue'],
1874+ ['rebase-continue'])
1875+
1876+ def test_abort_nothing(self):
1877+ self.run_bzr_error(['brz: ERROR: No rebase to abort'],
1878+ ['rebase-abort'])
1879+
1880+ def test_todo_nothing(self):
1881+ self.run_bzr_error(['brz: ERROR: No rebase in progress'],
1882+ ['rebase-todo'])
1883+
1884+ def test_onto(self):
1885+ self.make_file('hello', '42')
1886+ self.run_bzr('add')
1887+ self.run_bzr('commit -m that')
1888+ self.make_file('other', '43')
1889+ self.run_bzr('add')
1890+ self.run_bzr('commit -m that_other')
1891+ os.chdir('../feature')
1892+ self.make_file('hoi', "my data")
1893+ self.run_bzr('add')
1894+ self.run_bzr('commit -m this')
1895+ self.assertEquals(
1896+ '', self.run_bzr('rebase --onto -2 ../main')[0])
1897+ self.assertEquals(
1898+ '3\n', self.run_bzr('revno')[0])
1899+
1900+ def test_unrelated(self):
1901+ os.chdir('..')
1902+ os.mkdir('unrelated')
1903+ os.chdir('unrelated')
1904+ self.run_bzr('init')
1905+ self.make_file('hello', "hi world")
1906+ self.run_bzr('add')
1907+ self.run_bzr('commit -m x')
1908+ self.run_bzr_error(['brz: ERROR: Branches have no common ancestor, and no merge base.*'],
1909+ ['rebase', '../main'])
1910+
1911+ def test_verbose(self):
1912+ self.make_file('hello', '42')
1913+ self.run_bzr('commit -m that')
1914+ os.chdir('../feature')
1915+ self.make_file('hoi', "my data")
1916+ self.run_bzr('add')
1917+ self.run_bzr('commit -m this')
1918+ out, err = self.run_bzr('rebase -v ../main')
1919+ self.assertContainsRe(err, '1 revisions will be rebased:')
1920+ self.assertEqual('', out)
1921+ self.assertEqual('3\n', self.run_bzr('revno')[0])
1922+
1923+ def test_useless_merge(self):
1924+ self.make_file('bar', '42')
1925+ self.run_bzr('add')
1926+ self.run_bzr('commit -m that')
1927+ os.chdir('../feature')
1928+ self.make_file('hello', "my data")
1929+ self.run_bzr('commit -m this')
1930+ self.run_bzr('merge')
1931+ self.run_bzr('commit -m merge')
1932+ self.run_bzr('rebase')
1933+
1934+ def strip_last_revid_part(self, revid):
1935+ """Assume revid is a revid in the default form, and strip the part
1936+ which would be random.
1937+ """
1938+ return revid[:revid.rindex(b'-')]
1939+
1940+ def test_always_rebase_merges(self):
1941+ trunk = self.make_branch_and_tree('trunk')
1942+ trunk.commit('base')
1943+ feature2 = trunk.controldir.sprout('feature2').open_workingtree()
1944+ revid2 = feature2.commit('change')
1945+ feature = trunk.controldir.sprout('feature').open_workingtree()
1946+ feature.commit('change')
1947+ feature.merge_from_branch(feature2.branch)
1948+ feature.commit('merge')
1949+ feature.commit('change2')
1950+ trunk.commit('additional upstream change')
1951+ self.run_bzr('rebase --always-rebase-merges ../trunk', working_dir='feature')
1952+ # second revision back should be a merge of feature2
1953+ repo = feature.branch.repository
1954+ repo.lock_read()
1955+ self.addCleanup(repo.unlock)
1956+ tip = feature.last_revision()
1957+ merge_id = repo.get_graph().get_parent_map([tip])[tip][0]
1958+ merge_parents = repo.get_graph().get_parent_map([merge_id])[merge_id]
1959+ self.assertEqual(self.strip_last_revid_part(revid2),
1960+ self.strip_last_revid_part(merge_parents[1]))
1961+
1962+ def test_rebase_merge(self):
1963+ trunk = self.make_branch_and_tree('trunk')
1964+ trunk.commit('base')
1965+ feature2 = trunk.controldir.sprout('feature2').open_workingtree()
1966+ revid2 = feature2.commit('change')
1967+ feature = trunk.controldir.sprout('feature').open_workingtree()
1968+ feature.commit('change')
1969+ feature.merge_from_branch(feature2.branch)
1970+ feature.commit('merge')
1971+ feature.commit('change2')
1972+ trunk.commit('additional upstream change')
1973+ self.run_bzr('rebase ../trunk', working_dir='feature')
1974+ # second revision back should be a merge of feature2
1975+ repo = feature.branch.repository
1976+ repo.lock_read()
1977+ self.addCleanup(repo.unlock)
1978+ tip = feature.last_revision()
1979+ merge_id = repo.get_graph().get_parent_map([tip])[tip][0]
1980+ merge_parents = repo.get_graph().get_parent_map([merge_id])[merge_id]
1981+ self.assertEqual(self.strip_last_revid_part(revid2),
1982+ self.strip_last_revid_part(merge_parents[1]))
1983+
1984+ def test_directory(self):
1985+ self.make_file('test_directory1', "testing non-current directories")
1986+ self.run_bzr('add')
1987+ self.run_bzr('commit -m blah')
1988+ os.chdir('../feature')
1989+ self.make_file('test_directory2', "testing non-current directories")
1990+ self.run_bzr('add')
1991+ self.run_bzr('commit -m blah')
1992+ os.chdir('..')
1993+ self.assertEquals('', self.run_bzr('rebase -d feature main')[0])
1994+
1995+
1996+class ReplayTests(ExternalBase):
1997+
1998+ def test_replay(self):
1999+ os.mkdir('main')
2000+ os.chdir('main')
2001+ self.run_bzr('init')
2002+ open('bar', 'w').write('42')
2003+ self.run_bzr('add')
2004+ self.run_bzr('commit -m that')
2005+ os.mkdir('../feature')
2006+ os.chdir('../feature')
2007+ self.run_bzr('init')
2008+ branch = Branch.open('.')
2009+ open('hello', 'w').write("my data")
2010+ self.run_bzr('add')
2011+ self.run_bzr('commit -m this')
2012+ self.assertEquals(1, branch.revno())
2013+ self.run_bzr('replay -r1 ../main')
2014+ self.assertEquals(2, branch.revno())
2015+ self.assertTrue(os.path.exists('bar'))
2016+
2017+ def test_replay_open_range(self):
2018+ os.mkdir('main')
2019+ os.chdir('main')
2020+ self.run_bzr('init')
2021+ open('bar', 'w').write('42')
2022+ self.run_bzr('add')
2023+ self.run_bzr('commit -m that')
2024+ open('bar', 'w').write('84')
2025+ self.run_bzr('commit -m blathat')
2026+ os.mkdir('../feature')
2027+ os.chdir('../feature')
2028+ self.run_bzr('init')
2029+ branch = Branch.open('.')
2030+ open('hello', 'w').write("my data")
2031+ self.run_bzr('add')
2032+ self.run_bzr('commit -m this')
2033+ self.assertEquals(1, branch.revno())
2034+ self.run_bzr('replay -r1.. ../main')
2035+ self.assertEquals(3, branch.revno())
2036+ self.assertTrue(os.path.exists('bar'))
2037
2038=== added file 'breezy/plugins/rewrite/tests/test_maptree.py'
2039--- breezy/plugins/rewrite/tests/test_maptree.py 1970-01-01 00:00:00 +0000
2040+++ breezy/plugins/rewrite/tests/test_maptree.py 2020-01-11 23:46:53 +0000
2041@@ -0,0 +1,81 @@
2042+# Copyright (C) 2006-2007 by Jelmer Vernooij
2043+#
2044+# This program is free software; you can redistribute it and/or modify
2045+# it under the terms of the GNU General Public License as published by
2046+# the Free Software Foundation; either version 2 of the License, or
2047+# (at your option) any later version.
2048+#
2049+# This program is distributed in the hope that it will be useful,
2050+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2051+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2052+# GNU General Public License for more details.
2053+#
2054+# You should have received a copy of the GNU General Public License
2055+# along with this program; if not, write to the Free Software
2056+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2057+"""Tests for the maptree code."""
2058+
2059+from ....tests import (
2060+ TestCase,
2061+ TestCaseWithTransport,
2062+ )
2063+from ....treebuilder import (
2064+ TreeBuilder,
2065+ )
2066+
2067+from ..maptree import (
2068+ MapTree,
2069+ map_file_ids,
2070+ )
2071+
2072+
2073+class EmptyMapTreeTests(TestCaseWithTransport):
2074+
2075+ def setUp(self):
2076+ super(EmptyMapTreeTests, self).setUp()
2077+ tree = self.make_branch_and_tree('branch')
2078+ self.oldtree = tree
2079+
2080+ def test_has_filename(self):
2081+ self.oldtree.lock_write()
2082+ builder = TreeBuilder()
2083+ builder.start_tree(self.oldtree)
2084+ builder.build(['foo'])
2085+ builder.finish_tree()
2086+ self.maptree = MapTree(self.oldtree, {})
2087+ self.oldtree.unlock()
2088+ self.assertTrue(self.maptree.has_filename('foo'))
2089+ self.assertTrue(self.oldtree.has_filename('foo'))
2090+ self.assertFalse(self.maptree.has_filename('bar'))
2091+
2092+ def test_path2id(self):
2093+ self.oldtree.lock_write()
2094+ self.addCleanup(self.oldtree.unlock)
2095+ builder = TreeBuilder()
2096+ builder.start_tree(self.oldtree)
2097+ builder.build(['foo'])
2098+ builder.build(['bar'])
2099+ builder.build(['bla'])
2100+ builder.finish_tree()
2101+ self.maptree = MapTree(self.oldtree, {})
2102+ self.assertEquals(self.oldtree.path2id("foo"),
2103+ self.maptree.path2id("foo"))
2104+
2105+ def test_id2path(self):
2106+ self.oldtree.lock_write()
2107+ self.addCleanup(self.oldtree.unlock)
2108+ builder = TreeBuilder()
2109+ builder.start_tree(self.oldtree)
2110+ builder.build(['foo'])
2111+ builder.build(['bar'])
2112+ builder.build(['bla'])
2113+ builder.finish_tree()
2114+ self.maptree = MapTree(self.oldtree, {})
2115+ self.assertEquals(
2116+ "foo", self.maptree.id2path(self.maptree.path2id("foo")))
2117+
2118+
2119+class MapFileIdTests(TestCase):
2120+
2121+ def test_empty(self):
2122+ self.assertEquals({}, map_file_ids(None, [], []))
2123
2124=== added file 'breezy/plugins/rewrite/tests/test_pseudonyms.py'
2125--- breezy/plugins/rewrite/tests/test_pseudonyms.py 1970-01-01 00:00:00 +0000
2126+++ breezy/plugins/rewrite/tests/test_pseudonyms.py 2020-01-11 23:46:53 +0000
2127@@ -0,0 +1,39 @@
2128+# Copyright (C) 2010 by Jelmer Vernooij <jelmer@samba.org>
2129+#
2130+# This program is free software; you can redistribute it and/or modify
2131+# it under the terms of the GNU General Public License as published by
2132+# the Free Software Foundation; either version 2 of the License, or
2133+# (at your option) any later version.
2134+#
2135+# This program is distributed in the hope that it will be useful,
2136+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2137+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2138+# GNU General Public License for more details.
2139+#
2140+# You should have received a copy of the GNU General Public License
2141+# along with this program; if not, write to the Free Software
2142+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2143+
2144+"""Tests for pseudonym handling."""
2145+
2146+from ....revision import Revision
2147+from ....tests import TestCase
2148+
2149+from ..pseudonyms import extract_foreign_revids
2150+
2151+
2152+class ExtractForeignRevidTests(TestCase):
2153+
2154+ def test_no_foreign_revid(self):
2155+ x = Revision(b"myrevid")
2156+ self.assertEquals(set(), extract_foreign_revids(x))
2157+
2158+ def test_cscvs(self):
2159+ x = Revision(b"myrevid")
2160+ x.properties = {
2161+ "cscvs-svn-repository-uuid": "someuuid",
2162+ "cscvs-svn-revision-number": "4",
2163+ "cscvs-svn-branch-path": "/trunk"}
2164+ self.assertEquals(
2165+ set([("svn", "someuuid:4:trunk")]),
2166+ extract_foreign_revids(x))
2167
2168=== added file 'breezy/plugins/rewrite/tests/test_rebase.py'
2169--- breezy/plugins/rewrite/tests/test_rebase.py 1970-01-01 00:00:00 +0000
2170+++ breezy/plugins/rewrite/tests/test_rebase.py 2020-01-11 23:46:53 +0000
2171@@ -0,0 +1,669 @@
2172+# Copyright (C) 2006-2007 by Jelmer Vernooij
2173+#
2174+# This program is free software; you can redistribute it and/or modify
2175+# it under the terms of the GNU General Public License as published by
2176+# the Free Software Foundation; either version 2 of the License, or
2177+# (at your option) any later version.
2178+#
2179+# This program is distributed in the hope that it will be useful,
2180+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2181+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2182+# GNU General Public License for more details.
2183+#
2184+# You should have received a copy of the GNU General Public License
2185+# along with this program; if not, write to the Free Software
2186+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2187+
2188+"""Tests for the rebase code."""
2189+
2190+from ....conflicts import ConflictList
2191+from ....errors import (
2192+ UnknownFormatError,
2193+ NoSuchFile,
2194+ ConflictsInTree,
2195+ )
2196+from ....graph import (
2197+ Graph,
2198+ DictParentsProvider,
2199+ )
2200+from ....revision import NULL_REVISION
2201+from ....tests import TestCase, TestCaseWithTransport
2202+from ....tests.matchers import RevisionHistoryMatches
2203+
2204+from ..rebase import (
2205+ marshall_rebase_plan,
2206+ unmarshall_rebase_plan,
2207+ CommitBuilderRevisionRewriter,
2208+ generate_simple_plan,
2209+ generate_transpose_plan,
2210+ rebase_todo,
2211+ REBASE_PLAN_FILENAME,
2212+ REBASE_CURRENT_REVID_FILENAME,
2213+ RebaseState1,
2214+ ReplaySnapshotError,
2215+ WorkingTreeRevisionRewriter,
2216+ )
2217+
2218+
2219+class RebasePlanReadWriterTests(TestCase):
2220+
2221+ def test_simple_marshall_rebase_plan(self):
2222+ self.assertEqualDiff(b"""\
2223+# Bazaar rebase plan 1
2224+1 bla
2225+oldrev newrev newparent1 newparent2
2226+""", marshall_rebase_plan((1, b"bla"),
2227+ {b"oldrev": (b"newrev", (b"newparent1", b"newparent2"))}))
2228+
2229+ def test_simple_unmarshall_rebase_plan(self):
2230+ self.assertEquals(((1, b"bla"),
2231+ {b"oldrev": (b"newrev", (b"newparent1", b"newparent2"))}),
2232+ unmarshall_rebase_plan(b"""# Bazaar rebase plan 1
2233+1 bla
2234+oldrev newrev newparent1 newparent2
2235+"""))
2236+
2237+ def test_unmarshall_rebase_plan_formatunknown(self):
2238+ self.assertRaises(UnknownFormatError,
2239+ unmarshall_rebase_plan, b"""# Bazaar rebase plan x
2240+1 bla
2241+oldrev newrev newparent1 newparent2
2242+""")
2243+
2244+
2245+class ConversionTests(TestCaseWithTransport):
2246+
2247+ def test_simple(self):
2248+ wt = self.make_branch_and_tree('.')
2249+ b = wt.branch
2250+ with open('hello', 'w') as f:
2251+ f.write('hello world')
2252+ wt.add('hello')
2253+ wt.commit(message='add hello', rev_id=b"bla")
2254+ with open('hello', 'w') as f:
2255+ f.write('world')
2256+ wt.commit(message='change hello', rev_id=b"bloe")
2257+ wt.set_last_revision(b"bla")
2258+ b.generate_revision_history(b"bla")
2259+ with open('hello', 'w') as f:
2260+ f.write('world')
2261+ wt.commit(message='change hello', rev_id=b"bla2")
2262+
2263+ wt.branch.repository.lock_write()
2264+ newrev = CommitBuilderRevisionRewriter(wt.branch.repository)(
2265+ b"bla2", b"bla4", (b"bloe",))
2266+ self.assertEqual(b"bla4", newrev)
2267+ self.assertTrue(wt.branch.repository.has_revision(newrev))
2268+ self.assertEqual(
2269+ (b"bloe",), wt.branch.repository.get_parent_map([newrev])[newrev])
2270+ self.assertEqual(
2271+ "bla2", wt.branch.repository.get_revision(newrev).properties["rebase-of"])
2272+ wt.branch.repository.unlock()
2273+
2274+
2275+class PlanCreatorTests(TestCaseWithTransport):
2276+
2277+ def test_simple_plan_creator(self):
2278+ wt = self.make_branch_and_tree('.')
2279+ b = wt.branch
2280+ with open('hello', 'w') as f:
2281+ f.write('hello world')
2282+ wt.add('hello')
2283+ wt.commit(message='add hello', rev_id=b"bla")
2284+ with open('hello', 'w') as f:
2285+ f.write('world')
2286+ wt.commit(message='change hello', rev_id=b"bloe")
2287+ wt.set_last_revision(b"bla")
2288+ b.generate_revision_history(b"bla")
2289+ with open('hello', 'w') as f:
2290+ f.write('world')
2291+ wt.commit(message='change hello', rev_id=b"bla2")
2292+
2293+ b.repository.lock_read()
2294+ graph = b.repository.get_graph()
2295+ self.assertEquals(
2296+ {b'bla2': (b'newbla2', (b"bloe",))},
2297+ generate_simple_plan(
2298+ graph.find_difference(b.last_revision(), b"bla")[0],
2299+ b"bla2", None, b"bloe", graph, lambda y, _: b"new" + y))
2300+ b.repository.unlock()
2301+
2302+ def test_simple_plan_creator_extra_history(self):
2303+ wt = self.make_branch_and_tree('.')
2304+ b = wt.branch
2305+ with open('hello', 'w') as f:
2306+ f.write('hello world')
2307+ wt.add('hello')
2308+ wt.commit(message='add hello', rev_id=b"bla")
2309+ with open('hello', 'w') as f:
2310+ f.write('world')
2311+ wt.commit(message='change hello', rev_id=b"bloe")
2312+ wt.set_last_revision(b"bla")
2313+ b.generate_revision_history(b"bla")
2314+ with open('hello', 'w') as f:
2315+ f.write('world')
2316+ wt.commit(message='change hello', rev_id=b"bla2")
2317+ with open('hello', 'w') as f:
2318+ f.write('universe')
2319+ wt.commit(message='change hello again', rev_id=b"bla3")
2320+
2321+ with b.repository.lock_read():
2322+ graph = b.repository.get_graph()
2323+ self.assertEquals(
2324+ {b'bla2': (b'newbla2', (b"bloe",)), b'bla3': (b'newbla3', (b'newbla2',))},
2325+ generate_simple_plan(
2326+ graph.find_difference(b.last_revision(), b"bloe")[0],
2327+ b"bla2", None, b"bloe",
2328+ graph, lambda y, _: b"new" + y))
2329+
2330+ def test_generate_transpose_plan(self):
2331+ wt = self.make_branch_and_tree('.')
2332+ b = wt.branch
2333+ with open('hello', 'w') as f:
2334+ f.write('hello world')
2335+ wt.add('hello')
2336+ wt.commit(message='add hello', rev_id=b"bla")
2337+ with open('hello', 'w') as f:
2338+ f.write('world')
2339+ wt.commit(message='change hello', rev_id=b"bloe")
2340+ wt.set_last_revision(b"bla")
2341+ b.generate_revision_history(b"bla")
2342+ with open('hello', 'w') as f:
2343+ f.write('world')
2344+ wt.commit(message='change hello', rev_id=b"bla2")
2345+ with open('hello', 'w') as f:
2346+ f.write('universe')
2347+ wt.commit(message='change hello again', rev_id=b"bla3")
2348+ wt.set_last_revision(b"bla")
2349+ b.generate_revision_history(b"bla")
2350+ with open('hello', 'w') as f:
2351+ f.write('somebar')
2352+ wt.commit(message='change hello yet again', rev_id=b"blie")
2353+ wt.set_last_revision(NULL_REVISION)
2354+ b.generate_revision_history(NULL_REVISION)
2355+ wt.add('hello')
2356+ wt.commit(message='add hello', rev_id=b"lala")
2357+
2358+ b.repository.lock_read()
2359+ graph = b.repository.get_graph()
2360+ self.assertEquals({
2361+ b'blie': (b'newblie', (b'lala',))},
2362+ generate_transpose_plan(graph.iter_ancestry(
2363+ [b"blie"]),
2364+ {b"bla": b"lala"}, graph, lambda y, _: b"new" + y))
2365+ self.assertEquals({
2366+ b'bla2': (b'newbla2', (b'lala',)),
2367+ b'bla3': (b'newbla3', (b'newbla2',)),
2368+ b'blie': (b'newblie', (b'lala',)),
2369+ b'bloe': (b'newbloe', (b'lala',))},
2370+ generate_transpose_plan(graph.iter_ancestry(
2371+ b.repository._all_revision_ids()),
2372+ {b"bla": b"lala"}, graph, lambda y, _: b"new" + y))
2373+ b.repository.unlock()
2374+
2375+ def test_generate_transpose_plan_one(self):
2376+ graph = Graph(DictParentsProvider({"bla": ("bloe",), "bloe": (), "lala": ()}))
2377+ self.assertEquals(
2378+ {"bla": ("newbla", ("lala",))},
2379+ generate_transpose_plan(graph.iter_ancestry(
2380+ ["bla", "bloe"]), {"bloe": "lala"}, graph, lambda y, _: "new" + y))
2381+
2382+ def test_plan_with_already_merged(self):
2383+ """We need to use a merge base that makes sense.
2384+
2385+ A
2386+ | \
2387+ B D
2388+ | \|
2389+ C E
2390+
2391+ Rebasing E on C should result in:
2392+
2393+ A -> B -> C -> D -> E
2394+
2395+ with a plan of:
2396+
2397+ D -> (D', [C])
2398+ E -> (E', [D', C])
2399+ """
2400+ parents_map = {
2401+ "A": (),
2402+ "B": ("A",),
2403+ "C": ("B",),
2404+ "D": ("A",),
2405+ "E": ("D", "B")
2406+ }
2407+ graph = Graph(DictParentsProvider(parents_map))
2408+ self.assertEquals(
2409+ {"D": ("D'", ("C",)), "E": ("E'", ("D'",))},
2410+ generate_simple_plan(
2411+ ["D", "E"], "D", None, "C", graph, lambda y, _: y + "'"))
2412+
2413+ def test_plan_with_already_merged_skip_merges(self):
2414+ """We need to use a merge base that makes sense.
2415+
2416+ A
2417+ | \
2418+ B D
2419+ | \|
2420+ C E
2421+
2422+ Rebasing E on C should result in:
2423+
2424+ A -> B -> C -> D'
2425+
2426+ with a plan of:
2427+
2428+ D -> (D', [C])
2429+ """
2430+ parents_map = {
2431+ "A": (),
2432+ "B": ("A",),
2433+ "C": ("B",),
2434+ "D": ("A",),
2435+ "E": ("D", "B")
2436+ }
2437+ graph = Graph(DictParentsProvider(parents_map))
2438+ self.assertEquals(
2439+ {"D": ("D'", ("C",))},
2440+ generate_simple_plan(
2441+ ["D", "E"], "D", None, "C", graph, lambda y, _: y + "'", True))
2442+
2443+
2444+class RebaseStateTests(TestCaseWithTransport):
2445+
2446+ def setUp(self):
2447+ super(RebaseStateTests, self).setUp()
2448+ self.wt = self.make_branch_and_tree('.')
2449+ self.state = RebaseState1(self.wt)
2450+
2451+ def test_rebase_plan_exists_false(self):
2452+ self.assertFalse(self.state.has_plan())
2453+
2454+ def test_rebase_plan_exists_empty(self):
2455+ self.wt._transport.put_bytes(REBASE_PLAN_FILENAME, b"")
2456+ self.assertFalse(self.state.has_plan())
2457+
2458+ def test_rebase_plan_exists(self):
2459+ self.wt._transport.put_bytes(REBASE_PLAN_FILENAME, b"foo")
2460+ self.assertTrue(self.state.has_plan())
2461+
2462+ def test_remove_rebase_plan(self):
2463+ self.wt._transport.put_bytes(REBASE_PLAN_FILENAME, b"foo")
2464+ self.state.remove_plan()
2465+ self.assertFalse(self.state.has_plan())
2466+
2467+ def test_remove_rebase_plan_twice(self):
2468+ self.state.remove_plan()
2469+ self.assertFalse(self.state.has_plan())
2470+
2471+ def test_write_rebase_plan(self):
2472+ with open('hello', 'w') as f:
2473+ f.write('hello world')
2474+ self.wt.add('hello')
2475+ self.wt.commit(message='add hello', rev_id=b"bla")
2476+ self.state.write_plan(
2477+ {b"oldrev": (b"newrev", [b"newparent1", b"newparent2"])})
2478+ self.assertEqualDiff(b"""# Bazaar rebase plan 1
2479+1 bla
2480+oldrev newrev newparent1 newparent2
2481+""", self.wt._transport.get_bytes(REBASE_PLAN_FILENAME))
2482+
2483+ def test_read_rebase_plan_nonexistant(self):
2484+ self.assertRaises(NoSuchFile, self.state.read_plan)
2485+
2486+ def test_read_rebase_plan_empty(self):
2487+ self.wt._transport.put_bytes(REBASE_PLAN_FILENAME, b"")
2488+ self.assertRaises(NoSuchFile, self.state.read_plan)
2489+
2490+ def test_read_rebase_plan(self):
2491+ self.wt._transport.put_bytes(
2492+ REBASE_PLAN_FILENAME,
2493+ b"""# Bazaar rebase plan 1
2494+1 bla
2495+oldrev newrev newparent1 newparent2
2496+""")
2497+ self.assertEquals(
2498+ ((1, b"bla"), {b"oldrev": (b"newrev", (b"newparent1", b"newparent2"))}),
2499+ self.state.read_plan())
2500+
2501+ def test_read_nonexistant(self):
2502+ self.assertIs(None, self.state.read_active_revid())
2503+
2504+ def test_read_null(self):
2505+ self.wt._transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, NULL_REVISION)
2506+ self.assertIs(None, self.state.read_active_revid())
2507+
2508+ def test_read(self):
2509+ self.wt._transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, b"bla")
2510+ self.assertEquals(b"bla", self.state.read_active_revid())
2511+
2512+ def test_write(self):
2513+ self.state.write_active_revid(b"bloe")
2514+ self.assertEquals(b"bloe", self.state.read_active_revid())
2515+
2516+ def test_write_null(self):
2517+ self.state.write_active_revid(None)
2518+ self.assertIs(None, self.state.read_active_revid())
2519+
2520+
2521+class RebaseTodoTests(TestCase):
2522+
2523+ def test_done(self):
2524+ class Repository:
2525+ def has_revision(self, revid):
2526+ return revid == "bloe"
2527+ self.assertEquals(
2528+ [], list(rebase_todo(Repository(), {"bla": ("bloe", [])})))
2529+
2530+ def test_notstarted(self):
2531+ class Repository:
2532+ def has_revision(self, revid):
2533+ return False
2534+ self.assertEquals(["bla"], list(rebase_todo(Repository(), {"bla": ("bloe", [])})))
2535+
2536+ def test_halfway(self):
2537+ class Repository:
2538+ def has_revision(self, revid):
2539+ return revid == "bloe"
2540+ self.assertEquals(
2541+ ["ha"],
2542+ list(rebase_todo(Repository(), {"bla": ("bloe", []), "ha": ("hee", [])})))
2543+
2544+
2545+class ReplaySnapshotTests(TestCaseWithTransport):
2546+
2547+ def test_single_revision(self):
2548+ wt = self.make_branch_and_tree(".")
2549+ self.build_tree(['afile'])
2550+ wt.add(["afile"])
2551+ wt.commit("bla", rev_id=b"oldcommit")
2552+ wt.branch.repository.lock_write()
2553+ CommitBuilderRevisionRewriter(wt.branch.repository)(b"oldcommit", b"newcommit", ())
2554+ wt.branch.repository.unlock()
2555+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2556+ newrev = wt.branch.repository.get_revision(b"newcommit")
2557+ self.assertEquals([], newrev.parent_ids)
2558+ self.assertEquals(b"newcommit", newrev.revision_id)
2559+ self.assertEquals(oldrev.committer, newrev.committer)
2560+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2561+ self.assertEquals(oldrev.timezone, newrev.timezone)
2562+ tree = wt.branch.repository.revision_tree(b"newcommit")
2563+ self.assertEquals(b"newcommit", tree.get_file_revision("afile"))
2564+
2565+ def test_two_revisions(self):
2566+ wt = self.make_branch_and_tree("old")
2567+ self.build_tree_contents([('old/afile', 'afilecontents'), ('old/notherfile', 'notherfilecontents')])
2568+ wt.add(["afile"], [b"somefileid"])
2569+ wt.commit("bla", rev_id=b"oldparent")
2570+ wt.add(["notherfile"])
2571+ wt.commit("bla", rev_id=b"oldcommit")
2572+ oldrepos = wt.branch.repository
2573+ wt = self.make_branch_and_tree("new")
2574+ self.build_tree_contents([('new/afile', 'afilecontents'), ('new/notherfile', 'notherfilecontents')])
2575+ wt.add(["afile"], [b"afileid"])
2576+ wt.commit("bla", rev_id=b"newparent")
2577+ wt.branch.repository.fetch(oldrepos)
2578+ wt.branch.repository.lock_write()
2579+ CommitBuilderRevisionRewriter(wt.branch.repository)(
2580+ b"oldcommit", b"newcommit", (b"newparent",))
2581+ wt.branch.repository.unlock()
2582+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2583+ newrev = wt.branch.repository.get_revision(b"newcommit")
2584+ self.assertEquals([b"newparent"], newrev.parent_ids)
2585+ self.assertEquals(b"newcommit", newrev.revision_id)
2586+ self.assertEquals(oldrev.committer, newrev.committer)
2587+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2588+ self.assertEquals(oldrev.timezone, newrev.timezone)
2589+ tree = wt.branch.repository.revision_tree(b"newcommit")
2590+ self.assertEquals(b"afileid", tree.path2id("afile"))
2591+ self.assertEquals(b"newcommit", tree.get_file_revision("notherfile"))
2592+
2593+ def test_two_revisions_no_renames(self):
2594+ wt = self.make_branch_and_tree("old")
2595+ self.build_tree(['old/afile', 'old/notherfile'])
2596+ wt.add(["afile"], [b"somefileid"])
2597+ wt.commit("bla", rev_id=b"oldparent")
2598+ wt.add(["notherfile"])
2599+ wt.commit("bla", rev_id=b"oldcommit")
2600+ oldrepos = wt.branch.repository
2601+ wt = self.make_branch_and_tree("new")
2602+ self.build_tree(['new/afile', 'new/notherfile'])
2603+ wt.add(["afile"], [b"afileid"])
2604+ wt.commit("bla", rev_id=b"newparent")
2605+ wt.branch.repository.fetch(oldrepos)
2606+ wt.branch.repository.lock_write()
2607+ CommitBuilderRevisionRewriter(wt.branch.repository)(
2608+ b"oldcommit", b"newcommit", (b"newparent",))
2609+ wt.branch.repository.unlock()
2610+
2611+ def test_multi_revisions(self):
2612+ wt = self.make_branch_and_tree("old")
2613+ self.build_tree_contents(
2614+ [('old/afile', 'afilecontent'), ('old/sfile', 'sfilecontent'), ('old/notherfile', 'notherfilecontent')])
2615+ wt.add(['sfile'])
2616+ wt.add(["afile"], [b"somefileid"])
2617+ wt.commit("bla", rev_id=b"oldgrandparent")
2618+ open("old/afile", "w").write("data")
2619+ wt.commit("bla", rev_id=b"oldparent")
2620+ wt.add(["notherfile"])
2621+ wt.commit("bla", rev_id=b"oldcommit")
2622+ oldrepos = wt.branch.repository
2623+ wt = self.make_branch_and_tree("new")
2624+ self.build_tree_contents(
2625+ [('new/afile', 'afilecontent'), ('new/sfile', 'sfilecontent'), ('new/notherfile', 'notherfilecontent')])
2626+ wt.add(['sfile'])
2627+ wt.add(["afile"], [b"afileid"])
2628+ wt.commit("bla", rev_id=b"newgrandparent")
2629+ open("new/afile", "w").write("data")
2630+ wt.commit("bla", rev_id=b"newparent")
2631+ wt.branch.repository.fetch(oldrepos)
2632+ wt.branch.repository.lock_write()
2633+ CommitBuilderRevisionRewriter(wt.branch.repository)(
2634+ b"oldcommit", b"newcommit", (b"newparent",))
2635+ wt.branch.repository.unlock()
2636+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2637+ newrev = wt.branch.repository.get_revision(b"newcommit")
2638+ self.assertEquals([b"newparent"], newrev.parent_ids)
2639+ self.assertEquals(b"newcommit", newrev.revision_id)
2640+ self.assertEquals(oldrev.committer, newrev.committer)
2641+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2642+ self.assertEquals(oldrev.timezone, newrev.timezone)
2643+ tree = wt.branch.repository.revision_tree(b"newcommit")
2644+ self.assertEquals(b"afileid", tree.path2id("afile"))
2645+ self.assertEquals(b"newcommit", tree.get_file_revision("notherfile"))
2646+ self.assertEquals(b"newgrandparent", tree.get_file_revision("sfile"))
2647+
2648+ def test_maps_ids(self):
2649+ wt = self.make_branch_and_tree("old")
2650+ wt.commit("base", rev_id=b"base")
2651+ self.build_tree(['old/afile'])
2652+ wt.add(["afile"], ids=[b"originalid"])
2653+ wt.commit("bla", rev_id=b"oldparent")
2654+ with open("old/afile", "w") as f:
2655+ f.write("bloe")
2656+ wt.commit("bla", rev_id=b"oldcommit")
2657+ oldrepos = wt.branch.repository
2658+ wt = self.make_branch_and_tree("new")
2659+ self.build_tree(['new/afile'])
2660+ wt.add(["afile"], ids=[b"newid"])
2661+ wt.commit("bla", rev_id=b"newparent")
2662+ wt.branch.repository.fetch(oldrepos)
2663+ wt.branch.repository.lock_write()
2664+ CommitBuilderRevisionRewriter(wt.branch.repository)(
2665+ b"oldcommit", b"newcommit", (b"newparent",))
2666+ wt.branch.repository.unlock()
2667+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2668+ newrev = wt.branch.repository.get_revision(b"newcommit")
2669+ self.assertEquals([b"newparent"], newrev.parent_ids)
2670+ self.assertEquals(b"newcommit", newrev.revision_id)
2671+ self.assertEquals(oldrev.committer, newrev.committer)
2672+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2673+ self.assertEquals(oldrev.timezone, newrev.timezone)
2674+ tree = wt.branch.repository.revision_tree(b"newcommit")
2675+ self.assertEquals(b"newid", tree.path2id("afile"))
2676+ self.assertEquals(b"newcommit", tree.get_file_revision("afile"))
2677+
2678+
2679+class TestReplayWorkingtree(TestCaseWithTransport):
2680+ def test_conflicts(self):
2681+ wt = self.make_branch_and_tree("old")
2682+ wt.commit("base", rev_id=b"base")
2683+ self.build_tree(['old/afile'])
2684+ wt.add(["afile"], ids=[b"originalid"])
2685+ wt.commit("bla", rev_id=b"oldparent")
2686+ with open("old/afile", "w") as f:
2687+ f.write("bloe")
2688+ wt.commit("bla", rev_id=b"oldcommit")
2689+ oldrepos = wt.branch.repository
2690+ wt = self.make_branch_and_tree("new")
2691+ self.build_tree(['new/afile'])
2692+ wt.add(["afile"], ids=[b"newid"])
2693+ wt.commit("bla", rev_id=b"newparent")
2694+ wt.branch.repository.fetch(oldrepos)
2695+ with wt.lock_write():
2696+ replayer = WorkingTreeRevisionRewriter(wt, RebaseState1(wt))
2697+ self.assertRaises(
2698+ ConflictsInTree,
2699+ replayer, b"oldcommit", b"newcommit",
2700+ [b"newparent"], )
2701+
2702+ def test_simple(self):
2703+ wt = self.make_branch_and_tree("old")
2704+ wt.commit("base", rev_id=b"base")
2705+ self.build_tree(['old/afile'])
2706+ wt.add(["afile"], ids=[b"originalid"])
2707+ wt.commit("bla", rev_id=b"oldparent")
2708+ with open("old/afile", "w") as f:
2709+ f.write("bloe")
2710+ wt.commit("bla", rev_id=b"oldcommit")
2711+ wt = wt.controldir.sprout("new").open_workingtree()
2712+ self.build_tree(['new/bfile'])
2713+ wt.add(["bfile"], ids=[b"newid"])
2714+ wt.commit("bla", rev_id=b"newparent")
2715+ replayer = WorkingTreeRevisionRewriter(wt, RebaseState1(wt))
2716+ replayer(b"oldcommit", b"newcommit", [b"newparent"])
2717+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2718+ newrev = wt.branch.repository.get_revision(b"newcommit")
2719+ self.assertEquals([b"newparent"], newrev.parent_ids)
2720+ self.assertEquals(b"newcommit", newrev.revision_id)
2721+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2722+ self.assertEquals(oldrev.timezone, newrev.timezone)
2723+
2724+ def test_multiple(self):
2725+ # rebase from
2726+ # base: []
2727+ # oldparent: [base]
2728+ # newparent: [base]
2729+ # oldcommit: [oldparent, ghost]
2730+ # create newcommit by rebasing oldcommit from oldparent to newparent,
2731+ # keeping the merge of ghost.
2732+ # Common base:
2733+ wt = self.make_branch_and_tree("old")
2734+ wt.commit("base", rev_id=b"base")
2735+ # oldparent:
2736+ self.build_tree_contents([('old/afile', 'base content')])
2737+ wt.add(["afile"], ids=[b"originalid"])
2738+ wt.commit("bla", rev_id=b"oldparent")
2739+ # oldcommit (the delta getting rebased)
2740+ # - change the content of afile to be 'bloe'
2741+ with open("old/afile", "w") as f:
2742+ f.write("bloe")
2743+ wt.add_pending_merge(b"ghost")
2744+ wt.commit("bla", rev_id=b"oldcommit")
2745+ # newparent (the new base for the rebased commit)
2746+ new_tree = wt.controldir.sprout(
2747+ "new", revision_id=b'base').open_workingtree()
2748+ new_tree.branch.repository.fetch(wt.branch.repository)
2749+ wt = new_tree
2750+ self.build_tree_contents([('new/afile', 'base content')])
2751+ wt.add(["afile"], ids=[b"originalid"])
2752+ wt.commit("bla", rev_id=b"newparent")
2753+ # And do it!
2754+ wt.lock_write()
2755+ replayer = WorkingTreeRevisionRewriter(wt, RebaseState1(wt))
2756+ replayer(b"oldcommit", b"newcommit", (b"newparent", b"ghost"))
2757+ wt.unlock()
2758+ oldrev = wt.branch.repository.get_revision(b"oldcommit")
2759+ newrev = wt.branch.repository.get_revision(b"newcommit")
2760+ self.assertEquals([b"oldparent", b"ghost"], oldrev.parent_ids)
2761+ self.assertEquals([b"newparent", b"ghost"], newrev.parent_ids)
2762+ self.assertEquals(b"newcommit", newrev.revision_id)
2763+ self.assertEquals(oldrev.timestamp, newrev.timestamp)
2764+ self.assertEquals(oldrev.timezone, newrev.timezone)
2765+
2766+ def test_already_merged(self):
2767+ """We need to use a merge base that makes sense.
2768+
2769+ A
2770+ | \
2771+ B D
2772+ | \|
2773+ C E
2774+
2775+ Rebasing E on C should result in:
2776+
2777+ A -> B -> C -> D' -> E'
2778+
2779+ Ancestry:
2780+ A:
2781+ B: A
2782+ C: A, B
2783+ D: A
2784+ E: A, B, D
2785+ D': A, B, C
2786+ E': A, B, C, D'
2787+
2788+ """
2789+ oldwt = self.make_branch_and_tree("old")
2790+ self.build_tree(['old/afile'])
2791+ with open("old/afile", "w") as f:
2792+ f.write("A\n" * 10)
2793+ oldwt.add(["afile"])
2794+ oldwt.commit("base", rev_id=b"A")
2795+ newwt = oldwt.controldir.sprout("new").open_workingtree()
2796+ with open("old/afile", "w") as f:
2797+ f.write("A\n" * 10 + "B\n")
2798+ oldwt.commit("bla", rev_id=b"B")
2799+ with open("old/afile", "w") as f:
2800+ f.write("A\n" * 10 + "C\n")
2801+ oldwt.commit("bla", rev_id=b"C")
2802+ self.build_tree(['new/bfile'])
2803+ newwt.add(["bfile"])
2804+ with open("new/bfile", "w") as f:
2805+ f.write("D\n")
2806+ newwt.commit("bla", rev_id=b"D")
2807+ with open("new/afile", "w") as f:
2808+ f.write("E\n" + "A\n" * 10 + "B\n")
2809+ with open("new/bfile", "w") as f:
2810+ f.write("D\nE\n")
2811+ newwt.add_pending_merge(b"B")
2812+ newwt.commit("bla", rev_id=b"E")
2813+ newwt.branch.repository.fetch(oldwt.branch.repository)
2814+ newwt.lock_write()
2815+ replayer = WorkingTreeRevisionRewriter(newwt, RebaseState1(newwt))
2816+ replayer(b"D", b"D'", [b"C"])
2817+ newwt.unlock()
2818+ oldrev = newwt.branch.repository.get_revision(b"D")
2819+ newrev = newwt.branch.repository.get_revision(b"D'")
2820+ self.assertEquals([b"C"], newrev.parent_ids)
2821+ newwt.lock_write()
2822+ replayer = WorkingTreeRevisionRewriter(newwt, RebaseState1(newwt))
2823+ self.assertRaises(ConflictsInTree, replayer, b"E", b"E'", [b"D'"])
2824+ newwt.unlock()
2825+ with open("new/afile", 'r') as f:
2826+ self.assertEquals("E\n" + "A\n" * 10 + "C\n", f.read())
2827+ newwt.set_conflicts(ConflictList())
2828+ oldrev = newwt.branch.repository.get_revision(b"E")
2829+ replayer.commit_rebase(oldrev, b"E'")
2830+ newrev = newwt.branch.repository.get_revision(b"E'")
2831+ self.assertEquals([b"D'"], newrev.parent_ids)
2832+ self.assertThat(
2833+ newwt.branch,
2834+ RevisionHistoryMatches([b"A", b"B", b"C", b"D'", b"E'"]))
2835+
2836+
2837+class TestReplaySnapshotError(TestCase):
2838+
2839+ def test_create(self):
2840+ ReplaySnapshotError("message")
2841
2842=== added file 'breezy/plugins/rewrite/tests/test_upgrade.py'
2843--- breezy/plugins/rewrite/tests/test_upgrade.py 1970-01-01 00:00:00 +0000
2844+++ breezy/plugins/rewrite/tests/test_upgrade.py 2020-01-11 23:46:53 +0000
2845@@ -0,0 +1,32 @@
2846+# Copyright (C) 2007-2009 Jelmer Vernooij <jelmer@samba.org>
2847+
2848+# This program is free software; you can redistribute it and/or modify
2849+# it under the terms of the GNU General Public License as published by
2850+# the Free Software Foundation; either version 2 of the License, or
2851+# (at your option) any later version.
2852+
2853+# This program is distributed in the hope that it will be useful,
2854+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2855+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2856+# GNU General Public License for more details.
2857+
2858+# You should have received a copy of the GNU General Public License
2859+# along with this program; if not, write to the Free Software
2860+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2861+
2862+"""Mapping upgrade tests."""
2863+
2864+from ....tests import (
2865+ TestCase,
2866+ )
2867+
2868+from ..upgrade import (
2869+ UpgradeChangesContent,
2870+ )
2871+
2872+
2873+class TestUpgradeChangesContent(TestCase):
2874+
2875+ def test_init(self):
2876+ x = UpgradeChangesContent("revisionx")
2877+ self.assertEqual("revisionx", x.revid)
2878
2879=== added file 'breezy/plugins/rewrite/upgrade.py'
2880--- breezy/plugins/rewrite/upgrade.py 1970-01-01 00:00:00 +0000
2881+++ breezy/plugins/rewrite/upgrade.py 2020-01-11 23:46:53 +0000
2882@@ -0,0 +1,190 @@
2883+# Copyright (C) 2006-2009 by Jelmer Vernooij
2884+#
2885+# This program is free software; you can redistribute it and/or modify
2886+# it under the terms of the GNU General Public License as published by
2887+# the Free Software Foundation; either version 3 of the License, or
2888+# (at your option) any later version.
2889+#
2890+# This program is distributed in the hope that it will be useful,
2891+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2892+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2893+# GNU General Public License for more details.
2894+#
2895+# You should have received a copy of the GNU General Public License
2896+# along with this program; if not, write to the Free Software
2897+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2898+
2899+
2900+"""Upgrading revisions made with older versions of the mapping."""
2901+
2902+from __future__ import absolute_import
2903+
2904+from ... import (
2905+ osutils,
2906+ trace,
2907+ ui,
2908+ )
2909+from ...errors import (
2910+ BzrError,
2911+ )
2912+from .rebase import (
2913+ generate_transpose_plan,
2914+ CommitBuilderRevisionRewriter,
2915+ rebase,
2916+ rebase_todo,
2917+ )
2918+
2919+
2920+class UpgradeChangesContent(BzrError):
2921+ """Inconsistency was found upgrading the mapping of a revision."""
2922+ _fmt = """Upgrade will change contents in revision %(revid)s. Use --allow-changes to override."""
2923+
2924+ def __init__(self, revid):
2925+ self.revid = revid
2926+
2927+
2928+def create_deterministic_revid(revid, new_parents):
2929+ """Create a new deterministic revision id with specified new parents.
2930+
2931+ Prevents suffix to be appended needlessly.
2932+
2933+ :param revid: Original revision id.
2934+ :return: New revision id
2935+ """
2936+ if "-rebase-" in revid:
2937+ revid = revid[0:revid.rfind("-rebase-")]
2938+ return revid + "-rebase-" + osutils.sha_string(":".join(new_parents))[:8]
2939+
2940+
2941+def upgrade_tags(tags, repository, generate_rebase_map, determine_new_revid,
2942+ allow_changes=False, verbose=False, branch_renames=None,
2943+ branch_ancestry=None):
2944+ """Upgrade a tags dictionary."""
2945+ renames = {}
2946+ if branch_renames is not None:
2947+ renames.update(branch_renames)
2948+ pb = ui.ui_factory.nested_progress_bar()
2949+ try:
2950+ tags_dict = tags.get_tag_dict()
2951+ for i, (name, revid) in enumerate(tags_dict.iteritems()):
2952+ pb.update("upgrading tags", i, len(tags_dict))
2953+ if revid not in renames:
2954+ try:
2955+ repository.lock_read()
2956+ revid_exists = repository.has_revision(revid)
2957+ finally:
2958+ repository.unlock()
2959+ if revid_exists:
2960+ renames.update(upgrade_repository(
2961+ repository,
2962+ generate_rebase_map, determine_new_revid,
2963+ revision_id=revid, allow_changes=allow_changes,
2964+ verbose=verbose))
2965+ if (revid in renames and
2966+ (branch_ancestry is None or revid not in branch_ancestry)):
2967+ tags.set_tag(name, renames[revid])
2968+ finally:
2969+ pb.finished()
2970+
2971+
2972+def upgrade_branch(branch, generate_rebase_map, determine_new_revid,
2973+ allow_changes=False, verbose=False):
2974+ """Upgrade a branch to the current mapping version.
2975+
2976+ :param branch: Branch to upgrade.
2977+ :param foreign_repository: Repository to fetch new revisions from
2978+ :param allow_changes: Allow changes in mappings.
2979+ :param verbose: Whether to print verbose list of rewrites
2980+ """
2981+ revid = branch.last_revision()
2982+ renames = upgrade_repository(
2983+ branch.repository, generate_rebase_map,
2984+ determine_new_revid, revision_id=revid,
2985+ allow_changes=allow_changes, verbose=verbose)
2986+ if revid in renames:
2987+ branch.generate_revision_history(renames[revid])
2988+ ancestry = branch.repository.get_ancestry(
2989+ branch.last_revision(), topo_sorted=False)
2990+ upgrade_tags(
2991+ branch.tags, branch.repository, generate_rebase_map,
2992+ determine_new_revid, allow_changes=allow_changes, verbose=verbose,
2993+ branch_renames=renames, branch_ancestry=ancestry)
2994+ return renames
2995+
2996+
2997+def check_revision_changed(oldrev, newrev):
2998+ """Check if two revisions are different. This is exactly the same
2999+ as Revision.equals() except that it does not check the revision_id."""
3000+ if (newrev.inventory_sha1 != oldrev.inventory_sha1 or
3001+ newrev.timestamp != oldrev.timestamp or
3002+ newrev.message != oldrev.message or
3003+ newrev.timezone != oldrev.timezone or
3004+ newrev.committer != oldrev.committer or
3005+ newrev.properties != oldrev.properties):
3006+ raise UpgradeChangesContent(oldrev.revision_id)
3007+
3008+
3009+def create_upgrade_plan(repository, generate_rebase_map, determine_new_revid,
3010+ revision_id=None, allow_changes=False):
3011+ """Generate a rebase plan for upgrading revisions.
3012+
3013+ :param repository: Repository to do upgrade in
3014+ :param foreign_repository: Subversion repository to fetch new revisions
3015+ from.
3016+ :param new_mapping: New mapping to use.
3017+ :param revision_id: Revision to upgrade (None for all revisions in
3018+ repository.)
3019+ :param allow_changes: Whether an upgrade is allowed to change the contents
3020+ of revisions.
3021+ :return: Tuple with a rebase plan and map of renamed revisions.
3022+ """
3023+
3024+ graph = repository.get_graph()
3025+ upgrade_map = generate_rebase_map(revision_id)
3026+
3027+ if not allow_changes:
3028+ for oldrevid, newrevid in upgrade_map.iteritems():
3029+ oldrev = repository.get_revision(oldrevid)
3030+ newrev = repository.get_revision(newrevid)
3031+ check_revision_changed(oldrev, newrev)
3032+
3033+ if revision_id is None:
3034+ heads = repository.all_revision_ids()
3035+ else:
3036+ heads = [revision_id]
3037+
3038+ plan = generate_transpose_plan(
3039+ graph.iter_ancestry(heads), upgrade_map, graph, determine_new_revid)
3040+ def remove_parents(entry):
3041+ (oldrevid, (newrevid, parents)) = entry
3042+ return (oldrevid, newrevid)
3043+ upgrade_map.update(dict(map(remove_parents, plan.iteritems())))
3044+
3045+ return (plan, upgrade_map)
3046+
3047+
3048+def upgrade_repository(repository, generate_rebase_map,
3049+ determine_new_revid, revision_id=None,
3050+ allow_changes=False, verbose=False):
3051+ """Upgrade the revisions in repository until the specified stop revision.
3052+
3053+ :param repository: Repository in which to upgrade.
3054+ :param foreign_repository: Repository to fetch new revisions from.
3055+ :param new_mapping: New mapping.
3056+ :param revision_id: Revision id up until which to upgrade, or None for
3057+ all revisions.
3058+ :param allow_changes: Allow changes to mappings.
3059+ :param verbose: Whether to print list of rewrites
3060+ :return: Dictionary of mapped revisions
3061+ """
3062+ # Find revisions that need to be upgraded, create
3063+ # dictionary with revision ids in key, new parents in value
3064+ with repository.lock_write():
3065+ (plan, revid_renames) = create_upgrade_plan(
3066+ repository, generate_rebase_map, determine_new_revid,
3067+ revision_id=revision_id, allow_changes=allow_changes)
3068+ if verbose:
3069+ for revid in rebase_todo(repository, plan):
3070+ trace.note("%s -> %s" % (revid, plan[revid][0]))
3071+ rebase(repository, plan, CommitBuilderRevisionRewriter(repository))
3072+ return revid_renames

Subscribers

People subscribed via source and target branches