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