##// END OF EJS Templates
bookmarks: directly use base dict 'setitem'...
marmoute -
r32737:d6924192 default
parent child Browse files
Show More
@@ -1,621 +1,622 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
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 hex,
16 16 )
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 lock as lockmod,
21 21 obsolete,
22 22 txnutil,
23 23 util,
24 24 )
25 25
26 26 def _getbkfile(repo):
27 27 """Hook so that extensions that mess with the store can hook bm storage.
28 28
29 29 For core, this just handles wether we should see pending
30 30 bookmarks or the committed ones. Other extensions (like share)
31 31 may need to tweak this behavior further.
32 32 """
33 33 fp, pending = txnutil.trypending(repo.root, repo.vfs, 'bookmarks')
34 34 return fp
35 35
36 36 class bmstore(dict):
37 37 """Storage for bookmarks.
38 38
39 39 This object should do all bookmark-related reads and writes, so
40 40 that it's fairly simple to replace the storage underlying
41 41 bookmarks without having to clone the logic surrounding
42 42 bookmarks. This type also should manage the active bookmark, if
43 43 any.
44 44
45 45 This particular bmstore implementation stores bookmarks as
46 46 {hash}\s{name}\n (the same format as localtags) in
47 47 .hg/bookmarks. The mapping is stored as {name: nodeid}.
48 48 """
49 49
50 50 def __init__(self, repo):
51 51 dict.__init__(self)
52 52 self._repo = repo
53 53 nm = repo.changelog.nodemap
54 54 tonode = bin # force local lookup
55 setitem = dict.__setitem__
55 56 try:
56 57 bkfile = _getbkfile(repo)
57 58 for line in bkfile:
58 59 line = line.strip()
59 60 if not line:
60 61 continue
61 62 try:
62 63 sha, refspec = line.split(' ', 1)
63 64 node = tonode(sha)
64 65 if node in nm:
65 66 refspec = encoding.tolocal(refspec)
66 self[refspec] = node
67 setitem(self, refspec, node)
67 68 except (TypeError, ValueError):
68 69 # - bin(...) can raise TypeError
69 70 # - node in nm can raise ValueError for non-20-bytes entry
70 71 # - split(...) can raise ValueError for string without ' '
71 72 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
72 73 % line)
73 74 except IOError as inst:
74 75 if inst.errno != errno.ENOENT:
75 76 raise
76 77 self._clean = True
77 78 self._active = _readactive(repo, self)
78 79 self._aclean = True
79 80
80 81 @property
81 82 def active(self):
82 83 return self._active
83 84
84 85 @active.setter
85 86 def active(self, mark):
86 87 if mark is not None and mark not in self:
87 88 raise AssertionError('bookmark %s does not exist!' % mark)
88 89
89 90 self._active = mark
90 91 self._aclean = False
91 92
92 93 def __setitem__(self, *args, **kwargs):
93 94 self._clean = False
94 95 return dict.__setitem__(self, *args, **kwargs)
95 96
96 97 def __delitem__(self, key):
97 98 self._clean = False
98 99 return dict.__delitem__(self, key)
99 100
100 101 def recordchange(self, tr):
101 102 """record that bookmarks have been changed in a transaction
102 103
103 104 The transaction is then responsible for updating the file content."""
104 105 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
105 106 location='plain')
106 107 tr.hookargs['bookmark_moved'] = '1'
107 108
108 109 def _writerepo(self, repo):
109 110 """Factored out for extensibility"""
110 111 rbm = repo._bookmarks
111 112 if rbm.active not in self:
112 113 rbm.active = None
113 114 rbm._writeactive()
114 115
115 116 with repo.wlock():
116 117 file_ = repo.vfs('bookmarks', 'w', atomictemp=True,
117 118 checkambig=True)
118 119 try:
119 120 self._write(file_)
120 121 except: # re-raises
121 122 file_.discard()
122 123 raise
123 124 finally:
124 125 file_.close()
125 126
126 127 def _writeactive(self):
127 128 if self._aclean:
128 129 return
129 130 with self._repo.wlock():
130 131 if self._active is not None:
131 132 f = self._repo.vfs('bookmarks.current', 'w', atomictemp=True,
132 133 checkambig=True)
133 134 try:
134 135 f.write(encoding.fromlocal(self._active))
135 136 finally:
136 137 f.close()
137 138 else:
138 139 self._repo.vfs.tryunlink('bookmarks.current')
139 140 self._aclean = True
140 141
141 142 def _write(self, fp):
142 143 for name, node in self.iteritems():
143 144 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
144 145 self._clean = True
145 146 self._repo.invalidatevolatilesets()
146 147
147 148 def expandname(self, bname):
148 149 if bname == '.':
149 150 if self.active:
150 151 return self.active
151 152 else:
152 153 raise error.Abort(_("no active bookmark"))
153 154 return bname
154 155
155 156 def _readactive(repo, marks):
156 157 """
157 158 Get the active bookmark. We can have an active bookmark that updates
158 159 itself as we commit. This function returns the name of that bookmark.
159 160 It is stored in .hg/bookmarks.current
160 161 """
161 162 mark = None
162 163 try:
163 164 file = repo.vfs('bookmarks.current')
164 165 except IOError as inst:
165 166 if inst.errno != errno.ENOENT:
166 167 raise
167 168 return None
168 169 try:
169 170 # No readline() in osutil.posixfile, reading everything is
170 171 # cheap.
171 172 # Note that it's possible for readlines() here to raise
172 173 # IOError, since we might be reading the active mark over
173 174 # static-http which only tries to load the file when we try
174 175 # to read from it.
175 176 mark = encoding.tolocal((file.readlines() or [''])[0])
176 177 if mark == '' or mark not in marks:
177 178 mark = None
178 179 except IOError as inst:
179 180 if inst.errno != errno.ENOENT:
180 181 raise
181 182 return None
182 183 finally:
183 184 file.close()
184 185 return mark
185 186
186 187 def activate(repo, mark):
187 188 """
188 189 Set the given bookmark to be 'active', meaning that this bookmark will
189 190 follow new commits that are made.
190 191 The name is recorded in .hg/bookmarks.current
191 192 """
192 193 repo._bookmarks.active = mark
193 194 repo._bookmarks._writeactive()
194 195
195 196 def deactivate(repo):
196 197 """
197 198 Unset the active bookmark in this repository.
198 199 """
199 200 repo._bookmarks.active = None
200 201 repo._bookmarks._writeactive()
201 202
202 203 def isactivewdirparent(repo):
203 204 """
204 205 Tell whether the 'active' bookmark (the one that follows new commits)
205 206 points to one of the parents of the current working directory (wdir).
206 207
207 208 While this is normally the case, it can on occasion be false; for example,
208 209 immediately after a pull, the active bookmark can be moved to point
209 210 to a place different than the wdir. This is solved by running `hg update`.
210 211 """
211 212 mark = repo._activebookmark
212 213 marks = repo._bookmarks
213 214 parents = [p.node() for p in repo[None].parents()]
214 215 return (mark in marks and marks[mark] in parents)
215 216
216 217 def deletedivergent(repo, deletefrom, bm):
217 218 '''Delete divergent versions of bm on nodes in deletefrom.
218 219
219 220 Return True if at least one bookmark was deleted, False otherwise.'''
220 221 deleted = False
221 222 marks = repo._bookmarks
222 223 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
223 224 for mark in divergent:
224 225 if mark == '@' or '@' not in mark:
225 226 # can't be divergent by definition
226 227 continue
227 228 if mark and marks[mark] in deletefrom:
228 229 if mark != bm:
229 230 del marks[mark]
230 231 deleted = True
231 232 return deleted
232 233
233 234 def headsforactive(repo):
234 235 """Given a repo with an active bookmark, return divergent bookmark nodes.
235 236
236 237 Args:
237 238 repo: A repository with an active bookmark.
238 239
239 240 Returns:
240 241 A list of binary node ids that is the full list of other
241 242 revisions with bookmarks divergent from the active bookmark. If
242 243 there were no divergent bookmarks, then this list will contain
243 244 only one entry.
244 245 """
245 246 if not repo._activebookmark:
246 247 raise ValueError(
247 248 'headsforactive() only makes sense with an active bookmark')
248 249 name = repo._activebookmark.split('@', 1)[0]
249 250 heads = []
250 251 for mark, n in repo._bookmarks.iteritems():
251 252 if mark.split('@', 1)[0] == name:
252 253 heads.append(n)
253 254 return heads
254 255
255 256 def calculateupdate(ui, repo, checkout):
256 257 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
257 258 check out and where to move the active bookmark from, if needed.'''
258 259 movemarkfrom = None
259 260 if checkout is None:
260 261 activemark = repo._activebookmark
261 262 if isactivewdirparent(repo):
262 263 movemarkfrom = repo['.'].node()
263 264 elif activemark:
264 265 ui.status(_("updating to active bookmark %s\n") % activemark)
265 266 checkout = activemark
266 267 return (checkout, movemarkfrom)
267 268
268 269 def update(repo, parents, node):
269 270 deletefrom = parents
270 271 marks = repo._bookmarks
271 272 update = False
272 273 active = marks.active
273 274 if not active:
274 275 return False
275 276
276 277 if marks[active] in parents:
277 278 new = repo[node]
278 279 divs = [repo[b] for b in marks
279 280 if b.split('@', 1)[0] == active.split('@', 1)[0]]
280 281 anc = repo.changelog.ancestors([new.rev()])
281 282 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
282 283 if validdest(repo, repo[marks[active]], new):
283 284 marks[active] = new.node()
284 285 update = True
285 286
286 287 if deletedivergent(repo, deletefrom, active):
287 288 update = True
288 289
289 290 if update:
290 291 lock = tr = None
291 292 try:
292 293 lock = repo.lock()
293 294 tr = repo.transaction('bookmark')
294 295 marks.recordchange(tr)
295 296 tr.close()
296 297 finally:
297 298 lockmod.release(tr, lock)
298 299 return update
299 300
300 301 def listbinbookmarks(repo):
301 302 # We may try to list bookmarks on a repo type that does not
302 303 # support it (e.g., statichttprepository).
303 304 marks = getattr(repo, '_bookmarks', {})
304 305
305 306 hasnode = repo.changelog.hasnode
306 307 for k, v in marks.iteritems():
307 308 # don't expose local divergent bookmarks
308 309 if hasnode(v) and ('@' not in k or k.endswith('@')):
309 310 yield k, v
310 311
311 312 def listbookmarks(repo):
312 313 d = {}
313 314 for book, node in listbinbookmarks(repo):
314 315 d[book] = hex(node)
315 316 return d
316 317
317 318 def pushbookmark(repo, key, old, new):
318 319 w = l = tr = None
319 320 try:
320 321 w = repo.wlock()
321 322 l = repo.lock()
322 323 tr = repo.transaction('bookmarks')
323 324 marks = repo._bookmarks
324 325 existing = hex(marks.get(key, ''))
325 326 if existing != old and existing != new:
326 327 return False
327 328 if new == '':
328 329 del marks[key]
329 330 else:
330 331 if new not in repo:
331 332 return False
332 333 marks[key] = repo[new].node()
333 334 marks.recordchange(tr)
334 335 tr.close()
335 336 return True
336 337 finally:
337 338 lockmod.release(tr, l, w)
338 339
339 340 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
340 341 '''Compare bookmarks between srcmarks and dstmarks
341 342
342 343 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
343 344 differ, invalid)", each are list of bookmarks below:
344 345
345 346 :addsrc: added on src side (removed on dst side, perhaps)
346 347 :adddst: added on dst side (removed on src side, perhaps)
347 348 :advsrc: advanced on src side
348 349 :advdst: advanced on dst side
349 350 :diverge: diverge
350 351 :differ: changed, but changeset referred on src is unknown on dst
351 352 :invalid: unknown on both side
352 353 :same: same on both side
353 354
354 355 Each elements of lists in result tuple is tuple "(bookmark name,
355 356 changeset ID on source side, changeset ID on destination
356 357 side)". Each changeset IDs are 40 hexadecimal digit string or
357 358 None.
358 359
359 360 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
360 361 "invalid" list may be unknown for repo.
361 362
362 363 If "targets" is specified, only bookmarks listed in it are
363 364 examined.
364 365 '''
365 366
366 367 if targets:
367 368 bset = set(targets)
368 369 else:
369 370 srcmarkset = set(srcmarks)
370 371 dstmarkset = set(dstmarks)
371 372 bset = srcmarkset | dstmarkset
372 373
373 374 results = ([], [], [], [], [], [], [], [])
374 375 addsrc = results[0].append
375 376 adddst = results[1].append
376 377 advsrc = results[2].append
377 378 advdst = results[3].append
378 379 diverge = results[4].append
379 380 differ = results[5].append
380 381 invalid = results[6].append
381 382 same = results[7].append
382 383
383 384 for b in sorted(bset):
384 385 if b not in srcmarks:
385 386 if b in dstmarks:
386 387 adddst((b, None, dstmarks[b]))
387 388 else:
388 389 invalid((b, None, None))
389 390 elif b not in dstmarks:
390 391 addsrc((b, srcmarks[b], None))
391 392 else:
392 393 scid = srcmarks[b]
393 394 dcid = dstmarks[b]
394 395 if scid == dcid:
395 396 same((b, scid, dcid))
396 397 elif scid in repo and dcid in repo:
397 398 sctx = repo[scid]
398 399 dctx = repo[dcid]
399 400 if sctx.rev() < dctx.rev():
400 401 if validdest(repo, sctx, dctx):
401 402 advdst((b, scid, dcid))
402 403 else:
403 404 diverge((b, scid, dcid))
404 405 else:
405 406 if validdest(repo, dctx, sctx):
406 407 advsrc((b, scid, dcid))
407 408 else:
408 409 diverge((b, scid, dcid))
409 410 else:
410 411 # it is too expensive to examine in detail, in this case
411 412 differ((b, scid, dcid))
412 413
413 414 return results
414 415
415 416 def _diverge(ui, b, path, localmarks, remotenode):
416 417 '''Return appropriate diverged bookmark for specified ``path``
417 418
418 419 This returns None, if it is failed to assign any divergent
419 420 bookmark name.
420 421
421 422 This reuses already existing one with "@number" suffix, if it
422 423 refers ``remotenode``.
423 424 '''
424 425 if b == '@':
425 426 b = ''
426 427 # try to use an @pathalias suffix
427 428 # if an @pathalias already exists, we overwrite (update) it
428 429 if path.startswith("file:"):
429 430 path = util.url(path).path
430 431 for p, u in ui.configitems("paths"):
431 432 if u.startswith("file:"):
432 433 u = util.url(u).path
433 434 if path == u:
434 435 return '%s@%s' % (b, p)
435 436
436 437 # assign a unique "@number" suffix newly
437 438 for x in range(1, 100):
438 439 n = '%s@%d' % (b, x)
439 440 if n not in localmarks or localmarks[n] == remotenode:
440 441 return n
441 442
442 443 return None
443 444
444 445 def unhexlifybookmarks(marks):
445 446 binremotemarks = {}
446 447 for name, node in marks.items():
447 448 binremotemarks[name] = bin(node)
448 449 return binremotemarks
449 450
450 451 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
451 452 ui.debug("checking for updated bookmarks\n")
452 453 localmarks = repo._bookmarks
453 454 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
454 455 ) = comparebookmarks(repo, remotemarks, localmarks)
455 456
456 457 status = ui.status
457 458 warn = ui.warn
458 459 if ui.configbool('ui', 'quietbookmarkmove', False):
459 460 status = warn = ui.debug
460 461
461 462 explicit = set(explicit)
462 463 changed = []
463 464 for b, scid, dcid in addsrc:
464 465 if scid in repo: # add remote bookmarks for changes we already have
465 466 changed.append((b, scid, status,
466 467 _("adding remote bookmark %s\n") % (b)))
467 468 elif b in explicit:
468 469 explicit.remove(b)
469 470 ui.warn(_("remote bookmark %s points to locally missing %s\n")
470 471 % (b, hex(scid)[:12]))
471 472
472 473 for b, scid, dcid in advsrc:
473 474 changed.append((b, scid, status,
474 475 _("updating bookmark %s\n") % (b)))
475 476 # remove normal movement from explicit set
476 477 explicit.difference_update(d[0] for d in changed)
477 478
478 479 for b, scid, dcid in diverge:
479 480 if b in explicit:
480 481 explicit.discard(b)
481 482 changed.append((b, scid, status,
482 483 _("importing bookmark %s\n") % (b)))
483 484 else:
484 485 db = _diverge(ui, b, path, localmarks, scid)
485 486 if db:
486 487 changed.append((db, scid, warn,
487 488 _("divergent bookmark %s stored as %s\n") %
488 489 (b, db)))
489 490 else:
490 491 warn(_("warning: failed to assign numbered name "
491 492 "to divergent bookmark %s\n") % (b))
492 493 for b, scid, dcid in adddst + advdst:
493 494 if b in explicit:
494 495 explicit.discard(b)
495 496 changed.append((b, scid, status,
496 497 _("importing bookmark %s\n") % (b)))
497 498 for b, scid, dcid in differ:
498 499 if b in explicit:
499 500 explicit.remove(b)
500 501 ui.warn(_("remote bookmark %s points to locally missing %s\n")
501 502 % (b, hex(scid)[:12]))
502 503
503 504 if changed:
504 505 tr = trfunc()
505 506 for b, node, writer, msg in sorted(changed):
506 507 localmarks[b] = node
507 508 writer(msg)
508 509 localmarks.recordchange(tr)
509 510
510 511 def incoming(ui, repo, other):
511 512 '''Show bookmarks incoming from other to repo
512 513 '''
513 514 ui.status(_("searching for changed bookmarks\n"))
514 515
515 516 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
516 517 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
517 518 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
518 519
519 520 incomings = []
520 521 if ui.debugflag:
521 522 getid = lambda id: id
522 523 else:
523 524 getid = lambda id: id[:12]
524 525 if ui.verbose:
525 526 def add(b, id, st):
526 527 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
527 528 else:
528 529 def add(b, id, st):
529 530 incomings.append(" %-25s %s\n" % (b, getid(id)))
530 531 for b, scid, dcid in addsrc:
531 532 # i18n: "added" refers to a bookmark
532 533 add(b, hex(scid), _('added'))
533 534 for b, scid, dcid in advsrc:
534 535 # i18n: "advanced" refers to a bookmark
535 536 add(b, hex(scid), _('advanced'))
536 537 for b, scid, dcid in diverge:
537 538 # i18n: "diverged" refers to a bookmark
538 539 add(b, hex(scid), _('diverged'))
539 540 for b, scid, dcid in differ:
540 541 # i18n: "changed" refers to a bookmark
541 542 add(b, hex(scid), _('changed'))
542 543
543 544 if not incomings:
544 545 ui.status(_("no changed bookmarks found\n"))
545 546 return 1
546 547
547 548 for s in sorted(incomings):
548 549 ui.write(s)
549 550
550 551 return 0
551 552
552 553 def outgoing(ui, repo, other):
553 554 '''Show bookmarks outgoing from repo to other
554 555 '''
555 556 ui.status(_("searching for changed bookmarks\n"))
556 557
557 558 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
558 559 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
559 560 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
560 561
561 562 outgoings = []
562 563 if ui.debugflag:
563 564 getid = lambda id: id
564 565 else:
565 566 getid = lambda id: id[:12]
566 567 if ui.verbose:
567 568 def add(b, id, st):
568 569 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
569 570 else:
570 571 def add(b, id, st):
571 572 outgoings.append(" %-25s %s\n" % (b, getid(id)))
572 573 for b, scid, dcid in addsrc:
573 574 # i18n: "added refers to a bookmark
574 575 add(b, hex(scid), _('added'))
575 576 for b, scid, dcid in adddst:
576 577 # i18n: "deleted" refers to a bookmark
577 578 add(b, ' ' * 40, _('deleted'))
578 579 for b, scid, dcid in advsrc:
579 580 # i18n: "advanced" refers to a bookmark
580 581 add(b, hex(scid), _('advanced'))
581 582 for b, scid, dcid in diverge:
582 583 # i18n: "diverged" refers to a bookmark
583 584 add(b, hex(scid), _('diverged'))
584 585 for b, scid, dcid in differ:
585 586 # i18n: "changed" refers to a bookmark
586 587 add(b, hex(scid), _('changed'))
587 588
588 589 if not outgoings:
589 590 ui.status(_("no changed bookmarks found\n"))
590 591 return 1
591 592
592 593 for s in sorted(outgoings):
593 594 ui.write(s)
594 595
595 596 return 0
596 597
597 598 def summary(repo, other):
598 599 '''Compare bookmarks between repo and other for "hg summary" output
599 600
600 601 This returns "(# of incoming, # of outgoing)" tuple.
601 602 '''
602 603 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
603 604 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
604 605 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
605 606 return (len(addsrc), len(adddst))
606 607
607 608 def validdest(repo, old, new):
608 609 """Is the new bookmark destination a valid update from the old one"""
609 610 repo = repo.unfiltered()
610 611 if old == new:
611 612 # Old == new -> nothing to update.
612 613 return False
613 614 elif not old:
614 615 # old is nullrev, anything is valid.
615 616 # (new != nullrev has been excluded by the previous check)
616 617 return True
617 618 elif repo.obsstore:
618 619 return new.node() in obsolete.foreground(repo, [old.node()])
619 620 else:
620 621 # still an independent clause as it is lazier (and therefore faster)
621 622 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now