eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/patchbomb.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # patchbomb.py - sending Mercurial changesets as patch emails
       
     2 #
       
     3 #  Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 '''command to send changesets as (a series of) patch emails
       
     9 
       
    10 The series is started off with a "[PATCH 0 of N]" introduction, which
       
    11 describes the series as a whole.
       
    12 
       
    13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
       
    14 first line of the changeset description as the subject text. The
       
    15 message contains two or three body parts:
       
    16 
       
    17 - The changeset description.
       
    18 - [Optional] The result of running diffstat on the patch.
       
    19 - The patch itself, as generated by :hg:`export`.
       
    20 
       
    21 Each message refers to the first in the series using the In-Reply-To
       
    22 and References headers, so they will show up as a sequence in threaded
       
    23 mail and news readers, and in mail archives.
       
    24 
       
    25 To configure other defaults, add a section like this to your hgrc
       
    26 file::
       
    27 
       
    28   [email]
       
    29   from = My Name <my@email>
       
    30   to = recipient1, recipient2, ...
       
    31   cc = cc1, cc2, ...
       
    32   bcc = bcc1, bcc2, ...
       
    33   reply-to = address1, address2, ...
       
    34 
       
    35 Use ``[patchbomb]`` as configuration section name if you need to
       
    36 override global ``[email]`` address settings.
       
    37 
       
    38 Then you can use the :hg:`email` command to mail a series of
       
    39 changesets as a patchbomb.
       
    40 
       
    41 You can also either configure the method option in the email section
       
    42 to be a sendmail compatible mailer or fill out the [smtp] section so
       
    43 that the patchbomb extension can automatically send patchbombs
       
    44 directly from the commandline. See the [email] and [smtp] sections in
       
    45 hgrc(5) for details.
       
    46 '''
       
    47 
       
    48 import os, errno, socket, tempfile, cStringIO, time
       
    49 import email.MIMEMultipart, email.MIMEBase
       
    50 import email.Utils, email.Encoders, email.Generator
       
    51 from mercurial import cmdutil, commands, hg, mail, patch, util, discovery, url
       
    52 from mercurial.i18n import _
       
    53 from mercurial.node import bin
       
    54 
       
    55 def prompt(ui, prompt, default=None, rest=':'):
       
    56     if not ui.interactive() and default is None:
       
    57         raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
       
    58     if default:
       
    59         prompt += ' [%s]' % default
       
    60     prompt += rest
       
    61     while True:
       
    62         r = ui.prompt(prompt, default=default)
       
    63         if r:
       
    64             return r
       
    65         if default is not None:
       
    66             return default
       
    67         ui.warn(_('Please enter a valid value.\n'))
       
    68 
       
    69 def introneeded(opts, number):
       
    70     '''is an introductory message required?'''
       
    71     return number > 1 or opts.get('intro') or opts.get('desc')
       
    72 
       
    73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
       
    74               patchname=None):
       
    75 
       
    76     desc = []
       
    77     node = None
       
    78     body = ''
       
    79 
       
    80     for line in patchlines:
       
    81         if line.startswith('#'):
       
    82             if line.startswith('# Node ID'):
       
    83                 node = line.split()[-1]
       
    84             continue
       
    85         if line.startswith('diff -r') or line.startswith('diff --git'):
       
    86             break
       
    87         desc.append(line)
       
    88 
       
    89     if not patchname and not node:
       
    90         raise ValueError
       
    91 
       
    92     if opts.get('attach'):
       
    93         body = ('\n'.join(desc[1:]).strip() or
       
    94                 'Patch subject is complete summary.')
       
    95         body += '\n\n\n'
       
    96 
       
    97     if opts.get('plain'):
       
    98         while patchlines and patchlines[0].startswith('# '):
       
    99             patchlines.pop(0)
       
   100         if patchlines:
       
   101             patchlines.pop(0)
       
   102         while patchlines and not patchlines[0].strip():
       
   103             patchlines.pop(0)
       
   104 
       
   105     ds = patch.diffstat(patchlines)
       
   106     if opts.get('diffstat'):
       
   107         body += ds + '\n\n'
       
   108 
       
   109     if opts.get('attach') or opts.get('inline'):
       
   110         msg = email.MIMEMultipart.MIMEMultipart()
       
   111         if body:
       
   112             msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
       
   113         p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
       
   114         binnode = bin(node)
       
   115         # if node is mq patch, it will have the patch file's name as a tag
       
   116         if not patchname:
       
   117             patchtags = [t for t in repo.nodetags(binnode)
       
   118                          if t.endswith('.patch') or t.endswith('.diff')]
       
   119             if patchtags:
       
   120                 patchname = patchtags[0]
       
   121             elif total > 1:
       
   122                 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
       
   123                                                   binnode, seqno=idx, total=total)
       
   124             else:
       
   125                 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
       
   126         disposition = 'inline'
       
   127         if opts.get('attach'):
       
   128             disposition = 'attachment'
       
   129         p['Content-Disposition'] = disposition + '; filename=' + patchname
       
   130         msg.attach(p)
       
   131     else:
       
   132         body += '\n'.join(patchlines)
       
   133         msg = mail.mimetextpatch(body, display=opts.get('test'))
       
   134 
       
   135     flag = ' '.join(opts.get('flag'))
       
   136     if flag:
       
   137         flag = ' ' + flag
       
   138 
       
   139     subj = desc[0].strip().rstrip('. ')
       
   140     if not introneeded(opts, total):
       
   141         subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
       
   142     else:
       
   143         tlen = len(str(total))
       
   144         subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
       
   145     msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
       
   146     msg['X-Mercurial-Node'] = node
       
   147     return msg, subj, ds
       
   148 
       
   149 def patchbomb(ui, repo, *revs, **opts):
       
   150     '''send changesets by email
       
   151 
       
   152     By default, diffs are sent in the format generated by
       
   153     :hg:`export`, one per message. The series starts with a "[PATCH 0
       
   154     of N]" introduction, which describes the series as a whole.
       
   155 
       
   156     Each patch email has a Subject line of "[PATCH M of N] ...", using
       
   157     the first line of the changeset description as the subject text.
       
   158     The message contains two or three parts. First, the changeset
       
   159     description.
       
   160 
       
   161     With the -d/--diffstat option, if the diffstat program is
       
   162     installed, the result of running diffstat on the patch is inserted.
       
   163 
       
   164     Finally, the patch itself, as generated by :hg:`export`.
       
   165 
       
   166     With the -d/--diffstat or -c/--confirm options, you will be presented
       
   167     with a final summary of all messages and asked for confirmation before
       
   168     the messages are sent.
       
   169 
       
   170     By default the patch is included as text in the email body for
       
   171     easy reviewing. Using the -a/--attach option will instead create
       
   172     an attachment for the patch. With -i/--inline an inline attachment
       
   173     will be created.
       
   174 
       
   175     With -o/--outgoing, emails will be generated for patches not found
       
   176     in the destination repository (or only those which are ancestors
       
   177     of the specified revisions if any are provided)
       
   178 
       
   179     With -b/--bundle, changesets are selected as for --outgoing, but a
       
   180     single email containing a binary Mercurial bundle as an attachment
       
   181     will be sent.
       
   182 
       
   183     With -m/--mbox, instead of previewing each patchbomb message in a
       
   184     pager or sending the messages directly, it will create a UNIX
       
   185     mailbox file with the patch emails. This mailbox file can be
       
   186     previewed with any mail user agent which supports UNIX mbox
       
   187     files.
       
   188 
       
   189     With -n/--test, all steps will run, but mail will not be sent.
       
   190     You will be prompted for an email recipient address, a subject and
       
   191     an introductory message describing the patches of your patchbomb.
       
   192     Then when all is done, patchbomb messages are displayed. If the
       
   193     PAGER environment variable is set, your pager will be fired up once
       
   194     for each patchbomb message, so you can verify everything is alright.
       
   195 
       
   196     Examples::
       
   197 
       
   198       hg email -r 3000          # send patch 3000 only
       
   199       hg email -r 3000 -r 3001  # send patches 3000 and 3001
       
   200       hg email -r 3000:3005     # send patches 3000 through 3005
       
   201       hg email 3000             # send patch 3000 (deprecated)
       
   202 
       
   203       hg email -o               # send all patches not in default
       
   204       hg email -o DEST          # send all patches not in DEST
       
   205       hg email -o -r 3000       # send all ancestors of 3000 not in default
       
   206       hg email -o -r 3000 DEST  # send all ancestors of 3000 not in DEST
       
   207 
       
   208       hg email -b               # send bundle of all patches not in default
       
   209       hg email -b DEST          # send bundle of all patches not in DEST
       
   210       hg email -b -r 3000       # bundle of all ancestors of 3000 not in default
       
   211       hg email -b -r 3000 DEST  # bundle of all ancestors of 3000 not in DEST
       
   212 
       
   213       hg email -o -m mbox &&    # generate an mbox file...
       
   214         mutt -R -f mbox         # ... and view it with mutt
       
   215       hg email -o -m mbox &&    # generate an mbox file ...
       
   216         formail -s sendmail \\   # ... and use formail to send from the mbox
       
   217           -bm -t < mbox         # ... using sendmail
       
   218 
       
   219     Before using this command, you will need to enable email in your
       
   220     hgrc. See the [email] section in hgrc(5) for details.
       
   221     '''
       
   222 
       
   223     _charsets = mail._charsets(ui)
       
   224 
       
   225     bundle = opts.get('bundle')
       
   226     date = opts.get('date')
       
   227     mbox = opts.get('mbox')
       
   228     outgoing = opts.get('outgoing')
       
   229     rev = opts.get('rev')
       
   230     # internal option used by pbranches
       
   231     patches = opts.get('patches')
       
   232 
       
   233     def getoutgoing(dest, revs):
       
   234         '''Return the revisions present locally but not in dest'''
       
   235         dest = ui.expandpath(dest or 'default-push', dest or 'default')
       
   236         dest, branches = hg.parseurl(dest)
       
   237         revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
       
   238         if revs:
       
   239             revs = [repo.lookup(rev) for rev in revs]
       
   240         other = hg.repository(hg.remoteui(repo, opts), dest)
       
   241         ui.status(_('comparing with %s\n') % url.hidepassword(dest))
       
   242         o = discovery.findoutgoing(repo, other)
       
   243         if not o:
       
   244             ui.status(_("no changes found\n"))
       
   245             return []
       
   246         o = repo.changelog.nodesbetween(o, revs)[0]
       
   247         return [str(repo.changelog.rev(r)) for r in o]
       
   248 
       
   249     def getpatches(revs):
       
   250         for r in cmdutil.revrange(repo, revs):
       
   251             output = cStringIO.StringIO()
       
   252             cmdutil.export(repo, [r], fp=output,
       
   253                          opts=patch.diffopts(ui, opts))
       
   254             yield output.getvalue().split('\n')
       
   255 
       
   256     def getbundle(dest):
       
   257         tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
       
   258         tmpfn = os.path.join(tmpdir, 'bundle')
       
   259         try:
       
   260             commands.bundle(ui, repo, tmpfn, dest, **opts)
       
   261             return open(tmpfn, 'rb').read()
       
   262         finally:
       
   263             try:
       
   264                 os.unlink(tmpfn)
       
   265             except:
       
   266                 pass
       
   267             os.rmdir(tmpdir)
       
   268 
       
   269     if not (opts.get('test') or mbox):
       
   270         # really sending
       
   271         mail.validateconfig(ui)
       
   272 
       
   273     if not (revs or rev or outgoing or bundle or patches):
       
   274         raise util.Abort(_('specify at least one changeset with -r or -o'))
       
   275 
       
   276     if outgoing and bundle:
       
   277         raise util.Abort(_("--outgoing mode always on with --bundle;"
       
   278                            " do not re-specify --outgoing"))
       
   279 
       
   280     if outgoing or bundle:
       
   281         if len(revs) > 1:
       
   282             raise util.Abort(_("too many destinations"))
       
   283         dest = revs and revs[0] or None
       
   284         revs = []
       
   285 
       
   286     if rev:
       
   287         if revs:
       
   288             raise util.Abort(_('use only one form to specify the revision'))
       
   289         revs = rev
       
   290 
       
   291     if outgoing:
       
   292         revs = getoutgoing(dest, rev)
       
   293     if bundle:
       
   294         opts['revs'] = revs
       
   295 
       
   296     # start
       
   297     if date:
       
   298         start_time = util.parsedate(date)
       
   299     else:
       
   300         start_time = util.makedate()
       
   301 
       
   302     def genmsgid(id):
       
   303         return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
       
   304 
       
   305     def getdescription(body, sender):
       
   306         if opts.get('desc'):
       
   307             body = open(opts.get('desc')).read()
       
   308         else:
       
   309             ui.write(_('\nWrite the introductory message for the '
       
   310                        'patch series.\n\n'))
       
   311             body = ui.edit(body, sender)
       
   312         return body
       
   313 
       
   314     def getpatchmsgs(patches, patchnames=None):
       
   315         jumbo = []
       
   316         msgs = []
       
   317 
       
   318         ui.write(_('This patch series consists of %d patches.\n\n')
       
   319                  % len(patches))
       
   320 
       
   321         name = None
       
   322         for i, p in enumerate(patches):
       
   323             jumbo.extend(p)
       
   324             if patchnames:
       
   325                 name = patchnames[i]
       
   326             msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
       
   327                             len(patches), name)
       
   328             msgs.append(msg)
       
   329 
       
   330         if introneeded(opts, len(patches)):
       
   331             tlen = len(str(len(patches)))
       
   332 
       
   333             flag = ' '.join(opts.get('flag'))
       
   334             if flag:
       
   335                 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
       
   336             else:
       
   337                 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
       
   338             subj += ' ' + (opts.get('subject') or
       
   339                            prompt(ui, 'Subject: ', rest=subj))
       
   340 
       
   341             body = ''
       
   342             ds = patch.diffstat(jumbo)
       
   343             if ds and opts.get('diffstat'):
       
   344                 body = '\n' + ds
       
   345 
       
   346             body = getdescription(body, sender)
       
   347             msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
       
   348             msg['Subject'] = mail.headencode(ui, subj, _charsets,
       
   349                                              opts.get('test'))
       
   350 
       
   351             msgs.insert(0, (msg, subj, ds))
       
   352         return msgs
       
   353 
       
   354     def getbundlemsgs(bundle):
       
   355         subj = (opts.get('subject')
       
   356                 or prompt(ui, 'Subject:', 'A bundle for your repository'))
       
   357 
       
   358         body = getdescription('', sender)
       
   359         msg = email.MIMEMultipart.MIMEMultipart()
       
   360         if body:
       
   361             msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
       
   362         datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
       
   363         datapart.set_payload(bundle)
       
   364         bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
       
   365         datapart.add_header('Content-Disposition', 'attachment',
       
   366                             filename=bundlename)
       
   367         email.Encoders.encode_base64(datapart)
       
   368         msg.attach(datapart)
       
   369         msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
       
   370         return [(msg, subj, None)]
       
   371 
       
   372     sender = (opts.get('from') or ui.config('email', 'from') or
       
   373               ui.config('patchbomb', 'from') or
       
   374               prompt(ui, 'From', ui.username()))
       
   375 
       
   376     if patches:
       
   377         msgs = getpatchmsgs(patches, opts.get('patchnames'))
       
   378     elif bundle:
       
   379         msgs = getbundlemsgs(getbundle(dest))
       
   380     else:
       
   381         msgs = getpatchmsgs(list(getpatches(revs)))
       
   382 
       
   383     showaddrs = []
       
   384 
       
   385     def getaddrs(opt, prpt=None, default=None):
       
   386         addrs = opts.get(opt.replace('-', '_'))
       
   387         if opt != 'reply-to':
       
   388             showaddr = '%s:' % opt.capitalize()
       
   389         else:
       
   390             showaddr = 'Reply-To:'
       
   391 
       
   392         if addrs:
       
   393             showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
       
   394             return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
       
   395 
       
   396         addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
       
   397         if not addrs and prpt:
       
   398             addrs = prompt(ui, prpt, default)
       
   399 
       
   400         if addrs:
       
   401             showaddrs.append('%s %s' % (showaddr, addrs))
       
   402         return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
       
   403 
       
   404     to = getaddrs('to', 'To')
       
   405     cc = getaddrs('cc', 'Cc', '')
       
   406     bcc = getaddrs('bcc')
       
   407     replyto = getaddrs('reply-to')
       
   408 
       
   409     if opts.get('diffstat') or opts.get('confirm'):
       
   410         ui.write(_('\nFinal summary:\n\n'))
       
   411         ui.write('From: %s\n' % sender)
       
   412         for addr in showaddrs:
       
   413             ui.write('%s\n' % addr)
       
   414         for m, subj, ds in msgs:
       
   415             ui.write('Subject: %s\n' % subj)
       
   416             if ds:
       
   417                 ui.write(ds)
       
   418         ui.write('\n')
       
   419         if ui.promptchoice(_('are you sure you want to send (yn)?'),
       
   420                            (_('&Yes'), _('&No'))):
       
   421             raise util.Abort(_('patchbomb canceled'))
       
   422 
       
   423     ui.write('\n')
       
   424 
       
   425     parent = opts.get('in_reply_to') or None
       
   426     # angle brackets may be omitted, they're not semantically part of the msg-id
       
   427     if parent is not None:
       
   428         if not parent.startswith('<'):
       
   429             parent = '<' + parent
       
   430         if not parent.endswith('>'):
       
   431             parent += '>'
       
   432 
       
   433     first = True
       
   434 
       
   435     sender_addr = email.Utils.parseaddr(sender)[1]
       
   436     sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
       
   437     sendmail = None
       
   438     for i, (m, subj, ds) in enumerate(msgs):
       
   439         try:
       
   440             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
       
   441         except TypeError:
       
   442             m['Message-Id'] = genmsgid('patchbomb')
       
   443         if parent:
       
   444             m['In-Reply-To'] = parent
       
   445             m['References'] = parent
       
   446         if first:
       
   447             parent = m['Message-Id']
       
   448             first = False
       
   449 
       
   450         m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
       
   451         m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
       
   452 
       
   453         start_time = (start_time[0] + 1, start_time[1])
       
   454         m['From'] = sender
       
   455         m['To'] = ', '.join(to)
       
   456         if cc:
       
   457             m['Cc']  = ', '.join(cc)
       
   458         if bcc:
       
   459             m['Bcc'] = ', '.join(bcc)
       
   460         if replyto:
       
   461             m['Reply-To'] = ', '.join(replyto)
       
   462         if opts.get('test'):
       
   463             ui.status(_('Displaying '), subj, ' ...\n')
       
   464             ui.flush()
       
   465             if 'PAGER' in os.environ and not ui.plain():
       
   466                 fp = util.popen(os.environ['PAGER'], 'w')
       
   467             else:
       
   468                 fp = ui
       
   469             generator = email.Generator.Generator(fp, mangle_from_=False)
       
   470             try:
       
   471                 generator.flatten(m, 0)
       
   472                 fp.write('\n')
       
   473             except IOError, inst:
       
   474                 if inst.errno != errno.EPIPE:
       
   475                     raise
       
   476             if fp is not ui:
       
   477                 fp.close()
       
   478         elif mbox:
       
   479             ui.status(_('Writing '), subj, ' ...\n')
       
   480             ui.progress(_('writing'), i, item=subj, total=len(msgs))
       
   481             fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
       
   482             generator = email.Generator.Generator(fp, mangle_from_=True)
       
   483             # Should be time.asctime(), but Windows prints 2-characters day
       
   484             # of month instead of one. Make them print the same thing.
       
   485             date = time.strftime('%a %b %d %H:%M:%S %Y',
       
   486                                  time.localtime(start_time[0]))
       
   487             fp.write('From %s %s\n' % (sender_addr, date))
       
   488             generator.flatten(m, 0)
       
   489             fp.write('\n\n')
       
   490             fp.close()
       
   491         else:
       
   492             if not sendmail:
       
   493                 sendmail = mail.connect(ui)
       
   494             ui.status(_('Sending '), subj, ' ...\n')
       
   495             ui.progress(_('sending'), i, item=subj, total=len(msgs))
       
   496             # Exim does not remove the Bcc field
       
   497             del m['Bcc']
       
   498             fp = cStringIO.StringIO()
       
   499             generator = email.Generator.Generator(fp, mangle_from_=False)
       
   500             generator.flatten(m, 0)
       
   501             sendmail(sender, to + bcc + cc, fp.getvalue())
       
   502 
       
   503     ui.progress(_('writing'), None)
       
   504     ui.progress(_('sending'), None)
       
   505 
       
   506 emailopts = [
       
   507           ('a', 'attach', None, _('send patches as attachments')),
       
   508           ('i', 'inline', None, _('send patches as inline attachments')),
       
   509           ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
       
   510           ('c', 'cc', [], _('email addresses of copy recipients')),
       
   511           ('', 'confirm', None, _('ask for confirmation before sending')),
       
   512           ('d', 'diffstat', None, _('add diffstat output to messages')),
       
   513           ('', 'date', '', _('use the given date as the sending date')),
       
   514           ('', 'desc', '', _('use the given file as the series description')),
       
   515           ('f', 'from', '', _('email address of sender')),
       
   516           ('n', 'test', None, _('print messages that would be sent')),
       
   517           ('m', 'mbox', '',
       
   518            _('write messages to mbox file instead of sending them')),
       
   519           ('', 'reply-to', [], _('email addresses replies should be sent to')),
       
   520           ('s', 'subject', '',
       
   521            _('subject of first message (intro or single patch)')),
       
   522           ('', 'in-reply-to', '',
       
   523            _('message identifier to reply to')),
       
   524           ('', 'flag', [], _('flags to add in subject prefixes')),
       
   525           ('t', 'to', [], _('email addresses of recipients')),
       
   526          ]
       
   527 
       
   528 
       
   529 cmdtable = {
       
   530     "email":
       
   531         (patchbomb,
       
   532          [('g', 'git', None, _('use git extended diff format')),
       
   533           ('', 'plain', None, _('omit hg patch header')),
       
   534           ('o', 'outgoing', None,
       
   535            _('send changes not found in the target repository')),
       
   536           ('b', 'bundle', None,
       
   537            _('send changes not in target as a binary bundle')),
       
   538           ('', 'bundlename', 'bundle',
       
   539            _('name of the bundle attachment file'), _('NAME')),
       
   540           ('r', 'rev', [],
       
   541            _('a revision to send'), _('REV')),
       
   542           ('', 'force', None,
       
   543            _('run even when remote repository is unrelated '
       
   544              '(with -b/--bundle)')),
       
   545           ('', 'base', [],
       
   546            _('a base changeset to specify instead of a destination '
       
   547              '(with -b/--bundle)'),
       
   548            _('REV')),
       
   549           ('', 'intro', None,
       
   550            _('send an introduction email for a single patch')),
       
   551          ] + emailopts + commands.remoteopts,
       
   552          _('hg email [OPTION]... [DEST]...'))
       
   553 }