##// END OF EJS Templates
i18n: mark strings for translation in rebase extension
Martin Geisler -
r6964:b3239bad default
parent child Browse files
Show More
@@ -1,401 +1,401 b''
1 1 # rebase.py - rebasing feature for mercurial
2 2 #
3 3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 ''' Rebasing feature
9 9
10 10 This extension lets you rebase changesets in an existing Mercurial repository.
11 11
12 12 For more information:
13 13 http://www.selenic.com/mercurial/wiki/index.cgi/RebaseProject
14 14 '''
15 15
16 16 from mercurial import util, repair, merge, cmdutil, dispatch, commands
17 17 from mercurial.commands import templateopts
18 18 from mercurial.node import nullrev
19 19 from mercurial.i18n import _
20 20 import os, errno
21 21
22 22 def rebase(ui, repo, **opts):
23 23 """move changeset (and descendants) to a different branch
24 24
25 25 Rebase uses repeated merging to graft changesets from one part of history
26 26 onto another. This can be useful for linearizing local changes relative to
27 27 a master development tree.
28 28
29 29 If a rebase is interrupted to manually resolve a merge, it can be continued
30 30 with --continue or aborted with --abort.
31 31 """
32 32 originalwd = target = source = None
33 33 external = nullrev
34 34 state = skipped = {}
35 35
36 36 lock = wlock = None
37 37 try:
38 38 lock = repo.lock()
39 39 wlock = repo.wlock()
40 40
41 41 # Validate input and define rebasing points
42 42 destf = opts.get('dest', None)
43 43 srcf = opts.get('source', None)
44 44 basef = opts.get('base', None)
45 45 contf = opts.get('continue')
46 46 abortf = opts.get('abort')
47 47 collapsef = opts.get('collapse', False)
48 48 if contf or abortf:
49 49 if contf and abortf:
50 50 raise dispatch.ParseError('rebase',
51 51 _('cannot use both abort and continue'))
52 52 if collapsef:
53 53 raise dispatch.ParseError('rebase',
54 54 _('cannot use collapse with continue or abort'))
55 55
56 56 if (srcf or basef or destf):
57 57 raise dispatch.ParseError('rebase',
58 58 _('abort and continue do not allow specifying revisions'))
59 59
60 60 originalwd, target, state, collapsef, external = restorestatus(repo)
61 61 if abortf:
62 62 abort(repo, originalwd, target, state)
63 63 return
64 64 else:
65 65 if srcf and basef:
66 66 raise dispatch.ParseError('rebase', _('cannot specify both a '
67 67 'revision and a base'))
68 68 cmdutil.bail_if_changed(repo)
69 69 result = buildstate(repo, destf, srcf, basef, collapsef)
70 70 if result:
71 71 originalwd, target, state, external = result
72 72 else: # Empty state built, nothing to rebase
73 73 repo.ui.status(_('nothing to rebase\n'))
74 74 return
75 75
76 76 # Rebase
77 77 targetancestors = list(repo.changelog.ancestors(target))
78 78 targetancestors.append(target)
79 79
80 80 for rev in util.sort(state):
81 81 if state[rev] == -1:
82 82 storestatus(repo, originalwd, target, state, collapsef,
83 83 external)
84 84 rebasenode(repo, rev, target, state, skipped, targetancestors,
85 85 collapsef)
86 86 ui.note(_('rebase merging completed\n'))
87 87
88 88 if collapsef:
89 89 p1, p2 = defineparents(repo, min(state), target,
90 90 state, targetancestors)
91 91 concludenode(repo, rev, p1, external, state, collapsef,
92 92 last=True, skipped=skipped)
93 93
94 94 if 'qtip' in repo.tags():
95 95 updatemq(repo, state, skipped, **opts)
96 96
97 97 if not opts.get('keep'):
98 98 # Remove no more useful revisions
99 99 if (util.set(repo.changelog.descendants(min(state)))
100 100 - util.set(state.keys())):
101 101 ui.warn(_("warning: new changesets detected on source branch, "
102 102 "not stripping\n"))
103 103 else:
104 104 repair.strip(repo.ui, repo, repo[min(state)].node(), "strip")
105 105
106 106 clearstatus(repo)
107 107 ui.status(_("rebase completed\n"))
108 108 if skipped:
109 109 ui.note(_("%d revisions have been skipped\n") % len(skipped))
110 110 finally:
111 111 del lock, wlock
112 112
113 113 def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped={}):
114 114 """Skip commit if collapsing has been required and rev is not the last
115 115 revision, commit otherwise
116 116 """
117 117 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
118 118
119 119 if collapse and not last:
120 120 return None
121 121
122 122 # Commit, record the old nodeid
123 123 m, a, r = repo.status()[:3]
124 124 newrev = nullrev
125 125 try:
126 126 if last:
127 127 commitmsg = 'Collapsed revision'
128 128 for rebased in state:
129 129 if rebased not in skipped:
130 130 commitmsg += '\n* %s' % repo[rebased].description()
131 131 commitmsg = repo.ui.edit(commitmsg, repo.ui.username())
132 132 else:
133 133 commitmsg = repo[rev].description()
134 134 # Commit might fail if unresolved files exist
135 135 newrev = repo.commit(m+a+r,
136 136 text=commitmsg,
137 137 user=repo[rev].user(),
138 138 date=repo[rev].date(),
139 139 extra={'rebase_source': repo[rev].hex()})
140 140 return newrev
141 141 except util.Abort:
142 142 # Invalidate the previous setparents
143 143 repo.dirstate.invalidate()
144 144 raise
145 145
146 146 def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse):
147 147 'Rebase a single revision'
148 148 repo.ui.debug(_("rebasing %d:%s\n") % (rev, repo[rev].node()))
149 149
150 150 p1, p2 = defineparents(repo, rev, target, state, targetancestors)
151 151
152 152 # Merge phase
153 153 if len(repo.parents()) != 2:
154 154 # Update to target and merge it with local
155 155 merge.update(repo, p1, False, True, False)
156 156 repo.dirstate.write()
157 157 stats = merge.update(repo, rev, True, False, False)
158 158
159 159 if stats[3] > 0:
160 160 raise util.Abort(_('fix unresolved conflicts with hg resolve then '
161 161 'run hg rebase --continue'))
162 162 else: # we have an interrupted rebase
163 163 repo.ui.debug(_('resuming interrupted rebase\n'))
164 164
165 165
166 166 newrev = concludenode(repo, rev, p1, p2, state, collapse)
167 167
168 168 # Update the state
169 169 if newrev is not None:
170 170 state[rev] = repo[newrev].rev()
171 171 else:
172 172 if not collapse:
173 repo.ui.note('no changes, revision %d skipped\n' % rev)
174 repo.ui.debug('next revision set to %s\n' % p1)
173 repo.ui.note(_('no changes, revision %d skipped\n') % rev)
174 repo.ui.debug(_('next revision set to %s\n') % p1)
175 175 skipped[rev] = True
176 176 state[rev] = p1
177 177
178 178 def defineparents(repo, rev, target, state, targetancestors):
179 179 'Return the new parent relationship of the revision that will be rebased'
180 180 parents = repo[rev].parents()
181 181 p1 = p2 = nullrev
182 182
183 183 P1n = parents[0].rev()
184 184 if P1n in targetancestors:
185 185 p1 = target
186 186 elif P1n in state:
187 187 p1 = state[P1n]
188 188 else: # P1n external
189 189 p1 = target
190 190 p2 = P1n
191 191
192 192 if len(parents) == 2 and parents[1].rev() not in targetancestors:
193 193 P2n = parents[1].rev()
194 194 # interesting second parent
195 195 if P2n in state:
196 196 if p1 == target: # P1n in targetancestors or external
197 197 p1 = state[P2n]
198 198 else:
199 199 p2 = state[P2n]
200 200 else: # P2n external
201 201 if p2 != nullrev: # P1n external too => rev is a merged revision
202 202 raise util.Abort(_('cannot use revision %d as base, result '
203 203 'would have 3 parents') % rev)
204 204 p2 = P2n
205 205 return p1, p2
206 206
207 207 def updatemq(repo, state, skipped, **opts):
208 208 'Update rebased mq patches - finalize and then import them'
209 209 mqrebase = {}
210 210 for p in repo.mq.applied:
211 211 if repo[p.rev].rev() in state:
212 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
212 repo.ui.debug(_('revision %d is an mq patch (%s), finalize it.\n') %
213 213 (repo[p.rev].rev(), p.name))
214 214 mqrebase[repo[p.rev].rev()] = p.name
215 215
216 216 if mqrebase:
217 217 repo.mq.finish(repo, mqrebase.keys())
218 218
219 219 # We must start import from the newest revision
220 220 mq = mqrebase.keys()
221 221 mq.sort()
222 222 mq.reverse()
223 223 for rev in mq:
224 224 if rev not in skipped:
225 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev],
226 mqrebase[rev]))
225 repo.ui.debug(_('import mq patch %d (%s)\n')
226 % (state[rev], mqrebase[rev]))
227 227 repo.mq.qimport(repo, (), patchname=mqrebase[rev],
228 228 git=opts.get('git', False),rev=[str(state[rev])])
229 229 repo.mq.save_dirty()
230 230
231 231 def storestatus(repo, originalwd, target, state, collapse, external):
232 232 'Store the current status to allow recovery'
233 233 f = repo.opener("rebasestate", "w")
234 234 f.write(repo[originalwd].hex() + '\n')
235 235 f.write(repo[target].hex() + '\n')
236 236 f.write(repo[external].hex() + '\n')
237 237 f.write('%d\n' % int(collapse))
238 238 for d, v in state.items():
239 239 oldrev = repo[d].hex()
240 240 newrev = repo[v].hex()
241 241 f.write("%s:%s\n" % (oldrev, newrev))
242 242 f.close()
243 243 repo.ui.debug(_('rebase status stored\n'))
244 244
245 245 def clearstatus(repo):
246 246 'Remove the status files'
247 247 if os.path.exists(repo.join("rebasestate")):
248 248 util.unlink(repo.join("rebasestate"))
249 249
250 250 def restorestatus(repo):
251 251 'Restore a previously stored status'
252 252 try:
253 253 target = None
254 254 collapse = False
255 255 external = nullrev
256 256 state = {}
257 257 f = repo.opener("rebasestate")
258 258 for i, l in enumerate(f.read().splitlines()):
259 259 if i == 0:
260 260 originalwd = repo[l].rev()
261 261 elif i == 1:
262 262 target = repo[l].rev()
263 263 elif i == 2:
264 264 external = repo[l].rev()
265 265 elif i == 3:
266 266 collapse = bool(int(l))
267 267 else:
268 268 oldrev, newrev = l.split(':')
269 269 state[repo[oldrev].rev()] = repo[newrev].rev()
270 270 repo.ui.debug(_('rebase status resumed\n'))
271 271 return originalwd, target, state, collapse, external
272 272 except IOError, err:
273 273 if err.errno != errno.ENOENT:
274 274 raise
275 275 raise util.Abort(_('no rebase in progress'))
276 276
277 277 def abort(repo, originalwd, target, state):
278 278 'Restore the repository to its original state'
279 279 if util.set(repo.changelog.descendants(target)) - util.set(state.values()):
280 280 repo.ui.warn(_("warning: new changesets detected on target branch, "
281 281 "not stripping\n"))
282 282 else:
283 283 # Strip from the first rebased revision
284 284 merge.update(repo, repo[originalwd].rev(), False, True, False)
285 285 rebased = filter(lambda x: x > -1, state.values())
286 286 if rebased:
287 287 strippoint = min(rebased)
288 288 repair.strip(repo.ui, repo, repo[strippoint].node(), "strip")
289 289 clearstatus(repo)
290 290 repo.ui.status(_('rebase aborted\n'))
291 291
292 292 def buildstate(repo, dest, src, base, collapse):
293 293 'Define which revisions are going to be rebased and where'
294 294 state = {}
295 295 targetancestors = util.set()
296 296
297 297 if not dest:
298 298 # Destination defaults to the latest revision in the current branch
299 299 branch = repo[None].branch()
300 300 dest = repo[branch].rev()
301 301 else:
302 302 if 'qtip' in repo.tags() and (repo[dest].hex() in
303 303 [s.rev for s in repo.mq.applied]):
304 304 raise util.Abort(_('cannot rebase onto an applied mq patch'))
305 305 dest = repo[dest].rev()
306 306
307 307 if src:
308 308 commonbase = repo[src].ancestor(repo[dest])
309 309 if commonbase == repo[src]:
310 310 raise util.Abort(_('cannot rebase an ancestor'))
311 311 if commonbase == repo[dest]:
312 312 raise util.Abort(_('cannot rebase a descendant'))
313 313 source = repo[src].rev()
314 314 else:
315 315 if base:
316 316 cwd = repo[base].rev()
317 317 else:
318 318 cwd = repo['.'].rev()
319 319
320 320 if cwd == dest:
321 321 repo.ui.debug(_('already working on current\n'))
322 322 return None
323 323
324 324 targetancestors = util.set(repo.changelog.ancestors(dest))
325 325 if cwd in targetancestors:
326 326 repo.ui.debug(_('already working on the current branch\n'))
327 327 return None
328 328
329 329 cwdancestors = util.set(repo.changelog.ancestors(cwd))
330 330 cwdancestors.add(cwd)
331 331 rebasingbranch = cwdancestors - targetancestors
332 332 source = min(rebasingbranch)
333 333
334 334 repo.ui.debug(_('rebase onto %d starting from %d\n') % (dest, source))
335 335 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
336 336 external = nullrev
337 337 if collapse:
338 338 if not targetancestors:
339 339 targetancestors = util.set(repo.changelog.ancestors(dest))
340 340 for rev in state:
341 341 # Check externals and fail if there are more than one
342 342 for p in repo[rev].parents():
343 343 if (p.rev() not in state and p.rev() != source
344 344 and p.rev() not in targetancestors):
345 345 if external != nullrev:
346 346 raise util.Abort(_('unable to collapse, there is more '
347 347 'than one external parent'))
348 348 external = p.rev()
349 349
350 350 state[source] = nullrev
351 351 return repo['.'].rev(), repo[dest].rev(), state, external
352 352
353 353 def pulldelegate(pullfunction, repo, *args, **opts):
354 354 'Call rebase after pull if the latter has been invoked with --rebase'
355 355 if opts.get('rebase'):
356 356 if opts.get('update'):
357 357 raise util.Abort(_('--update and --rebase are not compatible'))
358 358
359 359 cmdutil.bail_if_changed(repo)
360 360 revsprepull = len(repo)
361 361 pullfunction(repo.ui, repo, *args, **opts)
362 362 revspostpull = len(repo)
363 363 if revspostpull > revsprepull:
364 364 rebase(repo.ui, repo, **opts)
365 365 else:
366 366 pullfunction(repo.ui, repo, *args, **opts)
367 367
368 368 def uisetup(ui):
369 369 'Replace pull with a decorator to provide --rebase option'
370 370 # cribbed from color.py
371 371 aliases, entry = cmdutil.findcmd(ui, 'pull', commands.table)
372 372 for candidatekey, candidateentry in commands.table.iteritems():
373 373 if candidateentry is entry:
374 374 cmdkey, cmdentry = candidatekey, entry
375 375 break
376 376
377 377 decorator = lambda ui, repo, *args, **opts: \
378 378 pulldelegate(cmdentry[0], repo, *args, **opts)
379 379 # make sure 'hg help cmd' still works
380 380 decorator.__doc__ = cmdentry[0].__doc__
381 381 decoratorentry = (decorator,) + cmdentry[1:]
382 382 rebaseopt = ('', 'rebase', None,
383 383 _("rebase working directory to branch head"))
384 384 decoratorentry[1].append(rebaseopt)
385 385 commands.table[cmdkey] = decoratorentry
386 386
387 387 cmdtable = {
388 388 "rebase":
389 389 (rebase,
390 390 [
391 391 ('', 'keep', False, _('keep original revisions')),
392 392 ('s', 'source', '', _('rebase from a given revision')),
393 393 ('b', 'base', '', _('rebase from the base of a given revision')),
394 394 ('d', 'dest', '', _('rebase onto a given revision')),
395 395 ('', 'collapse', False, _('collapse the rebased revisions')),
396 396 ('c', 'continue', False, _('continue an interrupted rebase')),
397 397 ('a', 'abort', False, _('abort an interrupted rebase')),] +
398 398 templateopts,
399 399 _('hg rebase [-s rev | -b rev] [-d rev] [--collapse] | [-c] | [-a] | '
400 400 '[--keep]')),
401 401 }
General Comments 0
You need to be logged in to leave comments. Login now