##// END OF EJS Templates
rebase: allow for rebasing descendants onto ancestors on different named branches...
Stefano Tortarolo -
r13733:4e2690a7 default
parent child Browse files
Show More
@@ -0,0 +1,171 b''
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
3 > graphlog=
4 > rebase=
5 >
6 > [alias]
7 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
8 > EOF
9
10
11 $ hg init a
12 $ cd a
13
14 $ echo A > A
15 $ hg ci -Am A
16 adding A
17
18 $ echo B > B
19 $ hg ci -Am B
20 adding B
21
22 $ hg up -q -C 0
23
24 $ echo C > C
25 $ hg ci -Am C
26 adding C
27 created new head
28
29 $ hg up -q -C 0
30
31 $ echo D > D
32 $ hg ci -Am D
33 adding D
34 created new head
35
36 $ hg merge -r 2
37 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
38 (branch merge, don't forget to commit)
39
40 $ hg ci -m E
41
42 $ hg up -q -C 3
43
44 $ echo F > F
45 $ hg ci -Am F
46 adding F
47 created new head
48
49 $ cd ..
50
51
52 Rebasing descendant onto ancestor across different named branches
53
54 $ hg clone -q -u . a a1
55
56 $ cd a1
57
58 $ hg branch dev
59 marked working directory as branch dev
60
61 $ echo x > x
62
63 $ hg add x
64
65 $ hg ci -m 'extra named branch'
66
67 $ hg tglog
68 @ 6: 'extra named branch' dev
69 |
70 o 5: 'F'
71 |
72 | o 4: 'E'
73 |/|
74 o | 3: 'D'
75 | |
76 | o 2: 'C'
77 |/
78 | o 1: 'B'
79 |/
80 o 0: 'A'
81
82 $ hg rebase -s 6 -d 5
83 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
84
85 $ hg tglog
86 @ 6: 'extra named branch'
87 |
88 o 5: 'F'
89 |
90 | o 4: 'E'
91 |/|
92 o | 3: 'D'
93 | |
94 | o 2: 'C'
95 |/
96 | o 1: 'B'
97 |/
98 o 0: 'A'
99
100 $ cd ..
101
102 Rebasing descendant onto ancestor across the same named branches
103
104 $ hg clone -q -u . a a2
105
106 $ cd a2
107
108 $ echo x > x
109
110 $ hg add x
111
112 $ hg ci -m 'G'
113
114 $ hg tglog
115 @ 6: 'G'
116 |
117 o 5: 'F'
118 |
119 | o 4: 'E'
120 |/|
121 o | 3: 'D'
122 | |
123 | o 2: 'C'
124 |/
125 | o 1: 'B'
126 |/
127 o 0: 'A'
128
129 $ hg rebase -s 6 -d 5
130 abort: source is descendant of destination
131 [255]
132
133 $ cd ..
134
135 Rebasing ancestor onto descendant across different named branches
136
137 $ hg clone -q -u . a a3
138
139 $ cd a3
140
141 $ hg branch dev
142 marked working directory as branch dev
143
144 $ echo x > x
145
146 $ hg add x
147
148 $ hg ci -m 'extra named branch'
149
150 $ hg tglog
151 @ 6: 'extra named branch' dev
152 |
153 o 5: 'F'
154 |
155 | o 4: 'E'
156 |/|
157 o | 3: 'D'
158 | |
159 | o 2: 'C'
160 |/
161 | o 1: 'B'
162 |/
163 o 0: 'A'
164
165 $ hg rebase -s 5 -d 6
166 abort: source is ancestor of destination
167 [255]
168
169 $ cd ..
170
171
@@ -1,588 +1,589 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:
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 for p in mq.applied:
364 for p in mq.applied:
365 rev = repo[p.node].rev()
365 rev = repo[p.node].rev()
366 if rev in state:
366 if rev in state:
367 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
367 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
368 (rev, p.name))
368 (rev, p.name))
369 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
369 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
370
370
371 if mqrebase:
371 if mqrebase:
372 mq.finish(repo, mqrebase.keys())
372 mq.finish(repo, mqrebase.keys())
373
373
374 # We must start import from the newest revision
374 # We must start import from the newest revision
375 for rev in sorted(mqrebase, reverse=True):
375 for rev in sorted(mqrebase, reverse=True):
376 if rev not in skipped:
376 if rev not in skipped:
377 name, isgit = mqrebase[rev]
377 name, isgit = mqrebase[rev]
378 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
378 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
379 mq.qimport(repo, (), patchname=name, git=isgit,
379 mq.qimport(repo, (), patchname=name, git=isgit,
380 rev=[str(state[rev])])
380 rev=[str(state[rev])])
381 mq.save_dirty()
381 mq.save_dirty()
382
382
383 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
383 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
384 external):
384 external):
385 'Store the current status to allow recovery'
385 'Store the current status to allow recovery'
386 f = repo.opener("rebasestate", "w")
386 f = repo.opener("rebasestate", "w")
387 f.write(repo[originalwd].hex() + '\n')
387 f.write(repo[originalwd].hex() + '\n')
388 f.write(repo[target].hex() + '\n')
388 f.write(repo[target].hex() + '\n')
389 f.write(repo[external].hex() + '\n')
389 f.write(repo[external].hex() + '\n')
390 f.write('%d\n' % int(collapse))
390 f.write('%d\n' % int(collapse))
391 f.write('%d\n' % int(keep))
391 f.write('%d\n' % int(keep))
392 f.write('%d\n' % int(keepbranches))
392 f.write('%d\n' % int(keepbranches))
393 for d, v in state.iteritems():
393 for d, v in state.iteritems():
394 oldrev = repo[d].hex()
394 oldrev = repo[d].hex()
395 newrev = repo[v].hex()
395 newrev = repo[v].hex()
396 f.write("%s:%s\n" % (oldrev, newrev))
396 f.write("%s:%s\n" % (oldrev, newrev))
397 f.close()
397 f.close()
398 repo.ui.debug('rebase status stored\n')
398 repo.ui.debug('rebase status stored\n')
399
399
400 def clearstatus(repo):
400 def clearstatus(repo):
401 'Remove the status files'
401 'Remove the status files'
402 if os.path.exists(repo.join("rebasestate")):
402 if os.path.exists(repo.join("rebasestate")):
403 util.unlinkpath(repo.join("rebasestate"))
403 util.unlinkpath(repo.join("rebasestate"))
404
404
405 def restorestatus(repo):
405 def restorestatus(repo):
406 'Restore a previously stored status'
406 'Restore a previously stored status'
407 try:
407 try:
408 target = None
408 target = None
409 collapse = False
409 collapse = False
410 external = nullrev
410 external = nullrev
411 state = {}
411 state = {}
412 f = repo.opener("rebasestate")
412 f = repo.opener("rebasestate")
413 for i, l in enumerate(f.read().splitlines()):
413 for i, l in enumerate(f.read().splitlines()):
414 if i == 0:
414 if i == 0:
415 originalwd = repo[l].rev()
415 originalwd = repo[l].rev()
416 elif i == 1:
416 elif i == 1:
417 target = repo[l].rev()
417 target = repo[l].rev()
418 elif i == 2:
418 elif i == 2:
419 external = repo[l].rev()
419 external = repo[l].rev()
420 elif i == 3:
420 elif i == 3:
421 collapse = bool(int(l))
421 collapse = bool(int(l))
422 elif i == 4:
422 elif i == 4:
423 keep = bool(int(l))
423 keep = bool(int(l))
424 elif i == 5:
424 elif i == 5:
425 keepbranches = bool(int(l))
425 keepbranches = bool(int(l))
426 else:
426 else:
427 oldrev, newrev = l.split(':')
427 oldrev, newrev = l.split(':')
428 state[repo[oldrev].rev()] = repo[newrev].rev()
428 state[repo[oldrev].rev()] = repo[newrev].rev()
429 skipped = set()
429 skipped = set()
430 # recompute the set of skipped revs
430 # recompute the set of skipped revs
431 if not collapse:
431 if not collapse:
432 seen = set([target])
432 seen = set([target])
433 for old, new in sorted(state.items()):
433 for old, new in sorted(state.items()):
434 if new != nullrev and new in seen:
434 if new != nullrev and new in seen:
435 skipped.add(old)
435 skipped.add(old)
436 seen.add(new)
436 seen.add(new)
437 repo.ui.debug('computed skipped revs: %s\n' % skipped)
437 repo.ui.debug('computed skipped revs: %s\n' % skipped)
438 repo.ui.debug('rebase status resumed\n')
438 repo.ui.debug('rebase status resumed\n')
439 return (originalwd, target, state, skipped,
439 return (originalwd, target, state, skipped,
440 collapse, keep, keepbranches, external)
440 collapse, keep, keepbranches, external)
441 except IOError, err:
441 except IOError, err:
442 if err.errno != errno.ENOENT:
442 if err.errno != errno.ENOENT:
443 raise
443 raise
444 raise util.Abort(_('no rebase in progress'))
444 raise util.Abort(_('no rebase in progress'))
445
445
446 def abort(repo, originalwd, target, state):
446 def abort(repo, originalwd, target, state):
447 'Restore the repository to its original state'
447 'Restore the repository to its original state'
448 if set(repo.changelog.descendants(target)) - set(state.values()):
448 if set(repo.changelog.descendants(target)) - set(state.values()):
449 repo.ui.warn(_("warning: new changesets detected on target branch, "
449 repo.ui.warn(_("warning: new changesets detected on target branch, "
450 "can't abort\n"))
450 "can't abort\n"))
451 return -1
451 return -1
452 else:
452 else:
453 # Strip from the first rebased revision
453 # Strip from the first rebased revision
454 merge.update(repo, repo[originalwd].rev(), False, True, False)
454 merge.update(repo, repo[originalwd].rev(), False, True, False)
455 rebased = filter(lambda x: x > -1 and x != target, state.values())
455 rebased = filter(lambda x: x > -1 and x != target, state.values())
456 if rebased:
456 if rebased:
457 strippoint = min(rebased)
457 strippoint = min(rebased)
458 # no backup of rebased cset versions needed
458 # no backup of rebased cset versions needed
459 repair.strip(repo.ui, repo, repo[strippoint].node())
459 repair.strip(repo.ui, repo, repo[strippoint].node())
460 clearstatus(repo)
460 clearstatus(repo)
461 repo.ui.warn(_('rebase aborted\n'))
461 repo.ui.warn(_('rebase aborted\n'))
462 return 0
462 return 0
463
463
464 def buildstate(repo, dest, src, base, detach):
464 def buildstate(repo, dest, src, base, detach):
465 'Define which revisions are going to be rebased and where'
465 'Define which revisions are going to be rebased and where'
466 targetancestors = set()
466 targetancestors = set()
467 detachset = set()
467 detachset = set()
468
468
469 if not dest:
469 if not dest:
470 # Destination defaults to the latest revision in the current branch
470 # Destination defaults to the latest revision in the current branch
471 branch = repo[None].branch()
471 branch = repo[None].branch()
472 dest = repo[branch].rev()
472 dest = repo[branch].rev()
473 else:
473 else:
474 dest = repo[dest].rev()
474 dest = repo[dest].rev()
475
475
476 # This check isn't strictly necessary, since mq detects commits over an
476 # This check isn't strictly necessary, since mq detects commits over an
477 # applied patch. But it prevents messing up the working directory when
477 # applied patch. But it prevents messing up the working directory when
478 # a partially completed rebase is blocked by mq.
478 # a partially completed rebase is blocked by mq.
479 if 'qtip' in repo.tags() and (repo[dest].node() in
479 if 'qtip' in repo.tags() and (repo[dest].node() in
480 [s.node for s in repo.mq.applied]):
480 [s.node for s in repo.mq.applied]):
481 raise util.Abort(_('cannot rebase onto an applied mq patch'))
481 raise util.Abort(_('cannot rebase onto an applied mq patch'))
482
482
483 if src:
483 if src:
484 commonbase = repo[src].ancestor(repo[dest])
484 commonbase = repo[src].ancestor(repo[dest])
485 samebranch = repo[src].branch() == repo[dest].branch()
485 if commonbase == repo[src]:
486 if commonbase == repo[src]:
486 raise util.Abort(_('source is ancestor of destination'))
487 raise util.Abort(_('source is ancestor of destination'))
487 if commonbase == repo[dest]:
488 if samebranch and commonbase == repo[dest]:
488 raise util.Abort(_('source is descendant of destination'))
489 raise util.Abort(_('source is descendant of destination'))
489 source = repo[src].rev()
490 source = repo[src].rev()
490 if detach:
491 if detach:
491 # We need to keep track of source's ancestors up to the common base
492 # We need to keep track of source's ancestors up to the common base
492 srcancestors = set(repo.changelog.ancestors(source))
493 srcancestors = set(repo.changelog.ancestors(source))
493 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
494 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
494 detachset = srcancestors - baseancestors
495 detachset = srcancestors - baseancestors
495 detachset.discard(commonbase.rev())
496 detachset.discard(commonbase.rev())
496 else:
497 else:
497 if base:
498 if base:
498 cwd = repo[base].rev()
499 cwd = repo[base].rev()
499 else:
500 else:
500 cwd = repo['.'].rev()
501 cwd = repo['.'].rev()
501
502
502 if cwd == dest:
503 if cwd == dest:
503 repo.ui.debug('source and destination are the same\n')
504 repo.ui.debug('source and destination are the same\n')
504 return None
505 return None
505
506
506 targetancestors = set(repo.changelog.ancestors(dest))
507 targetancestors = set(repo.changelog.ancestors(dest))
507 if cwd in targetancestors:
508 if cwd in targetancestors:
508 repo.ui.debug('source is ancestor of destination\n')
509 repo.ui.debug('source is ancestor of destination\n')
509 return None
510 return None
510
511
511 cwdancestors = set(repo.changelog.ancestors(cwd))
512 cwdancestors = set(repo.changelog.ancestors(cwd))
512 if dest in cwdancestors:
513 if dest in cwdancestors:
513 repo.ui.debug('source is descendant of destination\n')
514 repo.ui.debug('source is descendant of destination\n')
514 return None
515 return None
515
516
516 cwdancestors.add(cwd)
517 cwdancestors.add(cwd)
517 rebasingbranch = cwdancestors - targetancestors
518 rebasingbranch = cwdancestors - targetancestors
518 source = min(rebasingbranch)
519 source = min(rebasingbranch)
519
520
520 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
521 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
521 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
522 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
522 state.update(dict.fromkeys(detachset, nullmerge))
523 state.update(dict.fromkeys(detachset, nullmerge))
523 state[source] = nullrev
524 state[source] = nullrev
524 return repo['.'].rev(), repo[dest].rev(), state
525 return repo['.'].rev(), repo[dest].rev(), state
525
526
526 def pullrebase(orig, ui, repo, *args, **opts):
527 def pullrebase(orig, ui, repo, *args, **opts):
527 'Call rebase after pull if the latter has been invoked with --rebase'
528 'Call rebase after pull if the latter has been invoked with --rebase'
528 if opts.get('rebase'):
529 if opts.get('rebase'):
529 if opts.get('update'):
530 if opts.get('update'):
530 del opts['update']
531 del opts['update']
531 ui.debug('--update and --rebase are not compatible, ignoring '
532 ui.debug('--update and --rebase are not compatible, ignoring '
532 'the update flag\n')
533 'the update flag\n')
533
534
534 cmdutil.bail_if_changed(repo)
535 cmdutil.bail_if_changed(repo)
535 revsprepull = len(repo)
536 revsprepull = len(repo)
536 origpostincoming = commands.postincoming
537 origpostincoming = commands.postincoming
537 def _dummy(*args, **kwargs):
538 def _dummy(*args, **kwargs):
538 pass
539 pass
539 commands.postincoming = _dummy
540 commands.postincoming = _dummy
540 try:
541 try:
541 orig(ui, repo, *args, **opts)
542 orig(ui, repo, *args, **opts)
542 finally:
543 finally:
543 commands.postincoming = origpostincoming
544 commands.postincoming = origpostincoming
544 revspostpull = len(repo)
545 revspostpull = len(repo)
545 if revspostpull > revsprepull:
546 if revspostpull > revsprepull:
546 rebase(ui, repo, **opts)
547 rebase(ui, repo, **opts)
547 branch = repo[None].branch()
548 branch = repo[None].branch()
548 dest = repo[branch].rev()
549 dest = repo[branch].rev()
549 if dest != repo['.'].rev():
550 if dest != repo['.'].rev():
550 # there was nothing to rebase we force an update
551 # there was nothing to rebase we force an update
551 hg.update(repo, dest)
552 hg.update(repo, dest)
552 else:
553 else:
553 orig(ui, repo, *args, **opts)
554 orig(ui, repo, *args, **opts)
554
555
555 def uisetup(ui):
556 def uisetup(ui):
556 'Replace pull with a decorator to provide --rebase option'
557 'Replace pull with a decorator to provide --rebase option'
557 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
558 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
558 entry[1].append(('', 'rebase', None,
559 entry[1].append(('', 'rebase', None,
559 _("rebase working directory to branch head"))
560 _("rebase working directory to branch head"))
560 )
561 )
561
562
562 cmdtable = {
563 cmdtable = {
563 "rebase":
564 "rebase":
564 (rebase,
565 (rebase,
565 [
566 [
566 ('s', 'source', '',
567 ('s', 'source', '',
567 _('rebase from the specified changeset'), _('REV')),
568 _('rebase from the specified changeset'), _('REV')),
568 ('b', 'base', '',
569 ('b', 'base', '',
569 _('rebase from the base of the specified changeset '
570 _('rebase from the base of the specified changeset '
570 '(up to greatest common ancestor of base and dest)'),
571 '(up to greatest common ancestor of base and dest)'),
571 _('REV')),
572 _('REV')),
572 ('d', 'dest', '',
573 ('d', 'dest', '',
573 _('rebase onto the specified changeset'), _('REV')),
574 _('rebase onto the specified changeset'), _('REV')),
574 ('', 'collapse', False, _('collapse the rebased changesets')),
575 ('', 'collapse', False, _('collapse the rebased changesets')),
575 ('m', 'message', '',
576 ('m', 'message', '',
576 _('use text as collapse commit message'), _('TEXT')),
577 _('use text as collapse commit message'), _('TEXT')),
577 ('l', 'logfile', '',
578 ('l', 'logfile', '',
578 _('read collapse commit message from file'), _('FILE')),
579 _('read collapse commit message from file'), _('FILE')),
579 ('', 'keep', False, _('keep original changesets')),
580 ('', 'keep', False, _('keep original changesets')),
580 ('', 'keepbranches', False, _('keep original branch names')),
581 ('', 'keepbranches', False, _('keep original branch names')),
581 ('', 'detach', False, _('force detaching of source from its original '
582 ('', 'detach', False, _('force detaching of source from its original '
582 'branch')),
583 'branch')),
583 ('c', 'continue', False, _('continue an interrupted rebase')),
584 ('c', 'continue', False, _('continue an interrupted rebase')),
584 ('a', 'abort', False, _('abort an interrupted rebase'))] +
585 ('a', 'abort', False, _('abort an interrupted rebase'))] +
585 templateopts,
586 templateopts,
586 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
587 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
587 'hg rebase {-a|-c}'))
588 'hg rebase {-a|-c}'))
588 }
589 }
General Comments 0
You need to be logged in to leave comments. Login now