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