eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/bookmarks.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     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]