[PATCH RFC] reflog: adds a reflog extension

Durham Goode durham at fb.com
Wed Oct 1 20:45:45 CDT 2014


# HG changeset patch
# User Durham Goode <durham at fb.com>
# Date 1412200597 25200
#      Wed Oct 01 14:56:37 2014 -0700
# Node ID 942be96848993cf7ab5ed529db9c1f39c6d43c30
# Parent  939ce500c92a3dcc0e10815242361ff70a6fcae9
reflog: adds a reflog extension

This adds an extension that tracks the locations of the working copy and
bookmarks over time. It's still a proof of concept, but I wanted to throw
it out there to start the bike shedding early (like finding a better name
than 'reflog'). We're close enough to the release that I don't think it should
go in before that.

Running `hg reflog` by default shows the previous locations of the working
copy (most recent first).

~/myrepo> hg reflog
35a5fcfee452 > rebase -d master
32eee5e2d406 > up .^
b5d6dab4f900 > up foo -C

Specifying a bookmark name shows the locations of that bookmark over time.

~/myrepo> hg reflog foo
d1a696044ec0 > rebase -d master
35a5fcfee452 > rebase -d master
32eee5e2d406 > book foo -f

--date and --user flags exist to show more information about each entry.

~/myrepo> hg reflog foo --user --date
d1a696044ec0 durham 2014-10-01 18:32:14 > rebase -d master
35a5fcfee452 durham 2014-10-01 17:28:54 > rebase -d master
32eee5e2d406 durham 2014-10-01 17:28:30 > book foo -f

It's currently stored as a single .hg/reflog file that is append only. Each
entry can store an arbitrary number of hashes (like storing 2 hashes for a merge
state working copy), which means we could also potentially use this to track
heads in branches as well.

diff --git a/hgext/reflog.py b/hgext/reflog.py
new file mode 100644
--- /dev/null
+++ b/hgext/reflog.py
@@ -0,0 +1,152 @@
+# reflog.py
+#
+# Copyright 2013 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from mercurial import util, cmdutil, commands, hg, scmutil, localrepo
+from mercurial import bookmarks, dispatch, dirstate
+from mercurial.extensions import wrapcommand, wrapfunction
+from mercurial.node import nullid, hex
+from mercurial.i18n import _
+import errno, os, getpass, time
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+testedwith = 'internal'
+
+bookmarktype = 'bookmark'
+workingcopyparenttype = 'workingcopyparent'
+
+def extsetup(ui):
+    wrapfunction(dispatch, '_parse', recordcommand)
+    wrapfunction(bookmarks.bmstore, 'write', recordbookmarks)
+    wrapfunction(dirstate.dirstate, 'write', recorddirstateparents)
+
+def reposetup(ui, repo):
+    if isinstance(repo, localrepo.localrepository):
+        repo.reflog = Reflog(repo, currentcommand)
+        repo.dirstate.repo = repo
+
+currentcommand = ''
+def recordcommand(orig, ui, args):
+    """Records the current command line args for later logging to the reflog."""
+    global currentcommand
+    currentcommand = ' '.join(args)
+    return orig(ui, args)
+
+def recordbookmarks(orig, self):
+    """Records all bookmark changes to the reflog."""
+    repo = self._repo
+    oldmarks = bookmarks.bmstore(repo)
+    for mark, value in self.iteritems():
+        if value != oldmarks.get(mark):
+            repo.reflog.addentry(bookmarktype, mark, value) 
+    return orig(self)
+
+def recorddirstateparents(orig, self):
+    """Records all dirstate parent changes to the reflog."""
+    oldparents = [nullid]
+    try:
+        fp = self._opener("dirstate")
+        st = fp.read(40)
+        fp.close()
+        l = len(st)
+        if l == 40:
+            oldparents = [st[:20]]
+            oldparents.append(st[20:40])
+    except IOError, err:
+        pass
+    if oldparents != self.parents():
+        hashes = [hash for hash in self.parents() if hash != nullid]
+        self.repo.reflog.addentry(workingcopyparenttype, '.', hashes)
+    return orig(self)
+
+ at command('reflog',
+    [('', 'all', None, 'show history for all refs'),
+     ('', 'date', None, 'include timestamp information'),
+     ('', 'user', None, 'include user information'),
+     ], '[OPTION]... [REFNAME]')
+def reflog(ui, repo, *args, **opts):
+    """show the previous position of bookmarks and the working copy
+
+    The reflog is used to see the previous commits that bookmarks and the
+    working copy pointed to. By default it shows the previous locations of the
+    working copy.  Passing a bookmark name will show all the previous
+    positions of that bookmark. Passing --all will show the previous
+    locations of all bookmarks and the working copy.
+
+    By default the reflog only shows the commit hash and the command that was
+    running at that time. --date will also show the timestamp of the entry, and
+    --user will show the user name of the user who was executing the command.
+    """
+    refname = '.'
+    if args:
+        refname = args[0]
+    if opts.get('all'):
+        refname = None
+
+    for entry in repo.reflog.iter(refnamecond=refname):
+        timestamp, user, command, reftype, refname, hashes = entry
+        output = ','.join([hash[:12] for hash in hashes])
+        if opts.get('user'):
+            output += ' %s' % user
+        if opts.get('date'):
+            timestruct = time.localtime(timestamp)
+            timestring = time.strftime('%Y-%m-%d %H:%M:%S', timestruct)
+            output += ' %s' % timestring
+        output += ' > %s' % command
+        ui.status('%s\n' % output)
+
+class Reflog(object):
+    def __init__(self, repo, command):
+        self.repo = repo
+        self.command = command
+        self.user = getpass.getuser()
+        self.path = repo.join('reflog')
+
+    def __iter__(self):
+        return self._read()
+
+    def iter(self, reftypecond=None, refnamecond=None):
+        for entry in self._read():
+            time, user, command, reftype, refname, hashes = entry
+            if reftypecond and reftype != reftypecond:
+                continue
+            if refnamecond and refname != refnamecond:
+                continue
+            yield entry
+
+    def _read(self):
+        if not os.path.exists(self.path):
+            raise StopIteration()
+
+        f = open(self.path, 'r')
+        try:
+            raw = f.read()
+        finally:
+            f.close()
+
+        lines = reversed(raw.split('\0'))
+        for line in lines:
+            if not line:
+                continue
+            time, user, command, reftype, refname, hashes = line.split('\n')
+            time = int(time)
+            hashes = hashes.split(',')
+            yield (time, user, command, reftype, refname, hashes)
+
+    def addentry(self, reftype, refname, hashes):
+        if isinstance(hashes, str):
+            hashes = [hashes]
+
+        date = str(int(time.time()))
+        hashes = ','.join([hex(hash) for hash in hashes])
+        data = (date, self.user, self.command, reftype, refname, hashes)
+        data = '\n'.join(data)
+        f = open(self.path, 'a+')
+        try:
+            f.write(data + '\0')
+        finally:
+            f.close()


More information about the Mercurial-devel mailing list