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