changeset 6321:55ba3bc5b8fd

tag: allow multiple tags to be added or removed - Example: "hg tag -r 42 build-25 beta-1" will add tags build-25 and beta-1 for rev 42. - The deprecated and undocumented usage "hg tag arg1 arg2" used to emit a warning, then add tag arg1 for rev arg2 (equivalent to "hg tag -r arg2 arg1"). It will now add tags arg1 and arg2 for the current revision. - If one tag triggers an error, no tags are added/removed (all or nothing).
author John Coomes <john.coomes@sun.com>
date Fri, 14 Mar 2008 15:38:56 -0700
parents 9a9b02bcbcf4
children 108636b9b981
files mercurial/commands.py mercurial/localrepo.py tests/test-globalopts.out tests/test-help.out tests/test-tag tests/test-tag.out tests/test-tags.out
diffstat 7 files changed, 92 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Mon Mar 17 19:28:46 2008 +0200
+++ b/mercurial/commands.py	Fri Mar 14 15:38:56 2008 -0700
@@ -2589,8 +2589,8 @@
             if f in copy and (f in added or f in showcopy):
                 ui.write('  %s%s' % (repo.pathto(copy[f], cwd), end))
 
-def tag(ui, repo, name, rev_=None, **opts):
-    """add a tag for the current or given revision
+def tag(ui, repo, name1, *names, **opts):
+    """add one or more tags for the current or given revision
 
     Name a particular revision using <name>.
 
@@ -2609,47 +2609,49 @@
 
     See 'hg help dates' for a list of formats valid for -d/--date.
     """
-    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,) + names
+    if len(names) != len(dict.fromkeys(names)):
+        raise util.Abort(_('tag names must be unique'))
+    for n in names:
+        if n in ['tip', '.', 'null']:
+            raise util.Abort(_('the name \'%s\' is reserved') % n)
     if opts['rev'] and opts['remove']:
         raise util.Abort(_("--rev and --remove are incompatible"))
     if opts['rev']:
         rev_ = opts['rev']
     message = opts['message']
     if opts['remove']:
-        tagtype = repo.tagtype(name)
-
-        if not tagtype:
-            raise util.Abort(_('tag %s does not exist') % name)
-        if opts['local'] and tagtype == 'global':
-           raise util.Abort(_('%s tag is global') % name)
-        if not opts['local'] and tagtype == 'local':
-           raise util.Abort(_('%s tag is local') % name)
-
+        expectedtype = opts['local'] and 'local' or 'global'
+        for n in names:
+            if not repo.tagtype(n):
+                raise util.Abort(_('tag \'%s\' does not exist') % n)
+            if repo.tagtype(n) != expectedtype:
+                raise util.Abort(_('tag \'%s\' is not a %s tag') %
+                                 (n, expectedtype))
         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') % ', '.join(names)
+    elif not opts['force']:
+        for n in names:
+            if n in repo.tags():
+                raise util.Abort(_('tag \'%s\' already exists '
+                                   '(use -f to force)') % n)
     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 for changeset %s') %
+                   (', '.join(names), short(r)))
 
     date = opts.get('date')
     if date:
         date = util.parsedate(date)
 
-    repo.tag(name, r, message, opts['local'], opts['user'], date)
+    repo.tag(names, r, message, opts['local'], opts['user'], date)
 
 def tags(ui, repo):
     """list repository tags
@@ -3190,7 +3192,7 @@
           # -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,
--- a/mercurial/localrepo.py	Mon Mar 17 19:28:46 2008 +0200
+++ b/mercurial/localrepo.py	Fri Mar 14 15:38:56 2008 -0700
@@ -124,21 +124,29 @@
 
     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
 
+        if isinstance(names, str):
+            allchars = names
+            names = (names,)
+        else:
+            allchars = ''.join(names)
         for c in self.tag_disallowed:
-            if c in name:
+            if c in allchars:
                 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):
             fp.seek(0, 2)
             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()
 
         prevtags = ''
@@ -151,8 +159,9 @@
                 prevtags = fp.read()
 
             # local tags are stored in the current charset
-            writetag(fp, name, None, prevtags)
-            self.hook('tag', node=hex(node), tag=name, local=local)
+            writetags(fp, names, None, prevtags)
+            for name in names:
+                self.hook('tag', node=hex(node), tag=name, local=local)
             return
 
         if use_dirstate:
@@ -172,7 +181,7 @@
                 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'])
@@ -180,20 +189,24 @@
         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
+        names is a list of strings or, when adding a single tag, names may be a
+        string.
+        
+        if local is True, the tags are stored in a per-repository file.
+        otherwise, they are stored in the .hgtags file, and a new
         changeset is committed with the change.
 
         keyword arguments:
 
-        local: whether to store tag in non-version-controlled file
+        local: whether to store tags in non-version-controlled file
         (default False)
 
         message: commit message to use if committing
@@ -207,7 +220,7 @@
                 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'''
--- a/tests/test-globalopts.out	Mon Mar 17 19:28:46 2008 +0200
+++ b/tests/test-globalopts.out	Fri Mar 14 15:38:56 2008 -0700
@@ -188,7 +188,7 @@
  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
@@ -241,7 +241,7 @@
  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
--- a/tests/test-help.out	Mon Mar 17 19:28:46 2008 +0200
+++ b/tests/test-help.out	Fri Mar 14 15:38:56 2008 -0700
@@ -80,7 +80,7 @@
  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
@@ -129,7 +129,7 @@
  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
--- a/tests/test-tag	Mon Mar 17 19:28:46 2008 +0200
+++ b/tests/test-tag	Fri Mar 14 15:38:56 2008 -0700
@@ -10,11 +10,21 @@
 
 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" tap nada dot tip null . || echo "failed"
+hg tag -d "1000000 0" "bleah" || echo "failed"
+hg tag -d "1000000 0" "blecch" "bleah" || 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
--- a/tests/test-tag.out	Mon Mar 17 19:28:46 2008 +0200
+++ b/tests/test-tag.out	Fri Mar 14 15:38:56 2008 -0700
@@ -18,12 +18,26 @@
 
 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: tag names must be unique
+failed
+abort: the name 'tip' is reserved
+failed
+abort: tag 'bleah' already exists (use -f to force)
 failed
-use of 'hg tag NAME [REV]' is deprecated, please use 'hg tag [-r REV] NAME' instead
+abort: tag 'bleah' already exists (use -f to force)
+failed
+abort: tag 'blecch' does not exist
+failed
+abort: tag 'blecch' does not exist
+failed
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah0
+868cc8fbb43b754ad09fa109885d243fc49adae7 gack
+868cc8fbb43b754ad09fa109885d243fc49adae7 gawk
+868cc8fbb43b754ad09fa109885d243fc49adae7 gorp
+3807bcf62c5614cb6c16436b514d7764ca5f1631 gack
+0000000000000000000000000000000000000000 gack
+0000000000000000000000000000000000000000 gorp
 3ecf002a1c572a2f3bb4e665417e60fca65bbd42 bleah1
 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 foobar
--- a/tests/test-tags.out	Mon Mar 17 19:28:46 2008 +0200
+++ b/tests/test-tags.out	Fri Mar 14 15:38:56 2008 -0700
@@ -50,7 +50,7 @@
 
 tip                                5:57e1983b4a60
 % remove nonexistent tag
-abort: tag foobar does not exist
+abort: tag 'foobar' does not exist
 changeset:   5:57e1983b4a60
 tag:         tip
 user:        test
@@ -62,7 +62,7 @@
 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: tag 'bar' already exists (use -f to force)
 tip                                6:b5ff9d142648
 bar                                0:b409d9da318e
 adding foo
@@ -72,8 +72,8 @@
 tip                                4:40af5d225513
 bar                                2:72b852876a42
 adding foo
-abort: localtag tag is local
-abort: globaltag tag is global
+abort: tag 'localtag' is not a global tag
+abort: tag 'globaltag' is not a local tag
 tip                                1:a0b6fe111088
 localtag                           0:bbd179dfa0a7 local
 globaltag                          0:bbd179dfa0a7