##// END OF EJS Templates
rebase: fix bug that caused transitive copy records to disappear (issue4192)...
Augie Fackler -
r21826:2ba6c9b4 stable
parent child Browse files
Show More
@@ -1,962 +1,971 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, obsolete, error
18 from mercurial import extensions, patch, scmutil, phases, obsolete, error
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 revignored = -3
26 revignored = -3
27
27
28 cmdtable = {}
28 cmdtable = {}
29 command = cmdutil.command(cmdtable)
29 command = cmdutil.command(cmdtable)
30 testedwith = 'internal'
30 testedwith = 'internal'
31
31
32 def _savegraft(ctx, extra):
32 def _savegraft(ctx, extra):
33 s = ctx.extra().get('source', None)
33 s = ctx.extra().get('source', None)
34 if s is not None:
34 if s is not None:
35 extra['source'] = s
35 extra['source'] = s
36
36
37 def _savebranch(ctx, extra):
37 def _savebranch(ctx, extra):
38 extra['branch'] = ctx.branch()
38 extra['branch'] = ctx.branch()
39
39
40 def _makeextrafn(copiers):
40 def _makeextrafn(copiers):
41 """make an extrafn out of the given copy-functions.
41 """make an extrafn out of the given copy-functions.
42
42
43 A copy function takes a context and an extra dict, and mutates the
43 A copy function takes a context and an extra dict, and mutates the
44 extra dict as needed based on the given context.
44 extra dict as needed based on the given context.
45 """
45 """
46 def extrafn(ctx, extra):
46 def extrafn(ctx, extra):
47 for c in copiers:
47 for c in copiers:
48 c(ctx, extra)
48 c(ctx, extra)
49 return extrafn
49 return extrafn
50
50
51 @command('rebase',
51 @command('rebase',
52 [('s', 'source', '',
52 [('s', 'source', '',
53 _('rebase from the specified changeset'), _('REV')),
53 _('rebase from the specified changeset'), _('REV')),
54 ('b', 'base', '',
54 ('b', 'base', '',
55 _('rebase from the base of the specified changeset '
55 _('rebase from the base of the specified changeset '
56 '(up to greatest common ancestor of base and dest)'),
56 '(up to greatest common ancestor of base and dest)'),
57 _('REV')),
57 _('REV')),
58 ('r', 'rev', [],
58 ('r', 'rev', [],
59 _('rebase these revisions'),
59 _('rebase these revisions'),
60 _('REV')),
60 _('REV')),
61 ('d', 'dest', '',
61 ('d', 'dest', '',
62 _('rebase onto the specified changeset'), _('REV')),
62 _('rebase onto the specified changeset'), _('REV')),
63 ('', 'collapse', False, _('collapse the rebased changesets')),
63 ('', 'collapse', False, _('collapse the rebased changesets')),
64 ('m', 'message', '',
64 ('m', 'message', '',
65 _('use text as collapse commit message'), _('TEXT')),
65 _('use text as collapse commit message'), _('TEXT')),
66 ('e', 'edit', False, _('invoke editor on commit messages')),
66 ('e', 'edit', False, _('invoke editor on commit messages')),
67 ('l', 'logfile', '',
67 ('l', 'logfile', '',
68 _('read collapse commit message from file'), _('FILE')),
68 _('read collapse commit message from file'), _('FILE')),
69 ('', 'keep', False, _('keep original changesets')),
69 ('', 'keep', False, _('keep original changesets')),
70 ('', 'keepbranches', False, _('keep original branch names')),
70 ('', 'keepbranches', False, _('keep original branch names')),
71 ('D', 'detach', False, _('(DEPRECATED)')),
71 ('D', 'detach', False, _('(DEPRECATED)')),
72 ('t', 'tool', '', _('specify merge tool')),
72 ('t', 'tool', '', _('specify merge tool')),
73 ('c', 'continue', False, _('continue an interrupted rebase')),
73 ('c', 'continue', False, _('continue an interrupted rebase')),
74 ('a', 'abort', False, _('abort an interrupted rebase'))] +
74 ('a', 'abort', False, _('abort an interrupted rebase'))] +
75 templateopts,
75 templateopts,
76 _('[-s REV | -b REV] [-d REV] [OPTION]'))
76 _('[-s REV | -b REV] [-d REV] [OPTION]'))
77 def rebase(ui, repo, **opts):
77 def rebase(ui, repo, **opts):
78 """move changeset (and descendants) to a different branch
78 """move changeset (and descendants) to a different branch
79
79
80 Rebase uses repeated merging to graft changesets from one part of
80 Rebase uses repeated merging to graft changesets from one part of
81 history (the source) onto another (the destination). This can be
81 history (the source) onto another (the destination). This can be
82 useful for linearizing *local* changes relative to a master
82 useful for linearizing *local* changes relative to a master
83 development tree.
83 development tree.
84
84
85 You should not rebase changesets that have already been shared
85 You should not rebase changesets that have already been shared
86 with others. Doing so will force everybody else to perform the
86 with others. Doing so will force everybody else to perform the
87 same rebase or they will end up with duplicated changesets after
87 same rebase or they will end up with duplicated changesets after
88 pulling in your rebased changesets.
88 pulling in your rebased changesets.
89
89
90 In its default configuration, Mercurial will prevent you from
90 In its default configuration, Mercurial will prevent you from
91 rebasing published changes. See :hg:`help phases` for details.
91 rebasing published changes. See :hg:`help phases` for details.
92
92
93 If you don't specify a destination changeset (``-d/--dest``),
93 If you don't specify a destination changeset (``-d/--dest``),
94 rebase uses the current branch tip as the destination. (The
94 rebase uses the current branch tip as the destination. (The
95 destination changeset is not modified by rebasing, but new
95 destination changeset is not modified by rebasing, but new
96 changesets are added as its descendants.)
96 changesets are added as its descendants.)
97
97
98 You can specify which changesets to rebase in two ways: as a
98 You can specify which changesets to rebase in two ways: as a
99 "source" changeset or as a "base" changeset. Both are shorthand
99 "source" changeset or as a "base" changeset. Both are shorthand
100 for a topologically related set of changesets (the "source
100 for a topologically related set of changesets (the "source
101 branch"). If you specify source (``-s/--source``), rebase will
101 branch"). If you specify source (``-s/--source``), rebase will
102 rebase that changeset and all of its descendants onto dest. If you
102 rebase that changeset and all of its descendants onto dest. If you
103 specify base (``-b/--base``), rebase will select ancestors of base
103 specify base (``-b/--base``), rebase will select ancestors of base
104 back to but not including the common ancestor with dest. Thus,
104 back to but not including the common ancestor with dest. Thus,
105 ``-b`` is less precise but more convenient than ``-s``: you can
105 ``-b`` is less precise but more convenient than ``-s``: you can
106 specify any changeset in the source branch, and rebase will select
106 specify any changeset in the source branch, and rebase will select
107 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
107 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
108 uses the parent of the working directory as the base.
108 uses the parent of the working directory as the base.
109
109
110 For advanced usage, a third way is available through the ``--rev``
110 For advanced usage, a third way is available through the ``--rev``
111 option. It allows you to specify an arbitrary set of changesets to
111 option. It allows you to specify an arbitrary set of changesets to
112 rebase. Descendants of revs you specify with this option are not
112 rebase. Descendants of revs you specify with this option are not
113 automatically included in the rebase.
113 automatically included in the rebase.
114
114
115 By default, rebase recreates the changesets in the source branch
115 By default, rebase recreates the changesets in the source branch
116 as descendants of dest and then destroys the originals. Use
116 as descendants of dest and then destroys the originals. Use
117 ``--keep`` to preserve the original source changesets. Some
117 ``--keep`` to preserve the original source changesets. Some
118 changesets in the source branch (e.g. merges from the destination
118 changesets in the source branch (e.g. merges from the destination
119 branch) may be dropped if they no longer contribute any change.
119 branch) may be dropped if they no longer contribute any change.
120
120
121 One result of the rules for selecting the destination changeset
121 One result of the rules for selecting the destination changeset
122 and source branch is that, unlike ``merge``, rebase will do
122 and source branch is that, unlike ``merge``, rebase will do
123 nothing if you are at the branch tip of a named branch
123 nothing if you are at the branch tip of a named branch
124 with two heads. You need to explicitly specify source and/or
124 with two heads. You need to explicitly specify source and/or
125 destination (or ``update`` to the other head, if it's the head of
125 destination (or ``update`` to the other head, if it's the head of
126 the intended source branch).
126 the intended source branch).
127
127
128 If a rebase is interrupted to manually resolve a merge, it can be
128 If a rebase is interrupted to manually resolve a merge, it can be
129 continued with --continue/-c or aborted with --abort/-a.
129 continued with --continue/-c or aborted with --abort/-a.
130
130
131 Returns 0 on success, 1 if nothing to rebase or there are
131 Returns 0 on success, 1 if nothing to rebase or there are
132 unresolved conflicts.
132 unresolved conflicts.
133 """
133 """
134 originalwd = target = None
134 originalwd = target = None
135 activebookmark = None
135 activebookmark = None
136 external = nullrev
136 external = nullrev
137 state = {}
137 state = {}
138 skipped = set()
138 skipped = set()
139 targetancestors = set()
139 targetancestors = set()
140
140
141 editor = None
141 editor = None
142 if opts.get('edit'):
142 if opts.get('edit'):
143 editor = cmdutil.commitforceeditor
143 editor = cmdutil.commitforceeditor
144
144
145 lock = wlock = None
145 lock = wlock = None
146 try:
146 try:
147 wlock = repo.wlock()
147 wlock = repo.wlock()
148 lock = repo.lock()
148 lock = repo.lock()
149
149
150 # Validate input and define rebasing points
150 # Validate input and define rebasing points
151 destf = opts.get('dest', None)
151 destf = opts.get('dest', None)
152 srcf = opts.get('source', None)
152 srcf = opts.get('source', None)
153 basef = opts.get('base', None)
153 basef = opts.get('base', None)
154 revf = opts.get('rev', [])
154 revf = opts.get('rev', [])
155 contf = opts.get('continue')
155 contf = opts.get('continue')
156 abortf = opts.get('abort')
156 abortf = opts.get('abort')
157 collapsef = opts.get('collapse', False)
157 collapsef = opts.get('collapse', False)
158 collapsemsg = cmdutil.logmessage(ui, opts)
158 collapsemsg = cmdutil.logmessage(ui, opts)
159 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
159 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
160 extrafns = [_savegraft]
160 extrafns = [_savegraft]
161 if e:
161 if e:
162 extrafns = [e]
162 extrafns = [e]
163 keepf = opts.get('keep', False)
163 keepf = opts.get('keep', False)
164 keepbranchesf = opts.get('keepbranches', False)
164 keepbranchesf = opts.get('keepbranches', False)
165 # keepopen is not meant for use on the command line, but by
165 # keepopen is not meant for use on the command line, but by
166 # other extensions
166 # other extensions
167 keepopen = opts.get('keepopen', False)
167 keepopen = opts.get('keepopen', False)
168
168
169 if collapsemsg and not collapsef:
169 if collapsemsg and not collapsef:
170 raise util.Abort(
170 raise util.Abort(
171 _('message can only be specified with collapse'))
171 _('message can only be specified with collapse'))
172
172
173 if contf or abortf:
173 if contf or abortf:
174 if contf and abortf:
174 if contf and abortf:
175 raise util.Abort(_('cannot use both abort and continue'))
175 raise util.Abort(_('cannot use both abort and continue'))
176 if collapsef:
176 if collapsef:
177 raise util.Abort(
177 raise util.Abort(
178 _('cannot use collapse with continue or abort'))
178 _('cannot use collapse with continue or abort'))
179 if srcf or basef or destf:
179 if srcf or basef or destf:
180 raise util.Abort(
180 raise util.Abort(
181 _('abort and continue do not allow specifying revisions'))
181 _('abort and continue do not allow specifying revisions'))
182 if opts.get('tool', False):
182 if opts.get('tool', False):
183 ui.warn(_('tool option will be ignored\n'))
183 ui.warn(_('tool option will be ignored\n'))
184
184
185 try:
185 try:
186 (originalwd, target, state, skipped, collapsef, keepf,
186 (originalwd, target, state, skipped, collapsef, keepf,
187 keepbranchesf, external, activebookmark) = restorestatus(repo)
187 keepbranchesf, external, activebookmark) = restorestatus(repo)
188 except error.RepoLookupError:
188 except error.RepoLookupError:
189 if abortf:
189 if abortf:
190 clearstatus(repo)
190 clearstatus(repo)
191 repo.ui.warn(_('rebase aborted (no revision is removed,'
191 repo.ui.warn(_('rebase aborted (no revision is removed,'
192 ' only broken state is cleared)\n'))
192 ' only broken state is cleared)\n'))
193 return 0
193 return 0
194 else:
194 else:
195 msg = _('cannot continue inconsistent rebase')
195 msg = _('cannot continue inconsistent rebase')
196 hint = _('use "hg rebase --abort" to clear broken state')
196 hint = _('use "hg rebase --abort" to clear broken state')
197 raise util.Abort(msg, hint=hint)
197 raise util.Abort(msg, hint=hint)
198 if abortf:
198 if abortf:
199 return abort(repo, originalwd, target, state)
199 return abort(repo, originalwd, target, state)
200 else:
200 else:
201 if srcf and basef:
201 if srcf and basef:
202 raise util.Abort(_('cannot specify both a '
202 raise util.Abort(_('cannot specify both a '
203 'source and a base'))
203 'source and a base'))
204 if revf and basef:
204 if revf and basef:
205 raise util.Abort(_('cannot specify both a '
205 raise util.Abort(_('cannot specify both a '
206 'revision and a base'))
206 'revision and a base'))
207 if revf and srcf:
207 if revf and srcf:
208 raise util.Abort(_('cannot specify both a '
208 raise util.Abort(_('cannot specify both a '
209 'revision and a source'))
209 'revision and a source'))
210
210
211 cmdutil.checkunfinished(repo)
211 cmdutil.checkunfinished(repo)
212 cmdutil.bailifchanged(repo)
212 cmdutil.bailifchanged(repo)
213
213
214 if not destf:
214 if not destf:
215 # Destination defaults to the latest revision in the
215 # Destination defaults to the latest revision in the
216 # current branch
216 # current branch
217 branch = repo[None].branch()
217 branch = repo[None].branch()
218 dest = repo[branch]
218 dest = repo[branch]
219 else:
219 else:
220 dest = scmutil.revsingle(repo, destf)
220 dest = scmutil.revsingle(repo, destf)
221
221
222 if revf:
222 if revf:
223 rebaseset = scmutil.revrange(repo, revf)
223 rebaseset = scmutil.revrange(repo, revf)
224 if not rebaseset:
224 if not rebaseset:
225 ui.status(_('empty "rev" revision set - '
225 ui.status(_('empty "rev" revision set - '
226 'nothing to rebase\n'))
226 'nothing to rebase\n'))
227 return 1
227 return 1
228 elif srcf:
228 elif srcf:
229 src = scmutil.revrange(repo, [srcf])
229 src = scmutil.revrange(repo, [srcf])
230 if not src:
230 if not src:
231 ui.status(_('empty "source" revision set - '
231 ui.status(_('empty "source" revision set - '
232 'nothing to rebase\n'))
232 'nothing to rebase\n'))
233 return 1
233 return 1
234 rebaseset = repo.revs('(%ld)::', src)
234 rebaseset = repo.revs('(%ld)::', src)
235 assert rebaseset
235 assert rebaseset
236 else:
236 else:
237 base = scmutil.revrange(repo, [basef or '.'])
237 base = scmutil.revrange(repo, [basef or '.'])
238 if not base:
238 if not base:
239 ui.status(_('empty "base" revision set - '
239 ui.status(_('empty "base" revision set - '
240 "can't compute rebase set\n"))
240 "can't compute rebase set\n"))
241 return 1
241 return 1
242 rebaseset = repo.revs(
242 rebaseset = repo.revs(
243 '(children(ancestor(%ld, %d)) and ::(%ld))::',
243 '(children(ancestor(%ld, %d)) and ::(%ld))::',
244 base, dest, base)
244 base, dest, base)
245 if not rebaseset:
245 if not rebaseset:
246 if base == [dest.rev()]:
246 if base == [dest.rev()]:
247 if basef:
247 if basef:
248 ui.status(_('nothing to rebase - %s is both "base"'
248 ui.status(_('nothing to rebase - %s is both "base"'
249 ' and destination\n') % dest)
249 ' and destination\n') % dest)
250 else:
250 else:
251 ui.status(_('nothing to rebase - working directory '
251 ui.status(_('nothing to rebase - working directory '
252 'parent is also destination\n'))
252 'parent is also destination\n'))
253 elif not repo.revs('%ld - ::%d', base, dest):
253 elif not repo.revs('%ld - ::%d', base, dest):
254 if basef:
254 if basef:
255 ui.status(_('nothing to rebase - "base" %s is '
255 ui.status(_('nothing to rebase - "base" %s is '
256 'already an ancestor of destination '
256 'already an ancestor of destination '
257 '%s\n') %
257 '%s\n') %
258 ('+'.join(str(repo[r]) for r in base),
258 ('+'.join(str(repo[r]) for r in base),
259 dest))
259 dest))
260 else:
260 else:
261 ui.status(_('nothing to rebase - working '
261 ui.status(_('nothing to rebase - working '
262 'directory parent is already an '
262 'directory parent is already an '
263 'ancestor of destination %s\n') % dest)
263 'ancestor of destination %s\n') % dest)
264 else: # can it happen?
264 else: # can it happen?
265 ui.status(_('nothing to rebase from %s to %s\n') %
265 ui.status(_('nothing to rebase from %s to %s\n') %
266 ('+'.join(str(repo[r]) for r in base), dest))
266 ('+'.join(str(repo[r]) for r in base), dest))
267 return 1
267 return 1
268
268
269 if (not (keepf or obsolete._enabled)
269 if (not (keepf or obsolete._enabled)
270 and repo.revs('first(children(%ld) - %ld)',
270 and repo.revs('first(children(%ld) - %ld)',
271 rebaseset, rebaseset)):
271 rebaseset, rebaseset)):
272 raise util.Abort(
272 raise util.Abort(
273 _("can't remove original changesets with"
273 _("can't remove original changesets with"
274 " unrebased descendants"),
274 " unrebased descendants"),
275 hint=_('use --keep to keep original changesets'))
275 hint=_('use --keep to keep original changesets'))
276
276
277 result = buildstate(repo, dest, rebaseset, collapsef)
277 result = buildstate(repo, dest, rebaseset, collapsef)
278 if not result:
278 if not result:
279 # Empty state built, nothing to rebase
279 # Empty state built, nothing to rebase
280 ui.status(_('nothing to rebase\n'))
280 ui.status(_('nothing to rebase\n'))
281 return 1
281 return 1
282
282
283 root = min(rebaseset)
283 root = min(rebaseset)
284 if not keepf and not repo[root].mutable():
284 if not keepf and not repo[root].mutable():
285 raise util.Abort(_("can't rebase immutable changeset %s")
285 raise util.Abort(_("can't rebase immutable changeset %s")
286 % repo[root],
286 % repo[root],
287 hint=_('see hg help phases for details'))
287 hint=_('see hg help phases for details'))
288
288
289 originalwd, target, state = result
289 originalwd, target, state = result
290 if collapsef:
290 if collapsef:
291 targetancestors = repo.changelog.ancestors([target],
291 targetancestors = repo.changelog.ancestors([target],
292 inclusive=True)
292 inclusive=True)
293 external = externalparent(repo, state, targetancestors)
293 external = externalparent(repo, state, targetancestors)
294
294
295 if dest.closesbranch() and not keepbranchesf:
295 if dest.closesbranch() and not keepbranchesf:
296 ui.status(_('reopening closed branch head %s\n') % dest)
296 ui.status(_('reopening closed branch head %s\n') % dest)
297
297
298 if keepbranchesf:
298 if keepbranchesf:
299 # insert _savebranch at the start of extrafns so if
299 # insert _savebranch at the start of extrafns so if
300 # there's a user-provided extrafn it can clobber branch if
300 # there's a user-provided extrafn it can clobber branch if
301 # desired
301 # desired
302 extrafns.insert(0, _savebranch)
302 extrafns.insert(0, _savebranch)
303 if collapsef:
303 if collapsef:
304 branches = set()
304 branches = set()
305 for rev in state:
305 for rev in state:
306 branches.add(repo[rev].branch())
306 branches.add(repo[rev].branch())
307 if len(branches) > 1:
307 if len(branches) > 1:
308 raise util.Abort(_('cannot collapse multiple named '
308 raise util.Abort(_('cannot collapse multiple named '
309 'branches'))
309 'branches'))
310
310
311 # Rebase
311 # Rebase
312 if not targetancestors:
312 if not targetancestors:
313 targetancestors = repo.changelog.ancestors([target], inclusive=True)
313 targetancestors = repo.changelog.ancestors([target], inclusive=True)
314
314
315 # Keep track of the current bookmarks in order to reset them later
315 # Keep track of the current bookmarks in order to reset them later
316 currentbookmarks = repo._bookmarks.copy()
316 currentbookmarks = repo._bookmarks.copy()
317 activebookmark = activebookmark or repo._bookmarkcurrent
317 activebookmark = activebookmark or repo._bookmarkcurrent
318 if activebookmark:
318 if activebookmark:
319 bookmarks.unsetcurrent(repo)
319 bookmarks.unsetcurrent(repo)
320
320
321 extrafn = _makeextrafn(extrafns)
321 extrafn = _makeextrafn(extrafns)
322
322
323 sortedstate = sorted(state)
323 sortedstate = sorted(state)
324 total = len(sortedstate)
324 total = len(sortedstate)
325 pos = 0
325 pos = 0
326 for rev in sortedstate:
326 for rev in sortedstate:
327 pos += 1
327 pos += 1
328 if state[rev] == -1:
328 if state[rev] == -1:
329 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
329 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
330 _('changesets'), total)
330 _('changesets'), total)
331 p1, p2 = defineparents(repo, rev, target, state,
331 p1, p2 = defineparents(repo, rev, target, state,
332 targetancestors)
332 targetancestors)
333 storestatus(repo, originalwd, target, state, collapsef, keepf,
333 storestatus(repo, originalwd, target, state, collapsef, keepf,
334 keepbranchesf, external, activebookmark)
334 keepbranchesf, external, activebookmark)
335 if len(repo.parents()) == 2:
335 if len(repo.parents()) == 2:
336 repo.ui.debug('resuming interrupted rebase\n')
336 repo.ui.debug('resuming interrupted rebase\n')
337 else:
337 else:
338 try:
338 try:
339 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
339 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
340 'rebase')
340 'rebase')
341 stats = rebasenode(repo, rev, p1, state, collapsef)
341 stats = rebasenode(repo, rev, p1, state, collapsef)
342 if stats and stats[3] > 0:
342 if stats and stats[3] > 0:
343 raise error.InterventionRequired(
343 raise error.InterventionRequired(
344 _('unresolved conflicts (see hg '
344 _('unresolved conflicts (see hg '
345 'resolve, then hg rebase --continue)'))
345 'resolve, then hg rebase --continue)'))
346 finally:
346 finally:
347 ui.setconfig('ui', 'forcemerge', '', 'rebase')
347 ui.setconfig('ui', 'forcemerge', '', 'rebase')
348 cmdutil.duplicatecopies(repo, rev, target)
348 if collapsef:
349 cmdutil.duplicatecopies(repo, rev, target)
350 else:
351 # If we're not using --collapse, we need to
352 # duplicate copies between the revision we're
353 # rebasing and its first parent, but *not*
354 # duplicate any copies that have already been
355 # performed in the destination.
356 p1rev = repo[rev].p1().rev()
357 cmdutil.duplicatecopies(repo, rev, p1rev, skiprev=target)
349 if not collapsef:
358 if not collapsef:
350 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
359 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
351 editor=editor)
360 editor=editor)
352 else:
361 else:
353 # Skip commit if we are collapsing
362 # Skip commit if we are collapsing
354 repo.setparents(repo[p1].node())
363 repo.setparents(repo[p1].node())
355 newrev = None
364 newrev = None
356 # Update the state
365 # Update the state
357 if newrev is not None:
366 if newrev is not None:
358 state[rev] = repo[newrev].rev()
367 state[rev] = repo[newrev].rev()
359 else:
368 else:
360 if not collapsef:
369 if not collapsef:
361 ui.note(_('no changes, revision %d skipped\n') % rev)
370 ui.note(_('no changes, revision %d skipped\n') % rev)
362 ui.debug('next revision set to %s\n' % p1)
371 ui.debug('next revision set to %s\n' % p1)
363 skipped.add(rev)
372 skipped.add(rev)
364 state[rev] = p1
373 state[rev] = p1
365
374
366 ui.progress(_('rebasing'), None)
375 ui.progress(_('rebasing'), None)
367 ui.note(_('rebase merging completed\n'))
376 ui.note(_('rebase merging completed\n'))
368
377
369 if collapsef and not keepopen:
378 if collapsef and not keepopen:
370 p1, p2 = defineparents(repo, min(state), target,
379 p1, p2 = defineparents(repo, min(state), target,
371 state, targetancestors)
380 state, targetancestors)
372 if collapsemsg:
381 if collapsemsg:
373 commitmsg = collapsemsg
382 commitmsg = collapsemsg
374 else:
383 else:
375 commitmsg = 'Collapsed revision'
384 commitmsg = 'Collapsed revision'
376 for rebased in state:
385 for rebased in state:
377 if rebased not in skipped and state[rebased] > nullmerge:
386 if rebased not in skipped and state[rebased] > nullmerge:
378 commitmsg += '\n* %s' % repo[rebased].description()
387 commitmsg += '\n* %s' % repo[rebased].description()
379 editor = cmdutil.commitforceeditor
388 editor = cmdutil.commitforceeditor
380 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
389 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
381 extrafn=extrafn, editor=editor)
390 extrafn=extrafn, editor=editor)
382 for oldrev in state.iterkeys():
391 for oldrev in state.iterkeys():
383 if state[oldrev] > nullmerge:
392 if state[oldrev] > nullmerge:
384 state[oldrev] = newrev
393 state[oldrev] = newrev
385
394
386 if 'qtip' in repo.tags():
395 if 'qtip' in repo.tags():
387 updatemq(repo, state, skipped, **opts)
396 updatemq(repo, state, skipped, **opts)
388
397
389 if currentbookmarks:
398 if currentbookmarks:
390 # Nodeids are needed to reset bookmarks
399 # Nodeids are needed to reset bookmarks
391 nstate = {}
400 nstate = {}
392 for k, v in state.iteritems():
401 for k, v in state.iteritems():
393 if v > nullmerge:
402 if v > nullmerge:
394 nstate[repo[k].node()] = repo[v].node()
403 nstate[repo[k].node()] = repo[v].node()
395 # XXX this is the same as dest.node() for the non-continue path --
404 # XXX this is the same as dest.node() for the non-continue path --
396 # this should probably be cleaned up
405 # this should probably be cleaned up
397 targetnode = repo[target].node()
406 targetnode = repo[target].node()
398
407
399 # restore original working directory
408 # restore original working directory
400 # (we do this before stripping)
409 # (we do this before stripping)
401 newwd = state.get(originalwd, originalwd)
410 newwd = state.get(originalwd, originalwd)
402 if newwd not in [c.rev() for c in repo[None].parents()]:
411 if newwd not in [c.rev() for c in repo[None].parents()]:
403 ui.note(_("update back to initial working directory parent\n"))
412 ui.note(_("update back to initial working directory parent\n"))
404 hg.updaterepo(repo, newwd, False)
413 hg.updaterepo(repo, newwd, False)
405
414
406 if not keepf:
415 if not keepf:
407 collapsedas = None
416 collapsedas = None
408 if collapsef:
417 if collapsef:
409 collapsedas = newrev
418 collapsedas = newrev
410 clearrebased(ui, repo, state, skipped, collapsedas)
419 clearrebased(ui, repo, state, skipped, collapsedas)
411
420
412 if currentbookmarks:
421 if currentbookmarks:
413 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
422 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
414 if activebookmark not in repo._bookmarks:
423 if activebookmark not in repo._bookmarks:
415 # active bookmark was divergent one and has been deleted
424 # active bookmark was divergent one and has been deleted
416 activebookmark = None
425 activebookmark = None
417
426
418 clearstatus(repo)
427 clearstatus(repo)
419 ui.note(_("rebase completed\n"))
428 ui.note(_("rebase completed\n"))
420 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
429 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
421 if skipped:
430 if skipped:
422 ui.note(_("%d revisions have been skipped\n") % len(skipped))
431 ui.note(_("%d revisions have been skipped\n") % len(skipped))
423
432
424 if (activebookmark and
433 if (activebookmark and
425 repo['.'].node() == repo._bookmarks[activebookmark]):
434 repo['.'].node() == repo._bookmarks[activebookmark]):
426 bookmarks.setcurrent(repo, activebookmark)
435 bookmarks.setcurrent(repo, activebookmark)
427
436
428 finally:
437 finally:
429 release(lock, wlock)
438 release(lock, wlock)
430
439
431 def externalparent(repo, state, targetancestors):
440 def externalparent(repo, state, targetancestors):
432 """Return the revision that should be used as the second parent
441 """Return the revision that should be used as the second parent
433 when the revisions in state is collapsed on top of targetancestors.
442 when the revisions in state is collapsed on top of targetancestors.
434 Abort if there is more than one parent.
443 Abort if there is more than one parent.
435 """
444 """
436 parents = set()
445 parents = set()
437 source = min(state)
446 source = min(state)
438 for rev in state:
447 for rev in state:
439 if rev == source:
448 if rev == source:
440 continue
449 continue
441 for p in repo[rev].parents():
450 for p in repo[rev].parents():
442 if (p.rev() not in state
451 if (p.rev() not in state
443 and p.rev() not in targetancestors):
452 and p.rev() not in targetancestors):
444 parents.add(p.rev())
453 parents.add(p.rev())
445 if not parents:
454 if not parents:
446 return nullrev
455 return nullrev
447 if len(parents) == 1:
456 if len(parents) == 1:
448 return parents.pop()
457 return parents.pop()
449 raise util.Abort(_('unable to collapse on top of %s, there is more '
458 raise util.Abort(_('unable to collapse on top of %s, there is more '
450 'than one external parent: %s') %
459 'than one external parent: %s') %
451 (max(targetancestors),
460 (max(targetancestors),
452 ', '.join(str(p) for p in sorted(parents))))
461 ', '.join(str(p) for p in sorted(parents))))
453
462
454 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
463 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
455 'Commit the changes and store useful information in extra'
464 'Commit the changes and store useful information in extra'
456 try:
465 try:
457 repo.setparents(repo[p1].node(), repo[p2].node())
466 repo.setparents(repo[p1].node(), repo[p2].node())
458 ctx = repo[rev]
467 ctx = repo[rev]
459 if commitmsg is None:
468 if commitmsg is None:
460 commitmsg = ctx.description()
469 commitmsg = ctx.description()
461 extra = {'rebase_source': ctx.hex()}
470 extra = {'rebase_source': ctx.hex()}
462 if extrafn:
471 if extrafn:
463 extrafn(ctx, extra)
472 extrafn(ctx, extra)
464 # Commit might fail if unresolved files exist
473 # Commit might fail if unresolved files exist
465 newrev = repo.commit(text=commitmsg, user=ctx.user(),
474 newrev = repo.commit(text=commitmsg, user=ctx.user(),
466 date=ctx.date(), extra=extra, editor=editor)
475 date=ctx.date(), extra=extra, editor=editor)
467 repo.dirstate.setbranch(repo[newrev].branch())
476 repo.dirstate.setbranch(repo[newrev].branch())
468 targetphase = max(ctx.phase(), phases.draft)
477 targetphase = max(ctx.phase(), phases.draft)
469 # retractboundary doesn't overwrite upper phase inherited from parent
478 # retractboundary doesn't overwrite upper phase inherited from parent
470 newnode = repo[newrev].node()
479 newnode = repo[newrev].node()
471 if newnode:
480 if newnode:
472 phases.retractboundary(repo, targetphase, [newnode])
481 phases.retractboundary(repo, targetphase, [newnode])
473 return newrev
482 return newrev
474 except util.Abort:
483 except util.Abort:
475 # Invalidate the previous setparents
484 # Invalidate the previous setparents
476 repo.dirstate.invalidate()
485 repo.dirstate.invalidate()
477 raise
486 raise
478
487
479 def rebasenode(repo, rev, p1, state, collapse):
488 def rebasenode(repo, rev, p1, state, collapse):
480 'Rebase a single revision'
489 'Rebase a single revision'
481 # Merge phase
490 # Merge phase
482 # Update to target and merge it with local
491 # Update to target and merge it with local
483 if repo['.'].rev() != repo[p1].rev():
492 if repo['.'].rev() != repo[p1].rev():
484 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
493 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
485 merge.update(repo, p1, False, True, False)
494 merge.update(repo, p1, False, True, False)
486 else:
495 else:
487 repo.ui.debug(" already in target\n")
496 repo.ui.debug(" already in target\n")
488 repo.dirstate.write()
497 repo.dirstate.write()
489 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
498 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
490 if repo[rev].rev() == repo[min(state)].rev():
499 if repo[rev].rev() == repo[min(state)].rev():
491 # Case (1) initial changeset of a non-detaching rebase.
500 # Case (1) initial changeset of a non-detaching rebase.
492 # Let the merge mechanism find the base itself.
501 # Let the merge mechanism find the base itself.
493 base = None
502 base = None
494 elif not repo[rev].p2():
503 elif not repo[rev].p2():
495 # Case (2) detaching the node with a single parent, use this parent
504 # Case (2) detaching the node with a single parent, use this parent
496 base = repo[rev].p1().node()
505 base = repo[rev].p1().node()
497 else:
506 else:
498 # In case of merge, we need to pick the right parent as merge base.
507 # In case of merge, we need to pick the right parent as merge base.
499 #
508 #
500 # Imagine we have:
509 # Imagine we have:
501 # - M: currently rebase revision in this step
510 # - M: currently rebase revision in this step
502 # - A: one parent of M
511 # - A: one parent of M
503 # - B: second parent of M
512 # - B: second parent of M
504 # - D: destination of this merge step (p1 var)
513 # - D: destination of this merge step (p1 var)
505 #
514 #
506 # If we are rebasing on D, D is the successors of A or B. The right
515 # If we are rebasing on D, D is the successors of A or B. The right
507 # merge base is the one D succeed to. We pretend it is B for the rest
516 # merge base is the one D succeed to. We pretend it is B for the rest
508 # of this comment
517 # of this comment
509 #
518 #
510 # If we pick B as the base, the merge involves:
519 # If we pick B as the base, the merge involves:
511 # - changes from B to M (actual changeset payload)
520 # - changes from B to M (actual changeset payload)
512 # - changes from B to D (induced by rebase) as D is a rebased
521 # - changes from B to D (induced by rebase) as D is a rebased
513 # version of B)
522 # version of B)
514 # Which exactly represent the rebase operation.
523 # Which exactly represent the rebase operation.
515 #
524 #
516 # If we pick the A as the base, the merge involves
525 # If we pick the A as the base, the merge involves
517 # - changes from A to M (actual changeset payload)
526 # - changes from A to M (actual changeset payload)
518 # - changes from A to D (with include changes between unrelated A and B
527 # - changes from A to D (with include changes between unrelated A and B
519 # plus changes induced by rebase)
528 # plus changes induced by rebase)
520 # Which does not represent anything sensible and creates a lot of
529 # Which does not represent anything sensible and creates a lot of
521 # conflicts.
530 # conflicts.
522 for p in repo[rev].parents():
531 for p in repo[rev].parents():
523 if state.get(p.rev()) == repo[p1].rev():
532 if state.get(p.rev()) == repo[p1].rev():
524 base = p.node()
533 base = p.node()
525 break
534 break
526 else: # fallback when base not found
535 else: # fallback when base not found
527 base = None
536 base = None
528
537
529 # Raise because this function is called wrong (see issue 4106)
538 # Raise because this function is called wrong (see issue 4106)
530 raise AssertionError('no base found to rebase on '
539 raise AssertionError('no base found to rebase on '
531 '(rebasenode called wrong)')
540 '(rebasenode called wrong)')
532 if base is not None:
541 if base is not None:
533 repo.ui.debug(" detach base %d:%s\n" % (repo[base].rev(), repo[base]))
542 repo.ui.debug(" detach base %d:%s\n" % (repo[base].rev(), repo[base]))
534 # When collapsing in-place, the parent is the common ancestor, we
543 # When collapsing in-place, the parent is the common ancestor, we
535 # have to allow merging with it.
544 # have to allow merging with it.
536 return merge.update(repo, rev, True, True, False, base, collapse)
545 return merge.update(repo, rev, True, True, False, base, collapse)
537
546
538 def nearestrebased(repo, rev, state):
547 def nearestrebased(repo, rev, state):
539 """return the nearest ancestors of rev in the rebase result"""
548 """return the nearest ancestors of rev in the rebase result"""
540 rebased = [r for r in state if state[r] > nullmerge]
549 rebased = [r for r in state if state[r] > nullmerge]
541 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
550 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
542 if candidates:
551 if candidates:
543 return state[candidates[0]]
552 return state[candidates[0]]
544 else:
553 else:
545 return None
554 return None
546
555
547 def defineparents(repo, rev, target, state, targetancestors):
556 def defineparents(repo, rev, target, state, targetancestors):
548 'Return the new parent relationship of the revision that will be rebased'
557 'Return the new parent relationship of the revision that will be rebased'
549 parents = repo[rev].parents()
558 parents = repo[rev].parents()
550 p1 = p2 = nullrev
559 p1 = p2 = nullrev
551
560
552 P1n = parents[0].rev()
561 P1n = parents[0].rev()
553 if P1n in targetancestors:
562 if P1n in targetancestors:
554 p1 = target
563 p1 = target
555 elif P1n in state:
564 elif P1n in state:
556 if state[P1n] == nullmerge:
565 if state[P1n] == nullmerge:
557 p1 = target
566 p1 = target
558 elif state[P1n] == revignored:
567 elif state[P1n] == revignored:
559 p1 = nearestrebased(repo, P1n, state)
568 p1 = nearestrebased(repo, P1n, state)
560 if p1 is None:
569 if p1 is None:
561 p1 = target
570 p1 = target
562 else:
571 else:
563 p1 = state[P1n]
572 p1 = state[P1n]
564 else: # P1n external
573 else: # P1n external
565 p1 = target
574 p1 = target
566 p2 = P1n
575 p2 = P1n
567
576
568 if len(parents) == 2 and parents[1].rev() not in targetancestors:
577 if len(parents) == 2 and parents[1].rev() not in targetancestors:
569 P2n = parents[1].rev()
578 P2n = parents[1].rev()
570 # interesting second parent
579 # interesting second parent
571 if P2n in state:
580 if P2n in state:
572 if p1 == target: # P1n in targetancestors or external
581 if p1 == target: # P1n in targetancestors or external
573 p1 = state[P2n]
582 p1 = state[P2n]
574 elif state[P2n] == revignored:
583 elif state[P2n] == revignored:
575 p2 = nearestrebased(repo, P2n, state)
584 p2 = nearestrebased(repo, P2n, state)
576 if p2 is None:
585 if p2 is None:
577 # no ancestors rebased yet, detach
586 # no ancestors rebased yet, detach
578 p2 = target
587 p2 = target
579 else:
588 else:
580 p2 = state[P2n]
589 p2 = state[P2n]
581 else: # P2n external
590 else: # P2n external
582 if p2 != nullrev: # P1n external too => rev is a merged revision
591 if p2 != nullrev: # P1n external too => rev is a merged revision
583 raise util.Abort(_('cannot use revision %d as base, result '
592 raise util.Abort(_('cannot use revision %d as base, result '
584 'would have 3 parents') % rev)
593 'would have 3 parents') % rev)
585 p2 = P2n
594 p2 = P2n
586 repo.ui.debug(" future parents are %d and %d\n" %
595 repo.ui.debug(" future parents are %d and %d\n" %
587 (repo[p1].rev(), repo[p2].rev()))
596 (repo[p1].rev(), repo[p2].rev()))
588 return p1, p2
597 return p1, p2
589
598
590 def isagitpatch(repo, patchname):
599 def isagitpatch(repo, patchname):
591 'Return true if the given patch is in git format'
600 'Return true if the given patch is in git format'
592 mqpatch = os.path.join(repo.mq.path, patchname)
601 mqpatch = os.path.join(repo.mq.path, patchname)
593 for line in patch.linereader(file(mqpatch, 'rb')):
602 for line in patch.linereader(file(mqpatch, 'rb')):
594 if line.startswith('diff --git'):
603 if line.startswith('diff --git'):
595 return True
604 return True
596 return False
605 return False
597
606
598 def updatemq(repo, state, skipped, **opts):
607 def updatemq(repo, state, skipped, **opts):
599 'Update rebased mq patches - finalize and then import them'
608 'Update rebased mq patches - finalize and then import them'
600 mqrebase = {}
609 mqrebase = {}
601 mq = repo.mq
610 mq = repo.mq
602 original_series = mq.fullseries[:]
611 original_series = mq.fullseries[:]
603 skippedpatches = set()
612 skippedpatches = set()
604
613
605 for p in mq.applied:
614 for p in mq.applied:
606 rev = repo[p.node].rev()
615 rev = repo[p.node].rev()
607 if rev in state:
616 if rev in state:
608 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
617 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
609 (rev, p.name))
618 (rev, p.name))
610 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
619 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
611 else:
620 else:
612 # Applied but not rebased, not sure this should happen
621 # Applied but not rebased, not sure this should happen
613 skippedpatches.add(p.name)
622 skippedpatches.add(p.name)
614
623
615 if mqrebase:
624 if mqrebase:
616 mq.finish(repo, mqrebase.keys())
625 mq.finish(repo, mqrebase.keys())
617
626
618 # We must start import from the newest revision
627 # We must start import from the newest revision
619 for rev in sorted(mqrebase, reverse=True):
628 for rev in sorted(mqrebase, reverse=True):
620 if rev not in skipped:
629 if rev not in skipped:
621 name, isgit = mqrebase[rev]
630 name, isgit = mqrebase[rev]
622 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
631 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
623 mq.qimport(repo, (), patchname=name, git=isgit,
632 mq.qimport(repo, (), patchname=name, git=isgit,
624 rev=[str(state[rev])])
633 rev=[str(state[rev])])
625 else:
634 else:
626 # Rebased and skipped
635 # Rebased and skipped
627 skippedpatches.add(mqrebase[rev][0])
636 skippedpatches.add(mqrebase[rev][0])
628
637
629 # Patches were either applied and rebased and imported in
638 # Patches were either applied and rebased and imported in
630 # order, applied and removed or unapplied. Discard the removed
639 # order, applied and removed or unapplied. Discard the removed
631 # ones while preserving the original series order and guards.
640 # ones while preserving the original series order and guards.
632 newseries = [s for s in original_series
641 newseries = [s for s in original_series
633 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
642 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
634 mq.fullseries[:] = newseries
643 mq.fullseries[:] = newseries
635 mq.seriesdirty = True
644 mq.seriesdirty = True
636 mq.savedirty()
645 mq.savedirty()
637
646
638 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
647 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
639 'Move bookmarks to their correct changesets, and delete divergent ones'
648 'Move bookmarks to their correct changesets, and delete divergent ones'
640 marks = repo._bookmarks
649 marks = repo._bookmarks
641 for k, v in originalbookmarks.iteritems():
650 for k, v in originalbookmarks.iteritems():
642 if v in nstate:
651 if v in nstate:
643 # update the bookmarks for revs that have moved
652 # update the bookmarks for revs that have moved
644 marks[k] = nstate[v]
653 marks[k] = nstate[v]
645 bookmarks.deletedivergent(repo, [targetnode], k)
654 bookmarks.deletedivergent(repo, [targetnode], k)
646
655
647 marks.write()
656 marks.write()
648
657
649 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
658 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
650 external, activebookmark):
659 external, activebookmark):
651 'Store the current status to allow recovery'
660 'Store the current status to allow recovery'
652 f = repo.opener("rebasestate", "w")
661 f = repo.opener("rebasestate", "w")
653 f.write(repo[originalwd].hex() + '\n')
662 f.write(repo[originalwd].hex() + '\n')
654 f.write(repo[target].hex() + '\n')
663 f.write(repo[target].hex() + '\n')
655 f.write(repo[external].hex() + '\n')
664 f.write(repo[external].hex() + '\n')
656 f.write('%d\n' % int(collapse))
665 f.write('%d\n' % int(collapse))
657 f.write('%d\n' % int(keep))
666 f.write('%d\n' % int(keep))
658 f.write('%d\n' % int(keepbranches))
667 f.write('%d\n' % int(keepbranches))
659 f.write('%s\n' % (activebookmark or ''))
668 f.write('%s\n' % (activebookmark or ''))
660 for d, v in state.iteritems():
669 for d, v in state.iteritems():
661 oldrev = repo[d].hex()
670 oldrev = repo[d].hex()
662 if v > nullmerge:
671 if v > nullmerge:
663 newrev = repo[v].hex()
672 newrev = repo[v].hex()
664 else:
673 else:
665 newrev = v
674 newrev = v
666 f.write("%s:%s\n" % (oldrev, newrev))
675 f.write("%s:%s\n" % (oldrev, newrev))
667 f.close()
676 f.close()
668 repo.ui.debug('rebase status stored\n')
677 repo.ui.debug('rebase status stored\n')
669
678
670 def clearstatus(repo):
679 def clearstatus(repo):
671 'Remove the status files'
680 'Remove the status files'
672 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
681 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
673
682
674 def restorestatus(repo):
683 def restorestatus(repo):
675 'Restore a previously stored status'
684 'Restore a previously stored status'
676 try:
685 try:
677 keepbranches = None
686 keepbranches = None
678 target = None
687 target = None
679 collapse = False
688 collapse = False
680 external = nullrev
689 external = nullrev
681 activebookmark = None
690 activebookmark = None
682 state = {}
691 state = {}
683 f = repo.opener("rebasestate")
692 f = repo.opener("rebasestate")
684 for i, l in enumerate(f.read().splitlines()):
693 for i, l in enumerate(f.read().splitlines()):
685 if i == 0:
694 if i == 0:
686 originalwd = repo[l].rev()
695 originalwd = repo[l].rev()
687 elif i == 1:
696 elif i == 1:
688 target = repo[l].rev()
697 target = repo[l].rev()
689 elif i == 2:
698 elif i == 2:
690 external = repo[l].rev()
699 external = repo[l].rev()
691 elif i == 3:
700 elif i == 3:
692 collapse = bool(int(l))
701 collapse = bool(int(l))
693 elif i == 4:
702 elif i == 4:
694 keep = bool(int(l))
703 keep = bool(int(l))
695 elif i == 5:
704 elif i == 5:
696 keepbranches = bool(int(l))
705 keepbranches = bool(int(l))
697 elif i == 6 and not (len(l) == 81 and ':' in l):
706 elif i == 6 and not (len(l) == 81 and ':' in l):
698 # line 6 is a recent addition, so for backwards compatibility
707 # line 6 is a recent addition, so for backwards compatibility
699 # check that the line doesn't look like the oldrev:newrev lines
708 # check that the line doesn't look like the oldrev:newrev lines
700 activebookmark = l
709 activebookmark = l
701 else:
710 else:
702 oldrev, newrev = l.split(':')
711 oldrev, newrev = l.split(':')
703 if newrev in (str(nullmerge), str(revignored)):
712 if newrev in (str(nullmerge), str(revignored)):
704 state[repo[oldrev].rev()] = int(newrev)
713 state[repo[oldrev].rev()] = int(newrev)
705 else:
714 else:
706 state[repo[oldrev].rev()] = repo[newrev].rev()
715 state[repo[oldrev].rev()] = repo[newrev].rev()
707
716
708 if keepbranches is None:
717 if keepbranches is None:
709 raise util.Abort(_('.hg/rebasestate is incomplete'))
718 raise util.Abort(_('.hg/rebasestate is incomplete'))
710
719
711 skipped = set()
720 skipped = set()
712 # recompute the set of skipped revs
721 # recompute the set of skipped revs
713 if not collapse:
722 if not collapse:
714 seen = set([target])
723 seen = set([target])
715 for old, new in sorted(state.items()):
724 for old, new in sorted(state.items()):
716 if new != nullrev and new in seen:
725 if new != nullrev and new in seen:
717 skipped.add(old)
726 skipped.add(old)
718 seen.add(new)
727 seen.add(new)
719 repo.ui.debug('computed skipped revs: %s\n' %
728 repo.ui.debug('computed skipped revs: %s\n' %
720 (' '.join(str(r) for r in sorted(skipped)) or None))
729 (' '.join(str(r) for r in sorted(skipped)) or None))
721 repo.ui.debug('rebase status resumed\n')
730 repo.ui.debug('rebase status resumed\n')
722 return (originalwd, target, state, skipped,
731 return (originalwd, target, state, skipped,
723 collapse, keep, keepbranches, external, activebookmark)
732 collapse, keep, keepbranches, external, activebookmark)
724 except IOError, err:
733 except IOError, err:
725 if err.errno != errno.ENOENT:
734 if err.errno != errno.ENOENT:
726 raise
735 raise
727 raise util.Abort(_('no rebase in progress'))
736 raise util.Abort(_('no rebase in progress'))
728
737
729 def inrebase(repo, originalwd, state):
738 def inrebase(repo, originalwd, state):
730 '''check whether the working dir is in an interrupted rebase'''
739 '''check whether the working dir is in an interrupted rebase'''
731 parents = [p.rev() for p in repo.parents()]
740 parents = [p.rev() for p in repo.parents()]
732 if originalwd in parents:
741 if originalwd in parents:
733 return True
742 return True
734
743
735 for newrev in state.itervalues():
744 for newrev in state.itervalues():
736 if newrev in parents:
745 if newrev in parents:
737 return True
746 return True
738
747
739 return False
748 return False
740
749
741 def abort(repo, originalwd, target, state):
750 def abort(repo, originalwd, target, state):
742 'Restore the repository to its original state'
751 'Restore the repository to its original state'
743 dstates = [s for s in state.values() if s > nullrev]
752 dstates = [s for s in state.values() if s > nullrev]
744 immutable = [d for d in dstates if not repo[d].mutable()]
753 immutable = [d for d in dstates if not repo[d].mutable()]
745 cleanup = True
754 cleanup = True
746 if immutable:
755 if immutable:
747 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
756 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
748 % ', '.join(str(repo[r]) for r in immutable),
757 % ', '.join(str(repo[r]) for r in immutable),
749 hint=_('see hg help phases for details'))
758 hint=_('see hg help phases for details'))
750 cleanup = False
759 cleanup = False
751
760
752 descendants = set()
761 descendants = set()
753 if dstates:
762 if dstates:
754 descendants = set(repo.changelog.descendants(dstates))
763 descendants = set(repo.changelog.descendants(dstates))
755 if descendants - set(dstates):
764 if descendants - set(dstates):
756 repo.ui.warn(_("warning: new changesets detected on target branch, "
765 repo.ui.warn(_("warning: new changesets detected on target branch, "
757 "can't strip\n"))
766 "can't strip\n"))
758 cleanup = False
767 cleanup = False
759
768
760 if cleanup:
769 if cleanup:
761 # Update away from the rebase if necessary
770 # Update away from the rebase if necessary
762 if inrebase(repo, originalwd, state):
771 if inrebase(repo, originalwd, state):
763 merge.update(repo, repo[originalwd].rev(), False, True, False)
772 merge.update(repo, repo[originalwd].rev(), False, True, False)
764
773
765 # Strip from the first rebased revision
774 # Strip from the first rebased revision
766 rebased = filter(lambda x: x > -1 and x != target, state.values())
775 rebased = filter(lambda x: x > -1 and x != target, state.values())
767 if rebased:
776 if rebased:
768 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
777 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
769 # no backup of rebased cset versions needed
778 # no backup of rebased cset versions needed
770 repair.strip(repo.ui, repo, strippoints)
779 repair.strip(repo.ui, repo, strippoints)
771
780
772 clearstatus(repo)
781 clearstatus(repo)
773 repo.ui.warn(_('rebase aborted\n'))
782 repo.ui.warn(_('rebase aborted\n'))
774 return 0
783 return 0
775
784
776 def buildstate(repo, dest, rebaseset, collapse):
785 def buildstate(repo, dest, rebaseset, collapse):
777 '''Define which revisions are going to be rebased and where
786 '''Define which revisions are going to be rebased and where
778
787
779 repo: repo
788 repo: repo
780 dest: context
789 dest: context
781 rebaseset: set of rev
790 rebaseset: set of rev
782 '''
791 '''
783
792
784 # This check isn't strictly necessary, since mq detects commits over an
793 # This check isn't strictly necessary, since mq detects commits over an
785 # applied patch. But it prevents messing up the working directory when
794 # applied patch. But it prevents messing up the working directory when
786 # a partially completed rebase is blocked by mq.
795 # a partially completed rebase is blocked by mq.
787 if 'qtip' in repo.tags() and (dest.node() in
796 if 'qtip' in repo.tags() and (dest.node() in
788 [s.node for s in repo.mq.applied]):
797 [s.node for s in repo.mq.applied]):
789 raise util.Abort(_('cannot rebase onto an applied mq patch'))
798 raise util.Abort(_('cannot rebase onto an applied mq patch'))
790
799
791 roots = list(repo.set('roots(%ld)', rebaseset))
800 roots = list(repo.set('roots(%ld)', rebaseset))
792 if not roots:
801 if not roots:
793 raise util.Abort(_('no matching revisions'))
802 raise util.Abort(_('no matching revisions'))
794 roots.sort()
803 roots.sort()
795 state = {}
804 state = {}
796 detachset = set()
805 detachset = set()
797 for root in roots:
806 for root in roots:
798 commonbase = root.ancestor(dest)
807 commonbase = root.ancestor(dest)
799 if commonbase == root:
808 if commonbase == root:
800 raise util.Abort(_('source is ancestor of destination'))
809 raise util.Abort(_('source is ancestor of destination'))
801 if commonbase == dest:
810 if commonbase == dest:
802 samebranch = root.branch() == dest.branch()
811 samebranch = root.branch() == dest.branch()
803 if not collapse and samebranch and root in dest.children():
812 if not collapse and samebranch and root in dest.children():
804 repo.ui.debug('source is a child of destination\n')
813 repo.ui.debug('source is a child of destination\n')
805 return None
814 return None
806
815
807 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
816 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
808 state.update(dict.fromkeys(rebaseset, nullrev))
817 state.update(dict.fromkeys(rebaseset, nullrev))
809 # Rebase tries to turn <dest> into a parent of <root> while
818 # Rebase tries to turn <dest> into a parent of <root> while
810 # preserving the number of parents of rebased changesets:
819 # preserving the number of parents of rebased changesets:
811 #
820 #
812 # - A changeset with a single parent will always be rebased as a
821 # - A changeset with a single parent will always be rebased as a
813 # changeset with a single parent.
822 # changeset with a single parent.
814 #
823 #
815 # - A merge will be rebased as merge unless its parents are both
824 # - A merge will be rebased as merge unless its parents are both
816 # ancestors of <dest> or are themselves in the rebased set and
825 # ancestors of <dest> or are themselves in the rebased set and
817 # pruned while rebased.
826 # pruned while rebased.
818 #
827 #
819 # If one parent of <root> is an ancestor of <dest>, the rebased
828 # If one parent of <root> is an ancestor of <dest>, the rebased
820 # version of this parent will be <dest>. This is always true with
829 # version of this parent will be <dest>. This is always true with
821 # --base option.
830 # --base option.
822 #
831 #
823 # Otherwise, we need to *replace* the original parents with
832 # Otherwise, we need to *replace* the original parents with
824 # <dest>. This "detaches" the rebased set from its former location
833 # <dest>. This "detaches" the rebased set from its former location
825 # and rebases it onto <dest>. Changes introduced by ancestors of
834 # and rebases it onto <dest>. Changes introduced by ancestors of
826 # <root> not common with <dest> (the detachset, marked as
835 # <root> not common with <dest> (the detachset, marked as
827 # nullmerge) are "removed" from the rebased changesets.
836 # nullmerge) are "removed" from the rebased changesets.
828 #
837 #
829 # - If <root> has a single parent, set it to <dest>.
838 # - If <root> has a single parent, set it to <dest>.
830 #
839 #
831 # - If <root> is a merge, we cannot decide which parent to
840 # - If <root> is a merge, we cannot decide which parent to
832 # replace, the rebase operation is not clearly defined.
841 # replace, the rebase operation is not clearly defined.
833 #
842 #
834 # The table below sums up this behavior:
843 # The table below sums up this behavior:
835 #
844 #
836 # +------------------+----------------------+-------------------------+
845 # +------------------+----------------------+-------------------------+
837 # | | one parent | merge |
846 # | | one parent | merge |
838 # +------------------+----------------------+-------------------------+
847 # +------------------+----------------------+-------------------------+
839 # | parent in | new parent is <dest> | parents in ::<dest> are |
848 # | parent in | new parent is <dest> | parents in ::<dest> are |
840 # | ::<dest> | | remapped to <dest> |
849 # | ::<dest> | | remapped to <dest> |
841 # +------------------+----------------------+-------------------------+
850 # +------------------+----------------------+-------------------------+
842 # | unrelated source | new parent is <dest> | ambiguous, abort |
851 # | unrelated source | new parent is <dest> | ambiguous, abort |
843 # +------------------+----------------------+-------------------------+
852 # +------------------+----------------------+-------------------------+
844 #
853 #
845 # The actual abort is handled by `defineparents`
854 # The actual abort is handled by `defineparents`
846 if len(root.parents()) <= 1:
855 if len(root.parents()) <= 1:
847 # ancestors of <root> not ancestors of <dest>
856 # ancestors of <root> not ancestors of <dest>
848 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
857 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
849 [root.rev()]))
858 [root.rev()]))
850 for r in detachset:
859 for r in detachset:
851 if r not in state:
860 if r not in state:
852 state[r] = nullmerge
861 state[r] = nullmerge
853 if len(roots) > 1:
862 if len(roots) > 1:
854 # If we have multiple roots, we may have "hole" in the rebase set.
863 # If we have multiple roots, we may have "hole" in the rebase set.
855 # Rebase roots that descend from those "hole" should not be detached as
864 # Rebase roots that descend from those "hole" should not be detached as
856 # other root are. We use the special `revignored` to inform rebase that
865 # other root are. We use the special `revignored` to inform rebase that
857 # the revision should be ignored but that `defineparents` should search
866 # the revision should be ignored but that `defineparents` should search
858 # a rebase destination that make sense regarding rebased topology.
867 # a rebase destination that make sense regarding rebased topology.
859 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
868 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
860 for ignored in set(rebasedomain) - set(rebaseset):
869 for ignored in set(rebasedomain) - set(rebaseset):
861 state[ignored] = revignored
870 state[ignored] = revignored
862 return repo['.'].rev(), dest.rev(), state
871 return repo['.'].rev(), dest.rev(), state
863
872
864 def clearrebased(ui, repo, state, skipped, collapsedas=None):
873 def clearrebased(ui, repo, state, skipped, collapsedas=None):
865 """dispose of rebased revision at the end of the rebase
874 """dispose of rebased revision at the end of the rebase
866
875
867 If `collapsedas` is not None, the rebase was a collapse whose result if the
876 If `collapsedas` is not None, the rebase was a collapse whose result if the
868 `collapsedas` node."""
877 `collapsedas` node."""
869 if obsolete._enabled:
878 if obsolete._enabled:
870 markers = []
879 markers = []
871 for rev, newrev in sorted(state.items()):
880 for rev, newrev in sorted(state.items()):
872 if newrev >= 0:
881 if newrev >= 0:
873 if rev in skipped:
882 if rev in skipped:
874 succs = ()
883 succs = ()
875 elif collapsedas is not None:
884 elif collapsedas is not None:
876 succs = (repo[collapsedas],)
885 succs = (repo[collapsedas],)
877 else:
886 else:
878 succs = (repo[newrev],)
887 succs = (repo[newrev],)
879 markers.append((repo[rev], succs))
888 markers.append((repo[rev], succs))
880 if markers:
889 if markers:
881 obsolete.createmarkers(repo, markers)
890 obsolete.createmarkers(repo, markers)
882 else:
891 else:
883 rebased = [rev for rev in state if state[rev] > nullmerge]
892 rebased = [rev for rev in state if state[rev] > nullmerge]
884 if rebased:
893 if rebased:
885 stripped = []
894 stripped = []
886 for root in repo.set('roots(%ld)', rebased):
895 for root in repo.set('roots(%ld)', rebased):
887 if set(repo.changelog.descendants([root.rev()])) - set(state):
896 if set(repo.changelog.descendants([root.rev()])) - set(state):
888 ui.warn(_("warning: new changesets detected "
897 ui.warn(_("warning: new changesets detected "
889 "on source branch, not stripping\n"))
898 "on source branch, not stripping\n"))
890 else:
899 else:
891 stripped.append(root.node())
900 stripped.append(root.node())
892 if stripped:
901 if stripped:
893 # backup the old csets by default
902 # backup the old csets by default
894 repair.strip(ui, repo, stripped, "all")
903 repair.strip(ui, repo, stripped, "all")
895
904
896
905
897 def pullrebase(orig, ui, repo, *args, **opts):
906 def pullrebase(orig, ui, repo, *args, **opts):
898 'Call rebase after pull if the latter has been invoked with --rebase'
907 'Call rebase after pull if the latter has been invoked with --rebase'
899 if opts.get('rebase'):
908 if opts.get('rebase'):
900 if opts.get('update'):
909 if opts.get('update'):
901 del opts['update']
910 del opts['update']
902 ui.debug('--update and --rebase are not compatible, ignoring '
911 ui.debug('--update and --rebase are not compatible, ignoring '
903 'the update flag\n')
912 'the update flag\n')
904
913
905 movemarkfrom = repo['.'].node()
914 movemarkfrom = repo['.'].node()
906 revsprepull = len(repo)
915 revsprepull = len(repo)
907 origpostincoming = commands.postincoming
916 origpostincoming = commands.postincoming
908 def _dummy(*args, **kwargs):
917 def _dummy(*args, **kwargs):
909 pass
918 pass
910 commands.postincoming = _dummy
919 commands.postincoming = _dummy
911 try:
920 try:
912 orig(ui, repo, *args, **opts)
921 orig(ui, repo, *args, **opts)
913 finally:
922 finally:
914 commands.postincoming = origpostincoming
923 commands.postincoming = origpostincoming
915 revspostpull = len(repo)
924 revspostpull = len(repo)
916 if revspostpull > revsprepull:
925 if revspostpull > revsprepull:
917 # --rev option from pull conflict with rebase own --rev
926 # --rev option from pull conflict with rebase own --rev
918 # dropping it
927 # dropping it
919 if 'rev' in opts:
928 if 'rev' in opts:
920 del opts['rev']
929 del opts['rev']
921 rebase(ui, repo, **opts)
930 rebase(ui, repo, **opts)
922 branch = repo[None].branch()
931 branch = repo[None].branch()
923 dest = repo[branch].rev()
932 dest = repo[branch].rev()
924 if dest != repo['.'].rev():
933 if dest != repo['.'].rev():
925 # there was nothing to rebase we force an update
934 # there was nothing to rebase we force an update
926 hg.update(repo, dest)
935 hg.update(repo, dest)
927 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
936 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
928 ui.status(_("updating bookmark %s\n")
937 ui.status(_("updating bookmark %s\n")
929 % repo._bookmarkcurrent)
938 % repo._bookmarkcurrent)
930 else:
939 else:
931 if opts.get('tool'):
940 if opts.get('tool'):
932 raise util.Abort(_('--tool can only be used with --rebase'))
941 raise util.Abort(_('--tool can only be used with --rebase'))
933 orig(ui, repo, *args, **opts)
942 orig(ui, repo, *args, **opts)
934
943
935 def summaryhook(ui, repo):
944 def summaryhook(ui, repo):
936 if not os.path.exists(repo.join('rebasestate')):
945 if not os.path.exists(repo.join('rebasestate')):
937 return
946 return
938 try:
947 try:
939 state = restorestatus(repo)[2]
948 state = restorestatus(repo)[2]
940 except error.RepoLookupError:
949 except error.RepoLookupError:
941 # i18n: column positioning for "hg summary"
950 # i18n: column positioning for "hg summary"
942 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
951 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
943 ui.write(msg)
952 ui.write(msg)
944 return
953 return
945 numrebased = len([i for i in state.itervalues() if i != -1])
954 numrebased = len([i for i in state.itervalues() if i != -1])
946 # i18n: column positioning for "hg summary"
955 # i18n: column positioning for "hg summary"
947 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
956 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
948 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
957 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
949 ui.label(_('%d remaining'), 'rebase.remaining') %
958 ui.label(_('%d remaining'), 'rebase.remaining') %
950 (len(state) - numrebased)))
959 (len(state) - numrebased)))
951
960
952 def uisetup(ui):
961 def uisetup(ui):
953 'Replace pull with a decorator to provide --rebase option'
962 'Replace pull with a decorator to provide --rebase option'
954 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
963 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
955 entry[1].append(('', 'rebase', None,
964 entry[1].append(('', 'rebase', None,
956 _("rebase working directory to branch head")))
965 _("rebase working directory to branch head")))
957 entry[1].append(('t', 'tool', '',
966 entry[1].append(('t', 'tool', '',
958 _("specify merge tool for rebase")))
967 _("specify merge tool for rebase")))
959 cmdutil.summaryhooks.add('rebase', summaryhook)
968 cmdutil.summaryhooks.add('rebase', summaryhook)
960 cmdutil.unfinishedstates.append(
969 cmdutil.unfinishedstates.append(
961 ['rebasestate', False, False, _('rebase in progress'),
970 ['rebasestate', False, False, _('rebase in progress'),
962 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
971 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
@@ -1,242 +1,323 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > rebase=
3 > rebase=
4 >
4 >
5 > [alias]
5 > [alias]
6 > tlog = log --template "{rev}: '{desc}' {branches}\n"
6 > tlog = log --template "{rev}: '{desc}' {branches}\n"
7 > tglog = tlog --graph
7 > tglog = tlog --graph
8 > EOF
8 > EOF
9
9
10
10
11 $ hg init a
11 $ hg init a
12 $ cd a
12 $ cd a
13
13
14 $ mkdir d
14 $ mkdir d
15 $ echo a > a
15 $ echo a > a
16 $ hg ci -Am A
16 $ hg ci -Am A
17 adding a
17 adding a
18
18
19 $ echo b > d/b
19 $ echo b > d/b
20 $ hg ci -Am B
20 $ hg ci -Am B
21 adding d/b
21 adding d/b
22
22
23 $ hg mv d d-renamed
23 $ hg mv d d-renamed
24 moving d/b to d-renamed/b (glob)
24 moving d/b to d-renamed/b (glob)
25 $ hg ci -m 'rename B'
25 $ hg ci -m 'rename B'
26
26
27 $ hg up -q -C 1
27 $ hg up -q -C 1
28
28
29 $ hg mv a a-renamed
29 $ hg mv a a-renamed
30 $ echo x > d/x
30 $ echo x > d/x
31 $ hg add d/x
31 $ hg add d/x
32
32
33 $ hg ci -m 'rename A'
33 $ hg ci -m 'rename A'
34 created new head
34 created new head
35
35
36 $ hg tglog
36 $ hg tglog
37 @ 3: 'rename A'
37 @ 3: 'rename A'
38 |
38 |
39 | o 2: 'rename B'
39 | o 2: 'rename B'
40 |/
40 |/
41 o 1: 'B'
41 o 1: 'B'
42 |
42 |
43 o 0: 'A'
43 o 0: 'A'
44
44
45
45
46 Rename is tracked:
46 Rename is tracked:
47
47
48 $ hg tlog -p --git -r tip
48 $ hg tlog -p --git -r tip
49 3: 'rename A'
49 3: 'rename A'
50 diff --git a/a b/a-renamed
50 diff --git a/a b/a-renamed
51 rename from a
51 rename from a
52 rename to a-renamed
52 rename to a-renamed
53 diff --git a/d/x b/d/x
53 diff --git a/d/x b/d/x
54 new file mode 100644
54 new file mode 100644
55 --- /dev/null
55 --- /dev/null
56 +++ b/d/x
56 +++ b/d/x
57 @@ -0,0 +1,1 @@
57 @@ -0,0 +1,1 @@
58 +x
58 +x
59
59
60 Rebase the revision containing the rename:
60 Rebase the revision containing the rename:
61
61
62 $ hg rebase -s 3 -d 2
62 $ hg rebase -s 3 -d 2
63 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
63 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
64
64
65 $ hg tglog
65 $ hg tglog
66 @ 3: 'rename A'
66 @ 3: 'rename A'
67 |
67 |
68 o 2: 'rename B'
68 o 2: 'rename B'
69 |
69 |
70 o 1: 'B'
70 o 1: 'B'
71 |
71 |
72 o 0: 'A'
72 o 0: 'A'
73
73
74
74
75 Rename is not lost:
75 Rename is not lost:
76
76
77 $ hg tlog -p --git -r tip
77 $ hg tlog -p --git -r tip
78 3: 'rename A'
78 3: 'rename A'
79 diff --git a/a b/a-renamed
79 diff --git a/a b/a-renamed
80 rename from a
80 rename from a
81 rename to a-renamed
81 rename to a-renamed
82 diff --git a/d-renamed/x b/d-renamed/x
82 diff --git a/d-renamed/x b/d-renamed/x
83 new file mode 100644
83 new file mode 100644
84 --- /dev/null
84 --- /dev/null
85 +++ b/d-renamed/x
85 +++ b/d-renamed/x
86 @@ -0,0 +1,1 @@
86 @@ -0,0 +1,1 @@
87 +x
87 +x
88
88
89
89
90 Rebased revision does not contain information about b (issue3739)
90 Rebased revision does not contain information about b (issue3739)
91
91
92 $ hg log -r 3 --debug
92 $ hg log -r 3 --debug
93 changeset: 3:032a9b75e83bff1dcfb6cbfa4ef50a704bf1b569
93 changeset: 3:032a9b75e83bff1dcfb6cbfa4ef50a704bf1b569
94 tag: tip
94 tag: tip
95 phase: draft
95 phase: draft
96 parent: 2:220d0626d185f372d9d8f69d9c73b0811d7725f7
96 parent: 2:220d0626d185f372d9d8f69d9c73b0811d7725f7
97 parent: -1:0000000000000000000000000000000000000000
97 parent: -1:0000000000000000000000000000000000000000
98 manifest: 3:035d66b27a1b06b2d12b46d41a39adb7a200c370
98 manifest: 3:035d66b27a1b06b2d12b46d41a39adb7a200c370
99 user: test
99 user: test
100 date: Thu Jan 01 00:00:00 1970 +0000
100 date: Thu Jan 01 00:00:00 1970 +0000
101 files+: a-renamed d-renamed/x
101 files+: a-renamed d-renamed/x
102 files-: a
102 files-: a
103 extra: branch=default
103 extra: branch=default
104 extra: rebase_source=73a3ee40125d6f0f347082e5831ceccb3f005f8a
104 extra: rebase_source=73a3ee40125d6f0f347082e5831ceccb3f005f8a
105 description:
105 description:
106 rename A
106 rename A
107
107
108
108
109
109
110 $ cd ..
110 $ cd ..
111
111
112
112
113 $ hg init b
113 $ hg init b
114 $ cd b
114 $ cd b
115
115
116 $ echo a > a
116 $ echo a > a
117 $ hg ci -Am A
117 $ hg ci -Am A
118 adding a
118 adding a
119
119
120 $ echo b > b
120 $ echo b > b
121 $ hg ci -Am B
121 $ hg ci -Am B
122 adding b
122 adding b
123
123
124 $ hg cp b b-copied
124 $ hg cp b b-copied
125 $ hg ci -Am 'copy B'
125 $ hg ci -Am 'copy B'
126
126
127 $ hg up -q -C 1
127 $ hg up -q -C 1
128
128
129 $ hg cp a a-copied
129 $ hg cp a a-copied
130 $ hg ci -m 'copy A'
130 $ hg ci -m 'copy A'
131 created new head
131 created new head
132
132
133 $ hg tglog
133 $ hg tglog
134 @ 3: 'copy A'
134 @ 3: 'copy A'
135 |
135 |
136 | o 2: 'copy B'
136 | o 2: 'copy B'
137 |/
137 |/
138 o 1: 'B'
138 o 1: 'B'
139 |
139 |
140 o 0: 'A'
140 o 0: 'A'
141
141
142 Copy is tracked:
142 Copy is tracked:
143
143
144 $ hg tlog -p --git -r tip
144 $ hg tlog -p --git -r tip
145 3: 'copy A'
145 3: 'copy A'
146 diff --git a/a b/a-copied
146 diff --git a/a b/a-copied
147 copy from a
147 copy from a
148 copy to a-copied
148 copy to a-copied
149
149
150 Rebase the revision containing the copy:
150 Rebase the revision containing the copy:
151
151
152 $ hg rebase -s 3 -d 2
152 $ hg rebase -s 3 -d 2
153 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
153 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
154
154
155 $ hg tglog
155 $ hg tglog
156 @ 3: 'copy A'
156 @ 3: 'copy A'
157 |
157 |
158 o 2: 'copy B'
158 o 2: 'copy B'
159 |
159 |
160 o 1: 'B'
160 o 1: 'B'
161 |
161 |
162 o 0: 'A'
162 o 0: 'A'
163
163
164
164
165 Copy is not lost:
165 Copy is not lost:
166
166
167 $ hg tlog -p --git -r tip
167 $ hg tlog -p --git -r tip
168 3: 'copy A'
168 3: 'copy A'
169 diff --git a/a b/a-copied
169 diff --git a/a b/a-copied
170 copy from a
170 copy from a
171 copy to a-copied
171 copy to a-copied
172
172
173
173
174 Rebased revision does not contain information about b (issue3739)
174 Rebased revision does not contain information about b (issue3739)
175
175
176 $ hg log -r 3 --debug
176 $ hg log -r 3 --debug
177 changeset: 3:98f6e6dbf45ab54079c2237fbd11066a5c41a11d
177 changeset: 3:98f6e6dbf45ab54079c2237fbd11066a5c41a11d
178 tag: tip
178 tag: tip
179 phase: draft
179 phase: draft
180 parent: 2:39e588434882ff77d01229d169cdc77f29e8855e
180 parent: 2:39e588434882ff77d01229d169cdc77f29e8855e
181 parent: -1:0000000000000000000000000000000000000000
181 parent: -1:0000000000000000000000000000000000000000
182 manifest: 3:2232f329d66fffe3930d43479ae624f66322b04d
182 manifest: 3:2232f329d66fffe3930d43479ae624f66322b04d
183 user: test
183 user: test
184 date: Thu Jan 01 00:00:00 1970 +0000
184 date: Thu Jan 01 00:00:00 1970 +0000
185 files+: a-copied
185 files+: a-copied
186 extra: branch=default
186 extra: branch=default
187 extra: rebase_source=0a8162ff18a8900df8df8ef7ac0046955205613e
187 extra: rebase_source=0a8162ff18a8900df8df8ef7ac0046955205613e
188 description:
188 description:
189 copy A
189 copy A
190
190
191
191
192
192
193 $ cd ..
193 $ cd ..
194
194
195
195
196 Test rebase across repeating renames:
196 Test rebase across repeating renames:
197
197
198 $ hg init repo
198 $ hg init repo
199
199
200 $ cd repo
200 $ cd repo
201
201
202 $ echo testing > file1.txt
202 $ echo testing > file1.txt
203 $ hg add file1.txt
203 $ hg add file1.txt
204 $ hg ci -m "Adding file1"
204 $ hg ci -m "Adding file1"
205
205
206 $ hg rename file1.txt file2.txt
206 $ hg rename file1.txt file2.txt
207 $ hg ci -m "Rename file1 to file2"
207 $ hg ci -m "Rename file1 to file2"
208
208
209 $ echo Unrelated change > unrelated.txt
209 $ echo Unrelated change > unrelated.txt
210 $ hg add unrelated.txt
210 $ hg add unrelated.txt
211 $ hg ci -m "Unrelated change"
211 $ hg ci -m "Unrelated change"
212
212
213 $ hg rename file2.txt file1.txt
213 $ hg rename file2.txt file1.txt
214 $ hg ci -m "Rename file2 back to file1"
214 $ hg ci -m "Rename file2 back to file1"
215
215
216 $ hg update -r -2
216 $ hg update -r -2
217 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
217 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
218
218
219 $ echo Another unrelated change >> unrelated.txt
219 $ echo Another unrelated change >> unrelated.txt
220 $ hg ci -m "Another unrelated change"
220 $ hg ci -m "Another unrelated change"
221 created new head
221 created new head
222
222
223 $ hg tglog
223 $ hg tglog
224 @ 4: 'Another unrelated change'
224 @ 4: 'Another unrelated change'
225 |
225 |
226 | o 3: 'Rename file2 back to file1'
226 | o 3: 'Rename file2 back to file1'
227 |/
227 |/
228 o 2: 'Unrelated change'
228 o 2: 'Unrelated change'
229 |
229 |
230 o 1: 'Rename file1 to file2'
230 o 1: 'Rename file1 to file2'
231 |
231 |
232 o 0: 'Adding file1'
232 o 0: 'Adding file1'
233
233
234
234
235 $ hg rebase -s 4 -d 3
235 $ hg rebase -s 4 -d 3
236 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-backup.hg (glob)
236 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-backup.hg (glob)
237
237
238 $ hg diff --stat -c .
238 $ hg diff --stat -c .
239 unrelated.txt | 1 +
239 unrelated.txt | 1 +
240 1 files changed, 1 insertions(+), 0 deletions(-)
240 1 files changed, 1 insertions(+), 0 deletions(-)
241
241
242 $ cd ..
242 $ cd ..
243
244 Verify that copies get preserved (issue4192).
245 $ hg init copy-gets-preserved
246 $ cd copy-gets-preserved
247
248 $ echo a > a
249 $ hg add a
250 $ hg commit --message "File a created"
251 $ hg copy a b
252 $ echo b > b
253 $ hg commit --message "File b created as copy of a and modified"
254 $ hg copy b c
255 $ echo c > c
256 $ hg commit --message "File c created as copy of b and modified"
257 $ hg copy c d
258 $ echo d > d
259 $ hg commit --message "File d created as copy of c and modified"
260
261 Note that there are four entries in the log for d
262 $ hg tglog --follow d
263 @ 3: 'File d created as copy of c and modified'
264 |
265 o 2: 'File c created as copy of b and modified'
266 |
267 o 1: 'File b created as copy of a and modified'
268 |
269 o 0: 'File a created'
270
271 Update back to before we performed copies, and inject an unrelated change.
272 $ hg update 0
273 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
274
275 $ echo unrelated > unrelated
276 $ hg add unrelated
277 $ hg commit --message "Unrelated file created"
278 created new head
279 $ hg update 4
280 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
281
282 Rebase the copies on top of the unrelated change.
283 $ hg rebase --source 1 --dest 4
284 saved backup bundle to $TESTTMP/copy-gets-preserved/.hg/*.hg (glob)
285 $ hg update 4
286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287
288 There should still be four entries in the log for d
289 $ hg tglog --follow d
290 @ 4: 'File d created as copy of c and modified'
291 |
292 o 3: 'File c created as copy of b and modified'
293 |
294 o 2: 'File b created as copy of a and modified'
295 |
296 o 0: 'File a created'
297
298 Same steps as above, but with --collapse on rebase to make sure the
299 copy records collapse correctly.
300 $ hg co 1
301 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
302 $ echo more >> unrelated
303 $ hg ci -m 'unrelated commit is unrelated'
304 created new head
305 $ hg rebase -s 2 --dest 5 --collapse
306 merging b and c to c
307 merging c and d to d
308 saved backup bundle to $TESTTMP/copy-gets-preserved/.hg/*.hg (glob)
309 $ hg co tip
310 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
311
312 This should show both revision 3 and 0 since 'd' was transitively a
313 copy of 'a'.
314
315 $ hg tglog --follow d
316 @ 3: 'Collapsed revision
317 | * File b created as copy of a and modified
318 | * File c created as copy of b and modified
319 | * File d created as copy of c and modified'
320 o 0: 'File a created'
321
322
323 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now