##// END OF EJS Templates
bookmark: use 'applychanges' for bookmark deletion
Boris Feld -
r33481:67b5f81f default
parent child Browse files
Show More
@@ -1,817 +1,818 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
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 hex,
16 16 short,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 lock as lockmod,
22 22 obsutil,
23 23 pycompat,
24 24 scmutil,
25 25 txnutil,
26 26 util,
27 27 )
28 28
29 29 # label constants
30 30 # until 3.5, bookmarks.current was the advertised name, not
31 31 # bookmarks.active, so we must use both to avoid breaking old
32 32 # custom styles
33 33 activebookmarklabel = 'bookmarks.active bookmarks.current'
34 34
35 35 def _getbkfile(repo):
36 36 """Hook so that extensions that mess with the store can hook bm storage.
37 37
38 38 For core, this just handles wether we should see pending
39 39 bookmarks or the committed ones. Other extensions (like share)
40 40 may need to tweak this behavior further.
41 41 """
42 42 fp, pending = txnutil.trypending(repo.root, repo.vfs, 'bookmarks')
43 43 return fp
44 44
45 45 class bmstore(dict):
46 46 """Storage for bookmarks.
47 47
48 48 This object should do all bookmark-related reads and writes, so
49 49 that it's fairly simple to replace the storage underlying
50 50 bookmarks without having to clone the logic surrounding
51 51 bookmarks. This type also should manage the active bookmark, if
52 52 any.
53 53
54 54 This particular bmstore implementation stores bookmarks as
55 55 {hash}\s{name}\n (the same format as localtags) in
56 56 .hg/bookmarks. The mapping is stored as {name: nodeid}.
57 57 """
58 58
59 59 def __init__(self, repo):
60 60 dict.__init__(self)
61 61 self._repo = repo
62 62 self._clean = True
63 63 self._aclean = True
64 64 nm = repo.changelog.nodemap
65 65 tonode = bin # force local lookup
66 66 setitem = dict.__setitem__
67 67 try:
68 68 with _getbkfile(repo) as bkfile:
69 69 for line in bkfile:
70 70 line = line.strip()
71 71 if not line:
72 72 continue
73 73 try:
74 74 sha, refspec = line.split(' ', 1)
75 75 node = tonode(sha)
76 76 if node in nm:
77 77 refspec = encoding.tolocal(refspec)
78 78 setitem(self, refspec, node)
79 79 except (TypeError, ValueError):
80 80 # TypeError:
81 81 # - bin(...)
82 82 # ValueError:
83 83 # - node in nm, for non-20-bytes entry
84 84 # - split(...), for string without ' '
85 85 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
86 86 % line)
87 87 except IOError as inst:
88 88 if inst.errno != errno.ENOENT:
89 89 raise
90 90 self._active = _readactive(repo, self)
91 91
92 92 @property
93 93 def active(self):
94 94 return self._active
95 95
96 96 @active.setter
97 97 def active(self, mark):
98 98 if mark is not None and mark not in self:
99 99 raise AssertionError('bookmark %s does not exist!' % mark)
100 100
101 101 self._active = mark
102 102 self._aclean = False
103 103
104 104 def __setitem__(self, *args, **kwargs):
105 105 self._clean = False
106 106 return dict.__setitem__(self, *args, **kwargs)
107 107
108 108 def __delitem__(self, key):
109 109 self._clean = False
110 110 return dict.__delitem__(self, key)
111 111
112 112 def applychanges(self, repo, tr, changes):
113 113 """Apply a list of changes to bookmarks
114 114 """
115 115 for name, node in changes:
116 116 if node is None:
117 117 del self[name]
118 118 else:
119 119 self[name] = node
120 120 self.recordchange(tr)
121 121
122 122 def recordchange(self, tr):
123 123 """record that bookmarks have been changed in a transaction
124 124
125 125 The transaction is then responsible for updating the file content."""
126 126 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
127 127 location='plain')
128 128 tr.hookargs['bookmark_moved'] = '1'
129 129
130 130 def _writerepo(self, repo):
131 131 """Factored out for extensibility"""
132 132 rbm = repo._bookmarks
133 133 if rbm.active not in self:
134 134 rbm.active = None
135 135 rbm._writeactive()
136 136
137 137 with repo.wlock():
138 138 file_ = repo.vfs('bookmarks', 'w', atomictemp=True,
139 139 checkambig=True)
140 140 try:
141 141 self._write(file_)
142 142 except: # re-raises
143 143 file_.discard()
144 144 raise
145 145 finally:
146 146 file_.close()
147 147
148 148 def _writeactive(self):
149 149 if self._aclean:
150 150 return
151 151 with self._repo.wlock():
152 152 if self._active is not None:
153 153 f = self._repo.vfs('bookmarks.current', 'w', atomictemp=True,
154 154 checkambig=True)
155 155 try:
156 156 f.write(encoding.fromlocal(self._active))
157 157 finally:
158 158 f.close()
159 159 else:
160 160 self._repo.vfs.tryunlink('bookmarks.current')
161 161 self._aclean = True
162 162
163 163 def _write(self, fp):
164 164 for name, node in self.iteritems():
165 165 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
166 166 self._clean = True
167 167 self._repo.invalidatevolatilesets()
168 168
169 169 def expandname(self, bname):
170 170 if bname == '.':
171 171 if self.active:
172 172 return self.active
173 173 else:
174 174 raise error.Abort(_("no active bookmark"))
175 175 return bname
176 176
177 177 def checkconflict(self, mark, force=False, target=None):
178 178 """check repo for a potential clash of mark with an existing bookmark,
179 179 branch, or hash
180 180
181 181 If target is supplied, then check that we are moving the bookmark
182 182 forward.
183 183
184 184 If force is supplied, then forcibly move the bookmark to a new commit
185 185 regardless if it is a move forward.
186 186 """
187 187 cur = self._repo.changectx('.').node()
188 188 if mark in self and not force:
189 189 if target:
190 190 if self[mark] == target and target == cur:
191 191 # re-activating a bookmark
192 192 return
193 193 rev = self._repo[target].rev()
194 194 anc = self._repo.changelog.ancestors([rev])
195 195 bmctx = self._repo[self[mark]]
196 196 divs = [self._repo[b].node() for b in self
197 197 if b.split('@', 1)[0] == mark.split('@', 1)[0]]
198 198
199 199 # allow resolving a single divergent bookmark even if moving
200 200 # the bookmark across branches when a revision is specified
201 201 # that contains a divergent bookmark
202 202 if bmctx.rev() not in anc and target in divs:
203 203 deletedivergent(self._repo, [target], mark)
204 204 return
205 205
206 206 deletefrom = [b for b in divs
207 207 if self._repo[b].rev() in anc or b == target]
208 208 deletedivergent(self._repo, deletefrom, mark)
209 209 if validdest(self._repo, bmctx, self._repo[target]):
210 210 self._repo.ui.status(
211 211 _("moving bookmark '%s' forward from %s\n") %
212 212 (mark, short(bmctx.node())))
213 213 return
214 214 raise error.Abort(_("bookmark '%s' already exists "
215 215 "(use -f to force)") % mark)
216 216 if ((mark in self._repo.branchmap() or
217 217 mark == self._repo.dirstate.branch()) and not force):
218 218 raise error.Abort(
219 219 _("a bookmark cannot have the name of an existing branch"))
220 220 if len(mark) > 3 and not force:
221 221 try:
222 222 shadowhash = (mark in self._repo)
223 223 except error.LookupError: # ambiguous identifier
224 224 shadowhash = False
225 225 if shadowhash:
226 226 self._repo.ui.warn(
227 227 _("bookmark %s matches a changeset hash\n"
228 228 "(did you leave a -r out of an 'hg bookmark' "
229 229 "command?)\n")
230 230 % mark)
231 231
232 232 def _readactive(repo, marks):
233 233 """
234 234 Get the active bookmark. We can have an active bookmark that updates
235 235 itself as we commit. This function returns the name of that bookmark.
236 236 It is stored in .hg/bookmarks.current
237 237 """
238 238 mark = None
239 239 try:
240 240 file = repo.vfs('bookmarks.current')
241 241 except IOError as inst:
242 242 if inst.errno != errno.ENOENT:
243 243 raise
244 244 return None
245 245 try:
246 246 # No readline() in osutil.posixfile, reading everything is
247 247 # cheap.
248 248 # Note that it's possible for readlines() here to raise
249 249 # IOError, since we might be reading the active mark over
250 250 # static-http which only tries to load the file when we try
251 251 # to read from it.
252 252 mark = encoding.tolocal((file.readlines() or [''])[0])
253 253 if mark == '' or mark not in marks:
254 254 mark = None
255 255 except IOError as inst:
256 256 if inst.errno != errno.ENOENT:
257 257 raise
258 258 return None
259 259 finally:
260 260 file.close()
261 261 return mark
262 262
263 263 def activate(repo, mark):
264 264 """
265 265 Set the given bookmark to be 'active', meaning that this bookmark will
266 266 follow new commits that are made.
267 267 The name is recorded in .hg/bookmarks.current
268 268 """
269 269 repo._bookmarks.active = mark
270 270 repo._bookmarks._writeactive()
271 271
272 272 def deactivate(repo):
273 273 """
274 274 Unset the active bookmark in this repository.
275 275 """
276 276 repo._bookmarks.active = None
277 277 repo._bookmarks._writeactive()
278 278
279 279 def isactivewdirparent(repo):
280 280 """
281 281 Tell whether the 'active' bookmark (the one that follows new commits)
282 282 points to one of the parents of the current working directory (wdir).
283 283
284 284 While this is normally the case, it can on occasion be false; for example,
285 285 immediately after a pull, the active bookmark can be moved to point
286 286 to a place different than the wdir. This is solved by running `hg update`.
287 287 """
288 288 mark = repo._activebookmark
289 289 marks = repo._bookmarks
290 290 parents = [p.node() for p in repo[None].parents()]
291 291 return (mark in marks and marks[mark] in parents)
292 292
293 293 def deletedivergent(repo, deletefrom, bm):
294 294 '''Delete divergent versions of bm on nodes in deletefrom.
295 295
296 296 Return True if at least one bookmark was deleted, False otherwise.'''
297 297 deleted = False
298 298 marks = repo._bookmarks
299 299 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
300 300 for mark in divergent:
301 301 if mark == '@' or '@' not in mark:
302 302 # can't be divergent by definition
303 303 continue
304 304 if mark and marks[mark] in deletefrom:
305 305 if mark != bm:
306 306 del marks[mark]
307 307 deleted = True
308 308 return deleted
309 309
310 310 def headsforactive(repo):
311 311 """Given a repo with an active bookmark, return divergent bookmark nodes.
312 312
313 313 Args:
314 314 repo: A repository with an active bookmark.
315 315
316 316 Returns:
317 317 A list of binary node ids that is the full list of other
318 318 revisions with bookmarks divergent from the active bookmark. If
319 319 there were no divergent bookmarks, then this list will contain
320 320 only one entry.
321 321 """
322 322 if not repo._activebookmark:
323 323 raise ValueError(
324 324 'headsforactive() only makes sense with an active bookmark')
325 325 name = repo._activebookmark.split('@', 1)[0]
326 326 heads = []
327 327 for mark, n in repo._bookmarks.iteritems():
328 328 if mark.split('@', 1)[0] == name:
329 329 heads.append(n)
330 330 return heads
331 331
332 332 def calculateupdate(ui, repo, checkout):
333 333 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
334 334 check out and where to move the active bookmark from, if needed.'''
335 335 movemarkfrom = None
336 336 if checkout is None:
337 337 activemark = repo._activebookmark
338 338 if isactivewdirparent(repo):
339 339 movemarkfrom = repo['.'].node()
340 340 elif activemark:
341 341 ui.status(_("updating to active bookmark %s\n") % activemark)
342 342 checkout = activemark
343 343 return (checkout, movemarkfrom)
344 344
345 345 def update(repo, parents, node):
346 346 deletefrom = parents
347 347 marks = repo._bookmarks
348 348 update = False
349 349 active = marks.active
350 350 if not active:
351 351 return False
352 352
353 353 if marks[active] in parents:
354 354 new = repo[node]
355 355 divs = [repo[b] for b in marks
356 356 if b.split('@', 1)[0] == active.split('@', 1)[0]]
357 357 anc = repo.changelog.ancestors([new.rev()])
358 358 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
359 359 if validdest(repo, repo[marks[active]], new):
360 360 marks[active] = new.node()
361 361 update = True
362 362
363 363 if deletedivergent(repo, deletefrom, active):
364 364 update = True
365 365
366 366 if update:
367 367 lock = tr = None
368 368 try:
369 369 lock = repo.lock()
370 370 tr = repo.transaction('bookmark')
371 371 marks.recordchange(tr)
372 372 tr.close()
373 373 finally:
374 374 lockmod.release(tr, lock)
375 375 return update
376 376
377 377 def listbinbookmarks(repo):
378 378 # We may try to list bookmarks on a repo type that does not
379 379 # support it (e.g., statichttprepository).
380 380 marks = getattr(repo, '_bookmarks', {})
381 381
382 382 hasnode = repo.changelog.hasnode
383 383 for k, v in marks.iteritems():
384 384 # don't expose local divergent bookmarks
385 385 if hasnode(v) and ('@' not in k or k.endswith('@')):
386 386 yield k, v
387 387
388 388 def listbookmarks(repo):
389 389 d = {}
390 390 for book, node in listbinbookmarks(repo):
391 391 d[book] = hex(node)
392 392 return d
393 393
394 394 def pushbookmark(repo, key, old, new):
395 395 w = l = tr = None
396 396 try:
397 397 w = repo.wlock()
398 398 l = repo.lock()
399 399 tr = repo.transaction('bookmarks')
400 400 marks = repo._bookmarks
401 401 existing = hex(marks.get(key, ''))
402 402 if existing != old and existing != new:
403 403 return False
404 404 if new == '':
405 405 del marks[key]
406 406 else:
407 407 if new not in repo:
408 408 return False
409 409 marks[key] = repo[new].node()
410 410 marks.recordchange(tr)
411 411 tr.close()
412 412 return True
413 413 finally:
414 414 lockmod.release(tr, l, w)
415 415
416 416 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
417 417 '''Compare bookmarks between srcmarks and dstmarks
418 418
419 419 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
420 420 differ, invalid)", each are list of bookmarks below:
421 421
422 422 :addsrc: added on src side (removed on dst side, perhaps)
423 423 :adddst: added on dst side (removed on src side, perhaps)
424 424 :advsrc: advanced on src side
425 425 :advdst: advanced on dst side
426 426 :diverge: diverge
427 427 :differ: changed, but changeset referred on src is unknown on dst
428 428 :invalid: unknown on both side
429 429 :same: same on both side
430 430
431 431 Each elements of lists in result tuple is tuple "(bookmark name,
432 432 changeset ID on source side, changeset ID on destination
433 433 side)". Each changeset IDs are 40 hexadecimal digit string or
434 434 None.
435 435
436 436 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
437 437 "invalid" list may be unknown for repo.
438 438
439 439 If "targets" is specified, only bookmarks listed in it are
440 440 examined.
441 441 '''
442 442
443 443 if targets:
444 444 bset = set(targets)
445 445 else:
446 446 srcmarkset = set(srcmarks)
447 447 dstmarkset = set(dstmarks)
448 448 bset = srcmarkset | dstmarkset
449 449
450 450 results = ([], [], [], [], [], [], [], [])
451 451 addsrc = results[0].append
452 452 adddst = results[1].append
453 453 advsrc = results[2].append
454 454 advdst = results[3].append
455 455 diverge = results[4].append
456 456 differ = results[5].append
457 457 invalid = results[6].append
458 458 same = results[7].append
459 459
460 460 for b in sorted(bset):
461 461 if b not in srcmarks:
462 462 if b in dstmarks:
463 463 adddst((b, None, dstmarks[b]))
464 464 else:
465 465 invalid((b, None, None))
466 466 elif b not in dstmarks:
467 467 addsrc((b, srcmarks[b], None))
468 468 else:
469 469 scid = srcmarks[b]
470 470 dcid = dstmarks[b]
471 471 if scid == dcid:
472 472 same((b, scid, dcid))
473 473 elif scid in repo and dcid in repo:
474 474 sctx = repo[scid]
475 475 dctx = repo[dcid]
476 476 if sctx.rev() < dctx.rev():
477 477 if validdest(repo, sctx, dctx):
478 478 advdst((b, scid, dcid))
479 479 else:
480 480 diverge((b, scid, dcid))
481 481 else:
482 482 if validdest(repo, dctx, sctx):
483 483 advsrc((b, scid, dcid))
484 484 else:
485 485 diverge((b, scid, dcid))
486 486 else:
487 487 # it is too expensive to examine in detail, in this case
488 488 differ((b, scid, dcid))
489 489
490 490 return results
491 491
492 492 def _diverge(ui, b, path, localmarks, remotenode):
493 493 '''Return appropriate diverged bookmark for specified ``path``
494 494
495 495 This returns None, if it is failed to assign any divergent
496 496 bookmark name.
497 497
498 498 This reuses already existing one with "@number" suffix, if it
499 499 refers ``remotenode``.
500 500 '''
501 501 if b == '@':
502 502 b = ''
503 503 # try to use an @pathalias suffix
504 504 # if an @pathalias already exists, we overwrite (update) it
505 505 if path.startswith("file:"):
506 506 path = util.url(path).path
507 507 for p, u in ui.configitems("paths"):
508 508 if u.startswith("file:"):
509 509 u = util.url(u).path
510 510 if path == u:
511 511 return '%s@%s' % (b, p)
512 512
513 513 # assign a unique "@number" suffix newly
514 514 for x in range(1, 100):
515 515 n = '%s@%d' % (b, x)
516 516 if n not in localmarks or localmarks[n] == remotenode:
517 517 return n
518 518
519 519 return None
520 520
521 521 def unhexlifybookmarks(marks):
522 522 binremotemarks = {}
523 523 for name, node in marks.items():
524 524 binremotemarks[name] = bin(node)
525 525 return binremotemarks
526 526
527 527 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
528 528 ui.debug("checking for updated bookmarks\n")
529 529 localmarks = repo._bookmarks
530 530 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
531 531 ) = comparebookmarks(repo, remotemarks, localmarks)
532 532
533 533 status = ui.status
534 534 warn = ui.warn
535 535 if ui.configbool('ui', 'quietbookmarkmove', False):
536 536 status = warn = ui.debug
537 537
538 538 explicit = set(explicit)
539 539 changed = []
540 540 for b, scid, dcid in addsrc:
541 541 if scid in repo: # add remote bookmarks for changes we already have
542 542 changed.append((b, scid, status,
543 543 _("adding remote bookmark %s\n") % (b)))
544 544 elif b in explicit:
545 545 explicit.remove(b)
546 546 ui.warn(_("remote bookmark %s points to locally missing %s\n")
547 547 % (b, hex(scid)[:12]))
548 548
549 549 for b, scid, dcid in advsrc:
550 550 changed.append((b, scid, status,
551 551 _("updating bookmark %s\n") % (b)))
552 552 # remove normal movement from explicit set
553 553 explicit.difference_update(d[0] for d in changed)
554 554
555 555 for b, scid, dcid in diverge:
556 556 if b in explicit:
557 557 explicit.discard(b)
558 558 changed.append((b, scid, status,
559 559 _("importing bookmark %s\n") % (b)))
560 560 else:
561 561 db = _diverge(ui, b, path, localmarks, scid)
562 562 if db:
563 563 changed.append((db, scid, warn,
564 564 _("divergent bookmark %s stored as %s\n") %
565 565 (b, db)))
566 566 else:
567 567 warn(_("warning: failed to assign numbered name "
568 568 "to divergent bookmark %s\n") % (b))
569 569 for b, scid, dcid in adddst + advdst:
570 570 if b in explicit:
571 571 explicit.discard(b)
572 572 changed.append((b, scid, status,
573 573 _("importing bookmark %s\n") % (b)))
574 574 for b, scid, dcid in differ:
575 575 if b in explicit:
576 576 explicit.remove(b)
577 577 ui.warn(_("remote bookmark %s points to locally missing %s\n")
578 578 % (b, hex(scid)[:12]))
579 579
580 580 if changed:
581 581 tr = trfunc()
582 582 for b, node, writer, msg in sorted(changed):
583 583 localmarks[b] = node
584 584 writer(msg)
585 585 localmarks.recordchange(tr)
586 586
587 587 def incoming(ui, repo, other):
588 588 '''Show bookmarks incoming from other to repo
589 589 '''
590 590 ui.status(_("searching for changed bookmarks\n"))
591 591
592 592 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
593 593 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
594 594 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
595 595
596 596 incomings = []
597 597 if ui.debugflag:
598 598 getid = lambda id: id
599 599 else:
600 600 getid = lambda id: id[:12]
601 601 if ui.verbose:
602 602 def add(b, id, st):
603 603 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
604 604 else:
605 605 def add(b, id, st):
606 606 incomings.append(" %-25s %s\n" % (b, getid(id)))
607 607 for b, scid, dcid in addsrc:
608 608 # i18n: "added" refers to a bookmark
609 609 add(b, hex(scid), _('added'))
610 610 for b, scid, dcid in advsrc:
611 611 # i18n: "advanced" refers to a bookmark
612 612 add(b, hex(scid), _('advanced'))
613 613 for b, scid, dcid in diverge:
614 614 # i18n: "diverged" refers to a bookmark
615 615 add(b, hex(scid), _('diverged'))
616 616 for b, scid, dcid in differ:
617 617 # i18n: "changed" refers to a bookmark
618 618 add(b, hex(scid), _('changed'))
619 619
620 620 if not incomings:
621 621 ui.status(_("no changed bookmarks found\n"))
622 622 return 1
623 623
624 624 for s in sorted(incomings):
625 625 ui.write(s)
626 626
627 627 return 0
628 628
629 629 def outgoing(ui, repo, other):
630 630 '''Show bookmarks outgoing from repo to other
631 631 '''
632 632 ui.status(_("searching for changed bookmarks\n"))
633 633
634 634 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
635 635 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
636 636 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
637 637
638 638 outgoings = []
639 639 if ui.debugflag:
640 640 getid = lambda id: id
641 641 else:
642 642 getid = lambda id: id[:12]
643 643 if ui.verbose:
644 644 def add(b, id, st):
645 645 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
646 646 else:
647 647 def add(b, id, st):
648 648 outgoings.append(" %-25s %s\n" % (b, getid(id)))
649 649 for b, scid, dcid in addsrc:
650 650 # i18n: "added refers to a bookmark
651 651 add(b, hex(scid), _('added'))
652 652 for b, scid, dcid in adddst:
653 653 # i18n: "deleted" refers to a bookmark
654 654 add(b, ' ' * 40, _('deleted'))
655 655 for b, scid, dcid in advsrc:
656 656 # i18n: "advanced" refers to a bookmark
657 657 add(b, hex(scid), _('advanced'))
658 658 for b, scid, dcid in diverge:
659 659 # i18n: "diverged" refers to a bookmark
660 660 add(b, hex(scid), _('diverged'))
661 661 for b, scid, dcid in differ:
662 662 # i18n: "changed" refers to a bookmark
663 663 add(b, hex(scid), _('changed'))
664 664
665 665 if not outgoings:
666 666 ui.status(_("no changed bookmarks found\n"))
667 667 return 1
668 668
669 669 for s in sorted(outgoings):
670 670 ui.write(s)
671 671
672 672 return 0
673 673
674 674 def summary(repo, other):
675 675 '''Compare bookmarks between repo and other for "hg summary" output
676 676
677 677 This returns "(# of incoming, # of outgoing)" tuple.
678 678 '''
679 679 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
680 680 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
681 681 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
682 682 return (len(addsrc), len(adddst))
683 683
684 684 def validdest(repo, old, new):
685 685 """Is the new bookmark destination a valid update from the old one"""
686 686 repo = repo.unfiltered()
687 687 if old == new:
688 688 # Old == new -> nothing to update.
689 689 return False
690 690 elif not old:
691 691 # old is nullrev, anything is valid.
692 692 # (new != nullrev has been excluded by the previous check)
693 693 return True
694 694 elif repo.obsstore:
695 695 return new.node() in obsutil.foreground(repo, [old.node()])
696 696 else:
697 697 # still an independent clause as it is lazier (and therefore faster)
698 698 return old.descendant(new)
699 699
700 700 def checkformat(repo, mark):
701 701 """return a valid version of a potential bookmark name
702 702
703 703 Raises an abort error if the bookmark name is not valid.
704 704 """
705 705 mark = mark.strip()
706 706 if not mark:
707 707 raise error.Abort(_("bookmark names cannot consist entirely of "
708 708 "whitespace"))
709 709 scmutil.checknewlabel(repo, mark, 'bookmark')
710 710 return mark
711 711
712 712 def delete(repo, tr, names):
713 713 """remove a mark from the bookmark store
714 714
715 715 Raises an abort error if mark does not exist.
716 716 """
717 717 marks = repo._bookmarks
718 changes = []
718 719 for mark in names:
719 720 if mark not in marks:
720 721 raise error.Abort(_("bookmark '%s' does not exist") % mark)
721 722 if mark == repo._activebookmark:
722 723 deactivate(repo)
723 del marks[mark]
724 marks.recordchange(tr)
724 changes.append((mark, None))
725 marks.applychanges(repo, tr, changes)
725 726
726 727 def rename(repo, tr, old, new, force=False, inactive=False):
727 728 """rename a bookmark from old to new
728 729
729 730 If force is specified, then the new name can overwrite an existing
730 731 bookmark.
731 732
732 733 If inactive is specified, then do not activate the new bookmark.
733 734
734 735 Raises an abort error if old is not in the bookmark store.
735 736 """
736 737 marks = repo._bookmarks
737 738 mark = checkformat(repo, new)
738 739 if old not in marks:
739 740 raise error.Abort(_("bookmark '%s' does not exist") % old)
740 741 marks.checkconflict(mark, force)
741 742 marks[mark] = marks[old]
742 743 if repo._activebookmark == old and not inactive:
743 744 activate(repo, mark)
744 745 del marks[old]
745 746 marks.recordchange(tr)
746 747
747 748 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
748 749 """add a list of bookmarks
749 750
750 751 If force is specified, then the new name can overwrite an existing
751 752 bookmark.
752 753
753 754 If inactive is specified, then do not activate any bookmark. Otherwise, the
754 755 first bookmark is activated.
755 756
756 757 Raises an abort error if old is not in the bookmark store.
757 758 """
758 759 marks = repo._bookmarks
759 760 cur = repo.changectx('.').node()
760 761 newact = None
761 762 for mark in names:
762 763 mark = checkformat(repo, mark)
763 764 if newact is None:
764 765 newact = mark
765 766 if inactive and mark == repo._activebookmark:
766 767 deactivate(repo)
767 768 return
768 769 tgt = cur
769 770 if rev:
770 771 tgt = scmutil.revsingle(repo, rev).node()
771 772 marks.checkconflict(mark, force, tgt)
772 773 marks[mark] = tgt
773 774 if not inactive and cur == marks[newact] and not rev:
774 775 activate(repo, newact)
775 776 elif cur != tgt and newact == repo._activebookmark:
776 777 deactivate(repo)
777 778 marks.recordchange(tr)
778 779
779 780 def _printbookmarks(ui, repo, bmarks, **opts):
780 781 """private method to print bookmarks
781 782
782 783 Provides a way for extensions to control how bookmarks are printed (e.g.
783 784 prepend or postpend names)
784 785 """
785 786 opts = pycompat.byteskwargs(opts)
786 787 fm = ui.formatter('bookmarks', opts)
787 788 hexfn = fm.hexfunc
788 789 if len(bmarks) == 0 and fm.isplain():
789 790 ui.status(_("no bookmarks set\n"))
790 791 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
791 792 fm.startitem()
792 793 if not ui.quiet:
793 794 fm.plain(' %s ' % prefix, label=label)
794 795 fm.write('bookmark', '%s', bmark, label=label)
795 796 pad = " " * (25 - encoding.colwidth(bmark))
796 797 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
797 798 repo.changelog.rev(n), hexfn(n), label=label)
798 799 fm.data(active=(activebookmarklabel in label))
799 800 fm.plain('\n')
800 801 fm.end()
801 802
802 803 def printbookmarks(ui, repo, **opts):
803 804 """print bookmarks to a formatter
804 805
805 806 Provides a way for extensions to control how bookmarks are printed.
806 807 """
807 808 marks = repo._bookmarks
808 809 bmarks = {}
809 810 for bmark, n in sorted(marks.iteritems()):
810 811 active = repo._activebookmark
811 812 if bmark == active:
812 813 prefix, label = '*', activebookmarklabel
813 814 else:
814 815 prefix, label = ' ', ''
815 816
816 817 bmarks[bmark] = (n, prefix, label)
817 818 _printbookmarks(ui, repo, bmarks, **opts)
General Comments 0
You need to be logged in to leave comments. Login now