Refactor annotate copy support.
authorBrendan Cully <brendan@kublai.com>
Wed, 27 Sep 2006 09:10:21 -0700
changeset 3172 5c93dd0ae413
parent 3171 706277866251
child 3173 3466bd7b9754
Refactor annotate copy support.
mercurial/commands.py
mercurial/context.py
tests/test-annotate
tests/test-annotate.out
--- a/mercurial/commands.py	Wed Sep 27 09:35:53 2006 +0200
+++ b/mercurial/commands.py	Wed Sep 27 09:10:21 2006 -0700
@@ -612,8 +612,9 @@
     opmap = [['user', lambda x: ui.shortuser(x.user())],
              ['number', lambda x: str(x.rev())],
              ['changeset', lambda x: short(x.node())],
-             ['date', getdate]]
-    if not opts['user'] and not opts['changeset'] and not opts['date']:
+             ['date', getdate], ['follow', lambda x: x.path()]]
+    if (not opts['user'] and not opts['changeset'] and not opts['date']
+        and not opts['follow']):
         opts['number'] = 1
 
     ctx = repo.changectx(opts['rev'])
@@ -625,7 +626,7 @@
             ui.write(_("%s: binary file\n") % ((pats and rel) or abs))
             continue
 
-        lines = fctx.annotate()
+        lines = fctx.annotate(follow=opts.get('follow'))
         pieces = []
 
         for o, f in opmap:
@@ -2671,6 +2672,7 @@
     "^annotate":
         (annotate,
          [('r', 'rev', '', _('annotate the specified revision')),
+          ('f', 'follow', None, _('follow file copies and renames')),
           ('a', 'text', None, _('treat all files as text')),
           ('u', 'user', None, _('list the author')),
           ('d', 'date', None, _('list the date')),
--- a/mercurial/context.py	Wed Sep 27 09:35:53 2006 +0200
+++ b/mercurial/context.py	Wed Sep 27 09:10:21 2006 -0700
@@ -5,6 +5,10 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
+from demandload import *
+from node import *
+demandload(globals(), 'bdiff')
+
 from node import *
 from demandload import demandload
 demandload(globals(), "ancestor util")
@@ -165,13 +169,74 @@
         return [ filectx(self._repo, self._path, fileid=x,
                          filelog=self._filelog) for x in c ]
 
-    def annotate(self):
-        getctx = util.cachefunc(lambda x: filectx(self._repo, self._path,
-                                                  changeid=x,
-                                                  filelog=self._filelog))
-        hist = self._filelog.annotate(self._filenode)
+    def annotate(self, follow=False):
+        '''returns a list of tuples of (ctx, line) for each line
+        in the file, where ctx is the filectx of the node where
+        that line was last changed'''
+
+        def decorate(text, rev):
+            return ([rev] * len(text.splitlines()), text)
+
+        def pair(parent, child):
+            for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
+                child[0][b1:b2] = parent[0][a1:a2]
+            return child
+
+        getlog = util.cachefunc(lambda x: self._repo.file(x))
+        def getctx(path, fileid):
+            log = path == self._path and self._filelog or getlog(path)
+            return filectx(self._repo, path, fileid=fileid, filelog=log)
+        getctx = util.cachefunc(getctx)
+
+        def parents(f):
+            # we want to reuse filectx objects as much as possible
+            p = f._path
+            pl = [ (p, f._filelog.rev(n)) for n in f._filelog.parents(f._filenode) ]
+
+            if follow:
+                r = f.renamed()
+                if r:
+                    pl[0] = (r[0], getlog(r[0]).rev(r[1]))
 
-        return [(getctx(rev), line) for rev, line in hist]
+            return [ getctx(p, n) for p, n in pl if n != -1 ]
+                
+        # find all ancestors
+        needed = {self: 1}
+        visit = [self]
+        files = [self._path]
+        while visit:
+            f = visit.pop(0)
+            for p in parents(f):
+                if p not in needed:
+                    needed[p] = 1
+                    visit.append(p)
+                    if p._path not in files:
+                        files.append(p._path)
+                else:
+                    # count how many times we'll use this
+                    needed[p] += 1
+
+        # sort by revision (per file) which is a topological order
+        visit = []
+        files.reverse()
+        for f in files:
+            fn = [(n._filerev, n) for n in needed.keys() if n._path == f]
+            fn.sort()
+            visit.extend(fn)
+        hist = {}
+
+        for r, f in visit:
+            curr = decorate(f.data(), f)
+            for p in parents(f):
+                if p != nullid:
+                    curr = pair(hist[p], curr)
+                    # trim the history of unneeded revs
+                    needed[p] -= 1
+                    if not needed[p]:
+                        del hist[p]
+            hist[f] = curr
+
+        return zip(hist[f][0], hist[f][1].splitlines(1))
 
     def ancestor(self, fc2):
         """
--- a/tests/test-annotate	Wed Sep 27 09:35:53 2006 +0200
+++ b/tests/test-annotate	Wed Sep 27 09:10:21 2006 -0700
@@ -1,5 +1,7 @@
 #!/bin/sh
 
+export HGMERGE=true
+
 echo % init
 hg init
 
@@ -21,3 +23,53 @@
 
 echo % annotate -cdnu
 hg annotate -cdnu a
+
+cat <<EOF >>a
+a
+a
+EOF
+hg ci -ma1 -d '1 0'
+hg cp a b
+hg ci -mb -d '1 0'
+cat <<EOF >> b
+b
+b
+b
+EOF
+hg ci -mb2 -d '2 0'
+
+echo % annotate b
+hg annotate b
+echo % annotate -nf b
+hg annotate -nf b
+
+hg up -C 2
+cat <<EOF >> b
+b
+c
+b
+EOF
+hg ci -mb2.1 -d '2 0'
+hg merge
+hg ci -mmergeb -d '3 0'
+echo % annotate after merge
+hg annotate -nf b
+
+hg up -C 1
+hg cp a b
+cat <<EOF > b
+a
+z
+a
+EOF
+hg ci -mc -d '3 0'
+hg merge
+cat <<EOF >> b
+b
+c
+b
+EOF
+echo d >> b
+hg ci -mmerge2 -d '4 0'
+echo % annotate after rename merge
+hg annotate -nf b
--- a/tests/test-annotate.out	Wed Sep 27 09:35:53 2006 +0200
+++ b/tests/test-annotate.out	Wed Sep 27 09:10:21 2006 -0700
@@ -11,3 +11,40 @@
 nobody: a
 % annotate -cdnu
 nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000: a
+% annotate b
+2: a
+2: a
+2: a
+3: b
+3: b
+3: b
+% annotate -nf b
+0 a: a
+1 a: a
+1 a: a
+3 b: b
+3 b: b
+3 b: b
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+merging b
+0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+% annotate after merge
+0 a: a
+1 a: a
+1 a: a
+3 b: b
+4 b: c
+3 b: b
+0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+merging b
+0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+% annotate after rename merge
+0 a: a
+6 b: z
+1 a: a
+3 b: b
+4 b: c
+3 b: b
+7 b: d