##// END OF EJS Templates
histedit: proper phase conservation (issue3724)...
Pierre-Yves David -
r18440:35513c59 default
parent child Browse files
Show More
@@ -1,799 +1,811 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commands:
34 34 # p, pick = use commit
35 35 # e, edit = use commit, but stop for amending
36 36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 37 # d, drop = remove commit from history
38 38 # m, mess = edit message without changing commit content
39 39 #
40 40
41 41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 42 for each revision in your history. For example, if you had meant to add gamma
43 43 before beta, and then wanted to add delta in the same revision as beta, you
44 44 would reorganize the file to look like this::
45 45
46 46 pick 030b686bedc4 Add gamma
47 47 pick c561b4e977df Add beta
48 48 fold 7c2fd3b9020c Add delta
49 49
50 50 # Edit history between c561b4e977df and 7c2fd3b9020c
51 51 #
52 52 # Commands:
53 53 # p, pick = use commit
54 54 # e, edit = use commit, but stop for amending
55 55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 56 # d, drop = remove commit from history
57 57 # m, mess = edit message without changing commit content
58 58 #
59 59
60 60 At which point you close the editor and ``histedit`` starts working. When you
61 61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 62 those revisions together, offering you a chance to clean up the commit message::
63 63
64 64 Add beta
65 65 ***
66 66 Add delta
67 67
68 68 Edit the commit message to your liking, then close the editor. For
69 69 this example, let's assume that the commit message was changed to
70 70 ``Add beta and delta.`` After histedit has run and had a chance to
71 71 remove any old or temporary revisions it needed, the history looks
72 72 like this::
73 73
74 74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 75 | Add beta and delta.
76 76 |
77 77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 78 | Add gamma
79 79 |
80 80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 81 Add alpha
82 82
83 83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 84 ones) until after it has completed all the editing operations, so it will
85 85 probably perform several strip operations when it's done. For the above example,
86 86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 87 so you might need to be a little patient. You can choose to keep the original
88 88 revisions by passing the ``--keep`` flag.
89 89
90 90 The ``edit`` operation will drop you back to a command prompt,
91 91 allowing you to edit files freely, or even use ``hg record`` to commit
92 92 some changes as a separate commit. When you're done, any remaining
93 93 uncommitted changes will be committed as well. When done, run ``hg
94 94 histedit --continue`` to finish this step. You'll be prompted for a
95 95 new commit message, but the default commit message will be the
96 96 original message for the ``edit`` ed revision.
97 97
98 98 The ``message`` operation will give you a chance to revise a commit
99 99 message without changing the contents. It's a shortcut for doing
100 100 ``edit`` immediately followed by `hg histedit --continue``.
101 101
102 102 If ``histedit`` encounters a conflict when moving a revision (while
103 103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 104 ``edit`` with the difference that it won't prompt you for a commit
105 105 message when done. If you decide at this point that you don't like how
106 106 much work it will be to rearrange history, or that you made a mistake,
107 107 you can use ``hg histedit --abort`` to abandon the new changes you
108 108 have made and return to the state before you attempted to edit your
109 109 history.
110 110
111 111 If we clone the histedit-ed example repository above and add four more
112 112 changes, such that we have the following history::
113 113
114 114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 115 | Add theta
116 116 |
117 117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 118 | Add eta
119 119 |
120 120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 121 | Add zeta
122 122 |
123 123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 124 | Add epsilon
125 125 |
126 126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 127 | Add beta and delta.
128 128 |
129 129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 130 | Add gamma
131 131 |
132 132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 133 Add alpha
134 134
135 135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 137 repository that Mercurial does not detect to be related to the source
138 138 repo, you can add a ``--force`` option.
139 139 """
140 140
141 141 try:
142 142 import cPickle as pickle
143 143 except ImportError:
144 144 import pickle
145 145 import os
146 146
147 147 from mercurial import cmdutil
148 148 from mercurial import discovery
149 149 from mercurial import error
150 150 from mercurial import copies
151 151 from mercurial import context
152 152 from mercurial import hg
153 153 from mercurial import lock as lockmod
154 154 from mercurial import node
155 155 from mercurial import repair
156 156 from mercurial import scmutil
157 157 from mercurial import util
158 158 from mercurial import obsolete
159 159 from mercurial import merge as mergemod
160 160 from mercurial.i18n import _
161 161
162 162 cmdtable = {}
163 163 command = cmdutil.command(cmdtable)
164 164
165 165 testedwith = 'internal'
166 166
167 167 # i18n: command names and abbreviations must remain untranslated
168 168 editcomment = _("""# Edit history between %s and %s
169 169 #
170 170 # Commands:
171 171 # p, pick = use commit
172 172 # e, edit = use commit, but stop for amending
173 173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 174 # d, drop = remove commit from history
175 175 # m, mess = edit message without changing commit content
176 176 #
177 177 """)
178 178
179 179 def commitfuncfor(repo, src):
180 180 """Build a commit function for the replacement of <src>
181 181
182 182 This function ensure we apply the same treatement to all changesets.
183 183
184 184 - Add a 'histedit_source' entry in extra.
185 185
186 186 Note that fold have its own separated logic because its handling is a bit
187 187 different and not easily factored out of the fold method.
188 188 """
189 phasemin = src.phase()
189 190 def commitfunc(**kwargs):
190 extra = kwargs.get('extra', {}).copy()
191 extra['histedit_source'] = src.hex()
192 kwargs['extra'] = extra
193 return repo.commit(**kwargs)
191 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
192 try:
193 repo.ui.setconfig('phases', 'new-commit', phasemin)
194 extra = kwargs.get('extra', {}).copy()
195 extra['histedit_source'] = src.hex()
196 kwargs['extra'] = extra
197 return repo.commit(**kwargs)
198 finally:
199 repo.ui.restoreconfig(phasebackup)
194 200 return commitfunc
195 201
196 202
197 203
198 204 def applychanges(ui, repo, ctx, opts):
199 205 """Merge changeset from ctx (only) in the current working directory"""
200 206 wcpar = repo.dirstate.parents()[0]
201 207 if ctx.p1().node() == wcpar:
202 208 # edition ar "in place" we do not need to make any merge,
203 209 # just applies changes on parent for edition
204 210 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
205 211 stats = None
206 212 else:
207 213 try:
208 214 # ui.forcemerge is an internal variable, do not document
209 215 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
210 216 stats = mergemod.update(repo, ctx.node(), True, True, False,
211 217 ctx.p1().node())
212 218 finally:
213 219 repo.ui.setconfig('ui', 'forcemerge', '')
214 220 repo.setparents(wcpar, node.nullid)
215 221 repo.dirstate.write()
216 222 # fix up dirstate for copies and renames
217 223 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
218 224 return stats
219 225
220 226 def collapse(repo, first, last, commitopts):
221 227 """collapse the set of revisions from first to last as new one.
222 228
223 229 Expected commit options are:
224 230 - message
225 231 - date
226 232 - username
227 233 Commit message is edited in all cases.
228 234
229 235 This function works in memory."""
230 236 ctxs = list(repo.set('%d::%d', first, last))
231 237 if not ctxs:
232 238 return None
233 239 base = first.parents()[0]
234 240
235 241 # commit a new version of the old changeset, including the update
236 242 # collect all files which might be affected
237 243 files = set()
238 244 for ctx in ctxs:
239 245 files.update(ctx.files())
240 246
241 247 # Recompute copies (avoid recording a -> b -> a)
242 248 copied = copies.pathcopies(first, last)
243 249
244 250 # prune files which were reverted by the updates
245 251 def samefile(f):
246 252 if f in last.manifest():
247 253 a = last.filectx(f)
248 254 if f in base.manifest():
249 255 b = base.filectx(f)
250 256 return (a.data() == b.data()
251 257 and a.flags() == b.flags())
252 258 else:
253 259 return False
254 260 else:
255 261 return f not in base.manifest()
256 262 files = [f for f in files if not samefile(f)]
257 263 # commit version of these files as defined by head
258 264 headmf = last.manifest()
259 265 def filectxfn(repo, ctx, path):
260 266 if path in headmf:
261 267 fctx = last[path]
262 268 flags = fctx.flags()
263 269 mctx = context.memfilectx(fctx.path(), fctx.data(),
264 270 islink='l' in flags,
265 271 isexec='x' in flags,
266 272 copied=copied.get(path))
267 273 return mctx
268 274 raise IOError()
269 275
270 276 if commitopts.get('message'):
271 277 message = commitopts['message']
272 278 else:
273 279 message = first.description()
274 280 user = commitopts.get('user')
275 281 date = commitopts.get('date')
276 282 extra = commitopts.get('extra')
277 283
278 284 parents = (first.p1().node(), first.p2().node())
279 285 new = context.memctx(repo,
280 286 parents=parents,
281 287 text=message,
282 288 files=files,
283 289 filectxfn=filectxfn,
284 290 user=user,
285 291 date=date,
286 292 extra=extra)
287 293 new._text = cmdutil.commitforceeditor(repo, new, [])
288 294 return repo.commitctx(new)
289 295
290 296 def pick(ui, repo, ctx, ha, opts):
291 297 oldctx = repo[ha]
292 298 if oldctx.parents()[0] == ctx:
293 299 ui.debug('node %s unchanged\n' % ha)
294 300 return oldctx, []
295 301 hg.update(repo, ctx.node())
296 302 stats = applychanges(ui, repo, oldctx, opts)
297 303 if stats and stats[3] > 0:
298 304 raise util.Abort(_('Fix up the change and run '
299 305 'hg histedit --continue'))
300 306 # drop the second merge parent
301 307 commit = commitfuncfor(repo, oldctx)
302 308 n = commit(text=oldctx.description(), user=oldctx.user(),
303 309 date=oldctx.date(), extra=oldctx.extra())
304 310 if n is None:
305 311 ui.warn(_('%s: empty changeset\n')
306 312 % node.hex(ha))
307 313 return ctx, []
308 314 new = repo[n]
309 315 return new, [(oldctx.node(), (n,))]
310 316
311 317
312 318 def edit(ui, repo, ctx, ha, opts):
313 319 oldctx = repo[ha]
314 320 hg.update(repo, ctx.node())
315 321 applychanges(ui, repo, oldctx, opts)
316 322 raise util.Abort(_('Make changes as needed, you may commit or record as '
317 323 'needed now.\nWhen you are finished, run hg'
318 324 ' histedit --continue to resume.'))
319 325
320 326 def fold(ui, repo, ctx, ha, opts):
321 327 oldctx = repo[ha]
322 328 hg.update(repo, ctx.node())
323 329 stats = applychanges(ui, repo, oldctx, opts)
324 330 if stats and stats[3] > 0:
325 331 raise util.Abort(_('Fix up the change and run '
326 332 'hg histedit --continue'))
327 333 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
328 334 date=oldctx.date(), extra=oldctx.extra())
329 335 if n is None:
330 336 ui.warn(_('%s: empty changeset')
331 337 % node.hex(ha))
332 338 return ctx, []
333 339 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
334 340
335 341 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
336 342 parent = ctx.parents()[0].node()
337 343 hg.update(repo, parent)
338 344 ### prepare new commit data
339 345 commitopts = opts.copy()
340 346 # username
341 347 if ctx.user() == oldctx.user():
342 348 username = ctx.user()
343 349 else:
344 350 username = ui.username()
345 351 commitopts['user'] = username
346 352 # commit message
347 353 newmessage = '\n***\n'.join(
348 354 [ctx.description()] +
349 355 [repo[r].description() for r in internalchanges] +
350 356 [oldctx.description()]) + '\n'
351 357 commitopts['message'] = newmessage
352 358 # date
353 359 commitopts['date'] = max(ctx.date(), oldctx.date())
354 360 extra = ctx.extra().copy()
355 361 # histedit_source
356 362 # note: ctx is likely a temporary commit but that the best we can do here
357 363 # This is sufficient to solve issue3681 anyway
358 364 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
359 365 commitopts['extra'] = extra
360 n = collapse(repo, ctx, repo[newnode], commitopts)
366 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
367 try:
368 phasemin = max(ctx.phase(), oldctx.phase())
369 repo.ui.setconfig('phases', 'new-commit', phasemin)
370 n = collapse(repo, ctx, repo[newnode], commitopts)
371 finally:
372 repo.ui.restoreconfig(phasebackup)
361 373 if n is None:
362 374 return ctx, []
363 375 hg.update(repo, n)
364 376 replacements = [(oldctx.node(), (newnode,)),
365 377 (ctx.node(), (n,)),
366 378 (newnode, (n,)),
367 379 ]
368 380 for ich in internalchanges:
369 381 replacements.append((ich, (n,)))
370 382 return repo[n], replacements
371 383
372 384 def drop(ui, repo, ctx, ha, opts):
373 385 return ctx, [(repo[ha].node(), ())]
374 386
375 387
376 388 def message(ui, repo, ctx, ha, opts):
377 389 oldctx = repo[ha]
378 390 hg.update(repo, ctx.node())
379 391 stats = applychanges(ui, repo, oldctx, opts)
380 392 if stats and stats[3] > 0:
381 393 raise util.Abort(_('Fix up the change and run '
382 394 'hg histedit --continue'))
383 395 message = oldctx.description() + '\n'
384 396 message = ui.edit(message, ui.username())
385 397 commit = commitfuncfor(repo, oldctx)
386 398 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
387 399 extra=oldctx.extra())
388 400 newctx = repo[new]
389 401 if oldctx.node() != newctx.node():
390 402 return newctx, [(oldctx.node(), (new,))]
391 403 # We didn't make an edit, so just indicate no replaced nodes
392 404 return newctx, []
393 405
394 406 actiontable = {'p': pick,
395 407 'pick': pick,
396 408 'e': edit,
397 409 'edit': edit,
398 410 'f': fold,
399 411 'fold': fold,
400 412 'd': drop,
401 413 'drop': drop,
402 414 'm': message,
403 415 'mess': message,
404 416 }
405 417
406 418 @command('histedit',
407 419 [('', 'commands', '',
408 420 _('Read history edits from the specified file.')),
409 421 ('c', 'continue', False, _('continue an edit already in progress')),
410 422 ('k', 'keep', False,
411 423 _("don't strip old nodes after edit is complete")),
412 424 ('', 'abort', False, _('abort an edit in progress')),
413 425 ('o', 'outgoing', False, _('changesets not found in destination')),
414 426 ('f', 'force', False,
415 427 _('force outgoing even for unrelated repositories')),
416 428 ('r', 'rev', [], _('first revision to be edited'))],
417 429 _("[PARENT]"))
418 430 def histedit(ui, repo, *parent, **opts):
419 431 """interactively edit changeset history
420 432 """
421 433 # TODO only abort if we try and histedit mq patches, not just
422 434 # blanket if mq patches are applied somewhere
423 435 mq = getattr(repo, 'mq', None)
424 436 if mq and mq.applied:
425 437 raise util.Abort(_('source has mq patches applied'))
426 438
427 439 parent = list(parent) + opts.get('rev', [])
428 440 if opts.get('outgoing'):
429 441 if len(parent) > 1:
430 442 raise util.Abort(
431 443 _('only one repo argument allowed with --outgoing'))
432 444 elif parent:
433 445 parent = parent[0]
434 446
435 447 dest = ui.expandpath(parent or 'default-push', parent or 'default')
436 448 dest, revs = hg.parseurl(dest, None)[:2]
437 449 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
438 450
439 451 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
440 452 other = hg.peer(repo, opts, dest)
441 453
442 454 if revs:
443 455 revs = [repo.lookup(rev) for rev in revs]
444 456
445 457 parent = discovery.findcommonoutgoing(
446 458 repo, other, [], force=opts.get('force')).missing[0:1]
447 459 else:
448 460 if opts.get('force'):
449 461 raise util.Abort(_('--force only allowed with --outgoing'))
450 462
451 463 if opts.get('continue', False):
452 464 if len(parent) != 0:
453 465 raise util.Abort(_('no arguments allowed with --continue'))
454 466 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
455 467 currentparent, wantnull = repo.dirstate.parents()
456 468 parentctx = repo[parentctxnode]
457 469 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
458 470 replacements.extend(repl)
459 471 elif opts.get('abort', False):
460 472 if len(parent) != 0:
461 473 raise util.Abort(_('no arguments allowed with --abort'))
462 474 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
463 475 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
464 476 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
465 477 hg.clean(repo, topmost)
466 478 cleanupnode(ui, repo, 'created', tmpnodes)
467 479 cleanupnode(ui, repo, 'temp', leafs)
468 480 os.unlink(os.path.join(repo.path, 'histedit-state'))
469 481 return
470 482 else:
471 483 cmdutil.bailifchanged(repo)
472 484 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
473 485 raise util.Abort(_('history edit already in progress, try '
474 486 '--continue or --abort'))
475 487
476 488 topmost, empty = repo.dirstate.parents()
477 489
478 490 if len(parent) != 1:
479 491 raise util.Abort(_('histedit requires exactly one parent revision'))
480 492 parent = scmutil.revsingle(repo, parent[0]).node()
481 493
482 494 keep = opts.get('keep', False)
483 495 revs = between(repo, parent, topmost, keep)
484 496 if not revs:
485 497 ui.warn(_('nothing to edit\n'))
486 498 return 1
487 499
488 500 ctxs = [repo[r] for r in revs]
489 501 rules = opts.get('commands', '')
490 502 if not rules:
491 503 rules = '\n'.join([makedesc(c) for c in ctxs])
492 504 rules += '\n\n'
493 505 rules += editcomment % (node.short(parent), node.short(topmost))
494 506 rules = ui.edit(rules, ui.username())
495 507 # Save edit rules in .hg/histedit-last-edit.txt in case
496 508 # the user needs to ask for help after something
497 509 # surprising happens.
498 510 f = open(repo.join('histedit-last-edit.txt'), 'w')
499 511 f.write(rules)
500 512 f.close()
501 513 else:
502 514 f = open(rules)
503 515 rules = f.read()
504 516 f.close()
505 517 rules = [l for l in (r.strip() for r in rules.splitlines())
506 518 if l and not l[0] == '#']
507 519 rules = verifyrules(rules, repo, ctxs)
508 520
509 521 parentctx = repo[parent].parents()[0]
510 522 keep = opts.get('keep', False)
511 523 replacements = []
512 524
513 525
514 526 while rules:
515 527 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
516 528 action, ha = rules.pop(0)
517 529 ui.debug('histedit: processing %s %s\n' % (action, ha))
518 530 actfunc = actiontable[action]
519 531 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
520 532 replacements.extend(replacement_)
521 533
522 534 hg.update(repo, parentctx.node())
523 535
524 536 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
525 537 if mapping:
526 538 for prec, succs in mapping.iteritems():
527 539 if not succs:
528 540 ui.debug('histedit: %s is dropped\n' % node.short(prec))
529 541 else:
530 542 ui.debug('histedit: %s is replaced by %s\n' % (
531 543 node.short(prec), node.short(succs[0])))
532 544 if len(succs) > 1:
533 545 m = 'histedit: %s'
534 546 for n in succs[1:]:
535 547 ui.debug(m % node.short(n))
536 548
537 549 if not keep:
538 550 if mapping:
539 551 movebookmarks(ui, repo, mapping, topmost, ntm)
540 552 # TODO update mq state
541 553 if obsolete._enabled:
542 554 markers = []
543 555 # sort by revision number because it sound "right"
544 556 for prec in sorted(mapping, key=repo.changelog.rev):
545 557 succs = mapping[prec]
546 558 markers.append((repo[prec],
547 559 tuple(repo[s] for s in succs)))
548 560 if markers:
549 561 obsolete.createmarkers(repo, markers)
550 562 else:
551 563 cleanupnode(ui, repo, 'replaced', mapping)
552 564
553 565 cleanupnode(ui, repo, 'temp', tmpnodes)
554 566 os.unlink(os.path.join(repo.path, 'histedit-state'))
555 567 if os.path.exists(repo.sjoin('undo')):
556 568 os.unlink(repo.sjoin('undo'))
557 569
558 570
559 571 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
560 572 action, currentnode = rules.pop(0)
561 573 ctx = repo[currentnode]
562 574 # is there any new commit between the expected parent and "."
563 575 #
564 576 # note: does not take non linear new change in account (but previous
565 577 # implementation didn't used them anyway (issue3655)
566 578 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
567 579 if not newchildren:
568 580 # `parentctxnode` should match but no result. This means that
569 581 # currentnode is not a descendant from parentctxnode.
570 582 msg = _('working directory parent is not a descendant of %s')
571 583 hint = _('update to %s or descendant and run "hg histedit '
572 584 '--continue" again') % parentctx
573 585 raise util.Abort(msg % parentctx, hint=hint)
574 586 newchildren.pop(0) # remove parentctxnode
575 587 # Commit dirty working directory if necessary
576 588 new = None
577 589 m, a, r, d = repo.status()[:4]
578 590 if m or a or r or d:
579 591 # prepare the message for the commit to comes
580 592 if action in ('f', 'fold'):
581 593 message = 'fold-temp-revision %s' % currentnode
582 594 else:
583 595 message = ctx.description() + '\n'
584 596 if action in ('e', 'edit', 'm', 'mess'):
585 597 editor = cmdutil.commitforceeditor
586 598 else:
587 599 editor = False
588 600 commit = commitfuncfor(repo, ctx)
589 601 new = commit(text=message, user=ctx.user(),
590 602 date=ctx.date(), extra=ctx.extra(),
591 603 editor=editor)
592 604 if new is not None:
593 605 newchildren.append(new)
594 606
595 607 replacements = []
596 608 # track replacements
597 609 if ctx.node() not in newchildren:
598 610 # note: new children may be empty when the changeset is dropped.
599 611 # this happen e.g during conflicting pick where we revert content
600 612 # to parent.
601 613 replacements.append((ctx.node(), tuple(newchildren)))
602 614
603 615 if action in ('f', 'fold'):
604 616 # finalize fold operation if applicable
605 617 if new is None:
606 618 new = newchildren[-1]
607 619 else:
608 620 newchildren.pop() # remove new from internal changes
609 621 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
610 622 newchildren)
611 623 replacements.extend(repl)
612 624 elif newchildren:
613 625 # otherwize update "parentctx" before proceding to further operation
614 626 parentctx = repo[newchildren[-1]]
615 627 return parentctx, replacements
616 628
617 629
618 630 def between(repo, old, new, keep):
619 631 """select and validate the set of revision to edit
620 632
621 633 When keep is false, the specified set can't have children."""
622 634 ctxs = list(repo.set('%n::%n', old, new))
623 635 if ctxs and not keep:
624 636 if (not obsolete._enabled and
625 637 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
626 638 raise util.Abort(_('cannot edit history that would orphan nodes'))
627 639 root = ctxs[0] # list is already sorted by repo.set
628 640 if not root.phase():
629 641 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
630 642 return [c.node() for c in ctxs]
631 643
632 644
633 645 def writestate(repo, parentnode, rules, keep, topmost, replacements):
634 646 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
635 647 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
636 648 fp.close()
637 649
638 650 def readstate(repo):
639 651 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
640 652 """
641 653 fp = open(os.path.join(repo.path, 'histedit-state'))
642 654 return pickle.load(fp)
643 655
644 656
645 657 def makedesc(c):
646 658 """build a initial action line for a ctx `c`
647 659
648 660 line are in the form:
649 661
650 662 pick <hash> <rev> <summary>
651 663 """
652 664 summary = ''
653 665 if c.description():
654 666 summary = c.description().splitlines()[0]
655 667 line = 'pick %s %d %s' % (c, c.rev(), summary)
656 668 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
657 669
658 670 def verifyrules(rules, repo, ctxs):
659 671 """Verify that there exists exactly one edit rule per given changeset.
660 672
661 673 Will abort if there are to many or too few rules, a malformed rule,
662 674 or a rule on a changeset outside of the user-given range.
663 675 """
664 676 parsed = []
665 677 if len(rules) != len(ctxs):
666 678 raise util.Abort(_('must specify a rule for each changeset once'))
667 679 for r in rules:
668 680 if ' ' not in r:
669 681 raise util.Abort(_('malformed line "%s"') % r)
670 682 action, rest = r.split(' ', 1)
671 683 if ' ' in rest.strip():
672 684 ha, rest = rest.split(' ', 1)
673 685 else:
674 686 ha = r.strip()
675 687 try:
676 688 if repo[ha] not in ctxs:
677 689 raise util.Abort(
678 690 _('may not use changesets other than the ones listed'))
679 691 except error.RepoError:
680 692 raise util.Abort(_('unknown changeset %s listed') % ha)
681 693 if action not in actiontable:
682 694 raise util.Abort(_('unknown action "%s"') % action)
683 695 parsed.append([action, ha])
684 696 return parsed
685 697
686 698 def processreplacement(repo, replacements):
687 699 """process the list of replacements to return
688 700
689 701 1) the final mapping between original and created nodes
690 702 2) the list of temporary node created by histedit
691 703 3) the list of new commit created by histedit"""
692 704 allsuccs = set()
693 705 replaced = set()
694 706 fullmapping = {}
695 707 # initialise basic set
696 708 # fullmapping record all operation recorded in replacement
697 709 for rep in replacements:
698 710 allsuccs.update(rep[1])
699 711 replaced.add(rep[0])
700 712 fullmapping.setdefault(rep[0], set()).update(rep[1])
701 713 new = allsuccs - replaced
702 714 tmpnodes = allsuccs & replaced
703 715 # Reduce content fullmapping into direct relation between original nodes
704 716 # and final node created during history edition
705 717 # Dropped changeset are replaced by an empty list
706 718 toproceed = set(fullmapping)
707 719 final = {}
708 720 while toproceed:
709 721 for x in list(toproceed):
710 722 succs = fullmapping[x]
711 723 for s in list(succs):
712 724 if s in toproceed:
713 725 # non final node with unknown closure
714 726 # We can't process this now
715 727 break
716 728 elif s in final:
717 729 # non final node, replace with closure
718 730 succs.remove(s)
719 731 succs.update(final[s])
720 732 else:
721 733 final[x] = succs
722 734 toproceed.remove(x)
723 735 # remove tmpnodes from final mapping
724 736 for n in tmpnodes:
725 737 del final[n]
726 738 # we expect all changes involved in final to exist in the repo
727 739 # turn `final` into list (topologically sorted)
728 740 nm = repo.changelog.nodemap
729 741 for prec, succs in final.items():
730 742 final[prec] = sorted(succs, key=nm.get)
731 743
732 744 # computed topmost element (necessary for bookmark)
733 745 if new:
734 746 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
735 747 elif not final:
736 748 # Nothing rewritten at all. we won't need `newtopmost`
737 749 # It is the same as `oldtopmost` and `processreplacement` know it
738 750 newtopmost = None
739 751 else:
740 752 # every body died. The newtopmost is the parent of the root.
741 753 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
742 754
743 755 return final, tmpnodes, new, newtopmost
744 756
745 757 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
746 758 """Move bookmark from old to newly created node"""
747 759 if not mapping:
748 760 # if nothing got rewritten there is not purpose for this function
749 761 return
750 762 moves = []
751 763 for bk, old in sorted(repo._bookmarks.iteritems()):
752 764 if old == oldtopmost:
753 765 # special case ensure bookmark stay on tip.
754 766 #
755 767 # This is arguably a feature and we may only want that for the
756 768 # active bookmark. But the behavior is kept compatible with the old
757 769 # version for now.
758 770 moves.append((bk, newtopmost))
759 771 continue
760 772 base = old
761 773 new = mapping.get(base, None)
762 774 if new is None:
763 775 continue
764 776 while not new:
765 777 # base is killed, trying with parent
766 778 base = repo[base].p1().node()
767 779 new = mapping.get(base, (base,))
768 780 # nothing to move
769 781 moves.append((bk, new[-1]))
770 782 if moves:
771 783 marks = repo._bookmarks
772 784 for mark, new in moves:
773 785 old = marks[mark]
774 786 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
775 787 % (mark, node.short(old), node.short(new)))
776 788 marks[mark] = new
777 789 marks.write()
778 790
779 791 def cleanupnode(ui, repo, name, nodes):
780 792 """strip a group of nodes from the repository
781 793
782 794 The set of node to strip may contains unknown nodes."""
783 795 ui.debug('should strip %s nodes %s\n' %
784 796 (name, ', '.join([node.short(n) for n in nodes])))
785 797 lock = None
786 798 try:
787 799 lock = repo.lock()
788 800 # Find all node that need to be stripped
789 801 # (we hg %lr instead of %ln to silently ignore unknown item
790 802 nm = repo.changelog.nodemap
791 803 nodes = [n for n in nodes if n in nm]
792 804 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
793 805 for c in roots:
794 806 # We should process node in reverse order to strip tip most first.
795 807 # but this trigger a bug in changegroup hook.
796 808 # This would reduce bundle overhead
797 809 repair.strip(ui, repo, c)
798 810 finally:
799 811 lockmod.release(lock)
@@ -1,179 +1,404 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 Enable obsolete
4 4
5 5 $ cat > ${TESTTMP}/obs.py << EOF
6 6 > import mercurial.obsolete
7 7 > mercurial.obsolete._enabled = True
8 8 > EOF
9 9
10 10 $ cat >> $HGRCPATH << EOF
11 11 > [ui]
12 12 > logtemplate= {rev}:{node|short} {desc|firstline}
13 13 > [phases]
14 14 > publish=False
15 15 > [extensions]'
16 16 > histedit=
17 17 > rebase=
18 18 >
19 19 > obs=${TESTTMP}/obs.py
20 20 > EOF
21 21
22 22 $ hg init base
23 23 $ cd base
24 24
25 25 $ for x in a b c d e f ; do
26 26 > echo $x > $x
27 27 > hg add $x
28 28 > hg ci -m $x
29 29 > done
30 30
31 31 $ hg log --graph
32 32 @ 5:652413bf663e f
33 33 |
34 34 o 4:e860deea161a e
35 35 |
36 36 o 3:055a42cdd887 d
37 37 |
38 38 o 2:177f92b77385 c
39 39 |
40 40 o 1:d2ae7f538514 b
41 41 |
42 42 o 0:cb9a9f314b8b a
43 43
44 44
45 45 $ HGEDITOR=cat hg histedit 1
46 46 pick d2ae7f538514 1 b
47 47 pick 177f92b77385 2 c
48 48 pick 055a42cdd887 3 d
49 49 pick e860deea161a 4 e
50 50 pick 652413bf663e 5 f
51 51
52 52 # Edit history between d2ae7f538514 and 652413bf663e
53 53 #
54 54 # Commands:
55 55 # p, pick = use commit
56 56 # e, edit = use commit, but stop for amending
57 57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
58 58 # d, drop = remove commit from history
59 59 # m, mess = edit message without changing commit content
60 60 #
61 61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62 $ cat > commands.txt <<EOF
63 63 > pick 177f92b77385 2 c
64 64 > drop d2ae7f538514 1 b
65 65 > pick 055a42cdd887 3 d
66 66 > fold e860deea161a 4 e
67 67 > pick 652413bf663e 5 f
68 68 > EOF
69 69 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
70 70 saved backup bundle to $TESTTMP/base/.hg/strip-backup/96e494a2d553-backup.hg (glob)
71 71 $ hg log --graph --hidden
72 72 @ 8:cacdfd884a93 f
73 73 |
74 74 o 7:59d9f330561f d
75 75 |
76 76 o 6:b346ab9a313d c
77 77 |
78 78 | x 5:652413bf663e f
79 79 | |
80 80 | x 4:e860deea161a e
81 81 | |
82 82 | x 3:055a42cdd887 d
83 83 | |
84 84 | x 2:177f92b77385 c
85 85 | |
86 86 | x 1:d2ae7f538514 b
87 87 |/
88 88 o 0:cb9a9f314b8b a
89 89
90 90 $ hg debugobsolete
91 91 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '* *', 'user': 'test'} (glob)
92 92 177f92b773850b59254aa5e923436f921b55483b b346ab9a313db8537ecf96fca3ca3ca984ef3bd7 0 {'date': '* *', 'user': 'test'} (glob)
93 93 055a42cdd88768532f9cf79daa407fc8d138de9b 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 {'date': '* *', 'user': 'test'} (glob)
94 94 e860deea161a2f77de56603b340ebbb4536308ae 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 {'date': '* *', 'user': 'test'} (glob)
95 95 652413bf663ef2a641cab26574e46d5f5a64a55a cacdfd884a9321ec4e1de275ef3949fa953a1f83 0 {'date': '* *', 'user': 'test'} (glob)
96 96
97 97
98 98 Ensure hidden revision does not prevent histedit
99 99 -------------------------------------------------
100 100
101 101 create an hidden revision
102 102
103 103 $ cat > commands.txt <<EOF
104 104 > pick b346ab9a313d 6 c
105 105 > drop 59d9f330561f 7 d
106 106 > pick cacdfd884a93 8 f
107 107 > EOF
108 108 $ hg histedit 6 --commands commands.txt
109 109 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
110 110 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 111 $ hg log --graph
112 112 @ 9:c13eb81022ca f
113 113 |
114 114 o 6:b346ab9a313d c
115 115 |
116 116 o 0:cb9a9f314b8b a
117 117
118 118 check hidden revision are ignored (6 have hidden children 7 and 8)
119 119
120 120 $ cat > commands.txt <<EOF
121 121 > pick b346ab9a313d 6 c
122 122 > pick c13eb81022ca 8 f
123 123 > EOF
124 124 $ hg histedit 6 --commands commands.txt
125 125 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126
127 127
128 128
129 129 Test that rewriting leaving instability behind is allowed
130 130 ---------------------------------------------------------------------
131 131
132 132 $ hg up '.^'
133 133 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
134 134 $ hg log -r 'children(.)'
135 135 9:c13eb81022ca f (no-eol)
136 136 $ cat > commands.txt <<EOF
137 137 > edit b346ab9a313d 6 c
138 138 > EOF
139 139 $ hg histedit -r '.' --commands commands.txt
140 140 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
141 141 adding c
142 142 abort: Make changes as needed, you may commit or record as needed now.
143 143 When you are finished, run hg histedit --continue to resume.
144 144 [255]
145 145 $ echo c >> c
146 146 $ hg histedit --continue
147 147 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
148 148
149 149 $ hg log -r 'unstable()'
150 150 9:c13eb81022ca f (no-eol)
151 151
152 152 stabilise
153 153
154 154 $ hg rebase -r 'unstable()' -d .
155 155
156 Check that histedit respect phases
157 =========================================
158 156
159 (not directly related to the test file but doesn't deserve it's own test case)
157 Test phases support
158 ===========================================
159
160 Check that histedit respect immutability
161 -------------------------------------------
160 162
161 163 $ cat >> $HGRCPATH << EOF
162 164 > [ui]
163 165 > logtemplate= {rev}:{node|short} ({phase}) {desc|firstline}\n
164 166 > EOF
165 167
166 168 $ hg ph -pv '.^'
167 169 phase changed for 2 changesets
168 170 $ hg log -G
169 171 @ 11:b449568bf7fc (draft) f
170 172 |
171 173 o 10:40db8afa467b (public) c
172 174 |
173 175 o 0:cb9a9f314b8b (public) a
174 176
175 177 $ hg histedit -r '.~2'
176 178 abort: cannot edit immutable changeset: cb9a9f314b8b
177 179 [255]
178 180
179 181
182 Prepare further testing
183 -------------------------------------------
184
185 $ for x in g h i j k ; do
186 > echo $x > $x
187 > hg add $x
188 > hg ci -m $x
189 > done
190 $ hg phase --force --secret .~2
191 $ hg log -G
192 @ 16:ee118ab9fa44 (secret) k
193 |
194 o 15:3a6c53ee7f3d (secret) j
195 |
196 o 14:b605fb7503f2 (secret) i
197 |
198 o 13:7395e1ff83bd (draft) h
199 |
200 o 12:6b70183d2492 (draft) g
201 |
202 o 11:b449568bf7fc (draft) f
203 |
204 o 10:40db8afa467b (public) c
205 |
206 o 0:cb9a9f314b8b (public) a
207
208 $ cd ..
209
210 simple phase conservation
211 -------------------------------------------
212
213 Resulting changeset should conserve the phase of the original one whatever the
214 phases.new-commit option is.
215
216 New-commit as draft (default)
217
218 $ cp -r base simple-draft
219 $ cd simple-draft
220 $ cat > commands.txt <<EOF
221 > edit b449568bf7fc 11 f
222 > pick 6b70183d2492 12 g
223 > pick 7395e1ff83bd 13 h
224 > pick b605fb7503f2 14 i
225 > pick 3a6c53ee7f3d 15 j
226 > pick ee118ab9fa44 16 k
227 > EOF
228 $ hg histedit -r 'b449568bf7fc' --commands commands.txt
229 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
230 adding f
231 abort: Make changes as needed, you may commit or record as needed now.
232 When you are finished, run hg histedit --continue to resume.
233 [255]
234 $ echo f >> f
235 $ hg histedit --continue
236 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
238 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
239 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
240 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
241 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
242 $ hg log -G
243 @ 22:12e89af74238 (secret) k
244 |
245 o 21:636a8687b22e (secret) j
246 |
247 o 20:ccaf0a38653f (secret) i
248 |
249 o 19:11a89d1c2613 (draft) h
250 |
251 o 18:c1dec7ca82ea (draft) g
252 |
253 o 17:087281e68428 (draft) f
254 |
255 o 10:40db8afa467b (public) c
256 |
257 o 0:cb9a9f314b8b (public) a
258
259 $ cd ..
260
261
262 New-commit as draft (default)
263
264 $ cp -r base simple-secret
265 $ cd simple-secret
266 $ cat >> .hg/hgrc << EOF
267 > [phases]
268 > new-commit=secret
269 > EOF
270 $ cat > commands.txt <<EOF
271 > edit b449568bf7fc 11 f
272 > pick 6b70183d2492 12 g
273 > pick 7395e1ff83bd 13 h
274 > pick b605fb7503f2 14 i
275 > pick 3a6c53ee7f3d 15 j
276 > pick ee118ab9fa44 16 k
277 > EOF
278 $ hg histedit -r 'b449568bf7fc' --commands commands.txt
279 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
280 adding f
281 abort: Make changes as needed, you may commit or record as needed now.
282 When you are finished, run hg histedit --continue to resume.
283 [255]
284 $ echo f >> f
285 $ hg histedit --continue
286 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
288 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
289 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
292 $ hg log -G
293 @ 22:12e89af74238 (secret) k
294 |
295 o 21:636a8687b22e (secret) j
296 |
297 o 20:ccaf0a38653f (secret) i
298 |
299 o 19:11a89d1c2613 (draft) h
300 |
301 o 18:c1dec7ca82ea (draft) g
302 |
303 o 17:087281e68428 (draft) f
304 |
305 o 10:40db8afa467b (public) c
306 |
307 o 0:cb9a9f314b8b (public) a
308
309 $ cd ..
310
311
312 Changeset reordering
313 -------------------------------------------
314
315 If a secret changeset is put before a draft one, all descendant should be secret.
316 It seems more important to present the secret phase.
317
318 $ cp -r base reorder
319 $ cd reorder
320 $ cat > commands.txt <<EOF
321 > pick b449568bf7fc 11 f
322 > pick 3a6c53ee7f3d 15 j
323 > pick 6b70183d2492 12 g
324 > pick b605fb7503f2 14 i
325 > pick 7395e1ff83bd 13 h
326 > pick ee118ab9fa44 16 k
327 > EOF
328 $ hg histedit -r 'b449568bf7fc' --commands commands.txt
329 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
330 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
331 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
332 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
333 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
335 $ hg log -G
336 @ 21:558246857888 (secret) k
337 |
338 o 20:28bd44768535 (secret) h
339 |
340 o 19:d5395202aeb9 (secret) i
341 |
342 o 18:21edda8e341b (secret) g
343 |
344 o 17:5ab64f3a4832 (secret) j
345 |
346 o 11:b449568bf7fc (draft) f
347 |
348 o 10:40db8afa467b (public) c
349 |
350 o 0:cb9a9f314b8b (public) a
351
352 $ cd ..
353
354 Changeset folding
355 -------------------------------------------
356
357 Folding a secret changeset with a draft one turn the result secret (again,
358 better safe than sorry). Folding between same phase changeset still works
359
360 Note that there is a few reordering in this series for more extensive test
361
362 $ cp -r base folding
363 $ cd folding
364 $ cat >> .hg/hgrc << EOF
365 > [phases]
366 > new-commit=secret
367 > EOF
368 $ cat > commands.txt <<EOF
369 > pick 7395e1ff83bd 13 h
370 > fold b449568bf7fc 11 f
371 > pick 6b70183d2492 12 g
372 > fold 3a6c53ee7f3d 15 j
373 > pick b605fb7503f2 14 i
374 > fold ee118ab9fa44 16 k
375 > EOF
376 $ hg histedit -r 'b449568bf7fc' --commands commands.txt
377 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
378 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
379 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
380 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
381 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
383 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
384 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
385 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
386 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
387 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
388 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
389 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
390 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/58019c66f35f-backup.hg (glob)
391 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/83d1858e070b-backup.hg (glob)
392 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/859969f5ed7e-backup.hg (glob)
393 $ hg log -G
394 @ 19:f9daec13fb98 (secret) i
395 |
396 o 18:49807617f46a (secret) g
397 |
398 o 17:050280826e04 (draft) h
399 |
400 o 10:40db8afa467b (public) c
401 |
402 o 0:cb9a9f314b8b (public) a
403
404 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now