##// END OF EJS Templates
bookmarks: use isrevsymbol() for detecting collision with existing symbol...
Martin von Zweigbergk -
r37415:46d9f998 default
parent child Browse files
Show More
@@ -1,906 +1,906 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 shadowhash = (mark in self._repo)
241 shadowhash = scmutil.isrevsymbol(self._repo, mark)
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):
352 352 '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark
353 353 and where to move the active bookmark from, if needed.'''
354 354 checkout, movemarkfrom = None, None
355 355 activemark = repo._activebookmark
356 356 if isactivewdirparent(repo):
357 357 movemarkfrom = repo['.'].node()
358 358 elif activemark:
359 359 ui.status(_("updating to active bookmark %s\n") % activemark)
360 360 checkout = activemark
361 361 return (checkout, movemarkfrom)
362 362
363 363 def update(repo, parents, node):
364 364 deletefrom = parents
365 365 marks = repo._bookmarks
366 366 active = marks.active
367 367 if not active:
368 368 return False
369 369
370 370 bmchanges = []
371 371 if marks[active] in parents:
372 372 new = repo[node]
373 373 divs = [repo[b] for b in marks
374 374 if b.split('@', 1)[0] == active.split('@', 1)[0]]
375 375 anc = repo.changelog.ancestors([new.rev()])
376 376 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
377 377 if validdest(repo, repo[marks[active]], new):
378 378 bmchanges.append((active, new.node()))
379 379
380 380 for bm in divergent2delete(repo, deletefrom, active):
381 381 bmchanges.append((bm, None))
382 382
383 383 if bmchanges:
384 384 with repo.lock(), repo.transaction('bookmark') as tr:
385 385 marks.applychanges(repo, tr, bmchanges)
386 386 return bool(bmchanges)
387 387
388 388 def listbinbookmarks(repo):
389 389 # We may try to list bookmarks on a repo type that does not
390 390 # support it (e.g., statichttprepository).
391 391 marks = getattr(repo, '_bookmarks', {})
392 392
393 393 hasnode = repo.changelog.hasnode
394 394 for k, v in marks.iteritems():
395 395 # don't expose local divergent bookmarks
396 396 if hasnode(v) and ('@' not in k or k.endswith('@')):
397 397 yield k, v
398 398
399 399 def listbookmarks(repo):
400 400 d = {}
401 401 for book, node in listbinbookmarks(repo):
402 402 d[book] = hex(node)
403 403 return d
404 404
405 405 def pushbookmark(repo, key, old, new):
406 406 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
407 407 marks = repo._bookmarks
408 408 existing = hex(marks.get(key, ''))
409 409 if existing != old and existing != new:
410 410 return False
411 411 if new == '':
412 412 changes = [(key, None)]
413 413 else:
414 414 if new not in repo:
415 415 return False
416 416 changes = [(key, repo[new].node())]
417 417 marks.applychanges(repo, tr, changes)
418 418 return True
419 419
420 420 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
421 421 '''Compare bookmarks between srcmarks and dstmarks
422 422
423 423 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
424 424 differ, invalid)", each are list of bookmarks below:
425 425
426 426 :addsrc: added on src side (removed on dst side, perhaps)
427 427 :adddst: added on dst side (removed on src side, perhaps)
428 428 :advsrc: advanced on src side
429 429 :advdst: advanced on dst side
430 430 :diverge: diverge
431 431 :differ: changed, but changeset referred on src is unknown on dst
432 432 :invalid: unknown on both side
433 433 :same: same on both side
434 434
435 435 Each elements of lists in result tuple is tuple "(bookmark name,
436 436 changeset ID on source side, changeset ID on destination
437 437 side)". Each changeset IDs are 40 hexadecimal digit string or
438 438 None.
439 439
440 440 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
441 441 "invalid" list may be unknown for repo.
442 442
443 443 If "targets" is specified, only bookmarks listed in it are
444 444 examined.
445 445 '''
446 446
447 447 if targets:
448 448 bset = set(targets)
449 449 else:
450 450 srcmarkset = set(srcmarks)
451 451 dstmarkset = set(dstmarks)
452 452 bset = srcmarkset | dstmarkset
453 453
454 454 results = ([], [], [], [], [], [], [], [])
455 455 addsrc = results[0].append
456 456 adddst = results[1].append
457 457 advsrc = results[2].append
458 458 advdst = results[3].append
459 459 diverge = results[4].append
460 460 differ = results[5].append
461 461 invalid = results[6].append
462 462 same = results[7].append
463 463
464 464 for b in sorted(bset):
465 465 if b not in srcmarks:
466 466 if b in dstmarks:
467 467 adddst((b, None, dstmarks[b]))
468 468 else:
469 469 invalid((b, None, None))
470 470 elif b not in dstmarks:
471 471 addsrc((b, srcmarks[b], None))
472 472 else:
473 473 scid = srcmarks[b]
474 474 dcid = dstmarks[b]
475 475 if scid == dcid:
476 476 same((b, scid, dcid))
477 477 elif scid in repo and dcid in repo:
478 478 sctx = repo[scid]
479 479 dctx = repo[dcid]
480 480 if sctx.rev() < dctx.rev():
481 481 if validdest(repo, sctx, dctx):
482 482 advdst((b, scid, dcid))
483 483 else:
484 484 diverge((b, scid, dcid))
485 485 else:
486 486 if validdest(repo, dctx, sctx):
487 487 advsrc((b, scid, dcid))
488 488 else:
489 489 diverge((b, scid, dcid))
490 490 else:
491 491 # it is too expensive to examine in detail, in this case
492 492 differ((b, scid, dcid))
493 493
494 494 return results
495 495
496 496 def _diverge(ui, b, path, localmarks, remotenode):
497 497 '''Return appropriate diverged bookmark for specified ``path``
498 498
499 499 This returns None, if it is failed to assign any divergent
500 500 bookmark name.
501 501
502 502 This reuses already existing one with "@number" suffix, if it
503 503 refers ``remotenode``.
504 504 '''
505 505 if b == '@':
506 506 b = ''
507 507 # try to use an @pathalias suffix
508 508 # if an @pathalias already exists, we overwrite (update) it
509 509 if path.startswith("file:"):
510 510 path = util.url(path).path
511 511 for p, u in ui.configitems("paths"):
512 512 if u.startswith("file:"):
513 513 u = util.url(u).path
514 514 if path == u:
515 515 return '%s@%s' % (b, p)
516 516
517 517 # assign a unique "@number" suffix newly
518 518 for x in range(1, 100):
519 519 n = '%s@%d' % (b, x)
520 520 if n not in localmarks or localmarks[n] == remotenode:
521 521 return n
522 522
523 523 return None
524 524
525 525 def unhexlifybookmarks(marks):
526 526 binremotemarks = {}
527 527 for name, node in marks.items():
528 528 binremotemarks[name] = bin(node)
529 529 return binremotemarks
530 530
531 531 _binaryentry = struct.Struct('>20sH')
532 532
533 533 def binaryencode(bookmarks):
534 534 """encode a '(bookmark, node)' iterable into a binary stream
535 535
536 536 the binary format is:
537 537
538 538 <node><bookmark-length><bookmark-name>
539 539
540 540 :node: is a 20 bytes binary node,
541 541 :bookmark-length: an unsigned short,
542 542 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
543 543
544 544 wdirid (all bits set) will be used as a special value for "missing"
545 545 """
546 546 binarydata = []
547 547 for book, node in bookmarks:
548 548 if not node: # None or ''
549 549 node = wdirid
550 550 binarydata.append(_binaryentry.pack(node, len(book)))
551 551 binarydata.append(book)
552 552 return ''.join(binarydata)
553 553
554 554 def binarydecode(stream):
555 555 """decode a binary stream into an '(bookmark, node)' iterable
556 556
557 557 the binary format is:
558 558
559 559 <node><bookmark-length><bookmark-name>
560 560
561 561 :node: is a 20 bytes binary node,
562 562 :bookmark-length: an unsigned short,
563 563 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
564 564
565 565 wdirid (all bits set) will be used as a special value for "missing"
566 566 """
567 567 entrysize = _binaryentry.size
568 568 books = []
569 569 while True:
570 570 entry = stream.read(entrysize)
571 571 if len(entry) < entrysize:
572 572 if entry:
573 573 raise error.Abort(_('bad bookmark stream'))
574 574 break
575 575 node, length = _binaryentry.unpack(entry)
576 576 bookmark = stream.read(length)
577 577 if len(bookmark) < length:
578 578 if entry:
579 579 raise error.Abort(_('bad bookmark stream'))
580 580 if node == wdirid:
581 581 node = None
582 582 books.append((bookmark, node))
583 583 return books
584 584
585 585 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
586 586 ui.debug("checking for updated bookmarks\n")
587 587 localmarks = repo._bookmarks
588 588 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
589 589 ) = comparebookmarks(repo, remotemarks, localmarks)
590 590
591 591 status = ui.status
592 592 warn = ui.warn
593 593 if ui.configbool('ui', 'quietbookmarkmove'):
594 594 status = warn = ui.debug
595 595
596 596 explicit = set(explicit)
597 597 changed = []
598 598 for b, scid, dcid in addsrc:
599 599 if scid in repo: # add remote bookmarks for changes we already have
600 600 changed.append((b, scid, status,
601 601 _("adding remote bookmark %s\n") % (b)))
602 602 elif b in explicit:
603 603 explicit.remove(b)
604 604 ui.warn(_("remote bookmark %s points to locally missing %s\n")
605 605 % (b, hex(scid)[:12]))
606 606
607 607 for b, scid, dcid in advsrc:
608 608 changed.append((b, scid, status,
609 609 _("updating bookmark %s\n") % (b)))
610 610 # remove normal movement from explicit set
611 611 explicit.difference_update(d[0] for d in changed)
612 612
613 613 for b, scid, dcid in diverge:
614 614 if b in explicit:
615 615 explicit.discard(b)
616 616 changed.append((b, scid, status,
617 617 _("importing bookmark %s\n") % (b)))
618 618 else:
619 619 db = _diverge(ui, b, path, localmarks, scid)
620 620 if db:
621 621 changed.append((db, scid, warn,
622 622 _("divergent bookmark %s stored as %s\n") %
623 623 (b, db)))
624 624 else:
625 625 warn(_("warning: failed to assign numbered name "
626 626 "to divergent bookmark %s\n") % (b))
627 627 for b, scid, dcid in adddst + advdst:
628 628 if b in explicit:
629 629 explicit.discard(b)
630 630 changed.append((b, scid, status,
631 631 _("importing bookmark %s\n") % (b)))
632 632 for b, scid, dcid in differ:
633 633 if b in explicit:
634 634 explicit.remove(b)
635 635 ui.warn(_("remote bookmark %s points to locally missing %s\n")
636 636 % (b, hex(scid)[:12]))
637 637
638 638 if changed:
639 639 tr = trfunc()
640 640 changes = []
641 641 for b, node, writer, msg in sorted(changed):
642 642 changes.append((b, node))
643 643 writer(msg)
644 644 localmarks.applychanges(repo, tr, changes)
645 645
646 646 def incoming(ui, repo, other):
647 647 '''Show bookmarks incoming from other to repo
648 648 '''
649 649 ui.status(_("searching for changed bookmarks\n"))
650 650
651 651 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
652 652 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
653 653 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
654 654
655 655 incomings = []
656 656 if ui.debugflag:
657 657 getid = lambda id: id
658 658 else:
659 659 getid = lambda id: id[:12]
660 660 if ui.verbose:
661 661 def add(b, id, st):
662 662 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
663 663 else:
664 664 def add(b, id, st):
665 665 incomings.append(" %-25s %s\n" % (b, getid(id)))
666 666 for b, scid, dcid in addsrc:
667 667 # i18n: "added" refers to a bookmark
668 668 add(b, hex(scid), _('added'))
669 669 for b, scid, dcid in advsrc:
670 670 # i18n: "advanced" refers to a bookmark
671 671 add(b, hex(scid), _('advanced'))
672 672 for b, scid, dcid in diverge:
673 673 # i18n: "diverged" refers to a bookmark
674 674 add(b, hex(scid), _('diverged'))
675 675 for b, scid, dcid in differ:
676 676 # i18n: "changed" refers to a bookmark
677 677 add(b, hex(scid), _('changed'))
678 678
679 679 if not incomings:
680 680 ui.status(_("no changed bookmarks found\n"))
681 681 return 1
682 682
683 683 for s in sorted(incomings):
684 684 ui.write(s)
685 685
686 686 return 0
687 687
688 688 def outgoing(ui, repo, other):
689 689 '''Show bookmarks outgoing from repo to other
690 690 '''
691 691 ui.status(_("searching for changed bookmarks\n"))
692 692
693 693 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
694 694 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
695 695 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
696 696
697 697 outgoings = []
698 698 if ui.debugflag:
699 699 getid = lambda id: id
700 700 else:
701 701 getid = lambda id: id[:12]
702 702 if ui.verbose:
703 703 def add(b, id, st):
704 704 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
705 705 else:
706 706 def add(b, id, st):
707 707 outgoings.append(" %-25s %s\n" % (b, getid(id)))
708 708 for b, scid, dcid in addsrc:
709 709 # i18n: "added refers to a bookmark
710 710 add(b, hex(scid), _('added'))
711 711 for b, scid, dcid in adddst:
712 712 # i18n: "deleted" refers to a bookmark
713 713 add(b, ' ' * 40, _('deleted'))
714 714 for b, scid, dcid in advsrc:
715 715 # i18n: "advanced" refers to a bookmark
716 716 add(b, hex(scid), _('advanced'))
717 717 for b, scid, dcid in diverge:
718 718 # i18n: "diverged" refers to a bookmark
719 719 add(b, hex(scid), _('diverged'))
720 720 for b, scid, dcid in differ:
721 721 # i18n: "changed" refers to a bookmark
722 722 add(b, hex(scid), _('changed'))
723 723
724 724 if not outgoings:
725 725 ui.status(_("no changed bookmarks found\n"))
726 726 return 1
727 727
728 728 for s in sorted(outgoings):
729 729 ui.write(s)
730 730
731 731 return 0
732 732
733 733 def summary(repo, other):
734 734 '''Compare bookmarks between repo and other for "hg summary" output
735 735
736 736 This returns "(# of incoming, # of outgoing)" tuple.
737 737 '''
738 738 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
739 739 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
740 740 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
741 741 return (len(addsrc), len(adddst))
742 742
743 743 def validdest(repo, old, new):
744 744 """Is the new bookmark destination a valid update from the old one"""
745 745 repo = repo.unfiltered()
746 746 if old == new:
747 747 # Old == new -> nothing to update.
748 748 return False
749 749 elif not old:
750 750 # old is nullrev, anything is valid.
751 751 # (new != nullrev has been excluded by the previous check)
752 752 return True
753 753 elif repo.obsstore:
754 754 return new.node() in obsutil.foreground(repo, [old.node()])
755 755 else:
756 756 # still an independent clause as it is lazier (and therefore faster)
757 757 return old.descendant(new)
758 758
759 759 def checkformat(repo, mark):
760 760 """return a valid version of a potential bookmark name
761 761
762 762 Raises an abort error if the bookmark name is not valid.
763 763 """
764 764 mark = mark.strip()
765 765 if not mark:
766 766 raise error.Abort(_("bookmark names cannot consist entirely of "
767 767 "whitespace"))
768 768 scmutil.checknewlabel(repo, mark, 'bookmark')
769 769 return mark
770 770
771 771 def delete(repo, tr, names):
772 772 """remove a mark from the bookmark store
773 773
774 774 Raises an abort error if mark does not exist.
775 775 """
776 776 marks = repo._bookmarks
777 777 changes = []
778 778 for mark in names:
779 779 if mark not in marks:
780 780 raise error.Abort(_("bookmark '%s' does not exist") % mark)
781 781 if mark == repo._activebookmark:
782 782 deactivate(repo)
783 783 changes.append((mark, None))
784 784 marks.applychanges(repo, tr, changes)
785 785
786 786 def rename(repo, tr, old, new, force=False, inactive=False):
787 787 """rename a bookmark from old to new
788 788
789 789 If force is specified, then the new name can overwrite an existing
790 790 bookmark.
791 791
792 792 If inactive is specified, then do not activate the new bookmark.
793 793
794 794 Raises an abort error if old is not in the bookmark store.
795 795 """
796 796 marks = repo._bookmarks
797 797 mark = checkformat(repo, new)
798 798 if old not in marks:
799 799 raise error.Abort(_("bookmark '%s' does not exist") % old)
800 800 changes = []
801 801 for bm in marks.checkconflict(mark, force):
802 802 changes.append((bm, None))
803 803 changes.extend([(mark, marks[old]), (old, None)])
804 804 marks.applychanges(repo, tr, changes)
805 805 if repo._activebookmark == old and not inactive:
806 806 activate(repo, mark)
807 807
808 808 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
809 809 """add a list of bookmarks
810 810
811 811 If force is specified, then the new name can overwrite an existing
812 812 bookmark.
813 813
814 814 If inactive is specified, then do not activate any bookmark. Otherwise, the
815 815 first bookmark is activated.
816 816
817 817 Raises an abort error if old is not in the bookmark store.
818 818 """
819 819 marks = repo._bookmarks
820 820 cur = repo['.'].node()
821 821 newact = None
822 822 changes = []
823 823 hiddenrev = None
824 824
825 825 # unhide revs if any
826 826 if rev:
827 827 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
828 828
829 829 for mark in names:
830 830 mark = checkformat(repo, mark)
831 831 if newact is None:
832 832 newact = mark
833 833 if inactive and mark == repo._activebookmark:
834 834 deactivate(repo)
835 835 return
836 836 tgt = cur
837 837 if rev:
838 838 ctx = scmutil.revsingle(repo, rev)
839 839 if ctx.hidden():
840 840 hiddenrev = ctx.hex()[:12]
841 841 tgt = ctx.node()
842 842 for bm in marks.checkconflict(mark, force, tgt):
843 843 changes.append((bm, None))
844 844 changes.append((mark, tgt))
845 845
846 846 if hiddenrev:
847 847 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
848 848
849 849 if ctx.obsolete():
850 850 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
851 851 repo.ui.warn("(%s)\n" % msg)
852 852
853 853 marks.applychanges(repo, tr, changes)
854 854 if not inactive and cur == marks[newact] and not rev:
855 855 activate(repo, newact)
856 856 elif cur != tgt and newact == repo._activebookmark:
857 857 deactivate(repo)
858 858
859 859 def _printbookmarks(ui, repo, bmarks, **opts):
860 860 """private method to print bookmarks
861 861
862 862 Provides a way for extensions to control how bookmarks are printed (e.g.
863 863 prepend or postpend names)
864 864 """
865 865 opts = pycompat.byteskwargs(opts)
866 866 fm = ui.formatter('bookmarks', opts)
867 867 hexfn = fm.hexfunc
868 868 if len(bmarks) == 0 and fm.isplain():
869 869 ui.status(_("no bookmarks set\n"))
870 870 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
871 871 fm.startitem()
872 872 if not ui.quiet:
873 873 fm.plain(' %s ' % prefix, label=label)
874 874 fm.write('bookmark', '%s', bmark, label=label)
875 875 pad = " " * (25 - encoding.colwidth(bmark))
876 876 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
877 877 repo.changelog.rev(n), hexfn(n), label=label)
878 878 fm.data(active=(activebookmarklabel in label))
879 879 fm.plain('\n')
880 880 fm.end()
881 881
882 882 def printbookmarks(ui, repo, **opts):
883 883 """print bookmarks to a formatter
884 884
885 885 Provides a way for extensions to control how bookmarks are printed.
886 886 """
887 887 marks = repo._bookmarks
888 888 bmarks = {}
889 889 for bmark, n in sorted(marks.iteritems()):
890 890 active = repo._activebookmark
891 891 if bmark == active:
892 892 prefix, label = '*', activebookmarklabel
893 893 else:
894 894 prefix, label = ' ', ''
895 895
896 896 bmarks[bmark] = (n, prefix, label)
897 897 _printbookmarks(ui, repo, bmarks, **opts)
898 898
899 899 def preparehookargs(name, old, new):
900 900 if new is None:
901 901 new = ''
902 902 if old is None:
903 903 old = ''
904 904 return {'bookmark': name,
905 905 'node': hex(new),
906 906 'oldnode': hex(old)}
General Comments 0
You need to be logged in to leave comments. Login now