##// END OF EJS Templates
parseurl: consistently call second output "branches"...
Martin von Zweigbergk -
r37278:3809eafe default
parent child Browse files
Show More
@@ -1,1661 +1,1661 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 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 import errno
187 187 import os
188 188
189 189 from mercurial.i18n import _
190 190 from mercurial import (
191 191 bundle2,
192 192 cmdutil,
193 193 context,
194 194 copies,
195 195 destutil,
196 196 discovery,
197 197 error,
198 198 exchange,
199 199 extensions,
200 200 hg,
201 201 lock,
202 202 merge as mergemod,
203 203 mergeutil,
204 204 node,
205 205 obsolete,
206 206 pycompat,
207 207 registrar,
208 208 repair,
209 209 scmutil,
210 210 util,
211 211 )
212 212 from mercurial.utils import (
213 213 stringutil,
214 214 )
215 215
216 216 pickle = util.pickle
217 217 release = lock.release
218 218 cmdtable = {}
219 219 command = registrar.command(cmdtable)
220 220
221 221 configtable = {}
222 222 configitem = registrar.configitem(configtable)
223 223 configitem('experimental', 'histedit.autoverb',
224 224 default=False,
225 225 )
226 226 configitem('histedit', 'defaultrev',
227 227 default=None,
228 228 )
229 229 configitem('histedit', 'dropmissing',
230 230 default=False,
231 231 )
232 232 configitem('histedit', 'linelen',
233 233 default=80,
234 234 )
235 235 configitem('histedit', 'singletransaction',
236 236 default=False,
237 237 )
238 238
239 239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
240 240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
241 241 # be specifying the version(s) of Mercurial they are tested with, or
242 242 # leave the attribute unspecified.
243 243 testedwith = 'ships-with-hg-core'
244 244
245 245 actiontable = {}
246 246 primaryactions = set()
247 247 secondaryactions = set()
248 248 tertiaryactions = set()
249 249 internalactions = set()
250 250
251 251 def geteditcomment(ui, first, last):
252 252 """ construct the editor comment
253 253 The comment includes::
254 254 - an intro
255 255 - sorted primary commands
256 256 - sorted short commands
257 257 - sorted long commands
258 258 - additional hints
259 259
260 260 Commands are only included once.
261 261 """
262 262 intro = _("""Edit history between %s and %s
263 263
264 264 Commits are listed from least to most recent
265 265
266 266 You can reorder changesets by reordering the lines
267 267
268 268 Commands:
269 269 """)
270 270 actions = []
271 271 def addverb(v):
272 272 a = actiontable[v]
273 273 lines = a.message.split("\n")
274 274 if len(a.verbs):
275 275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
276 276 actions.append(" %s = %s" % (v, lines[0]))
277 277 actions.extend([' %s' for l in lines[1:]])
278 278
279 279 for v in (
280 280 sorted(primaryactions) +
281 281 sorted(secondaryactions) +
282 282 sorted(tertiaryactions)
283 283 ):
284 284 addverb(v)
285 285 actions.append('')
286 286
287 287 hints = []
288 288 if ui.configbool('histedit', 'dropmissing'):
289 289 hints.append("Deleting a changeset from the list "
290 290 "will DISCARD it from the edited history!")
291 291
292 292 lines = (intro % (first, last)).split('\n') + actions + hints
293 293
294 294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
295 295
296 296 class histeditstate(object):
297 297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
298 298 topmost=None, replacements=None, lock=None, wlock=None):
299 299 self.repo = repo
300 300 self.actions = actions
301 301 self.keep = keep
302 302 self.topmost = topmost
303 303 self.parentctxnode = parentctxnode
304 304 self.lock = lock
305 305 self.wlock = wlock
306 306 self.backupfile = None
307 307 if replacements is None:
308 308 self.replacements = []
309 309 else:
310 310 self.replacements = replacements
311 311
312 312 def read(self):
313 313 """Load histedit state from disk and set fields appropriately."""
314 314 try:
315 315 state = self.repo.vfs.read('histedit-state')
316 316 except IOError as err:
317 317 if err.errno != errno.ENOENT:
318 318 raise
319 319 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
320 320
321 321 if state.startswith('v1\n'):
322 322 data = self._load()
323 323 parentctxnode, rules, keep, topmost, replacements, backupfile = data
324 324 else:
325 325 data = pickle.loads(state)
326 326 parentctxnode, rules, keep, topmost, replacements = data
327 327 backupfile = None
328 328
329 329 self.parentctxnode = parentctxnode
330 330 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
331 331 actions = parserules(rules, self)
332 332 self.actions = actions
333 333 self.keep = keep
334 334 self.topmost = topmost
335 335 self.replacements = replacements
336 336 self.backupfile = backupfile
337 337
338 338 def write(self, tr=None):
339 339 if tr:
340 340 tr.addfilegenerator('histedit-state', ('histedit-state',),
341 341 self._write, location='plain')
342 342 else:
343 343 with self.repo.vfs("histedit-state", "w") as f:
344 344 self._write(f)
345 345
346 346 def _write(self, fp):
347 347 fp.write('v1\n')
348 348 fp.write('%s\n' % node.hex(self.parentctxnode))
349 349 fp.write('%s\n' % node.hex(self.topmost))
350 350 fp.write('%s\n' % ('True' if self.keep else 'False'))
351 351 fp.write('%d\n' % len(self.actions))
352 352 for action in self.actions:
353 353 fp.write('%s\n' % action.tostate())
354 354 fp.write('%d\n' % len(self.replacements))
355 355 for replacement in self.replacements:
356 356 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
357 357 for r in replacement[1])))
358 358 backupfile = self.backupfile
359 359 if not backupfile:
360 360 backupfile = ''
361 361 fp.write('%s\n' % backupfile)
362 362
363 363 def _load(self):
364 364 fp = self.repo.vfs('histedit-state', 'r')
365 365 lines = [l[:-1] for l in fp.readlines()]
366 366
367 367 index = 0
368 368 lines[index] # version number
369 369 index += 1
370 370
371 371 parentctxnode = node.bin(lines[index])
372 372 index += 1
373 373
374 374 topmost = node.bin(lines[index])
375 375 index += 1
376 376
377 377 keep = lines[index] == 'True'
378 378 index += 1
379 379
380 380 # Rules
381 381 rules = []
382 382 rulelen = int(lines[index])
383 383 index += 1
384 384 for i in xrange(rulelen):
385 385 ruleaction = lines[index]
386 386 index += 1
387 387 rule = lines[index]
388 388 index += 1
389 389 rules.append((ruleaction, rule))
390 390
391 391 # Replacements
392 392 replacements = []
393 393 replacementlen = int(lines[index])
394 394 index += 1
395 395 for i in xrange(replacementlen):
396 396 replacement = lines[index]
397 397 original = node.bin(replacement[:40])
398 398 succ = [node.bin(replacement[i:i + 40]) for i in
399 399 range(40, len(replacement), 40)]
400 400 replacements.append((original, succ))
401 401 index += 1
402 402
403 403 backupfile = lines[index]
404 404 index += 1
405 405
406 406 fp.close()
407 407
408 408 return parentctxnode, rules, keep, topmost, replacements, backupfile
409 409
410 410 def clear(self):
411 411 if self.inprogress():
412 412 self.repo.vfs.unlink('histedit-state')
413 413
414 414 def inprogress(self):
415 415 return self.repo.vfs.exists('histedit-state')
416 416
417 417
418 418 class histeditaction(object):
419 419 def __init__(self, state, node):
420 420 self.state = state
421 421 self.repo = state.repo
422 422 self.node = node
423 423
424 424 @classmethod
425 425 def fromrule(cls, state, rule):
426 426 """Parses the given rule, returning an instance of the histeditaction.
427 427 """
428 428 ruleid = rule.strip().split(' ', 1)[0]
429 429 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
430 430 # Check for validation of rule ids and get the rulehash
431 431 try:
432 432 rev = node.bin(ruleid)
433 433 except TypeError:
434 434 try:
435 435 _ctx = scmutil.revsingle(state.repo, ruleid)
436 436 rulehash = _ctx.hex()
437 437 rev = node.bin(rulehash)
438 438 except error.RepoLookupError:
439 439 raise error.ParseError("invalid changeset %s" % ruleid)
440 440 return cls(state, rev)
441 441
442 442 def verify(self, prev, expected, seen):
443 443 """ Verifies semantic correctness of the rule"""
444 444 repo = self.repo
445 445 ha = node.hex(self.node)
446 446 try:
447 447 self.node = repo[ha].node()
448 448 except error.RepoError:
449 449 raise error.ParseError(_('unknown changeset %s listed')
450 450 % ha[:12])
451 451 if self.node is not None:
452 452 self._verifynodeconstraints(prev, expected, seen)
453 453
454 454 def _verifynodeconstraints(self, prev, expected, seen):
455 455 # by default command need a node in the edited list
456 456 if self.node not in expected:
457 457 raise error.ParseError(_('%s "%s" changeset was not a candidate')
458 458 % (self.verb, node.short(self.node)),
459 459 hint=_('only use listed changesets'))
460 460 # and only one command per node
461 461 if self.node in seen:
462 462 raise error.ParseError(_('duplicated command for changeset %s') %
463 463 node.short(self.node))
464 464
465 465 def torule(self):
466 466 """build a histedit rule line for an action
467 467
468 468 by default lines are in the form:
469 469 <hash> <rev> <summary>
470 470 """
471 471 ctx = self.repo[self.node]
472 472 summary = _getsummary(ctx)
473 473 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
474 474 # trim to 75 columns by default so it's not stupidly wide in my editor
475 475 # (the 5 more are left for verb)
476 476 maxlen = self.repo.ui.configint('histedit', 'linelen')
477 477 maxlen = max(maxlen, 22) # avoid truncating hash
478 478 return stringutil.ellipsis(line, maxlen)
479 479
480 480 def tostate(self):
481 481 """Print an action in format used by histedit state files
482 482 (the first line is a verb, the remainder is the second)
483 483 """
484 484 return "%s\n%s" % (self.verb, node.hex(self.node))
485 485
486 486 def run(self):
487 487 """Runs the action. The default behavior is simply apply the action's
488 488 rulectx onto the current parentctx."""
489 489 self.applychange()
490 490 self.continuedirty()
491 491 return self.continueclean()
492 492
493 493 def applychange(self):
494 494 """Applies the changes from this action's rulectx onto the current
495 495 parentctx, but does not commit them."""
496 496 repo = self.repo
497 497 rulectx = repo[self.node]
498 498 repo.ui.pushbuffer(error=True, labeled=True)
499 499 hg.update(repo, self.state.parentctxnode, quietempty=True)
500 500 stats = applychanges(repo.ui, repo, rulectx, {})
501 501 repo.dirstate.setbranch(rulectx.branch())
502 502 if stats.unresolvedcount:
503 503 buf = repo.ui.popbuffer()
504 504 repo.ui.write(buf)
505 505 raise error.InterventionRequired(
506 506 _('Fix up the change (%s %s)') %
507 507 (self.verb, node.short(self.node)),
508 508 hint=_('hg histedit --continue to resume'))
509 509 else:
510 510 repo.ui.popbuffer()
511 511
512 512 def continuedirty(self):
513 513 """Continues the action when changes have been applied to the working
514 514 copy. The default behavior is to commit the dirty changes."""
515 515 repo = self.repo
516 516 rulectx = repo[self.node]
517 517
518 518 editor = self.commiteditor()
519 519 commit = commitfuncfor(repo, rulectx)
520 520
521 521 commit(text=rulectx.description(), user=rulectx.user(),
522 522 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
523 523
524 524 def commiteditor(self):
525 525 """The editor to be used to edit the commit message."""
526 526 return False
527 527
528 528 def continueclean(self):
529 529 """Continues the action when the working copy is clean. The default
530 530 behavior is to accept the current commit as the new version of the
531 531 rulectx."""
532 532 ctx = self.repo['.']
533 533 if ctx.node() == self.state.parentctxnode:
534 534 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
535 535 node.short(self.node))
536 536 return ctx, [(self.node, tuple())]
537 537 if ctx.node() == self.node:
538 538 # Nothing changed
539 539 return ctx, []
540 540 return ctx, [(self.node, (ctx.node(),))]
541 541
542 542 def commitfuncfor(repo, src):
543 543 """Build a commit function for the replacement of <src>
544 544
545 545 This function ensure we apply the same treatment to all changesets.
546 546
547 547 - Add a 'histedit_source' entry in extra.
548 548
549 549 Note that fold has its own separated logic because its handling is a bit
550 550 different and not easily factored out of the fold method.
551 551 """
552 552 phasemin = src.phase()
553 553 def commitfunc(**kwargs):
554 554 overrides = {('phases', 'new-commit'): phasemin}
555 555 with repo.ui.configoverride(overrides, 'histedit'):
556 556 extra = kwargs.get(r'extra', {}).copy()
557 557 extra['histedit_source'] = src.hex()
558 558 kwargs[r'extra'] = extra
559 559 return repo.commit(**kwargs)
560 560 return commitfunc
561 561
562 562 def applychanges(ui, repo, ctx, opts):
563 563 """Merge changeset from ctx (only) in the current working directory"""
564 564 wcpar = repo.dirstate.parents()[0]
565 565 if ctx.p1().node() == wcpar:
566 566 # edits are "in place" we do not need to make any merge,
567 567 # just applies changes on parent for editing
568 568 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
569 569 stats = mergemod.updateresult(0, 0, 0, 0)
570 570 else:
571 571 try:
572 572 # ui.forcemerge is an internal variable, do not document
573 573 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
574 574 'histedit')
575 575 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
576 576 finally:
577 577 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
578 578 return stats
579 579
580 580 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
581 581 """collapse the set of revisions from first to last as new one.
582 582
583 583 Expected commit options are:
584 584 - message
585 585 - date
586 586 - username
587 587 Commit message is edited in all cases.
588 588
589 589 This function works in memory."""
590 590 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
591 591 if not ctxs:
592 592 return None
593 593 for c in ctxs:
594 594 if not c.mutable():
595 595 raise error.ParseError(
596 596 _("cannot fold into public change %s") % node.short(c.node()))
597 597 base = firstctx.parents()[0]
598 598
599 599 # commit a new version of the old changeset, including the update
600 600 # collect all files which might be affected
601 601 files = set()
602 602 for ctx in ctxs:
603 603 files.update(ctx.files())
604 604
605 605 # Recompute copies (avoid recording a -> b -> a)
606 606 copied = copies.pathcopies(base, lastctx)
607 607
608 608 # prune files which were reverted by the updates
609 609 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
610 610 # commit version of these files as defined by head
611 611 headmf = lastctx.manifest()
612 612 def filectxfn(repo, ctx, path):
613 613 if path in headmf:
614 614 fctx = lastctx[path]
615 615 flags = fctx.flags()
616 616 mctx = context.memfilectx(repo, ctx,
617 617 fctx.path(), fctx.data(),
618 618 islink='l' in flags,
619 619 isexec='x' in flags,
620 620 copied=copied.get(path))
621 621 return mctx
622 622 return None
623 623
624 624 if commitopts.get('message'):
625 625 message = commitopts['message']
626 626 else:
627 627 message = firstctx.description()
628 628 user = commitopts.get('user')
629 629 date = commitopts.get('date')
630 630 extra = commitopts.get('extra')
631 631
632 632 parents = (firstctx.p1().node(), firstctx.p2().node())
633 633 editor = None
634 634 if not skipprompt:
635 635 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
636 636 new = context.memctx(repo,
637 637 parents=parents,
638 638 text=message,
639 639 files=files,
640 640 filectxfn=filectxfn,
641 641 user=user,
642 642 date=date,
643 643 extra=extra,
644 644 editor=editor)
645 645 return repo.commitctx(new)
646 646
647 647 def _isdirtywc(repo):
648 648 return repo[None].dirty(missing=True)
649 649
650 650 def abortdirty():
651 651 raise error.Abort(_('working copy has pending changes'),
652 652 hint=_('amend, commit, or revert them and run histedit '
653 653 '--continue, or abort with histedit --abort'))
654 654
655 655 def action(verbs, message, priority=False, internal=False):
656 656 def wrap(cls):
657 657 assert not priority or not internal
658 658 verb = verbs[0]
659 659 if priority:
660 660 primaryactions.add(verb)
661 661 elif internal:
662 662 internalactions.add(verb)
663 663 elif len(verbs) > 1:
664 664 secondaryactions.add(verb)
665 665 else:
666 666 tertiaryactions.add(verb)
667 667
668 668 cls.verb = verb
669 669 cls.verbs = verbs
670 670 cls.message = message
671 671 for verb in verbs:
672 672 actiontable[verb] = cls
673 673 return cls
674 674 return wrap
675 675
676 676 @action(['pick', 'p'],
677 677 _('use commit'),
678 678 priority=True)
679 679 class pick(histeditaction):
680 680 def run(self):
681 681 rulectx = self.repo[self.node]
682 682 if rulectx.parents()[0].node() == self.state.parentctxnode:
683 683 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
684 684 return rulectx, []
685 685
686 686 return super(pick, self).run()
687 687
688 688 @action(['edit', 'e'],
689 689 _('use commit, but stop for amending'),
690 690 priority=True)
691 691 class edit(histeditaction):
692 692 def run(self):
693 693 repo = self.repo
694 694 rulectx = repo[self.node]
695 695 hg.update(repo, self.state.parentctxnode, quietempty=True)
696 696 applychanges(repo.ui, repo, rulectx, {})
697 697 raise error.InterventionRequired(
698 698 _('Editing (%s), you may commit or record as needed now.')
699 699 % node.short(self.node),
700 700 hint=_('hg histedit --continue to resume'))
701 701
702 702 def commiteditor(self):
703 703 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
704 704
705 705 @action(['fold', 'f'],
706 706 _('use commit, but combine it with the one above'))
707 707 class fold(histeditaction):
708 708 def verify(self, prev, expected, seen):
709 709 """ Verifies semantic correctness of the fold rule"""
710 710 super(fold, self).verify(prev, expected, seen)
711 711 repo = self.repo
712 712 if not prev:
713 713 c = repo[self.node].parents()[0]
714 714 elif not prev.verb in ('pick', 'base'):
715 715 return
716 716 else:
717 717 c = repo[prev.node]
718 718 if not c.mutable():
719 719 raise error.ParseError(
720 720 _("cannot fold into public change %s") % node.short(c.node()))
721 721
722 722
723 723 def continuedirty(self):
724 724 repo = self.repo
725 725 rulectx = repo[self.node]
726 726
727 727 commit = commitfuncfor(repo, rulectx)
728 728 commit(text='fold-temp-revision %s' % node.short(self.node),
729 729 user=rulectx.user(), date=rulectx.date(),
730 730 extra=rulectx.extra())
731 731
732 732 def continueclean(self):
733 733 repo = self.repo
734 734 ctx = repo['.']
735 735 rulectx = repo[self.node]
736 736 parentctxnode = self.state.parentctxnode
737 737 if ctx.node() == parentctxnode:
738 738 repo.ui.warn(_('%s: empty changeset\n') %
739 739 node.short(self.node))
740 740 return ctx, [(self.node, (parentctxnode,))]
741 741
742 742 parentctx = repo[parentctxnode]
743 743 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
744 744 parentctx.rev(),
745 745 parentctx.rev()))
746 746 if not newcommits:
747 747 repo.ui.warn(_('%s: cannot fold - working copy is not a '
748 748 'descendant of previous commit %s\n') %
749 749 (node.short(self.node), node.short(parentctxnode)))
750 750 return ctx, [(self.node, (ctx.node(),))]
751 751
752 752 middlecommits = newcommits.copy()
753 753 middlecommits.discard(ctx.node())
754 754
755 755 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
756 756 middlecommits)
757 757
758 758 def skipprompt(self):
759 759 """Returns true if the rule should skip the message editor.
760 760
761 761 For example, 'fold' wants to show an editor, but 'rollup'
762 762 doesn't want to.
763 763 """
764 764 return False
765 765
766 766 def mergedescs(self):
767 767 """Returns true if the rule should merge messages of multiple changes.
768 768
769 769 This exists mainly so that 'rollup' rules can be a subclass of
770 770 'fold'.
771 771 """
772 772 return True
773 773
774 774 def firstdate(self):
775 775 """Returns true if the rule should preserve the date of the first
776 776 change.
777 777
778 778 This exists mainly so that 'rollup' rules can be a subclass of
779 779 'fold'.
780 780 """
781 781 return False
782 782
783 783 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
784 784 parent = ctx.parents()[0].node()
785 785 repo.ui.pushbuffer()
786 786 hg.update(repo, parent)
787 787 repo.ui.popbuffer()
788 788 ### prepare new commit data
789 789 commitopts = {}
790 790 commitopts['user'] = ctx.user()
791 791 # commit message
792 792 if not self.mergedescs():
793 793 newmessage = ctx.description()
794 794 else:
795 795 newmessage = '\n***\n'.join(
796 796 [ctx.description()] +
797 797 [repo[r].description() for r in internalchanges] +
798 798 [oldctx.description()]) + '\n'
799 799 commitopts['message'] = newmessage
800 800 # date
801 801 if self.firstdate():
802 802 commitopts['date'] = ctx.date()
803 803 else:
804 804 commitopts['date'] = max(ctx.date(), oldctx.date())
805 805 extra = ctx.extra().copy()
806 806 # histedit_source
807 807 # note: ctx is likely a temporary commit but that the best we can do
808 808 # here. This is sufficient to solve issue3681 anyway.
809 809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
810 810 commitopts['extra'] = extra
811 811 phasemin = max(ctx.phase(), oldctx.phase())
812 812 overrides = {('phases', 'new-commit'): phasemin}
813 813 with repo.ui.configoverride(overrides, 'histedit'):
814 814 n = collapse(repo, ctx, repo[newnode], commitopts,
815 815 skipprompt=self.skipprompt())
816 816 if n is None:
817 817 return ctx, []
818 818 repo.ui.pushbuffer()
819 819 hg.update(repo, n)
820 820 repo.ui.popbuffer()
821 821 replacements = [(oldctx.node(), (newnode,)),
822 822 (ctx.node(), (n,)),
823 823 (newnode, (n,)),
824 824 ]
825 825 for ich in internalchanges:
826 826 replacements.append((ich, (n,)))
827 827 return repo[n], replacements
828 828
829 829 @action(['base', 'b'],
830 830 _('checkout changeset and apply further changesets from there'))
831 831 class base(histeditaction):
832 832
833 833 def run(self):
834 834 if self.repo['.'].node() != self.node:
835 835 mergemod.update(self.repo, self.node, False, True)
836 836 # branchmerge, force)
837 837 return self.continueclean()
838 838
839 839 def continuedirty(self):
840 840 abortdirty()
841 841
842 842 def continueclean(self):
843 843 basectx = self.repo['.']
844 844 return basectx, []
845 845
846 846 def _verifynodeconstraints(self, prev, expected, seen):
847 847 # base can only be use with a node not in the edited set
848 848 if self.node in expected:
849 849 msg = _('%s "%s" changeset was an edited list candidate')
850 850 raise error.ParseError(
851 851 msg % (self.verb, node.short(self.node)),
852 852 hint=_('base must only use unlisted changesets'))
853 853
854 854 @action(['_multifold'],
855 855 _(
856 856 """fold subclass used for when multiple folds happen in a row
857 857
858 858 We only want to fire the editor for the folded message once when
859 859 (say) four changes are folded down into a single change. This is
860 860 similar to rollup, but we should preserve both messages so that
861 861 when the last fold operation runs we can show the user all the
862 862 commit messages in their editor.
863 863 """),
864 864 internal=True)
865 865 class _multifold(fold):
866 866 def skipprompt(self):
867 867 return True
868 868
869 869 @action(["roll", "r"],
870 870 _("like fold, but discard this commit's description and date"))
871 871 class rollup(fold):
872 872 def mergedescs(self):
873 873 return False
874 874
875 875 def skipprompt(self):
876 876 return True
877 877
878 878 def firstdate(self):
879 879 return True
880 880
881 881 @action(["drop", "d"],
882 882 _('remove commit from history'))
883 883 class drop(histeditaction):
884 884 def run(self):
885 885 parentctx = self.repo[self.state.parentctxnode]
886 886 return parentctx, [(self.node, tuple())]
887 887
888 888 @action(["mess", "m"],
889 889 _('edit commit message without changing commit content'),
890 890 priority=True)
891 891 class message(histeditaction):
892 892 def commiteditor(self):
893 893 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
894 894
895 895 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
896 896 """utility function to find the first outgoing changeset
897 897
898 898 Used by initialization code"""
899 899 if opts is None:
900 900 opts = {}
901 901 dest = ui.expandpath(remote or 'default-push', remote or 'default')
902 dest, revs = hg.parseurl(dest, None)[:2]
902 dest, branches = hg.parseurl(dest, None)[:2]
903 903 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
904 904
905 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
905 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
906 906 other = hg.peer(repo, opts, dest)
907 907
908 908 if revs:
909 909 revs = [repo.lookup(rev) for rev in revs]
910 910
911 911 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
912 912 if not outgoing.missing:
913 913 raise error.Abort(_('no outgoing ancestors'))
914 914 roots = list(repo.revs("roots(%ln)", outgoing.missing))
915 915 if 1 < len(roots):
916 916 msg = _('there are ambiguous outgoing revisions')
917 917 hint = _("see 'hg help histedit' for more detail")
918 918 raise error.Abort(msg, hint=hint)
919 919 return repo.lookup(roots[0])
920 920
921 921 @command('histedit',
922 922 [('', 'commands', '',
923 923 _('read history edits from the specified file'), _('FILE')),
924 924 ('c', 'continue', False, _('continue an edit already in progress')),
925 925 ('', 'edit-plan', False, _('edit remaining actions list')),
926 926 ('k', 'keep', False,
927 927 _("don't strip old nodes after edit is complete")),
928 928 ('', 'abort', False, _('abort an edit in progress')),
929 929 ('o', 'outgoing', False, _('changesets not found in destination')),
930 930 ('f', 'force', False,
931 931 _('force outgoing even for unrelated repositories')),
932 932 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
933 933 cmdutil.formatteropts,
934 934 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
935 935 def histedit(ui, repo, *freeargs, **opts):
936 936 """interactively edit changeset history
937 937
938 938 This command lets you edit a linear series of changesets (up to
939 939 and including the working directory, which should be clean).
940 940 You can:
941 941
942 942 - `pick` to [re]order a changeset
943 943
944 944 - `drop` to omit changeset
945 945
946 946 - `mess` to reword the changeset commit message
947 947
948 948 - `fold` to combine it with the preceding changeset (using the later date)
949 949
950 950 - `roll` like fold, but discarding this commit's description and date
951 951
952 952 - `edit` to edit this changeset (preserving date)
953 953
954 954 - `base` to checkout changeset and apply further changesets from there
955 955
956 956 There are a number of ways to select the root changeset:
957 957
958 958 - Specify ANCESTOR directly
959 959
960 960 - Use --outgoing -- it will be the first linear changeset not
961 961 included in destination. (See :hg:`help config.paths.default-push`)
962 962
963 963 - Otherwise, the value from the "histedit.defaultrev" config option
964 964 is used as a revset to select the base revision when ANCESTOR is not
965 965 specified. The first revision returned by the revset is used. By
966 966 default, this selects the editable history that is unique to the
967 967 ancestry of the working directory.
968 968
969 969 .. container:: verbose
970 970
971 971 If you use --outgoing, this command will abort if there are ambiguous
972 972 outgoing revisions. For example, if there are multiple branches
973 973 containing outgoing revisions.
974 974
975 975 Use "min(outgoing() and ::.)" or similar revset specification
976 976 instead of --outgoing to specify edit target revision exactly in
977 977 such ambiguous situation. See :hg:`help revsets` for detail about
978 978 selecting revisions.
979 979
980 980 .. container:: verbose
981 981
982 982 Examples:
983 983
984 984 - A number of changes have been made.
985 985 Revision 3 is no longer needed.
986 986
987 987 Start history editing from revision 3::
988 988
989 989 hg histedit -r 3
990 990
991 991 An editor opens, containing the list of revisions,
992 992 with specific actions specified::
993 993
994 994 pick 5339bf82f0ca 3 Zworgle the foobar
995 995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
996 996 pick 0a9639fcda9d 5 Morgify the cromulancy
997 997
998 998 Additional information about the possible actions
999 999 to take appears below the list of revisions.
1000 1000
1001 1001 To remove revision 3 from the history,
1002 1002 its action (at the beginning of the relevant line)
1003 1003 is changed to 'drop'::
1004 1004
1005 1005 drop 5339bf82f0ca 3 Zworgle the foobar
1006 1006 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1007 1007 pick 0a9639fcda9d 5 Morgify the cromulancy
1008 1008
1009 1009 - A number of changes have been made.
1010 1010 Revision 2 and 4 need to be swapped.
1011 1011
1012 1012 Start history editing from revision 2::
1013 1013
1014 1014 hg histedit -r 2
1015 1015
1016 1016 An editor opens, containing the list of revisions,
1017 1017 with specific actions specified::
1018 1018
1019 1019 pick 252a1af424ad 2 Blorb a morgwazzle
1020 1020 pick 5339bf82f0ca 3 Zworgle the foobar
1021 1021 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1022 1022
1023 1023 To swap revision 2 and 4, its lines are swapped
1024 1024 in the editor::
1025 1025
1026 1026 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1027 1027 pick 5339bf82f0ca 3 Zworgle the foobar
1028 1028 pick 252a1af424ad 2 Blorb a morgwazzle
1029 1029
1030 1030 Returns 0 on success, 1 if user intervention is required (not only
1031 1031 for intentional "edit" command, but also for resolving unexpected
1032 1032 conflicts).
1033 1033 """
1034 1034 state = histeditstate(repo)
1035 1035 try:
1036 1036 state.wlock = repo.wlock()
1037 1037 state.lock = repo.lock()
1038 1038 _histedit(ui, repo, state, *freeargs, **opts)
1039 1039 finally:
1040 1040 release(state.lock, state.wlock)
1041 1041
1042 1042 goalcontinue = 'continue'
1043 1043 goalabort = 'abort'
1044 1044 goaleditplan = 'edit-plan'
1045 1045 goalnew = 'new'
1046 1046
1047 1047 def _getgoal(opts):
1048 1048 if opts.get('continue'):
1049 1049 return goalcontinue
1050 1050 if opts.get('abort'):
1051 1051 return goalabort
1052 1052 if opts.get('edit_plan'):
1053 1053 return goaleditplan
1054 1054 return goalnew
1055 1055
1056 1056 def _readfile(ui, path):
1057 1057 if path == '-':
1058 1058 with ui.timeblockedsection('histedit'):
1059 1059 return ui.fin.read()
1060 1060 else:
1061 1061 with open(path, 'rb') as f:
1062 1062 return f.read()
1063 1063
1064 1064 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1065 1065 # TODO only abort if we try to histedit mq patches, not just
1066 1066 # blanket if mq patches are applied somewhere
1067 1067 mq = getattr(repo, 'mq', None)
1068 1068 if mq and mq.applied:
1069 1069 raise error.Abort(_('source has mq patches applied'))
1070 1070
1071 1071 # basic argument incompatibility processing
1072 1072 outg = opts.get('outgoing')
1073 1073 editplan = opts.get('edit_plan')
1074 1074 abort = opts.get('abort')
1075 1075 force = opts.get('force')
1076 1076 if force and not outg:
1077 1077 raise error.Abort(_('--force only allowed with --outgoing'))
1078 1078 if goal == 'continue':
1079 1079 if any((outg, abort, revs, freeargs, rules, editplan)):
1080 1080 raise error.Abort(_('no arguments allowed with --continue'))
1081 1081 elif goal == 'abort':
1082 1082 if any((outg, revs, freeargs, rules, editplan)):
1083 1083 raise error.Abort(_('no arguments allowed with --abort'))
1084 1084 elif goal == 'edit-plan':
1085 1085 if any((outg, revs, freeargs)):
1086 1086 raise error.Abort(_('only --commands argument allowed with '
1087 1087 '--edit-plan'))
1088 1088 else:
1089 1089 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1090 1090 raise error.Abort(_('history edit already in progress, try '
1091 1091 '--continue or --abort'))
1092 1092 if outg:
1093 1093 if revs:
1094 1094 raise error.Abort(_('no revisions allowed with --outgoing'))
1095 1095 if len(freeargs) > 1:
1096 1096 raise error.Abort(
1097 1097 _('only one repo argument allowed with --outgoing'))
1098 1098 else:
1099 1099 revs.extend(freeargs)
1100 1100 if len(revs) == 0:
1101 1101 defaultrev = destutil.desthistedit(ui, repo)
1102 1102 if defaultrev is not None:
1103 1103 revs.append(defaultrev)
1104 1104
1105 1105 if len(revs) != 1:
1106 1106 raise error.Abort(
1107 1107 _('histedit requires exactly one ancestor revision'))
1108 1108
1109 1109 def _histedit(ui, repo, state, *freeargs, **opts):
1110 1110 opts = pycompat.byteskwargs(opts)
1111 1111 fm = ui.formatter('histedit', opts)
1112 1112 fm.startitem()
1113 1113 goal = _getgoal(opts)
1114 1114 revs = opts.get('rev', [])
1115 1115 rules = opts.get('commands', '')
1116 1116 state.keep = opts.get('keep', False)
1117 1117
1118 1118 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1119 1119
1120 1120 # rebuild state
1121 1121 if goal == goalcontinue:
1122 1122 state.read()
1123 1123 state = bootstrapcontinue(ui, state, opts)
1124 1124 elif goal == goaleditplan:
1125 1125 _edithisteditplan(ui, repo, state, rules)
1126 1126 return
1127 1127 elif goal == goalabort:
1128 1128 _aborthistedit(ui, repo, state)
1129 1129 return
1130 1130 else:
1131 1131 # goal == goalnew
1132 1132 _newhistedit(ui, repo, state, revs, freeargs, opts)
1133 1133
1134 1134 _continuehistedit(ui, repo, state)
1135 1135 _finishhistedit(ui, repo, state, fm)
1136 1136 fm.end()
1137 1137
1138 1138 def _continuehistedit(ui, repo, state):
1139 1139 """This function runs after either:
1140 1140 - bootstrapcontinue (if the goal is 'continue')
1141 1141 - _newhistedit (if the goal is 'new')
1142 1142 """
1143 1143 # preprocess rules so that we can hide inner folds from the user
1144 1144 # and only show one editor
1145 1145 actions = state.actions[:]
1146 1146 for idx, (action, nextact) in enumerate(
1147 1147 zip(actions, actions[1:] + [None])):
1148 1148 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1149 1149 state.actions[idx].__class__ = _multifold
1150 1150
1151 1151 # Force an initial state file write, so the user can run --abort/continue
1152 1152 # even if there's an exception before the first transaction serialize.
1153 1153 state.write()
1154 1154
1155 1155 total = len(state.actions)
1156 1156 pos = 0
1157 1157 tr = None
1158 1158 # Don't use singletransaction by default since it rolls the entire
1159 1159 # transaction back if an unexpected exception happens (like a
1160 1160 # pretxncommit hook throws, or the user aborts the commit msg editor).
1161 1161 if ui.configbool("histedit", "singletransaction"):
1162 1162 # Don't use a 'with' for the transaction, since actions may close
1163 1163 # and reopen a transaction. For example, if the action executes an
1164 1164 # external process it may choose to commit the transaction first.
1165 1165 tr = repo.transaction('histedit')
1166 1166 with util.acceptintervention(tr):
1167 1167 while state.actions:
1168 1168 state.write(tr=tr)
1169 1169 actobj = state.actions[0]
1170 1170 pos += 1
1171 1171 ui.progress(_("editing"), pos, actobj.torule(),
1172 1172 _('changes'), total)
1173 1173 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1174 1174 actobj.torule()))
1175 1175 parentctx, replacement_ = actobj.run()
1176 1176 state.parentctxnode = parentctx.node()
1177 1177 state.replacements.extend(replacement_)
1178 1178 state.actions.pop(0)
1179 1179
1180 1180 state.write()
1181 1181 ui.progress(_("editing"), None)
1182 1182
1183 1183 def _finishhistedit(ui, repo, state, fm):
1184 1184 """This action runs when histedit is finishing its session"""
1185 1185 repo.ui.pushbuffer()
1186 1186 hg.update(repo, state.parentctxnode, quietempty=True)
1187 1187 repo.ui.popbuffer()
1188 1188
1189 1189 mapping, tmpnodes, created, ntm = processreplacement(state)
1190 1190 if mapping:
1191 1191 for prec, succs in mapping.iteritems():
1192 1192 if not succs:
1193 1193 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1194 1194 else:
1195 1195 ui.debug('histedit: %s is replaced by %s\n' % (
1196 1196 node.short(prec), node.short(succs[0])))
1197 1197 if len(succs) > 1:
1198 1198 m = 'histedit: %s'
1199 1199 for n in succs[1:]:
1200 1200 ui.debug(m % node.short(n))
1201 1201
1202 1202 if not state.keep:
1203 1203 if mapping:
1204 1204 movetopmostbookmarks(repo, state.topmost, ntm)
1205 1205 # TODO update mq state
1206 1206 else:
1207 1207 mapping = {}
1208 1208
1209 1209 for n in tmpnodes:
1210 1210 mapping[n] = ()
1211 1211
1212 1212 # remove entries about unknown nodes
1213 1213 nodemap = repo.unfiltered().changelog.nodemap
1214 1214 mapping = {k: v for k, v in mapping.items()
1215 1215 if k in nodemap and all(n in nodemap for n in v)}
1216 1216 scmutil.cleanupnodes(repo, mapping, 'histedit')
1217 1217 hf = fm.hexfunc
1218 1218 fl = fm.formatlist
1219 1219 fd = fm.formatdict
1220 1220 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1221 1221 for oldn, newn in mapping.iteritems()},
1222 1222 key="oldnode", value="newnodes")
1223 1223 fm.data(nodechanges=nodechanges)
1224 1224
1225 1225 state.clear()
1226 1226 if os.path.exists(repo.sjoin('undo')):
1227 1227 os.unlink(repo.sjoin('undo'))
1228 1228 if repo.vfs.exists('histedit-last-edit.txt'):
1229 1229 repo.vfs.unlink('histedit-last-edit.txt')
1230 1230
1231 1231 def _aborthistedit(ui, repo, state):
1232 1232 try:
1233 1233 state.read()
1234 1234 __, leafs, tmpnodes, __ = processreplacement(state)
1235 1235 ui.debug('restore wc to old parent %s\n'
1236 1236 % node.short(state.topmost))
1237 1237
1238 1238 # Recover our old commits if necessary
1239 1239 if not state.topmost in repo and state.backupfile:
1240 1240 backupfile = repo.vfs.join(state.backupfile)
1241 1241 f = hg.openpath(ui, backupfile)
1242 1242 gen = exchange.readbundle(ui, f, backupfile)
1243 1243 with repo.transaction('histedit.abort') as tr:
1244 1244 bundle2.applybundle(repo, gen, tr, source='histedit',
1245 1245 url='bundle:' + backupfile)
1246 1246
1247 1247 os.remove(backupfile)
1248 1248
1249 1249 # check whether we should update away
1250 1250 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1251 1251 state.parentctxnode, leafs | tmpnodes):
1252 1252 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1253 1253 cleanupnode(ui, repo, tmpnodes)
1254 1254 cleanupnode(ui, repo, leafs)
1255 1255 except Exception:
1256 1256 if state.inprogress():
1257 1257 ui.warn(_('warning: encountered an exception during histedit '
1258 1258 '--abort; the repository may not have been completely '
1259 1259 'cleaned up\n'))
1260 1260 raise
1261 1261 finally:
1262 1262 state.clear()
1263 1263
1264 1264 def _edithisteditplan(ui, repo, state, rules):
1265 1265 state.read()
1266 1266 if not rules:
1267 1267 comment = geteditcomment(ui,
1268 1268 node.short(state.parentctxnode),
1269 1269 node.short(state.topmost))
1270 1270 rules = ruleeditor(repo, ui, state.actions, comment)
1271 1271 else:
1272 1272 rules = _readfile(ui, rules)
1273 1273 actions = parserules(rules, state)
1274 1274 ctxs = [repo[act.node] \
1275 1275 for act in state.actions if act.node]
1276 1276 warnverifyactions(ui, repo, actions, state, ctxs)
1277 1277 state.actions = actions
1278 1278 state.write()
1279 1279
1280 1280 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1281 1281 outg = opts.get('outgoing')
1282 1282 rules = opts.get('commands', '')
1283 1283 force = opts.get('force')
1284 1284
1285 1285 cmdutil.checkunfinished(repo)
1286 1286 cmdutil.bailifchanged(repo)
1287 1287
1288 1288 topmost, empty = repo.dirstate.parents()
1289 1289 if outg:
1290 1290 if freeargs:
1291 1291 remote = freeargs[0]
1292 1292 else:
1293 1293 remote = None
1294 1294 root = findoutgoing(ui, repo, remote, force, opts)
1295 1295 else:
1296 1296 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1297 1297 if len(rr) != 1:
1298 1298 raise error.Abort(_('The specified revisions must have '
1299 1299 'exactly one common root'))
1300 1300 root = rr[0].node()
1301 1301
1302 1302 revs = between(repo, root, topmost, state.keep)
1303 1303 if not revs:
1304 1304 raise error.Abort(_('%s is not an ancestor of working directory') %
1305 1305 node.short(root))
1306 1306
1307 1307 ctxs = [repo[r] for r in revs]
1308 1308 if not rules:
1309 1309 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1310 1310 actions = [pick(state, r) for r in revs]
1311 1311 rules = ruleeditor(repo, ui, actions, comment)
1312 1312 else:
1313 1313 rules = _readfile(ui, rules)
1314 1314 actions = parserules(rules, state)
1315 1315 warnverifyactions(ui, repo, actions, state, ctxs)
1316 1316
1317 1317 parentctxnode = repo[root].parents()[0].node()
1318 1318
1319 1319 state.parentctxnode = parentctxnode
1320 1320 state.actions = actions
1321 1321 state.topmost = topmost
1322 1322 state.replacements = []
1323 1323
1324 1324 ui.log("histedit", "%d actions to histedit", len(actions),
1325 1325 histedit_num_actions=len(actions))
1326 1326
1327 1327 # Create a backup so we can always abort completely.
1328 1328 backupfile = None
1329 1329 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1330 1330 backupfile = repair.backupbundle(repo, [parentctxnode],
1331 1331 [topmost], root, 'histedit')
1332 1332 state.backupfile = backupfile
1333 1333
1334 1334 def _getsummary(ctx):
1335 1335 # a common pattern is to extract the summary but default to the empty
1336 1336 # string
1337 1337 summary = ctx.description() or ''
1338 1338 if summary:
1339 1339 summary = summary.splitlines()[0]
1340 1340 return summary
1341 1341
1342 1342 def bootstrapcontinue(ui, state, opts):
1343 1343 repo = state.repo
1344 1344
1345 1345 ms = mergemod.mergestate.read(repo)
1346 1346 mergeutil.checkunresolved(ms)
1347 1347
1348 1348 if state.actions:
1349 1349 actobj = state.actions.pop(0)
1350 1350
1351 1351 if _isdirtywc(repo):
1352 1352 actobj.continuedirty()
1353 1353 if _isdirtywc(repo):
1354 1354 abortdirty()
1355 1355
1356 1356 parentctx, replacements = actobj.continueclean()
1357 1357
1358 1358 state.parentctxnode = parentctx.node()
1359 1359 state.replacements.extend(replacements)
1360 1360
1361 1361 return state
1362 1362
1363 1363 def between(repo, old, new, keep):
1364 1364 """select and validate the set of revision to edit
1365 1365
1366 1366 When keep is false, the specified set can't have children."""
1367 1367 revs = repo.revs('%n::%n', old, new)
1368 1368 if revs and not keep:
1369 1369 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1370 1370 repo.revs('(%ld::) - (%ld)', revs, revs)):
1371 1371 raise error.Abort(_('can only histedit a changeset together '
1372 1372 'with all its descendants'))
1373 1373 if repo.revs('(%ld) and merge()', revs):
1374 1374 raise error.Abort(_('cannot edit history that contains merges'))
1375 1375 root = repo[revs.first()] # list is already sorted by repo.revs()
1376 1376 if not root.mutable():
1377 1377 raise error.Abort(_('cannot edit public changeset: %s') % root,
1378 1378 hint=_("see 'hg help phases' for details"))
1379 1379 return pycompat.maplist(repo.changelog.node, revs)
1380 1380
1381 1381 def ruleeditor(repo, ui, actions, editcomment=""):
1382 1382 """open an editor to edit rules
1383 1383
1384 1384 rules are in the format [ [act, ctx], ...] like in state.rules
1385 1385 """
1386 1386 if repo.ui.configbool("experimental", "histedit.autoverb"):
1387 1387 newact = util.sortdict()
1388 1388 for act in actions:
1389 1389 ctx = repo[act.node]
1390 1390 summary = _getsummary(ctx)
1391 1391 fword = summary.split(' ', 1)[0].lower()
1392 1392 added = False
1393 1393
1394 1394 # if it doesn't end with the special character '!' just skip this
1395 1395 if fword.endswith('!'):
1396 1396 fword = fword[:-1]
1397 1397 if fword in primaryactions | secondaryactions | tertiaryactions:
1398 1398 act.verb = fword
1399 1399 # get the target summary
1400 1400 tsum = summary[len(fword) + 1:].lstrip()
1401 1401 # safe but slow: reverse iterate over the actions so we
1402 1402 # don't clash on two commits having the same summary
1403 1403 for na, l in reversed(list(newact.iteritems())):
1404 1404 actx = repo[na.node]
1405 1405 asum = _getsummary(actx)
1406 1406 if asum == tsum:
1407 1407 added = True
1408 1408 l.append(act)
1409 1409 break
1410 1410
1411 1411 if not added:
1412 1412 newact[act] = []
1413 1413
1414 1414 # copy over and flatten the new list
1415 1415 actions = []
1416 1416 for na, l in newact.iteritems():
1417 1417 actions.append(na)
1418 1418 actions += l
1419 1419
1420 1420 rules = '\n'.join([act.torule() for act in actions])
1421 1421 rules += '\n\n'
1422 1422 rules += editcomment
1423 1423 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1424 1424 repopath=repo.path, action='histedit')
1425 1425
1426 1426 # Save edit rules in .hg/histedit-last-edit.txt in case
1427 1427 # the user needs to ask for help after something
1428 1428 # surprising happens.
1429 1429 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1430 1430 f.write(rules)
1431 1431
1432 1432 return rules
1433 1433
1434 1434 def parserules(rules, state):
1435 1435 """Read the histedit rules string and return list of action objects """
1436 1436 rules = [l for l in (r.strip() for r in rules.splitlines())
1437 1437 if l and not l.startswith('#')]
1438 1438 actions = []
1439 1439 for r in rules:
1440 1440 if ' ' not in r:
1441 1441 raise error.ParseError(_('malformed line "%s"') % r)
1442 1442 verb, rest = r.split(' ', 1)
1443 1443
1444 1444 if verb not in actiontable:
1445 1445 raise error.ParseError(_('unknown action "%s"') % verb)
1446 1446
1447 1447 action = actiontable[verb].fromrule(state, rest)
1448 1448 actions.append(action)
1449 1449 return actions
1450 1450
1451 1451 def warnverifyactions(ui, repo, actions, state, ctxs):
1452 1452 try:
1453 1453 verifyactions(actions, state, ctxs)
1454 1454 except error.ParseError:
1455 1455 if repo.vfs.exists('histedit-last-edit.txt'):
1456 1456 ui.warn(_('warning: histedit rules saved '
1457 1457 'to: .hg/histedit-last-edit.txt\n'))
1458 1458 raise
1459 1459
1460 1460 def verifyactions(actions, state, ctxs):
1461 1461 """Verify that there exists exactly one action per given changeset and
1462 1462 other constraints.
1463 1463
1464 1464 Will abort if there are to many or too few rules, a malformed rule,
1465 1465 or a rule on a changeset outside of the user-given range.
1466 1466 """
1467 1467 expected = set(c.node() for c in ctxs)
1468 1468 seen = set()
1469 1469 prev = None
1470 1470
1471 1471 if actions and actions[0].verb in ['roll', 'fold']:
1472 1472 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1473 1473 actions[0].verb)
1474 1474
1475 1475 for action in actions:
1476 1476 action.verify(prev, expected, seen)
1477 1477 prev = action
1478 1478 if action.node is not None:
1479 1479 seen.add(action.node)
1480 1480 missing = sorted(expected - seen) # sort to stabilize output
1481 1481
1482 1482 if state.repo.ui.configbool('histedit', 'dropmissing'):
1483 1483 if len(actions) == 0:
1484 1484 raise error.ParseError(_('no rules provided'),
1485 1485 hint=_('use strip extension to remove commits'))
1486 1486
1487 1487 drops = [drop(state, n) for n in missing]
1488 1488 # put the in the beginning so they execute immediately and
1489 1489 # don't show in the edit-plan in the future
1490 1490 actions[:0] = drops
1491 1491 elif missing:
1492 1492 raise error.ParseError(_('missing rules for changeset %s') %
1493 1493 node.short(missing[0]),
1494 1494 hint=_('use "drop %s" to discard, see also: '
1495 1495 "'hg help -e histedit.config'")
1496 1496 % node.short(missing[0]))
1497 1497
1498 1498 def adjustreplacementsfrommarkers(repo, oldreplacements):
1499 1499 """Adjust replacements from obsolescence markers
1500 1500
1501 1501 Replacements structure is originally generated based on
1502 1502 histedit's state and does not account for changes that are
1503 1503 not recorded there. This function fixes that by adding
1504 1504 data read from obsolescence markers"""
1505 1505 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1506 1506 return oldreplacements
1507 1507
1508 1508 unfi = repo.unfiltered()
1509 1509 nm = unfi.changelog.nodemap
1510 1510 obsstore = repo.obsstore
1511 1511 newreplacements = list(oldreplacements)
1512 1512 oldsuccs = [r[1] for r in oldreplacements]
1513 1513 # successors that have already been added to succstocheck once
1514 1514 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1515 1515 succstocheck = list(seensuccs)
1516 1516 while succstocheck:
1517 1517 n = succstocheck.pop()
1518 1518 missing = nm.get(n) is None
1519 1519 markers = obsstore.successors.get(n, ())
1520 1520 if missing and not markers:
1521 1521 # dead end, mark it as such
1522 1522 newreplacements.append((n, ()))
1523 1523 for marker in markers:
1524 1524 nsuccs = marker[1]
1525 1525 newreplacements.append((n, nsuccs))
1526 1526 for nsucc in nsuccs:
1527 1527 if nsucc not in seensuccs:
1528 1528 seensuccs.add(nsucc)
1529 1529 succstocheck.append(nsucc)
1530 1530
1531 1531 return newreplacements
1532 1532
1533 1533 def processreplacement(state):
1534 1534 """process the list of replacements to return
1535 1535
1536 1536 1) the final mapping between original and created nodes
1537 1537 2) the list of temporary node created by histedit
1538 1538 3) the list of new commit created by histedit"""
1539 1539 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1540 1540 allsuccs = set()
1541 1541 replaced = set()
1542 1542 fullmapping = {}
1543 1543 # initialize basic set
1544 1544 # fullmapping records all operations recorded in replacement
1545 1545 for rep in replacements:
1546 1546 allsuccs.update(rep[1])
1547 1547 replaced.add(rep[0])
1548 1548 fullmapping.setdefault(rep[0], set()).update(rep[1])
1549 1549 new = allsuccs - replaced
1550 1550 tmpnodes = allsuccs & replaced
1551 1551 # Reduce content fullmapping into direct relation between original nodes
1552 1552 # and final node created during history edition
1553 1553 # Dropped changeset are replaced by an empty list
1554 1554 toproceed = set(fullmapping)
1555 1555 final = {}
1556 1556 while toproceed:
1557 1557 for x in list(toproceed):
1558 1558 succs = fullmapping[x]
1559 1559 for s in list(succs):
1560 1560 if s in toproceed:
1561 1561 # non final node with unknown closure
1562 1562 # We can't process this now
1563 1563 break
1564 1564 elif s in final:
1565 1565 # non final node, replace with closure
1566 1566 succs.remove(s)
1567 1567 succs.update(final[s])
1568 1568 else:
1569 1569 final[x] = succs
1570 1570 toproceed.remove(x)
1571 1571 # remove tmpnodes from final mapping
1572 1572 for n in tmpnodes:
1573 1573 del final[n]
1574 1574 # we expect all changes involved in final to exist in the repo
1575 1575 # turn `final` into list (topologically sorted)
1576 1576 nm = state.repo.changelog.nodemap
1577 1577 for prec, succs in final.items():
1578 1578 final[prec] = sorted(succs, key=nm.get)
1579 1579
1580 1580 # computed topmost element (necessary for bookmark)
1581 1581 if new:
1582 1582 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1583 1583 elif not final:
1584 1584 # Nothing rewritten at all. we won't need `newtopmost`
1585 1585 # It is the same as `oldtopmost` and `processreplacement` know it
1586 1586 newtopmost = None
1587 1587 else:
1588 1588 # every body died. The newtopmost is the parent of the root.
1589 1589 r = state.repo.changelog.rev
1590 1590 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1591 1591
1592 1592 return final, tmpnodes, new, newtopmost
1593 1593
1594 1594 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1595 1595 """Move bookmark from oldtopmost to newly created topmost
1596 1596
1597 1597 This is arguably a feature and we may only want that for the active
1598 1598 bookmark. But the behavior is kept compatible with the old version for now.
1599 1599 """
1600 1600 if not oldtopmost or not newtopmost:
1601 1601 return
1602 1602 oldbmarks = repo.nodebookmarks(oldtopmost)
1603 1603 if oldbmarks:
1604 1604 with repo.lock(), repo.transaction('histedit') as tr:
1605 1605 marks = repo._bookmarks
1606 1606 changes = []
1607 1607 for name in oldbmarks:
1608 1608 changes.append((name, newtopmost))
1609 1609 marks.applychanges(repo, tr, changes)
1610 1610
1611 1611 def cleanupnode(ui, repo, nodes):
1612 1612 """strip a group of nodes from the repository
1613 1613
1614 1614 The set of node to strip may contains unknown nodes."""
1615 1615 with repo.lock():
1616 1616 # do not let filtering get in the way of the cleanse
1617 1617 # we should probably get rid of obsolescence marker created during the
1618 1618 # histedit, but we currently do not have such information.
1619 1619 repo = repo.unfiltered()
1620 1620 # Find all nodes that need to be stripped
1621 1621 # (we use %lr instead of %ln to silently ignore unknown items)
1622 1622 nm = repo.changelog.nodemap
1623 1623 nodes = sorted(n for n in nodes if n in nm)
1624 1624 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1625 1625 if roots:
1626 1626 repair.strip(ui, repo, roots)
1627 1627
1628 1628 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1629 1629 if isinstance(nodelist, str):
1630 1630 nodelist = [nodelist]
1631 1631 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1632 1632 state = histeditstate(repo)
1633 1633 state.read()
1634 1634 histedit_nodes = {action.node for action
1635 1635 in state.actions if action.node}
1636 1636 common_nodes = histedit_nodes & set(nodelist)
1637 1637 if common_nodes:
1638 1638 raise error.Abort(_("histedit in progress, can't strip %s")
1639 1639 % ', '.join(node.short(x) for x in common_nodes))
1640 1640 return orig(ui, repo, nodelist, *args, **kwargs)
1641 1641
1642 1642 extensions.wrapfunction(repair, 'strip', stripwrapper)
1643 1643
1644 1644 def summaryhook(ui, repo):
1645 1645 if not os.path.exists(repo.vfs.join('histedit-state')):
1646 1646 return
1647 1647 state = histeditstate(repo)
1648 1648 state.read()
1649 1649 if state.actions:
1650 1650 # i18n: column positioning for "hg summary"
1651 1651 ui.write(_('hist: %s (histedit --continue)\n') %
1652 1652 (ui.label(_('%d remaining'), 'histedit.remaining') %
1653 1653 len(state.actions)))
1654 1654
1655 1655 def extsetup(ui):
1656 1656 cmdutil.summaryhooks.add('histedit', summaryhook)
1657 1657 cmdutil.unfinishedstates.append(
1658 1658 ['histedit-state', False, True, _('histedit in progress'),
1659 1659 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1660 1660 cmdutil.afterresolvedstates.append(
1661 1661 ['histedit-state', _('hg histedit --continue')])
@@ -1,1142 +1,1142 b''
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import shutil
15 15 import stat
16 16
17 17 from .i18n import _
18 18 from .node import (
19 19 nullid,
20 20 )
21 21
22 22 from . import (
23 23 bookmarks,
24 24 bundlerepo,
25 25 cacheutil,
26 26 cmdutil,
27 27 destutil,
28 28 discovery,
29 29 error,
30 30 exchange,
31 31 extensions,
32 32 httppeer,
33 33 localrepo,
34 34 lock,
35 35 logcmdutil,
36 36 logexchange,
37 37 merge as mergemod,
38 38 node,
39 39 phases,
40 40 scmutil,
41 41 sshpeer,
42 42 statichttprepo,
43 43 ui as uimod,
44 44 unionrepo,
45 45 url,
46 46 util,
47 47 verify as verifymod,
48 48 vfs as vfsmod,
49 49 )
50 50
51 51 from .utils import (
52 52 stringutil,
53 53 )
54 54
55 55 release = lock.release
56 56
57 57 # shared features
58 58 sharedbookmarks = 'bookmarks'
59 59
60 60 def _local(path):
61 61 path = util.expandpath(util.urllocalpath(path))
62 62 return (os.path.isfile(path) and bundlerepo or localrepo)
63 63
64 64 def addbranchrevs(lrepo, other, branches, revs):
65 65 peer = other.peer() # a courtesy to callers using a localrepo for other
66 66 hashbranch, branches = branches
67 67 if not hashbranch and not branches:
68 68 x = revs or None
69 69 if util.safehasattr(revs, 'first'):
70 70 y = revs.first()
71 71 elif revs:
72 72 y = revs[0]
73 73 else:
74 74 y = None
75 75 return x, y
76 76 if revs:
77 77 revs = list(revs)
78 78 else:
79 79 revs = []
80 80
81 81 if not peer.capable('branchmap'):
82 82 if branches:
83 83 raise error.Abort(_("remote branch lookup not supported"))
84 84 revs.append(hashbranch)
85 85 return revs, revs[0]
86 86 branchmap = peer.branchmap()
87 87
88 88 def primary(branch):
89 89 if branch == '.':
90 90 if not lrepo:
91 91 raise error.Abort(_("dirstate branch not accessible"))
92 92 branch = lrepo.dirstate.branch()
93 93 if branch in branchmap:
94 94 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
95 95 return True
96 96 else:
97 97 return False
98 98
99 99 for branch in branches:
100 100 if not primary(branch):
101 101 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
102 102 if hashbranch:
103 103 if not primary(hashbranch):
104 104 revs.append(hashbranch)
105 105 return revs, revs[0]
106 106
107 107 def parseurl(path, branches=None):
108 108 '''parse url#branch, returning (url, (branch, branches))'''
109 109
110 110 u = util.url(path)
111 111 branch = None
112 112 if u.fragment:
113 113 branch = u.fragment
114 114 u.fragment = None
115 115 return bytes(u), (branch, branches or [])
116 116
117 117 schemes = {
118 118 'bundle': bundlerepo,
119 119 'union': unionrepo,
120 120 'file': _local,
121 121 'http': httppeer,
122 122 'https': httppeer,
123 123 'ssh': sshpeer,
124 124 'static-http': statichttprepo,
125 125 }
126 126
127 127 def _peerlookup(path):
128 128 u = util.url(path)
129 129 scheme = u.scheme or 'file'
130 130 thing = schemes.get(scheme) or schemes['file']
131 131 try:
132 132 return thing(path)
133 133 except TypeError:
134 134 # we can't test callable(thing) because 'thing' can be an unloaded
135 135 # module that implements __call__
136 136 if not util.safehasattr(thing, 'instance'):
137 137 raise
138 138 return thing
139 139
140 140 def islocal(repo):
141 141 '''return true if repo (or path pointing to repo) is local'''
142 142 if isinstance(repo, bytes):
143 143 try:
144 144 return _peerlookup(repo).islocal(repo)
145 145 except AttributeError:
146 146 return False
147 147 return repo.local()
148 148
149 149 def openpath(ui, path):
150 150 '''open path with open if local, url.open if remote'''
151 151 pathurl = util.url(path, parsequery=False, parsefragment=False)
152 152 if pathurl.islocal():
153 153 return util.posixfile(pathurl.localpath(), 'rb')
154 154 else:
155 155 return url.open(ui, path)
156 156
157 157 # a list of (ui, repo) functions called for wire peer initialization
158 158 wirepeersetupfuncs = []
159 159
160 160 def _peerorrepo(ui, path, create=False, presetupfuncs=None):
161 161 """return a repository object for the specified path"""
162 162 obj = _peerlookup(path).instance(ui, path, create)
163 163 ui = getattr(obj, "ui", ui)
164 164 for f in presetupfuncs or []:
165 165 f(ui, obj)
166 166 for name, module in extensions.extensions(ui):
167 167 hook = getattr(module, 'reposetup', None)
168 168 if hook:
169 169 hook(ui, obj)
170 170 if not obj.local():
171 171 for f in wirepeersetupfuncs:
172 172 f(ui, obj)
173 173 return obj
174 174
175 175 def repository(ui, path='', create=False, presetupfuncs=None):
176 176 """return a repository object for the specified path"""
177 177 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs)
178 178 repo = peer.local()
179 179 if not repo:
180 180 raise error.Abort(_("repository '%s' is not local") %
181 181 (path or peer.url()))
182 182 return repo.filtered('visible')
183 183
184 184 def peer(uiorrepo, opts, path, create=False):
185 185 '''return a repository peer for the specified path'''
186 186 rui = remoteui(uiorrepo, opts)
187 187 return _peerorrepo(rui, path, create).peer()
188 188
189 189 def defaultdest(source):
190 190 '''return default destination of clone if none is given
191 191
192 192 >>> defaultdest(b'foo')
193 193 'foo'
194 194 >>> defaultdest(b'/foo/bar')
195 195 'bar'
196 196 >>> defaultdest(b'/')
197 197 ''
198 198 >>> defaultdest(b'')
199 199 ''
200 200 >>> defaultdest(b'http://example.org/')
201 201 ''
202 202 >>> defaultdest(b'http://example.org/foo/')
203 203 'foo'
204 204 '''
205 205 path = util.url(source).path
206 206 if not path:
207 207 return ''
208 208 return os.path.basename(os.path.normpath(path))
209 209
210 210 def sharedreposource(repo):
211 211 """Returns repository object for source repository of a shared repo.
212 212
213 213 If repo is not a shared repository, returns None.
214 214 """
215 215 if repo.sharedpath == repo.path:
216 216 return None
217 217
218 218 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
219 219 return repo.srcrepo
220 220
221 221 # the sharedpath always ends in the .hg; we want the path to the repo
222 222 source = repo.vfs.split(repo.sharedpath)[0]
223 223 srcurl, branches = parseurl(source)
224 224 srcrepo = repository(repo.ui, srcurl)
225 225 repo.srcrepo = srcrepo
226 226 return srcrepo
227 227
228 228 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
229 229 relative=False):
230 230 '''create a shared repository'''
231 231
232 232 if not islocal(source):
233 233 raise error.Abort(_('can only share local repositories'))
234 234
235 235 if not dest:
236 236 dest = defaultdest(source)
237 237 else:
238 238 dest = ui.expandpath(dest)
239 239
240 240 if isinstance(source, bytes):
241 241 origsource = ui.expandpath(source)
242 242 source, branches = parseurl(origsource)
243 243 srcrepo = repository(ui, source)
244 244 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
245 245 else:
246 246 srcrepo = source.local()
247 247 origsource = source = srcrepo.url()
248 248 checkout = None
249 249
250 250 sharedpath = srcrepo.sharedpath # if our source is already sharing
251 251
252 252 destwvfs = vfsmod.vfs(dest, realpath=True)
253 253 destvfs = vfsmod.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
254 254
255 255 if destvfs.lexists():
256 256 raise error.Abort(_('destination already exists'))
257 257
258 258 if not destwvfs.isdir():
259 259 destwvfs.mkdir()
260 260 destvfs.makedir()
261 261
262 262 requirements = ''
263 263 try:
264 264 requirements = srcrepo.vfs.read('requires')
265 265 except IOError as inst:
266 266 if inst.errno != errno.ENOENT:
267 267 raise
268 268
269 269 if relative:
270 270 try:
271 271 sharedpath = os.path.relpath(sharedpath, destvfs.base)
272 272 requirements += 'relshared\n'
273 273 except (IOError, ValueError) as e:
274 274 # ValueError is raised on Windows if the drive letters differ on
275 275 # each path
276 276 raise error.Abort(_('cannot calculate relative path'),
277 277 hint=stringutil.forcebytestr(e))
278 278 else:
279 279 requirements += 'shared\n'
280 280
281 281 destvfs.write('requires', requirements)
282 282 destvfs.write('sharedpath', sharedpath)
283 283
284 284 r = repository(ui, destwvfs.base)
285 285 postshare(srcrepo, r, bookmarks=bookmarks, defaultpath=defaultpath)
286 286 _postshareupdate(r, update, checkout=checkout)
287 287 return r
288 288
289 289 def unshare(ui, repo):
290 290 """convert a shared repository to a normal one
291 291
292 292 Copy the store data to the repo and remove the sharedpath data.
293 293 """
294 294
295 295 destlock = lock = None
296 296 lock = repo.lock()
297 297 try:
298 298 # we use locks here because if we race with commit, we
299 299 # can end up with extra data in the cloned revlogs that's
300 300 # not pointed to by changesets, thus causing verify to
301 301 # fail
302 302
303 303 destlock = copystore(ui, repo, repo.path)
304 304
305 305 sharefile = repo.vfs.join('sharedpath')
306 306 util.rename(sharefile, sharefile + '.old')
307 307
308 308 repo.requirements.discard('shared')
309 309 repo.requirements.discard('relshared')
310 310 repo._writerequirements()
311 311 finally:
312 312 destlock and destlock.release()
313 313 lock and lock.release()
314 314
315 315 # update store, spath, svfs and sjoin of repo
316 316 repo.unfiltered().__init__(repo.baseui, repo.root)
317 317
318 318 # TODO: figure out how to access subrepos that exist, but were previously
319 319 # removed from .hgsub
320 320 c = repo['.']
321 321 subs = c.substate
322 322 for s in sorted(subs):
323 323 c.sub(s).unshare()
324 324
325 325 def postshare(sourcerepo, destrepo, bookmarks=True, defaultpath=None):
326 326 """Called after a new shared repo is created.
327 327
328 328 The new repo only has a requirements file and pointer to the source.
329 329 This function configures additional shared data.
330 330
331 331 Extensions can wrap this function and write additional entries to
332 332 destrepo/.hg/shared to indicate additional pieces of data to be shared.
333 333 """
334 334 default = defaultpath or sourcerepo.ui.config('paths', 'default')
335 335 if default:
336 336 template = ('[paths]\n'
337 337 'default = %s\n')
338 338 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
339 339
340 340 with destrepo.wlock():
341 341 if bookmarks:
342 342 destrepo.vfs.write('shared', sharedbookmarks + '\n')
343 343
344 344 def _postshareupdate(repo, update, checkout=None):
345 345 """Maybe perform a working directory update after a shared repo is created.
346 346
347 347 ``update`` can be a boolean or a revision to update to.
348 348 """
349 349 if not update:
350 350 return
351 351
352 352 repo.ui.status(_("updating working directory\n"))
353 353 if update is not True:
354 354 checkout = update
355 355 for test in (checkout, 'default', 'tip'):
356 356 if test is None:
357 357 continue
358 358 try:
359 359 uprev = repo.lookup(test)
360 360 break
361 361 except error.RepoLookupError:
362 362 continue
363 363 _update(repo, uprev)
364 364
365 365 def copystore(ui, srcrepo, destpath):
366 366 '''copy files from store of srcrepo in destpath
367 367
368 368 returns destlock
369 369 '''
370 370 destlock = None
371 371 try:
372 372 hardlink = None
373 373 num = 0
374 374 closetopic = [None]
375 375 def prog(topic, pos):
376 376 if pos is None:
377 377 closetopic[0] = topic
378 378 else:
379 379 ui.progress(topic, pos + num)
380 380 srcpublishing = srcrepo.publishing()
381 381 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
382 382 dstvfs = vfsmod.vfs(destpath)
383 383 for f in srcrepo.store.copylist():
384 384 if srcpublishing and f.endswith('phaseroots'):
385 385 continue
386 386 dstbase = os.path.dirname(f)
387 387 if dstbase and not dstvfs.exists(dstbase):
388 388 dstvfs.mkdir(dstbase)
389 389 if srcvfs.exists(f):
390 390 if f.endswith('data'):
391 391 # 'dstbase' may be empty (e.g. revlog format 0)
392 392 lockfile = os.path.join(dstbase, "lock")
393 393 # lock to avoid premature writing to the target
394 394 destlock = lock.lock(dstvfs, lockfile)
395 395 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
396 396 hardlink, progress=prog)
397 397 num += n
398 398 if hardlink:
399 399 ui.debug("linked %d files\n" % num)
400 400 if closetopic[0]:
401 401 ui.progress(closetopic[0], None)
402 402 else:
403 403 ui.debug("copied %d files\n" % num)
404 404 if closetopic[0]:
405 405 ui.progress(closetopic[0], None)
406 406 return destlock
407 407 except: # re-raises
408 408 release(destlock)
409 409 raise
410 410
411 411 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
412 412 rev=None, update=True, stream=False):
413 413 """Perform a clone using a shared repo.
414 414
415 415 The store for the repository will be located at <sharepath>/.hg. The
416 416 specified revisions will be cloned or pulled from "source". A shared repo
417 417 will be created at "dest" and a working copy will be created if "update" is
418 418 True.
419 419 """
420 420 revs = None
421 421 if rev:
422 422 if not srcpeer.capable('lookup'):
423 423 raise error.Abort(_("src repository does not support "
424 424 "revision lookup and so doesn't "
425 425 "support clone by revision"))
426 426 revs = [srcpeer.lookup(r) for r in rev]
427 427
428 428 # Obtain a lock before checking for or cloning the pooled repo otherwise
429 429 # 2 clients may race creating or populating it.
430 430 pooldir = os.path.dirname(sharepath)
431 431 # lock class requires the directory to exist.
432 432 try:
433 433 util.makedir(pooldir, False)
434 434 except OSError as e:
435 435 if e.errno != errno.EEXIST:
436 436 raise
437 437
438 438 poolvfs = vfsmod.vfs(pooldir)
439 439 basename = os.path.basename(sharepath)
440 440
441 441 with lock.lock(poolvfs, '%s.lock' % basename):
442 442 if os.path.exists(sharepath):
443 443 ui.status(_('(sharing from existing pooled repository %s)\n') %
444 444 basename)
445 445 else:
446 446 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
447 447 # Always use pull mode because hardlinks in share mode don't work
448 448 # well. Never update because working copies aren't necessary in
449 449 # share mode.
450 450 clone(ui, peeropts, source, dest=sharepath, pull=True,
451 451 rev=rev, update=False, stream=stream)
452 452
453 453 # Resolve the value to put in [paths] section for the source.
454 454 if islocal(source):
455 455 defaultpath = os.path.abspath(util.urllocalpath(source))
456 456 else:
457 457 defaultpath = source
458 458
459 459 sharerepo = repository(ui, path=sharepath)
460 460 share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
461 461 defaultpath=defaultpath)
462 462
463 463 # We need to perform a pull against the dest repo to fetch bookmarks
464 464 # and other non-store data that isn't shared by default. In the case of
465 465 # non-existing shared repo, this means we pull from the remote twice. This
466 466 # is a bit weird. But at the time it was implemented, there wasn't an easy
467 467 # way to pull just non-changegroup data.
468 468 destrepo = repository(ui, path=dest)
469 469 exchange.pull(destrepo, srcpeer, heads=revs)
470 470
471 471 _postshareupdate(destrepo, update)
472 472
473 473 return srcpeer, peer(ui, peeropts, dest)
474 474
475 475 # Recomputing branch cache might be slow on big repos,
476 476 # so just copy it
477 477 def _copycache(srcrepo, dstcachedir, fname):
478 478 """copy a cache from srcrepo to destcachedir (if it exists)"""
479 479 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
480 480 dstbranchcache = os.path.join(dstcachedir, fname)
481 481 if os.path.exists(srcbranchcache):
482 482 if not os.path.exists(dstcachedir):
483 483 os.mkdir(dstcachedir)
484 484 util.copyfile(srcbranchcache, dstbranchcache)
485 485
486 486 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
487 487 update=True, stream=False, branch=None, shareopts=None):
488 488 """Make a copy of an existing repository.
489 489
490 490 Create a copy of an existing repository in a new directory. The
491 491 source and destination are URLs, as passed to the repository
492 492 function. Returns a pair of repository peers, the source and
493 493 newly created destination.
494 494
495 495 The location of the source is added to the new repository's
496 496 .hg/hgrc file, as the default to be used for future pulls and
497 497 pushes.
498 498
499 499 If an exception is raised, the partly cloned/updated destination
500 500 repository will be deleted.
501 501
502 502 Arguments:
503 503
504 504 source: repository object or URL
505 505
506 506 dest: URL of destination repository to create (defaults to base
507 507 name of source repository)
508 508
509 509 pull: always pull from source repository, even in local case or if the
510 510 server prefers streaming
511 511
512 512 stream: stream raw data uncompressed from repository (fast over
513 513 LAN, slow over WAN)
514 514
515 515 rev: revision to clone up to (implies pull=True)
516 516
517 517 update: update working directory after clone completes, if
518 518 destination is local repository (True means update to default rev,
519 519 anything else is treated as a revision)
520 520
521 521 branch: branches to clone
522 522
523 523 shareopts: dict of options to control auto sharing behavior. The "pool" key
524 524 activates auto sharing mode and defines the directory for stores. The
525 525 "mode" key determines how to construct the directory name of the shared
526 526 repository. "identity" means the name is derived from the node of the first
527 527 changeset in the repository. "remote" means the name is derived from the
528 528 remote's path/URL. Defaults to "identity."
529 529 """
530 530
531 531 if isinstance(source, bytes):
532 532 origsource = ui.expandpath(source)
533 source, branch = parseurl(origsource, branch)
533 source, branches = parseurl(origsource, branch)
534 534 srcpeer = peer(ui, peeropts, source)
535 535 else:
536 536 srcpeer = source.peer() # in case we were called with a localrepo
537 branch = (None, branch or [])
537 branches = (None, branch or [])
538 538 origsource = source = srcpeer.url()
539 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
539 rev, checkout = addbranchrevs(srcpeer, srcpeer, branches, rev)
540 540
541 541 if dest is None:
542 542 dest = defaultdest(source)
543 543 if dest:
544 544 ui.status(_("destination directory: %s\n") % dest)
545 545 else:
546 546 dest = ui.expandpath(dest)
547 547
548 548 dest = util.urllocalpath(dest)
549 549 source = util.urllocalpath(source)
550 550
551 551 if not dest:
552 552 raise error.Abort(_("empty destination path is not valid"))
553 553
554 554 destvfs = vfsmod.vfs(dest, expandpath=True)
555 555 if destvfs.lexists():
556 556 if not destvfs.isdir():
557 557 raise error.Abort(_("destination '%s' already exists") % dest)
558 558 elif destvfs.listdir():
559 559 raise error.Abort(_("destination '%s' is not empty") % dest)
560 560
561 561 shareopts = shareopts or {}
562 562 sharepool = shareopts.get('pool')
563 563 sharenamemode = shareopts.get('mode')
564 564 if sharepool and islocal(dest):
565 565 sharepath = None
566 566 if sharenamemode == 'identity':
567 567 # Resolve the name from the initial changeset in the remote
568 568 # repository. This returns nullid when the remote is empty. It
569 569 # raises RepoLookupError if revision 0 is filtered or otherwise
570 570 # not available. If we fail to resolve, sharing is not enabled.
571 571 try:
572 572 rootnode = srcpeer.lookup('0')
573 573 if rootnode != node.nullid:
574 574 sharepath = os.path.join(sharepool, node.hex(rootnode))
575 575 else:
576 576 ui.status(_('(not using pooled storage: '
577 577 'remote appears to be empty)\n'))
578 578 except error.RepoLookupError:
579 579 ui.status(_('(not using pooled storage: '
580 580 'unable to resolve identity of remote)\n'))
581 581 elif sharenamemode == 'remote':
582 582 sharepath = os.path.join(
583 583 sharepool, node.hex(hashlib.sha1(source).digest()))
584 584 else:
585 585 raise error.Abort(_('unknown share naming mode: %s') %
586 586 sharenamemode)
587 587
588 588 if sharepath:
589 589 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
590 590 dest, pull=pull, rev=rev, update=update,
591 591 stream=stream)
592 592
593 593 srclock = destlock = cleandir = None
594 594 srcrepo = srcpeer.local()
595 595 try:
596 596 abspath = origsource
597 597 if islocal(origsource):
598 598 abspath = os.path.abspath(util.urllocalpath(origsource))
599 599
600 600 if islocal(dest):
601 601 cleandir = dest
602 602
603 603 copy = False
604 604 if (srcrepo and srcrepo.cancopy() and islocal(dest)
605 605 and not phases.hassecret(srcrepo)):
606 606 copy = not pull and not rev
607 607
608 608 if copy:
609 609 try:
610 610 # we use a lock here because if we race with commit, we
611 611 # can end up with extra data in the cloned revlogs that's
612 612 # not pointed to by changesets, thus causing verify to
613 613 # fail
614 614 srclock = srcrepo.lock(wait=False)
615 615 except error.LockError:
616 616 copy = False
617 617
618 618 if copy:
619 619 srcrepo.hook('preoutgoing', throw=True, source='clone')
620 620 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
621 621 if not os.path.exists(dest):
622 622 os.mkdir(dest)
623 623 else:
624 624 # only clean up directories we create ourselves
625 625 cleandir = hgdir
626 626 try:
627 627 destpath = hgdir
628 628 util.makedir(destpath, notindexed=True)
629 629 except OSError as inst:
630 630 if inst.errno == errno.EEXIST:
631 631 cleandir = None
632 632 raise error.Abort(_("destination '%s' already exists")
633 633 % dest)
634 634 raise
635 635
636 636 destlock = copystore(ui, srcrepo, destpath)
637 637 # copy bookmarks over
638 638 srcbookmarks = srcrepo.vfs.join('bookmarks')
639 639 dstbookmarks = os.path.join(destpath, 'bookmarks')
640 640 if os.path.exists(srcbookmarks):
641 641 util.copyfile(srcbookmarks, dstbookmarks)
642 642
643 643 dstcachedir = os.path.join(destpath, 'cache')
644 644 for cache in cacheutil.cachetocopy(srcrepo):
645 645 _copycache(srcrepo, dstcachedir, cache)
646 646
647 647 # we need to re-init the repo after manually copying the data
648 648 # into it
649 649 destpeer = peer(srcrepo, peeropts, dest)
650 650 srcrepo.hook('outgoing', source='clone',
651 651 node=node.hex(node.nullid))
652 652 else:
653 653 try:
654 654 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
655 655 # only pass ui when no srcrepo
656 656 except OSError as inst:
657 657 if inst.errno == errno.EEXIST:
658 658 cleandir = None
659 659 raise error.Abort(_("destination '%s' already exists")
660 660 % dest)
661 661 raise
662 662
663 663 revs = None
664 664 if rev:
665 665 if not srcpeer.capable('lookup'):
666 666 raise error.Abort(_("src repository does not support "
667 667 "revision lookup and so doesn't "
668 668 "support clone by revision"))
669 669 revs = [srcpeer.lookup(r) for r in rev]
670 670 checkout = revs[0]
671 671 local = destpeer.local()
672 672 if local:
673 673 u = util.url(abspath)
674 674 defaulturl = bytes(u)
675 675 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
676 676 if not stream:
677 677 if pull:
678 678 stream = False
679 679 else:
680 680 stream = None
681 681 # internal config: ui.quietbookmarkmove
682 682 overrides = {('ui', 'quietbookmarkmove'): True}
683 683 with local.ui.configoverride(overrides, 'clone'):
684 684 exchange.pull(local, srcpeer, revs,
685 685 streamclonerequested=stream)
686 686 elif srcrepo:
687 687 exchange.push(srcrepo, destpeer, revs=revs,
688 688 bookmarks=srcrepo._bookmarks.keys())
689 689 else:
690 690 raise error.Abort(_("clone from remote to remote not supported")
691 691 )
692 692
693 693 cleandir = None
694 694
695 695 destrepo = destpeer.local()
696 696 if destrepo:
697 697 template = uimod.samplehgrcs['cloned']
698 698 u = util.url(abspath)
699 699 u.passwd = None
700 700 defaulturl = bytes(u)
701 701 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
702 702 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
703 703
704 704 if ui.configbool('experimental', 'remotenames'):
705 705 logexchange.pullremotenames(destrepo, srcpeer)
706 706
707 707 if update:
708 708 if update is not True:
709 709 checkout = srcpeer.lookup(update)
710 710 uprev = None
711 711 status = None
712 712 if checkout is not None:
713 713 try:
714 714 uprev = destrepo.lookup(checkout)
715 715 except error.RepoLookupError:
716 716 if update is not True:
717 717 try:
718 718 uprev = destrepo.lookup(update)
719 719 except error.RepoLookupError:
720 720 pass
721 721 if uprev is None:
722 722 try:
723 723 uprev = destrepo._bookmarks['@']
724 724 update = '@'
725 725 bn = destrepo[uprev].branch()
726 726 if bn == 'default':
727 727 status = _("updating to bookmark @\n")
728 728 else:
729 729 status = (_("updating to bookmark @ on branch %s\n")
730 730 % bn)
731 731 except KeyError:
732 732 try:
733 733 uprev = destrepo.branchtip('default')
734 734 except error.RepoLookupError:
735 735 uprev = destrepo.lookup('tip')
736 736 if not status:
737 737 bn = destrepo[uprev].branch()
738 738 status = _("updating to branch %s\n") % bn
739 739 destrepo.ui.status(status)
740 740 _update(destrepo, uprev)
741 741 if update in destrepo._bookmarks:
742 742 bookmarks.activate(destrepo, update)
743 743 finally:
744 744 release(srclock, destlock)
745 745 if cleandir is not None:
746 746 shutil.rmtree(cleandir, True)
747 747 if srcpeer is not None:
748 748 srcpeer.close()
749 749 return srcpeer, destpeer
750 750
751 751 def _showstats(repo, stats, quietempty=False):
752 752 if quietempty and stats.isempty():
753 753 return
754 754 repo.ui.status(_("%d files updated, %d files merged, "
755 755 "%d files removed, %d files unresolved\n") % (
756 756 stats.updatedcount, stats.mergedcount,
757 757 stats.removedcount, stats.unresolvedcount))
758 758
759 759 def updaterepo(repo, node, overwrite, updatecheck=None):
760 760 """Update the working directory to node.
761 761
762 762 When overwrite is set, changes are clobbered, merged else
763 763
764 764 returns stats (see pydoc mercurial.merge.applyupdates)"""
765 765 return mergemod.update(repo, node, False, overwrite,
766 766 labels=['working copy', 'destination'],
767 767 updatecheck=updatecheck)
768 768
769 769 def update(repo, node, quietempty=False, updatecheck=None):
770 770 """update the working directory to node"""
771 771 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
772 772 _showstats(repo, stats, quietempty)
773 773 if stats.unresolvedcount:
774 774 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
775 775 return stats.unresolvedcount > 0
776 776
777 777 # naming conflict in clone()
778 778 _update = update
779 779
780 780 def clean(repo, node, show_stats=True, quietempty=False):
781 781 """forcibly switch the working directory to node, clobbering changes"""
782 782 stats = updaterepo(repo, node, True)
783 783 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
784 784 if show_stats:
785 785 _showstats(repo, stats, quietempty)
786 786 return stats.unresolvedcount > 0
787 787
788 788 # naming conflict in updatetotally()
789 789 _clean = clean
790 790
791 791 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
792 792 """Update the working directory with extra care for non-file components
793 793
794 794 This takes care of non-file components below:
795 795
796 796 :bookmark: might be advanced or (in)activated
797 797
798 798 This takes arguments below:
799 799
800 800 :checkout: to which revision the working directory is updated
801 801 :brev: a name, which might be a bookmark to be activated after updating
802 802 :clean: whether changes in the working directory can be discarded
803 803 :updatecheck: how to deal with a dirty working directory
804 804
805 805 Valid values for updatecheck are (None => linear):
806 806
807 807 * abort: abort if the working directory is dirty
808 808 * none: don't check (merge working directory changes into destination)
809 809 * linear: check that update is linear before merging working directory
810 810 changes into destination
811 811 * noconflict: check that the update does not result in file merges
812 812
813 813 This returns whether conflict is detected at updating or not.
814 814 """
815 815 if updatecheck is None:
816 816 updatecheck = ui.config('commands', 'update.check')
817 817 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
818 818 # If not configured, or invalid value configured
819 819 updatecheck = 'linear'
820 820 with repo.wlock():
821 821 movemarkfrom = None
822 822 warndest = False
823 823 if checkout is None:
824 824 updata = destutil.destupdate(repo, clean=clean)
825 825 checkout, movemarkfrom, brev = updata
826 826 warndest = True
827 827
828 828 if clean:
829 829 ret = _clean(repo, checkout)
830 830 else:
831 831 if updatecheck == 'abort':
832 832 cmdutil.bailifchanged(repo, merge=False)
833 833 updatecheck = 'none'
834 834 ret = _update(repo, checkout, updatecheck=updatecheck)
835 835
836 836 if not ret and movemarkfrom:
837 837 if movemarkfrom == repo['.'].node():
838 838 pass # no-op update
839 839 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
840 840 b = ui.label(repo._activebookmark, 'bookmarks.active')
841 841 ui.status(_("updating bookmark %s\n") % b)
842 842 else:
843 843 # this can happen with a non-linear update
844 844 b = ui.label(repo._activebookmark, 'bookmarks')
845 845 ui.status(_("(leaving bookmark %s)\n") % b)
846 846 bookmarks.deactivate(repo)
847 847 elif brev in repo._bookmarks:
848 848 if brev != repo._activebookmark:
849 849 b = ui.label(brev, 'bookmarks.active')
850 850 ui.status(_("(activating bookmark %s)\n") % b)
851 851 bookmarks.activate(repo, brev)
852 852 elif brev:
853 853 if repo._activebookmark:
854 854 b = ui.label(repo._activebookmark, 'bookmarks')
855 855 ui.status(_("(leaving bookmark %s)\n") % b)
856 856 bookmarks.deactivate(repo)
857 857
858 858 if warndest:
859 859 destutil.statusotherdests(ui, repo)
860 860
861 861 return ret
862 862
863 863 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
864 864 abort=False):
865 865 """Branch merge with node, resolving changes. Return true if any
866 866 unresolved conflicts."""
867 867 if not abort:
868 868 stats = mergemod.update(repo, node, True, force, mergeforce=mergeforce,
869 869 labels=labels)
870 870 else:
871 871 ms = mergemod.mergestate.read(repo)
872 872 if ms.active():
873 873 # there were conflicts
874 874 node = ms.localctx.hex()
875 875 else:
876 876 # there were no conficts, mergestate was not stored
877 877 node = repo['.'].hex()
878 878
879 879 repo.ui.status(_("aborting the merge, updating back to"
880 880 " %s\n") % node[:12])
881 881 stats = mergemod.update(repo, node, branchmerge=False, force=True,
882 882 labels=labels)
883 883
884 884 _showstats(repo, stats)
885 885 if stats.unresolvedcount:
886 886 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
887 887 "or 'hg merge --abort' to abandon\n"))
888 888 elif remind and not abort:
889 889 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
890 890 return stats.unresolvedcount > 0
891 891
892 892 def _incoming(displaychlist, subreporecurse, ui, repo, source,
893 893 opts, buffered=False):
894 894 """
895 895 Helper for incoming / gincoming.
896 896 displaychlist gets called with
897 897 (remoterepo, incomingchangesetlist, displayer) parameters,
898 898 and is supposed to contain only code that can't be unified.
899 899 """
900 900 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
901 901 other = peer(repo, opts, source)
902 902 ui.status(_('comparing with %s\n') % util.hidepassword(source))
903 903 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
904 904
905 905 if revs:
906 906 revs = [other.lookup(rev) for rev in revs]
907 907 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
908 908 revs, opts["bundle"], opts["force"])
909 909 try:
910 910 if not chlist:
911 911 ui.status(_("no changes found\n"))
912 912 return subreporecurse()
913 913 ui.pager('incoming')
914 914 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
915 915 buffered=buffered)
916 916 displaychlist(other, chlist, displayer)
917 917 displayer.close()
918 918 finally:
919 919 cleanupfn()
920 920 subreporecurse()
921 921 return 0 # exit code is zero since we found incoming changes
922 922
923 923 def incoming(ui, repo, source, opts):
924 924 def subreporecurse():
925 925 ret = 1
926 926 if opts.get('subrepos'):
927 927 ctx = repo[None]
928 928 for subpath in sorted(ctx.substate):
929 929 sub = ctx.sub(subpath)
930 930 ret = min(ret, sub.incoming(ui, source, opts))
931 931 return ret
932 932
933 933 def display(other, chlist, displayer):
934 934 limit = logcmdutil.getlimit(opts)
935 935 if opts.get('newest_first'):
936 936 chlist.reverse()
937 937 count = 0
938 938 for n in chlist:
939 939 if limit is not None and count >= limit:
940 940 break
941 941 parents = [p for p in other.changelog.parents(n) if p != nullid]
942 942 if opts.get('no_merges') and len(parents) == 2:
943 943 continue
944 944 count += 1
945 945 displayer.show(other[n])
946 946 return _incoming(display, subreporecurse, ui, repo, source, opts)
947 947
948 948 def _outgoing(ui, repo, dest, opts):
949 949 path = ui.paths.getpath(dest, default=('default-push', 'default'))
950 950 if not path:
951 951 raise error.Abort(_('default repository not configured!'),
952 952 hint=_("see 'hg help config.paths'"))
953 953 dest = path.pushloc or path.loc
954 954 branches = path.branch, opts.get('branch') or []
955 955
956 956 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
957 957 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
958 958 if revs:
959 959 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
960 960
961 961 other = peer(repo, opts, dest)
962 962 outgoing = discovery.findcommonoutgoing(repo, other, revs,
963 963 force=opts.get('force'))
964 964 o = outgoing.missing
965 965 if not o:
966 966 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
967 967 return o, other
968 968
969 969 def outgoing(ui, repo, dest, opts):
970 970 def recurse():
971 971 ret = 1
972 972 if opts.get('subrepos'):
973 973 ctx = repo[None]
974 974 for subpath in sorted(ctx.substate):
975 975 sub = ctx.sub(subpath)
976 976 ret = min(ret, sub.outgoing(ui, dest, opts))
977 977 return ret
978 978
979 979 limit = logcmdutil.getlimit(opts)
980 980 o, other = _outgoing(ui, repo, dest, opts)
981 981 if not o:
982 982 cmdutil.outgoinghooks(ui, repo, other, opts, o)
983 983 return recurse()
984 984
985 985 if opts.get('newest_first'):
986 986 o.reverse()
987 987 ui.pager('outgoing')
988 988 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
989 989 count = 0
990 990 for n in o:
991 991 if limit is not None and count >= limit:
992 992 break
993 993 parents = [p for p in repo.changelog.parents(n) if p != nullid]
994 994 if opts.get('no_merges') and len(parents) == 2:
995 995 continue
996 996 count += 1
997 997 displayer.show(repo[n])
998 998 displayer.close()
999 999 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1000 1000 recurse()
1001 1001 return 0 # exit code is zero since we found outgoing changes
1002 1002
1003 1003 def verify(repo):
1004 1004 """verify the consistency of a repository"""
1005 1005 ret = verifymod.verify(repo)
1006 1006
1007 1007 # Broken subrepo references in hidden csets don't seem worth worrying about,
1008 1008 # since they can't be pushed/pulled, and --hidden can be used if they are a
1009 1009 # concern.
1010 1010
1011 1011 # pathto() is needed for -R case
1012 1012 revs = repo.revs("filelog(%s)",
1013 1013 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1014 1014
1015 1015 if revs:
1016 1016 repo.ui.status(_('checking subrepo links\n'))
1017 1017 for rev in revs:
1018 1018 ctx = repo[rev]
1019 1019 try:
1020 1020 for subpath in ctx.substate:
1021 1021 try:
1022 1022 ret = (ctx.sub(subpath, allowcreate=False).verify()
1023 1023 or ret)
1024 1024 except error.RepoError as e:
1025 1025 repo.ui.warn(('%s: %s\n') % (rev, e))
1026 1026 except Exception:
1027 1027 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1028 1028 node.short(ctx.node()))
1029 1029
1030 1030 return ret
1031 1031
1032 1032 def remoteui(src, opts):
1033 1033 'build a remote ui from ui or repo and opts'
1034 1034 if util.safehasattr(src, 'baseui'): # looks like a repository
1035 1035 dst = src.baseui.copy() # drop repo-specific config
1036 1036 src = src.ui # copy target options from repo
1037 1037 else: # assume it's a global ui object
1038 1038 dst = src.copy() # keep all global options
1039 1039
1040 1040 # copy ssh-specific options
1041 1041 for o in 'ssh', 'remotecmd':
1042 1042 v = opts.get(o) or src.config('ui', o)
1043 1043 if v:
1044 1044 dst.setconfig("ui", o, v, 'copied')
1045 1045
1046 1046 # copy bundle-specific options
1047 1047 r = src.config('bundle', 'mainreporoot')
1048 1048 if r:
1049 1049 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1050 1050
1051 1051 # copy selected local settings to the remote ui
1052 1052 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1053 1053 for key, val in src.configitems(sect):
1054 1054 dst.setconfig(sect, key, val, 'copied')
1055 1055 v = src.config('web', 'cacerts')
1056 1056 if v:
1057 1057 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1058 1058
1059 1059 return dst
1060 1060
1061 1061 # Files of interest
1062 1062 # Used to check if the repository has changed looking at mtime and size of
1063 1063 # these files.
1064 1064 foi = [('spath', '00changelog.i'),
1065 1065 ('spath', 'phaseroots'), # ! phase can change content at the same size
1066 1066 ('spath', 'obsstore'),
1067 1067 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1068 1068 ]
1069 1069
1070 1070 class cachedlocalrepo(object):
1071 1071 """Holds a localrepository that can be cached and reused."""
1072 1072
1073 1073 def __init__(self, repo):
1074 1074 """Create a new cached repo from an existing repo.
1075 1075
1076 1076 We assume the passed in repo was recently created. If the
1077 1077 repo has changed between when it was created and when it was
1078 1078 turned into a cache, it may not refresh properly.
1079 1079 """
1080 1080 assert isinstance(repo, localrepo.localrepository)
1081 1081 self._repo = repo
1082 1082 self._state, self.mtime = self._repostate()
1083 1083 self._filtername = repo.filtername
1084 1084
1085 1085 def fetch(self):
1086 1086 """Refresh (if necessary) and return a repository.
1087 1087
1088 1088 If the cached instance is out of date, it will be recreated
1089 1089 automatically and returned.
1090 1090
1091 1091 Returns a tuple of the repo and a boolean indicating whether a new
1092 1092 repo instance was created.
1093 1093 """
1094 1094 # We compare the mtimes and sizes of some well-known files to
1095 1095 # determine if the repo changed. This is not precise, as mtimes
1096 1096 # are susceptible to clock skew and imprecise filesystems and
1097 1097 # file content can change while maintaining the same size.
1098 1098
1099 1099 state, mtime = self._repostate()
1100 1100 if state == self._state:
1101 1101 return self._repo, False
1102 1102
1103 1103 repo = repository(self._repo.baseui, self._repo.url())
1104 1104 if self._filtername:
1105 1105 self._repo = repo.filtered(self._filtername)
1106 1106 else:
1107 1107 self._repo = repo.unfiltered()
1108 1108 self._state = state
1109 1109 self.mtime = mtime
1110 1110
1111 1111 return self._repo, True
1112 1112
1113 1113 def _repostate(self):
1114 1114 state = []
1115 1115 maxmtime = -1
1116 1116 for attr, fname in foi:
1117 1117 prefix = getattr(self._repo, attr)
1118 1118 p = os.path.join(prefix, fname)
1119 1119 try:
1120 1120 st = os.stat(p)
1121 1121 except OSError:
1122 1122 st = os.stat(prefix)
1123 1123 state.append((st[stat.ST_MTIME], st.st_size))
1124 1124 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1125 1125
1126 1126 return tuple(state), maxmtime
1127 1127
1128 1128 def copy(self):
1129 1129 """Obtain a copy of this class instance.
1130 1130
1131 1131 A new localrepository instance is obtained. The new instance should be
1132 1132 completely independent of the original.
1133 1133 """
1134 1134 repo = repository(self._repo.baseui, self._repo.origroot)
1135 1135 if self._filtername:
1136 1136 repo = repo.filtered(self._filtername)
1137 1137 else:
1138 1138 repo = repo.unfiltered()
1139 1139 c = cachedlocalrepo(repo)
1140 1140 c._state = self._state
1141 1141 c.mtime = self.mtime
1142 1142 return c
General Comments 0
You need to be logged in to leave comments. Login now