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