diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -2589,8 +2589,8 @@ def status(ui, repo, *pats, **opts): 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 . @@ -2609,47 +2609,49 @@ def tag(ui, repo, name, rev_=None, **opt 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 @@ table = { # -l/--local is already there, commitopts cannot be used ('m', 'message', '', _('use 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 @@ -124,21 +124,29 @@ 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 + 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 @@ class localrepository(repo.repository): 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 @@ 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']) @@ -180,20 +189,24 @@ 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 + 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 @@ 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 @@ -188,7 +188,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 @@ -241,7 +241,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 @@ -80,7 +80,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 @@ -129,7 +129,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-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" 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 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: 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 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: tag 'foobar' does not exist changeset: 5:57e1983b4a60 tag: tip user: test @@ -62,7 +62,7 @@ bar 0:b40 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 @@ bar 2:72b 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