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