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