##// END OF EJS Templates
py3: fix exception in pull when several things happen to a bookmark...
Valentin Gatien-Baron -
r45357:f189c528 stable
parent child Browse files
Show More
@@ -1,1059 +1,1060
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 466 def isdivergent(b):
467 467 return b'@' in b and not b.endswith(b'@')
468 468
469 469
470 470 def listbinbookmarks(repo):
471 471 # We may try to list bookmarks on a repo type that does not
472 472 # support it (e.g., statichttprepository).
473 473 marks = getattr(repo, '_bookmarks', {})
474 474
475 475 hasnode = repo.changelog.hasnode
476 476 for k, v in pycompat.iteritems(marks):
477 477 # don't expose local divergent bookmarks
478 478 if hasnode(v) and not isdivergent(k):
479 479 yield k, v
480 480
481 481
482 482 def listbookmarks(repo):
483 483 d = {}
484 484 for book, node in listbinbookmarks(repo):
485 485 d[book] = hex(node)
486 486 return d
487 487
488 488
489 489 def pushbookmark(repo, key, old, new):
490 490 if isdivergent(key):
491 491 return False
492 492 if bookmarksinstore(repo):
493 493 wlock = util.nullcontextmanager()
494 494 else:
495 495 wlock = repo.wlock()
496 496 with wlock, repo.lock(), repo.transaction(b'bookmarks') as tr:
497 497 marks = repo._bookmarks
498 498 existing = hex(marks.get(key, b''))
499 499 if existing != old and existing != new:
500 500 return False
501 501 if new == b'':
502 502 changes = [(key, None)]
503 503 else:
504 504 if new not in repo:
505 505 return False
506 506 changes = [(key, repo[new].node())]
507 507 marks.applychanges(repo, tr, changes)
508 508 return True
509 509
510 510
511 511 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
512 512 '''Compare bookmarks between srcmarks and dstmarks
513 513
514 514 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
515 515 differ, invalid)", each are list of bookmarks below:
516 516
517 517 :addsrc: added on src side (removed on dst side, perhaps)
518 518 :adddst: added on dst side (removed on src side, perhaps)
519 519 :advsrc: advanced on src side
520 520 :advdst: advanced on dst side
521 521 :diverge: diverge
522 522 :differ: changed, but changeset referred on src is unknown on dst
523 523 :invalid: unknown on both side
524 524 :same: same on both side
525 525
526 526 Each elements of lists in result tuple is tuple "(bookmark name,
527 527 changeset ID on source side, changeset ID on destination
528 528 side)". Each changeset ID is a binary node or None.
529 529
530 530 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
531 531 "invalid" list may be unknown for repo.
532 532
533 533 If "targets" is specified, only bookmarks listed in it are
534 534 examined.
535 535 '''
536 536
537 537 if targets:
538 538 bset = set(targets)
539 539 else:
540 540 srcmarkset = set(srcmarks)
541 541 dstmarkset = set(dstmarks)
542 542 bset = srcmarkset | dstmarkset
543 543
544 544 results = ([], [], [], [], [], [], [], [])
545 545 addsrc = results[0].append
546 546 adddst = results[1].append
547 547 advsrc = results[2].append
548 548 advdst = results[3].append
549 549 diverge = results[4].append
550 550 differ = results[5].append
551 551 invalid = results[6].append
552 552 same = results[7].append
553 553
554 554 for b in sorted(bset):
555 555 if b not in srcmarks:
556 556 if b in dstmarks:
557 557 adddst((b, None, dstmarks[b]))
558 558 else:
559 559 invalid((b, None, None))
560 560 elif b not in dstmarks:
561 561 addsrc((b, srcmarks[b], None))
562 562 else:
563 563 scid = srcmarks[b]
564 564 dcid = dstmarks[b]
565 565 if scid == dcid:
566 566 same((b, scid, dcid))
567 567 elif scid in repo and dcid in repo:
568 568 sctx = repo[scid]
569 569 dctx = repo[dcid]
570 570 if sctx.rev() < dctx.rev():
571 571 if validdest(repo, sctx, dctx):
572 572 advdst((b, scid, dcid))
573 573 else:
574 574 diverge((b, scid, dcid))
575 575 else:
576 576 if validdest(repo, dctx, sctx):
577 577 advsrc((b, scid, dcid))
578 578 else:
579 579 diverge((b, scid, dcid))
580 580 else:
581 581 # it is too expensive to examine in detail, in this case
582 582 differ((b, scid, dcid))
583 583
584 584 return results
585 585
586 586
587 587 def _diverge(ui, b, path, localmarks, remotenode):
588 588 '''Return appropriate diverged bookmark for specified ``path``
589 589
590 590 This returns None, if it is failed to assign any divergent
591 591 bookmark name.
592 592
593 593 This reuses already existing one with "@number" suffix, if it
594 594 refers ``remotenode``.
595 595 '''
596 596 if b == b'@':
597 597 b = b''
598 598 # try to use an @pathalias suffix
599 599 # if an @pathalias already exists, we overwrite (update) it
600 600 if path.startswith(b"file:"):
601 601 path = util.url(path).path
602 602 for p, u in ui.configitems(b"paths"):
603 603 if u.startswith(b"file:"):
604 604 u = util.url(u).path
605 605 if path == u:
606 606 return b'%s@%s' % (b, p)
607 607
608 608 # assign a unique "@number" suffix newly
609 609 for x in range(1, 100):
610 610 n = b'%s@%d' % (b, x)
611 611 if n not in localmarks or localmarks[n] == remotenode:
612 612 return n
613 613
614 614 return None
615 615
616 616
617 617 def unhexlifybookmarks(marks):
618 618 binremotemarks = {}
619 619 for name, node in marks.items():
620 620 binremotemarks[name] = bin(node)
621 621 return binremotemarks
622 622
623 623
624 624 _binaryentry = struct.Struct(b'>20sH')
625 625
626 626
627 627 def binaryencode(bookmarks):
628 628 """encode a '(bookmark, node)' iterable into a binary stream
629 629
630 630 the binary format is:
631 631
632 632 <node><bookmark-length><bookmark-name>
633 633
634 634 :node: is a 20 bytes binary node,
635 635 :bookmark-length: an unsigned short,
636 636 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
637 637
638 638 wdirid (all bits set) will be used as a special value for "missing"
639 639 """
640 640 binarydata = []
641 641 for book, node in bookmarks:
642 642 if not node: # None or ''
643 643 node = wdirid
644 644 binarydata.append(_binaryentry.pack(node, len(book)))
645 645 binarydata.append(book)
646 646 return b''.join(binarydata)
647 647
648 648
649 649 def binarydecode(stream):
650 650 """decode a binary stream into an '(bookmark, node)' iterable
651 651
652 652 the binary format is:
653 653
654 654 <node><bookmark-length><bookmark-name>
655 655
656 656 :node: is a 20 bytes binary node,
657 657 :bookmark-length: an unsigned short,
658 658 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
659 659
660 660 wdirid (all bits set) will be used as a special value for "missing"
661 661 """
662 662 entrysize = _binaryentry.size
663 663 books = []
664 664 while True:
665 665 entry = stream.read(entrysize)
666 666 if len(entry) < entrysize:
667 667 if entry:
668 668 raise error.Abort(_(b'bad bookmark stream'))
669 669 break
670 670 node, length = _binaryentry.unpack(entry)
671 671 bookmark = stream.read(length)
672 672 if len(bookmark) < length:
673 673 if entry:
674 674 raise error.Abort(_(b'bad bookmark stream'))
675 675 if node == wdirid:
676 676 node = None
677 677 books.append((bookmark, node))
678 678 return books
679 679
680 680
681 681 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
682 682 ui.debug(b"checking for updated bookmarks\n")
683 683 localmarks = repo._bookmarks
684 684 (
685 685 addsrc,
686 686 adddst,
687 687 advsrc,
688 688 advdst,
689 689 diverge,
690 690 differ,
691 691 invalid,
692 692 same,
693 693 ) = comparebookmarks(repo, remotemarks, localmarks)
694 694
695 695 status = ui.status
696 696 warn = ui.warn
697 697 if ui.configbool(b'ui', b'quietbookmarkmove'):
698 698 status = warn = ui.debug
699 699
700 700 explicit = set(explicit)
701 701 changed = []
702 702 for b, scid, dcid in addsrc:
703 703 if scid in repo: # add remote bookmarks for changes we already have
704 704 changed.append(
705 705 (b, scid, status, _(b"adding remote bookmark %s\n") % b)
706 706 )
707 707 elif b in explicit:
708 708 explicit.remove(b)
709 709 ui.warn(
710 710 _(b"remote bookmark %s points to locally missing %s\n")
711 711 % (b, hex(scid)[:12])
712 712 )
713 713
714 714 for b, scid, dcid in advsrc:
715 715 changed.append((b, scid, status, _(b"updating bookmark %s\n") % b))
716 716 # remove normal movement from explicit set
717 717 explicit.difference_update(d[0] for d in changed)
718 718
719 719 for b, scid, dcid in diverge:
720 720 if b in explicit:
721 721 explicit.discard(b)
722 722 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
723 723 else:
724 724 db = _diverge(ui, b, path, localmarks, scid)
725 725 if db:
726 726 changed.append(
727 727 (
728 728 db,
729 729 scid,
730 730 warn,
731 731 _(b"divergent bookmark %s stored as %s\n") % (b, db),
732 732 )
733 733 )
734 734 else:
735 735 warn(
736 736 _(
737 737 b"warning: failed to assign numbered name "
738 738 b"to divergent bookmark %s\n"
739 739 )
740 740 % b
741 741 )
742 742 for b, scid, dcid in adddst + advdst:
743 743 if b in explicit:
744 744 explicit.discard(b)
745 745 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
746 746 for b, scid, dcid in differ:
747 747 if b in explicit:
748 748 explicit.remove(b)
749 749 ui.warn(
750 750 _(b"remote bookmark %s points to locally missing %s\n")
751 751 % (b, hex(scid)[:12])
752 752 )
753 753
754 754 if changed:
755 755 tr = trfunc()
756 756 changes = []
757 for b, node, writer, msg in sorted(changed):
757 key = lambda t: (t[0], t[1] or b'')
758 for b, node, writer, msg in sorted(changed, key=key):
758 759 changes.append((b, node))
759 760 writer(msg)
760 761 localmarks.applychanges(repo, tr, changes)
761 762
762 763
763 764 def incoming(ui, repo, peer):
764 765 '''Show bookmarks incoming from other to repo
765 766 '''
766 767 ui.status(_(b"searching for changed bookmarks\n"))
767 768
768 769 with peer.commandexecutor() as e:
769 770 remotemarks = unhexlifybookmarks(
770 771 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
771 772 )
772 773
773 774 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
774 775 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
775 776
776 777 incomings = []
777 778 if ui.debugflag:
778 779 getid = lambda id: id
779 780 else:
780 781 getid = lambda id: id[:12]
781 782 if ui.verbose:
782 783
783 784 def add(b, id, st):
784 785 incomings.append(b" %-25s %s %s\n" % (b, getid(id), st))
785 786
786 787 else:
787 788
788 789 def add(b, id, st):
789 790 incomings.append(b" %-25s %s\n" % (b, getid(id)))
790 791
791 792 for b, scid, dcid in addsrc:
792 793 # i18n: "added" refers to a bookmark
793 794 add(b, hex(scid), _(b'added'))
794 795 for b, scid, dcid in advsrc:
795 796 # i18n: "advanced" refers to a bookmark
796 797 add(b, hex(scid), _(b'advanced'))
797 798 for b, scid, dcid in diverge:
798 799 # i18n: "diverged" refers to a bookmark
799 800 add(b, hex(scid), _(b'diverged'))
800 801 for b, scid, dcid in differ:
801 802 # i18n: "changed" refers to a bookmark
802 803 add(b, hex(scid), _(b'changed'))
803 804
804 805 if not incomings:
805 806 ui.status(_(b"no changed bookmarks found\n"))
806 807 return 1
807 808
808 809 for s in sorted(incomings):
809 810 ui.write(s)
810 811
811 812 return 0
812 813
813 814
814 815 def outgoing(ui, repo, other):
815 816 '''Show bookmarks outgoing from repo to other
816 817 '''
817 818 ui.status(_(b"searching for changed bookmarks\n"))
818 819
819 820 remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks'))
820 821 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
821 822 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
822 823
823 824 outgoings = []
824 825 if ui.debugflag:
825 826 getid = lambda id: id
826 827 else:
827 828 getid = lambda id: id[:12]
828 829 if ui.verbose:
829 830
830 831 def add(b, id, st):
831 832 outgoings.append(b" %-25s %s %s\n" % (b, getid(id), st))
832 833
833 834 else:
834 835
835 836 def add(b, id, st):
836 837 outgoings.append(b" %-25s %s\n" % (b, getid(id)))
837 838
838 839 for b, scid, dcid in addsrc:
839 840 # i18n: "added refers to a bookmark
840 841 add(b, hex(scid), _(b'added'))
841 842 for b, scid, dcid in adddst:
842 843 # i18n: "deleted" refers to a bookmark
843 844 add(b, b' ' * 40, _(b'deleted'))
844 845 for b, scid, dcid in advsrc:
845 846 # i18n: "advanced" refers to a bookmark
846 847 add(b, hex(scid), _(b'advanced'))
847 848 for b, scid, dcid in diverge:
848 849 # i18n: "diverged" refers to a bookmark
849 850 add(b, hex(scid), _(b'diverged'))
850 851 for b, scid, dcid in differ:
851 852 # i18n: "changed" refers to a bookmark
852 853 add(b, hex(scid), _(b'changed'))
853 854
854 855 if not outgoings:
855 856 ui.status(_(b"no changed bookmarks found\n"))
856 857 return 1
857 858
858 859 for s in sorted(outgoings):
859 860 ui.write(s)
860 861
861 862 return 0
862 863
863 864
864 865 def summary(repo, peer):
865 866 '''Compare bookmarks between repo and other for "hg summary" output
866 867
867 868 This returns "(# of incoming, # of outgoing)" tuple.
868 869 '''
869 870 with peer.commandexecutor() as e:
870 871 remotemarks = unhexlifybookmarks(
871 872 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
872 873 )
873 874
874 875 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
875 876 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
876 877 return (len(addsrc), len(adddst))
877 878
878 879
879 880 def validdest(repo, old, new):
880 881 """Is the new bookmark destination a valid update from the old one"""
881 882 repo = repo.unfiltered()
882 883 if old == new:
883 884 # Old == new -> nothing to update.
884 885 return False
885 886 elif not old:
886 887 # old is nullrev, anything is valid.
887 888 # (new != nullrev has been excluded by the previous check)
888 889 return True
889 890 elif repo.obsstore:
890 891 return new.node() in obsutil.foreground(repo, [old.node()])
891 892 else:
892 893 # still an independent clause as it is lazier (and therefore faster)
893 894 return old.isancestorof(new)
894 895
895 896
896 897 def checkformat(repo, mark):
897 898 """return a valid version of a potential bookmark name
898 899
899 900 Raises an abort error if the bookmark name is not valid.
900 901 """
901 902 mark = mark.strip()
902 903 if not mark:
903 904 raise error.Abort(
904 905 _(b"bookmark names cannot consist entirely of whitespace")
905 906 )
906 907 scmutil.checknewlabel(repo, mark, b'bookmark')
907 908 return mark
908 909
909 910
910 911 def delete(repo, tr, names):
911 912 """remove a mark from the bookmark store
912 913
913 914 Raises an abort error if mark does not exist.
914 915 """
915 916 marks = repo._bookmarks
916 917 changes = []
917 918 for mark in names:
918 919 if mark not in marks:
919 920 raise error.Abort(_(b"bookmark '%s' does not exist") % mark)
920 921 if mark == repo._activebookmark:
921 922 deactivate(repo)
922 923 changes.append((mark, None))
923 924 marks.applychanges(repo, tr, changes)
924 925
925 926
926 927 def rename(repo, tr, old, new, force=False, inactive=False):
927 928 """rename a bookmark from old to new
928 929
929 930 If force is specified, then the new name can overwrite an existing
930 931 bookmark.
931 932
932 933 If inactive is specified, then do not activate the new bookmark.
933 934
934 935 Raises an abort error if old is not in the bookmark store.
935 936 """
936 937 marks = repo._bookmarks
937 938 mark = checkformat(repo, new)
938 939 if old not in marks:
939 940 raise error.Abort(_(b"bookmark '%s' does not exist") % old)
940 941 changes = []
941 942 for bm in marks.checkconflict(mark, force):
942 943 changes.append((bm, None))
943 944 changes.extend([(mark, marks[old]), (old, None)])
944 945 marks.applychanges(repo, tr, changes)
945 946 if repo._activebookmark == old and not inactive:
946 947 activate(repo, mark)
947 948
948 949
949 950 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
950 951 """add a list of bookmarks
951 952
952 953 If force is specified, then the new name can overwrite an existing
953 954 bookmark.
954 955
955 956 If inactive is specified, then do not activate any bookmark. Otherwise, the
956 957 first bookmark is activated.
957 958
958 959 Raises an abort error if old is not in the bookmark store.
959 960 """
960 961 marks = repo._bookmarks
961 962 cur = repo[b'.'].node()
962 963 newact = None
963 964 changes = []
964 965
965 966 # unhide revs if any
966 967 if rev:
967 968 repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn')
968 969
969 970 ctx = scmutil.revsingle(repo, rev, None)
970 971 # bookmarking wdir means creating a bookmark on p1 and activating it
971 972 activatenew = not inactive and ctx.rev() is None
972 973 if ctx.node() is None:
973 974 ctx = ctx.p1()
974 975 tgt = ctx.node()
975 976 assert tgt
976 977
977 978 for mark in names:
978 979 mark = checkformat(repo, mark)
979 980 if newact is None:
980 981 newact = mark
981 982 if inactive and mark == repo._activebookmark:
982 983 deactivate(repo)
983 984 continue
984 985 for bm in marks.checkconflict(mark, force, tgt):
985 986 changes.append((bm, None))
986 987 changes.append((mark, tgt))
987 988
988 989 # nothing changed but for the one deactivated above
989 990 if not changes:
990 991 return
991 992
992 993 if ctx.hidden():
993 994 repo.ui.warn(_(b"bookmarking hidden changeset %s\n") % ctx.hex()[:12])
994 995
995 996 if ctx.obsolete():
996 997 msg = obsutil._getfilteredreason(repo, ctx.hex()[:12], ctx)
997 998 repo.ui.warn(b"(%s)\n" % msg)
998 999
999 1000 marks.applychanges(repo, tr, changes)
1000 1001 if activatenew and cur == marks[newact]:
1001 1002 activate(repo, newact)
1002 1003 elif cur != tgt and newact == repo._activebookmark:
1003 1004 deactivate(repo)
1004 1005
1005 1006
1006 1007 def _printbookmarks(ui, repo, fm, bmarks):
1007 1008 """private method to print bookmarks
1008 1009
1009 1010 Provides a way for extensions to control how bookmarks are printed (e.g.
1010 1011 prepend or postpend names)
1011 1012 """
1012 1013 hexfn = fm.hexfunc
1013 1014 if len(bmarks) == 0 and fm.isplain():
1014 1015 ui.status(_(b"no bookmarks set\n"))
1015 1016 for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)):
1016 1017 fm.startitem()
1017 1018 fm.context(repo=repo)
1018 1019 if not ui.quiet:
1019 1020 fm.plain(b' %s ' % prefix, label=label)
1020 1021 fm.write(b'bookmark', b'%s', bmark, label=label)
1021 1022 pad = b" " * (25 - encoding.colwidth(bmark))
1022 1023 fm.condwrite(
1023 1024 not ui.quiet,
1024 1025 b'rev node',
1025 1026 pad + b' %d:%s',
1026 1027 repo.changelog.rev(n),
1027 1028 hexfn(n),
1028 1029 label=label,
1029 1030 )
1030 1031 fm.data(active=(activebookmarklabel in label))
1031 1032 fm.plain(b'\n')
1032 1033
1033 1034
1034 1035 def printbookmarks(ui, repo, fm, names=None):
1035 1036 """print bookmarks by the given formatter
1036 1037
1037 1038 Provides a way for extensions to control how bookmarks are printed.
1038 1039 """
1039 1040 marks = repo._bookmarks
1040 1041 bmarks = {}
1041 1042 for bmark in names or marks:
1042 1043 if bmark not in marks:
1043 1044 raise error.Abort(_(b"bookmark '%s' does not exist") % bmark)
1044 1045 active = repo._activebookmark
1045 1046 if bmark == active:
1046 1047 prefix, label = b'*', activebookmarklabel
1047 1048 else:
1048 1049 prefix, label = b' ', b''
1049 1050
1050 1051 bmarks[bmark] = (marks[bmark], prefix, label)
1051 1052 _printbookmarks(ui, repo, fm, bmarks)
1052 1053
1053 1054
1054 1055 def preparehookargs(name, old, new):
1055 1056 if new is None:
1056 1057 new = b''
1057 1058 if old is None:
1058 1059 old = b''
1059 1060 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