[PATCH] hg tag: add/remove multiple tags; run tag hook only once

John Coomes John.Coomes at sun.com
Mon Dec 17 18:34:35 CST 2007


Patch to allow 'hg tag' to add or remove multiple tags.  Some examples
at

http://www.selenic.com/pipermail/mercurial-devel/2007-December/003667.html

The pretag and tag hooks are run once for each tag.  The intent is
that all tags should be added, or none, so if any pretag hook (or
other correctness check) fails no tags are added.

This also fixes a bug which caused the tag hook to be run twice for a
global tag.

# HG changeset patch
# User John Coomes <john.coomes at sun.com>
# Date 1197923354 28800
# Node ID 8851170e9360bfd29b74fd3378d632b1ce6e1240
# Parent  04c76f296ad6f8e32180d6c0fd3982e5faaff3cd
hg tag:  add/remove multiple tags; run tag hook only once

- allow multiple tags to be added or removed in one invocation, e.g.,
  hg tag -r 42 build-25 beta-1
- remove 'hg tag NAME REV' deprecation warning; that syntax is now
  interpreted as 'hg tag NAME1 NAME2'
- run the tag hook for global tags just once per tag, after the commit

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2397,8 +2397,14 @@ def status(ui, repo, *pats, **opts):
                 if copied:
                     ui.write('  %s%s' % (repo.pathto(copied, cwd), end))
 
-def tag(ui, repo, name, rev_=None, **opts):
-    """add a tag for the current or given revision
+def _tagabort(singular, plural, badtags):
+    """abort with a message listing the offending tag names"""
+    if len(badtags) == 1:
+        raise util.Abort(singular % badtags[0])
+    raise util.Abort(plural % (", ".join(badtags)))
+
+def tag(ui, repo, name1, *othernames, **opts):
+    """add one or more tags for the current or given revision
 
     Name a particular revision using <name>.
 
@@ -2415,36 +2421,51 @@ def tag(ui, repo, name, rev_=None, **opt
     necessary.  The file '.hg/localtags' is used for local tags (not
     shared among repositories).
     """
-    if name in ['tip', '.', 'null']:
-        raise util.Abort(_("the name '%s' is reserved") % name)
-    if rev_ is not None:
-        ui.warn(_("use of 'hg tag NAME [REV]' is deprecated, "
-                  "please use 'hg tag [-r REV] NAME' instead\n"))
-        if opts['rev']:
-            raise util.Abort(_("use only one form to specify the revision"))
+    rev_ = None
+    names = (name1,) + othernames
+
+    if othernames and len(names) != len({}.fromkeys(names)):
+        badlist = list(names)
+        for key in {}.fromkeys(names):
+            badlist.remove(key)
+        _tagabort(_("the name %s appears more than once"),
+                  _("the names %s appear more than once"),
+                  ["'" + n + "'" for n in util.unique(badlist)])
+    badlist = ["'" + n + "'" for n in names if n in ['tip', '.', 'null']]
+    if badlist:
+        _tagabort(_("the name %s is reserved"),
+                  _("the names %s are reserved"), badlist)
+
     if opts['rev'] and opts['remove']:
         raise util.Abort(_("--rev and --remove are incompatible"))
     if opts['rev']:
         rev_ = opts['rev']
     message = opts['message']
+    msgplural = (len(names) > 1 and 's') or ''
     if opts['remove']:
-        if not name in repo.tags():
-            raise util.Abort(_('tag %s does not exist') % name)
+        badlist = ["'" + n + "'" for n in names if n not in repo.tags()]
+        if badlist:
+            _tagabort(_('the tag %s does not exist'),
+                      _('the tags %s do not exist'), badlist)
         rev_ = nullid
         if not message:
-            message = _('Removed tag %s') % name
-    elif name in repo.tags() and not opts['force']:
-        raise util.Abort(_('a tag named %s already exists (use -f to force)')
-                         % name)
+            message = _('Removed tag%s %s') % (msgplural, ", ".join(names))
+    elif not opts['force']:
+        badlist = ["'" + n + "'" for n in names if n in repo.tags()]
+        if badlist:
+            _tagabort(_('the tag %s already exists (use -f to force)'),
+                      _('the tags %s already exist (use -f to force)'),
+                      badlist)
     if not rev_ and repo.dirstate.parents()[1] != nullid:
         raise util.Abort(_('uncommitted merge - please provide a '
                            'specific revision'))
     r = repo.changectx(rev_).node()
 
     if not message:
-        message = _('Added tag %s for changeset %s') % (name, short(r))
+        message = (_('Added tag%s %s for changeset %s') %
+                   (msgplural, ", ".join(names), short(r)))
 
-    repo.tag(name, r, message, opts['local'], opts['user'], opts['date'])
+    repo.tag(names, r, message, opts['local'], opts['user'], opts['date'])
 
 def tags(ui, repo):
     """list repository tags
@@ -2938,7 +2959,7 @@ table = {
           # -l/--local is already there, commitopts cannot be used
           ('m', 'message', '', _('use <text> as commit message')),
          ] + commitopts2,
-         _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME')),
+         _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME ...')),
     "tags": (tags, [], _('hg tags')),
     "tip":
         (tip,
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -107,22 +107,25 @@ class localrepository(repo.repository):
 
     tag_disallowed = ':\r\n'
 
-    def _tag(self, name, node, message, local, user, date, parent=None,
+    def _tag(self, names, node, message, local, user, date, parent=None,
              extra={}):
         use_dirstate = parent is None
 
+        allnames = "".join(names)
         for c in self.tag_disallowed:
-            if c in name:
+            if c in allnames:
                 raise util.Abort(_('%r cannot be used in a tag name') % c)
 
-        self.hook('pretag', throw=True, node=hex(node), tag=name, local=local)
+        for name in names:
+            self.hook('pretag', throw=True, node=hex(node), tag=name,
+                      local=local)
 
-        def writetag(fp, name, munge, prevtags):
+        def writetags(fp, names, munge, prevtags):
             if prevtags and prevtags[-1] != '\n':
                 fp.write('\n')
-            fp.write('%s %s\n' % (hex(node), munge and munge(name) or name))
+            for name in names:
+                fp.write('%s %s\n' % (hex(node), munge and munge(name) or name))
             fp.close()
-            self.hook('tag', node=hex(node), tag=name, local=local)
 
         prevtags = ''
         if local:
@@ -134,7 +137,9 @@ class localrepository(repo.repository):
                 prevtags = fp.read()
 
             # local tags are stored in the current charset
-            writetag(fp, name, None, prevtags)
+            writetags(fp, names, None, prevtags)
+            for name in names:
+                self.hook('tag', node=hex(node), tag=name, local=local)
             return
 
         if use_dirstate:
@@ -154,7 +159,7 @@ class localrepository(repo.repository):
                 fp.write(prevtags)
 
         # committed tags are stored in UTF-8
-        writetag(fp, name, util.fromlocal, prevtags)
+        writetags(fp, names, util.fromlocal, prevtags)
 
         if use_dirstate and '.hgtags' not in self.dirstate:
             self.add(['.hgtags'])
@@ -162,12 +167,13 @@ class localrepository(repo.repository):
         tagnode = self.commit(['.hgtags'], message, user, date, p1=parent,
                               extra=extra)
 
-        self.hook('tag', node=hex(node), tag=name, local=local)
+        for name in names:
+            self.hook('tag', node=hex(node), tag=name, local=local)
 
         return tagnode
 
-    def tag(self, name, node, message, local, user, date):
-        '''tag a revision with a symbolic name.
+    def tag(self, names, node, message, local, user, date):
+        '''tag a revision with one or more symbolic names.
 
         if local is True, the tag is stored in a per-repository file.
         otherwise, it is stored in the .hgtags file, and a new
@@ -189,8 +195,7 @@ class localrepository(repo.repository):
                 raise util.Abort(_('working copy of .hgtags is changed '
                                    '(please commit .hgtags manually)'))
 
-
-        self._tag(name, node, message, local, user, date)
+        self._tag(names, node, message, local, user, date)
 
     def tags(self):
         '''return a mapping of tag to node'''
diff --git a/tests/test-globalopts.out b/tests/test-globalopts.out
--- a/tests/test-globalopts.out
+++ b/tests/test-globalopts.out
@@ -181,7 +181,7 @@ list of commands:
  serve        export the repository via HTTP
  showconfig   show combined config settings from all hgrc files
  status       show changed files in the working directory
- tag          add a tag for the current or given revision
+ tag          add one or more tags for the current or given revision
  tags         list repository tags
  tip          show the tip revision
  unbundle     apply one or more changegroup files
@@ -233,7 +233,7 @@ list of commands:
  serve        export the repository via HTTP
  showconfig   show combined config settings from all hgrc files
  status       show changed files in the working directory
- tag          add a tag for the current or given revision
+ tag          add one or more tags for the current or given revision
  tags         list repository tags
  tip          show the tip revision
  unbundle     apply one or more changegroup files
diff --git a/tests/test-help.out b/tests/test-help.out
--- a/tests/test-help.out
+++ b/tests/test-help.out
@@ -79,7 +79,7 @@ list of commands:
  serve        export the repository via HTTP
  showconfig   show combined config settings from all hgrc files
  status       show changed files in the working directory
- tag          add a tag for the current or given revision
+ tag          add one or more tags for the current or given revision
  tags         list repository tags
  tip          show the tip revision
  unbundle     apply one or more changegroup files
@@ -127,7 +127,7 @@ use "hg -v help" to show aliases and glo
  serve        export the repository via HTTP
  showconfig   show combined config settings from all hgrc files
  status       show changed files in the working directory
- tag          add a tag for the current or given revision
+ tag          add one or more tags for the current or given revision
  tags         list repository tags
  tip          show the tip revision
  unbundle     apply one or more changegroup files
diff --git a/tests/test-hook.out b/tests/test-hook.out
--- a/tests/test-hook.out
+++ b/tests/test-hook.out
@@ -40,7 +40,6 @@ added 3 changesets with 2 changes to 2 f
 added 3 changesets with 2 changes to 2 files
 (run 'hg update' to get a working copy)
 pretag hook: HG_LOCAL=0 HG_NODE=4c52fb2e402287dd5dc052090682536c8406c321 HG_TAG=a 
-tag hook: HG_LOCAL=0 HG_NODE=4c52fb2e402287dd5dc052090682536c8406c321 HG_TAG=a 
 precommit hook: HG_PARENT1=4c52fb2e402287dd5dc052090682536c8406c321 
 pretxncommit hook: HG_NODE=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 HG_PARENT1=4c52fb2e402287dd5dc052090682536c8406c321 
 4:8ea2ef7ad3e8
diff --git a/tests/test-tag b/tests/test-tag
--- a/tests/test-tag
+++ b/tests/test-tag
@@ -10,11 +10,21 @@ hg history
 
 echo foo >> .hgtags
 hg tag -d "1000000 0" "bleah2" || echo "failed"
-hg tag -d "1000000 0" -r 0 "bleah2" 1 || echo "failed"
 
 hg revert .hgtags
+hg tag -d "1000000 0" -r 0 x y z y y z || echo "failed"
+hg tag -d "1000000 0" tip tap null nada . dot || echo "failed"
+hg tag -d "1000000 0" "bleah" || echo "failed"
+hg tag -d "1000000 0" "bleah" "blecch" || echo "failed"
+
+hg tag -d "1000000 0" --remove "blecch" || echo "failed"
+hg tag -d "1000000 0" --remove "bleah" "blecch" "blough" || echo "failed"
+
 hg tag -d "1000000 0" -r 0 "bleah0"
-hg tag -l -d "1000000 0" "bleah1" 1
+hg tag -l -d "1000000 0" -r 1 "bleah1"
+hg tag -d "1000000 0" gack gawk gorp
+hg tag -d "1000000 0" -f gack
+hg tag -d "1000000 0" --remove gack gorp
 
 cat .hgtags
 cat .hg/localtags
diff --git a/tests/test-tag.out b/tests/test-tag.out
--- a/tests/test-tag.out
+++ b/tests/test-tag.out
@@ -18,12 +18,26 @@ summary:     test
 
 abort: working copy of .hgtags is changed (please commit .hgtags manually)
 failed
-use of 'hg tag NAME [REV]' is deprecated, please use 'hg tag [-r REV] NAME' instead
-abort: use only one form to specify the revision
+abort: the names 'y', 'z' appear more than once
 failed
-use of 'hg tag NAME [REV]' is deprecated, please use 'hg tag [-r REV] NAME' instead
+abort: the names 'tip', 'null', '.' are reserved
+failed
+abort: the tag 'bleah' already exists (use -f to force)
+failed
+abort: the tag 'bleah' already exists (use -f to force)
+failed
+abort: the tag 'blecch' does not exist
+failed
+abort: the tags 'blecch', 'blough' do not exist
+failed
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah0
+868cc8fbb43b754ad09fa109885d243fc49adae7 gack
+868cc8fbb43b754ad09fa109885d243fc49adae7 gawk
+868cc8fbb43b754ad09fa109885d243fc49adae7 gorp
+8990e39091eb986fa5930705ffb2bf68ddbe8133 gack
+0000000000000000000000000000000000000000 gack
+0000000000000000000000000000000000000000 gorp
 3ecf002a1c572a2f3bb4e665417e60fca65bbd42 bleah1
 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 foobar
diff --git a/tests/test-tags.out b/tests/test-tags.out
--- a/tests/test-tags.out
+++ b/tests/test-tags.out
@@ -50,7 +50,7 @@ summary:     Removed tag bar
 
 tip                                5:57e1983b4a60
 % remove nonexistent tag
-abort: tag foobar does not exist
+abort: the tag 'foobar' does not exist
 changeset:   5:57e1983b4a60
 tag:         tip
 user:        test
@@ -62,7 +62,7 @@ 1 files updated, 0 files merged, 0 files
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 tip                                6:b5ff9d142648
 bar                                0:b409d9da318e
-abort: a tag named bar already exists (use -f to force)
+abort: the tag 'bar' already exists (use -f to force)
 tip                                6:b5ff9d142648
 bar                                0:b409d9da318e
 adding foo





More information about the Mercurial-devel mailing list