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