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