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