##// END OF EJS Templates
bookmarks: guard against listing bookmarks on unsupported repos...
Martin Geisler -
r12026:19389543 stable
parent child Browse files
Show More
@@ -1,537 +1,542 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 .hgrc::
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 227 try:
228 228 bookmarks = {}
229 229 for line in self.opener('bookmarks'):
230 230 sha, refspec = line.strip().split(' ', 1)
231 231 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
232 232 except:
233 233 pass
234 234 return bookmarks
235 235
236 236 @util.propertycache
237 237 def _bookmarkcurrent(self):
238 238 '''Get the current bookmark
239 239
240 240 If we use gittishsh branches we have a current bookmark that
241 241 we are on. This function returns the name of the bookmark. It
242 242 is stored in .hg/bookmarks.current
243 243 '''
244 244 mark = None
245 245 if os.path.exists(self.join('bookmarks.current')):
246 246 file = self.opener('bookmarks.current')
247 247 # No readline() in posixfile_nt, reading everything is cheap
248 248 mark = (file.readlines() or [''])[0]
249 249 if mark == '':
250 250 mark = None
251 251 file.close()
252 252 return mark
253 253
254 254 def rollback(self, *args):
255 255 if os.path.exists(self.join('undo.bookmarks')):
256 256 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
257 257 return super(bookmark_repo, self).rollback(*args)
258 258
259 259 def lookup(self, key):
260 260 if key in self._bookmarks:
261 261 key = self._bookmarks[key]
262 262 return super(bookmark_repo, self).lookup(key)
263 263
264 264 def _bookmarksupdate(self, parents, node):
265 265 marks = self._bookmarks
266 266 update = False
267 267 if ui.configbool('bookmarks', 'track.current'):
268 268 mark = self._bookmarkcurrent
269 269 if mark and marks[mark] in parents:
270 270 marks[mark] = node
271 271 update = True
272 272 else:
273 273 for mark, n in marks.items():
274 274 if n in parents:
275 275 marks[mark] = node
276 276 update = True
277 277 if update:
278 278 write(self)
279 279
280 280 def commitctx(self, ctx, error=False):
281 281 """Add a revision to the repository and
282 282 move the bookmark"""
283 283 wlock = self.wlock() # do both commit and bookmark with lock held
284 284 try:
285 285 node = super(bookmark_repo, self).commitctx(ctx, error)
286 286 if node is None:
287 287 return None
288 288 parents = self.changelog.parents(node)
289 289 if parents[1] == nullid:
290 290 parents = (parents[0],)
291 291
292 292 self._bookmarksupdate(parents, node)
293 293 return node
294 294 finally:
295 295 wlock.release()
296 296
297 297 def pull(self, remote, heads=None, force=False):
298 298 result = super(bookmark_repo, self).pull(remote, heads, force)
299 299
300 300 self.ui.debug("checking for updated bookmarks\n")
301 301 rb = remote.listkeys('bookmarks')
302 302 changes = 0
303 303 for k in rb.keys():
304 304 if k in self._bookmarks:
305 305 nr, nl = rb[k], self._bookmarks[k]
306 306 if nr in self:
307 307 cr = self[nr]
308 308 cl = self[nl]
309 309 if cl.rev() >= cr.rev():
310 310 continue
311 311 if cr in cl.descendants():
312 312 self._bookmarks[k] = cr.node()
313 313 changes += 1
314 314 self.ui.status(_("updating bookmark %s\n") % k)
315 315 else:
316 316 self.ui.warn(_("not updating divergent"
317 317 " bookmark %s\n") % k)
318 318 if changes:
319 319 write(repo)
320 320
321 321 return result
322 322
323 323 def push(self, remote, force=False, revs=None, newbranch=False):
324 324 result = super(bookmark_repo, self).push(remote, force, revs,
325 325 newbranch)
326 326
327 327 self.ui.debug("checking for updated bookmarks\n")
328 328 rb = remote.listkeys('bookmarks')
329 329 for k in rb.keys():
330 330 if k in self._bookmarks:
331 331 nr, nl = rb[k], self._bookmarks[k]
332 332 if nr in self:
333 333 cr = self[nr]
334 334 cl = self[nl]
335 335 if cl in cr.descendants():
336 336 r = remote.pushkey('bookmarks', k, nr, nl)
337 337 if r:
338 338 self.ui.status(_("updating bookmark %s\n") % k)
339 339 else:
340 340 self.ui.warn(_('updating bookmark %s'
341 341 ' failed!\n') % k)
342 342
343 343 return result
344 344
345 345 def addchangegroup(self, *args, **kwargs):
346 346 parents = self.dirstate.parents()
347 347
348 348 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
349 349 if result > 1:
350 350 # We have more heads than before
351 351 return result
352 352 node = self.changelog.tip()
353 353
354 354 self._bookmarksupdate(parents, node)
355 355 return result
356 356
357 357 def _findtags(self):
358 358 """Merge bookmarks with normal tags"""
359 359 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
360 360 tags.update(self._bookmarks)
361 361 return (tags, tagtypes)
362 362
363 363 if hasattr(repo, 'invalidate'):
364 364 def invalidate(self):
365 365 super(bookmark_repo, self).invalidate()
366 366 for attr in ('_bookmarks', '_bookmarkcurrent'):
367 367 if attr in self.__dict__:
368 368 delattr(self, attr)
369 369
370 370 repo.__class__ = bookmark_repo
371 371
372 372 def listbookmarks(repo):
373 # We may try to list bookmarks on a repo type that does not
374 # support it (e.g., statichttprepository).
375 if not hasattr(repo, '_bookmarks'):
376 return {}
377
373 378 d = {}
374 379 for k, v in repo._bookmarks.iteritems():
375 380 d[k] = hex(v)
376 381 return d
377 382
378 383 def pushbookmark(repo, key, old, new):
379 384 w = repo.wlock()
380 385 try:
381 386 marks = repo._bookmarks
382 387 if hex(marks.get(key, '')) != old:
383 388 return False
384 389 if new == '':
385 390 del marks[key]
386 391 else:
387 392 if new not in repo:
388 393 return False
389 394 marks[key] = repo[new].node()
390 395 write(repo)
391 396 return True
392 397 finally:
393 398 w.release()
394 399
395 400 def pull(oldpull, ui, repo, source="default", **opts):
396 401 # translate bookmark args to rev args for actual pull
397 402 if opts.get('bookmark'):
398 403 # this is an unpleasant hack as pull will do this internally
399 404 source, branches = hg.parseurl(ui.expandpath(source),
400 405 opts.get('branch'))
401 406 other = hg.repository(hg.remoteui(repo, opts), source)
402 407 rb = other.listkeys('bookmarks')
403 408
404 409 for b in opts['bookmark']:
405 410 if b not in rb:
406 411 raise util.Abort(_('remote bookmark %s not found!') % b)
407 412 opts.setdefault('rev', []).append(b)
408 413
409 414 result = oldpull(ui, repo, source, **opts)
410 415
411 416 # update specified bookmarks
412 417 if opts.get('bookmark'):
413 418 for b in opts['bookmark']:
414 419 # explicit pull overrides local bookmark if any
415 420 ui.status(_("importing bookmark %s\n") % b)
416 421 repo._bookmarks[b] = repo[rb[b]].node()
417 422 write(repo)
418 423
419 424 return result
420 425
421 426 def push(oldpush, ui, repo, dest=None, **opts):
422 427 dopush = True
423 428 if opts.get('bookmark'):
424 429 dopush = False
425 430 for b in opts['bookmark']:
426 431 if b in repo._bookmarks:
427 432 dopush = True
428 433 opts.setdefault('rev', []).append(b)
429 434
430 435 result = 0
431 436 if dopush:
432 437 result = oldpush(ui, repo, dest, **opts)
433 438
434 439 if opts.get('bookmark'):
435 440 # this is an unpleasant hack as push will do this internally
436 441 dest = ui.expandpath(dest or 'default-push', dest or 'default')
437 442 dest, branches = hg.parseurl(dest, opts.get('branch'))
438 443 other = hg.repository(hg.remoteui(repo, opts), dest)
439 444 rb = other.listkeys('bookmarks')
440 445 for b in opts['bookmark']:
441 446 # explicit push overrides remote bookmark if any
442 447 if b in repo._bookmarks:
443 448 ui.status(_("exporting bookmark %s\n") % b)
444 449 new = repo[b].hex()
445 450 elif b in rb:
446 451 ui.status(_("deleting remote bookmark %s\n") % b)
447 452 new = '' # delete
448 453 else:
449 454 ui.warn(_('bookmark %s does not exist on the local or remote repository!\n') % b)
450 455 return 2
451 456 old = rb.get(b, '')
452 457 r = other.pushkey('bookmarks', b, old, new)
453 458 if not r:
454 459 ui.warn(_('updating bookmark %s failed!\n') % b)
455 460 if not result:
456 461 result = 2
457 462
458 463 return result
459 464
460 465 def diffbookmarks(ui, repo, remote):
461 466 ui.status(_("searching for changes\n"))
462 467
463 468 lmarks = repo.listkeys('bookmarks')
464 469 rmarks = remote.listkeys('bookmarks')
465 470
466 471 diff = set(rmarks) - set(lmarks)
467 472 for k in diff:
468 473 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
469 474
470 475 if len(diff) <= 0:
471 476 ui.status(_("no changes found\n"))
472 477 return 1
473 478 return 0
474 479
475 480 def incoming(oldincoming, ui, repo, source="default", **opts):
476 481 if opts.get('bookmarks'):
477 482 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
478 483 other = hg.repository(hg.remoteui(repo, opts), source)
479 484 ui.status(_('comparing with %s\n') % url.hidepassword(source))
480 485 return diffbookmarks(ui, repo, other)
481 486 else:
482 487 return oldincoming(ui, repo, source, **opts)
483 488
484 489 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
485 490 if opts.get('bookmarks'):
486 491 dest = ui.expandpath(dest or 'default-push', dest or 'default')
487 492 dest, branches = hg.parseurl(dest, opts.get('branch'))
488 493 other = hg.repository(hg.remoteui(repo, opts), dest)
489 494 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
490 495 return diffbookmarks(ui, other, repo)
491 496 else:
492 497 return oldoutgoing(ui, repo, dest, **opts)
493 498
494 499 def uisetup(ui):
495 500 extensions.wrapfunction(repair, "strip", strip)
496 501 if ui.configbool('bookmarks', 'track.current'):
497 502 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
498 503
499 504 entry = extensions.wrapcommand(commands.table, 'pull', pull)
500 505 entry[1].append(('B', 'bookmark', [],
501 506 _("bookmark to import")))
502 507 entry = extensions.wrapcommand(commands.table, 'push', push)
503 508 entry[1].append(('B', 'bookmark', [],
504 509 _("bookmark to export")))
505 510 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
506 511 entry[1].append(('B', 'bookmarks', False,
507 512 _("compare bookmark")))
508 513 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
509 514 entry[1].append(('B', 'bookmarks', False,
510 515 _("compare bookmark")))
511 516
512 517 pushkey.register('bookmarks', pushbookmark, listbookmarks)
513 518
514 519 def updatecurbookmark(orig, ui, repo, *args, **opts):
515 520 '''Set the current bookmark
516 521
517 522 If the user updates to a bookmark we update the .hg/bookmarks.current
518 523 file.
519 524 '''
520 525 res = orig(ui, repo, *args, **opts)
521 526 rev = opts['rev']
522 527 if not rev and len(args) > 0:
523 528 rev = args[0]
524 529 setcurrent(repo, rev)
525 530 return res
526 531
527 532 cmdtable = {
528 533 "bookmarks":
529 534 (bookmark,
530 535 [('f', 'force', False, _('force')),
531 536 ('r', 'rev', '', _('revision'), _('REV')),
532 537 ('d', 'delete', False, _('delete a given bookmark')),
533 538 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
534 539 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
535 540 }
536 541
537 542 colortable = {'bookmarks.current': 'green'}
General Comments 0
You need to be logged in to leave comments. Login now