##// END OF EJS Templates
bookmarks: calculateupdate() returns a bookmark, not a rev...
Martin von Zweigbergk -
r37377:e27298bf default
parent child Browse files
Show More
@@ -1,907 +1,907 b''
1 1 # Mercurial bookmark support code
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
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
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import struct
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 bin,
16 16 hex,
17 17 short,
18 18 wdirid,
19 19 )
20 20 from . import (
21 21 encoding,
22 22 error,
23 23 obsutil,
24 24 pycompat,
25 25 scmutil,
26 26 txnutil,
27 27 util,
28 28 )
29 29
30 30 # label constants
31 31 # until 3.5, bookmarks.current was the advertised name, not
32 32 # bookmarks.active, so we must use both to avoid breaking old
33 33 # custom styles
34 34 activebookmarklabel = 'bookmarks.active bookmarks.current'
35 35
36 36 def _getbkfile(repo):
37 37 """Hook so that extensions that mess with the store can hook bm storage.
38 38
39 39 For core, this just handles wether we should see pending
40 40 bookmarks or the committed ones. Other extensions (like share)
41 41 may need to tweak this behavior further.
42 42 """
43 43 fp, pending = txnutil.trypending(repo.root, repo.vfs, 'bookmarks')
44 44 return fp
45 45
46 46 class bmstore(dict):
47 47 """Storage for bookmarks.
48 48
49 49 This object should do all bookmark-related reads and writes, so
50 50 that it's fairly simple to replace the storage underlying
51 51 bookmarks without having to clone the logic surrounding
52 52 bookmarks. This type also should manage the active bookmark, if
53 53 any.
54 54
55 55 This particular bmstore implementation stores bookmarks as
56 56 {hash}\s{name}\n (the same format as localtags) in
57 57 .hg/bookmarks. The mapping is stored as {name: nodeid}.
58 58 """
59 59
60 60 def __init__(self, repo):
61 61 dict.__init__(self)
62 62 self._repo = repo
63 63 self._clean = True
64 64 self._aclean = True
65 65 nm = repo.changelog.nodemap
66 66 tonode = bin # force local lookup
67 67 setitem = dict.__setitem__
68 68 try:
69 69 with _getbkfile(repo) as bkfile:
70 70 for line in bkfile:
71 71 line = line.strip()
72 72 if not line:
73 73 continue
74 74 try:
75 75 sha, refspec = line.split(' ', 1)
76 76 node = tonode(sha)
77 77 if node in nm:
78 78 refspec = encoding.tolocal(refspec)
79 79 setitem(self, refspec, node)
80 80 except (TypeError, ValueError):
81 81 # TypeError:
82 82 # - bin(...)
83 83 # ValueError:
84 84 # - node in nm, for non-20-bytes entry
85 85 # - split(...), for string without ' '
86 86 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
87 87 % pycompat.bytestr(line))
88 88 except IOError as inst:
89 89 if inst.errno != errno.ENOENT:
90 90 raise
91 91 self._active = _readactive(repo, self)
92 92
93 93 @property
94 94 def active(self):
95 95 return self._active
96 96
97 97 @active.setter
98 98 def active(self, mark):
99 99 if mark is not None and mark not in self:
100 100 raise AssertionError('bookmark %s does not exist!' % mark)
101 101
102 102 self._active = mark
103 103 self._aclean = False
104 104
105 105 def __setitem__(self, *args, **kwargs):
106 106 raise error.ProgrammingError("use 'bookmarks.applychanges' instead")
107 107
108 108 def _set(self, key, value):
109 109 self._clean = False
110 110 return dict.__setitem__(self, key, value)
111 111
112 112 def __delitem__(self, key):
113 113 raise error.ProgrammingError("use 'bookmarks.applychanges' instead")
114 114
115 115 def _del(self, key):
116 116 self._clean = False
117 117 return dict.__delitem__(self, key)
118 118
119 119 def update(self, *others):
120 120 raise error.ProgrammingError("use 'bookmarks.applychanges' instead")
121 121
122 122 def applychanges(self, repo, tr, changes):
123 123 """Apply a list of changes to bookmarks
124 124 """
125 125 bmchanges = tr.changes.get('bookmarks')
126 126 for name, node in changes:
127 127 old = self.get(name)
128 128 if node is None:
129 129 self._del(name)
130 130 else:
131 131 self._set(name, node)
132 132 if bmchanges is not None:
133 133 # if a previous value exist preserve the "initial" value
134 134 previous = bmchanges.get(name)
135 135 if previous is not None:
136 136 old = previous[0]
137 137 bmchanges[name] = (old, node)
138 138 self._recordchange(tr)
139 139
140 140 def _recordchange(self, tr):
141 141 """record that bookmarks have been changed in a transaction
142 142
143 143 The transaction is then responsible for updating the file content."""
144 144 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
145 145 location='plain')
146 146 tr.hookargs['bookmark_moved'] = '1'
147 147
148 148 def _writerepo(self, repo):
149 149 """Factored out for extensibility"""
150 150 rbm = repo._bookmarks
151 151 if rbm.active not in self:
152 152 rbm.active = None
153 153 rbm._writeactive()
154 154
155 155 with repo.wlock():
156 156 file_ = repo.vfs('bookmarks', 'w', atomictemp=True,
157 157 checkambig=True)
158 158 try:
159 159 self._write(file_)
160 160 except: # re-raises
161 161 file_.discard()
162 162 raise
163 163 finally:
164 164 file_.close()
165 165
166 166 def _writeactive(self):
167 167 if self._aclean:
168 168 return
169 169 with self._repo.wlock():
170 170 if self._active is not None:
171 171 f = self._repo.vfs('bookmarks.current', 'w', atomictemp=True,
172 172 checkambig=True)
173 173 try:
174 174 f.write(encoding.fromlocal(self._active))
175 175 finally:
176 176 f.close()
177 177 else:
178 178 self._repo.vfs.tryunlink('bookmarks.current')
179 179 self._aclean = True
180 180
181 181 def _write(self, fp):
182 182 for name, node in sorted(self.iteritems()):
183 183 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
184 184 self._clean = True
185 185 self._repo.invalidatevolatilesets()
186 186
187 187 def expandname(self, bname):
188 188 if bname == '.':
189 189 if self.active:
190 190 return self.active
191 191 else:
192 192 raise error.Abort(_("no active bookmark"))
193 193 return bname
194 194
195 195 def checkconflict(self, mark, force=False, target=None):
196 196 """check repo for a potential clash of mark with an existing bookmark,
197 197 branch, or hash
198 198
199 199 If target is supplied, then check that we are moving the bookmark
200 200 forward.
201 201
202 202 If force is supplied, then forcibly move the bookmark to a new commit
203 203 regardless if it is a move forward.
204 204
205 205 If divergent bookmark are to be deleted, they will be returned as list.
206 206 """
207 207 cur = self._repo['.'].node()
208 208 if mark in self and not force:
209 209 if target:
210 210 if self[mark] == target and target == cur:
211 211 # re-activating a bookmark
212 212 return []
213 213 rev = self._repo[target].rev()
214 214 anc = self._repo.changelog.ancestors([rev])
215 215 bmctx = self._repo[self[mark]]
216 216 divs = [self._repo[b].node() for b in self
217 217 if b.split('@', 1)[0] == mark.split('@', 1)[0]]
218 218
219 219 # allow resolving a single divergent bookmark even if moving
220 220 # the bookmark across branches when a revision is specified
221 221 # that contains a divergent bookmark
222 222 if bmctx.rev() not in anc and target in divs:
223 223 return divergent2delete(self._repo, [target], mark)
224 224
225 225 deletefrom = [b for b in divs
226 226 if self._repo[b].rev() in anc or b == target]
227 227 delbms = divergent2delete(self._repo, deletefrom, mark)
228 228 if validdest(self._repo, bmctx, self._repo[target]):
229 229 self._repo.ui.status(
230 230 _("moving bookmark '%s' forward from %s\n") %
231 231 (mark, short(bmctx.node())))
232 232 return delbms
233 233 raise error.Abort(_("bookmark '%s' already exists "
234 234 "(use -f to force)") % mark)
235 235 if ((mark in self._repo.branchmap() or
236 236 mark == self._repo.dirstate.branch()) and not force):
237 237 raise error.Abort(
238 238 _("a bookmark cannot have the name of an existing branch"))
239 239 if len(mark) > 3 and not force:
240 240 try:
241 241 shadowhash = (mark in self._repo)
242 242 except error.LookupError: # ambiguous identifier
243 243 shadowhash = False
244 244 if shadowhash:
245 245 self._repo.ui.warn(
246 246 _("bookmark %s matches a changeset hash\n"
247 247 "(did you leave a -r out of an 'hg bookmark' "
248 248 "command?)\n")
249 249 % mark)
250 250 return []
251 251
252 252 def _readactive(repo, marks):
253 253 """
254 254 Get the active bookmark. We can have an active bookmark that updates
255 255 itself as we commit. This function returns the name of that bookmark.
256 256 It is stored in .hg/bookmarks.current
257 257 """
258 258 mark = None
259 259 try:
260 260 file = repo.vfs('bookmarks.current')
261 261 except IOError as inst:
262 262 if inst.errno != errno.ENOENT:
263 263 raise
264 264 return None
265 265 try:
266 266 # No readline() in osutil.posixfile, reading everything is
267 267 # cheap.
268 268 # Note that it's possible for readlines() here to raise
269 269 # IOError, since we might be reading the active mark over
270 270 # static-http which only tries to load the file when we try
271 271 # to read from it.
272 272 mark = encoding.tolocal((file.readlines() or [''])[0])
273 273 if mark == '' or mark not in marks:
274 274 mark = None
275 275 except IOError as inst:
276 276 if inst.errno != errno.ENOENT:
277 277 raise
278 278 return None
279 279 finally:
280 280 file.close()
281 281 return mark
282 282
283 283 def activate(repo, mark):
284 284 """
285 285 Set the given bookmark to be 'active', meaning that this bookmark will
286 286 follow new commits that are made.
287 287 The name is recorded in .hg/bookmarks.current
288 288 """
289 289 repo._bookmarks.active = mark
290 290 repo._bookmarks._writeactive()
291 291
292 292 def deactivate(repo):
293 293 """
294 294 Unset the active bookmark in this repository.
295 295 """
296 296 repo._bookmarks.active = None
297 297 repo._bookmarks._writeactive()
298 298
299 299 def isactivewdirparent(repo):
300 300 """
301 301 Tell whether the 'active' bookmark (the one that follows new commits)
302 302 points to one of the parents of the current working directory (wdir).
303 303
304 304 While this is normally the case, it can on occasion be false; for example,
305 305 immediately after a pull, the active bookmark can be moved to point
306 306 to a place different than the wdir. This is solved by running `hg update`.
307 307 """
308 308 mark = repo._activebookmark
309 309 marks = repo._bookmarks
310 310 parents = [p.node() for p in repo[None].parents()]
311 311 return (mark in marks and marks[mark] in parents)
312 312
313 313 def divergent2delete(repo, deletefrom, bm):
314 314 """find divergent versions of bm on nodes in deletefrom.
315 315
316 316 the list of bookmark to delete."""
317 317 todelete = []
318 318 marks = repo._bookmarks
319 319 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
320 320 for mark in divergent:
321 321 if mark == '@' or '@' not in mark:
322 322 # can't be divergent by definition
323 323 continue
324 324 if mark and marks[mark] in deletefrom:
325 325 if mark != bm:
326 326 todelete.append(mark)
327 327 return todelete
328 328
329 329 def headsforactive(repo):
330 330 """Given a repo with an active bookmark, return divergent bookmark nodes.
331 331
332 332 Args:
333 333 repo: A repository with an active bookmark.
334 334
335 335 Returns:
336 336 A list of binary node ids that is the full list of other
337 337 revisions with bookmarks divergent from the active bookmark. If
338 338 there were no divergent bookmarks, then this list will contain
339 339 only one entry.
340 340 """
341 341 if not repo._activebookmark:
342 342 raise ValueError(
343 343 'headsforactive() only makes sense with an active bookmark')
344 344 name = repo._activebookmark.split('@', 1)[0]
345 345 heads = []
346 346 for mark, n in repo._bookmarks.iteritems():
347 347 if mark.split('@', 1)[0] == name:
348 348 heads.append(n)
349 349 return heads
350 350
351 351 def calculateupdate(ui, repo, checkout):
352 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
353 check out and where to move the active bookmark from, if needed.'''
352 '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark
353 and where to move the active bookmark from, if needed.'''
354 354 movemarkfrom = None
355 355 if checkout is None:
356 356 activemark = repo._activebookmark
357 357 if isactivewdirparent(repo):
358 358 movemarkfrom = repo['.'].node()
359 359 elif activemark:
360 360 ui.status(_("updating to active bookmark %s\n") % activemark)
361 361 checkout = activemark
362 362 return (checkout, movemarkfrom)
363 363
364 364 def update(repo, parents, node):
365 365 deletefrom = parents
366 366 marks = repo._bookmarks
367 367 active = marks.active
368 368 if not active:
369 369 return False
370 370
371 371 bmchanges = []
372 372 if marks[active] in parents:
373 373 new = repo[node]
374 374 divs = [repo[b] for b in marks
375 375 if b.split('@', 1)[0] == active.split('@', 1)[0]]
376 376 anc = repo.changelog.ancestors([new.rev()])
377 377 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
378 378 if validdest(repo, repo[marks[active]], new):
379 379 bmchanges.append((active, new.node()))
380 380
381 381 for bm in divergent2delete(repo, deletefrom, active):
382 382 bmchanges.append((bm, None))
383 383
384 384 if bmchanges:
385 385 with repo.lock(), repo.transaction('bookmark') as tr:
386 386 marks.applychanges(repo, tr, bmchanges)
387 387 return bool(bmchanges)
388 388
389 389 def listbinbookmarks(repo):
390 390 # We may try to list bookmarks on a repo type that does not
391 391 # support it (e.g., statichttprepository).
392 392 marks = getattr(repo, '_bookmarks', {})
393 393
394 394 hasnode = repo.changelog.hasnode
395 395 for k, v in marks.iteritems():
396 396 # don't expose local divergent bookmarks
397 397 if hasnode(v) and ('@' not in k or k.endswith('@')):
398 398 yield k, v
399 399
400 400 def listbookmarks(repo):
401 401 d = {}
402 402 for book, node in listbinbookmarks(repo):
403 403 d[book] = hex(node)
404 404 return d
405 405
406 406 def pushbookmark(repo, key, old, new):
407 407 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
408 408 marks = repo._bookmarks
409 409 existing = hex(marks.get(key, ''))
410 410 if existing != old and existing != new:
411 411 return False
412 412 if new == '':
413 413 changes = [(key, None)]
414 414 else:
415 415 if new not in repo:
416 416 return False
417 417 changes = [(key, repo[new].node())]
418 418 marks.applychanges(repo, tr, changes)
419 419 return True
420 420
421 421 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
422 422 '''Compare bookmarks between srcmarks and dstmarks
423 423
424 424 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
425 425 differ, invalid)", each are list of bookmarks below:
426 426
427 427 :addsrc: added on src side (removed on dst side, perhaps)
428 428 :adddst: added on dst side (removed on src side, perhaps)
429 429 :advsrc: advanced on src side
430 430 :advdst: advanced on dst side
431 431 :diverge: diverge
432 432 :differ: changed, but changeset referred on src is unknown on dst
433 433 :invalid: unknown on both side
434 434 :same: same on both side
435 435
436 436 Each elements of lists in result tuple is tuple "(bookmark name,
437 437 changeset ID on source side, changeset ID on destination
438 438 side)". Each changeset IDs are 40 hexadecimal digit string or
439 439 None.
440 440
441 441 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
442 442 "invalid" list may be unknown for repo.
443 443
444 444 If "targets" is specified, only bookmarks listed in it are
445 445 examined.
446 446 '''
447 447
448 448 if targets:
449 449 bset = set(targets)
450 450 else:
451 451 srcmarkset = set(srcmarks)
452 452 dstmarkset = set(dstmarks)
453 453 bset = srcmarkset | dstmarkset
454 454
455 455 results = ([], [], [], [], [], [], [], [])
456 456 addsrc = results[0].append
457 457 adddst = results[1].append
458 458 advsrc = results[2].append
459 459 advdst = results[3].append
460 460 diverge = results[4].append
461 461 differ = results[5].append
462 462 invalid = results[6].append
463 463 same = results[7].append
464 464
465 465 for b in sorted(bset):
466 466 if b not in srcmarks:
467 467 if b in dstmarks:
468 468 adddst((b, None, dstmarks[b]))
469 469 else:
470 470 invalid((b, None, None))
471 471 elif b not in dstmarks:
472 472 addsrc((b, srcmarks[b], None))
473 473 else:
474 474 scid = srcmarks[b]
475 475 dcid = dstmarks[b]
476 476 if scid == dcid:
477 477 same((b, scid, dcid))
478 478 elif scid in repo and dcid in repo:
479 479 sctx = repo[scid]
480 480 dctx = repo[dcid]
481 481 if sctx.rev() < dctx.rev():
482 482 if validdest(repo, sctx, dctx):
483 483 advdst((b, scid, dcid))
484 484 else:
485 485 diverge((b, scid, dcid))
486 486 else:
487 487 if validdest(repo, dctx, sctx):
488 488 advsrc((b, scid, dcid))
489 489 else:
490 490 diverge((b, scid, dcid))
491 491 else:
492 492 # it is too expensive to examine in detail, in this case
493 493 differ((b, scid, dcid))
494 494
495 495 return results
496 496
497 497 def _diverge(ui, b, path, localmarks, remotenode):
498 498 '''Return appropriate diverged bookmark for specified ``path``
499 499
500 500 This returns None, if it is failed to assign any divergent
501 501 bookmark name.
502 502
503 503 This reuses already existing one with "@number" suffix, if it
504 504 refers ``remotenode``.
505 505 '''
506 506 if b == '@':
507 507 b = ''
508 508 # try to use an @pathalias suffix
509 509 # if an @pathalias already exists, we overwrite (update) it
510 510 if path.startswith("file:"):
511 511 path = util.url(path).path
512 512 for p, u in ui.configitems("paths"):
513 513 if u.startswith("file:"):
514 514 u = util.url(u).path
515 515 if path == u:
516 516 return '%s@%s' % (b, p)
517 517
518 518 # assign a unique "@number" suffix newly
519 519 for x in range(1, 100):
520 520 n = '%s@%d' % (b, x)
521 521 if n not in localmarks or localmarks[n] == remotenode:
522 522 return n
523 523
524 524 return None
525 525
526 526 def unhexlifybookmarks(marks):
527 527 binremotemarks = {}
528 528 for name, node in marks.items():
529 529 binremotemarks[name] = bin(node)
530 530 return binremotemarks
531 531
532 532 _binaryentry = struct.Struct('>20sH')
533 533
534 534 def binaryencode(bookmarks):
535 535 """encode a '(bookmark, node)' iterable into a binary stream
536 536
537 537 the binary format is:
538 538
539 539 <node><bookmark-length><bookmark-name>
540 540
541 541 :node: is a 20 bytes binary node,
542 542 :bookmark-length: an unsigned short,
543 543 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
544 544
545 545 wdirid (all bits set) will be used as a special value for "missing"
546 546 """
547 547 binarydata = []
548 548 for book, node in bookmarks:
549 549 if not node: # None or ''
550 550 node = wdirid
551 551 binarydata.append(_binaryentry.pack(node, len(book)))
552 552 binarydata.append(book)
553 553 return ''.join(binarydata)
554 554
555 555 def binarydecode(stream):
556 556 """decode a binary stream into an '(bookmark, node)' iterable
557 557
558 558 the binary format is:
559 559
560 560 <node><bookmark-length><bookmark-name>
561 561
562 562 :node: is a 20 bytes binary node,
563 563 :bookmark-length: an unsigned short,
564 564 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
565 565
566 566 wdirid (all bits set) will be used as a special value for "missing"
567 567 """
568 568 entrysize = _binaryentry.size
569 569 books = []
570 570 while True:
571 571 entry = stream.read(entrysize)
572 572 if len(entry) < entrysize:
573 573 if entry:
574 574 raise error.Abort(_('bad bookmark stream'))
575 575 break
576 576 node, length = _binaryentry.unpack(entry)
577 577 bookmark = stream.read(length)
578 578 if len(bookmark) < length:
579 579 if entry:
580 580 raise error.Abort(_('bad bookmark stream'))
581 581 if node == wdirid:
582 582 node = None
583 583 books.append((bookmark, node))
584 584 return books
585 585
586 586 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
587 587 ui.debug("checking for updated bookmarks\n")
588 588 localmarks = repo._bookmarks
589 589 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
590 590 ) = comparebookmarks(repo, remotemarks, localmarks)
591 591
592 592 status = ui.status
593 593 warn = ui.warn
594 594 if ui.configbool('ui', 'quietbookmarkmove'):
595 595 status = warn = ui.debug
596 596
597 597 explicit = set(explicit)
598 598 changed = []
599 599 for b, scid, dcid in addsrc:
600 600 if scid in repo: # add remote bookmarks for changes we already have
601 601 changed.append((b, scid, status,
602 602 _("adding remote bookmark %s\n") % (b)))
603 603 elif b in explicit:
604 604 explicit.remove(b)
605 605 ui.warn(_("remote bookmark %s points to locally missing %s\n")
606 606 % (b, hex(scid)[:12]))
607 607
608 608 for b, scid, dcid in advsrc:
609 609 changed.append((b, scid, status,
610 610 _("updating bookmark %s\n") % (b)))
611 611 # remove normal movement from explicit set
612 612 explicit.difference_update(d[0] for d in changed)
613 613
614 614 for b, scid, dcid in diverge:
615 615 if b in explicit:
616 616 explicit.discard(b)
617 617 changed.append((b, scid, status,
618 618 _("importing bookmark %s\n") % (b)))
619 619 else:
620 620 db = _diverge(ui, b, path, localmarks, scid)
621 621 if db:
622 622 changed.append((db, scid, warn,
623 623 _("divergent bookmark %s stored as %s\n") %
624 624 (b, db)))
625 625 else:
626 626 warn(_("warning: failed to assign numbered name "
627 627 "to divergent bookmark %s\n") % (b))
628 628 for b, scid, dcid in adddst + advdst:
629 629 if b in explicit:
630 630 explicit.discard(b)
631 631 changed.append((b, scid, status,
632 632 _("importing bookmark %s\n") % (b)))
633 633 for b, scid, dcid in differ:
634 634 if b in explicit:
635 635 explicit.remove(b)
636 636 ui.warn(_("remote bookmark %s points to locally missing %s\n")
637 637 % (b, hex(scid)[:12]))
638 638
639 639 if changed:
640 640 tr = trfunc()
641 641 changes = []
642 642 for b, node, writer, msg in sorted(changed):
643 643 changes.append((b, node))
644 644 writer(msg)
645 645 localmarks.applychanges(repo, tr, changes)
646 646
647 647 def incoming(ui, repo, other):
648 648 '''Show bookmarks incoming from other to repo
649 649 '''
650 650 ui.status(_("searching for changed bookmarks\n"))
651 651
652 652 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
653 653 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
654 654 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
655 655
656 656 incomings = []
657 657 if ui.debugflag:
658 658 getid = lambda id: id
659 659 else:
660 660 getid = lambda id: id[:12]
661 661 if ui.verbose:
662 662 def add(b, id, st):
663 663 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
664 664 else:
665 665 def add(b, id, st):
666 666 incomings.append(" %-25s %s\n" % (b, getid(id)))
667 667 for b, scid, dcid in addsrc:
668 668 # i18n: "added" refers to a bookmark
669 669 add(b, hex(scid), _('added'))
670 670 for b, scid, dcid in advsrc:
671 671 # i18n: "advanced" refers to a bookmark
672 672 add(b, hex(scid), _('advanced'))
673 673 for b, scid, dcid in diverge:
674 674 # i18n: "diverged" refers to a bookmark
675 675 add(b, hex(scid), _('diverged'))
676 676 for b, scid, dcid in differ:
677 677 # i18n: "changed" refers to a bookmark
678 678 add(b, hex(scid), _('changed'))
679 679
680 680 if not incomings:
681 681 ui.status(_("no changed bookmarks found\n"))
682 682 return 1
683 683
684 684 for s in sorted(incomings):
685 685 ui.write(s)
686 686
687 687 return 0
688 688
689 689 def outgoing(ui, repo, other):
690 690 '''Show bookmarks outgoing from repo to other
691 691 '''
692 692 ui.status(_("searching for changed bookmarks\n"))
693 693
694 694 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
695 695 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
696 696 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
697 697
698 698 outgoings = []
699 699 if ui.debugflag:
700 700 getid = lambda id: id
701 701 else:
702 702 getid = lambda id: id[:12]
703 703 if ui.verbose:
704 704 def add(b, id, st):
705 705 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
706 706 else:
707 707 def add(b, id, st):
708 708 outgoings.append(" %-25s %s\n" % (b, getid(id)))
709 709 for b, scid, dcid in addsrc:
710 710 # i18n: "added refers to a bookmark
711 711 add(b, hex(scid), _('added'))
712 712 for b, scid, dcid in adddst:
713 713 # i18n: "deleted" refers to a bookmark
714 714 add(b, ' ' * 40, _('deleted'))
715 715 for b, scid, dcid in advsrc:
716 716 # i18n: "advanced" refers to a bookmark
717 717 add(b, hex(scid), _('advanced'))
718 718 for b, scid, dcid in diverge:
719 719 # i18n: "diverged" refers to a bookmark
720 720 add(b, hex(scid), _('diverged'))
721 721 for b, scid, dcid in differ:
722 722 # i18n: "changed" refers to a bookmark
723 723 add(b, hex(scid), _('changed'))
724 724
725 725 if not outgoings:
726 726 ui.status(_("no changed bookmarks found\n"))
727 727 return 1
728 728
729 729 for s in sorted(outgoings):
730 730 ui.write(s)
731 731
732 732 return 0
733 733
734 734 def summary(repo, other):
735 735 '''Compare bookmarks between repo and other for "hg summary" output
736 736
737 737 This returns "(# of incoming, # of outgoing)" tuple.
738 738 '''
739 739 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
740 740 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
741 741 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
742 742 return (len(addsrc), len(adddst))
743 743
744 744 def validdest(repo, old, new):
745 745 """Is the new bookmark destination a valid update from the old one"""
746 746 repo = repo.unfiltered()
747 747 if old == new:
748 748 # Old == new -> nothing to update.
749 749 return False
750 750 elif not old:
751 751 # old is nullrev, anything is valid.
752 752 # (new != nullrev has been excluded by the previous check)
753 753 return True
754 754 elif repo.obsstore:
755 755 return new.node() in obsutil.foreground(repo, [old.node()])
756 756 else:
757 757 # still an independent clause as it is lazier (and therefore faster)
758 758 return old.descendant(new)
759 759
760 760 def checkformat(repo, mark):
761 761 """return a valid version of a potential bookmark name
762 762
763 763 Raises an abort error if the bookmark name is not valid.
764 764 """
765 765 mark = mark.strip()
766 766 if not mark:
767 767 raise error.Abort(_("bookmark names cannot consist entirely of "
768 768 "whitespace"))
769 769 scmutil.checknewlabel(repo, mark, 'bookmark')
770 770 return mark
771 771
772 772 def delete(repo, tr, names):
773 773 """remove a mark from the bookmark store
774 774
775 775 Raises an abort error if mark does not exist.
776 776 """
777 777 marks = repo._bookmarks
778 778 changes = []
779 779 for mark in names:
780 780 if mark not in marks:
781 781 raise error.Abort(_("bookmark '%s' does not exist") % mark)
782 782 if mark == repo._activebookmark:
783 783 deactivate(repo)
784 784 changes.append((mark, None))
785 785 marks.applychanges(repo, tr, changes)
786 786
787 787 def rename(repo, tr, old, new, force=False, inactive=False):
788 788 """rename a bookmark from old to new
789 789
790 790 If force is specified, then the new name can overwrite an existing
791 791 bookmark.
792 792
793 793 If inactive is specified, then do not activate the new bookmark.
794 794
795 795 Raises an abort error if old is not in the bookmark store.
796 796 """
797 797 marks = repo._bookmarks
798 798 mark = checkformat(repo, new)
799 799 if old not in marks:
800 800 raise error.Abort(_("bookmark '%s' does not exist") % old)
801 801 changes = []
802 802 for bm in marks.checkconflict(mark, force):
803 803 changes.append((bm, None))
804 804 changes.extend([(mark, marks[old]), (old, None)])
805 805 marks.applychanges(repo, tr, changes)
806 806 if repo._activebookmark == old and not inactive:
807 807 activate(repo, mark)
808 808
809 809 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
810 810 """add a list of bookmarks
811 811
812 812 If force is specified, then the new name can overwrite an existing
813 813 bookmark.
814 814
815 815 If inactive is specified, then do not activate any bookmark. Otherwise, the
816 816 first bookmark is activated.
817 817
818 818 Raises an abort error if old is not in the bookmark store.
819 819 """
820 820 marks = repo._bookmarks
821 821 cur = repo['.'].node()
822 822 newact = None
823 823 changes = []
824 824 hiddenrev = None
825 825
826 826 # unhide revs if any
827 827 if rev:
828 828 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
829 829
830 830 for mark in names:
831 831 mark = checkformat(repo, mark)
832 832 if newact is None:
833 833 newact = mark
834 834 if inactive and mark == repo._activebookmark:
835 835 deactivate(repo)
836 836 return
837 837 tgt = cur
838 838 if rev:
839 839 ctx = scmutil.revsingle(repo, rev)
840 840 if ctx.hidden():
841 841 hiddenrev = ctx.hex()[:12]
842 842 tgt = ctx.node()
843 843 for bm in marks.checkconflict(mark, force, tgt):
844 844 changes.append((bm, None))
845 845 changes.append((mark, tgt))
846 846
847 847 if hiddenrev:
848 848 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
849 849
850 850 if ctx.obsolete():
851 851 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
852 852 repo.ui.warn("(%s)\n" % msg)
853 853
854 854 marks.applychanges(repo, tr, changes)
855 855 if not inactive and cur == marks[newact] and not rev:
856 856 activate(repo, newact)
857 857 elif cur != tgt and newact == repo._activebookmark:
858 858 deactivate(repo)
859 859
860 860 def _printbookmarks(ui, repo, bmarks, **opts):
861 861 """private method to print bookmarks
862 862
863 863 Provides a way for extensions to control how bookmarks are printed (e.g.
864 864 prepend or postpend names)
865 865 """
866 866 opts = pycompat.byteskwargs(opts)
867 867 fm = ui.formatter('bookmarks', opts)
868 868 hexfn = fm.hexfunc
869 869 if len(bmarks) == 0 and fm.isplain():
870 870 ui.status(_("no bookmarks set\n"))
871 871 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
872 872 fm.startitem()
873 873 if not ui.quiet:
874 874 fm.plain(' %s ' % prefix, label=label)
875 875 fm.write('bookmark', '%s', bmark, label=label)
876 876 pad = " " * (25 - encoding.colwidth(bmark))
877 877 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
878 878 repo.changelog.rev(n), hexfn(n), label=label)
879 879 fm.data(active=(activebookmarklabel in label))
880 880 fm.plain('\n')
881 881 fm.end()
882 882
883 883 def printbookmarks(ui, repo, **opts):
884 884 """print bookmarks to a formatter
885 885
886 886 Provides a way for extensions to control how bookmarks are printed.
887 887 """
888 888 marks = repo._bookmarks
889 889 bmarks = {}
890 890 for bmark, n in sorted(marks.iteritems()):
891 891 active = repo._activebookmark
892 892 if bmark == active:
893 893 prefix, label = '*', activebookmarklabel
894 894 else:
895 895 prefix, label = ' ', ''
896 896
897 897 bmarks[bmark] = (n, prefix, label)
898 898 _printbookmarks(ui, repo, bmarks, **opts)
899 899
900 900 def preparehookargs(name, old, new):
901 901 if new is None:
902 902 new = ''
903 903 if old is None:
904 904 old = ''
905 905 return {'bookmark': name,
906 906 'node': hex(new),
907 907 'oldnode': hex(old)}
@@ -1,415 +1,415 b''
1 1 # destutil.py - Mercurial utility function for command destination
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com> and other
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
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from . import (
12 12 bookmarks,
13 13 error,
14 14 obsutil,
15 15 scmutil,
16 16 stack
17 17 )
18 18
19 19 def _destupdateobs(repo, clean):
20 20 """decide of an update destination from obsolescence markers"""
21 21 node = None
22 22 wc = repo[None]
23 23 p1 = wc.p1()
24 24 movemark = None
25 25
26 26 if p1.obsolete() and not p1.children():
27 27 # allow updating to successors
28 28 successors = obsutil.successorssets(repo, p1.node())
29 29
30 30 # behavior of certain cases is as follows,
31 31 #
32 32 # divergent changesets: update to highest rev, similar to what
33 33 # is currently done when there are more than one head
34 34 # (i.e. 'tip')
35 35 #
36 36 # replaced changesets: same as divergent except we know there
37 37 # is no conflict
38 38 #
39 39 # pruned changeset: no update is done; though, we could
40 40 # consider updating to the first non-obsolete parent,
41 41 # similar to what is current done for 'hg prune'
42 42
43 43 if successors:
44 44 # flatten the list here handles both divergent (len > 1)
45 45 # and the usual case (len = 1)
46 46 successors = [n for sub in successors for n in sub]
47 47
48 48 # get the max revision for the given successors set,
49 49 # i.e. the 'tip' of a set
50 50 node = repo.revs('max(%ln)', successors).first()
51 51 if bookmarks.isactivewdirparent(repo):
52 52 movemark = repo['.'].node()
53 53 return node, movemark, None
54 54
55 55 def _destupdatebook(repo, clean):
56 56 """decide on an update destination from active bookmark"""
57 57 # we also move the active bookmark, if any
58 activemark = None
59 node, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
60 if node is not None:
61 activemark = node
58 node = None
59 activemark, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
60 if activemark is not None:
61 node = repo.lookup(activemark)
62 62 return node, movemark, activemark
63 63
64 64 def _destupdatebranch(repo, clean):
65 65 """decide on an update destination from current branch
66 66
67 67 This ignores closed branch heads.
68 68 """
69 69 wc = repo[None]
70 70 movemark = node = None
71 71 currentbranch = wc.branch()
72 72
73 73 if clean:
74 74 currentbranch = repo['.'].branch()
75 75
76 76 if currentbranch in repo.branchmap():
77 77 heads = repo.branchheads(currentbranch)
78 78 if heads:
79 79 node = repo.revs('max(.::(%ln))', heads).first()
80 80 if bookmarks.isactivewdirparent(repo):
81 81 movemark = repo['.'].node()
82 82 elif currentbranch == 'default' and not wc.p1():
83 83 # "null" parent belongs to "default" branch, but it doesn't exist, so
84 84 # update to the tipmost non-closed branch head
85 85 node = repo.revs('max(head() and not closed())').first()
86 86 else:
87 87 node = repo['.'].node()
88 88 return node, movemark, None
89 89
90 90 def _destupdatebranchfallback(repo, clean):
91 91 """decide on an update destination from closed heads in current branch"""
92 92 wc = repo[None]
93 93 currentbranch = wc.branch()
94 94 movemark = None
95 95 if currentbranch in repo.branchmap():
96 96 # here, all descendant branch heads are closed
97 97 heads = repo.branchheads(currentbranch, closed=True)
98 98 assert heads, "any branch has at least one head"
99 99 node = repo.revs('max(.::(%ln))', heads).first()
100 100 assert node is not None, ("any revision has at least "
101 101 "one descendant branch head")
102 102 if bookmarks.isactivewdirparent(repo):
103 103 movemark = repo['.'].node()
104 104 else:
105 105 # here, no "default" branch, and all branches are closed
106 106 node = repo.lookup('tip')
107 107 assert node is not None, "'tip' exists even in empty repository"
108 108 return node, movemark, None
109 109
110 110 # order in which each step should be evaluated
111 111 # steps are run until one finds a destination
112 112 destupdatesteps = ['evolution', 'bookmark', 'branch', 'branchfallback']
113 113 # mapping to ease extension overriding steps.
114 114 destupdatestepmap = {'evolution': _destupdateobs,
115 115 'bookmark': _destupdatebook,
116 116 'branch': _destupdatebranch,
117 117 'branchfallback': _destupdatebranchfallback,
118 118 }
119 119
120 120 def destupdate(repo, clean=False):
121 121 """destination for bare update operation
122 122
123 123 return (rev, movemark, activemark)
124 124
125 125 - rev: the revision to update to,
126 126 - movemark: node to move the active bookmark from
127 127 (cf bookmark.calculate update),
128 128 - activemark: a bookmark to activate at the end of the update.
129 129 """
130 130 node = movemark = activemark = None
131 131
132 132 for step in destupdatesteps:
133 133 node, movemark, activemark = destupdatestepmap[step](repo, clean)
134 134 if node is not None:
135 135 break
136 136 rev = repo[node].rev()
137 137
138 138 return rev, movemark, activemark
139 139
140 140 msgdestmerge = {
141 141 # too many matching divergent bookmark
142 142 'toomanybookmarks':
143 143 {'merge':
144 144 (_("multiple matching bookmarks to merge -"
145 145 " please merge with an explicit rev or bookmark"),
146 146 _("run 'hg heads' to see all heads")),
147 147 'rebase':
148 148 (_("multiple matching bookmarks to rebase -"
149 149 " please rebase to an explicit rev or bookmark"),
150 150 _("run 'hg heads' to see all heads")),
151 151 },
152 152 # no other matching divergent bookmark
153 153 'nootherbookmarks':
154 154 {'merge':
155 155 (_("no matching bookmark to merge - "
156 156 "please merge with an explicit rev or bookmark"),
157 157 _("run 'hg heads' to see all heads")),
158 158 'rebase':
159 159 (_("no matching bookmark to rebase - "
160 160 "please rebase to an explicit rev or bookmark"),
161 161 _("run 'hg heads' to see all heads")),
162 162 },
163 163 # branch have too many unbookmarked heads, no obvious destination
164 164 'toomanyheads':
165 165 {'merge':
166 166 (_("branch '%s' has %d heads - please merge with an explicit rev"),
167 167 _("run 'hg heads .' to see heads")),
168 168 'rebase':
169 169 (_("branch '%s' has %d heads - please rebase to an explicit rev"),
170 170 _("run 'hg heads .' to see heads")),
171 171 },
172 172 # branch have no other unbookmarked heads
173 173 'bookmarkedheads':
174 174 {'merge':
175 175 (_("heads are bookmarked - please merge with an explicit rev"),
176 176 _("run 'hg heads' to see all heads")),
177 177 'rebase':
178 178 (_("heads are bookmarked - please rebase to an explicit rev"),
179 179 _("run 'hg heads' to see all heads")),
180 180 },
181 181 # branch have just a single heads, but there is other branches
182 182 'nootherbranchheads':
183 183 {'merge':
184 184 (_("branch '%s' has one head - please merge with an explicit rev"),
185 185 _("run 'hg heads' to see all heads")),
186 186 'rebase':
187 187 (_("branch '%s' has one head - please rebase to an explicit rev"),
188 188 _("run 'hg heads' to see all heads")),
189 189 },
190 190 # repository have a single head
191 191 'nootherheads':
192 192 {'merge':
193 193 (_('nothing to merge'),
194 194 None),
195 195 'rebase':
196 196 (_('nothing to rebase'),
197 197 None),
198 198 },
199 199 # repository have a single head and we are not on it
200 200 'nootherheadsbehind':
201 201 {'merge':
202 202 (_('nothing to merge'),
203 203 _("use 'hg update' instead")),
204 204 'rebase':
205 205 (_('nothing to rebase'),
206 206 _("use 'hg update' instead")),
207 207 },
208 208 # We are not on a head
209 209 'notatheads':
210 210 {'merge':
211 211 (_('working directory not at a head revision'),
212 212 _("use 'hg update' or merge with an explicit revision")),
213 213 'rebase':
214 214 (_('working directory not at a head revision'),
215 215 _("use 'hg update' or rebase to an explicit revision"))
216 216 },
217 217 'emptysourceset':
218 218 {'merge':
219 219 (_('source set is empty'),
220 220 None),
221 221 'rebase':
222 222 (_('source set is empty'),
223 223 None),
224 224 },
225 225 'multiplebranchessourceset':
226 226 {'merge':
227 227 (_('source set is rooted in multiple branches'),
228 228 None),
229 229 'rebase':
230 230 (_('rebaseset is rooted in multiple named branches'),
231 231 _('specify an explicit destination with --dest')),
232 232 },
233 233 }
234 234
235 235 def _destmergebook(repo, action='merge', sourceset=None, destspace=None):
236 236 """find merge destination in the active bookmark case"""
237 237 node = None
238 238 bmheads = bookmarks.headsforactive(repo)
239 239 curhead = repo[repo._activebookmark].node()
240 240 if len(bmheads) == 2:
241 241 if curhead == bmheads[0]:
242 242 node = bmheads[1]
243 243 else:
244 244 node = bmheads[0]
245 245 elif len(bmheads) > 2:
246 246 msg, hint = msgdestmerge['toomanybookmarks'][action]
247 247 raise error.ManyMergeDestAbort(msg, hint=hint)
248 248 elif len(bmheads) <= 1:
249 249 msg, hint = msgdestmerge['nootherbookmarks'][action]
250 250 raise error.NoMergeDestAbort(msg, hint=hint)
251 251 assert node is not None
252 252 return node
253 253
254 254 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True,
255 255 destspace=None):
256 256 """find merge destination based on branch heads"""
257 257 node = None
258 258
259 259 if sourceset is None:
260 260 sourceset = [repo[repo.dirstate.p1()].rev()]
261 261 branch = repo.dirstate.branch()
262 262 elif not sourceset:
263 263 msg, hint = msgdestmerge['emptysourceset'][action]
264 264 raise error.NoMergeDestAbort(msg, hint=hint)
265 265 else:
266 266 branch = None
267 267 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
268 268 if branch is not None and ctx.branch() != branch:
269 269 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
270 270 raise error.ManyMergeDestAbort(msg, hint=hint)
271 271 branch = ctx.branch()
272 272
273 273 bheads = repo.branchheads(branch)
274 274 onhead = repo.revs('%ld and %ln', sourceset, bheads)
275 275 if onheadcheck and not onhead:
276 276 # Case A: working copy if not on a head. (merge only)
277 277 #
278 278 # This is probably a user mistake We bailout pointing at 'hg update'
279 279 if len(repo.heads()) <= 1:
280 280 msg, hint = msgdestmerge['nootherheadsbehind'][action]
281 281 else:
282 282 msg, hint = msgdestmerge['notatheads'][action]
283 283 raise error.Abort(msg, hint=hint)
284 284 # remove heads descendants of source from the set
285 285 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
286 286 # filters out bookmarked heads
287 287 nbhs = list(repo.revs('%ld - bookmark()', bheads))
288 288
289 289 if destspace is not None:
290 290 # restrict search space
291 291 # used in the 'hg pull --rebase' case, see issue 5214.
292 292 nbhs = list(repo.revs('%ld and %ld', destspace, nbhs))
293 293
294 294 if len(nbhs) > 1:
295 295 # Case B: There is more than 1 other anonymous heads
296 296 #
297 297 # This means that there will be more than 1 candidate. This is
298 298 # ambiguous. We abort asking the user to pick as explicit destination
299 299 # instead.
300 300 msg, hint = msgdestmerge['toomanyheads'][action]
301 301 msg %= (branch, len(bheads) + 1)
302 302 raise error.ManyMergeDestAbort(msg, hint=hint)
303 303 elif not nbhs:
304 304 # Case B: There is no other anonymous heads
305 305 #
306 306 # This means that there is no natural candidate to merge with.
307 307 # We abort, with various messages for various cases.
308 308 if bheads:
309 309 msg, hint = msgdestmerge['bookmarkedheads'][action]
310 310 elif len(repo.heads()) > 1:
311 311 msg, hint = msgdestmerge['nootherbranchheads'][action]
312 312 msg %= branch
313 313 elif not onhead:
314 314 # if 'onheadcheck == False' (rebase case),
315 315 # this was not caught in Case A.
316 316 msg, hint = msgdestmerge['nootherheadsbehind'][action]
317 317 else:
318 318 msg, hint = msgdestmerge['nootherheads'][action]
319 319 raise error.NoMergeDestAbort(msg, hint=hint)
320 320 else:
321 321 node = nbhs[0]
322 322 assert node is not None
323 323 return node
324 324
325 325 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True,
326 326 destspace=None):
327 327 """return the default destination for a merge
328 328
329 329 (or raise exception about why it can't pick one)
330 330
331 331 :action: the action being performed, controls emitted error message
332 332 """
333 333 # destspace is here to work around issues with `hg pull --rebase` see
334 334 # issue5214 for details
335 335 if repo._activebookmark:
336 336 node = _destmergebook(repo, action=action, sourceset=sourceset,
337 337 destspace=destspace)
338 338 else:
339 339 node = _destmergebranch(repo, action=action, sourceset=sourceset,
340 340 onheadcheck=onheadcheck, destspace=destspace)
341 341 return repo[node].rev()
342 342
343 343 def desthistedit(ui, repo):
344 344 """Default base revision to edit for `hg histedit`."""
345 345 default = ui.config('histedit', 'defaultrev')
346 346
347 347 if default is None:
348 348 revs = stack.getstack(repo)
349 349 elif default:
350 350 revs = scmutil.revrange(repo, [default])
351 351
352 352 if revs:
353 353 # The revset supplied by the user may not be in ascending order nor
354 354 # take the first revision. So do this manually.
355 355 revs.sort()
356 356 return revs.first()
357 357
358 358 return None
359 359
360 360 def stackbase(ui, repo):
361 361 revs = stack.getstack(repo)
362 362 return revs.first() if revs else None
363 363
364 364 def _statusotherbook(ui, repo):
365 365 bmheads = bookmarks.headsforactive(repo)
366 366 curhead = repo[repo._activebookmark].node()
367 367 if repo.revs('%n and parents()', curhead):
368 368 # we are on the active bookmark
369 369 bmheads = [b for b in bmheads if curhead != b]
370 370 if bmheads:
371 371 msg = _('%i other divergent bookmarks for "%s"\n')
372 372 ui.status(msg % (len(bmheads), repo._activebookmark))
373 373
374 374 def _statusotherbranchheads(ui, repo):
375 375 currentbranch = repo.dirstate.branch()
376 376 allheads = repo.branchheads(currentbranch, closed=True)
377 377 heads = repo.branchheads(currentbranch)
378 378 if repo.revs('%ln and parents()', allheads):
379 379 # we are on a head, even though it might be closed
380 380 #
381 381 # on closed otherheads
382 382 # ========= ==========
383 383 # o 0 all heads for current branch are closed
384 384 # N only descendant branch heads are closed
385 385 # x 0 there is only one non-closed branch head
386 386 # N there are some non-closed branch heads
387 387 # ========= ==========
388 388 otherheads = repo.revs('%ln - parents()', heads)
389 389 if repo['.'].closesbranch():
390 390 ui.warn(_('no open descendant heads on branch "%s", '
391 391 'updating to a closed head\n') %
392 392 (currentbranch))
393 393 if otherheads:
394 394 ui.warn(_("(committing will reopen the head, "
395 395 "use 'hg heads .' to see %i other heads)\n") %
396 396 (len(otherheads)))
397 397 else:
398 398 ui.warn(_('(committing will reopen branch "%s")\n') %
399 399 (currentbranch))
400 400 elif otherheads:
401 401 curhead = repo['.']
402 402 ui.status(_('updated to "%s: %s"\n') % (curhead,
403 403 curhead.description().split('\n')[0]))
404 404 ui.status(_('%i other heads for branch "%s"\n') %
405 405 (len(otherheads), currentbranch))
406 406
407 407 def statusotherdests(ui, repo):
408 408 """Print message about other head"""
409 409 # XXX we should probably include a hint:
410 410 # - about what to do
411 411 # - how to see such heads
412 412 if repo._activebookmark:
413 413 _statusotherbook(ui, repo)
414 414 else:
415 415 _statusotherbranchheads(ui, repo)
General Comments 0
You need to be logged in to leave comments. Login now