hgext/patchbomb.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
   108 
   108 
   109 configtable = {}
   109 configtable = {}
   110 configitem = registrar.configitem(configtable)
   110 configitem = registrar.configitem(configtable)
   111 
   111 
   112 configitem(
   112 configitem(
   113     'patchbomb', 'bundletype', default=None,
   113     b'patchbomb', b'bundletype', default=None,
   114 )
   114 )
   115 configitem(
   115 configitem(
   116     'patchbomb', 'bcc', default=None,
   116     b'patchbomb', b'bcc', default=None,
   117 )
   117 )
   118 configitem(
   118 configitem(
   119     'patchbomb', 'cc', default=None,
   119     b'patchbomb', b'cc', default=None,
   120 )
   120 )
   121 configitem(
   121 configitem(
   122     'patchbomb', 'confirm', default=False,
   122     b'patchbomb', b'confirm', default=False,
   123 )
   123 )
   124 configitem(
   124 configitem(
   125     'patchbomb', 'flagtemplate', default=None,
   125     b'patchbomb', b'flagtemplate', default=None,
   126 )
   126 )
   127 configitem(
   127 configitem(
   128     'patchbomb', 'from', default=None,
   128     b'patchbomb', b'from', default=None,
   129 )
   129 )
   130 configitem(
   130 configitem(
   131     'patchbomb', 'intro', default='auto',
   131     b'patchbomb', b'intro', default=b'auto',
   132 )
   132 )
   133 configitem(
   133 configitem(
   134     'patchbomb', 'publicurl', default=None,
   134     b'patchbomb', b'publicurl', default=None,
   135 )
   135 )
   136 configitem(
   136 configitem(
   137     'patchbomb', 'reply-to', default=None,
   137     b'patchbomb', b'reply-to', default=None,
   138 )
   138 )
   139 configitem(
   139 configitem(
   140     'patchbomb', 'to', default=None,
   140     b'patchbomb', b'to', default=None,
   141 )
   141 )
   142 
   142 
   143 if pycompat.ispy3:
   143 if pycompat.ispy3:
   144     _bytesgenerator = emailgen.BytesGenerator
   144     _bytesgenerator = emailgen.BytesGenerator
   145 else:
   145 else:
   147 
   147 
   148 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   148 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   149 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   149 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   150 # be specifying the version(s) of Mercurial they are tested with, or
   150 # be specifying the version(s) of Mercurial they are tested with, or
   151 # leave the attribute unspecified.
   151 # leave the attribute unspecified.
   152 testedwith = 'ships-with-hg-core'
   152 testedwith = b'ships-with-hg-core'
   153 
   153 
   154 
   154 
   155 def _addpullheader(seq, ctx):
   155 def _addpullheader(seq, ctx):
   156     """Add a header pointing to a public URL where the changeset is available
   156     """Add a header pointing to a public URL where the changeset is available
   157     """
   157     """
   158     repo = ctx.repo()
   158     repo = ctx.repo()
   159     # experimental config: patchbomb.publicurl
   159     # experimental config: patchbomb.publicurl
   160     # waiting for some logic that check that the changeset are available on the
   160     # waiting for some logic that check that the changeset are available on the
   161     # destination before patchbombing anything.
   161     # destination before patchbombing anything.
   162     publicurl = repo.ui.config('patchbomb', 'publicurl')
   162     publicurl = repo.ui.config(b'patchbomb', b'publicurl')
   163     if publicurl:
   163     if publicurl:
   164         return 'Available At %s\n' '#              hg pull %s -r %s' % (
   164         return b'Available At %s\n' b'#              hg pull %s -r %s' % (
   165             publicurl,
   165             publicurl,
   166             publicurl,
   166             publicurl,
   167             ctx,
   167             ctx,
   168         )
   168         )
   169     return None
   169     return None
   170 
   170 
   171 
   171 
   172 def uisetup(ui):
   172 def uisetup(ui):
   173     cmdutil.extraexport.append('pullurl')
   173     cmdutil.extraexport.append(b'pullurl')
   174     cmdutil.extraexportmap['pullurl'] = _addpullheader
   174     cmdutil.extraexportmap[b'pullurl'] = _addpullheader
   175 
   175 
   176 
   176 
   177 def reposetup(ui, repo):
   177 def reposetup(ui, repo):
   178     if not repo.local():
   178     if not repo.local():
   179         return
   179         return
   180     repo._wlockfreeprefix.add('last-email.txt')
   180     repo._wlockfreeprefix.add(b'last-email.txt')
   181 
   181 
   182 
   182 
   183 def prompt(ui, prompt, default=None, rest=':'):
   183 def prompt(ui, prompt, default=None, rest=b':'):
   184     if default:
   184     if default:
   185         prompt += ' [%s]' % default
   185         prompt += b' [%s]' % default
   186     return ui.prompt(prompt + rest, default)
   186     return ui.prompt(prompt + rest, default)
   187 
   187 
   188 
   188 
   189 def introwanted(ui, opts, number):
   189 def introwanted(ui, opts, number):
   190     '''is an introductory message apparently wanted?'''
   190     '''is an introductory message apparently wanted?'''
   191     introconfig = ui.config('patchbomb', 'intro')
   191     introconfig = ui.config(b'patchbomb', b'intro')
   192     if opts.get('intro') or opts.get('desc'):
   192     if opts.get(b'intro') or opts.get(b'desc'):
   193         intro = True
   193         intro = True
   194     elif introconfig == 'always':
   194     elif introconfig == b'always':
   195         intro = True
   195         intro = True
   196     elif introconfig == 'never':
   196     elif introconfig == b'never':
   197         intro = False
   197         intro = False
   198     elif introconfig == 'auto':
   198     elif introconfig == b'auto':
   199         intro = number > 1
   199         intro = number > 1
   200     else:
   200     else:
   201         ui.write_err(
   201         ui.write_err(
   202             _('warning: invalid patchbomb.intro value "%s"\n') % introconfig
   202             _(b'warning: invalid patchbomb.intro value "%s"\n') % introconfig
   203         )
   203         )
   204         ui.write_err(_('(should be one of always, never, auto)\n'))
   204         ui.write_err(_(b'(should be one of always, never, auto)\n'))
   205         intro = number > 1
   205         intro = number > 1
   206     return intro
   206     return intro
   207 
   207 
   208 
   208 
   209 def _formatflags(ui, repo, rev, flags):
   209 def _formatflags(ui, repo, rev, flags):
   210     """build flag string optionally by template"""
   210     """build flag string optionally by template"""
   211     tmpl = ui.config('patchbomb', 'flagtemplate')
   211     tmpl = ui.config(b'patchbomb', b'flagtemplate')
   212     if not tmpl:
   212     if not tmpl:
   213         return ' '.join(flags)
   213         return b' '.join(flags)
   214     out = util.stringio()
   214     out = util.stringio()
   215     opts = {'template': templater.unquotestring(tmpl)}
   215     opts = {b'template': templater.unquotestring(tmpl)}
   216     with formatter.templateformatter(ui, out, 'patchbombflag', opts) as fm:
   216     with formatter.templateformatter(ui, out, b'patchbombflag', opts) as fm:
   217         fm.startitem()
   217         fm.startitem()
   218         fm.context(ctx=repo[rev])
   218         fm.context(ctx=repo[rev])
   219         fm.write('flags', '%s', fm.formatlist(flags, name='flag'))
   219         fm.write(b'flags', b'%s', fm.formatlist(flags, name=b'flag'))
   220     return out.getvalue()
   220     return out.getvalue()
   221 
   221 
   222 
   222 
   223 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
   223 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
   224     """build prefix to patch subject"""
   224     """build prefix to patch subject"""
   225     flag = _formatflags(ui, repo, rev, flags)
   225     flag = _formatflags(ui, repo, rev, flags)
   226     if flag:
   226     if flag:
   227         flag = ' ' + flag
   227         flag = b' ' + flag
   228 
   228 
   229     if not numbered:
   229     if not numbered:
   230         return '[PATCH%s]' % flag
   230         return b'[PATCH%s]' % flag
   231     else:
   231     else:
   232         tlen = len("%d" % total)
   232         tlen = len(b"%d" % total)
   233         return '[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
   233         return b'[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
   234 
   234 
   235 
   235 
   236 def makepatch(
   236 def makepatch(
   237     ui,
   237     ui,
   238     repo,
   238     repo,
   246     patchname=None,
   246     patchname=None,
   247 ):
   247 ):
   248 
   248 
   249     desc = []
   249     desc = []
   250     node = None
   250     node = None
   251     body = ''
   251     body = b''
   252 
   252 
   253     for line in patchlines:
   253     for line in patchlines:
   254         if line.startswith('#'):
   254         if line.startswith(b'#'):
   255             if line.startswith('# Node ID'):
   255             if line.startswith(b'# Node ID'):
   256                 node = line.split()[-1]
   256                 node = line.split()[-1]
   257             continue
   257             continue
   258         if line.startswith('diff -r') or line.startswith('diff --git'):
   258         if line.startswith(b'diff -r') or line.startswith(b'diff --git'):
   259             break
   259             break
   260         desc.append(line)
   260         desc.append(line)
   261 
   261 
   262     if not patchname and not node:
   262     if not patchname and not node:
   263         raise ValueError
   263         raise ValueError
   264 
   264 
   265     if opts.get('attach') and not opts.get('body'):
   265     if opts.get(b'attach') and not opts.get(b'body'):
   266         body = (
   266         body = (
   267             '\n'.join(desc[1:]).strip() or 'Patch subject is complete summary.'
   267             b'\n'.join(desc[1:]).strip()
   268         )
   268             or b'Patch subject is complete summary.'
   269         body += '\n\n\n'
   269         )
   270 
   270         body += b'\n\n\n'
   271     if opts.get('plain'):
   271 
   272         while patchlines and patchlines[0].startswith('# '):
   272     if opts.get(b'plain'):
       
   273         while patchlines and patchlines[0].startswith(b'# '):
   273             patchlines.pop(0)
   274             patchlines.pop(0)
   274         if patchlines:
   275         if patchlines:
   275             patchlines.pop(0)
   276             patchlines.pop(0)
   276         while patchlines and not patchlines[0].strip():
   277         while patchlines and not patchlines[0].strip():
   277             patchlines.pop(0)
   278             patchlines.pop(0)
   278 
   279 
   279     ds = patch.diffstat(patchlines)
   280     ds = patch.diffstat(patchlines)
   280     if opts.get('diffstat'):
   281     if opts.get(b'diffstat'):
   281         body += ds + '\n\n'
   282         body += ds + b'\n\n'
   282 
   283 
   283     addattachment = opts.get('attach') or opts.get('inline')
   284     addattachment = opts.get(b'attach') or opts.get(b'inline')
   284     if not addattachment or opts.get('body'):
   285     if not addattachment or opts.get(b'body'):
   285         body += '\n'.join(patchlines)
   286         body += b'\n'.join(patchlines)
   286 
   287 
   287     if addattachment:
   288     if addattachment:
   288         msg = emimemultipart.MIMEMultipart()
   289         msg = emimemultipart.MIMEMultipart()
   289         if body:
   290         if body:
   290             msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
   291             msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(b'test')))
   291         p = mail.mimetextpatch(
   292         p = mail.mimetextpatch(
   292             '\n'.join(patchlines), 'x-patch', opts.get('test')
   293             b'\n'.join(patchlines), b'x-patch', opts.get(b'test')
   293         )
   294         )
   294         binnode = nodemod.bin(node)
   295         binnode = nodemod.bin(node)
   295         # if node is mq patch, it will have the patch file's name as a tag
   296         # if node is mq patch, it will have the patch file's name as a tag
   296         if not patchname:
   297         if not patchname:
   297             patchtags = [
   298             patchtags = [
   298                 t
   299                 t
   299                 for t in repo.nodetags(binnode)
   300                 for t in repo.nodetags(binnode)
   300                 if t.endswith('.patch') or t.endswith('.diff')
   301                 if t.endswith(b'.patch') or t.endswith(b'.diff')
   301             ]
   302             ]
   302             if patchtags:
   303             if patchtags:
   303                 patchname = patchtags[0]
   304                 patchname = patchtags[0]
   304             elif total > 1:
   305             elif total > 1:
   305                 patchname = cmdutil.makefilename(
   306                 patchname = cmdutil.makefilename(
   306                     repo[node], '%b-%n.patch', seqno=idx, total=total
   307                     repo[node], b'%b-%n.patch', seqno=idx, total=total
   307                 )
   308                 )
   308             else:
   309             else:
   309                 patchname = cmdutil.makefilename(repo[node], '%b.patch')
   310                 patchname = cmdutil.makefilename(repo[node], b'%b.patch')
   310         disposition = r'inline'
   311         disposition = r'inline'
   311         if opts.get('attach'):
   312         if opts.get(b'attach'):
   312             disposition = r'attachment'
   313             disposition = r'attachment'
   313         p[r'Content-Disposition'] = (
   314         p[r'Content-Disposition'] = (
   314             disposition + r'; filename=' + encoding.strfromlocal(patchname)
   315             disposition + r'; filename=' + encoding.strfromlocal(patchname)
   315         )
   316         )
   316         msg.attach(p)
   317         msg.attach(p)
   317     else:
   318     else:
   318         msg = mail.mimetextpatch(body, display=opts.get('test'))
   319         msg = mail.mimetextpatch(body, display=opts.get(b'test'))
   319 
   320 
   320     prefix = _formatprefix(
   321     prefix = _formatprefix(
   321         ui, repo, rev, opts.get('flag'), idx, total, numbered
   322         ui, repo, rev, opts.get(b'flag'), idx, total, numbered
   322     )
   323     )
   323     subj = desc[0].strip().rstrip('. ')
   324     subj = desc[0].strip().rstrip(b'. ')
   324     if not numbered:
   325     if not numbered:
   325         subj = ' '.join([prefix, opts.get('subject') or subj])
   326         subj = b' '.join([prefix, opts.get(b'subject') or subj])
   326     else:
   327     else:
   327         subj = ' '.join([prefix, subj])
   328         subj = b' '.join([prefix, subj])
   328     msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
   329     msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(b'test'))
   329     msg['X-Mercurial-Node'] = node
   330     msg[b'X-Mercurial-Node'] = node
   330     msg['X-Mercurial-Series-Index'] = '%i' % idx
   331     msg[b'X-Mercurial-Series-Index'] = b'%i' % idx
   331     msg['X-Mercurial-Series-Total'] = '%i' % total
   332     msg[b'X-Mercurial-Series-Total'] = b'%i' % total
   332     return msg, subj, ds
   333     return msg, subj, ds
   333 
   334 
   334 
   335 
   335 def _getpatches(repo, revs, **opts):
   336 def _getpatches(repo, revs, **opts):
   336     """return a list of patches for a list of revisions
   337     """return a list of patches for a list of revisions
   337 
   338 
   338     Each patch in the list is itself a list of lines.
   339     Each patch in the list is itself a list of lines.
   339     """
   340     """
   340     ui = repo.ui
   341     ui = repo.ui
   341     prev = repo['.'].rev()
   342     prev = repo[b'.'].rev()
   342     for r in revs:
   343     for r in revs:
   343         if r == prev and (repo[None].files() or repo[None].deleted()):
   344         if r == prev and (repo[None].files() or repo[None].deleted()):
   344             ui.warn(
   345             ui.warn(
   345                 _('warning: working directory has ' 'uncommitted changes\n')
   346                 _(b'warning: working directory has ' b'uncommitted changes\n')
   346             )
   347             )
   347         output = stringio()
   348         output = stringio()
   348         cmdutil.exportfile(
   349         cmdutil.exportfile(
   349             repo, [r], output, opts=patch.difffeatureopts(ui, opts, git=True)
   350             repo, [r], output, opts=patch.difffeatureopts(ui, opts, git=True)
   350         )
   351         )
   351         yield output.getvalue().split('\n')
   352         yield output.getvalue().split(b'\n')
   352 
   353 
   353 
   354 
   354 def _getbundle(repo, dest, **opts):
   355 def _getbundle(repo, dest, **opts):
   355     """return a bundle containing changesets missing in "dest"
   356     """return a bundle containing changesets missing in "dest"
   356 
   357 
   358     `bundle` command.
   359     `bundle` command.
   359 
   360 
   360     The bundle is a returned as a single in-memory binary blob.
   361     The bundle is a returned as a single in-memory binary blob.
   361     """
   362     """
   362     ui = repo.ui
   363     ui = repo.ui
   363     tmpdir = pycompat.mkdtemp(prefix='hg-email-bundle-')
   364     tmpdir = pycompat.mkdtemp(prefix=b'hg-email-bundle-')
   364     tmpfn = os.path.join(tmpdir, 'bundle')
   365     tmpfn = os.path.join(tmpdir, b'bundle')
   365     btype = ui.config('patchbomb', 'bundletype')
   366     btype = ui.config(b'patchbomb', b'bundletype')
   366     if btype:
   367     if btype:
   367         opts[r'type'] = btype
   368         opts[r'type'] = btype
   368     try:
   369     try:
   369         commands.bundle(ui, repo, tmpfn, dest, **opts)
   370         commands.bundle(ui, repo, tmpfn, dest, **opts)
   370         return util.readfile(tmpfn)
   371         return util.readfile(tmpfn)
   387     ui = repo.ui
   388     ui = repo.ui
   388     if opts.get(r'desc'):
   389     if opts.get(r'desc'):
   389         body = open(opts.get(r'desc')).read()
   390         body = open(opts.get(r'desc')).read()
   390     else:
   391     else:
   391         ui.write(
   392         ui.write(
   392             _('\nWrite the introductory message for the ' 'patch series.\n\n')
   393             _(b'\nWrite the introductory message for the ' b'patch series.\n\n')
   393         )
   394         )
   394         body = ui.edit(
   395         body = ui.edit(
   395             defaultbody, sender, repopath=repo.path, action='patchbombbody'
   396             defaultbody, sender, repopath=repo.path, action=b'patchbombbody'
   396         )
   397         )
   397         # Save series description in case sendmail fails
   398         # Save series description in case sendmail fails
   398         msgfile = repo.vfs('last-email.txt', 'wb')
   399         msgfile = repo.vfs(b'last-email.txt', b'wb')
   399         msgfile.write(body)
   400         msgfile.write(body)
   400         msgfile.close()
   401         msgfile.close()
   401     return body
   402     return body
   402 
   403 
   403 
   404 
   408     The list is always one message long in that case.
   409     The list is always one message long in that case.
   409     """
   410     """
   410     ui = repo.ui
   411     ui = repo.ui
   411     _charsets = mail._charsets(ui)
   412     _charsets = mail._charsets(ui)
   412     subj = opts.get(r'subject') or prompt(
   413     subj = opts.get(r'subject') or prompt(
   413         ui, 'Subject:', 'A bundle for your repository'
   414         ui, b'Subject:', b'A bundle for your repository'
   414     )
   415     )
   415 
   416 
   416     body = _getdescription(repo, '', sender, **opts)
   417     body = _getdescription(repo, b'', sender, **opts)
   417     msg = emimemultipart.MIMEMultipart()
   418     msg = emimemultipart.MIMEMultipart()
   418     if body:
   419     if body:
   419         msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
   420         msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
   420     datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
   421     datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
   421     datapart.set_payload(bundle)
   422     datapart.set_payload(bundle)
   422     bundlename = '%s.hg' % opts.get(r'bundlename', 'bundle')
   423     bundlename = b'%s.hg' % opts.get(r'bundlename', b'bundle')
   423     datapart.add_header(
   424     datapart.add_header(
   424         r'Content-Disposition',
   425         r'Content-Disposition',
   425         r'attachment',
   426         r'attachment',
   426         filename=encoding.strfromlocal(bundlename),
   427         filename=encoding.strfromlocal(bundlename),
   427     )
   428     )
   428     emailencoders.encode_base64(datapart)
   429     emailencoders.encode_base64(datapart)
   429     msg.attach(datapart)
   430     msg.attach(datapart)
   430     msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
   431     msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
   431     return [(msg, subj, None)]
   432     return [(msg, subj, None)]
   432 
   433 
   433 
   434 
   434 def _makeintro(repo, sender, revs, patches, **opts):
   435 def _makeintro(repo, sender, revs, patches, **opts):
   435     """make an introduction email, asking the user for content if needed
   436     """make an introduction email, asking the user for content if needed
   441     # use the last revision which is likely to be a bookmarked head
   442     # use the last revision which is likely to be a bookmarked head
   442     prefix = _formatprefix(
   443     prefix = _formatprefix(
   443         ui, repo, revs.last(), opts.get(r'flag'), 0, len(patches), numbered=True
   444         ui, repo, revs.last(), opts.get(r'flag'), 0, len(patches), numbered=True
   444     )
   445     )
   445     subj = opts.get(r'subject') or prompt(
   446     subj = opts.get(r'subject') or prompt(
   446         ui, '(optional) Subject: ', rest=prefix, default=''
   447         ui, b'(optional) Subject: ', rest=prefix, default=b''
   447     )
   448     )
   448     if not subj:
   449     if not subj:
   449         return None  # skip intro if the user doesn't bother
   450         return None  # skip intro if the user doesn't bother
   450 
   451 
   451     subj = prefix + ' ' + subj
   452     subj = prefix + b' ' + subj
   452 
   453 
   453     body = ''
   454     body = b''
   454     if opts.get(r'diffstat'):
   455     if opts.get(r'diffstat'):
   455         # generate a cumulative diffstat of the whole patch series
   456         # generate a cumulative diffstat of the whole patch series
   456         diffstat = patch.diffstat(sum(patches, []))
   457         diffstat = patch.diffstat(sum(patches, []))
   457         body = '\n' + diffstat
   458         body = b'\n' + diffstat
   458     else:
   459     else:
   459         diffstat = None
   460         diffstat = None
   460 
   461 
   461     body = _getdescription(repo, body, sender, **opts)
   462     body = _getdescription(repo, body, sender, **opts)
   462     msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
   463     msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
   463     msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
   464     msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
   464     return (msg, subj, diffstat)
   465     return (msg, subj, diffstat)
   465 
   466 
   466 
   467 
   467 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
   468 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
   468     """return a list of emails from a list of patches
   469     """return a list of emails from a list of patches
   475     ui = repo.ui
   476     ui = repo.ui
   476     _charsets = mail._charsets(ui)
   477     _charsets = mail._charsets(ui)
   477     patches = list(_getpatches(repo, revs, **opts))
   478     patches = list(_getpatches(repo, revs, **opts))
   478     msgs = []
   479     msgs = []
   479 
   480 
   480     ui.write(_('this patch series consists of %d patches.\n\n') % len(patches))
   481     ui.write(_(b'this patch series consists of %d patches.\n\n') % len(patches))
   481 
   482 
   482     # build the intro message, or skip it if the user declines
   483     # build the intro message, or skip it if the user declines
   483     if introwanted(ui, bytesopts, len(patches)):
   484     if introwanted(ui, bytesopts, len(patches)):
   484         msg = _makeintro(repo, sender, revs, patches, **opts)
   485         msg = _makeintro(repo, sender, revs, patches, **opts)
   485         if msg:
   486         if msg:
   512 
   513 
   513 
   514 
   514 def _getoutgoing(repo, dest, revs):
   515 def _getoutgoing(repo, dest, revs):
   515     '''Return the revisions present locally but not in dest'''
   516     '''Return the revisions present locally but not in dest'''
   516     ui = repo.ui
   517     ui = repo.ui
   517     url = ui.expandpath(dest or 'default-push', dest or 'default')
   518     url = ui.expandpath(dest or b'default-push', dest or b'default')
   518     url = hg.parseurl(url)[0]
   519     url = hg.parseurl(url)[0]
   519     ui.status(_('comparing with %s\n') % util.hidepassword(url))
   520     ui.status(_(b'comparing with %s\n') % util.hidepassword(url))
   520 
   521 
   521     revs = [r for r in revs if r >= 0]
   522     revs = [r for r in revs if r >= 0]
   522     if not revs:
   523     if not revs:
   523         revs = [repo.changelog.tiprev()]
   524         revs = [repo.changelog.tiprev()]
   524     revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
   525     revs = repo.revs(b'outgoing(%s) and ::%ld', dest or b'', revs)
   525     if not revs:
   526     if not revs:
   526         ui.status(_("no changes found\n"))
   527         ui.status(_(b"no changes found\n"))
   527     return revs
   528     return revs
   528 
   529 
   529 
   530 
   530 def _msgid(node, timestamp):
   531 def _msgid(node, timestamp):
   531     hostname = encoding.strtolocal(socket.getfqdn())
   532     hostname = encoding.strtolocal(socket.getfqdn())
   532     hostname = encoding.environ.get('HGHOSTNAME', hostname)
   533     hostname = encoding.environ.get(b'HGHOSTNAME', hostname)
   533     return '<%s.%d@%s>' % (node, timestamp, hostname)
   534     return b'<%s.%d@%s>' % (node, timestamp, hostname)
   534 
   535 
   535 
   536 
   536 emailopts = [
   537 emailopts = [
   537     ('', 'body', None, _('send patches as inline message text (default)')),
   538     (b'', b'body', None, _(b'send patches as inline message text (default)')),
   538     ('a', 'attach', None, _('send patches as attachments')),
   539     (b'a', b'attach', None, _(b'send patches as attachments')),
   539     ('i', 'inline', None, _('send patches as inline attachments')),
   540     (b'i', b'inline', None, _(b'send patches as inline attachments')),
   540     (
   541     (
   541         '',
   542         b'',
   542         'bcc',
   543         b'bcc',
   543         [],
   544         [],
   544         _('email addresses of blind carbon copy recipients'),
   545         _(b'email addresses of blind carbon copy recipients'),
   545         _('EMAIL'),
   546         _(b'EMAIL'),
   546     ),
   547     ),
   547     ('c', 'cc', [], _('email addresses of copy recipients'), _('EMAIL')),
   548     (b'c', b'cc', [], _(b'email addresses of copy recipients'), _(b'EMAIL')),
   548     ('', 'confirm', None, _('ask for confirmation before sending')),
   549     (b'', b'confirm', None, _(b'ask for confirmation before sending')),
   549     ('d', 'diffstat', None, _('add diffstat output to messages')),
   550     (b'd', b'diffstat', None, _(b'add diffstat output to messages')),
   550     ('', 'date', '', _('use the given date as the sending date'), _('DATE')),
       
   551     (
   551     (
   552         '',
   552         b'',
   553         'desc',
   553         b'date',
   554         '',
   554         b'',
   555         _('use the given file as the series description'),
   555         _(b'use the given date as the sending date'),
   556         _('FILE'),
   556         _(b'DATE'),
   557     ),
       
   558     ('f', 'from', '', _('email address of sender'), _('EMAIL')),
       
   559     ('n', 'test', None, _('print messages that would be sent')),
       
   560     (
       
   561         'm',
       
   562         'mbox',
       
   563         '',
       
   564         _('write messages to mbox file instead of sending them'),
       
   565         _('FILE'),
       
   566     ),
   557     ),
   567     (
   558     (
   568         '',
   559         b'',
   569         'reply-to',
   560         b'desc',
   570         [],
   561         b'',
   571         _('email addresses replies should be sent to'),
   562         _(b'use the given file as the series description'),
   572         _('EMAIL'),
   563         _(b'FILE'),
       
   564     ),
       
   565     (b'f', b'from', b'', _(b'email address of sender'), _(b'EMAIL')),
       
   566     (b'n', b'test', None, _(b'print messages that would be sent')),
       
   567     (
       
   568         b'm',
       
   569         b'mbox',
       
   570         b'',
       
   571         _(b'write messages to mbox file instead of sending them'),
       
   572         _(b'FILE'),
   573     ),
   573     ),
   574     (
   574     (
   575         's',
   575         b'',
   576         'subject',
   576         b'reply-to',
   577         '',
   577         [],
   578         _('subject of first message (intro or single patch)'),
   578         _(b'email addresses replies should be sent to'),
   579         _('TEXT'),
   579         _(b'EMAIL'),
   580     ),
   580     ),
   581     ('', 'in-reply-to', '', _('message identifier to reply to'), _('MSGID')),
   581     (
   582     ('', 'flag', [], _('flags to add in subject prefixes'), _('FLAG')),
   582         b's',
   583     ('t', 'to', [], _('email addresses of recipients'), _('EMAIL')),
   583         b'subject',
       
   584         b'',
       
   585         _(b'subject of first message (intro or single patch)'),
       
   586         _(b'TEXT'),
       
   587     ),
       
   588     (
       
   589         b'',
       
   590         b'in-reply-to',
       
   591         b'',
       
   592         _(b'message identifier to reply to'),
       
   593         _(b'MSGID'),
       
   594     ),
       
   595     (b'', b'flag', [], _(b'flags to add in subject prefixes'), _(b'FLAG')),
       
   596     (b't', b'to', [], _(b'email addresses of recipients'), _(b'EMAIL')),
   584 ]
   597 ]
   585 
   598 
   586 
   599 
   587 @command(
   600 @command(
   588     'email',
   601     b'email',
   589     [
   602     [
   590         ('g', 'git', None, _('use git extended diff format')),
   603         (b'g', b'git', None, _(b'use git extended diff format')),
   591         ('', 'plain', None, _('omit hg patch header')),
   604         (b'', b'plain', None, _(b'omit hg patch header')),
   592         (
   605         (
   593             'o',
   606             b'o',
   594             'outgoing',
   607             b'outgoing',
   595             None,
   608             None,
   596             _('send changes not found in the target repository'),
   609             _(b'send changes not found in the target repository'),
   597         ),
   610         ),
   598         (
   611         (
   599             'b',
   612             b'b',
   600             'bundle',
   613             b'bundle',
   601             None,
   614             None,
   602             _('send changes not in target as a binary bundle'),
   615             _(b'send changes not in target as a binary bundle'),
   603         ),
   616         ),
   604         (
   617         (
   605             'B',
   618             b'B',
   606             'bookmark',
   619             b'bookmark',
   607             '',
   620             b'',
   608             _('send changes only reachable by given bookmark'),
   621             _(b'send changes only reachable by given bookmark'),
   609             _('BOOKMARK'),
   622             _(b'BOOKMARK'),
   610         ),
   623         ),
   611         (
   624         (
   612             '',
   625             b'',
   613             'bundlename',
   626             b'bundlename',
   614             'bundle',
   627             b'bundle',
   615             _('name of the bundle attachment file'),
   628             _(b'name of the bundle attachment file'),
   616             _('NAME'),
   629             _(b'NAME'),
   617         ),
   630         ),
   618         ('r', 'rev', [], _('a revision to send'), _('REV')),
   631         (b'r', b'rev', [], _(b'a revision to send'), _(b'REV')),
   619         (
   632         (
   620             '',
   633             b'',
   621             'force',
   634             b'force',
   622             None,
   635             None,
   623             _(
   636             _(
   624                 'run even when remote repository is unrelated '
   637                 b'run even when remote repository is unrelated '
   625                 '(with -b/--bundle)'
   638                 b'(with -b/--bundle)'
   626             ),
   639             ),
   627         ),
   640         ),
   628         (
   641         (
   629             '',
   642             b'',
   630             'base',
   643             b'base',
   631             [],
   644             [],
   632             _(
   645             _(
   633                 'a base changeset to specify instead of a destination '
   646                 b'a base changeset to specify instead of a destination '
   634                 '(with -b/--bundle)'
   647                 b'(with -b/--bundle)'
   635             ),
   648             ),
   636             _('REV'),
   649             _(b'REV'),
   637         ),
   650         ),
   638         ('', 'intro', None, _('send an introduction email for a single patch')),
   651         (
       
   652             b'',
       
   653             b'intro',
       
   654             None,
       
   655             _(b'send an introduction email for a single patch'),
       
   656         ),
   639     ]
   657     ]
   640     + emailopts
   658     + emailopts
   641     + cmdutil.remoteopts,
   659     + cmdutil.remoteopts,
   642     _('hg email [OPTION]... [DEST]...'),
   660     _(b'hg email [OPTION]... [DEST]...'),
   643     helpcategory=command.CATEGORY_IMPORT_EXPORT,
   661     helpcategory=command.CATEGORY_IMPORT_EXPORT,
   644 )
   662 )
   645 def email(ui, repo, *revs, **opts):
   663 def email(ui, repo, *revs, **opts):
   646     '''send changesets by email
   664     '''send changesets by email
   647 
   665 
   729     '''
   747     '''
   730     opts = pycompat.byteskwargs(opts)
   748     opts = pycompat.byteskwargs(opts)
   731 
   749 
   732     _charsets = mail._charsets(ui)
   750     _charsets = mail._charsets(ui)
   733 
   751 
   734     bundle = opts.get('bundle')
   752     bundle = opts.get(b'bundle')
   735     date = opts.get('date')
   753     date = opts.get(b'date')
   736     mbox = opts.get('mbox')
   754     mbox = opts.get(b'mbox')
   737     outgoing = opts.get('outgoing')
   755     outgoing = opts.get(b'outgoing')
   738     rev = opts.get('rev')
   756     rev = opts.get(b'rev')
   739     bookmark = opts.get('bookmark')
   757     bookmark = opts.get(b'bookmark')
   740 
   758 
   741     if not (opts.get('test') or mbox):
   759     if not (opts.get(b'test') or mbox):
   742         # really sending
   760         # really sending
   743         mail.validateconfig(ui)
   761         mail.validateconfig(ui)
   744 
   762 
   745     if not (revs or rev or outgoing or bundle or bookmark):
   763     if not (revs or rev or outgoing or bundle or bookmark):
   746         raise error.Abort(_('specify at least one changeset with -B, -r or -o'))
   764         raise error.Abort(
       
   765             _(b'specify at least one changeset with -B, -r or -o')
       
   766         )
   747 
   767 
   748     if outgoing and bundle:
   768     if outgoing and bundle:
   749         raise error.Abort(
   769         raise error.Abort(
   750             _(
   770             _(
   751                 "--outgoing mode always on with --bundle;"
   771                 b"--outgoing mode always on with --bundle;"
   752                 " do not re-specify --outgoing"
   772                 b" do not re-specify --outgoing"
   753             )
   773             )
   754         )
   774         )
   755     if rev and bookmark:
   775     if rev and bookmark:
   756         raise error.Abort(_("-r and -B are mutually exclusive"))
   776         raise error.Abort(_(b"-r and -B are mutually exclusive"))
   757 
   777 
   758     if outgoing or bundle:
   778     if outgoing or bundle:
   759         if len(revs) > 1:
   779         if len(revs) > 1:
   760             raise error.Abort(_("too many destinations"))
   780             raise error.Abort(_(b"too many destinations"))
   761         if revs:
   781         if revs:
   762             dest = revs[0]
   782             dest = revs[0]
   763         else:
   783         else:
   764             dest = None
   784             dest = None
   765         revs = []
   785         revs = []
   766 
   786 
   767     if rev:
   787     if rev:
   768         if revs:
   788         if revs:
   769             raise error.Abort(_('use only one form to specify the revision'))
   789             raise error.Abort(_(b'use only one form to specify the revision'))
   770         revs = rev
   790         revs = rev
   771     elif bookmark:
   791     elif bookmark:
   772         if bookmark not in repo._bookmarks:
   792         if bookmark not in repo._bookmarks:
   773             raise error.Abort(_("bookmark '%s' not found") % bookmark)
   793             raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
   774         revs = scmutil.bookmarkrevs(repo, bookmark)
   794         revs = scmutil.bookmarkrevs(repo, bookmark)
   775 
   795 
   776     revs = scmutil.revrange(repo, revs)
   796     revs = scmutil.revrange(repo, revs)
   777     if outgoing:
   797     if outgoing:
   778         revs = _getoutgoing(repo, dest, revs)
   798         revs = _getoutgoing(repo, dest, revs)
   779     if bundle:
   799     if bundle:
   780         opts['revs'] = ["%d" % r for r in revs]
   800         opts[b'revs'] = [b"%d" % r for r in revs]
   781 
   801 
   782     # check if revision exist on the public destination
   802     # check if revision exist on the public destination
   783     publicurl = repo.ui.config('patchbomb', 'publicurl')
   803     publicurl = repo.ui.config(b'patchbomb', b'publicurl')
   784     if publicurl:
   804     if publicurl:
   785         repo.ui.debug('checking that revision exist in the public repo\n')
   805         repo.ui.debug(b'checking that revision exist in the public repo\n')
   786         try:
   806         try:
   787             publicpeer = hg.peer(repo, {}, publicurl)
   807             publicpeer = hg.peer(repo, {}, publicurl)
   788         except error.RepoError:
   808         except error.RepoError:
   789             repo.ui.write_err(
   809             repo.ui.write_err(
   790                 _('unable to access public repo: %s\n') % publicurl
   810                 _(b'unable to access public repo: %s\n') % publicurl
   791             )
   811             )
   792             raise
   812             raise
   793         if not publicpeer.capable('known'):
   813         if not publicpeer.capable(b'known'):
   794             repo.ui.debug('skipping existence checks: public repo too old\n')
   814             repo.ui.debug(b'skipping existence checks: public repo too old\n')
   795         else:
   815         else:
   796             out = [repo[r] for r in revs]
   816             out = [repo[r] for r in revs]
   797             known = publicpeer.known(h.node() for h in out)
   817             known = publicpeer.known(h.node() for h in out)
   798             missing = []
   818             missing = []
   799             for idx, h in enumerate(out):
   819             for idx, h in enumerate(out):
   800                 if not known[idx]:
   820                 if not known[idx]:
   801                     missing.append(h)
   821                     missing.append(h)
   802             if missing:
   822             if missing:
   803                 if len(missing) > 1:
   823                 if len(missing) > 1:
   804                     msg = _('public "%s" is missing %s and %i others')
   824                     msg = _(b'public "%s" is missing %s and %i others')
   805                     msg %= (publicurl, missing[0], len(missing) - 1)
   825                     msg %= (publicurl, missing[0], len(missing) - 1)
   806                 else:
   826                 else:
   807                     msg = _('public url %s is missing %s')
   827                     msg = _(b'public url %s is missing %s')
   808                     msg %= (publicurl, missing[0])
   828                     msg %= (publicurl, missing[0])
   809                 missingrevs = [ctx.rev() for ctx in missing]
   829                 missingrevs = [ctx.rev() for ctx in missing]
   810                 revhint = ' '.join(
   830                 revhint = b' '.join(
   811                     '-r %s' % h for h in repo.set('heads(%ld)', missingrevs)
   831                     b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs)
   812                 )
   832                 )
   813                 hint = _("use 'hg push %s %s'") % (publicurl, revhint)
   833                 hint = _(b"use 'hg push %s %s'") % (publicurl, revhint)
   814                 raise error.Abort(msg, hint=hint)
   834                 raise error.Abort(msg, hint=hint)
   815 
   835 
   816     # start
   836     # start
   817     if date:
   837     if date:
   818         start_time = dateutil.parsedate(date)
   838         start_time = dateutil.parsedate(date)
   822     def genmsgid(id):
   842     def genmsgid(id):
   823         return _msgid(id[:20], int(start_time[0]))
   843         return _msgid(id[:20], int(start_time[0]))
   824 
   844 
   825     # deprecated config: patchbomb.from
   845     # deprecated config: patchbomb.from
   826     sender = (
   846     sender = (
   827         opts.get('from')
   847         opts.get(b'from')
   828         or ui.config('email', 'from')
   848         or ui.config(b'email', b'from')
   829         or ui.config('patchbomb', 'from')
   849         or ui.config(b'patchbomb', b'from')
   830         or prompt(ui, 'From', ui.username())
   850         or prompt(ui, b'From', ui.username())
   831     )
   851     )
   832 
   852 
   833     if bundle:
   853     if bundle:
   834         stropts = pycompat.strkwargs(opts)
   854         stropts = pycompat.strkwargs(opts)
   835         bundledata = _getbundle(repo, dest, **stropts)
   855         bundledata = _getbundle(repo, dest, **stropts)
   841 
   861 
   842     showaddrs = []
   862     showaddrs = []
   843 
   863 
   844     def getaddrs(header, ask=False, default=None):
   864     def getaddrs(header, ask=False, default=None):
   845         configkey = header.lower()
   865         configkey = header.lower()
   846         opt = header.replace('-', '_').lower()
   866         opt = header.replace(b'-', b'_').lower()
   847         addrs = opts.get(opt)
   867         addrs = opts.get(opt)
   848         if addrs:
   868         if addrs:
   849             showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
   869             showaddrs.append(b'%s: %s' % (header, b', '.join(addrs)))
   850             return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
   870             return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test'))
   851 
   871 
   852         # not on the command line: fallback to config and then maybe ask
   872         # not on the command line: fallback to config and then maybe ask
   853         addr = ui.config('email', configkey) or ui.config(
   873         addr = ui.config(b'email', configkey) or ui.config(
   854             'patchbomb', configkey
   874             b'patchbomb', configkey
   855         )
   875         )
   856         if not addr:
   876         if not addr:
   857             specified = ui.hasconfig('email', configkey) or ui.hasconfig(
   877             specified = ui.hasconfig(b'email', configkey) or ui.hasconfig(
   858                 'patchbomb', configkey
   878                 b'patchbomb', configkey
   859             )
   879             )
   860             if not specified and ask:
   880             if not specified and ask:
   861                 addr = prompt(ui, header, default=default)
   881                 addr = prompt(ui, header, default=default)
   862         if addr:
   882         if addr:
   863             showaddrs.append('%s: %s' % (header, addr))
   883             showaddrs.append(b'%s: %s' % (header, addr))
   864             return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
   884             return mail.addrlistencode(ui, [addr], _charsets, opts.get(b'test'))
   865         elif default:
   885         elif default:
   866             return mail.addrlistencode(
   886             return mail.addrlistencode(
   867                 ui, [default], _charsets, opts.get('test')
   887                 ui, [default], _charsets, opts.get(b'test')
   868             )
   888             )
   869         return []
   889         return []
   870 
   890 
   871     to = getaddrs('To', ask=True)
   891     to = getaddrs(b'To', ask=True)
   872     if not to:
   892     if not to:
   873         # we can get here in non-interactive mode
   893         # we can get here in non-interactive mode
   874         raise error.Abort(_('no recipient addresses provided'))
   894         raise error.Abort(_(b'no recipient addresses provided'))
   875     cc = getaddrs('Cc', ask=True, default='')
   895     cc = getaddrs(b'Cc', ask=True, default=b'')
   876     bcc = getaddrs('Bcc')
   896     bcc = getaddrs(b'Bcc')
   877     replyto = getaddrs('Reply-To')
   897     replyto = getaddrs(b'Reply-To')
   878 
   898 
   879     confirm = ui.configbool('patchbomb', 'confirm')
   899     confirm = ui.configbool(b'patchbomb', b'confirm')
   880     confirm |= bool(opts.get('diffstat') or opts.get('confirm'))
   900     confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm'))
   881 
   901 
   882     if confirm:
   902     if confirm:
   883         ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
   903         ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary')
   884         ui.write(('From: %s\n' % sender), label='patchbomb.from')
   904         ui.write((b'From: %s\n' % sender), label=b'patchbomb.from')
   885         for addr in showaddrs:
   905         for addr in showaddrs:
   886             ui.write('%s\n' % addr, label='patchbomb.to')
   906             ui.write(b'%s\n' % addr, label=b'patchbomb.to')
   887         for m, subj, ds in msgs:
   907         for m, subj, ds in msgs:
   888             ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
   908             ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject')
   889             if ds:
   909             if ds:
   890                 ui.write(ds, label='patchbomb.diffstats')
   910                 ui.write(ds, label=b'patchbomb.diffstats')
   891         ui.write('\n')
   911         ui.write(b'\n')
   892         if ui.promptchoice(
   912         if ui.promptchoice(
   893             _('are you sure you want to send (yn)?' '$$ &Yes $$ &No')
   913             _(b'are you sure you want to send (yn)?' b'$$ &Yes $$ &No')
   894         ):
   914         ):
   895             raise error.Abort(_('patchbomb canceled'))
   915             raise error.Abort(_(b'patchbomb canceled'))
   896 
   916 
   897     ui.write('\n')
   917     ui.write(b'\n')
   898 
   918 
   899     parent = opts.get('in_reply_to') or None
   919     parent = opts.get(b'in_reply_to') or None
   900     # angle brackets may be omitted, they're not semantically part of the msg-id
   920     # angle brackets may be omitted, they're not semantically part of the msg-id
   901     if parent is not None:
   921     if parent is not None:
   902         if not parent.startswith('<'):
   922         if not parent.startswith(b'<'):
   903             parent = '<' + parent
   923             parent = b'<' + parent
   904         if not parent.endswith('>'):
   924         if not parent.endswith(b'>'):
   905             parent += '>'
   925             parent += b'>'
   906 
   926 
   907     sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
   927     sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
   908     sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
   928     sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test'))
   909     sendmail = None
   929     sendmail = None
   910     firstpatch = None
   930     firstpatch = None
   911     progress = ui.makeprogress(_('sending'), unit=_('emails'), total=len(msgs))
   931     progress = ui.makeprogress(
       
   932         _(b'sending'), unit=_(b'emails'), total=len(msgs)
       
   933     )
   912     for i, (m, subj, ds) in enumerate(msgs):
   934     for i, (m, subj, ds) in enumerate(msgs):
   913         try:
   935         try:
   914             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
   936             m[b'Message-Id'] = genmsgid(m[b'X-Mercurial-Node'])
   915             if not firstpatch:
   937             if not firstpatch:
   916                 firstpatch = m['Message-Id']
   938                 firstpatch = m[b'Message-Id']
   917             m['X-Mercurial-Series-Id'] = firstpatch
   939             m[b'X-Mercurial-Series-Id'] = firstpatch
   918         except TypeError:
   940         except TypeError:
   919             m['Message-Id'] = genmsgid('patchbomb')
   941             m[b'Message-Id'] = genmsgid(b'patchbomb')
   920         if parent:
   942         if parent:
   921             m['In-Reply-To'] = parent
   943             m[b'In-Reply-To'] = parent
   922             m['References'] = parent
   944             m[b'References'] = parent
   923         if not parent or 'X-Mercurial-Node' not in m:
   945         if not parent or b'X-Mercurial-Node' not in m:
   924             parent = m['Message-Id']
   946             parent = m[b'Message-Id']
   925 
   947 
   926         m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
   948         m[b'User-Agent'] = b'Mercurial-patchbomb/%s' % util.version()
   927         m['Date'] = eutil.formatdate(start_time[0], localtime=True)
   949         m[b'Date'] = eutil.formatdate(start_time[0], localtime=True)
   928 
   950 
   929         start_time = (start_time[0] + 1, start_time[1])
   951         start_time = (start_time[0] + 1, start_time[1])
   930         m['From'] = sender
   952         m[b'From'] = sender
   931         m['To'] = ', '.join(to)
   953         m[b'To'] = b', '.join(to)
   932         if cc:
   954         if cc:
   933             m['Cc'] = ', '.join(cc)
   955             m[b'Cc'] = b', '.join(cc)
   934         if bcc:
   956         if bcc:
   935             m['Bcc'] = ', '.join(bcc)
   957             m[b'Bcc'] = b', '.join(bcc)
   936         if replyto:
   958         if replyto:
   937             m['Reply-To'] = ', '.join(replyto)
   959             m[b'Reply-To'] = b', '.join(replyto)
   938         # Fix up all headers to be native strings.
   960         # Fix up all headers to be native strings.
   939         # TODO(durin42): this should probably be cleaned up above in the future.
   961         # TODO(durin42): this should probably be cleaned up above in the future.
   940         if pycompat.ispy3:
   962         if pycompat.ispy3:
   941             for hdr, val in list(m.items()):
   963             for hdr, val in list(m.items()):
   942                 change = False
   964                 change = False
   950                         # prevent duplicate headers
   972                         # prevent duplicate headers
   951                         del m[hdr]
   973                         del m[hdr]
   952                     change = True
   974                     change = True
   953                 if change:
   975                 if change:
   954                     m[hdr] = val
   976                     m[hdr] = val
   955         if opts.get('test'):
   977         if opts.get(b'test'):
   956             ui.status(_('displaying '), subj, ' ...\n')
   978             ui.status(_(b'displaying '), subj, b' ...\n')
   957             ui.pager('email')
   979             ui.pager(b'email')
   958             generator = _bytesgenerator(ui, mangle_from_=False)
   980             generator = _bytesgenerator(ui, mangle_from_=False)
   959             try:
   981             try:
   960                 generator.flatten(m, 0)
   982                 generator.flatten(m, 0)
   961                 ui.write('\n')
   983                 ui.write(b'\n')
   962             except IOError as inst:
   984             except IOError as inst:
   963                 if inst.errno != errno.EPIPE:
   985                 if inst.errno != errno.EPIPE:
   964                     raise
   986                     raise
   965         else:
   987         else:
   966             if not sendmail:
   988             if not sendmail:
   967                 sendmail = mail.connect(ui, mbox=mbox)
   989                 sendmail = mail.connect(ui, mbox=mbox)
   968             ui.status(_('sending '), subj, ' ...\n')
   990             ui.status(_(b'sending '), subj, b' ...\n')
   969             progress.update(i, item=subj)
   991             progress.update(i, item=subj)
   970             if not mbox:
   992             if not mbox:
   971                 # Exim does not remove the Bcc field
   993                 # Exim does not remove the Bcc field
   972                 del m['Bcc']
   994                 del m[b'Bcc']
   973             fp = stringio()
   995             fp = stringio()
   974             generator = _bytesgenerator(fp, mangle_from_=False)
   996             generator = _bytesgenerator(fp, mangle_from_=False)
   975             generator.flatten(m, 0)
   997             generator.flatten(m, 0)
   976             alldests = to + bcc + cc
   998             alldests = to + bcc + cc
   977             alldests = [encoding.strfromlocal(d) for d in alldests]
   999             alldests = [encoding.strfromlocal(d) for d in alldests]