hgext/journal.py
changeset 29502 8361131b4768
parent 29443 cf092a3d202a
child 29503 0103b673d6ca
equal deleted inserted replaced
29501:be68a4445041 29502:8361131b4768
    13 
    13 
    14 from __future__ import absolute_import
    14 from __future__ import absolute_import
    15 
    15 
    16 import collections
    16 import collections
    17 import os
    17 import os
       
    18 import weakref
    18 
    19 
    19 from mercurial.i18n import _
    20 from mercurial.i18n import _
    20 
    21 
    21 from mercurial import (
    22 from mercurial import (
    22     bookmarks,
    23     bookmarks,
    23     cmdutil,
    24     cmdutil,
    24     commands,
    25     commands,
       
    26     dirstate,
    25     dispatch,
    27     dispatch,
    26     error,
    28     error,
    27     extensions,
    29     extensions,
       
    30     localrepo,
       
    31     lock,
    28     node,
    32     node,
    29     util,
    33     util,
    30 )
    34 )
    31 
    35 
    32 cmdtable = {}
    36 cmdtable = {}
    41 # storage format version; increment when the format changes
    45 # storage format version; increment when the format changes
    42 storageversion = 0
    46 storageversion = 0
    43 
    47 
    44 # namespaces
    48 # namespaces
    45 bookmarktype = 'bookmark'
    49 bookmarktype = 'bookmark'
       
    50 wdirparenttype = 'wdirparent'
    46 
    51 
    47 # Journal recording, register hooks and storage object
    52 # Journal recording, register hooks and storage object
    48 def extsetup(ui):
    53 def extsetup(ui):
    49     extensions.wrapfunction(dispatch, 'runcommand', runcommand)
    54     extensions.wrapfunction(dispatch, 'runcommand', runcommand)
    50     extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
    55     extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
       
    56     extensions.wrapfunction(
       
    57         dirstate.dirstate, '_writedirstate', recorddirstateparents)
       
    58     extensions.wrapfunction(
       
    59         localrepo.localrepository.dirstate, 'func', wrapdirstate)
    51 
    60 
    52 def reposetup(ui, repo):
    61 def reposetup(ui, repo):
    53     if repo.local():
    62     if repo.local():
    54         repo.journal = journalstorage(repo)
    63         repo.journal = journalstorage(repo)
    55 
    64 
    56 def runcommand(orig, lui, repo, cmd, fullargs, *args):
    65 def runcommand(orig, lui, repo, cmd, fullargs, *args):
    57     """Track the command line options for recording in the journal"""
    66     """Track the command line options for recording in the journal"""
    58     journalstorage.recordcommand(*fullargs)
    67     journalstorage.recordcommand(*fullargs)
    59     return orig(lui, repo, cmd, fullargs, *args)
    68     return orig(lui, repo, cmd, fullargs, *args)
    60 
    69 
       
    70 # hooks to record dirstate changes
       
    71 def wrapdirstate(orig, repo):
       
    72     """Make journal storage available to the dirstate object"""
       
    73     dirstate = orig(repo)
       
    74     if util.safehasattr(repo, 'journal'):
       
    75         dirstate.journalstorage = repo.journal
       
    76     return dirstate
       
    77 
       
    78 def recorddirstateparents(orig, dirstate, dirstatefp):
       
    79     """Records all dirstate parent changes in the journal."""
       
    80     if util.safehasattr(dirstate, 'journalstorage'):
       
    81         old = [node.nullid, node.nullid]
       
    82         nodesize = len(node.nullid)
       
    83         try:
       
    84             # The only source for the old state is in the dirstate file still
       
    85             # on disk; the in-memory dirstate object only contains the new
       
    86             # state. dirstate._opendirstatefile() switches beteen .hg/dirstate
       
    87             # and .hg/dirstate.pending depending on the transaction state.
       
    88             with dirstate._opendirstatefile() as fp:
       
    89                 state = fp.read(2 * nodesize)
       
    90             if len(state) == 2 * nodesize:
       
    91                 old = [state[:nodesize], state[nodesize:]]
       
    92         except IOError:
       
    93             pass
       
    94 
       
    95         new = dirstate.parents()
       
    96         if old != new:
       
    97             # only record two hashes if there was a merge
       
    98             oldhashes = old[:1] if old[1] == node.nullid else old
       
    99             newhashes = new[:1] if new[1] == node.nullid else new
       
   100             dirstate.journalstorage.record(
       
   101                 wdirparenttype, '.', oldhashes, newhashes)
       
   102 
       
   103     return orig(dirstate, dirstatefp)
       
   104 
       
   105 # hooks to record bookmark changes (both local and remote)
    61 def recordbookmarks(orig, store, fp):
   106 def recordbookmarks(orig, store, fp):
    62     """Records all bookmark changes in the journal."""
   107     """Records all bookmark changes in the journal."""
    63     repo = store._repo
   108     repo = store._repo
    64     if util.safehasattr(repo, 'journal'):
   109     if util.safehasattr(repo, 'journal'):
    65         oldmarks = bookmarks.bmstore(repo)
   110         oldmarks = bookmarks.bmstore(repo)
   115     Entries are stored with NUL bytes as separators. See the journalentry
   160     Entries are stored with NUL bytes as separators. See the journalentry
   116     class for the per-entry structure.
   161     class for the per-entry structure.
   117 
   162 
   118     The file format starts with an integer version, delimited by a NUL.
   163     The file format starts with an integer version, delimited by a NUL.
   119 
   164 
       
   165     This storage uses a dedicated lock; this makes it easier to avoid issues
       
   166     with adding entries that added when the regular wlock is unlocked (e.g.
       
   167     the dirstate).
       
   168 
   120     """
   169     """
   121     _currentcommand = ()
   170     _currentcommand = ()
       
   171     _lockref = None
   122 
   172 
   123     def __init__(self, repo):
   173     def __init__(self, repo):
   124         self.repo = repo
       
   125         self.user = util.getuser()
   174         self.user = util.getuser()
       
   175         self.ui = repo.ui
   126         self.vfs = repo.vfs
   176         self.vfs = repo.vfs
   127 
   177 
   128     # track the current command for recording in journal entries
   178     # track the current command for recording in journal entries
   129     @property
   179     @property
   130     def command(self):
   180     def command(self):
   140         """Set the current hg arguments, stored with recorded entries"""
   190         """Set the current hg arguments, stored with recorded entries"""
   141         # Set the current command on the class because we may have started
   191         # Set the current command on the class because we may have started
   142         # with a non-local repo (cloning for example).
   192         # with a non-local repo (cloning for example).
   143         cls._currentcommand = fullargs
   193         cls._currentcommand = fullargs
   144 
   194 
       
   195     def jlock(self):
       
   196         """Create a lock for the journal file"""
       
   197         if self._lockref and self._lockref():
       
   198             raise error.Abort(_('journal lock does not support nesting'))
       
   199         desc = _('journal of %s') % self.vfs.base
       
   200         try:
       
   201             l = lock.lock(self.vfs, 'journal.lock', 0, desc=desc)
       
   202         except error.LockHeld as inst:
       
   203             self.ui.warn(
       
   204                 _("waiting for lock on %s held by %r\n") % (desc, inst.locker))
       
   205             # default to 600 seconds timeout
       
   206             l = lock.lock(
       
   207                 self.vfs, 'journal.lock',
       
   208                 int(self.ui.config("ui", "timeout", "600")), desc=desc)
       
   209             self.ui.warn(_("got lock after %s seconds\n") % l.delay)
       
   210         self._lockref = weakref.ref(l)
       
   211         return l
       
   212 
   145     def record(self, namespace, name, oldhashes, newhashes):
   213     def record(self, namespace, name, oldhashes, newhashes):
   146         """Record a new journal entry
   214         """Record a new journal entry
   147 
   215 
   148         * namespace: an opaque string; this can be used to filter on the type
   216         * namespace: an opaque string; this can be used to filter on the type
   149           of recorded entries.
   217           of recorded entries.
   161 
   229 
   162         entry = journalentry(
   230         entry = journalentry(
   163             util.makedate(), self.user, self.command, namespace, name,
   231             util.makedate(), self.user, self.command, namespace, name,
   164             oldhashes, newhashes)
   232             oldhashes, newhashes)
   165 
   233 
   166         with self.repo.wlock():
   234         with self.jlock():
   167             version = None
   235             version = None
   168             # open file in amend mode to ensure it is created if missing
   236             # open file in amend mode to ensure it is created if missing
   169             with self.vfs('journal', mode='a+b', atomictemp=True) as f:
   237             with self.vfs('journal', mode='a+b', atomictemp=True) as f:
   170                 f.seek(0, os.SEEK_SET)
   238                 f.seek(0, os.SEEK_SET)
   171                 # Read just enough bytes to get a version number (up to 2
   239                 # Read just enough bytes to get a version number (up to 2
   174                 if version and version != str(storageversion):
   242                 if version and version != str(storageversion):
   175                     # different version of the storage. Exit early (and not
   243                     # different version of the storage. Exit early (and not
   176                     # write anything) if this is not a version we can handle or
   244                     # write anything) if this is not a version we can handle or
   177                     # the file is corrupt. In future, perhaps rotate the file
   245                     # the file is corrupt. In future, perhaps rotate the file
   178                     # instead?
   246                     # instead?
   179                     self.repo.ui.warn(
   247                     self.ui.warn(
   180                         _("unsupported journal file version '%s'\n") % version)
   248                         _("unsupported journal file version '%s'\n") % version)
   181                     return
   249                     return
   182                 if not version:
   250                 if not version:
   183                     # empty file, write version first
   251                     # empty file, write version first
   184                     f.write(str(storageversion) + '\0')
   252                     f.write(str(storageversion) + '\0')
   227 # journal reading
   295 # journal reading
   228 # log options that don't make sense for journal
   296 # log options that don't make sense for journal
   229 _ignoreopts = ('no-merges', 'graph')
   297 _ignoreopts = ('no-merges', 'graph')
   230 @command(
   298 @command(
   231     'journal', [
   299     'journal', [
       
   300         ('', 'all', None, 'show history for all names'),
   232         ('c', 'commits', None, 'show commit metadata'),
   301         ('c', 'commits', None, 'show commit metadata'),
   233     ] + [opt for opt in commands.logopts if opt[1] not in _ignoreopts],
   302     ] + [opt for opt in commands.logopts if opt[1] not in _ignoreopts],
   234     '[OPTION]... [BOOKMARKNAME]')
   303     '[OPTION]... [BOOKMARKNAME]')
   235 def journal(ui, repo, *args, **opts):
   304 def journal(ui, repo, *args, **opts):
   236     """show the previous position of bookmarks
   305     """show the previous position of bookmarks and the working copy
   237 
   306 
   238     The journal is used to see the previous commits of bookmarks. By default
   307     The journal is used to see the previous commits that bookmarks and the
   239     the previous locations for all bookmarks are shown.  Passing a bookmark
   308     working copy pointed to. By default the previous locations for the working
   240     name will show all the previous positions of that bookmark.
   309     copy.  Passing a bookmark name will show all the previous positions of
       
   310     that bookmark. Use the --all switch to show previous locations for all
       
   311     bookmarks and the working copy; each line will then include the bookmark
       
   312     name, or '.' for the working copy, as well.
   241 
   313 
   242     By default hg journal only shows the commit hash and the command that was
   314     By default hg journal only shows the commit hash and the command that was
   243     running at that time. -v/--verbose will show the prior hash, the user, and
   315     running at that time. -v/--verbose will show the prior hash, the user, and
   244     the time at which it happened.
   316     the time at which it happened.
   245 
   317 
   248     switches to alter the log output for these.
   320     switches to alter the log output for these.
   249 
   321 
   250     `hg journal -T json` can be used to produce machine readable output.
   322     `hg journal -T json` can be used to produce machine readable output.
   251 
   323 
   252     """
   324     """
   253     bookmarkname = None
   325     name = '.'
       
   326     if opts.get('all'):
       
   327         if args:
       
   328             raise error.Abort(
       
   329                 _("You can't combine --all and filtering on a name"))
       
   330         name = None
   254     if args:
   331     if args:
   255         bookmarkname = args[0]
   332         name = args[0]
   256 
   333 
   257     fm = ui.formatter('journal', opts)
   334     fm = ui.formatter('journal', opts)
   258 
   335 
   259     if opts.get("template") != "json":
   336     if opts.get("template") != "json":
   260         if bookmarkname is None:
   337         if name is None:
   261             name = _('all bookmarks')
   338             displayname = _('the working copy and bookmarks')
   262         else:
   339         else:
   263             name = "'%s'" % bookmarkname
   340             displayname = "'%s'" % name
   264         ui.status(_("previous locations of %s:\n") % name)
   341         ui.status(_("previous locations of %s:\n") % displayname)
   265 
   342 
   266     limit = cmdutil.loglimit(opts)
   343     limit = cmdutil.loglimit(opts)
   267     entry = None
   344     entry = None
   268     for count, entry in enumerate(repo.journal.filtered(name=bookmarkname)):
   345     for count, entry in enumerate(repo.journal.filtered(name=name)):
   269         if count == limit:
   346         if count == limit:
   270             break
   347             break
   271         newhashesstr = ','.join([node.short(hash) for hash in entry.newhashes])
   348         newhashesstr = ','.join([node.short(hash) for hash in entry.newhashes])
   272         oldhashesstr = ','.join([node.short(hash) for hash in entry.oldhashes])
   349         oldhashesstr = ','.join([node.short(hash) for hash in entry.oldhashes])
   273 
   350 
   274         fm.startitem()
   351         fm.startitem()
   275         fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr)
   352         fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr)
   276         fm.write('newhashes', '%s', newhashesstr)
   353         fm.write('newhashes', '%s', newhashesstr)
   277         fm.condwrite(ui.verbose, 'user', ' %s', entry.user.ljust(8))
   354         fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user)
       
   355         fm.condwrite(opts.get('all'), 'name', '  %-8s', entry.name)
   278 
   356 
   279         timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2')
   357         timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2')
   280         fm.condwrite(ui.verbose, 'date', ' %s', timestring)
   358         fm.condwrite(ui.verbose, 'date', ' %s', timestring)
   281         fm.write('command', '  %s\n', entry.command)
   359         fm.write('command', '  %s\n', entry.command)
   282 
   360