##// END OF EJS Templates
rebase: allow creation obsolescence relation instead of stripping...
Pierre-Yves David -
r17612:fc2a6114 default
parent child Browse files
Show More
@@ -0,0 +1,175 b''
1 ==========================
2 Test rebase with obsolete
3 ==========================
4
5 Enable obsolete
6
7 $ cat > ${TESTTMP}/obs.py << EOF
8 > import mercurial.obsolete
9 > mercurial.obsolete._enabled = True
10 > EOF
11 $ cat >> $HGRCPATH << EOF
12 > [ui]
13 > logtemplate= {rev}:{node|short} {desc|firstline}
14 > [phases]
15 > publish=False
16 > [extensions]'
17 > rebase=
18 >
19 > obs=${TESTTMP}/obs.py
20 > EOF
21
22 Setup rebase canonical repo
23
24 $ hg init base
25 $ cd base
26 $ hg unbundle "$TESTDIR/bundles/rebase.hg"
27 adding changesets
28 adding manifests
29 adding file changes
30 added 8 changesets with 7 changes to 7 files (+2 heads)
31 (run 'hg heads' to see heads, 'hg merge' to merge)
32 $ hg up tip
33 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 $ hg log -G
35 @ 7:02de42196ebe H
36 |
37 | o 6:eea13746799a G
38 |/|
39 o | 5:24b6387c8c8c F
40 | |
41 | o 4:9520eea781bc E
42 |/
43 | o 3:32af7686d403 D
44 | |
45 | o 2:5fddd98957c8 C
46 | |
47 | o 1:42ccdea3bb16 B
48 |/
49 o 0:cd010b8cd998 A
50
51 $ cd ..
52
53 simple rebase
54 ---------------------------------
55
56 $ hg clone base simple
57 updating to branch default
58 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 $ cd simple
60 $ hg up 32af7686d403
61 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
62 $ hg rebase -d eea13746799a
63 $ hg log -G
64 @ 10:8eeb3c33ad33 D
65 |
66 o 9:2327fea05063 C
67 |
68 o 8:e4e5be0395b2 B
69 |
70 | o 7:02de42196ebe H
71 | |
72 o | 6:eea13746799a G
73 |\|
74 | o 5:24b6387c8c8c F
75 | |
76 o | 4:9520eea781bc E
77 |/
78 o 0:cd010b8cd998 A
79
80 $ hg log --hidden -G
81 @ 10:8eeb3c33ad33 D
82 |
83 o 9:2327fea05063 C
84 |
85 o 8:e4e5be0395b2 B
86 |
87 | o 7:02de42196ebe H
88 | |
89 o | 6:eea13746799a G
90 |\|
91 | o 5:24b6387c8c8c F
92 | |
93 o | 4:9520eea781bc E
94 |/
95 | x 3:32af7686d403 D
96 | |
97 | x 2:5fddd98957c8 C
98 | |
99 | x 1:42ccdea3bb16 B
100 |/
101 o 0:cd010b8cd998 A
102
103 $ hg debugobsolete
104 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 e4e5be0395b2cbd471ed22a26b1b6a1a0658a794 0 {'date': '*', 'user': 'test'} (glob)
105 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 2327fea05063f39961b14cb69435a9898dc9a245 0 {'date': '*', 'user': 'test'} (glob)
106 32af7686d403cf45b5d95f2d70cebea587ac806a 8eeb3c33ad33d452c89e5dcf611c347f978fb42b 0 {'date': '*', 'user': 'test'} (glob)
107
108
109 $ cd ..
110
111 empty changeset
112 ---------------------------------
113
114 $ hg clone base empty
115 updating to branch default
116 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
117 $ cd empty
118 $ hg up eea13746799a
119 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
120
121 We make a copy of both the first changeset in the rebased and some other in the
122 set.
123
124 $ hg graft 42ccdea3bb16 32af7686d403
125 grafting revision 1
126 grafting revision 3
127 $ hg rebase -s 42ccdea3bb16 -d .
128 $ hg log -G
129 @ 10:5ae4c968c6ac C
130 |
131 o 9:08483444fef9 D
132 |
133 o 8:8877864f1edb B
134 |
135 | o 7:02de42196ebe H
136 | |
137 o | 6:eea13746799a G
138 |\|
139 | o 5:24b6387c8c8c F
140 | |
141 o | 4:9520eea781bc E
142 |/
143 o 0:cd010b8cd998 A
144
145 $ hg log --hidden -G
146 @ 10:5ae4c968c6ac C
147 |
148 o 9:08483444fef9 D
149 |
150 o 8:8877864f1edb B
151 |
152 | o 7:02de42196ebe H
153 | |
154 o | 6:eea13746799a G
155 |\|
156 | o 5:24b6387c8c8c F
157 | |
158 o | 4:9520eea781bc E
159 |/
160 | x 3:32af7686d403 D
161 | |
162 | x 2:5fddd98957c8 C
163 | |
164 | x 1:42ccdea3bb16 B
165 |/
166 o 0:cd010b8cd998 A
167
168 $ hg debugobsolete
169 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 08483444fef91d6224f6655ee586a65d263ad34c 0 {'date': '*', 'user': 'test'} (glob)
170 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
171 32af7686d403cf45b5d95f2d70cebea587ac806a 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
172
173
174 $ cd ..
175
@@ -1,712 +1,720 b''
1 # rebase.py - rebasing feature for mercurial
1 # rebase.py - rebasing feature for mercurial
2 #
2 #
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to move sets of revisions to a different ancestor
8 '''command to move sets of revisions to a different ancestor
9
9
10 This extension lets you rebase changesets in an existing Mercurial
10 This extension lets you rebase changesets in an existing Mercurial
11 repository.
11 repository.
12
12
13 For more information:
13 For more information:
14 http://mercurial.selenic.com/wiki/RebaseExtension
14 http://mercurial.selenic.com/wiki/RebaseExtension
15 '''
15 '''
16
16
17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
18 from mercurial import extensions, patch, scmutil, phases
18 from mercurial import extensions, patch, scmutil, phases, obsolete
19 from mercurial.commands import templateopts
19 from mercurial.commands import templateopts
20 from mercurial.node import nullrev
20 from mercurial.node import nullrev
21 from mercurial.lock import release
21 from mercurial.lock import release
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 import os, errno
23 import os, errno
24
24
25 nullmerge = -2
25 nullmerge = -2
26
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29 testedwith = 'internal'
29 testedwith = 'internal'
30
30
31 @command('rebase',
31 @command('rebase',
32 [('s', 'source', '',
32 [('s', 'source', '',
33 _('rebase from the specified changeset'), _('REV')),
33 _('rebase from the specified changeset'), _('REV')),
34 ('b', 'base', '',
34 ('b', 'base', '',
35 _('rebase from the base of the specified changeset '
35 _('rebase from the base of the specified changeset '
36 '(up to greatest common ancestor of base and dest)'),
36 '(up to greatest common ancestor of base and dest)'),
37 _('REV')),
37 _('REV')),
38 ('r', 'rev', [],
38 ('r', 'rev', [],
39 _('rebase these revisions'),
39 _('rebase these revisions'),
40 _('REV')),
40 _('REV')),
41 ('d', 'dest', '',
41 ('d', 'dest', '',
42 _('rebase onto the specified changeset'), _('REV')),
42 _('rebase onto the specified changeset'), _('REV')),
43 ('', 'collapse', False, _('collapse the rebased changesets')),
43 ('', 'collapse', False, _('collapse the rebased changesets')),
44 ('m', 'message', '',
44 ('m', 'message', '',
45 _('use text as collapse commit message'), _('TEXT')),
45 _('use text as collapse commit message'), _('TEXT')),
46 ('e', 'edit', False, _('invoke editor on commit messages')),
46 ('e', 'edit', False, _('invoke editor on commit messages')),
47 ('l', 'logfile', '',
47 ('l', 'logfile', '',
48 _('read collapse commit message from file'), _('FILE')),
48 _('read collapse commit message from file'), _('FILE')),
49 ('', 'keep', False, _('keep original changesets')),
49 ('', 'keep', False, _('keep original changesets')),
50 ('', 'keepbranches', False, _('keep original branch names')),
50 ('', 'keepbranches', False, _('keep original branch names')),
51 ('D', 'detach', False, _('(DEPRECATED)')),
51 ('D', 'detach', False, _('(DEPRECATED)')),
52 ('t', 'tool', '', _('specify merge tool')),
52 ('t', 'tool', '', _('specify merge tool')),
53 ('c', 'continue', False, _('continue an interrupted rebase')),
53 ('c', 'continue', False, _('continue an interrupted rebase')),
54 ('a', 'abort', False, _('abort an interrupted rebase'))] +
54 ('a', 'abort', False, _('abort an interrupted rebase'))] +
55 templateopts,
55 templateopts,
56 _('[-s REV | -b REV] [-d REV] [OPTION]'))
56 _('[-s REV | -b REV] [-d REV] [OPTION]'))
57 def rebase(ui, repo, **opts):
57 def rebase(ui, repo, **opts):
58 """move changeset (and descendants) to a different branch
58 """move changeset (and descendants) to a different branch
59
59
60 Rebase uses repeated merging to graft changesets from one part of
60 Rebase uses repeated merging to graft changesets from one part of
61 history (the source) onto another (the destination). This can be
61 history (the source) onto another (the destination). This can be
62 useful for linearizing *local* changes relative to a master
62 useful for linearizing *local* changes relative to a master
63 development tree.
63 development tree.
64
64
65 You should not rebase changesets that have already been shared
65 You should not rebase changesets that have already been shared
66 with others. Doing so will force everybody else to perform the
66 with others. Doing so will force everybody else to perform the
67 same rebase or they will end up with duplicated changesets after
67 same rebase or they will end up with duplicated changesets after
68 pulling in your rebased changesets.
68 pulling in your rebased changesets.
69
69
70 If you don't specify a destination changeset (``-d/--dest``),
70 If you don't specify a destination changeset (``-d/--dest``),
71 rebase uses the tipmost head of the current named branch as the
71 rebase uses the tipmost head of the current named branch as the
72 destination. (The destination changeset is not modified by
72 destination. (The destination changeset is not modified by
73 rebasing, but new changesets are added as its descendants.)
73 rebasing, but new changesets are added as its descendants.)
74
74
75 You can specify which changesets to rebase in two ways: as a
75 You can specify which changesets to rebase in two ways: as a
76 "source" changeset or as a "base" changeset. Both are shorthand
76 "source" changeset or as a "base" changeset. Both are shorthand
77 for a topologically related set of changesets (the "source
77 for a topologically related set of changesets (the "source
78 branch"). If you specify source (``-s/--source``), rebase will
78 branch"). If you specify source (``-s/--source``), rebase will
79 rebase that changeset and all of its descendants onto dest. If you
79 rebase that changeset and all of its descendants onto dest. If you
80 specify base (``-b/--base``), rebase will select ancestors of base
80 specify base (``-b/--base``), rebase will select ancestors of base
81 back to but not including the common ancestor with dest. Thus,
81 back to but not including the common ancestor with dest. Thus,
82 ``-b`` is less precise but more convenient than ``-s``: you can
82 ``-b`` is less precise but more convenient than ``-s``: you can
83 specify any changeset in the source branch, and rebase will select
83 specify any changeset in the source branch, and rebase will select
84 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
84 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
85 uses the parent of the working directory as the base.
85 uses the parent of the working directory as the base.
86
86
87 By default, rebase recreates the changesets in the source branch
87 By default, rebase recreates the changesets in the source branch
88 as descendants of dest and then destroys the originals. Use
88 as descendants of dest and then destroys the originals. Use
89 ``--keep`` to preserve the original source changesets. Some
89 ``--keep`` to preserve the original source changesets. Some
90 changesets in the source branch (e.g. merges from the destination
90 changesets in the source branch (e.g. merges from the destination
91 branch) may be dropped if they no longer contribute any change.
91 branch) may be dropped if they no longer contribute any change.
92
92
93 One result of the rules for selecting the destination changeset
93 One result of the rules for selecting the destination changeset
94 and source branch is that, unlike ``merge``, rebase will do
94 and source branch is that, unlike ``merge``, rebase will do
95 nothing if you are at the latest (tipmost) head of a named branch
95 nothing if you are at the latest (tipmost) head of a named branch
96 with two heads. You need to explicitly specify source and/or
96 with two heads. You need to explicitly specify source and/or
97 destination (or ``update`` to the other head, if it's the head of
97 destination (or ``update`` to the other head, if it's the head of
98 the intended source branch).
98 the intended source branch).
99
99
100 If a rebase is interrupted to manually resolve a merge, it can be
100 If a rebase is interrupted to manually resolve a merge, it can be
101 continued with --continue/-c or aborted with --abort/-a.
101 continued with --continue/-c or aborted with --abort/-a.
102
102
103 Returns 0 on success, 1 if nothing to rebase.
103 Returns 0 on success, 1 if nothing to rebase.
104 """
104 """
105 originalwd = target = None
105 originalwd = target = None
106 external = nullrev
106 external = nullrev
107 state = {}
107 state = {}
108 skipped = set()
108 skipped = set()
109 targetancestors = set()
109 targetancestors = set()
110
110
111 editor = None
111 editor = None
112 if opts.get('edit'):
112 if opts.get('edit'):
113 editor = cmdutil.commitforceeditor
113 editor = cmdutil.commitforceeditor
114
114
115 lock = wlock = None
115 lock = wlock = None
116 try:
116 try:
117 wlock = repo.wlock()
117 wlock = repo.wlock()
118 lock = repo.lock()
118 lock = repo.lock()
119
119
120 # Validate input and define rebasing points
120 # Validate input and define rebasing points
121 destf = opts.get('dest', None)
121 destf = opts.get('dest', None)
122 srcf = opts.get('source', None)
122 srcf = opts.get('source', None)
123 basef = opts.get('base', None)
123 basef = opts.get('base', None)
124 revf = opts.get('rev', [])
124 revf = opts.get('rev', [])
125 contf = opts.get('continue')
125 contf = opts.get('continue')
126 abortf = opts.get('abort')
126 abortf = opts.get('abort')
127 collapsef = opts.get('collapse', False)
127 collapsef = opts.get('collapse', False)
128 collapsemsg = cmdutil.logmessage(ui, opts)
128 collapsemsg = cmdutil.logmessage(ui, opts)
129 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
129 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
130 keepf = opts.get('keep', False)
130 keepf = opts.get('keep', False)
131 keepbranchesf = opts.get('keepbranches', False)
131 keepbranchesf = opts.get('keepbranches', False)
132 # keepopen is not meant for use on the command line, but by
132 # keepopen is not meant for use on the command line, but by
133 # other extensions
133 # other extensions
134 keepopen = opts.get('keepopen', False)
134 keepopen = opts.get('keepopen', False)
135
135
136 if collapsemsg and not collapsef:
136 if collapsemsg and not collapsef:
137 raise util.Abort(
137 raise util.Abort(
138 _('message can only be specified with collapse'))
138 _('message can only be specified with collapse'))
139
139
140 if contf or abortf:
140 if contf or abortf:
141 if contf and abortf:
141 if contf and abortf:
142 raise util.Abort(_('cannot use both abort and continue'))
142 raise util.Abort(_('cannot use both abort and continue'))
143 if collapsef:
143 if collapsef:
144 raise util.Abort(
144 raise util.Abort(
145 _('cannot use collapse with continue or abort'))
145 _('cannot use collapse with continue or abort'))
146 if srcf or basef or destf:
146 if srcf or basef or destf:
147 raise util.Abort(
147 raise util.Abort(
148 _('abort and continue do not allow specifying revisions'))
148 _('abort and continue do not allow specifying revisions'))
149 if opts.get('tool', False):
149 if opts.get('tool', False):
150 ui.warn(_('tool option will be ignored\n'))
150 ui.warn(_('tool option will be ignored\n'))
151
151
152 (originalwd, target, state, skipped, collapsef, keepf,
152 (originalwd, target, state, skipped, collapsef, keepf,
153 keepbranchesf, external) = restorestatus(repo)
153 keepbranchesf, external) = restorestatus(repo)
154 if abortf:
154 if abortf:
155 return abort(repo, originalwd, target, state)
155 return abort(repo, originalwd, target, state)
156 else:
156 else:
157 if srcf and basef:
157 if srcf and basef:
158 raise util.Abort(_('cannot specify both a '
158 raise util.Abort(_('cannot specify both a '
159 'source and a base'))
159 'source and a base'))
160 if revf and basef:
160 if revf and basef:
161 raise util.Abort(_('cannot specify both a '
161 raise util.Abort(_('cannot specify both a '
162 'revision and a base'))
162 'revision and a base'))
163 if revf and srcf:
163 if revf and srcf:
164 raise util.Abort(_('cannot specify both a '
164 raise util.Abort(_('cannot specify both a '
165 'revision and a source'))
165 'revision and a source'))
166
166
167 cmdutil.bailifchanged(repo)
167 cmdutil.bailifchanged(repo)
168
168
169 if not destf:
169 if not destf:
170 # Destination defaults to the latest revision in the
170 # Destination defaults to the latest revision in the
171 # current branch
171 # current branch
172 branch = repo[None].branch()
172 branch = repo[None].branch()
173 dest = repo[branch]
173 dest = repo[branch]
174 else:
174 else:
175 dest = scmutil.revsingle(repo, destf)
175 dest = scmutil.revsingle(repo, destf)
176
176
177 if revf:
177 if revf:
178 rebaseset = repo.revs('%lr', revf)
178 rebaseset = repo.revs('%lr', revf)
179 elif srcf:
179 elif srcf:
180 src = scmutil.revrange(repo, [srcf])
180 src = scmutil.revrange(repo, [srcf])
181 rebaseset = repo.revs('(%ld)::', src)
181 rebaseset = repo.revs('(%ld)::', src)
182 else:
182 else:
183 base = scmutil.revrange(repo, [basef or '.'])
183 base = scmutil.revrange(repo, [basef or '.'])
184 rebaseset = repo.revs(
184 rebaseset = repo.revs(
185 '(children(ancestor(%ld, %d)) and ::(%ld))::',
185 '(children(ancestor(%ld, %d)) and ::(%ld))::',
186 base, dest, base)
186 base, dest, base)
187
187
188 if rebaseset:
188 if rebaseset:
189 root = min(rebaseset)
189 root = min(rebaseset)
190 else:
190 else:
191 root = None
191 root = None
192
192
193 if not rebaseset:
193 if not rebaseset:
194 repo.ui.debug('base is ancestor of destination\n')
194 repo.ui.debug('base is ancestor of destination\n')
195 result = None
195 result = None
196 elif not keepf and list(repo.revs('first(children(%ld) - %ld)',
196 elif not keepf and list(repo.revs('first(children(%ld) - %ld)',
197 rebaseset, rebaseset)):
197 rebaseset, rebaseset)):
198 raise util.Abort(
198 raise util.Abort(
199 _("can't remove original changesets with"
199 _("can't remove original changesets with"
200 " unrebased descendants"),
200 " unrebased descendants"),
201 hint=_('use --keep to keep original changesets'))
201 hint=_('use --keep to keep original changesets'))
202 elif not keepf and not repo[root].mutable():
202 elif not keepf and not repo[root].mutable():
203 raise util.Abort(_("can't rebase immutable changeset %s")
203 raise util.Abort(_("can't rebase immutable changeset %s")
204 % repo[root],
204 % repo[root],
205 hint=_('see hg help phases for details'))
205 hint=_('see hg help phases for details'))
206 else:
206 else:
207 result = buildstate(repo, dest, rebaseset, collapsef)
207 result = buildstate(repo, dest, rebaseset, collapsef)
208
208
209 if not result:
209 if not result:
210 # Empty state built, nothing to rebase
210 # Empty state built, nothing to rebase
211 ui.status(_('nothing to rebase\n'))
211 ui.status(_('nothing to rebase\n'))
212 return 1
212 return 1
213 else:
213 else:
214 originalwd, target, state = result
214 originalwd, target, state = result
215 if collapsef:
215 if collapsef:
216 targetancestors = set(repo.changelog.ancestors([target]))
216 targetancestors = set(repo.changelog.ancestors([target]))
217 targetancestors.add(target)
217 targetancestors.add(target)
218 external = checkexternal(repo, state, targetancestors)
218 external = checkexternal(repo, state, targetancestors)
219
219
220 if keepbranchesf:
220 if keepbranchesf:
221 assert not extrafn, 'cannot use both keepbranches and extrafn'
221 assert not extrafn, 'cannot use both keepbranches and extrafn'
222 def extrafn(ctx, extra):
222 def extrafn(ctx, extra):
223 extra['branch'] = ctx.branch()
223 extra['branch'] = ctx.branch()
224 if collapsef:
224 if collapsef:
225 branches = set()
225 branches = set()
226 for rev in state:
226 for rev in state:
227 branches.add(repo[rev].branch())
227 branches.add(repo[rev].branch())
228 if len(branches) > 1:
228 if len(branches) > 1:
229 raise util.Abort(_('cannot collapse multiple named '
229 raise util.Abort(_('cannot collapse multiple named '
230 'branches'))
230 'branches'))
231
231
232
232
233 # Rebase
233 # Rebase
234 if not targetancestors:
234 if not targetancestors:
235 targetancestors = set(repo.changelog.ancestors([target]))
235 targetancestors = set(repo.changelog.ancestors([target]))
236 targetancestors.add(target)
236 targetancestors.add(target)
237
237
238 # Keep track of the current bookmarks in order to reset them later
238 # Keep track of the current bookmarks in order to reset them later
239 currentbookmarks = repo._bookmarks.copy()
239 currentbookmarks = repo._bookmarks.copy()
240 activebookmark = repo._bookmarkcurrent
240 activebookmark = repo._bookmarkcurrent
241 if activebookmark:
241 if activebookmark:
242 bookmarks.unsetcurrent(repo)
242 bookmarks.unsetcurrent(repo)
243
243
244 sortedstate = sorted(state)
244 sortedstate = sorted(state)
245 total = len(sortedstate)
245 total = len(sortedstate)
246 pos = 0
246 pos = 0
247 for rev in sortedstate:
247 for rev in sortedstate:
248 pos += 1
248 pos += 1
249 if state[rev] == -1:
249 if state[rev] == -1:
250 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
250 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
251 _('changesets'), total)
251 _('changesets'), total)
252 storestatus(repo, originalwd, target, state, collapsef, keepf,
252 storestatus(repo, originalwd, target, state, collapsef, keepf,
253 keepbranchesf, external)
253 keepbranchesf, external)
254 p1, p2 = defineparents(repo, rev, target, state,
254 p1, p2 = defineparents(repo, rev, target, state,
255 targetancestors)
255 targetancestors)
256 if len(repo.parents()) == 2:
256 if len(repo.parents()) == 2:
257 repo.ui.debug('resuming interrupted rebase\n')
257 repo.ui.debug('resuming interrupted rebase\n')
258 else:
258 else:
259 try:
259 try:
260 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
260 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
261 stats = rebasenode(repo, rev, p1, state, collapsef)
261 stats = rebasenode(repo, rev, p1, state, collapsef)
262 if stats and stats[3] > 0:
262 if stats and stats[3] > 0:
263 raise util.Abort(_('unresolved conflicts (see hg '
263 raise util.Abort(_('unresolved conflicts (see hg '
264 'resolve, then hg rebase --continue)'))
264 'resolve, then hg rebase --continue)'))
265 finally:
265 finally:
266 ui.setconfig('ui', 'forcemerge', '')
266 ui.setconfig('ui', 'forcemerge', '')
267 cmdutil.duplicatecopies(repo, rev, target)
267 cmdutil.duplicatecopies(repo, rev, target)
268 if not collapsef:
268 if not collapsef:
269 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
269 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
270 editor=editor)
270 editor=editor)
271 else:
271 else:
272 # Skip commit if we are collapsing
272 # Skip commit if we are collapsing
273 repo.setparents(repo[p1].node())
273 repo.setparents(repo[p1].node())
274 newrev = None
274 newrev = None
275 # Update the state
275 # Update the state
276 if newrev is not None:
276 if newrev is not None:
277 state[rev] = repo[newrev].rev()
277 state[rev] = repo[newrev].rev()
278 else:
278 else:
279 if not collapsef:
279 if not collapsef:
280 ui.note(_('no changes, revision %d skipped\n') % rev)
280 ui.note(_('no changes, revision %d skipped\n') % rev)
281 ui.debug('next revision set to %s\n' % p1)
281 ui.debug('next revision set to %s\n' % p1)
282 skipped.add(rev)
282 skipped.add(rev)
283 state[rev] = p1
283 state[rev] = p1
284
284
285 ui.progress(_('rebasing'), None)
285 ui.progress(_('rebasing'), None)
286 ui.note(_('rebase merging completed\n'))
286 ui.note(_('rebase merging completed\n'))
287
287
288 if collapsef and not keepopen:
288 if collapsef and not keepopen:
289 p1, p2 = defineparents(repo, min(state), target,
289 p1, p2 = defineparents(repo, min(state), target,
290 state, targetancestors)
290 state, targetancestors)
291 if collapsemsg:
291 if collapsemsg:
292 commitmsg = collapsemsg
292 commitmsg = collapsemsg
293 else:
293 else:
294 commitmsg = 'Collapsed revision'
294 commitmsg = 'Collapsed revision'
295 for rebased in state:
295 for rebased in state:
296 if rebased not in skipped and state[rebased] != nullmerge:
296 if rebased not in skipped and state[rebased] != nullmerge:
297 commitmsg += '\n* %s' % repo[rebased].description()
297 commitmsg += '\n* %s' % repo[rebased].description()
298 commitmsg = ui.edit(commitmsg, repo.ui.username())
298 commitmsg = ui.edit(commitmsg, repo.ui.username())
299 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
299 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
300 extrafn=extrafn, editor=editor)
300 extrafn=extrafn, editor=editor)
301
301
302 if 'qtip' in repo.tags():
302 if 'qtip' in repo.tags():
303 updatemq(repo, state, skipped, **opts)
303 updatemq(repo, state, skipped, **opts)
304
304
305 if currentbookmarks:
305 if currentbookmarks:
306 # Nodeids are needed to reset bookmarks
306 # Nodeids are needed to reset bookmarks
307 nstate = {}
307 nstate = {}
308 for k, v in state.iteritems():
308 for k, v in state.iteritems():
309 if v != nullmerge:
309 if v != nullmerge:
310 nstate[repo[k].node()] = repo[v].node()
310 nstate[repo[k].node()] = repo[v].node()
311
311
312 if not keepf:
312 if not keepf:
313 clearrebased(ui, repo, state)
313 clearrebased(ui, repo, state)
314
314
315 if currentbookmarks:
315 if currentbookmarks:
316 updatebookmarks(repo, nstate, currentbookmarks, **opts)
316 updatebookmarks(repo, nstate, currentbookmarks, **opts)
317
317
318 clearstatus(repo)
318 clearstatus(repo)
319 ui.note(_("rebase completed\n"))
319 ui.note(_("rebase completed\n"))
320 if os.path.exists(repo.sjoin('undo')):
320 if os.path.exists(repo.sjoin('undo')):
321 util.unlinkpath(repo.sjoin('undo'))
321 util.unlinkpath(repo.sjoin('undo'))
322 if skipped:
322 if skipped:
323 ui.note(_("%d revisions have been skipped\n") % len(skipped))
323 ui.note(_("%d revisions have been skipped\n") % len(skipped))
324
324
325 if (activebookmark and
325 if (activebookmark and
326 repo['tip'].node() == repo._bookmarks[activebookmark]):
326 repo['tip'].node() == repo._bookmarks[activebookmark]):
327 bookmarks.setcurrent(repo, activebookmark)
327 bookmarks.setcurrent(repo, activebookmark)
328
328
329 finally:
329 finally:
330 release(lock, wlock)
330 release(lock, wlock)
331
331
332 def checkexternal(repo, state, targetancestors):
332 def checkexternal(repo, state, targetancestors):
333 """Check whether one or more external revisions need to be taken in
333 """Check whether one or more external revisions need to be taken in
334 consideration. In the latter case, abort.
334 consideration. In the latter case, abort.
335 """
335 """
336 external = nullrev
336 external = nullrev
337 source = min(state)
337 source = min(state)
338 for rev in state:
338 for rev in state:
339 if rev == source:
339 if rev == source:
340 continue
340 continue
341 # Check externals and fail if there are more than one
341 # Check externals and fail if there are more than one
342 for p in repo[rev].parents():
342 for p in repo[rev].parents():
343 if (p.rev() not in state
343 if (p.rev() not in state
344 and p.rev() not in targetancestors):
344 and p.rev() not in targetancestors):
345 if external != nullrev:
345 if external != nullrev:
346 raise util.Abort(_('unable to collapse, there is more '
346 raise util.Abort(_('unable to collapse, there is more '
347 'than one external parent'))
347 'than one external parent'))
348 external = p.rev()
348 external = p.rev()
349 return external
349 return external
350
350
351 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
351 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
352 'Commit the changes and store useful information in extra'
352 'Commit the changes and store useful information in extra'
353 try:
353 try:
354 repo.setparents(repo[p1].node(), repo[p2].node())
354 repo.setparents(repo[p1].node(), repo[p2].node())
355 ctx = repo[rev]
355 ctx = repo[rev]
356 if commitmsg is None:
356 if commitmsg is None:
357 commitmsg = ctx.description()
357 commitmsg = ctx.description()
358 extra = {'rebase_source': ctx.hex()}
358 extra = {'rebase_source': ctx.hex()}
359 if extrafn:
359 if extrafn:
360 extrafn(ctx, extra)
360 extrafn(ctx, extra)
361 # Commit might fail if unresolved files exist
361 # Commit might fail if unresolved files exist
362 newrev = repo.commit(text=commitmsg, user=ctx.user(),
362 newrev = repo.commit(text=commitmsg, user=ctx.user(),
363 date=ctx.date(), extra=extra, editor=editor)
363 date=ctx.date(), extra=extra, editor=editor)
364 repo.dirstate.setbranch(repo[newrev].branch())
364 repo.dirstate.setbranch(repo[newrev].branch())
365 targetphase = max(ctx.phase(), phases.draft)
365 targetphase = max(ctx.phase(), phases.draft)
366 # retractboundary doesn't overwrite upper phase inherited from parent
366 # retractboundary doesn't overwrite upper phase inherited from parent
367 newnode = repo[newrev].node()
367 newnode = repo[newrev].node()
368 if newnode:
368 if newnode:
369 phases.retractboundary(repo, targetphase, [newnode])
369 phases.retractboundary(repo, targetphase, [newnode])
370 return newrev
370 return newrev
371 except util.Abort:
371 except util.Abort:
372 # Invalidate the previous setparents
372 # Invalidate the previous setparents
373 repo.dirstate.invalidate()
373 repo.dirstate.invalidate()
374 raise
374 raise
375
375
376 def rebasenode(repo, rev, p1, state, collapse):
376 def rebasenode(repo, rev, p1, state, collapse):
377 'Rebase a single revision'
377 'Rebase a single revision'
378 # Merge phase
378 # Merge phase
379 # Update to target and merge it with local
379 # Update to target and merge it with local
380 if repo['.'].rev() != repo[p1].rev():
380 if repo['.'].rev() != repo[p1].rev():
381 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
381 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
382 merge.update(repo, p1, False, True, False)
382 merge.update(repo, p1, False, True, False)
383 else:
383 else:
384 repo.ui.debug(" already in target\n")
384 repo.ui.debug(" already in target\n")
385 repo.dirstate.write()
385 repo.dirstate.write()
386 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
386 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
387 base = None
387 base = None
388 if repo[rev].rev() != repo[min(state)].rev():
388 if repo[rev].rev() != repo[min(state)].rev():
389 base = repo[rev].p1().node()
389 base = repo[rev].p1().node()
390 # When collapsing in-place, the parent is the common ancestor, we
390 # When collapsing in-place, the parent is the common ancestor, we
391 # have to allow merging with it.
391 # have to allow merging with it.
392 return merge.update(repo, rev, True, True, False, base, collapse)
392 return merge.update(repo, rev, True, True, False, base, collapse)
393
393
394 def defineparents(repo, rev, target, state, targetancestors):
394 def defineparents(repo, rev, target, state, targetancestors):
395 'Return the new parent relationship of the revision that will be rebased'
395 'Return the new parent relationship of the revision that will be rebased'
396 parents = repo[rev].parents()
396 parents = repo[rev].parents()
397 p1 = p2 = nullrev
397 p1 = p2 = nullrev
398
398
399 P1n = parents[0].rev()
399 P1n = parents[0].rev()
400 if P1n in targetancestors:
400 if P1n in targetancestors:
401 p1 = target
401 p1 = target
402 elif P1n in state:
402 elif P1n in state:
403 if state[P1n] == nullmerge:
403 if state[P1n] == nullmerge:
404 p1 = target
404 p1 = target
405 else:
405 else:
406 p1 = state[P1n]
406 p1 = state[P1n]
407 else: # P1n external
407 else: # P1n external
408 p1 = target
408 p1 = target
409 p2 = P1n
409 p2 = P1n
410
410
411 if len(parents) == 2 and parents[1].rev() not in targetancestors:
411 if len(parents) == 2 and parents[1].rev() not in targetancestors:
412 P2n = parents[1].rev()
412 P2n = parents[1].rev()
413 # interesting second parent
413 # interesting second parent
414 if P2n in state:
414 if P2n in state:
415 if p1 == target: # P1n in targetancestors or external
415 if p1 == target: # P1n in targetancestors or external
416 p1 = state[P2n]
416 p1 = state[P2n]
417 else:
417 else:
418 p2 = state[P2n]
418 p2 = state[P2n]
419 else: # P2n external
419 else: # P2n external
420 if p2 != nullrev: # P1n external too => rev is a merged revision
420 if p2 != nullrev: # P1n external too => rev is a merged revision
421 raise util.Abort(_('cannot use revision %d as base, result '
421 raise util.Abort(_('cannot use revision %d as base, result '
422 'would have 3 parents') % rev)
422 'would have 3 parents') % rev)
423 p2 = P2n
423 p2 = P2n
424 repo.ui.debug(" future parents are %d and %d\n" %
424 repo.ui.debug(" future parents are %d and %d\n" %
425 (repo[p1].rev(), repo[p2].rev()))
425 (repo[p1].rev(), repo[p2].rev()))
426 return p1, p2
426 return p1, p2
427
427
428 def isagitpatch(repo, patchname):
428 def isagitpatch(repo, patchname):
429 'Return true if the given patch is in git format'
429 'Return true if the given patch is in git format'
430 mqpatch = os.path.join(repo.mq.path, patchname)
430 mqpatch = os.path.join(repo.mq.path, patchname)
431 for line in patch.linereader(file(mqpatch, 'rb')):
431 for line in patch.linereader(file(mqpatch, 'rb')):
432 if line.startswith('diff --git'):
432 if line.startswith('diff --git'):
433 return True
433 return True
434 return False
434 return False
435
435
436 def updatemq(repo, state, skipped, **opts):
436 def updatemq(repo, state, skipped, **opts):
437 'Update rebased mq patches - finalize and then import them'
437 'Update rebased mq patches - finalize and then import them'
438 mqrebase = {}
438 mqrebase = {}
439 mq = repo.mq
439 mq = repo.mq
440 original_series = mq.fullseries[:]
440 original_series = mq.fullseries[:]
441 skippedpatches = set()
441 skippedpatches = set()
442
442
443 for p in mq.applied:
443 for p in mq.applied:
444 rev = repo[p.node].rev()
444 rev = repo[p.node].rev()
445 if rev in state:
445 if rev in state:
446 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
446 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
447 (rev, p.name))
447 (rev, p.name))
448 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
448 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
449 else:
449 else:
450 # Applied but not rebased, not sure this should happen
450 # Applied but not rebased, not sure this should happen
451 skippedpatches.add(p.name)
451 skippedpatches.add(p.name)
452
452
453 if mqrebase:
453 if mqrebase:
454 mq.finish(repo, mqrebase.keys())
454 mq.finish(repo, mqrebase.keys())
455
455
456 # We must start import from the newest revision
456 # We must start import from the newest revision
457 for rev in sorted(mqrebase, reverse=True):
457 for rev in sorted(mqrebase, reverse=True):
458 if rev not in skipped:
458 if rev not in skipped:
459 name, isgit = mqrebase[rev]
459 name, isgit = mqrebase[rev]
460 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
460 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
461 mq.qimport(repo, (), patchname=name, git=isgit,
461 mq.qimport(repo, (), patchname=name, git=isgit,
462 rev=[str(state[rev])])
462 rev=[str(state[rev])])
463 else:
463 else:
464 # Rebased and skipped
464 # Rebased and skipped
465 skippedpatches.add(mqrebase[rev][0])
465 skippedpatches.add(mqrebase[rev][0])
466
466
467 # Patches were either applied and rebased and imported in
467 # Patches were either applied and rebased and imported in
468 # order, applied and removed or unapplied. Discard the removed
468 # order, applied and removed or unapplied. Discard the removed
469 # ones while preserving the original series order and guards.
469 # ones while preserving the original series order and guards.
470 newseries = [s for s in original_series
470 newseries = [s for s in original_series
471 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
471 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
472 mq.fullseries[:] = newseries
472 mq.fullseries[:] = newseries
473 mq.seriesdirty = True
473 mq.seriesdirty = True
474 mq.savedirty()
474 mq.savedirty()
475
475
476 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
476 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
477 'Move bookmarks to their correct changesets'
477 'Move bookmarks to their correct changesets'
478 for k, v in originalbookmarks.iteritems():
478 for k, v in originalbookmarks.iteritems():
479 if v in nstate:
479 if v in nstate:
480 if nstate[v] != nullmerge:
480 if nstate[v] != nullmerge:
481 # update the bookmarks for revs that have moved
481 # update the bookmarks for revs that have moved
482 repo._bookmarks[k] = nstate[v]
482 repo._bookmarks[k] = nstate[v]
483
483
484 bookmarks.write(repo)
484 bookmarks.write(repo)
485
485
486 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
486 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
487 external):
487 external):
488 'Store the current status to allow recovery'
488 'Store the current status to allow recovery'
489 f = repo.opener("rebasestate", "w")
489 f = repo.opener("rebasestate", "w")
490 f.write(repo[originalwd].hex() + '\n')
490 f.write(repo[originalwd].hex() + '\n')
491 f.write(repo[target].hex() + '\n')
491 f.write(repo[target].hex() + '\n')
492 f.write(repo[external].hex() + '\n')
492 f.write(repo[external].hex() + '\n')
493 f.write('%d\n' % int(collapse))
493 f.write('%d\n' % int(collapse))
494 f.write('%d\n' % int(keep))
494 f.write('%d\n' % int(keep))
495 f.write('%d\n' % int(keepbranches))
495 f.write('%d\n' % int(keepbranches))
496 for d, v in state.iteritems():
496 for d, v in state.iteritems():
497 oldrev = repo[d].hex()
497 oldrev = repo[d].hex()
498 if v != nullmerge:
498 if v != nullmerge:
499 newrev = repo[v].hex()
499 newrev = repo[v].hex()
500 else:
500 else:
501 newrev = v
501 newrev = v
502 f.write("%s:%s\n" % (oldrev, newrev))
502 f.write("%s:%s\n" % (oldrev, newrev))
503 f.close()
503 f.close()
504 repo.ui.debug('rebase status stored\n')
504 repo.ui.debug('rebase status stored\n')
505
505
506 def clearstatus(repo):
506 def clearstatus(repo):
507 'Remove the status files'
507 'Remove the status files'
508 if os.path.exists(repo.join("rebasestate")):
508 if os.path.exists(repo.join("rebasestate")):
509 util.unlinkpath(repo.join("rebasestate"))
509 util.unlinkpath(repo.join("rebasestate"))
510
510
511 def restorestatus(repo):
511 def restorestatus(repo):
512 'Restore a previously stored status'
512 'Restore a previously stored status'
513 try:
513 try:
514 target = None
514 target = None
515 collapse = False
515 collapse = False
516 external = nullrev
516 external = nullrev
517 state = {}
517 state = {}
518 f = repo.opener("rebasestate")
518 f = repo.opener("rebasestate")
519 for i, l in enumerate(f.read().splitlines()):
519 for i, l in enumerate(f.read().splitlines()):
520 if i == 0:
520 if i == 0:
521 originalwd = repo[l].rev()
521 originalwd = repo[l].rev()
522 elif i == 1:
522 elif i == 1:
523 target = repo[l].rev()
523 target = repo[l].rev()
524 elif i == 2:
524 elif i == 2:
525 external = repo[l].rev()
525 external = repo[l].rev()
526 elif i == 3:
526 elif i == 3:
527 collapse = bool(int(l))
527 collapse = bool(int(l))
528 elif i == 4:
528 elif i == 4:
529 keep = bool(int(l))
529 keep = bool(int(l))
530 elif i == 5:
530 elif i == 5:
531 keepbranches = bool(int(l))
531 keepbranches = bool(int(l))
532 else:
532 else:
533 oldrev, newrev = l.split(':')
533 oldrev, newrev = l.split(':')
534 if newrev != str(nullmerge):
534 if newrev != str(nullmerge):
535 state[repo[oldrev].rev()] = repo[newrev].rev()
535 state[repo[oldrev].rev()] = repo[newrev].rev()
536 else:
536 else:
537 state[repo[oldrev].rev()] = int(newrev)
537 state[repo[oldrev].rev()] = int(newrev)
538 skipped = set()
538 skipped = set()
539 # recompute the set of skipped revs
539 # recompute the set of skipped revs
540 if not collapse:
540 if not collapse:
541 seen = set([target])
541 seen = set([target])
542 for old, new in sorted(state.items()):
542 for old, new in sorted(state.items()):
543 if new != nullrev and new in seen:
543 if new != nullrev and new in seen:
544 skipped.add(old)
544 skipped.add(old)
545 seen.add(new)
545 seen.add(new)
546 repo.ui.debug('computed skipped revs: %s\n' % skipped)
546 repo.ui.debug('computed skipped revs: %s\n' % skipped)
547 repo.ui.debug('rebase status resumed\n')
547 repo.ui.debug('rebase status resumed\n')
548 return (originalwd, target, state, skipped,
548 return (originalwd, target, state, skipped,
549 collapse, keep, keepbranches, external)
549 collapse, keep, keepbranches, external)
550 except IOError, err:
550 except IOError, err:
551 if err.errno != errno.ENOENT:
551 if err.errno != errno.ENOENT:
552 raise
552 raise
553 raise util.Abort(_('no rebase in progress'))
553 raise util.Abort(_('no rebase in progress'))
554
554
555 def abort(repo, originalwd, target, state):
555 def abort(repo, originalwd, target, state):
556 'Restore the repository to its original state'
556 'Restore the repository to its original state'
557 dstates = [s for s in state.values() if s != nullrev]
557 dstates = [s for s in state.values() if s != nullrev]
558 immutable = [d for d in dstates if not repo[d].mutable()]
558 immutable = [d for d in dstates if not repo[d].mutable()]
559 if immutable:
559 if immutable:
560 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
560 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
561 % ', '.join(str(repo[r]) for r in immutable),
561 % ', '.join(str(repo[r]) for r in immutable),
562 hint=_('see hg help phases for details'))
562 hint=_('see hg help phases for details'))
563
563
564 descendants = set()
564 descendants = set()
565 if dstates:
565 if dstates:
566 descendants = set(repo.changelog.descendants(dstates))
566 descendants = set(repo.changelog.descendants(dstates))
567 if descendants - set(dstates):
567 if descendants - set(dstates):
568 repo.ui.warn(_("warning: new changesets detected on target branch, "
568 repo.ui.warn(_("warning: new changesets detected on target branch, "
569 "can't abort\n"))
569 "can't abort\n"))
570 return -1
570 return -1
571 else:
571 else:
572 # Strip from the first rebased revision
572 # Strip from the first rebased revision
573 merge.update(repo, repo[originalwd].rev(), False, True, False)
573 merge.update(repo, repo[originalwd].rev(), False, True, False)
574 rebased = filter(lambda x: x > -1 and x != target, state.values())
574 rebased = filter(lambda x: x > -1 and x != target, state.values())
575 if rebased:
575 if rebased:
576 strippoint = min(rebased)
576 strippoint = min(rebased)
577 # no backup of rebased cset versions needed
577 # no backup of rebased cset versions needed
578 repair.strip(repo.ui, repo, repo[strippoint].node())
578 repair.strip(repo.ui, repo, repo[strippoint].node())
579 clearstatus(repo)
579 clearstatus(repo)
580 repo.ui.warn(_('rebase aborted\n'))
580 repo.ui.warn(_('rebase aborted\n'))
581 return 0
581 return 0
582
582
583 def buildstate(repo, dest, rebaseset, collapse):
583 def buildstate(repo, dest, rebaseset, collapse):
584 '''Define which revisions are going to be rebased and where
584 '''Define which revisions are going to be rebased and where
585
585
586 repo: repo
586 repo: repo
587 dest: context
587 dest: context
588 rebaseset: set of rev
588 rebaseset: set of rev
589 '''
589 '''
590
590
591 # This check isn't strictly necessary, since mq detects commits over an
591 # This check isn't strictly necessary, since mq detects commits over an
592 # applied patch. But it prevents messing up the working directory when
592 # applied patch. But it prevents messing up the working directory when
593 # a partially completed rebase is blocked by mq.
593 # a partially completed rebase is blocked by mq.
594 if 'qtip' in repo.tags() and (dest.node() in
594 if 'qtip' in repo.tags() and (dest.node() in
595 [s.node for s in repo.mq.applied]):
595 [s.node for s in repo.mq.applied]):
596 raise util.Abort(_('cannot rebase onto an applied mq patch'))
596 raise util.Abort(_('cannot rebase onto an applied mq patch'))
597
597
598 roots = list(repo.set('roots(%ld)', rebaseset))
598 roots = list(repo.set('roots(%ld)', rebaseset))
599 if not roots:
599 if not roots:
600 raise util.Abort(_('no matching revisions'))
600 raise util.Abort(_('no matching revisions'))
601 if len(roots) > 1:
601 if len(roots) > 1:
602 raise util.Abort(_("can't rebase multiple roots"))
602 raise util.Abort(_("can't rebase multiple roots"))
603 root = roots[0]
603 root = roots[0]
604
604
605 commonbase = root.ancestor(dest)
605 commonbase = root.ancestor(dest)
606 if commonbase == root:
606 if commonbase == root:
607 raise util.Abort(_('source is ancestor of destination'))
607 raise util.Abort(_('source is ancestor of destination'))
608 if commonbase == dest:
608 if commonbase == dest:
609 samebranch = root.branch() == dest.branch()
609 samebranch = root.branch() == dest.branch()
610 if not collapse and samebranch and root in dest.children():
610 if not collapse and samebranch and root in dest.children():
611 repo.ui.debug('source is a child of destination\n')
611 repo.ui.debug('source is a child of destination\n')
612 return None
612 return None
613
613
614 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
614 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
615 state = dict.fromkeys(rebaseset, nullrev)
615 state = dict.fromkeys(rebaseset, nullrev)
616 # Rebase tries to turn <dest> into a parent of <root> while
616 # Rebase tries to turn <dest> into a parent of <root> while
617 # preserving the number of parents of rebased changesets:
617 # preserving the number of parents of rebased changesets:
618 #
618 #
619 # - A changeset with a single parent will always be rebased as a
619 # - A changeset with a single parent will always be rebased as a
620 # changeset with a single parent.
620 # changeset with a single parent.
621 #
621 #
622 # - A merge will be rebased as merge unless its parents are both
622 # - A merge will be rebased as merge unless its parents are both
623 # ancestors of <dest> or are themselves in the rebased set and
623 # ancestors of <dest> or are themselves in the rebased set and
624 # pruned while rebased.
624 # pruned while rebased.
625 #
625 #
626 # If one parent of <root> is an ancestor of <dest>, the rebased
626 # If one parent of <root> is an ancestor of <dest>, the rebased
627 # version of this parent will be <dest>. This is always true with
627 # version of this parent will be <dest>. This is always true with
628 # --base option.
628 # --base option.
629 #
629 #
630 # Otherwise, we need to *replace* the original parents with
630 # Otherwise, we need to *replace* the original parents with
631 # <dest>. This "detaches" the rebased set from its former location
631 # <dest>. This "detaches" the rebased set from its former location
632 # and rebases it onto <dest>. Changes introduced by ancestors of
632 # and rebases it onto <dest>. Changes introduced by ancestors of
633 # <root> not common with <dest> (the detachset, marked as
633 # <root> not common with <dest> (the detachset, marked as
634 # nullmerge) are "removed" from the rebased changesets.
634 # nullmerge) are "removed" from the rebased changesets.
635 #
635 #
636 # - If <root> has a single parent, set it to <dest>.
636 # - If <root> has a single parent, set it to <dest>.
637 #
637 #
638 # - If <root> is a merge, we cannot decide which parent to
638 # - If <root> is a merge, we cannot decide which parent to
639 # replace, the rebase operation is not clearly defined.
639 # replace, the rebase operation is not clearly defined.
640 #
640 #
641 # The table below sums up this behavior:
641 # The table below sums up this behavior:
642 #
642 #
643 # +--------------------+----------------------+-------------------------+
643 # +--------------------+----------------------+-------------------------+
644 # | | one parent | merge |
644 # | | one parent | merge |
645 # +--------------------+----------------------+-------------------------+
645 # +--------------------+----------------------+-------------------------+
646 # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
646 # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
647 # | | | remapped to <dest> |
647 # | | | remapped to <dest> |
648 # +--------------------+----------------------+-------------------------+
648 # +--------------------+----------------------+-------------------------+
649 # | unrelated source | new parent is <dest> | ambiguous, abort |
649 # | unrelated source | new parent is <dest> | ambiguous, abort |
650 # +--------------------+----------------------+-------------------------+
650 # +--------------------+----------------------+-------------------------+
651 #
651 #
652 # The actual abort is handled by `defineparents`
652 # The actual abort is handled by `defineparents`
653 if len(root.parents()) <= 1:
653 if len(root.parents()) <= 1:
654 # (strict) ancestors of <root> not ancestors of <dest>
654 # (strict) ancestors of <root> not ancestors of <dest>
655 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
655 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
656 state.update(dict.fromkeys(detachset, nullmerge))
656 state.update(dict.fromkeys(detachset, nullmerge))
657 return repo['.'].rev(), dest.rev(), state
657 return repo['.'].rev(), dest.rev(), state
658
658
659 def clearrebased(ui, repo, state):
659 def clearrebased(ui, repo, state):
660 """dispose of rebased revision at the end of the rebase"""
660 """dispose of rebased revision at the end of the rebase"""
661 rebased = [rev for rev in state if state[rev] != nullmerge]
661 if obsolete._enabled:
662 if rebased:
662 markers = []
663 if set(repo.changelog.descendants([min(rebased)])) - set(state):
663 for rev, newrev in sorted(state.items()):
664 ui.warn(_("warning: new changesets detected "
664 if newrev >= 0:
665 "on source branch, not stripping\n"))
665 markers.append((repo[rev], (repo[newrev],)))
666 else:
666 if markers:
667 # backup the old csets by default
667 obsolete.createmarkers(repo, markers)
668 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
668 else:
669 rebased = [rev for rev in state if state[rev] != nullmerge]
670 if rebased:
671 if set(repo.changelog.descendants([min(rebased)])) - set(state):
672 ui.warn(_("warning: new changesets detected "
673 "on source branch, not stripping\n"))
674 else:
675 # backup the old csets by default
676 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
669
677
670
678
671 def pullrebase(orig, ui, repo, *args, **opts):
679 def pullrebase(orig, ui, repo, *args, **opts):
672 'Call rebase after pull if the latter has been invoked with --rebase'
680 'Call rebase after pull if the latter has been invoked with --rebase'
673 if opts.get('rebase'):
681 if opts.get('rebase'):
674 if opts.get('update'):
682 if opts.get('update'):
675 del opts['update']
683 del opts['update']
676 ui.debug('--update and --rebase are not compatible, ignoring '
684 ui.debug('--update and --rebase are not compatible, ignoring '
677 'the update flag\n')
685 'the update flag\n')
678
686
679 movemarkfrom = repo['.'].node()
687 movemarkfrom = repo['.'].node()
680 cmdutil.bailifchanged(repo)
688 cmdutil.bailifchanged(repo)
681 revsprepull = len(repo)
689 revsprepull = len(repo)
682 origpostincoming = commands.postincoming
690 origpostincoming = commands.postincoming
683 def _dummy(*args, **kwargs):
691 def _dummy(*args, **kwargs):
684 pass
692 pass
685 commands.postincoming = _dummy
693 commands.postincoming = _dummy
686 try:
694 try:
687 orig(ui, repo, *args, **opts)
695 orig(ui, repo, *args, **opts)
688 finally:
696 finally:
689 commands.postincoming = origpostincoming
697 commands.postincoming = origpostincoming
690 revspostpull = len(repo)
698 revspostpull = len(repo)
691 if revspostpull > revsprepull:
699 if revspostpull > revsprepull:
692 rebase(ui, repo, **opts)
700 rebase(ui, repo, **opts)
693 branch = repo[None].branch()
701 branch = repo[None].branch()
694 dest = repo[branch].rev()
702 dest = repo[branch].rev()
695 if dest != repo['.'].rev():
703 if dest != repo['.'].rev():
696 # there was nothing to rebase we force an update
704 # there was nothing to rebase we force an update
697 hg.update(repo, dest)
705 hg.update(repo, dest)
698 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
706 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
699 ui.status(_("updating bookmark %s\n")
707 ui.status(_("updating bookmark %s\n")
700 % repo._bookmarkcurrent)
708 % repo._bookmarkcurrent)
701 else:
709 else:
702 if opts.get('tool'):
710 if opts.get('tool'):
703 raise util.Abort(_('--tool can only be used with --rebase'))
711 raise util.Abort(_('--tool can only be used with --rebase'))
704 orig(ui, repo, *args, **opts)
712 orig(ui, repo, *args, **opts)
705
713
706 def uisetup(ui):
714 def uisetup(ui):
707 'Replace pull with a decorator to provide --rebase option'
715 'Replace pull with a decorator to provide --rebase option'
708 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
716 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
709 entry[1].append(('', 'rebase', None,
717 entry[1].append(('', 'rebase', None,
710 _("rebase working directory to branch head")))
718 _("rebase working directory to branch head")))
711 entry[1].append(('t', 'tool', '',
719 entry[1].append(('t', 'tool', '',
712 _("specify merge tool for rebase")))
720 _("specify merge tool for rebase")))
General Comments 0
You need to be logged in to leave comments. Login now