diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -166,37 +166,44 @@ class localrepository(object):
                     return
                 s = l.split(" ", 1)
                 if len(s) != 2:
-                    self.ui.warn(_("%s: ignoring invalid tag\n") % context)
+                    self.ui.warn(_("%s: cannot parse entry\n") % context)
                     return
                 node, key = s
+                key = key.strip()
                 try:
                     bin_n = bin(node)
                 except TypeError:
-                    self.ui.warn(_("%s: ignoring invalid tag\n") % context)
+                    self.ui.warn(_("%s: node '%s' is not well formed\n") %
+                                 (context, node))
                     return
                 if bin_n not in self.changelog.nodemap:
-                    self.ui.warn(_("%s: ignoring invalid tag\n") % context)
+                    self.ui.warn(_("%s: tag '%s' refers to unknown node\n") %
+                                 (context, key))
                     return
-                self.tagscache[key.strip()] = bin_n
+                self.tagscache[key] = bin_n
 
-            # read each head of the tags file, ending with the tip
+            # read the tags file from each head, ending with the tip,
             # and add each tag found to the map, with "newer" ones
             # taking precedence
+            heads = self.heads()
+            heads.reverse()
             fl = self.file(".hgtags")
-            h = fl.heads()
-            h.reverse()
-            for r in h:
+            for node in heads:
+                change = self.changelog.read(node)
+                rev = self.changelog.rev(node)
+                fn, ff = self.manifest.find(change[0], '.hgtags')
+                if fn is None: continue
                 count = 0
-                for l in fl.read(r).splitlines():
+                for l in fl.read(fn).splitlines():
                     count += 1
-                    parsetag(l, ".hgtags:%d" % count)
-
+                    parsetag(l, _(".hgtags (rev %d:%s), line %d") %
+                             (rev, short(node), count))
             try:
                 f = self.opener("localtags")
                 count = 0
                 for l in f:
                     count += 1
-                    parsetag(l, "localtags:%d" % count)
+                    parsetag(l, _("localtags, line %d") % count)
             except IOError:
                 pass
 
diff --git a/mercurial/manifest.py b/mercurial/manifest.py
--- a/mercurial/manifest.py
+++ b/mercurial/manifest.py
@@ -43,48 +43,61 @@ class manifest(revlog):
     def diff(self, a, b):
         return mdiff.textdiff(str(a), str(b))
 
+    def _search(self, m, s, lo=0, hi=None):
+        '''return a tuple (start, end) that says where to find s within m.
+
+        If the string is found m[start:end] are the line containing
+        that string.  If start == end the string was not found and
+        they indicate the proper sorted insertion point.  This was
+        taken from bisect_left, and modified to find line start/end as
+        it goes along.
+
+        m should be a buffer or a string
+        s is a string'''
+        def advance(i, c):
+            while i < lenm and m[i] != c:
+                i += 1
+            return i
+        lenm = len(m)
+        if not hi:
+            hi = lenm
+        while lo < hi:
+            mid = (lo + hi) // 2
+            start = mid
+            while start > 0 and m[start-1] != '\n':
+                start -= 1
+            end = advance(start, '\0')
+            if m[start:end] < s:
+                # we know that after the null there are 40 bytes of sha1
+                # this translates to the bisect lo = mid + 1
+                lo = advance(end + 40, '\n') + 1
+            else:
+                # this translates to the bisect hi = mid
+                hi = start
+        end = advance(lo, '\0')
+        found = m[lo:end]
+        if cmp(s, found) == 0:
+            # we know that after the null there are 40 bytes of sha1
+            end = advance(end + 40, '\n')
+            return (lo, end+1)
+        else:
+            return (lo, lo)
+
+    def find(self, node, f):
+        '''look up entry for a single file efficiently.
+        return (node, flag) pair if found, (None, None) if not.'''
+        if self.mapcache and node == self.mapcache[0]:
+            return self.mapcache[1].get(f), self.mapcache[2].get(f)
+        text = self.revision(node)
+        start, end = self._search(text, f)
+        if start == end:
+            return None, None
+        l = text[start:end]
+        f, n = l.split('\0')
+        return bin(n[:40]), n[40:-1] == 'x'
+
     def add(self, map, flags, transaction, link, p1=None, p2=None,
             changed=None):
-
-        # returns a tuple (start, end).  If the string is found
-        # m[start:end] are the line containing that string.  If start == end
-        # the string was not found and they indicate the proper sorted 
-        # insertion point.  This was taken from bisect_left, and modified
-        # to find line start/end as it goes along.
-        #
-        # m should be a buffer or a string
-        # s is a string
-        #
-        def manifestsearch(m, s, lo=0, hi=None):
-            def advance(i, c):
-                while i < lenm and m[i] != c:
-                    i += 1
-                return i
-            lenm = len(m)
-            if not hi:
-                hi = lenm
-            while lo < hi:
-                mid = (lo + hi) // 2
-                start = mid
-                while start > 0 and m[start-1] != '\n':
-                    start -= 1
-                end = advance(start, '\0')
-                if m[start:end] < s:
-                    # we know that after the null there are 40 bytes of sha1
-                    # this translates to the bisect lo = mid + 1
-                    lo = advance(end + 40, '\n') + 1
-                else:
-                    # this translates to the bisect hi = mid
-                    hi = start
-            end = advance(lo, '\0')
-            found = m[lo:end]
-            if cmp(s, found) == 0: 
-                # we know that after the null there are 40 bytes of sha1
-                end = advance(end + 40, '\n')
-                return (lo, end+1)
-            else:
-                return (lo, lo)
-
         # apply the changes collected during the bisect loop to our addlist
         # return a delta suitable for addrevision
         def addlistdelta(addlist, x):
@@ -137,7 +150,7 @@ class manifest(revlog):
             for w in work:
                 f = w[0]
                 # bs will either be the index of the item or the insert point
-                start, end = manifestsearch(addbuf, f, start)
+                start, end = self._search(addbuf, f, start)
                 if w[1] == 0:
                     l = "%s\000%s%s\n" % (f, hex(map[f]),
                                           flags[f] and "x" or '')
diff --git a/tests/test-tags b/tests/test-tags
--- a/tests/test-tags
+++ b/tests/test-tags
@@ -32,12 +32,31 @@ hg id
 hg status
 
 hg commit -m "merge" -d "1000000 0"
+
+# create fake head, make sure tag not visible afterwards
+cp .hgtags tags
+hg tag -d "1000000 0" last
+hg rm .hgtags
+hg commit -m "remove" -d "1000000 0"
+
+mv tags .hgtags
+hg add .hgtags
+hg commit -m "readd" -d "1000000 0"
+
+hg tags
+
 # invalid tags
 echo "spam" >> .hgtags
 echo >> .hgtags
 echo "foo bar" >> .hgtags
 echo "$T invalid" | sed "s/..../a5a5/" >> .hg/localtags
 hg commit -m "tags" -d "1000000 0"
+
+# report tag parse error on other head
+hg up 3
+echo 'x y' >> .hgtags
+hg commit -m "head" -d "1000000 0"
+
 hg tags
 hg tip
 
diff --git a/tests/test-tags.out b/tests/test-tags.out
--- a/tests/test-tags.out
+++ b/tests/test-tags.out
@@ -16,17 +16,26 @@ 1 files updated, 0 files merged, 0 files
 (branch merge, don't forget to commit)
 8216907a933d+8a3ca90d111d+ tip
 M .hgtags
-.hgtags:2: ignoring invalid tag
-.hgtags:4: ignoring invalid tag
-localtags:1: ignoring invalid tag
-tip                                4:fd868a874787a7b5af31e1675666ce691c803035
+tip                                6:c6af9d771a81bb9c7f267ec03491224a9f8ba1cd
 first                              0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4
-changeset:   4:fd868a874787
-.hgtags:2: ignoring invalid tag
-.hgtags:4: ignoring invalid tag
-localtags:1: ignoring invalid tag
+.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry
+.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed
+localtags, line 1: tag 'invalid' refers to unknown node
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry
+.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed
+.hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed
+localtags, line 1: tag 'invalid' refers to unknown node
+tip                                8:4ca6f1b1a68c77be687a03aaeb1614671ba59b20
+first                              0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4
+changeset:   8:4ca6f1b1a68c
+.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry
+.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed
+.hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed
+localtags, line 1: tag 'invalid' refers to unknown node
 tag:         tip
+parent:      3:b2ef3841386b
 user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
-summary:     tags
+summary:     head