##// END OF EJS Templates
bookmarks: rewrite _bookmarks method documentation
Nicolas Dumazet -
r11641:ead7550f stable
parent child Browse files
Show More
@@ -1,531 +1,531 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 mark in marks and not force:
140 140 raise util.Abort(_("a bookmark of the same name already exists"))
141 141 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
142 142 and not force):
143 143 raise util.Abort(
144 144 _("a bookmark cannot have the name of an existing branch"))
145 145 if rev:
146 146 marks[mark] = repo.lookup(rev)
147 147 else:
148 148 marks[mark] = repo.changectx('.').node()
149 149 setcurrent(repo, mark)
150 150 write(repo)
151 151 return
152 152
153 153 if mark is None:
154 154 if rev:
155 155 raise util.Abort(_("bookmark name required"))
156 156 if len(marks) == 0:
157 157 ui.status(_("no bookmarks set\n"))
158 158 else:
159 159 for bmark, n in marks.iteritems():
160 160 if ui.configbool('bookmarks', 'track.current'):
161 161 current = repo._bookmarkcurrent
162 162 if bmark == current and n == cur:
163 163 prefix, label = '*', 'bookmarks.current'
164 164 else:
165 165 prefix, label = ' ', ''
166 166 else:
167 167 if n == cur:
168 168 prefix, label = '*', 'bookmarks.current'
169 169 else:
170 170 prefix, label = ' ', ''
171 171
172 172 if ui.quiet:
173 173 ui.write("%s\n" % bmark, label=label)
174 174 else:
175 175 ui.write(" %s %-25s %d:%s\n" % (
176 176 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
177 177 label=label)
178 178 return
179 179
180 180 def _revstostrip(changelog, node):
181 181 srev = changelog.rev(node)
182 182 tostrip = [srev]
183 183 saveheads = []
184 184 for r in xrange(srev, len(changelog)):
185 185 parents = changelog.parentrevs(r)
186 186 if parents[0] in tostrip or parents[1] in tostrip:
187 187 tostrip.append(r)
188 188 if parents[1] != nullrev:
189 189 for p in parents:
190 190 if p not in tostrip and p > srev:
191 191 saveheads.append(p)
192 192 return [r for r in tostrip if r not in saveheads]
193 193
194 194 def strip(oldstrip, ui, repo, node, backup="all"):
195 195 """Strip bookmarks if revisions are stripped using
196 196 the mercurial.strip method. This usually happens during
197 197 qpush and qpop"""
198 198 revisions = _revstostrip(repo.changelog, node)
199 199 marks = repo._bookmarks
200 200 update = []
201 201 for mark, n in marks.iteritems():
202 202 if repo.changelog.rev(n) in revisions:
203 203 update.append(mark)
204 204 oldstrip(ui, repo, node, backup)
205 205 if len(update) > 0:
206 206 for m in update:
207 207 marks[m] = repo.changectx('.').node()
208 208 write(repo)
209 209
210 210 def reposetup(ui, repo):
211 211 if not repo.local():
212 212 return
213 213
214 214 class bookmark_repo(repo.__class__):
215 215
216 216 @util.propertycache
217 217 def _bookmarks(self):
218 218 '''Parse .hg/bookmarks file and return a dictionary
219 219
220 220 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
221 in the .hg/bookmarks file. They are read returned as a dictionary
222 with name => hash values.
221 in the .hg/bookmarks file.
222 Read the file and return a (name=>nodeid) dictionary
223 223 '''
224 224 try:
225 225 bookmarks = {}
226 226 for line in self.opener('bookmarks'):
227 227 sha, refspec = line.strip().split(' ', 1)
228 228 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
229 229 except:
230 230 pass
231 231 return bookmarks
232 232
233 233 @util.propertycache
234 234 def _bookmarkcurrent(self):
235 235 '''Get the current bookmark
236 236
237 237 If we use gittishsh branches we have a current bookmark that
238 238 we are on. This function returns the name of the bookmark. It
239 239 is stored in .hg/bookmarks.current
240 240 '''
241 241 mark = None
242 242 if os.path.exists(self.join('bookmarks.current')):
243 243 file = self.opener('bookmarks.current')
244 244 # No readline() in posixfile_nt, reading everything is cheap
245 245 mark = (file.readlines() or [''])[0]
246 246 if mark == '':
247 247 mark = None
248 248 file.close()
249 249 return mark
250 250
251 251 def rollback(self, *args):
252 252 if os.path.exists(self.join('undo.bookmarks')):
253 253 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
254 254 return super(bookmark_repo, self).rollback(*args)
255 255
256 256 def lookup(self, key):
257 257 if key in self._bookmarks:
258 258 key = self._bookmarks[key]
259 259 return super(bookmark_repo, self).lookup(key)
260 260
261 261 def _bookmarksupdate(self, parents, node):
262 262 marks = self._bookmarks
263 263 update = False
264 264 if ui.configbool('bookmarks', 'track.current'):
265 265 mark = self._bookmarkcurrent
266 266 if mark and marks[mark] in parents:
267 267 marks[mark] = node
268 268 update = True
269 269 else:
270 270 for mark, n in marks.items():
271 271 if n in parents:
272 272 marks[mark] = node
273 273 update = True
274 274 if update:
275 275 write(self)
276 276
277 277 def commitctx(self, ctx, error=False):
278 278 """Add a revision to the repository and
279 279 move the bookmark"""
280 280 wlock = self.wlock() # do both commit and bookmark with lock held
281 281 try:
282 282 node = super(bookmark_repo, self).commitctx(ctx, error)
283 283 if node is None:
284 284 return None
285 285 parents = self.changelog.parents(node)
286 286 if parents[1] == nullid:
287 287 parents = (parents[0],)
288 288
289 289 self._bookmarksupdate(parents, node)
290 290 return node
291 291 finally:
292 292 wlock.release()
293 293
294 294 def pull(self, remote, heads=None, force=False):
295 295 result = super(bookmark_repo, self).pull(remote, heads, force)
296 296
297 297 self.ui.debug("checking for updated bookmarks\n")
298 298 rb = remote.listkeys('bookmarks')
299 299 changes = 0
300 300 for k in rb.keys():
301 301 if k in self._bookmarks:
302 302 nr, nl = rb[k], self._bookmarks[k]
303 303 if nr in self:
304 304 cr = self[nr]
305 305 cl = self[nl]
306 306 if cl.rev() >= cr.rev():
307 307 continue
308 308 if cr in cl.descendants():
309 309 self._bookmarks[k] = cr.node()
310 310 changes += 1
311 311 self.ui.status(_("updating bookmark %s\n") % k)
312 312 else:
313 313 self.ui.warn(_("not updating divergent"
314 314 " bookmark %s\n") % k)
315 315 if changes:
316 316 write(repo)
317 317
318 318 return result
319 319
320 320 def push(self, remote, force=False, revs=None, newbranch=False):
321 321 result = super(bookmark_repo, self).push(remote, force, revs,
322 322 newbranch)
323 323
324 324 self.ui.debug("checking for updated bookmarks\n")
325 325 rb = remote.listkeys('bookmarks')
326 326 for k in rb.keys():
327 327 if k in self._bookmarks:
328 328 nr, nl = rb[k], self._bookmarks[k]
329 329 if nr in self:
330 330 cr = self[nr]
331 331 cl = self[nl]
332 332 if cl in cr.descendants():
333 333 r = remote.pushkey('bookmarks', k, nr, nl)
334 334 if r:
335 335 self.ui.status(_("updating bookmark %s\n") % k)
336 336 else:
337 337 self.ui.warn(_('updating bookmark %s'
338 338 ' failed!\n') % k)
339 339
340 340 return result
341 341
342 342 def addchangegroup(self, *args, **kwargs):
343 343 parents = self.dirstate.parents()
344 344
345 345 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
346 346 if result > 1:
347 347 # We have more heads than before
348 348 return result
349 349 node = self.changelog.tip()
350 350
351 351 self._bookmarksupdate(parents, node)
352 352 return result
353 353
354 354 def _findtags(self):
355 355 """Merge bookmarks with normal tags"""
356 356 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
357 357 tags.update(self._bookmarks)
358 358 return (tags, tagtypes)
359 359
360 360 if hasattr(repo, 'invalidate'):
361 361 def invalidate(self):
362 362 super(bookmark_repo, self).invalidate()
363 363 for attr in ('_bookmarks', '_bookmarkcurrent'):
364 364 if attr in self.__dict__:
365 365 delattr(self, attr)
366 366
367 367 repo.__class__ = bookmark_repo
368 368
369 369 def listbookmarks(repo):
370 370 d = {}
371 371 for k, v in repo._bookmarks.iteritems():
372 372 d[k] = hex(v)
373 373 return d
374 374
375 375 def pushbookmark(repo, key, old, new):
376 376 w = repo.wlock()
377 377 try:
378 378 marks = repo._bookmarks
379 379 if hex(marks.get(key, '')) != old:
380 380 return False
381 381 if new == '':
382 382 del marks[key]
383 383 else:
384 384 if new not in repo:
385 385 return False
386 386 marks[key] = repo[new].node()
387 387 write(repo)
388 388 return True
389 389 finally:
390 390 w.release()
391 391
392 392 def pull(oldpull, ui, repo, source="default", **opts):
393 393 # translate bookmark args to rev args for actual pull
394 394 if opts.get('bookmark'):
395 395 # this is an unpleasant hack as pull will do this internally
396 396 source, branches = hg.parseurl(ui.expandpath(source),
397 397 opts.get('branch'))
398 398 other = hg.repository(hg.remoteui(repo, opts), source)
399 399 rb = other.listkeys('bookmarks')
400 400
401 401 for b in opts['bookmark']:
402 402 if b not in rb:
403 403 raise util.Abort(_('remote bookmark %s not found!') % b)
404 404 opts.setdefault('rev', []).append(b)
405 405
406 406 result = oldpull(ui, repo, source, **opts)
407 407
408 408 # update specified bookmarks
409 409 if opts.get('bookmark'):
410 410 for b in opts['bookmark']:
411 411 # explicit pull overrides local bookmark if any
412 412 ui.status(_("importing bookmark %s\n") % b)
413 413 repo._bookmarks[b] = repo[rb[b]].node()
414 414 write(repo)
415 415
416 416 return result
417 417
418 418 def push(oldpush, ui, repo, dest=None, **opts):
419 419 dopush = True
420 420 if opts.get('bookmark'):
421 421 dopush = False
422 422 for b in opts['bookmark']:
423 423 if b in repo._bookmarks:
424 424 dopush = True
425 425 opts.setdefault('rev', []).append(b)
426 426
427 427 result = 0
428 428 if dopush:
429 429 result = oldpush(ui, repo, dest, **opts)
430 430
431 431 if opts.get('bookmark'):
432 432 # this is an unpleasant hack as push will do this internally
433 433 dest = ui.expandpath(dest or 'default-push', dest or 'default')
434 434 dest, branches = hg.parseurl(dest, opts.get('branch'))
435 435 other = hg.repository(hg.remoteui(repo, opts), dest)
436 436 rb = other.listkeys('bookmarks')
437 437 for b in opts['bookmark']:
438 438 # explicit push overrides remote bookmark if any
439 439 if b in repo._bookmarks:
440 440 ui.status(_("exporting bookmark %s\n") % b)
441 441 new = repo[b].hex()
442 442 else:
443 443 ui.status(_("deleting remote bookmark %s\n") % b)
444 444 new = '' # delete
445 445 old = rb.get(b, '')
446 446 r = other.pushkey('bookmarks', b, old, new)
447 447 if not r:
448 448 ui.warn(_('updating bookmark %s failed!\n') % b)
449 449 if not result:
450 450 result = 2
451 451
452 452 return result
453 453
454 454 def diffbookmarks(ui, repo, remote):
455 455 ui.status(_("searching for changes\n"))
456 456
457 457 lmarks = repo.listkeys('bookmarks')
458 458 rmarks = remote.listkeys('bookmarks')
459 459
460 460 diff = set(rmarks) - set(lmarks)
461 461 for k in diff:
462 462 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
463 463
464 464 if len(diff) <= 0:
465 465 ui.status(_("no changes found\n"))
466 466 return 1
467 467 return 0
468 468
469 469 def incoming(oldincoming, ui, repo, source="default", **opts):
470 470 if opts.get('bookmarks'):
471 471 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
472 472 other = hg.repository(hg.remoteui(repo, opts), source)
473 473 ui.status(_('comparing with %s\n') % url.hidepassword(source))
474 474 return diffbookmarks(ui, repo, other)
475 475 else:
476 476 return oldincoming(ui, repo, source, **opts)
477 477
478 478 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
479 479 if opts.get('bookmarks'):
480 480 dest = ui.expandpath(dest or 'default-push', dest or 'default')
481 481 dest, branches = hg.parseurl(dest, opts.get('branch'))
482 482 other = hg.repository(hg.remoteui(repo, opts), dest)
483 483 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
484 484 return diffbookmarks(ui, other, repo)
485 485 else:
486 486 return oldoutgoing(ui, repo, dest, **opts)
487 487
488 488 def uisetup(ui):
489 489 extensions.wrapfunction(repair, "strip", strip)
490 490 if ui.configbool('bookmarks', 'track.current'):
491 491 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
492 492
493 493 entry = extensions.wrapcommand(commands.table, 'pull', pull)
494 494 entry[1].append(('B', 'bookmark', [],
495 495 _("bookmark to import")))
496 496 entry = extensions.wrapcommand(commands.table, 'push', push)
497 497 entry[1].append(('B', 'bookmark', [],
498 498 _("bookmark to export")))
499 499 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
500 500 entry[1].append(('B', 'bookmarks', False,
501 501 _("compare bookmark")))
502 502 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
503 503 entry[1].append(('B', 'bookmarks', False,
504 504 _("compare bookmark")))
505 505
506 506 pushkey.register('bookmarks', pushbookmark, listbookmarks)
507 507
508 508 def updatecurbookmark(orig, ui, repo, *args, **opts):
509 509 '''Set the current bookmark
510 510
511 511 If the user updates to a bookmark we update the .hg/bookmarks.current
512 512 file.
513 513 '''
514 514 res = orig(ui, repo, *args, **opts)
515 515 rev = opts['rev']
516 516 if not rev and len(args) > 0:
517 517 rev = args[0]
518 518 setcurrent(repo, rev)
519 519 return res
520 520
521 521 cmdtable = {
522 522 "bookmarks":
523 523 (bookmark,
524 524 [('f', 'force', False, _('force')),
525 525 ('r', 'rev', '', _('revision'), _('REV')),
526 526 ('d', 'delete', False, _('delete a given bookmark')),
527 527 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
528 528 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
529 529 }
530 530
531 531 colortable = {'bookmarks.current': 'green'}
General Comments 0
You need to be logged in to leave comments. Login now