##// END OF EJS Templates
rebase: don't mark file as removed if missing in parent's manifest (issue2725)
Stefano Tortarolo -
r13778:46c30432 default
parent child Browse files
Show More
@@ -1,600 +1,600 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
17 from mercurial import hg, util, repair, merge, cmdutil, commands
18 from mercurial import extensions, ancestor, copies, patch
18 from mercurial import extensions, ancestor, copies, patch
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 def rebase(ui, repo, **opts):
27 def rebase(ui, repo, **opts):
28 """move changeset (and descendants) to a different branch
28 """move changeset (and descendants) to a different branch
29
29
30 Rebase uses repeated merging to graft changesets from one part of
30 Rebase uses repeated merging to graft changesets from one part of
31 history (the source) onto another (the destination). This can be
31 history (the source) onto another (the destination). This can be
32 useful for linearizing *local* changes relative to a master
32 useful for linearizing *local* changes relative to a master
33 development tree.
33 development tree.
34
34
35 You should not rebase changesets that have already been shared
35 You should not rebase changesets that have already been shared
36 with others. Doing so will force everybody else to perform the
36 with others. Doing so will force everybody else to perform the
37 same rebase or they will end up with duplicated changesets after
37 same rebase or they will end up with duplicated changesets after
38 pulling in your rebased changesets.
38 pulling in your rebased changesets.
39
39
40 If you don't specify a destination changeset (``-d/--dest``),
40 If you don't specify a destination changeset (``-d/--dest``),
41 rebase uses the tipmost head of the current named branch as the
41 rebase uses the tipmost head of the current named branch as the
42 destination. (The destination changeset is not modified by
42 destination. (The destination changeset is not modified by
43 rebasing, but new changesets are added as its descendants.)
43 rebasing, but new changesets are added as its descendants.)
44
44
45 You can specify which changesets to rebase in two ways: as a
45 You can specify which changesets to rebase in two ways: as a
46 "source" changeset or as a "base" changeset. Both are shorthand
46 "source" changeset or as a "base" changeset. Both are shorthand
47 for a topologically related set of changesets (the "source
47 for a topologically related set of changesets (the "source
48 branch"). If you specify source (``-s/--source``), rebase will
48 branch"). If you specify source (``-s/--source``), rebase will
49 rebase that changeset and all of its descendants onto dest. If you
49 rebase that changeset and all of its descendants onto dest. If you
50 specify base (``-b/--base``), rebase will select ancestors of base
50 specify base (``-b/--base``), rebase will select ancestors of base
51 back to but not including the common ancestor with dest. Thus,
51 back to but not including the common ancestor with dest. Thus,
52 ``-b`` is less precise but more convenient than ``-s``: you can
52 ``-b`` is less precise but more convenient than ``-s``: you can
53 specify any changeset in the source branch, and rebase will select
53 specify any changeset in the source branch, and rebase will select
54 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
54 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
55 uses the parent of the working directory as the base.
55 uses the parent of the working directory as the base.
56
56
57 By default, rebase recreates the changesets in the source branch
57 By default, rebase recreates the changesets in the source branch
58 as descendants of dest and then destroys the originals. Use
58 as descendants of dest and then destroys the originals. Use
59 ``--keep`` to preserve the original source changesets. Some
59 ``--keep`` to preserve the original source changesets. Some
60 changesets in the source branch (e.g. merges from the destination
60 changesets in the source branch (e.g. merges from the destination
61 branch) may be dropped if they no longer contribute any change.
61 branch) may be dropped if they no longer contribute any change.
62
62
63 One result of the rules for selecting the destination changeset
63 One result of the rules for selecting the destination changeset
64 and source branch is that, unlike ``merge``, rebase will do
64 and source branch is that, unlike ``merge``, rebase will do
65 nothing if you are at the latest (tipmost) head of a named branch
65 nothing if you are at the latest (tipmost) head of a named branch
66 with two heads. You need to explicitly specify source and/or
66 with two heads. You need to explicitly specify source and/or
67 destination (or ``update`` to the other head, if it's the head of
67 destination (or ``update`` to the other head, if it's the head of
68 the intended source branch).
68 the intended source branch).
69
69
70 If a rebase is interrupted to manually resolve a merge, it can be
70 If a rebase is interrupted to manually resolve a merge, it can be
71 continued with --continue/-c or aborted with --abort/-a.
71 continued with --continue/-c or aborted with --abort/-a.
72
72
73 Returns 0 on success, 1 if nothing to rebase.
73 Returns 0 on success, 1 if nothing to rebase.
74 """
74 """
75 originalwd = target = None
75 originalwd = target = None
76 external = nullrev
76 external = nullrev
77 state = {}
77 state = {}
78 skipped = set()
78 skipped = set()
79 targetancestors = set()
79 targetancestors = set()
80
80
81 lock = wlock = None
81 lock = wlock = None
82 try:
82 try:
83 lock = repo.lock()
83 lock = repo.lock()
84 wlock = repo.wlock()
84 wlock = repo.wlock()
85
85
86 # Validate input and define rebasing points
86 # Validate input and define rebasing points
87 destf = opts.get('dest', None)
87 destf = opts.get('dest', None)
88 srcf = opts.get('source', None)
88 srcf = opts.get('source', None)
89 basef = opts.get('base', None)
89 basef = opts.get('base', None)
90 contf = opts.get('continue')
90 contf = opts.get('continue')
91 abortf = opts.get('abort')
91 abortf = opts.get('abort')
92 collapsef = opts.get('collapse', False)
92 collapsef = opts.get('collapse', False)
93 collapsemsg = cmdutil.logmessage(opts)
93 collapsemsg = cmdutil.logmessage(opts)
94 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
94 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
95 keepf = opts.get('keep', False)
95 keepf = opts.get('keep', False)
96 keepbranchesf = opts.get('keepbranches', False)
96 keepbranchesf = opts.get('keepbranches', False)
97 detachf = opts.get('detach', False)
97 detachf = opts.get('detach', False)
98 # keepopen is not meant for use on the command line, but by
98 # keepopen is not meant for use on the command line, but by
99 # other extensions
99 # other extensions
100 keepopen = opts.get('keepopen', False)
100 keepopen = opts.get('keepopen', False)
101
101
102 if collapsemsg and not collapsef:
102 if collapsemsg and not collapsef:
103 raise util.Abort(
103 raise util.Abort(
104 _('message can only be specified with collapse'))
104 _('message can only be specified with collapse'))
105
105
106 if contf or abortf:
106 if contf or abortf:
107 if contf and abortf:
107 if contf and abortf:
108 raise util.Abort(_('cannot use both abort and continue'))
108 raise util.Abort(_('cannot use both abort and continue'))
109 if collapsef:
109 if collapsef:
110 raise util.Abort(
110 raise util.Abort(
111 _('cannot use collapse with continue or abort'))
111 _('cannot use collapse with continue or abort'))
112 if detachf:
112 if detachf:
113 raise util.Abort(_('cannot use detach with continue or abort'))
113 raise util.Abort(_('cannot use detach with continue or abort'))
114 if srcf or basef or destf:
114 if srcf or basef or destf:
115 raise util.Abort(
115 raise util.Abort(
116 _('abort and continue do not allow specifying revisions'))
116 _('abort and continue do not allow specifying revisions'))
117
117
118 (originalwd, target, state, skipped, collapsef, keepf,
118 (originalwd, target, state, skipped, collapsef, keepf,
119 keepbranchesf, external) = restorestatus(repo)
119 keepbranchesf, external) = restorestatus(repo)
120 if abortf:
120 if abortf:
121 return abort(repo, originalwd, target, state)
121 return abort(repo, originalwd, target, state)
122 else:
122 else:
123 if srcf and basef:
123 if srcf and basef:
124 raise util.Abort(_('cannot specify both a '
124 raise util.Abort(_('cannot specify both a '
125 'revision and a base'))
125 'revision and a base'))
126 if detachf:
126 if detachf:
127 if not srcf:
127 if not srcf:
128 raise util.Abort(
128 raise util.Abort(
129 _('detach requires a revision to be specified'))
129 _('detach requires a revision to be specified'))
130 if basef:
130 if basef:
131 raise util.Abort(_('cannot specify a base with detach'))
131 raise util.Abort(_('cannot specify a base with detach'))
132
132
133 cmdutil.bail_if_changed(repo)
133 cmdutil.bail_if_changed(repo)
134 result = buildstate(repo, destf, srcf, basef, detachf)
134 result = buildstate(repo, destf, srcf, basef, detachf)
135 if not result:
135 if not result:
136 # Empty state built, nothing to rebase
136 # Empty state built, nothing to rebase
137 ui.status(_('nothing to rebase\n'))
137 ui.status(_('nothing to rebase\n'))
138 return 1
138 return 1
139 else:
139 else:
140 originalwd, target, state = result
140 originalwd, target, state = result
141 if collapsef:
141 if collapsef:
142 targetancestors = set(repo.changelog.ancestors(target))
142 targetancestors = set(repo.changelog.ancestors(target))
143 external = checkexternal(repo, state, targetancestors)
143 external = checkexternal(repo, state, targetancestors)
144
144
145 if keepbranchesf:
145 if keepbranchesf:
146 assert not extrafn, 'cannot use both keepbranches and extrafn'
146 assert not extrafn, 'cannot use both keepbranches and extrafn'
147 def extrafn(ctx, extra):
147 def extrafn(ctx, extra):
148 extra['branch'] = ctx.branch()
148 extra['branch'] = ctx.branch()
149
149
150 # Rebase
150 # Rebase
151 if not targetancestors:
151 if not targetancestors:
152 targetancestors = set(repo.changelog.ancestors(target))
152 targetancestors = set(repo.changelog.ancestors(target))
153 targetancestors.add(target)
153 targetancestors.add(target)
154
154
155 sortedstate = sorted(state)
155 sortedstate = sorted(state)
156 total = len(sortedstate)
156 total = len(sortedstate)
157 pos = 0
157 pos = 0
158 for rev in sortedstate:
158 for rev in sortedstate:
159 pos += 1
159 pos += 1
160 if state[rev] == -1:
160 if state[rev] == -1:
161 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
161 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
162 _('changesets'), total)
162 _('changesets'), total)
163 storestatus(repo, originalwd, target, state, collapsef, keepf,
163 storestatus(repo, originalwd, target, state, collapsef, keepf,
164 keepbranchesf, external)
164 keepbranchesf, external)
165 p1, p2 = defineparents(repo, rev, target, state,
165 p1, p2 = defineparents(repo, rev, target, state,
166 targetancestors)
166 targetancestors)
167 if len(repo.parents()) == 2:
167 if len(repo.parents()) == 2:
168 repo.ui.debug('resuming interrupted rebase\n')
168 repo.ui.debug('resuming interrupted rebase\n')
169 else:
169 else:
170 stats = rebasenode(repo, rev, p1, p2, state)
170 stats = rebasenode(repo, rev, p1, p2, state)
171 if stats and stats[3] > 0:
171 if stats and stats[3] > 0:
172 raise util.Abort(_('unresolved conflicts (see hg '
172 raise util.Abort(_('unresolved conflicts (see hg '
173 'resolve, then hg rebase --continue)'))
173 'resolve, then hg rebase --continue)'))
174 updatedirstate(repo, rev, target, p2)
174 updatedirstate(repo, rev, target, p2)
175 if not collapsef:
175 if not collapsef:
176 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn)
176 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn)
177 else:
177 else:
178 # Skip commit if we are collapsing
178 # Skip commit if we are collapsing
179 repo.dirstate.setparents(repo[p1].node())
179 repo.dirstate.setparents(repo[p1].node())
180 newrev = None
180 newrev = None
181 # Update the state
181 # Update the state
182 if newrev is not None:
182 if newrev is not None:
183 state[rev] = repo[newrev].rev()
183 state[rev] = repo[newrev].rev()
184 else:
184 else:
185 if not collapsef:
185 if not collapsef:
186 ui.note(_('no changes, revision %d skipped\n') % rev)
186 ui.note(_('no changes, revision %d skipped\n') % rev)
187 ui.debug('next revision set to %s\n' % p1)
187 ui.debug('next revision set to %s\n' % p1)
188 skipped.add(rev)
188 skipped.add(rev)
189 state[rev] = p1
189 state[rev] = p1
190
190
191 ui.progress(_('rebasing'), None)
191 ui.progress(_('rebasing'), None)
192 ui.note(_('rebase merging completed\n'))
192 ui.note(_('rebase merging completed\n'))
193
193
194 if collapsef and not keepopen:
194 if collapsef and not keepopen:
195 p1, p2 = defineparents(repo, min(state), target,
195 p1, p2 = defineparents(repo, min(state), target,
196 state, targetancestors)
196 state, targetancestors)
197 if collapsemsg:
197 if collapsemsg:
198 commitmsg = collapsemsg
198 commitmsg = collapsemsg
199 else:
199 else:
200 commitmsg = 'Collapsed revision'
200 commitmsg = 'Collapsed revision'
201 for rebased in state:
201 for rebased in state:
202 if rebased not in skipped and state[rebased] != nullmerge:
202 if rebased not in skipped and state[rebased] != nullmerge:
203 commitmsg += '\n* %s' % repo[rebased].description()
203 commitmsg += '\n* %s' % repo[rebased].description()
204 commitmsg = ui.edit(commitmsg, repo.ui.username())
204 commitmsg = ui.edit(commitmsg, repo.ui.username())
205 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
205 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
206 extrafn=extrafn)
206 extrafn=extrafn)
207
207
208 if 'qtip' in repo.tags():
208 if 'qtip' in repo.tags():
209 updatemq(repo, state, skipped, **opts)
209 updatemq(repo, state, skipped, **opts)
210
210
211 if not keepf:
211 if not keepf:
212 # Remove no more useful revisions
212 # Remove no more useful revisions
213 rebased = [rev for rev in state if state[rev] != nullmerge]
213 rebased = [rev for rev in state if state[rev] != nullmerge]
214 if rebased:
214 if rebased:
215 if set(repo.changelog.descendants(min(rebased))) - set(state):
215 if set(repo.changelog.descendants(min(rebased))) - set(state):
216 ui.warn(_("warning: new changesets detected "
216 ui.warn(_("warning: new changesets detected "
217 "on source branch, not stripping\n"))
217 "on source branch, not stripping\n"))
218 else:
218 else:
219 # backup the old csets by default
219 # backup the old csets by default
220 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
220 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
221
221
222 clearstatus(repo)
222 clearstatus(repo)
223 ui.note(_("rebase completed\n"))
223 ui.note(_("rebase completed\n"))
224 if os.path.exists(repo.sjoin('undo')):
224 if os.path.exists(repo.sjoin('undo')):
225 util.unlinkpath(repo.sjoin('undo'))
225 util.unlinkpath(repo.sjoin('undo'))
226 if skipped:
226 if skipped:
227 ui.note(_("%d revisions have been skipped\n") % len(skipped))
227 ui.note(_("%d revisions have been skipped\n") % len(skipped))
228 finally:
228 finally:
229 release(lock, wlock)
229 release(lock, wlock)
230
230
231 def rebasemerge(repo, rev, first=False):
231 def rebasemerge(repo, rev, first=False):
232 'return the correct ancestor'
232 'return the correct ancestor'
233 oldancestor = ancestor.ancestor
233 oldancestor = ancestor.ancestor
234
234
235 def newancestor(a, b, pfunc):
235 def newancestor(a, b, pfunc):
236 if b == rev:
236 if b == rev:
237 return repo[rev].parents()[0].rev()
237 return repo[rev].parents()[0].rev()
238 return oldancestor(a, b, pfunc)
238 return oldancestor(a, b, pfunc)
239
239
240 if not first:
240 if not first:
241 ancestor.ancestor = newancestor
241 ancestor.ancestor = newancestor
242 else:
242 else:
243 repo.ui.debug("first revision, do not change ancestor\n")
243 repo.ui.debug("first revision, do not change ancestor\n")
244 try:
244 try:
245 stats = merge.update(repo, rev, True, True, False)
245 stats = merge.update(repo, rev, True, True, False)
246 return stats
246 return stats
247 finally:
247 finally:
248 ancestor.ancestor = oldancestor
248 ancestor.ancestor = oldancestor
249
249
250 def checkexternal(repo, state, targetancestors):
250 def checkexternal(repo, state, targetancestors):
251 """Check whether one or more external revisions need to be taken in
251 """Check whether one or more external revisions need to be taken in
252 consideration. In the latter case, abort.
252 consideration. In the latter case, abort.
253 """
253 """
254 external = nullrev
254 external = nullrev
255 source = min(state)
255 source = min(state)
256 for rev in state:
256 for rev in state:
257 if rev == source:
257 if rev == source:
258 continue
258 continue
259 # Check externals and fail if there are more than one
259 # Check externals and fail if there are more than one
260 for p in repo[rev].parents():
260 for p in repo[rev].parents():
261 if (p.rev() not in state
261 if (p.rev() not in state
262 and p.rev() not in targetancestors):
262 and p.rev() not in targetancestors):
263 if external != nullrev:
263 if external != nullrev:
264 raise util.Abort(_('unable to collapse, there is more '
264 raise util.Abort(_('unable to collapse, there is more '
265 'than one external parent'))
265 'than one external parent'))
266 external = p.rev()
266 external = p.rev()
267 return external
267 return external
268
268
269 def updatedirstate(repo, rev, p1, p2):
269 def updatedirstate(repo, rev, p1, p2):
270 """Keep track of renamed files in the revision that is going to be rebased
270 """Keep track of renamed files in the revision that is going to be rebased
271 """
271 """
272 # Here we simulate the copies and renames in the source changeset
272 # Here we simulate the copies and renames in the source changeset
273 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
273 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
274 m1 = repo[rev].manifest()
274 m1 = repo[rev].manifest()
275 m2 = repo[p1].manifest()
275 m2 = repo[p1].manifest()
276 for k, v in cop.iteritems():
276 for k, v in cop.iteritems():
277 if k in m1:
277 if k in m1:
278 if v in m1 or v in m2:
278 if v in m1 or v in m2:
279 repo.dirstate.copy(v, k)
279 repo.dirstate.copy(v, k)
280 if v in m2 and v not in m1:
280 if v in m2 and v not in m1 and k in m2:
281 repo.dirstate.remove(v)
281 repo.dirstate.remove(v)
282
282
283 def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None):
283 def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None):
284 'Commit the changes and store useful information in extra'
284 'Commit the changes and store useful information in extra'
285 try:
285 try:
286 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
286 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
287 ctx = repo[rev]
287 ctx = repo[rev]
288 if commitmsg is None:
288 if commitmsg is None:
289 commitmsg = ctx.description()
289 commitmsg = ctx.description()
290 extra = {'rebase_source': ctx.hex()}
290 extra = {'rebase_source': ctx.hex()}
291 if extrafn:
291 if extrafn:
292 extrafn(ctx, extra)
292 extrafn(ctx, extra)
293 # Commit might fail if unresolved files exist
293 # Commit might fail if unresolved files exist
294 newrev = repo.commit(text=commitmsg, user=ctx.user(),
294 newrev = repo.commit(text=commitmsg, user=ctx.user(),
295 date=ctx.date(), extra=extra)
295 date=ctx.date(), extra=extra)
296 repo.dirstate.setbranch(repo[newrev].branch())
296 repo.dirstate.setbranch(repo[newrev].branch())
297 return newrev
297 return newrev
298 except util.Abort:
298 except util.Abort:
299 # Invalidate the previous setparents
299 # Invalidate the previous setparents
300 repo.dirstate.invalidate()
300 repo.dirstate.invalidate()
301 raise
301 raise
302
302
303 def rebasenode(repo, rev, p1, p2, state):
303 def rebasenode(repo, rev, p1, p2, state):
304 'Rebase a single revision'
304 'Rebase a single revision'
305 # Merge phase
305 # Merge phase
306 # Update to target and merge it with local
306 # Update to target and merge it with local
307 if repo['.'].rev() != repo[p1].rev():
307 if repo['.'].rev() != repo[p1].rev():
308 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
308 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
309 merge.update(repo, p1, False, True, False)
309 merge.update(repo, p1, False, True, False)
310 else:
310 else:
311 repo.ui.debug(" already in target\n")
311 repo.ui.debug(" already in target\n")
312 repo.dirstate.write()
312 repo.dirstate.write()
313 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
313 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
314 first = repo[rev].rev() == repo[min(state)].rev()
314 first = repo[rev].rev() == repo[min(state)].rev()
315 stats = rebasemerge(repo, rev, first)
315 stats = rebasemerge(repo, rev, first)
316 return stats
316 return stats
317
317
318 def defineparents(repo, rev, target, state, targetancestors):
318 def defineparents(repo, rev, target, state, targetancestors):
319 'Return the new parent relationship of the revision that will be rebased'
319 'Return the new parent relationship of the revision that will be rebased'
320 parents = repo[rev].parents()
320 parents = repo[rev].parents()
321 p1 = p2 = nullrev
321 p1 = p2 = nullrev
322
322
323 P1n = parents[0].rev()
323 P1n = parents[0].rev()
324 if P1n in targetancestors:
324 if P1n in targetancestors:
325 p1 = target
325 p1 = target
326 elif P1n in state:
326 elif P1n in state:
327 if state[P1n] == nullmerge:
327 if state[P1n] == nullmerge:
328 p1 = target
328 p1 = target
329 else:
329 else:
330 p1 = state[P1n]
330 p1 = state[P1n]
331 else: # P1n external
331 else: # P1n external
332 p1 = target
332 p1 = target
333 p2 = P1n
333 p2 = P1n
334
334
335 if len(parents) == 2 and parents[1].rev() not in targetancestors:
335 if len(parents) == 2 and parents[1].rev() not in targetancestors:
336 P2n = parents[1].rev()
336 P2n = parents[1].rev()
337 # interesting second parent
337 # interesting second parent
338 if P2n in state:
338 if P2n in state:
339 if p1 == target: # P1n in targetancestors or external
339 if p1 == target: # P1n in targetancestors or external
340 p1 = state[P2n]
340 p1 = state[P2n]
341 else:
341 else:
342 p2 = state[P2n]
342 p2 = state[P2n]
343 else: # P2n external
343 else: # P2n external
344 if p2 != nullrev: # P1n external too => rev is a merged revision
344 if p2 != nullrev: # P1n external too => rev is a merged revision
345 raise util.Abort(_('cannot use revision %d as base, result '
345 raise util.Abort(_('cannot use revision %d as base, result '
346 'would have 3 parents') % rev)
346 'would have 3 parents') % rev)
347 p2 = P2n
347 p2 = P2n
348 repo.ui.debug(" future parents are %d and %d\n" %
348 repo.ui.debug(" future parents are %d and %d\n" %
349 (repo[p1].rev(), repo[p2].rev()))
349 (repo[p1].rev(), repo[p2].rev()))
350 return p1, p2
350 return p1, p2
351
351
352 def isagitpatch(repo, patchname):
352 def isagitpatch(repo, patchname):
353 'Return true if the given patch is in git format'
353 'Return true if the given patch is in git format'
354 mqpatch = os.path.join(repo.mq.path, patchname)
354 mqpatch = os.path.join(repo.mq.path, patchname)
355 for line in patch.linereader(file(mqpatch, 'rb')):
355 for line in patch.linereader(file(mqpatch, 'rb')):
356 if line.startswith('diff --git'):
356 if line.startswith('diff --git'):
357 return True
357 return True
358 return False
358 return False
359
359
360 def updatemq(repo, state, skipped, **opts):
360 def updatemq(repo, state, skipped, **opts):
361 'Update rebased mq patches - finalize and then import them'
361 'Update rebased mq patches - finalize and then import them'
362 mqrebase = {}
362 mqrebase = {}
363 mq = repo.mq
363 mq = repo.mq
364 original_series = mq.full_series[:]
364 original_series = mq.full_series[:]
365
365
366 for p in mq.applied:
366 for p in mq.applied:
367 rev = repo[p.node].rev()
367 rev = repo[p.node].rev()
368 if rev in state:
368 if rev in state:
369 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
369 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
370 (rev, p.name))
370 (rev, p.name))
371 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
371 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
372
372
373 if mqrebase:
373 if mqrebase:
374 mq.finish(repo, mqrebase.keys())
374 mq.finish(repo, mqrebase.keys())
375
375
376 # We must start import from the newest revision
376 # We must start import from the newest revision
377 for rev in sorted(mqrebase, reverse=True):
377 for rev in sorted(mqrebase, reverse=True):
378 if rev not in skipped:
378 if rev not in skipped:
379 name, isgit = mqrebase[rev]
379 name, isgit = mqrebase[rev]
380 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
380 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
381 mq.qimport(repo, (), patchname=name, git=isgit,
381 mq.qimport(repo, (), patchname=name, git=isgit,
382 rev=[str(state[rev])])
382 rev=[str(state[rev])])
383
383
384 # Restore missing guards
384 # Restore missing guards
385 for s in original_series:
385 for s in original_series:
386 pname = mq.guard_re.split(s, 1)[0]
386 pname = mq.guard_re.split(s, 1)[0]
387 if pname in mq.full_series:
387 if pname in mq.full_series:
388 repo.ui.debug('restoring guard for patch %s' % (pname))
388 repo.ui.debug('restoring guard for patch %s' % (pname))
389 mq.full_series.remove(pname)
389 mq.full_series.remove(pname)
390 mq.full_series.append(s)
390 mq.full_series.append(s)
391 mq.series_dirty = True
391 mq.series_dirty = True
392 mq.save_dirty()
392 mq.save_dirty()
393
393
394 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
394 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
395 external):
395 external):
396 'Store the current status to allow recovery'
396 'Store the current status to allow recovery'
397 f = repo.opener("rebasestate", "w")
397 f = repo.opener("rebasestate", "w")
398 f.write(repo[originalwd].hex() + '\n')
398 f.write(repo[originalwd].hex() + '\n')
399 f.write(repo[target].hex() + '\n')
399 f.write(repo[target].hex() + '\n')
400 f.write(repo[external].hex() + '\n')
400 f.write(repo[external].hex() + '\n')
401 f.write('%d\n' % int(collapse))
401 f.write('%d\n' % int(collapse))
402 f.write('%d\n' % int(keep))
402 f.write('%d\n' % int(keep))
403 f.write('%d\n' % int(keepbranches))
403 f.write('%d\n' % int(keepbranches))
404 for d, v in state.iteritems():
404 for d, v in state.iteritems():
405 oldrev = repo[d].hex()
405 oldrev = repo[d].hex()
406 newrev = repo[v].hex()
406 newrev = repo[v].hex()
407 f.write("%s:%s\n" % (oldrev, newrev))
407 f.write("%s:%s\n" % (oldrev, newrev))
408 f.close()
408 f.close()
409 repo.ui.debug('rebase status stored\n')
409 repo.ui.debug('rebase status stored\n')
410
410
411 def clearstatus(repo):
411 def clearstatus(repo):
412 'Remove the status files'
412 'Remove the status files'
413 if os.path.exists(repo.join("rebasestate")):
413 if os.path.exists(repo.join("rebasestate")):
414 util.unlinkpath(repo.join("rebasestate"))
414 util.unlinkpath(repo.join("rebasestate"))
415
415
416 def restorestatus(repo):
416 def restorestatus(repo):
417 'Restore a previously stored status'
417 'Restore a previously stored status'
418 try:
418 try:
419 target = None
419 target = None
420 collapse = False
420 collapse = False
421 external = nullrev
421 external = nullrev
422 state = {}
422 state = {}
423 f = repo.opener("rebasestate")
423 f = repo.opener("rebasestate")
424 for i, l in enumerate(f.read().splitlines()):
424 for i, l in enumerate(f.read().splitlines()):
425 if i == 0:
425 if i == 0:
426 originalwd = repo[l].rev()
426 originalwd = repo[l].rev()
427 elif i == 1:
427 elif i == 1:
428 target = repo[l].rev()
428 target = repo[l].rev()
429 elif i == 2:
429 elif i == 2:
430 external = repo[l].rev()
430 external = repo[l].rev()
431 elif i == 3:
431 elif i == 3:
432 collapse = bool(int(l))
432 collapse = bool(int(l))
433 elif i == 4:
433 elif i == 4:
434 keep = bool(int(l))
434 keep = bool(int(l))
435 elif i == 5:
435 elif i == 5:
436 keepbranches = bool(int(l))
436 keepbranches = bool(int(l))
437 else:
437 else:
438 oldrev, newrev = l.split(':')
438 oldrev, newrev = l.split(':')
439 state[repo[oldrev].rev()] = repo[newrev].rev()
439 state[repo[oldrev].rev()] = repo[newrev].rev()
440 skipped = set()
440 skipped = set()
441 # recompute the set of skipped revs
441 # recompute the set of skipped revs
442 if not collapse:
442 if not collapse:
443 seen = set([target])
443 seen = set([target])
444 for old, new in sorted(state.items()):
444 for old, new in sorted(state.items()):
445 if new != nullrev and new in seen:
445 if new != nullrev and new in seen:
446 skipped.add(old)
446 skipped.add(old)
447 seen.add(new)
447 seen.add(new)
448 repo.ui.debug('computed skipped revs: %s\n' % skipped)
448 repo.ui.debug('computed skipped revs: %s\n' % skipped)
449 repo.ui.debug('rebase status resumed\n')
449 repo.ui.debug('rebase status resumed\n')
450 return (originalwd, target, state, skipped,
450 return (originalwd, target, state, skipped,
451 collapse, keep, keepbranches, external)
451 collapse, keep, keepbranches, external)
452 except IOError, err:
452 except IOError, err:
453 if err.errno != errno.ENOENT:
453 if err.errno != errno.ENOENT:
454 raise
454 raise
455 raise util.Abort(_('no rebase in progress'))
455 raise util.Abort(_('no rebase in progress'))
456
456
457 def abort(repo, originalwd, target, state):
457 def abort(repo, originalwd, target, state):
458 'Restore the repository to its original state'
458 'Restore the repository to its original state'
459 if set(repo.changelog.descendants(target)) - set(state.values()):
459 if set(repo.changelog.descendants(target)) - set(state.values()):
460 repo.ui.warn(_("warning: new changesets detected on target branch, "
460 repo.ui.warn(_("warning: new changesets detected on target branch, "
461 "can't abort\n"))
461 "can't abort\n"))
462 return -1
462 return -1
463 else:
463 else:
464 # Strip from the first rebased revision
464 # Strip from the first rebased revision
465 merge.update(repo, repo[originalwd].rev(), False, True, False)
465 merge.update(repo, repo[originalwd].rev(), False, True, False)
466 rebased = filter(lambda x: x > -1 and x != target, state.values())
466 rebased = filter(lambda x: x > -1 and x != target, state.values())
467 if rebased:
467 if rebased:
468 strippoint = min(rebased)
468 strippoint = min(rebased)
469 # no backup of rebased cset versions needed
469 # no backup of rebased cset versions needed
470 repair.strip(repo.ui, repo, repo[strippoint].node())
470 repair.strip(repo.ui, repo, repo[strippoint].node())
471 clearstatus(repo)
471 clearstatus(repo)
472 repo.ui.warn(_('rebase aborted\n'))
472 repo.ui.warn(_('rebase aborted\n'))
473 return 0
473 return 0
474
474
475 def buildstate(repo, dest, src, base, detach):
475 def buildstate(repo, dest, src, base, detach):
476 'Define which revisions are going to be rebased and where'
476 'Define which revisions are going to be rebased and where'
477 targetancestors = set()
477 targetancestors = set()
478 detachset = set()
478 detachset = set()
479
479
480 if not dest:
480 if not dest:
481 # Destination defaults to the latest revision in the current branch
481 # Destination defaults to the latest revision in the current branch
482 branch = repo[None].branch()
482 branch = repo[None].branch()
483 dest = repo[branch].rev()
483 dest = repo[branch].rev()
484 else:
484 else:
485 dest = repo[dest].rev()
485 dest = repo[dest].rev()
486
486
487 # This check isn't strictly necessary, since mq detects commits over an
487 # This check isn't strictly necessary, since mq detects commits over an
488 # applied patch. But it prevents messing up the working directory when
488 # applied patch. But it prevents messing up the working directory when
489 # a partially completed rebase is blocked by mq.
489 # a partially completed rebase is blocked by mq.
490 if 'qtip' in repo.tags() and (repo[dest].node() in
490 if 'qtip' in repo.tags() and (repo[dest].node() in
491 [s.node for s in repo.mq.applied]):
491 [s.node for s in repo.mq.applied]):
492 raise util.Abort(_('cannot rebase onto an applied mq patch'))
492 raise util.Abort(_('cannot rebase onto an applied mq patch'))
493
493
494 if src:
494 if src:
495 commonbase = repo[src].ancestor(repo[dest])
495 commonbase = repo[src].ancestor(repo[dest])
496 samebranch = repo[src].branch() == repo[dest].branch()
496 samebranch = repo[src].branch() == repo[dest].branch()
497 if commonbase == repo[src]:
497 if commonbase == repo[src]:
498 raise util.Abort(_('source is ancestor of destination'))
498 raise util.Abort(_('source is ancestor of destination'))
499 if samebranch and commonbase == repo[dest]:
499 if samebranch and commonbase == repo[dest]:
500 raise util.Abort(_('source is descendant of destination'))
500 raise util.Abort(_('source is descendant of destination'))
501 source = repo[src].rev()
501 source = repo[src].rev()
502 if detach:
502 if detach:
503 # We need to keep track of source's ancestors up to the common base
503 # We need to keep track of source's ancestors up to the common base
504 srcancestors = set(repo.changelog.ancestors(source))
504 srcancestors = set(repo.changelog.ancestors(source))
505 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
505 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
506 detachset = srcancestors - baseancestors
506 detachset = srcancestors - baseancestors
507 detachset.discard(commonbase.rev())
507 detachset.discard(commonbase.rev())
508 else:
508 else:
509 if base:
509 if base:
510 cwd = repo[base].rev()
510 cwd = repo[base].rev()
511 else:
511 else:
512 cwd = repo['.'].rev()
512 cwd = repo['.'].rev()
513
513
514 if cwd == dest:
514 if cwd == dest:
515 repo.ui.debug('source and destination are the same\n')
515 repo.ui.debug('source and destination are the same\n')
516 return None
516 return None
517
517
518 targetancestors = set(repo.changelog.ancestors(dest))
518 targetancestors = set(repo.changelog.ancestors(dest))
519 if cwd in targetancestors:
519 if cwd in targetancestors:
520 repo.ui.debug('source is ancestor of destination\n')
520 repo.ui.debug('source is ancestor of destination\n')
521 return None
521 return None
522
522
523 cwdancestors = set(repo.changelog.ancestors(cwd))
523 cwdancestors = set(repo.changelog.ancestors(cwd))
524 if dest in cwdancestors:
524 if dest in cwdancestors:
525 repo.ui.debug('source is descendant of destination\n')
525 repo.ui.debug('source is descendant of destination\n')
526 return None
526 return None
527
527
528 cwdancestors.add(cwd)
528 cwdancestors.add(cwd)
529 rebasingbranch = cwdancestors - targetancestors
529 rebasingbranch = cwdancestors - targetancestors
530 source = min(rebasingbranch)
530 source = min(rebasingbranch)
531
531
532 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
532 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
533 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
533 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
534 state.update(dict.fromkeys(detachset, nullmerge))
534 state.update(dict.fromkeys(detachset, nullmerge))
535 state[source] = nullrev
535 state[source] = nullrev
536 return repo['.'].rev(), repo[dest].rev(), state
536 return repo['.'].rev(), repo[dest].rev(), state
537
537
538 def pullrebase(orig, ui, repo, *args, **opts):
538 def pullrebase(orig, ui, repo, *args, **opts):
539 'Call rebase after pull if the latter has been invoked with --rebase'
539 'Call rebase after pull if the latter has been invoked with --rebase'
540 if opts.get('rebase'):
540 if opts.get('rebase'):
541 if opts.get('update'):
541 if opts.get('update'):
542 del opts['update']
542 del opts['update']
543 ui.debug('--update and --rebase are not compatible, ignoring '
543 ui.debug('--update and --rebase are not compatible, ignoring '
544 'the update flag\n')
544 'the update flag\n')
545
545
546 cmdutil.bail_if_changed(repo)
546 cmdutil.bail_if_changed(repo)
547 revsprepull = len(repo)
547 revsprepull = len(repo)
548 origpostincoming = commands.postincoming
548 origpostincoming = commands.postincoming
549 def _dummy(*args, **kwargs):
549 def _dummy(*args, **kwargs):
550 pass
550 pass
551 commands.postincoming = _dummy
551 commands.postincoming = _dummy
552 try:
552 try:
553 orig(ui, repo, *args, **opts)
553 orig(ui, repo, *args, **opts)
554 finally:
554 finally:
555 commands.postincoming = origpostincoming
555 commands.postincoming = origpostincoming
556 revspostpull = len(repo)
556 revspostpull = len(repo)
557 if revspostpull > revsprepull:
557 if revspostpull > revsprepull:
558 rebase(ui, repo, **opts)
558 rebase(ui, repo, **opts)
559 branch = repo[None].branch()
559 branch = repo[None].branch()
560 dest = repo[branch].rev()
560 dest = repo[branch].rev()
561 if dest != repo['.'].rev():
561 if dest != repo['.'].rev():
562 # there was nothing to rebase we force an update
562 # there was nothing to rebase we force an update
563 hg.update(repo, dest)
563 hg.update(repo, dest)
564 else:
564 else:
565 orig(ui, repo, *args, **opts)
565 orig(ui, repo, *args, **opts)
566
566
567 def uisetup(ui):
567 def uisetup(ui):
568 'Replace pull with a decorator to provide --rebase option'
568 'Replace pull with a decorator to provide --rebase option'
569 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
569 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
570 entry[1].append(('', 'rebase', None,
570 entry[1].append(('', 'rebase', None,
571 _("rebase working directory to branch head"))
571 _("rebase working directory to branch head"))
572 )
572 )
573
573
574 cmdtable = {
574 cmdtable = {
575 "rebase":
575 "rebase":
576 (rebase,
576 (rebase,
577 [
577 [
578 ('s', 'source', '',
578 ('s', 'source', '',
579 _('rebase from the specified changeset'), _('REV')),
579 _('rebase from the specified changeset'), _('REV')),
580 ('b', 'base', '',
580 ('b', 'base', '',
581 _('rebase from the base of the specified changeset '
581 _('rebase from the base of the specified changeset '
582 '(up to greatest common ancestor of base and dest)'),
582 '(up to greatest common ancestor of base and dest)'),
583 _('REV')),
583 _('REV')),
584 ('d', 'dest', '',
584 ('d', 'dest', '',
585 _('rebase onto the specified changeset'), _('REV')),
585 _('rebase onto the specified changeset'), _('REV')),
586 ('', 'collapse', False, _('collapse the rebased changesets')),
586 ('', 'collapse', False, _('collapse the rebased changesets')),
587 ('m', 'message', '',
587 ('m', 'message', '',
588 _('use text as collapse commit message'), _('TEXT')),
588 _('use text as collapse commit message'), _('TEXT')),
589 ('l', 'logfile', '',
589 ('l', 'logfile', '',
590 _('read collapse commit message from file'), _('FILE')),
590 _('read collapse commit message from file'), _('FILE')),
591 ('', 'keep', False, _('keep original changesets')),
591 ('', 'keep', False, _('keep original changesets')),
592 ('', 'keepbranches', False, _('keep original branch names')),
592 ('', 'keepbranches', False, _('keep original branch names')),
593 ('', 'detach', False, _('force detaching of source from its original '
593 ('', 'detach', False, _('force detaching of source from its original '
594 'branch')),
594 'branch')),
595 ('c', 'continue', False, _('continue an interrupted rebase')),
595 ('c', 'continue', False, _('continue an interrupted rebase')),
596 ('a', 'abort', False, _('abort an interrupted rebase'))] +
596 ('a', 'abort', False, _('abort an interrupted rebase'))] +
597 templateopts,
597 templateopts,
598 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
598 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
599 'hg rebase {-a|-c}'))
599 'hg rebase {-a|-c}'))
600 }
600 }
@@ -1,121 +1,170 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > graphlog=
3 > graphlog=
4 > rebase=
4 > rebase=
5 >
5 >
6 > [alias]
6 > [alias]
7 > tlog = log --template "{rev}: '{desc}' {branches}\n"
7 > tlog = log --template "{rev}: '{desc}' {branches}\n"
8 > tglog = tlog --graph
8 > tglog = tlog --graph
9 > EOF
9 > EOF
10
10
11
11
12 $ hg init a
12 $ hg init a
13 $ cd a
13 $ cd a
14
14
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 > b
19 $ echo b > b
20 $ hg ci -Am B
20 $ hg ci -Am B
21 adding b
21 adding b
22
22
23 $ hg up -q -C 0
23 $ hg up -q -C 0
24
24
25 $ hg mv a a-renamed
25 $ hg mv a a-renamed
26
26
27 $ hg ci -m 'rename A'
27 $ hg ci -m 'rename A'
28 created new head
28 created new head
29
29
30 $ hg tglog
30 $ hg tglog
31 @ 2: 'rename A'
31 @ 2: 'rename A'
32 |
32 |
33 | o 1: 'B'
33 | o 1: 'B'
34 |/
34 |/
35 o 0: 'A'
35 o 0: 'A'
36
36
37
37
38 Rename is tracked:
38 Rename is tracked:
39
39
40 $ hg tlog -p --git -r tip
40 $ hg tlog -p --git -r tip
41 2: 'rename A'
41 2: 'rename A'
42 diff --git a/a b/a-renamed
42 diff --git a/a b/a-renamed
43 rename from a
43 rename from a
44 rename to a-renamed
44 rename to a-renamed
45
45
46 Rebase the revision containing the rename:
46 Rebase the revision containing the rename:
47
47
48 $ hg rebase -s 2 -d 1
48 $ hg rebase -s 2 -d 1
49 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
49 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
50
50
51 $ hg tglog
51 $ hg tglog
52 @ 2: 'rename A'
52 @ 2: 'rename A'
53 |
53 |
54 o 1: 'B'
54 o 1: 'B'
55 |
55 |
56 o 0: 'A'
56 o 0: 'A'
57
57
58
58
59 Rename is not lost:
59 Rename is not lost:
60
60
61 $ hg tlog -p --git -r tip
61 $ hg tlog -p --git -r tip
62 2: 'rename A'
62 2: 'rename A'
63 diff --git a/a b/a-renamed
63 diff --git a/a b/a-renamed
64 rename from a
64 rename from a
65 rename to a-renamed
65 rename to a-renamed
66
66
67 $ cd ..
67 $ cd ..
68
68
69
69
70 $ hg init b
70 $ hg init b
71 $ cd b
71 $ cd b
72
72
73 $ echo a > a
73 $ echo a > a
74 $ hg ci -Am A
74 $ hg ci -Am A
75 adding a
75 adding a
76
76
77 $ echo b > b
77 $ echo b > b
78 $ hg ci -Am B
78 $ hg ci -Am B
79 adding b
79 adding b
80
80
81 $ hg up -q -C 0
81 $ hg up -q -C 0
82
82
83 $ hg cp a a-copied
83 $ hg cp a a-copied
84 $ hg ci -m 'copy A'
84 $ hg ci -m 'copy A'
85 created new head
85 created new head
86
86
87 $ hg tglog
87 $ hg tglog
88 @ 2: 'copy A'
88 @ 2: 'copy A'
89 |
89 |
90 | o 1: 'B'
90 | o 1: 'B'
91 |/
91 |/
92 o 0: 'A'
92 o 0: 'A'
93
93
94 Copy is tracked:
94 Copy is tracked:
95
95
96 $ hg tlog -p --git -r tip
96 $ hg tlog -p --git -r tip
97 2: 'copy A'
97 2: 'copy A'
98 diff --git a/a b/a-copied
98 diff --git a/a b/a-copied
99 copy from a
99 copy from a
100 copy to a-copied
100 copy to a-copied
101
101
102 Rebase the revision containing the copy:
102 Rebase the revision containing the copy:
103
103
104 $ hg rebase -s 2 -d 1
104 $ hg rebase -s 2 -d 1
105 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
105 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
106
106
107 $ hg tglog
107 $ hg tglog
108 @ 2: 'copy A'
108 @ 2: 'copy A'
109 |
109 |
110 o 1: 'B'
110 o 1: 'B'
111 |
111 |
112 o 0: 'A'
112 o 0: 'A'
113
113
114 Copy is not lost:
114 Copy is not lost:
115
115
116 $ hg tlog -p --git -r tip
116 $ hg tlog -p --git -r tip
117 2: 'copy A'
117 2: 'copy A'
118 diff --git a/a b/a-copied
118 diff --git a/a b/a-copied
119 copy from a
119 copy from a
120 copy to a-copied
120 copy to a-copied
121
121
122 $ cd ..
123
124
125 Test rebase across repeating renames:
126
127 $ hg init repo
128
129 $ cd repo
130
131 $ echo testing > file1.txt
132 $ hg add file1.txt
133 $ hg ci -m "Adding file1"
134
135 $ hg rename file1.txt file2.txt
136 $ hg ci -m "Rename file1 to file2"
137
138 $ echo Unrelated change > unrelated.txt
139 $ hg add unrelated.txt
140 $ hg ci -m "Unrelated change"
141
142 $ hg rename file2.txt file1.txt
143 $ hg ci -m "Rename file2 back to file1"
144
145 $ hg update -r -2
146 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
147
148 $ echo Another unrelated change >> unrelated.txt
149 $ hg ci -m "Another unrelated change"
150 created new head
151
152 $ hg tglog
153 @ 4: 'Another unrelated change'
154 |
155 | o 3: 'Rename file2 back to file1'
156 |/
157 o 2: 'Unrelated change'
158 |
159 o 1: 'Rename file1 to file2'
160 |
161 o 0: 'Adding file1'
162
163
164 $ hg rebase -s 4 -d 3
165 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-backup.hg (glob)
166
167 $ hg diff --stat -c .
168 unrelated.txt | 1 +
169 1 files changed, 1 insertions(+), 0 deletions(-)
170
General Comments 0
You need to be logged in to leave comments. Login now