##// END OF EJS Templates
bookmarks: write bookmarks file deterministically...
Gregory Szorc -
r36471:bfb4494f 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 % 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 for name, node in self.iteritems():
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.changectx('.').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 352 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
353 353 check out 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.changectx('.').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)}
General Comments 0
You need to be logged in to leave comments. Login now