##// END OF EJS Templates
bookmarks: fix _bookmarks/lookup() reentrancy issue (issue2016)...
Patrick Mezard -
r12392:74129048 stable
parent child Browse files
Show More
@@ -1,543 +1,547 b''
1 1 # Mercurial extension to provide the 'hg bookmark' command
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 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your configuration file::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, hex, short
33 33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 34 import os
35 35
36 36 def write(repo):
37 37 '''Write bookmarks
38 38
39 39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 40 in a format equal to those of localtags.
41 41
42 42 We also store a backup of the previous state in undo.bookmarks that
43 43 can be copied back on rollback.
44 44 '''
45 45 refs = repo._bookmarks
46 46 if os.path.exists(repo.join('bookmarks')):
47 47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 48 if repo._bookmarkcurrent not in refs:
49 49 setcurrent(repo, None)
50 50 wlock = repo.wlock()
51 51 try:
52 52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 53 for refspec, node in refs.iteritems():
54 54 file.write("%s %s\n" % (hex(node), refspec))
55 55 file.rename()
56 56
57 57 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
58 58 try:
59 59 os.utime(repo.sjoin('00changelog.i'), None)
60 60 except OSError:
61 61 pass
62 62
63 63 finally:
64 64 wlock.release()
65 65
66 66 def setcurrent(repo, mark):
67 67 '''Set the name of the bookmark that we are currently on
68 68
69 69 Set the name of the bookmark that we are on (hg update <bookmark>).
70 70 The name is recorded in .hg/bookmarks.current
71 71 '''
72 72 current = repo._bookmarkcurrent
73 73 if current == mark:
74 74 return
75 75
76 76 refs = repo._bookmarks
77 77
78 78 # do not update if we do update to a rev equal to the current bookmark
79 79 if (mark and mark not in refs and
80 80 current and refs[current] == repo.changectx('.').node()):
81 81 return
82 82 if mark not in refs:
83 83 mark = ''
84 84 wlock = repo.wlock()
85 85 try:
86 86 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
87 87 file.write(mark)
88 88 file.rename()
89 89 finally:
90 90 wlock.release()
91 91 repo._bookmarkcurrent = mark
92 92
93 93 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
94 94 '''track a line of development with movable markers
95 95
96 96 Bookmarks are pointers to certain commits that move when
97 97 committing. Bookmarks are local. They can be renamed, copied and
98 98 deleted. It is possible to use bookmark names in :hg:`merge` and
99 99 :hg:`update` to merge and update respectively to a given bookmark.
100 100
101 101 You can use :hg:`bookmark NAME` to set a bookmark on the working
102 102 directory's parent revision with the given name. If you specify
103 103 a revision using -r REV (where REV may be an existing bookmark),
104 104 the bookmark is assigned to that revision.
105 105 '''
106 106 hexfn = ui.debugflag and hex or short
107 107 marks = repo._bookmarks
108 108 cur = repo.changectx('.').node()
109 109
110 110 if rename:
111 111 if rename not in marks:
112 112 raise util.Abort(_("a bookmark of this name does not exist"))
113 113 if mark in marks and not force:
114 114 raise util.Abort(_("a bookmark of the same name already exists"))
115 115 if mark is None:
116 116 raise util.Abort(_("new bookmark name required"))
117 117 marks[mark] = marks[rename]
118 118 del marks[rename]
119 119 if repo._bookmarkcurrent == rename:
120 120 setcurrent(repo, mark)
121 121 write(repo)
122 122 return
123 123
124 124 if delete:
125 125 if mark is None:
126 126 raise util.Abort(_("bookmark name required"))
127 127 if mark not in marks:
128 128 raise util.Abort(_("a bookmark of this name does not exist"))
129 129 if mark == repo._bookmarkcurrent:
130 130 setcurrent(repo, None)
131 131 del marks[mark]
132 132 write(repo)
133 133 return
134 134
135 135 if mark != None:
136 136 if "\n" in mark:
137 137 raise util.Abort(_("bookmark name cannot contain newlines"))
138 138 mark = mark.strip()
139 139 if not mark:
140 140 raise util.Abort(_("bookmark names cannot consist entirely of "
141 141 "whitespace"))
142 142 if mark in marks and not force:
143 143 raise util.Abort(_("a bookmark of the same name already exists"))
144 144 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
145 145 and not force):
146 146 raise util.Abort(
147 147 _("a bookmark cannot have the name of an existing branch"))
148 148 if rev:
149 149 marks[mark] = repo.lookup(rev)
150 150 else:
151 151 marks[mark] = repo.changectx('.').node()
152 152 setcurrent(repo, mark)
153 153 write(repo)
154 154 return
155 155
156 156 if mark is None:
157 157 if rev:
158 158 raise util.Abort(_("bookmark name required"))
159 159 if len(marks) == 0:
160 160 ui.status(_("no bookmarks set\n"))
161 161 else:
162 162 for bmark, n in marks.iteritems():
163 163 if ui.configbool('bookmarks', 'track.current'):
164 164 current = repo._bookmarkcurrent
165 165 if bmark == current and n == cur:
166 166 prefix, label = '*', 'bookmarks.current'
167 167 else:
168 168 prefix, label = ' ', ''
169 169 else:
170 170 if n == cur:
171 171 prefix, label = '*', 'bookmarks.current'
172 172 else:
173 173 prefix, label = ' ', ''
174 174
175 175 if ui.quiet:
176 176 ui.write("%s\n" % bmark, label=label)
177 177 else:
178 178 ui.write(" %s %-25s %d:%s\n" % (
179 179 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
180 180 label=label)
181 181 return
182 182
183 183 def _revstostrip(changelog, node):
184 184 srev = changelog.rev(node)
185 185 tostrip = [srev]
186 186 saveheads = []
187 187 for r in xrange(srev, len(changelog)):
188 188 parents = changelog.parentrevs(r)
189 189 if parents[0] in tostrip or parents[1] in tostrip:
190 190 tostrip.append(r)
191 191 if parents[1] != nullrev:
192 192 for p in parents:
193 193 if p not in tostrip and p > srev:
194 194 saveheads.append(p)
195 195 return [r for r in tostrip if r not in saveheads]
196 196
197 197 def strip(oldstrip, ui, repo, node, backup="all"):
198 198 """Strip bookmarks if revisions are stripped using
199 199 the mercurial.strip method. This usually happens during
200 200 qpush and qpop"""
201 201 revisions = _revstostrip(repo.changelog, node)
202 202 marks = repo._bookmarks
203 203 update = []
204 204 for mark, n in marks.iteritems():
205 205 if repo.changelog.rev(n) in revisions:
206 206 update.append(mark)
207 207 oldstrip(ui, repo, node, backup)
208 208 if len(update) > 0:
209 209 for m in update:
210 210 marks[m] = repo.changectx('.').node()
211 211 write(repo)
212 212
213 213 def reposetup(ui, repo):
214 214 if not repo.local():
215 215 return
216 216
217 217 class bookmark_repo(repo.__class__):
218 218
219 219 @util.propertycache
220 220 def _bookmarks(self):
221 221 '''Parse .hg/bookmarks file and return a dictionary
222 222
223 223 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
224 224 in the .hg/bookmarks file.
225 225 Read the file and return a (name=>nodeid) dictionary
226 226 '''
227 self._loadingbookmarks = True
227 228 try:
228 229 bookmarks = {}
229 230 for line in self.opener('bookmarks'):
230 231 sha, refspec = line.strip().split(' ', 1)
231 232 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
232 233 except:
233 234 pass
235 self._loadingbookmarks = False
234 236 return bookmarks
235 237
236 238 @util.propertycache
237 239 def _bookmarkcurrent(self):
238 240 '''Get the current bookmark
239 241
240 242 If we use gittishsh branches we have a current bookmark that
241 243 we are on. This function returns the name of the bookmark. It
242 244 is stored in .hg/bookmarks.current
243 245 '''
244 246 mark = None
245 247 if os.path.exists(self.join('bookmarks.current')):
246 248 file = self.opener('bookmarks.current')
247 249 # No readline() in posixfile_nt, reading everything is cheap
248 250 mark = (file.readlines() or [''])[0]
249 251 if mark == '':
250 252 mark = None
251 253 file.close()
252 254 return mark
253 255
254 256 def rollback(self, *args):
255 257 if os.path.exists(self.join('undo.bookmarks')):
256 258 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
257 259 return super(bookmark_repo, self).rollback(*args)
258 260
259 261 def lookup(self, key):
262 if not getattr(self, '_loadingbookmarks', False):
260 263 if key in self._bookmarks:
261 264 key = self._bookmarks[key]
262 265 return super(bookmark_repo, self).lookup(key)
263 266
264 267 def _bookmarksupdate(self, parents, node):
265 268 marks = self._bookmarks
266 269 update = False
267 270 if ui.configbool('bookmarks', 'track.current'):
268 271 mark = self._bookmarkcurrent
269 272 if mark and marks[mark] in parents:
270 273 marks[mark] = node
271 274 update = True
272 275 else:
273 276 for mark, n in marks.items():
274 277 if n in parents:
275 278 marks[mark] = node
276 279 update = True
277 280 if update:
278 281 write(self)
279 282
280 283 def commitctx(self, ctx, error=False):
281 284 """Add a revision to the repository and
282 285 move the bookmark"""
283 286 wlock = self.wlock() # do both commit and bookmark with lock held
284 287 try:
285 288 node = super(bookmark_repo, self).commitctx(ctx, error)
286 289 if node is None:
287 290 return None
288 291 parents = self.changelog.parents(node)
289 292 if parents[1] == nullid:
290 293 parents = (parents[0],)
291 294
292 295 self._bookmarksupdate(parents, node)
293 296 return node
294 297 finally:
295 298 wlock.release()
296 299
297 300 def pull(self, remote, heads=None, force=False):
298 301 result = super(bookmark_repo, self).pull(remote, heads, force)
299 302
300 303 self.ui.debug("checking for updated bookmarks\n")
301 304 rb = remote.listkeys('bookmarks')
302 305 changes = 0
303 306 for k in rb.keys():
304 307 if k in self._bookmarks:
305 308 nr, nl = rb[k], self._bookmarks[k]
306 309 if nr in self:
307 310 cr = self[nr]
308 311 cl = self[nl]
309 312 if cl.rev() >= cr.rev():
310 313 continue
311 314 if cr in cl.descendants():
312 315 self._bookmarks[k] = cr.node()
313 316 changes += 1
314 317 self.ui.status(_("updating bookmark %s\n") % k)
315 318 else:
316 319 self.ui.warn(_("not updating divergent"
317 320 " bookmark %s\n") % k)
318 321 if changes:
319 322 write(repo)
320 323
321 324 return result
322 325
323 326 def push(self, remote, force=False, revs=None, newbranch=False):
324 327 result = super(bookmark_repo, self).push(remote, force, revs,
325 328 newbranch)
326 329
327 330 self.ui.debug("checking for updated bookmarks\n")
328 331 rb = remote.listkeys('bookmarks')
329 332 for k in rb.keys():
330 333 if k in self._bookmarks:
331 334 nr, nl = rb[k], self._bookmarks[k]
332 335 if nr in self:
333 336 cr = self[nr]
334 337 cl = self[nl]
335 338 if cl in cr.descendants():
336 339 r = remote.pushkey('bookmarks', k, nr, nl)
337 340 if r:
338 341 self.ui.status(_("updating bookmark %s\n") % k)
339 342 else:
340 343 self.ui.warn(_('updating bookmark %s'
341 344 ' failed!\n') % k)
342 345
343 346 return result
344 347
345 348 def addchangegroup(self, *args, **kwargs):
346 349 parents = self.dirstate.parents()
347 350
348 351 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
349 352 if result > 1:
350 353 # We have more heads than before
351 354 return result
352 355 node = self.changelog.tip()
353 356
354 357 self._bookmarksupdate(parents, node)
355 358 return result
356 359
357 360 def _findtags(self):
358 361 """Merge bookmarks with normal tags"""
359 362 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
363 if not getattr(self, '_loadingbookmarks', False):
360 364 tags.update(self._bookmarks)
361 365 return (tags, tagtypes)
362 366
363 367 if hasattr(repo, 'invalidate'):
364 368 def invalidate(self):
365 369 super(bookmark_repo, self).invalidate()
366 370 for attr in ('_bookmarks', '_bookmarkcurrent'):
367 371 if attr in self.__dict__:
368 372 delattr(self, attr)
369 373
370 374 repo.__class__ = bookmark_repo
371 375
372 376 def listbookmarks(repo):
373 377 # We may try to list bookmarks on a repo type that does not
374 378 # support it (e.g., statichttprepository).
375 379 if not hasattr(repo, '_bookmarks'):
376 380 return {}
377 381
378 382 d = {}
379 383 for k, v in repo._bookmarks.iteritems():
380 384 d[k] = hex(v)
381 385 return d
382 386
383 387 def pushbookmark(repo, key, old, new):
384 388 w = repo.wlock()
385 389 try:
386 390 marks = repo._bookmarks
387 391 if hex(marks.get(key, '')) != old:
388 392 return False
389 393 if new == '':
390 394 del marks[key]
391 395 else:
392 396 if new not in repo:
393 397 return False
394 398 marks[key] = repo[new].node()
395 399 write(repo)
396 400 return True
397 401 finally:
398 402 w.release()
399 403
400 404 def pull(oldpull, ui, repo, source="default", **opts):
401 405 # translate bookmark args to rev args for actual pull
402 406 if opts.get('bookmark'):
403 407 # this is an unpleasant hack as pull will do this internally
404 408 source, branches = hg.parseurl(ui.expandpath(source),
405 409 opts.get('branch'))
406 410 other = hg.repository(hg.remoteui(repo, opts), source)
407 411 rb = other.listkeys('bookmarks')
408 412
409 413 for b in opts['bookmark']:
410 414 if b not in rb:
411 415 raise util.Abort(_('remote bookmark %s not found!') % b)
412 416 opts.setdefault('rev', []).append(b)
413 417
414 418 result = oldpull(ui, repo, source, **opts)
415 419
416 420 # update specified bookmarks
417 421 if opts.get('bookmark'):
418 422 for b in opts['bookmark']:
419 423 # explicit pull overrides local bookmark if any
420 424 ui.status(_("importing bookmark %s\n") % b)
421 425 repo._bookmarks[b] = repo[rb[b]].node()
422 426 write(repo)
423 427
424 428 return result
425 429
426 430 def push(oldpush, ui, repo, dest=None, **opts):
427 431 dopush = True
428 432 if opts.get('bookmark'):
429 433 dopush = False
430 434 for b in opts['bookmark']:
431 435 if b in repo._bookmarks:
432 436 dopush = True
433 437 opts.setdefault('rev', []).append(b)
434 438
435 439 result = 0
436 440 if dopush:
437 441 result = oldpush(ui, repo, dest, **opts)
438 442
439 443 if opts.get('bookmark'):
440 444 # this is an unpleasant hack as push will do this internally
441 445 dest = ui.expandpath(dest or 'default-push', dest or 'default')
442 446 dest, branches = hg.parseurl(dest, opts.get('branch'))
443 447 other = hg.repository(hg.remoteui(repo, opts), dest)
444 448 rb = other.listkeys('bookmarks')
445 449 for b in opts['bookmark']:
446 450 # explicit push overrides remote bookmark if any
447 451 if b in repo._bookmarks:
448 452 ui.status(_("exporting bookmark %s\n") % b)
449 453 new = repo[b].hex()
450 454 elif b in rb:
451 455 ui.status(_("deleting remote bookmark %s\n") % b)
452 456 new = '' # delete
453 457 else:
454 458 ui.warn(_('bookmark %s does not exist on the local '
455 459 'or remote repository!\n') % b)
456 460 return 2
457 461 old = rb.get(b, '')
458 462 r = other.pushkey('bookmarks', b, old, new)
459 463 if not r:
460 464 ui.warn(_('updating bookmark %s failed!\n') % b)
461 465 if not result:
462 466 result = 2
463 467
464 468 return result
465 469
466 470 def diffbookmarks(ui, repo, remote):
467 471 ui.status(_("searching for changes\n"))
468 472
469 473 lmarks = repo.listkeys('bookmarks')
470 474 rmarks = remote.listkeys('bookmarks')
471 475
472 476 diff = set(rmarks) - set(lmarks)
473 477 for k in diff:
474 478 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
475 479
476 480 if len(diff) <= 0:
477 481 ui.status(_("no changes found\n"))
478 482 return 1
479 483 return 0
480 484
481 485 def incoming(oldincoming, ui, repo, source="default", **opts):
482 486 if opts.get('bookmarks'):
483 487 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
484 488 other = hg.repository(hg.remoteui(repo, opts), source)
485 489 ui.status(_('comparing with %s\n') % url.hidepassword(source))
486 490 return diffbookmarks(ui, repo, other)
487 491 else:
488 492 return oldincoming(ui, repo, source, **opts)
489 493
490 494 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
491 495 if opts.get('bookmarks'):
492 496 dest = ui.expandpath(dest or 'default-push', dest or 'default')
493 497 dest, branches = hg.parseurl(dest, opts.get('branch'))
494 498 other = hg.repository(hg.remoteui(repo, opts), dest)
495 499 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
496 500 return diffbookmarks(ui, other, repo)
497 501 else:
498 502 return oldoutgoing(ui, repo, dest, **opts)
499 503
500 504 def uisetup(ui):
501 505 extensions.wrapfunction(repair, "strip", strip)
502 506 if ui.configbool('bookmarks', 'track.current'):
503 507 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
504 508
505 509 entry = extensions.wrapcommand(commands.table, 'pull', pull)
506 510 entry[1].append(('B', 'bookmark', [],
507 511 _("bookmark to import")))
508 512 entry = extensions.wrapcommand(commands.table, 'push', push)
509 513 entry[1].append(('B', 'bookmark', [],
510 514 _("bookmark to export")))
511 515 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
512 516 entry[1].append(('B', 'bookmarks', False,
513 517 _("compare bookmark")))
514 518 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
515 519 entry[1].append(('B', 'bookmarks', False,
516 520 _("compare bookmark")))
517 521
518 522 pushkey.register('bookmarks', pushbookmark, listbookmarks)
519 523
520 524 def updatecurbookmark(orig, ui, repo, *args, **opts):
521 525 '''Set the current bookmark
522 526
523 527 If the user updates to a bookmark we update the .hg/bookmarks.current
524 528 file.
525 529 '''
526 530 res = orig(ui, repo, *args, **opts)
527 531 rev = opts['rev']
528 532 if not rev and len(args) > 0:
529 533 rev = args[0]
530 534 setcurrent(repo, rev)
531 535 return res
532 536
533 537 cmdtable = {
534 538 "bookmarks":
535 539 (bookmark,
536 540 [('f', 'force', False, _('force')),
537 541 ('r', 'rev', '', _('revision'), _('REV')),
538 542 ('d', 'delete', False, _('delete a given bookmark')),
539 543 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
540 544 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
541 545 }
542 546
543 547 colortable = {'bookmarks.current': 'green'}
@@ -1,45 +1,60 b''
1 1 #!/bin/sh
2 2
3 3 . $TESTDIR/helpers.sh
4 4 echo "[extensions]" >> $HGRCPATH
5 5 echo "bookmarks=" >> $HGRCPATH
6 6 echo "mq=" >> $HGRCPATH
7 7
8 8 hg init
9 9
10 10 echo qqq>qqq.txt
11 11
12 12 echo % add file
13 13 hg add
14 14
15 15 echo % commit first revision
16 16 hg ci -m 1 -u user -d "1 0"
17 17
18 18 echo % set bookmark
19 19 hg book test
20 20
21 21 echo www>>qqq.txt
22 22
23 23 echo % commit second revision
24 24 hg ci -m 2 -u usr -d "1 0"
25 25
26 26 echo % set bookmark
27 27 hg book test2
28 28
29 29 echo % update to -2
30 30 hg update -r -2
31 31
32 32 echo eee>>qqq.txt
33 33
34 34 echo % commit new head
35 35 hg ci -m 3 -u user -d "1 0"
36 36
37 37 echo % bookmarks updated?
38 38 hg book
39 39
40 40 echo % strip to revision 1
41 41 hg strip 1 | hidebackup
42 42
43 43 echo % list bookmarks
44 44 hg book
45 45
46 echo '% test immediate rollback and reentrancy issue'
47 echo "mq=!" >> $HGRCPATH
48 hg init repo
49 cd repo
50 echo a > a
51 hg ci -Am adda
52 echo b > b
53 hg ci -Am addb
54 hg bookmarks markb
55 hg rollback
56 hg bookmarks
57 hg bookmarks markb
58 hg bookmarks
59 cd ..
60
@@ -1,18 +1,24 b''
1 1 % add file
2 2 adding qqq.txt
3 3 % commit first revision
4 4 % set bookmark
5 5 % commit second revision
6 6 % set bookmark
7 7 % update to -2
8 8 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
9 9 % commit new head
10 10 created new head
11 11 % bookmarks updated?
12 12 test 1:16b24da7e457
13 13 test2 1:16b24da7e457
14 14 % strip to revision 1
15 15 saved backup bundle to
16 16 % list bookmarks
17 17 * test 1:9f1b7e78eff8
18 18 * test2 1:9f1b7e78eff8
19 % test immediate rollback and reentrancy issue
20 adding a
21 adding b
22 rolling back to revision 0 (undo commit)
23 no bookmarks set
24 * markb 0:07f494440405
General Comments 0
You need to be logged in to leave comments. Login now