23 from mercurial import hg |
22 from mercurial import hg |
24 from mercurial import node |
23 from mercurial import node |
25 from mercurial import patch |
24 from mercurial import patch |
26 from mercurial import repair |
25 from mercurial import repair |
27 from mercurial import scmutil |
26 from mercurial import scmutil |
28 from mercurial import url |
|
29 from mercurial import util |
27 from mercurial import util |
30 from mercurial.i18n import _ |
28 from mercurial.i18n import _ |
31 |
29 |
32 |
30 |
33 editcomment = """ |
31 editcomment = """ |
42 # m, mess = edit message without changing commit content |
40 # m, mess = edit message without changing commit content |
43 # |
41 # |
44 """ |
42 """ |
45 |
43 |
46 def between(repo, old, new, keep): |
44 def between(repo, old, new, keep): |
47 revs = [old, ] |
45 revs = [old] |
48 current = old |
46 current = old |
49 while current != new: |
47 while current != new: |
50 ctx = repo[current] |
48 ctx = repo[current] |
51 if not keep and len(ctx.children()) > 1: |
49 if not keep and len(ctx.children()) > 1: |
52 raise util.Abort(_('cannot edit history that would orphan nodes')) |
50 raise util.Abort(_('cannot edit history that would orphan nodes')) |
87 ui.warn(_('%s: empty changeset') |
85 ui.warn(_('%s: empty changeset') |
88 % node.hex(ha)) |
86 % node.hex(ha)) |
89 return ctx, [], [], [] |
87 return ctx, [], [], [] |
90 finally: |
88 finally: |
91 os.unlink(patchfile) |
89 os.unlink(patchfile) |
92 except Exception, inst: |
90 except Exception: |
93 raise util.Abort(_('Fix up the change and run ' |
91 raise util.Abort(_('Fix up the change and run ' |
94 'hg histedit --continue')) |
92 'hg histedit --continue')) |
95 n = repo.commit(text=oldctx.description(), user=oldctx.user(), date=oldctx.date(), |
93 n = repo.commit(text=oldctx.description(), user=oldctx.user(), |
96 extra=oldctx.extra()) |
94 date=oldctx.date(), extra=oldctx.extra()) |
97 return repo[n], [n, ], [oldctx.node(), ], [] |
95 return repo[n], [n], [oldctx.node()], [] |
98 |
96 |
99 |
97 |
100 def edit(ui, repo, ctx, ha, opts): |
98 def edit(ui, repo, ctx, ha, opts): |
101 oldctx = repo[ha] |
99 oldctx = repo[ha] |
102 hg.update(repo, ctx.node()) |
100 hg.update(repo, ctx.node()) |
115 files = set() |
113 files = set() |
116 try: |
114 try: |
117 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
115 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
118 finally: |
116 finally: |
119 os.unlink(patchfile) |
117 os.unlink(patchfile) |
120 except Exception, inst: |
118 except Exception: |
121 pass |
119 pass |
122 raise util.Abort(_('Make changes as needed, you may commit or record as ' |
120 raise util.Abort(_('Make changes as needed, you may commit or record as ' |
123 'needed now.\nWhen you are finished, run hg' |
121 'needed now.\nWhen you are finished, run hg' |
124 ' histedit --continue to resume.')) |
122 ' histedit --continue to resume.')) |
125 |
123 |
145 ui.warn(_('%s: empty changeset') |
143 ui.warn(_('%s: empty changeset') |
146 % node.hex(ha)) |
144 % node.hex(ha)) |
147 return ctx, [], [], [] |
145 return ctx, [], [], [] |
148 finally: |
146 finally: |
149 os.unlink(patchfile) |
147 os.unlink(patchfile) |
150 except Exception, inst: |
148 except Exception: |
151 raise util.Abort(_('Fix up the change and run ' |
149 raise util.Abort(_('Fix up the change and run ' |
152 'hg histedit --continue')) |
150 'hg histedit --continue')) |
153 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), date=oldctx.date(), |
151 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), |
154 extra=oldctx.extra()) |
152 date=oldctx.date(), extra=oldctx.extra()) |
155 return finishfold(ui, repo, ctx, oldctx, n, opts, []) |
153 return finishfold(ui, repo, ctx, oldctx, n, opts, []) |
156 |
154 |
157 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges): |
155 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges): |
158 parent = ctx.parents()[0].node() |
156 parent = ctx.parents()[0].node() |
159 hg.update(repo, parent) |
157 hg.update(repo, parent) |
172 try: |
170 try: |
173 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
171 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
174 finally: |
172 finally: |
175 os.unlink(patchfile) |
173 os.unlink(patchfile) |
176 newmessage = '\n***\n'.join( |
174 newmessage = '\n***\n'.join( |
177 [ctx.description(), ] + |
175 [ctx.description()] + |
178 [repo[r].description() for r in internalchanges] + |
176 [repo[r].description() for r in internalchanges] + |
179 [oldctx.description(), ]) |
177 [oldctx.description()]) |
180 # If the changesets are from the same author, keep it. |
178 # If the changesets are from the same author, keep it. |
181 if ctx.user() == oldctx.user(): |
179 if ctx.user() == oldctx.user(): |
182 username = ctx.user() |
180 username = ctx.user() |
183 else: |
181 else: |
184 username = ui.username() |
182 username = ui.username() |
185 newmessage = ui.edit(newmessage, username) |
183 newmessage = ui.edit(newmessage, username) |
186 n = repo.commit(text=newmessage, user=username, date=max(ctx.date(), oldctx.date()), |
184 n = repo.commit(text=newmessage, user=username, |
187 extra=oldctx.extra()) |
185 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra()) |
188 return repo[n], [n, ], [oldctx.node(), ctx.node() ], [newnode, ] |
186 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode] |
189 |
187 |
190 def drop(ui, repo, ctx, ha, opts): |
188 def drop(ui, repo, ctx, ha, opts): |
191 return ctx, [], [repo[ha].node(), ], [] |
189 return ctx, [], [repo[ha].node()], [] |
192 |
190 |
193 |
191 |
194 def message(ui, repo, ctx, ha, opts): |
192 def message(ui, repo, ctx, ha, opts): |
195 oldctx = repo[ha] |
193 oldctx = repo[ha] |
196 hg.update(repo, ctx.node()) |
194 hg.update(repo, ctx.node()) |
209 files = set() |
207 files = set() |
210 try: |
208 try: |
211 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
209 patch.patch(ui, repo, patchfile, files=files, eolmode=None) |
212 finally: |
210 finally: |
213 os.unlink(patchfile) |
211 os.unlink(patchfile) |
214 except Exception, inst: |
212 except Exception: |
215 raise util.Abort(_('Fix up the change and run ' |
213 raise util.Abort(_('Fix up the change and run ' |
216 'hg histedit --continue')) |
214 'hg histedit --continue')) |
217 message = oldctx.description() |
215 message = oldctx.description() |
218 message = ui.edit(message, ui.username()) |
216 message = ui.edit(message, ui.username()) |
219 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(), |
217 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(), |
253 raise util.Abort(_('source has mq patches applied')) |
251 raise util.Abort(_('source has mq patches applied')) |
254 |
252 |
255 parent = list(parent) + opts.get('rev', []) |
253 parent = list(parent) + opts.get('rev', []) |
256 if opts.get('outgoing'): |
254 if opts.get('outgoing'): |
257 if len(parent) > 1: |
255 if len(parent) > 1: |
258 raise util.Abort(_('only one repo argument allowed with --outgoing')) |
256 raise util.Abort( |
|
257 _('only one repo argument allowed with --outgoing')) |
259 elif parent: |
258 elif parent: |
260 parent = parent[0] |
259 parent = parent[0] |
261 |
260 |
262 dest = ui.expandpath(parent or 'default-push', parent or 'default') |
261 dest = ui.expandpath(parent or 'default-push', parent or 'default') |
263 dest, revs = hg.parseurl(dest, None)[:2] |
262 dest, revs = hg.parseurl(dest, None)[:2] |
277 |
276 |
278 if opts.get('continue', False): |
277 if opts.get('continue', False): |
279 if len(parent) != 0: |
278 if len(parent) != 0: |
280 raise util.Abort(_('no arguments allowed with --continue')) |
279 raise util.Abort(_('no arguments allowed with --continue')) |
281 (parentctxnode, created, replaced, |
280 (parentctxnode, created, replaced, |
282 tmpnodes, existing, rules, keep, tip, replacemap ) = readstate(repo) |
281 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo) |
283 currentparent, wantnull = repo.dirstate.parents() |
282 currentparent, wantnull = repo.dirstate.parents() |
284 parentctx = repo[parentctxnode] |
283 parentctx = repo[parentctxnode] |
285 # discover any nodes the user has added in the interim |
284 # discover any nodes the user has added in the interim |
286 newchildren = [c for c in parentctx.children() |
285 newchildren = [c for c in parentctx.children() |
287 if c.node() not in existing] |
286 if c.node() not in existing] |
288 action, currentnode = rules.pop(0) |
287 action, currentnode = rules.pop(0) |
289 while newchildren: |
288 while newchildren: |
290 if action in ['f', 'fold', ]: |
289 if action in ('f', 'fold'): |
291 tmpnodes.extend([n.node() for n in newchildren]) |
290 tmpnodes.extend([n.node() for n in newchildren]) |
292 else: |
291 else: |
293 created.extend([n.node() for n in newchildren]) |
292 created.extend([n.node() for n in newchildren]) |
294 newchildren = filter(lambda x: x.node() not in existing, |
293 newchildren = filter(lambda x: x.node() not in existing, |
295 reduce(lambda x, y: x + y, |
294 reduce(lambda x, y: x + y, |
298 m, a, r, d = repo.status()[:4] |
297 m, a, r, d = repo.status()[:4] |
299 oldctx = repo[currentnode] |
298 oldctx = repo[currentnode] |
300 message = oldctx.description() |
299 message = oldctx.description() |
301 if action in ('e', 'edit', 'm', 'mess'): |
300 if action in ('e', 'edit', 'm', 'mess'): |
302 message = ui.edit(message, ui.username()) |
301 message = ui.edit(message, ui.username()) |
303 elif action in ('f', 'fold', ): |
302 elif action in ('f', 'fold'): |
304 message = 'fold-temp-revision %s' % currentnode |
303 message = 'fold-temp-revision %s' % currentnode |
305 new = None |
304 new = None |
306 if m or a or r or d: |
305 if m or a or r or d: |
307 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(), |
306 new = repo.commit(text=message, user=oldctx.user(), |
308 extra=oldctx.extra()) |
307 date=oldctx.date(), extra=oldctx.extra()) |
309 |
308 |
310 if action in ('f', 'fold'): |
309 if action in ('f', 'fold'): |
311 if new: |
310 if new: |
312 tmpnodes.append(new) |
311 tmpnodes.append(new) |
313 else: |
312 else: |
314 new = newchildren[-1] |
313 new = newchildren[-1] |
315 (parentctx, created_, |
314 (parentctx, created_, replaced_, tmpnodes_) = finishfold( |
316 replaced_, tmpnodes_, ) = finishfold(ui, repo, |
315 ui, repo, parentctx, oldctx, new, opts, newchildren) |
317 parentctx, oldctx, new, |
|
318 opts, newchildren) |
|
319 replaced.extend(replaced_) |
316 replaced.extend(replaced_) |
320 created.extend(created_) |
317 created.extend(created_) |
321 tmpnodes.extend(tmpnodes_) |
318 tmpnodes.extend(tmpnodes_) |
322 elif action not in ('d', 'drop'): |
319 elif action not in ('d', 'drop'): |
323 if new != oldctx.node(): |
320 if new != oldctx.node(): |
336 hg.clean(repo, tip) |
333 hg.clean(repo, tip) |
337 ui.debug('should strip created nodes %s\n' % |
334 ui.debug('should strip created nodes %s\n' % |
338 ', '.join([node.hex(n)[:12] for n in created])) |
335 ', '.join([node.hex(n)[:12] for n in created])) |
339 ui.debug('should strip temp nodes %s\n' % |
336 ui.debug('should strip temp nodes %s\n' % |
340 ', '.join([node.hex(n)[:12] for n in tmpnodes])) |
337 ', '.join([node.hex(n)[:12] for n in tmpnodes])) |
341 for nodes in (created, tmpnodes, ): |
338 for nodes in (created, tmpnodes): |
342 for n in reversed(nodes): |
339 for n in reversed(nodes): |
343 try: |
340 try: |
344 repair.strip(ui, repo, n) |
341 repair.strip(ui, repo, n) |
345 except error.LookupError: |
342 except error.LookupError: |
346 pass |
343 pass |
365 ctxs = [repo[r] for r in revs] |
362 ctxs = [repo[r] for r in revs] |
366 existing = [r.node() for r in ctxs] |
363 existing = [r.node() for r in ctxs] |
367 rules = opts.get('commands', '') |
364 rules = opts.get('commands', '') |
368 if not rules: |
365 if not rules: |
369 rules = '\n'.join([makedesc(c) for c in ctxs]) |
366 rules = '\n'.join([makedesc(c) for c in ctxs]) |
370 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12], ) |
367 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12]) |
371 rules = ui.edit(rules, ui.username()) |
368 rules = ui.edit(rules, ui.username()) |
372 # Save edit rules in .hg/histedit-last-edit.txt in case |
369 # Save edit rules in .hg/histedit-last-edit.txt in case |
373 # the user needs to ask for help after something |
370 # the user needs to ask for help after something |
374 # surprising happens. |
371 # surprising happens. |
375 f = open(repo.join('histedit-last-edit.txt'), 'w') |
372 f = open(repo.join('histedit-last-edit.txt'), 'w') |
390 tmpnodes = [] |
387 tmpnodes = [] |
391 created = [] |
388 created = [] |
392 |
389 |
393 |
390 |
394 while rules: |
391 while rules: |
395 writestate(repo, parentctx.node(), created, replaced, tmpnodes, existing, |
392 writestate(repo, parentctx.node(), created, replaced, |
396 rules, keep, tip, replacemap) |
393 tmpnodes, existing, rules, keep, tip, replacemap) |
397 action, ha = rules.pop(0) |
394 action, ha = rules.pop(0) |
398 (parentctx, created_, |
395 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action]( |
399 replaced_, tmpnodes_, ) = actiontable[action](ui, repo, |
396 ui, repo, parentctx, ha, opts) |
400 parentctx, ha, |
|
401 opts) |
|
402 |
397 |
403 hexshort = lambda x: node.hex(x)[:12] |
398 hexshort = lambda x: node.hex(x)[:12] |
404 |
399 |
405 if replaced_: |
400 if replaced_: |
406 clen, rlen = len(created_), len(replaced_) |
401 clen, rlen = len(created_), len(replaced_) |
413 assert rlen == 1, ('unexpected replacement of ' |
408 assert rlen == 1, ('unexpected replacement of ' |
414 '%d changes with %d changes' % (rlen, clen)) |
409 '%d changes with %d changes' % (rlen, clen)) |
415 # made more changesets than we're replacing |
410 # made more changesets than we're replacing |
416 # TODO synthesize patch names for created patches |
411 # TODO synthesize patch names for created patches |
417 replacemap[replaced_[0]] = created_[-1] |
412 replacemap[replaced_[0]] = created_[-1] |
418 ui.debug('histedit: created many, assuming %s replaced by %s' % ( |
413 ui.debug('histedit: created many, assuming %s replaced by %s' % |
419 hexshort(replaced_[0]), hexshort(created_[-1]))) |
414 (hexshort(replaced_[0]), hexshort(created_[-1]))) |
420 elif rlen > clen: |
415 elif rlen > clen: |
421 if not created_: |
416 if not created_: |
422 # This must be a drop. Try and put our metadata on |
417 # This must be a drop. Try and put our metadata on |
423 # the parent change. |
418 # the parent change. |
424 assert rlen == 1 |
419 assert rlen == 1 |
449 |
444 |
450 hg.update(repo, parentctx.node()) |
445 hg.update(repo, parentctx.node()) |
451 |
446 |
452 if not keep: |
447 if not keep: |
453 if replacemap: |
448 if replacemap: |
454 ui.note('histedit: Should update metadata for the following ' |
449 ui.note(_('histedit: Should update metadata for the following ' |
455 'changes:\n') |
450 'changes:\n')) |
456 |
451 |
457 def copybms(old, new): |
452 def copybms(old, new): |
458 if old in tmpnodes or old in created: |
453 if old in tmpnodes or old in created: |
459 # can't have any metadata we'd want to update |
454 # can't have any metadata we'd want to update |
460 return |
455 return |
461 while new in replacemap: |
456 while new in replacemap: |
462 new = replacemap[new] |
457 new = replacemap[new] |
463 ui.note('histedit: %s to %s\n' % (hexshort(old), hexshort(new))) |
458 ui.note(_('histedit: %s to %s\n') % (hexshort(old), |
|
459 hexshort(new))) |
464 octx = repo[old] |
460 octx = repo[old] |
465 marks = octx.bookmarks() |
461 marks = octx.bookmarks() |
466 if marks: |
462 if marks: |
467 ui.note('histedit: moving bookmarks %s\n' % |
463 ui.note(_('histedit: moving bookmarks %s\n') % |
468 ', '.join(marks)) |
464 ', '.join(marks)) |
469 for mark in marks: |
465 for mark in marks: |
470 repo._bookmarks[mark] = new |
466 repo._bookmarks[mark] = new |
471 bookmarks.write(repo) |
467 bookmarks.write(repo) |
472 |
468 |
473 # We assume that bookmarks on the tip should remain |
469 # We assume that bookmarks on the tip should remain |
524 |
520 |
525 Will abort if there are to many or too few rules, a malformed rule, |
521 Will abort if there are to many or too few rules, a malformed rule, |
526 or a rule on a changeset outside of the user-given range. |
522 or a rule on a changeset outside of the user-given range. |
527 """ |
523 """ |
528 parsed = [] |
524 parsed = [] |
529 first = True |
|
530 if len(rules) != len(ctxs): |
525 if len(rules) != len(ctxs): |
531 raise util.Abort(_('must specify a rule for each changeset once')) |
526 raise util.Abort(_('must specify a rule for each changeset once')) |
532 for r in rules: |
527 for r in rules: |
533 if ' ' not in r: |
528 if ' ' not in r: |
534 raise util.Abort(_('malformed line "%s"') % r) |
529 raise util.Abort(_('malformed line "%s"') % r) |
537 ha, rest = rest.split(' ', 1) |
532 ha, rest = rest.split(' ', 1) |
538 else: |
533 else: |
539 ha = r.strip() |
534 ha = r.strip() |
540 try: |
535 try: |
541 if repo[ha] not in ctxs: |
536 if repo[ha] not in ctxs: |
542 raise util.Abort(_('may not use changesets other than the ones listed')) |
537 raise util.Abort( |
|
538 _('may not use changesets other than the ones listed')) |
543 except error.RepoError: |
539 except error.RepoError: |
544 raise util.Abort(_('unknown changeset %s listed') % ha) |
540 raise util.Abort(_('unknown changeset %s listed') % ha) |
545 if action not in actiontable: |
541 if action not in actiontable: |
546 raise util.Abort(_('unknown action "%s"') % action) |
542 raise util.Abort(_('unknown action "%s"') % action) |
547 parsed.append([action, ha]) |
543 parsed.append([action, ha]) |
549 |
545 |
550 |
546 |
551 cmdtable = { |
547 cmdtable = { |
552 "histedit": |
548 "histedit": |
553 (histedit, |
549 (histedit, |
554 [('', 'commands', '', _('Read history edits from the specified file.')), |
550 [('', 'commands', '', _( |
|
551 'Read history edits from the specified file.')), |
555 ('c', 'continue', False, _('continue an edit already in progress')), |
552 ('c', 'continue', False, _('continue an edit already in progress')), |
556 ('k', 'keep', False, _("don't strip old nodes after edit is complete")), |
553 ('k', 'keep', False, _( |
|
554 "don't strip old nodes after edit is complete")), |
557 ('', 'abort', False, _('abort an edit in progress')), |
555 ('', 'abort', False, _('abort an edit in progress')), |
558 ('o', 'outgoing', False, _('changesets not found in destination')), |
556 ('o', 'outgoing', False, _('changesets not found in destination')), |
559 ('f', 'force', False, _('force outgoing even for unrelated repositories')), |
557 ('f', 'force', False, _( |
|
558 'force outgoing even for unrelated repositories')), |
560 ('r', 'rev', [], _('first revision to be edited')), |
559 ('r', 'rev', [], _('first revision to be edited')), |
561 ], |
560 ], |
562 __doc__, |
561 __doc__, |
563 ), |
562 ), |
564 } |
563 } |