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