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 |
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 |
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) |
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] |