##// END OF EJS Templates
patchbomb: don't unintentionally duplicate headers...
Augie Fackler -
r39070:cecb05c3 default
parent child Browse files
Show More
@@ -1,551 +1,552
1 1 test-abort-checkin.t
2 2 test-absorb-filefixupstate.py
3 3 test-absorb-phase.t
4 4 test-absorb-strip.t
5 5 test-add.t
6 6 test-addremove-similar.t
7 7 test-addremove.t
8 8 test-alias.t
9 9 test-amend-subrepo.t
10 10 test-amend.t
11 11 test-ancestor.py
12 12 test-annotate.py
13 13 test-annotate.t
14 14 test-archive-symlinks.t
15 15 test-atomictempfile.py
16 16 test-audit-path.t
17 17 test-audit-subrepo.t
18 18 test-automv.t
19 19 test-backout.t
20 20 test-backwards-remove.t
21 21 test-bad-pull.t
22 22 test-basic.t
23 23 test-bheads.t
24 24 test-bisect.t
25 25 test-bisect2.t
26 26 test-bisect3.t
27 27 test-blackbox.t
28 28 test-bookmarks-current.t
29 29 test-bookmarks-merge.t
30 30 test-bookmarks-pushpull.t
31 31 test-bookmarks-rebase.t
32 32 test-bookmarks-strip.t
33 33 test-bookmarks.t
34 34 test-branch-change.t
35 35 test-branch-option.t
36 36 test-branch-tag-confict.t
37 37 test-branches.t
38 38 test-bundle-phases.t
39 39 test-bundle-r.t
40 40 test-bundle-type.t
41 41 test-bundle-vs-outgoing.t
42 42 test-bundle.t
43 43 test-bundle2-exchange.t
44 44 test-bundle2-format.t
45 45 test-bundle2-multiple-changegroups.t
46 46 test-bundle2-pushback.t
47 47 test-bundle2-remote-changegroup.t
48 48 test-cappedreader.py
49 49 test-casecollision.t
50 50 test-cat.t
51 51 test-cbor.py
52 52 test-censor.t
53 53 test-changelog-exec.t
54 54 test-check-commit.t
55 55 test-check-execute.t
56 56 test-check-interfaces.py
57 57 test-check-module-imports.t
58 58 test-check-pyflakes.t
59 59 test-check-pylint.t
60 60 test-check-shbang.t
61 61 test-children.t
62 62 test-clone-cgi.t
63 63 test-clone-pull-corruption.t
64 64 test-clone-r.t
65 65 test-clone-update-order.t
66 66 test-clonebundles.t
67 67 test-commit-amend.t
68 68 test-commit-interactive.t
69 69 test-commit-multiple.t
70 70 test-commit-unresolved.t
71 71 test-commit.t
72 72 test-committer.t
73 73 test-completion.t
74 74 test-config-env.py
75 75 test-config.t
76 76 test-conflict.t
77 77 test-confused-revert.t
78 78 test-context.py
79 79 test-contrib-check-code.t
80 80 test-contrib-check-commit.t
81 81 test-convert-authormap.t
82 82 test-convert-clonebranches.t
83 83 test-convert-cvs-branch.t
84 84 test-convert-cvs-detectmerge.t
85 85 test-convert-cvs-synthetic.t
86 86 test-convert-cvs.t
87 87 test-convert-cvsnt-mergepoints.t
88 88 test-convert-datesort.t
89 89 test-convert-filemap.t
90 90 test-convert-hg-sink.t
91 91 test-convert-hg-source.t
92 92 test-convert-hg-startrev.t
93 93 test-convert-splicemap.t
94 94 test-convert-tagsbranch-topology.t
95 95 test-copy-move-merge.t
96 96 test-copy.t
97 97 test-copytrace-heuristics.t
98 98 test-debugbuilddag.t
99 99 test-debugbundle.t
100 100 test-debugextensions.t
101 101 test-debugindexdot.t
102 102 test-debugrename.t
103 103 test-default-push.t
104 104 test-diff-antipatience.t
105 105 test-diff-binary-file.t
106 106 test-diff-change.t
107 107 test-diff-copy-depth.t
108 108 test-diff-hashes.t
109 109 test-diff-ignore-whitespace.t
110 110 test-diff-indent-heuristic.t
111 111 test-diff-issue2761.t
112 112 test-diff-newlines.t
113 113 test-diff-reverse.t
114 114 test-diff-subdir.t
115 115 test-diff-unified.t
116 116 test-diff-upgrade.t
117 117 test-diffdir.t
118 118 test-diffstat.t
119 119 test-directaccess.t
120 120 test-dirstate-backup.t
121 121 test-dirstate-nonnormalset.t
122 122 test-dirstate.t
123 123 test-dispatch.py
124 124 test-doctest.py
125 125 test-double-merge.t
126 126 test-drawdag.t
127 127 test-duplicateoptions.py
128 128 test-editor-filename.t
129 129 test-empty-dir.t
130 130 test-empty-file.t
131 131 test-empty-group.t
132 132 test-empty.t
133 133 test-encode.t
134 134 test-encoding-func.py
135 135 test-encoding.t
136 136 test-eol-add.t
137 137 test-eol-clone.t
138 138 test-eol-hook.t
139 139 test-eol-patch.t
140 140 test-eol-tag.t
141 141 test-eol-update.t
142 142 test-eol.t
143 143 test-eolfilename.t
144 144 test-excessive-merge.t
145 145 test-exchange-obsmarkers-case-A1.t
146 146 test-exchange-obsmarkers-case-A2.t
147 147 test-exchange-obsmarkers-case-A3.t
148 148 test-exchange-obsmarkers-case-A4.t
149 149 test-exchange-obsmarkers-case-A5.t
150 150 test-exchange-obsmarkers-case-A6.t
151 151 test-exchange-obsmarkers-case-A7.t
152 152 test-exchange-obsmarkers-case-B1.t
153 153 test-exchange-obsmarkers-case-B2.t
154 154 test-exchange-obsmarkers-case-B3.t
155 155 test-exchange-obsmarkers-case-B4.t
156 156 test-exchange-obsmarkers-case-B5.t
157 157 test-exchange-obsmarkers-case-B6.t
158 158 test-exchange-obsmarkers-case-B7.t
159 159 test-exchange-obsmarkers-case-C1.t
160 160 test-exchange-obsmarkers-case-C2.t
161 161 test-exchange-obsmarkers-case-C3.t
162 162 test-exchange-obsmarkers-case-C4.t
163 163 test-exchange-obsmarkers-case-D1.t
164 164 test-exchange-obsmarkers-case-D2.t
165 165 test-exchange-obsmarkers-case-D3.t
166 166 test-exchange-obsmarkers-case-D4.t
167 167 test-execute-bit.t
168 168 test-export.t
169 169 test-extdata.t
170 170 test-extdiff.t
171 171 test-extensions-afterloaded.t
172 172 test-extensions-wrapfunction.py
173 173 test-extra-filelog-entry.t
174 174 test-fetch.t
175 175 test-filebranch.t
176 176 test-filecache.py
177 177 test-filelog.py
178 178 test-fileset-generated.t
179 179 test-fileset.t
180 180 test-fix-topology.t
181 181 test-flags.t
182 182 test-generaldelta.t
183 183 test-getbundle.t
184 184 test-git-export.t
185 185 test-glog-topological.t
186 186 test-gpg.t
187 187 test-graft.t
188 188 test-hg-parseurl.py
189 189 test-hghave.t
190 190 test-hgignore.t
191 191 test-hgk.t
192 192 test-hgrc.t
193 193 test-hgweb-bundle.t
194 194 test-hgweb-descend-empties.t
195 195 test-hgweb-empty.t
196 196 test-hgweb-removed.t
197 197 test-hgwebdir-paths.py
198 198 test-hgwebdirsym.t
199 199 test-histedit-arguments.t
200 200 test-histedit-base.t
201 201 test-histedit-bookmark-motion.t
202 202 test-histedit-commute.t
203 203 test-histedit-drop.t
204 204 test-histedit-edit.t
205 205 test-histedit-fold-non-commute.t
206 206 test-histedit-fold.t
207 207 test-histedit-no-backup.t
208 208 test-histedit-no-change.t
209 209 test-histedit-non-commute-abort.t
210 210 test-histedit-non-commute.t
211 211 test-histedit-obsolete.t
212 212 test-histedit-outgoing.t
213 213 test-histedit-templates.t
214 214 test-http-branchmap.t
215 215 test-http-bundle1.t
216 216 test-http-clone-r.t
217 217 test-http.t
218 218 test-hybridencode.py
219 219 test-identify.t
220 220 test-impexp-branch.t
221 221 test-import-bypass.t
222 222 test-import-eol.t
223 223 test-import-merge.t
224 224 test-import-unknown.t
225 225 test-import.t
226 226 test-imports-checker.t
227 227 test-incoming-outgoing.t
228 228 test-inherit-mode.t
229 229 test-init.t
230 230 test-issue1089.t
231 231 test-issue1102.t
232 232 test-issue1175.t
233 233 test-issue1306.t
234 234 test-issue1438.t
235 235 test-issue1502.t
236 236 test-issue1802.t
237 237 test-issue1877.t
238 238 test-issue1993.t
239 239 test-issue2137.t
240 240 test-issue3084.t
241 241 test-issue4074.t
242 242 test-issue522.t
243 243 test-issue586.t
244 244 test-issue612.t
245 245 test-issue619.t
246 246 test-issue660.t
247 247 test-issue672.t
248 248 test-issue842.t
249 249 test-journal-exists.t
250 250 test-journal-share.t
251 251 test-journal.t
252 252 test-known.t
253 253 test-largefiles-cache.t
254 254 test-largefiles-misc.t
255 255 test-largefiles-small-disk.t
256 256 test-largefiles-update.t
257 257 test-largefiles.t
258 258 test-lfs-largefiles.t
259 259 test-lfs-pointer.py
260 260 test-linelog.py
261 261 test-linerange.py
262 262 test-locate.t
263 263 test-lock-badness.t
264 264 test-log-linerange.t
265 265 test-log.t
266 266 test-logexchange.t
267 267 test-lrucachedict.py
268 268 test-mactext.t
269 269 test-mailmap.t
270 270 test-manifest-merging.t
271 271 test-manifest.py
272 272 test-manifest.t
273 273 test-match.py
274 274 test-mdiff.py
275 275 test-merge-changedelete.t
276 276 test-merge-closedheads.t
277 277 test-merge-commit.t
278 278 test-merge-criss-cross.t
279 279 test-merge-default.t
280 280 test-merge-force.t
281 281 test-merge-halt.t
282 282 test-merge-internal-tools-pattern.t
283 283 test-merge-local.t
284 284 test-merge-remove.t
285 285 test-merge-revert.t
286 286 test-merge-revert2.t
287 287 test-merge-subrepos.t
288 288 test-merge-symlinks.t
289 289 test-merge-tools.t
290 290 test-merge-types.t
291 291 test-merge1.t
292 292 test-merge10.t
293 293 test-merge2.t
294 294 test-merge4.t
295 295 test-merge5.t
296 296 test-merge6.t
297 297 test-merge7.t
298 298 test-merge8.t
299 299 test-merge9.t
300 300 test-minifileset.py
301 301 test-minirst.py
302 302 test-mq-git.t
303 303 test-mq-header-date.t
304 304 test-mq-header-from.t
305 305 test-mq-merge.t
306 306 test-mq-pull-from-bundle.t
307 307 test-mq-qclone-http.t
308 308 test-mq-qdelete.t
309 309 test-mq-qdiff.t
310 310 test-mq-qfold.t
311 311 test-mq-qgoto.t
312 312 test-mq-qimport-fail-cleanup.t
313 313 test-mq-qnew.t
314 314 test-mq-qpush-exact.t
315 315 test-mq-qqueue.t
316 316 test-mq-qrefresh-interactive.t
317 317 test-mq-qrefresh-replace-log-message.t
318 318 test-mq-qrefresh.t
319 319 test-mq-qrename.t
320 320 test-mq-qsave.t
321 321 test-mq-safety.t
322 322 test-mq-subrepo.t
323 323 test-mq-symlinks.t
324 324 test-mv-cp-st-diff.t
325 325 test-narrow-acl.t
326 326 test-narrow-archive.t
327 327 test-narrow-clone-no-ellipsis.t
328 328 test-narrow-clone-non-narrow-server.t
329 329 test-narrow-clone-nonlinear.t
330 330 test-narrow-clone.t
331 331 test-narrow-commit.t
332 332 test-narrow-copies.t
333 333 test-narrow-debugcommands.t
334 334 test-narrow-debugrebuilddirstate.t
335 335 test-narrow-exchange-merges.t
336 336 test-narrow-exchange.t
337 337 test-narrow-expanddirstate.t
338 338 test-narrow-merge.t
339 339 test-narrow-patch.t
340 340 test-narrow-patterns.t
341 341 test-narrow-pull.t
342 342 test-narrow-rebase.t
343 343 test-narrow-shallow-merges.t
344 344 test-narrow-shallow.t
345 345 test-narrow-strip.t
346 346 test-narrow-update.t
347 347 test-narrow-widen.t
348 348 test-narrow.t
349 349 test-nested-repo.t
350 350 test-newbranch.t
351 351 test-nointerrupt.t
352 352 test-obshistory.t
353 353 test-obsmarker-template.t
354 354 test-obsmarkers-effectflag.t
355 355 test-obsolete-bundle-strip.t
356 356 test-obsolete-changeset-exchange.t
357 357 test-obsolete-checkheads.t
358 358 test-obsolete-distributed.t
359 359 test-obsolete-divergent.t
360 360 test-obsolete-tag-cache.t
361 361 test-pager.t
362 362 test-parents.t
363 363 test-parseindex2.py
364 364 test-patch-offset.t
365 365 test-patch.t
366 test-patchbomb-bookmark.t
366 367 test-patchbomb-tls.t
367 368 test-pathconflicts-merge.t
368 369 test-pathconflicts-update.t
369 370 test-pathencode.py
370 371 test-pending.t
371 372 test-permissions.t
372 373 test-phases.t
373 374 test-pull-branch.t
374 375 test-pull-http.t
375 376 test-pull-permission.t
376 377 test-pull-pull-corruption.t
377 378 test-pull-r.t
378 379 test-pull-update.t
379 380 test-pull.t
380 381 test-purge.t
381 382 test-push-checkheads-partial-C1.t
382 383 test-push-checkheads-partial-C2.t
383 384 test-push-checkheads-partial-C3.t
384 385 test-push-checkheads-partial-C4.t
385 386 test-push-checkheads-pruned-B1.t
386 387 test-push-checkheads-pruned-B2.t
387 388 test-push-checkheads-pruned-B3.t
388 389 test-push-checkheads-pruned-B4.t
389 390 test-push-checkheads-pruned-B5.t
390 391 test-push-checkheads-pruned-B6.t
391 392 test-push-checkheads-pruned-B7.t
392 393 test-push-checkheads-pruned-B8.t
393 394 test-push-checkheads-superceed-A1.t
394 395 test-push-checkheads-superceed-A2.t
395 396 test-push-checkheads-superceed-A3.t
396 397 test-push-checkheads-superceed-A4.t
397 398 test-push-checkheads-superceed-A5.t
398 399 test-push-checkheads-superceed-A6.t
399 400 test-push-checkheads-superceed-A7.t
400 401 test-push-checkheads-superceed-A8.t
401 402 test-push-checkheads-unpushed-D1.t
402 403 test-push-checkheads-unpushed-D2.t
403 404 test-push-checkheads-unpushed-D3.t
404 405 test-push-checkheads-unpushed-D4.t
405 406 test-push-checkheads-unpushed-D5.t
406 407 test-push-checkheads-unpushed-D6.t
407 408 test-push-checkheads-unpushed-D7.t
408 409 test-push-http.t
409 410 test-push-warn.t
410 411 test-push.t
411 412 test-pushvars.t
412 413 test-qrecord.t
413 414 test-rebase-abort.t
414 415 test-rebase-backup.t
415 416 test-rebase-base-flag.t
416 417 test-rebase-bookmarks.t
417 418 test-rebase-brute-force.t
418 419 test-rebase-cache.t
419 420 test-rebase-check-restore.t
420 421 test-rebase-collapse.t
421 422 test-rebase-conflicts.t
422 423 test-rebase-dest.t
423 424 test-rebase-detach.t
424 425 test-rebase-emptycommit.t
425 426 test-rebase-inmemory.t
426 427 test-rebase-interruptions.t
427 428 test-rebase-issue-noparam-single-rev.t
428 429 test-rebase-legacy.t
429 430 test-rebase-mq-skip.t
430 431 test-rebase-mq.t
431 432 test-rebase-named-branches.t
432 433 test-rebase-newancestor.t
433 434 test-rebase-obsolete.t
434 435 test-rebase-parameters.t
435 436 test-rebase-partial.t
436 437 test-rebase-pull.t
437 438 test-rebase-rename.t
438 439 test-rebase-scenario-global.t
439 440 test-rebase-templates.t
440 441 test-rebase-transaction.t
441 442 test-rebuildstate.t
442 443 test-record.t
443 444 test-relink.t
444 445 test-remove.t
445 446 test-removeemptydirs.t
446 447 test-rename-after-merge.t
447 448 test-rename-dir-merge.t
448 449 test-rename-merge1.t
449 450 test-rename.t
450 451 test-repair-strip.t
451 452 test-repo-compengines.t
452 453 test-resolve.t
453 454 test-revert-flags.t
454 455 test-revert-interactive.t
455 456 test-revert-unknown.t
456 457 test-revisions.t
457 458 test-revlog-ancestry.py
458 459 test-revlog-group-emptyiter.t
459 460 test-revlog-mmapindex.t
460 461 test-revlog-packentry.t
461 462 test-revlog-raw.py
462 463 test-revlog-v2.t
463 464 test-revset-dirstate-parents.t
464 465 test-revset-legacy-lookup.t
465 466 test-revset-outgoing.t
466 467 test-rollback.t
467 468 test-run-tests.py
468 469 test-run-tests.t
469 470 test-schemes.t
470 471 test-serve.t
471 472 test-setdiscovery.t
472 473 test-share.t
473 474 test-shelve.t
474 475 test-show-stack.t
475 476 test-show-work.t
476 477 test-show.t
477 478 test-simple-update.t
478 479 test-simplekeyvaluefile.py
479 480 test-simplemerge.py
480 481 test-single-head.t
481 482 test-sparse-clear.t
482 483 test-sparse-clone.t
483 484 test-sparse-import.t
484 485 test-sparse-merges.t
485 486 test-sparse-profiles.t
486 487 test-sparse-requirement.t
487 488 test-sparse-verbose-json.t
488 489 test-sparse.t
489 490 test-split.t
490 491 test-ssh-bundle1.t
491 492 test-ssh-clone-r.t
492 493 test-ssh-proto-unbundle.t
493 494 test-ssh-proto.t
494 495 test-ssh.t
495 496 test-sshserver.py
496 497 test-stack.t
497 498 test-status-inprocess.py
498 499 test-status-rev.t
499 500 test-status-terse.t
500 501 test-strict.t
501 502 test-strip-cross.t
502 503 test-strip.t
503 504 test-subrepo-deep-nested-change.t
504 505 test-subrepo-missing.t
505 506 test-subrepo-paths.t
506 507 test-subrepo-recursion.t
507 508 test-subrepo-relative-path.t
508 509 test-subrepo.t
509 510 test-symlink-os-yes-fs-no.py
510 511 test-symlink-placeholder.t
511 512 test-symlinks.t
512 513 test-tag.t
513 514 test-tags.t
514 515 test-template-basic.t
515 516 test-template-functions.t
516 517 test-template-keywords.t
517 518 test-template-map.t
518 519 test-transplant.t
519 520 test-treemanifest.t
520 521 test-ui-color.py
521 522 test-ui-config.py
522 523 test-ui-verbosity.py
523 524 test-unamend.t
524 525 test-unbundlehash.t
525 526 test-uncommit.t
526 527 test-unified-test.t
527 528 test-unionrepo.t
528 529 test-unrelated-pull.t
529 530 test-up-local-change.t
530 531 test-update-branches.t
531 532 test-update-dest.t
532 533 test-update-issue1456.t
533 534 test-update-names.t
534 535 test-update-reverse.t
535 536 test-upgrade-repo.t
536 537 test-url-download.t
537 538 test-url-rev.t
538 539 test-url.py
539 540 test-username-newline.t
540 541 test-util.py
541 542 test-verify.t
542 543 test-walk.t
543 544 test-walkrepo.py
544 545 test-websub.t
545 546 test-win32text.t
546 547 test-wireproto-clientreactor.py
547 548 test-wireproto-framing.py
548 549 test-wireproto-serverreactor.py
549 550 test-wireproto.py
550 551 test-wsgirequest.py
551 552 test-xdg.t
@@ -1,825 +1,832
1 1 # patchbomb.py - sending Mercurial changesets as patch emails
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to send changesets as (a series of) patch emails
9 9
10 10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 11 describes the series as a whole.
12 12
13 13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 14 first line of the changeset description as the subject text. The
15 15 message contains two or three body parts:
16 16
17 17 - The changeset description.
18 18 - [Optional] The result of running diffstat on the patch.
19 19 - The patch itself, as generated by :hg:`export`.
20 20
21 21 Each message refers to the first in the series using the In-Reply-To
22 22 and References headers, so they will show up as a sequence in threaded
23 23 mail and news readers, and in mail archives.
24 24
25 25 To configure other defaults, add a section like this to your
26 26 configuration file::
27 27
28 28 [email]
29 29 from = My Name <my@email>
30 30 to = recipient1, recipient2, ...
31 31 cc = cc1, cc2, ...
32 32 bcc = bcc1, bcc2, ...
33 33 reply-to = address1, address2, ...
34 34
35 35 Use ``[patchbomb]`` as configuration section name if you need to
36 36 override global ``[email]`` address settings.
37 37
38 38 Then you can use the :hg:`email` command to mail a series of
39 39 changesets as a patchbomb.
40 40
41 41 You can also either configure the method option in the email section
42 42 to be a sendmail compatible mailer or fill out the [smtp] section so
43 43 that the patchbomb extension can automatically send patchbombs
44 44 directly from the commandline. See the [email] and [smtp] sections in
45 45 hgrc(5) for details.
46 46
47 47 By default, :hg:`email` will prompt for a ``To`` or ``CC`` header if
48 48 you do not supply one via configuration or the command line. You can
49 49 override this to never prompt by configuring an empty value::
50 50
51 51 [email]
52 52 cc =
53 53
54 54 You can control the default inclusion of an introduction message with the
55 55 ``patchbomb.intro`` configuration option. The configuration is always
56 56 overwritten by command line flags like --intro and --desc::
57 57
58 58 [patchbomb]
59 59 intro=auto # include introduction message if more than 1 patch (default)
60 60 intro=never # never include an introduction message
61 61 intro=always # always include an introduction message
62 62
63 63 You can specify a template for flags to be added in subject prefixes. Flags
64 64 specified by --flag option are exported as ``{flags}`` keyword::
65 65
66 66 [patchbomb]
67 67 flagtemplate = "{separate(' ',
68 68 ifeq(branch, 'default', '', branch|upper),
69 69 flags)}"
70 70
71 71 You can set patchbomb to always ask for confirmation by setting
72 72 ``patchbomb.confirm`` to true.
73 73 '''
74 74 from __future__ import absolute_import
75 75
76 76 import email.encoders as emailencoders
77 77 import email.generator as emailgen
78 78 import email.mime.base as emimebase
79 79 import email.mime.multipart as emimemultipart
80 80 import email.utils as eutil
81 81 import errno
82 82 import os
83 83 import socket
84 84
85 85 from mercurial.i18n import _
86 86 from mercurial import (
87 87 cmdutil,
88 88 commands,
89 89 encoding,
90 90 error,
91 91 formatter,
92 92 hg,
93 93 mail,
94 94 node as nodemod,
95 95 patch,
96 96 pycompat,
97 97 registrar,
98 98 scmutil,
99 99 templater,
100 100 util,
101 101 )
102 102 from mercurial.utils import dateutil
103 103 stringio = util.stringio
104 104
105 105 cmdtable = {}
106 106 command = registrar.command(cmdtable)
107 107
108 108 configtable = {}
109 109 configitem = registrar.configitem(configtable)
110 110
111 111 configitem('patchbomb', 'bundletype',
112 112 default=None,
113 113 )
114 114 configitem('patchbomb', 'bcc',
115 115 default=None,
116 116 )
117 117 configitem('patchbomb', 'cc',
118 118 default=None,
119 119 )
120 120 configitem('patchbomb', 'confirm',
121 121 default=False,
122 122 )
123 123 configitem('patchbomb', 'flagtemplate',
124 124 default=None,
125 125 )
126 126 configitem('patchbomb', 'from',
127 127 default=None,
128 128 )
129 129 configitem('patchbomb', 'intro',
130 130 default='auto',
131 131 )
132 132 configitem('patchbomb', 'publicurl',
133 133 default=None,
134 134 )
135 135 configitem('patchbomb', 'reply-to',
136 136 default=None,
137 137 )
138 138 configitem('patchbomb', 'to',
139 139 default=None,
140 140 )
141 141
142 142 if pycompat.ispy3:
143 143 _bytesgenerator = emailgen.BytesGenerator
144 144 else:
145 145 _bytesgenerator = lambda f: f
146 146
147 147 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
148 148 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
149 149 # be specifying the version(s) of Mercurial they are tested with, or
150 150 # leave the attribute unspecified.
151 151 testedwith = 'ships-with-hg-core'
152 152
153 153 def _addpullheader(seq, ctx):
154 154 """Add a header pointing to a public URL where the changeset is available
155 155 """
156 156 repo = ctx.repo()
157 157 # experimental config: patchbomb.publicurl
158 158 # waiting for some logic that check that the changeset are available on the
159 159 # destination before patchbombing anything.
160 160 publicurl = repo.ui.config('patchbomb', 'publicurl')
161 161 if publicurl:
162 162 return ('Available At %s\n'
163 163 '# hg pull %s -r %s' % (publicurl, publicurl, ctx))
164 164 return None
165 165
166 166 def uisetup(ui):
167 167 cmdutil.extraexport.append('pullurl')
168 168 cmdutil.extraexportmap['pullurl'] = _addpullheader
169 169
170 170 def reposetup(ui, repo):
171 171 if not repo.local():
172 172 return
173 173 repo._wlockfreeprefix.add('last-email.txt')
174 174
175 175 def prompt(ui, prompt, default=None, rest=':'):
176 176 if default:
177 177 prompt += ' [%s]' % default
178 178 return ui.prompt(prompt + rest, default)
179 179
180 180 def introwanted(ui, opts, number):
181 181 '''is an introductory message apparently wanted?'''
182 182 introconfig = ui.config('patchbomb', 'intro')
183 183 if opts.get('intro') or opts.get('desc'):
184 184 intro = True
185 185 elif introconfig == 'always':
186 186 intro = True
187 187 elif introconfig == 'never':
188 188 intro = False
189 189 elif introconfig == 'auto':
190 190 intro = 1 < number
191 191 else:
192 192 ui.write_err(_('warning: invalid patchbomb.intro value "%s"\n')
193 193 % introconfig)
194 194 ui.write_err(_('(should be one of always, never, auto)\n'))
195 195 intro = 1 < number
196 196 return intro
197 197
198 198 def _formatflags(ui, repo, rev, flags):
199 199 """build flag string optionally by template"""
200 200 tmpl = ui.config('patchbomb', 'flagtemplate')
201 201 if not tmpl:
202 202 return ' '.join(flags)
203 203 out = util.stringio()
204 204 opts = {'template': templater.unquotestring(tmpl)}
205 205 with formatter.templateformatter(ui, out, 'patchbombflag', opts) as fm:
206 206 fm.startitem()
207 207 fm.context(ctx=repo[rev])
208 208 fm.write('flags', '%s', fm.formatlist(flags, name='flag'))
209 209 return out.getvalue()
210 210
211 211 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
212 212 """build prefix to patch subject"""
213 213 flag = _formatflags(ui, repo, rev, flags)
214 214 if flag:
215 215 flag = ' ' + flag
216 216
217 217 if not numbered:
218 218 return '[PATCH%s]' % flag
219 219 else:
220 220 tlen = len("%d" % total)
221 221 return '[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
222 222
223 223 def makepatch(ui, repo, rev, patchlines, opts, _charsets, idx, total, numbered,
224 224 patchname=None):
225 225
226 226 desc = []
227 227 node = None
228 228 body = ''
229 229
230 230 for line in patchlines:
231 231 if line.startswith('#'):
232 232 if line.startswith('# Node ID'):
233 233 node = line.split()[-1]
234 234 continue
235 235 if line.startswith('diff -r') or line.startswith('diff --git'):
236 236 break
237 237 desc.append(line)
238 238
239 239 if not patchname and not node:
240 240 raise ValueError
241 241
242 242 if opts.get('attach') and not opts.get('body'):
243 243 body = ('\n'.join(desc[1:]).strip() or
244 244 'Patch subject is complete summary.')
245 245 body += '\n\n\n'
246 246
247 247 if opts.get('plain'):
248 248 while patchlines and patchlines[0].startswith('# '):
249 249 patchlines.pop(0)
250 250 if patchlines:
251 251 patchlines.pop(0)
252 252 while patchlines and not patchlines[0].strip():
253 253 patchlines.pop(0)
254 254
255 255 ds = patch.diffstat(patchlines)
256 256 if opts.get('diffstat'):
257 257 body += ds + '\n\n'
258 258
259 259 addattachment = opts.get('attach') or opts.get('inline')
260 260 if not addattachment or opts.get('body'):
261 261 body += '\n'.join(patchlines)
262 262
263 263 if addattachment:
264 264 msg = emimemultipart.MIMEMultipart()
265 265 if body:
266 266 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
267 267 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
268 268 opts.get('test'))
269 269 binnode = nodemod.bin(node)
270 270 # if node is mq patch, it will have the patch file's name as a tag
271 271 if not patchname:
272 272 patchtags = [t for t in repo.nodetags(binnode)
273 273 if t.endswith('.patch') or t.endswith('.diff')]
274 274 if patchtags:
275 275 patchname = patchtags[0]
276 276 elif total > 1:
277 277 patchname = cmdutil.makefilename(repo[node], '%b-%n.patch',
278 278 seqno=idx, total=total)
279 279 else:
280 280 patchname = cmdutil.makefilename(repo[node], '%b.patch')
281 281 disposition = 'inline'
282 282 if opts.get('attach'):
283 283 disposition = 'attachment'
284 284 p['Content-Disposition'] = disposition + '; filename=' + patchname
285 285 msg.attach(p)
286 286 else:
287 287 msg = mail.mimetextpatch(body, display=opts.get('test'))
288 288
289 289 prefix = _formatprefix(ui, repo, rev, opts.get('flag'), idx, total,
290 290 numbered)
291 291 subj = desc[0].strip().rstrip('. ')
292 292 if not numbered:
293 293 subj = ' '.join([prefix, opts.get('subject') or subj])
294 294 else:
295 295 subj = ' '.join([prefix, subj])
296 296 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
297 297 msg['X-Mercurial-Node'] = node
298 298 msg['X-Mercurial-Series-Index'] = '%i' % idx
299 299 msg['X-Mercurial-Series-Total'] = '%i' % total
300 300 return msg, subj, ds
301 301
302 302 def _getpatches(repo, revs, **opts):
303 303 """return a list of patches for a list of revisions
304 304
305 305 Each patch in the list is itself a list of lines.
306 306 """
307 307 ui = repo.ui
308 308 prev = repo['.'].rev()
309 309 for r in revs:
310 310 if r == prev and (repo[None].files() or repo[None].deleted()):
311 311 ui.warn(_('warning: working directory has '
312 312 'uncommitted changes\n'))
313 313 output = stringio()
314 314 cmdutil.exportfile(repo, [r], output,
315 315 opts=patch.difffeatureopts(ui, opts, git=True))
316 316 yield output.getvalue().split('\n')
317 317 def _getbundle(repo, dest, **opts):
318 318 """return a bundle containing changesets missing in "dest"
319 319
320 320 The `opts` keyword-arguments are the same as the one accepted by the
321 321 `bundle` command.
322 322
323 323 The bundle is a returned as a single in-memory binary blob.
324 324 """
325 325 ui = repo.ui
326 326 tmpdir = pycompat.mkdtemp(prefix='hg-email-bundle-')
327 327 tmpfn = os.path.join(tmpdir, 'bundle')
328 328 btype = ui.config('patchbomb', 'bundletype')
329 329 if btype:
330 330 opts[r'type'] = btype
331 331 try:
332 332 commands.bundle(ui, repo, tmpfn, dest, **opts)
333 333 return util.readfile(tmpfn)
334 334 finally:
335 335 try:
336 336 os.unlink(tmpfn)
337 337 except OSError:
338 338 pass
339 339 os.rmdir(tmpdir)
340 340
341 341 def _getdescription(repo, defaultbody, sender, **opts):
342 342 """obtain the body of the introduction message and return it
343 343
344 344 This is also used for the body of email with an attached bundle.
345 345
346 346 The body can be obtained either from the command line option or entered by
347 347 the user through the editor.
348 348 """
349 349 ui = repo.ui
350 350 if opts.get(r'desc'):
351 351 body = open(opts.get(r'desc')).read()
352 352 else:
353 353 ui.write(_('\nWrite the introductory message for the '
354 354 'patch series.\n\n'))
355 355 body = ui.edit(defaultbody, sender, repopath=repo.path,
356 356 action='patchbombbody')
357 357 # Save series description in case sendmail fails
358 358 msgfile = repo.vfs('last-email.txt', 'wb')
359 359 msgfile.write(body)
360 360 msgfile.close()
361 361 return body
362 362
363 363 def _getbundlemsgs(repo, sender, bundle, **opts):
364 364 """Get the full email for sending a given bundle
365 365
366 366 This function returns a list of "email" tuples (subject, content, None).
367 367 The list is always one message long in that case.
368 368 """
369 369 ui = repo.ui
370 370 _charsets = mail._charsets(ui)
371 371 subj = (opts.get(r'subject')
372 372 or prompt(ui, 'Subject:', 'A bundle for your repository'))
373 373
374 374 body = _getdescription(repo, '', sender, **opts)
375 375 msg = emimemultipart.MIMEMultipart()
376 376 if body:
377 377 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
378 378 datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
379 379 datapart.set_payload(bundle)
380 380 bundlename = '%s.hg' % opts.get(r'bundlename', 'bundle')
381 381 datapart.add_header(r'Content-Disposition', r'attachment',
382 382 filename=encoding.strfromlocal(bundlename))
383 383 emailencoders.encode_base64(datapart)
384 384 msg.attach(datapart)
385 385 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
386 386 return [(msg, subj, None)]
387 387
388 388 def _makeintro(repo, sender, revs, patches, **opts):
389 389 """make an introduction email, asking the user for content if needed
390 390
391 391 email is returned as (subject, body, cumulative-diffstat)"""
392 392 ui = repo.ui
393 393 _charsets = mail._charsets(ui)
394 394
395 395 # use the last revision which is likely to be a bookmarked head
396 396 prefix = _formatprefix(ui, repo, revs.last(), opts.get(r'flag'),
397 397 0, len(patches), numbered=True)
398 398 subj = (opts.get(r'subject') or
399 399 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
400 400 if not subj:
401 401 return None # skip intro if the user doesn't bother
402 402
403 403 subj = prefix + ' ' + subj
404 404
405 405 body = ''
406 406 if opts.get(r'diffstat'):
407 407 # generate a cumulative diffstat of the whole patch series
408 408 diffstat = patch.diffstat(sum(patches, []))
409 409 body = '\n' + diffstat
410 410 else:
411 411 diffstat = None
412 412
413 413 body = _getdescription(repo, body, sender, **opts)
414 414 msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
415 415 msg['Subject'] = mail.headencode(ui, subj, _charsets,
416 416 opts.get(r'test'))
417 417 return (msg, subj, diffstat)
418 418
419 419 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
420 420 """return a list of emails from a list of patches
421 421
422 422 This involves introduction message creation if necessary.
423 423
424 424 This function returns a list of "email" tuples (subject, content, None).
425 425 """
426 426 bytesopts = pycompat.byteskwargs(opts)
427 427 ui = repo.ui
428 428 _charsets = mail._charsets(ui)
429 429 patches = list(_getpatches(repo, revs, **opts))
430 430 msgs = []
431 431
432 432 ui.write(_('this patch series consists of %d patches.\n\n')
433 433 % len(patches))
434 434
435 435 # build the intro message, or skip it if the user declines
436 436 if introwanted(ui, bytesopts, len(patches)):
437 437 msg = _makeintro(repo, sender, revs, patches, **opts)
438 438 if msg:
439 439 msgs.append(msg)
440 440
441 441 # are we going to send more than one message?
442 442 numbered = len(msgs) + len(patches) > 1
443 443
444 444 # now generate the actual patch messages
445 445 name = None
446 446 assert len(revs) == len(patches)
447 447 for i, (r, p) in enumerate(zip(revs, patches)):
448 448 if patchnames:
449 449 name = patchnames[i]
450 450 msg = makepatch(ui, repo, r, p, bytesopts, _charsets,
451 451 i + 1, len(patches), numbered, name)
452 452 msgs.append(msg)
453 453
454 454 return msgs
455 455
456 456 def _getoutgoing(repo, dest, revs):
457 457 '''Return the revisions present locally but not in dest'''
458 458 ui = repo.ui
459 459 url = ui.expandpath(dest or 'default-push', dest or 'default')
460 460 url = hg.parseurl(url)[0]
461 461 ui.status(_('comparing with %s\n') % util.hidepassword(url))
462 462
463 463 revs = [r for r in revs if r >= 0]
464 464 if not revs:
465 465 revs = [repo.changelog.tiprev()]
466 466 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
467 467 if not revs:
468 468 ui.status(_("no changes found\n"))
469 469 return revs
470 470
471 471 emailopts = [
472 472 ('', 'body', None, _('send patches as inline message text (default)')),
473 473 ('a', 'attach', None, _('send patches as attachments')),
474 474 ('i', 'inline', None, _('send patches as inline attachments')),
475 475 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
476 476 ('c', 'cc', [], _('email addresses of copy recipients')),
477 477 ('', 'confirm', None, _('ask for confirmation before sending')),
478 478 ('d', 'diffstat', None, _('add diffstat output to messages')),
479 479 ('', 'date', '', _('use the given date as the sending date')),
480 480 ('', 'desc', '', _('use the given file as the series description')),
481 481 ('f', 'from', '', _('email address of sender')),
482 482 ('n', 'test', None, _('print messages that would be sent')),
483 483 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
484 484 ('', 'reply-to', [], _('email addresses replies should be sent to')),
485 485 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
486 486 ('', 'in-reply-to', '', _('message identifier to reply to')),
487 487 ('', 'flag', [], _('flags to add in subject prefixes')),
488 488 ('t', 'to', [], _('email addresses of recipients'))]
489 489
490 490 @command('email',
491 491 [('g', 'git', None, _('use git extended diff format')),
492 492 ('', 'plain', None, _('omit hg patch header')),
493 493 ('o', 'outgoing', None,
494 494 _('send changes not found in the target repository')),
495 495 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
496 496 ('B', 'bookmark', '', _('send changes only reachable by given bookmark')),
497 497 ('', 'bundlename', 'bundle',
498 498 _('name of the bundle attachment file'), _('NAME')),
499 499 ('r', 'rev', [], _('a revision to send'), _('REV')),
500 500 ('', 'force', None, _('run even when remote repository is unrelated '
501 501 '(with -b/--bundle)')),
502 502 ('', 'base', [], _('a base changeset to specify instead of a destination '
503 503 '(with -b/--bundle)'), _('REV')),
504 504 ('', 'intro', None, _('send an introduction email for a single patch')),
505 505 ] + emailopts + cmdutil.remoteopts,
506 506 _('hg email [OPTION]... [DEST]...'))
507 507 def email(ui, repo, *revs, **opts):
508 508 '''send changesets by email
509 509
510 510 By default, diffs are sent in the format generated by
511 511 :hg:`export`, one per message. The series starts with a "[PATCH 0
512 512 of N]" introduction, which describes the series as a whole.
513 513
514 514 Each patch email has a Subject line of "[PATCH M of N] ...", using
515 515 the first line of the changeset description as the subject text.
516 516 The message contains two or three parts. First, the changeset
517 517 description.
518 518
519 519 With the -d/--diffstat option, if the diffstat program is
520 520 installed, the result of running diffstat on the patch is inserted.
521 521
522 522 Finally, the patch itself, as generated by :hg:`export`.
523 523
524 524 With the -d/--diffstat or --confirm options, you will be presented
525 525 with a final summary of all messages and asked for confirmation before
526 526 the messages are sent.
527 527
528 528 By default the patch is included as text in the email body for
529 529 easy reviewing. Using the -a/--attach option will instead create
530 530 an attachment for the patch. With -i/--inline an inline attachment
531 531 will be created. You can include a patch both as text in the email
532 532 body and as a regular or an inline attachment by combining the
533 533 -a/--attach or -i/--inline with the --body option.
534 534
535 535 With -B/--bookmark changesets reachable by the given bookmark are
536 536 selected.
537 537
538 538 With -o/--outgoing, emails will be generated for patches not found
539 539 in the destination repository (or only those which are ancestors
540 540 of the specified revisions if any are provided)
541 541
542 542 With -b/--bundle, changesets are selected as for --outgoing, but a
543 543 single email containing a binary Mercurial bundle as an attachment
544 544 will be sent. Use the ``patchbomb.bundletype`` config option to
545 545 control the bundle type as with :hg:`bundle --type`.
546 546
547 547 With -m/--mbox, instead of previewing each patchbomb message in a
548 548 pager or sending the messages directly, it will create a UNIX
549 549 mailbox file with the patch emails. This mailbox file can be
550 550 previewed with any mail user agent which supports UNIX mbox
551 551 files.
552 552
553 553 With -n/--test, all steps will run, but mail will not be sent.
554 554 You will be prompted for an email recipient address, a subject and
555 555 an introductory message describing the patches of your patchbomb.
556 556 Then when all is done, patchbomb messages are displayed.
557 557
558 558 In case email sending fails, you will find a backup of your series
559 559 introductory message in ``.hg/last-email.txt``.
560 560
561 561 The default behavior of this command can be customized through
562 562 configuration. (See :hg:`help patchbomb` for details)
563 563
564 564 Examples::
565 565
566 566 hg email -r 3000 # send patch 3000 only
567 567 hg email -r 3000 -r 3001 # send patches 3000 and 3001
568 568 hg email -r 3000:3005 # send patches 3000 through 3005
569 569 hg email 3000 # send patch 3000 (deprecated)
570 570
571 571 hg email -o # send all patches not in default
572 572 hg email -o DEST # send all patches not in DEST
573 573 hg email -o -r 3000 # send all ancestors of 3000 not in default
574 574 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
575 575
576 576 hg email -B feature # send all ancestors of feature bookmark
577 577
578 578 hg email -b # send bundle of all patches not in default
579 579 hg email -b DEST # send bundle of all patches not in DEST
580 580 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
581 581 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
582 582
583 583 hg email -o -m mbox && # generate an mbox file...
584 584 mutt -R -f mbox # ... and view it with mutt
585 585 hg email -o -m mbox && # generate an mbox file ...
586 586 formail -s sendmail \\ # ... and use formail to send from the mbox
587 587 -bm -t < mbox # ... using sendmail
588 588
589 589 Before using this command, you will need to enable email in your
590 590 hgrc. See the [email] section in hgrc(5) for details.
591 591 '''
592 592 opts = pycompat.byteskwargs(opts)
593 593
594 594 _charsets = mail._charsets(ui)
595 595
596 596 bundle = opts.get('bundle')
597 597 date = opts.get('date')
598 598 mbox = opts.get('mbox')
599 599 outgoing = opts.get('outgoing')
600 600 rev = opts.get('rev')
601 601 bookmark = opts.get('bookmark')
602 602
603 603 if not (opts.get('test') or mbox):
604 604 # really sending
605 605 mail.validateconfig(ui)
606 606
607 607 if not (revs or rev or outgoing or bundle or bookmark):
608 608 raise error.Abort(_('specify at least one changeset with -B, -r or -o'))
609 609
610 610 if outgoing and bundle:
611 611 raise error.Abort(_("--outgoing mode always on with --bundle;"
612 612 " do not re-specify --outgoing"))
613 613 if rev and bookmark:
614 614 raise error.Abort(_("-r and -B are mutually exclusive"))
615 615
616 616 if outgoing or bundle:
617 617 if len(revs) > 1:
618 618 raise error.Abort(_("too many destinations"))
619 619 if revs:
620 620 dest = revs[0]
621 621 else:
622 622 dest = None
623 623 revs = []
624 624
625 625 if rev:
626 626 if revs:
627 627 raise error.Abort(_('use only one form to specify the revision'))
628 628 revs = rev
629 629 elif bookmark:
630 630 if bookmark not in repo._bookmarks:
631 631 raise error.Abort(_("bookmark '%s' not found") % bookmark)
632 632 revs = scmutil.bookmarkrevs(repo, bookmark)
633 633
634 634 revs = scmutil.revrange(repo, revs)
635 635 if outgoing:
636 636 revs = _getoutgoing(repo, dest, revs)
637 637 if bundle:
638 638 opts['revs'] = ["%d" % r for r in revs]
639 639
640 640 # check if revision exist on the public destination
641 641 publicurl = repo.ui.config('patchbomb', 'publicurl')
642 642 if publicurl:
643 643 repo.ui.debug('checking that revision exist in the public repo\n')
644 644 try:
645 645 publicpeer = hg.peer(repo, {}, publicurl)
646 646 except error.RepoError:
647 647 repo.ui.write_err(_('unable to access public repo: %s\n')
648 648 % publicurl)
649 649 raise
650 650 if not publicpeer.capable('known'):
651 651 repo.ui.debug('skipping existence checks: public repo too old\n')
652 652 else:
653 653 out = [repo[r] for r in revs]
654 654 known = publicpeer.known(h.node() for h in out)
655 655 missing = []
656 656 for idx, h in enumerate(out):
657 657 if not known[idx]:
658 658 missing.append(h)
659 659 if missing:
660 660 if 1 < len(missing):
661 661 msg = _('public "%s" is missing %s and %i others')
662 662 msg %= (publicurl, missing[0], len(missing) - 1)
663 663 else:
664 664 msg = _('public url %s is missing %s')
665 665 msg %= (publicurl, missing[0])
666 666 missingrevs = [ctx.rev() for ctx in missing]
667 667 revhint = ' '.join('-r %s' % h
668 668 for h in repo.set('heads(%ld)', missingrevs))
669 669 hint = _("use 'hg push %s %s'") % (publicurl, revhint)
670 670 raise error.Abort(msg, hint=hint)
671 671
672 672 # start
673 673 if date:
674 674 start_time = dateutil.parsedate(date)
675 675 else:
676 676 start_time = dateutil.makedate()
677 677
678 678 def genmsgid(id):
679 679 return '<%s.%d@%s>' % (id[:20], int(start_time[0]),
680 680 encoding.strtolocal(socket.getfqdn()))
681 681
682 682 # deprecated config: patchbomb.from
683 683 sender = (opts.get('from') or ui.config('email', 'from') or
684 684 ui.config('patchbomb', 'from') or
685 685 prompt(ui, 'From', ui.username()))
686 686
687 687 if bundle:
688 688 stropts = pycompat.strkwargs(opts)
689 689 bundledata = _getbundle(repo, dest, **stropts)
690 690 bundleopts = stropts.copy()
691 691 bundleopts.pop(r'bundle', None) # already processed
692 692 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
693 693 else:
694 694 msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))
695 695
696 696 showaddrs = []
697 697
698 698 def getaddrs(header, ask=False, default=None):
699 699 configkey = header.lower()
700 700 opt = header.replace('-', '_').lower()
701 701 addrs = opts.get(opt)
702 702 if addrs:
703 703 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
704 704 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
705 705
706 706 # not on the command line: fallback to config and then maybe ask
707 707 addr = (ui.config('email', configkey) or
708 708 ui.config('patchbomb', configkey))
709 709 if not addr:
710 710 specified = (ui.hasconfig('email', configkey) or
711 711 ui.hasconfig('patchbomb', configkey))
712 712 if not specified and ask:
713 713 addr = prompt(ui, header, default=default)
714 714 if addr:
715 715 showaddrs.append('%s: %s' % (header, addr))
716 716 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
717 717 elif default:
718 718 return mail.addrlistencode(
719 719 ui, [default], _charsets, opts.get('test'))
720 720 return []
721 721
722 722 to = getaddrs('To', ask=True)
723 723 if not to:
724 724 # we can get here in non-interactive mode
725 725 raise error.Abort(_('no recipient addresses provided'))
726 726 cc = getaddrs('Cc', ask=True, default='')
727 727 bcc = getaddrs('Bcc')
728 728 replyto = getaddrs('Reply-To')
729 729
730 730 confirm = ui.configbool('patchbomb', 'confirm')
731 731 confirm |= bool(opts.get('diffstat') or opts.get('confirm'))
732 732
733 733 if confirm:
734 734 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
735 735 ui.write(('From: %s\n' % sender), label='patchbomb.from')
736 736 for addr in showaddrs:
737 737 ui.write('%s\n' % addr, label='patchbomb.to')
738 738 for m, subj, ds in msgs:
739 739 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
740 740 if ds:
741 741 ui.write(ds, label='patchbomb.diffstats')
742 742 ui.write('\n')
743 743 if ui.promptchoice(_('are you sure you want to send (yn)?'
744 744 '$$ &Yes $$ &No')):
745 745 raise error.Abort(_('patchbomb canceled'))
746 746
747 747 ui.write('\n')
748 748
749 749 parent = opts.get('in_reply_to') or None
750 750 # angle brackets may be omitted, they're not semantically part of the msg-id
751 751 if parent is not None:
752 752 if not parent.startswith('<'):
753 753 parent = '<' + parent
754 754 if not parent.endswith('>'):
755 755 parent += '>'
756 756
757 757 sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
758 758 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
759 759 sendmail = None
760 760 firstpatch = None
761 761 progress = ui.makeprogress(_('sending'), unit=_('emails'), total=len(msgs))
762 762 for i, (m, subj, ds) in enumerate(msgs):
763 763 try:
764 764 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
765 765 if not firstpatch:
766 766 firstpatch = m['Message-Id']
767 767 m['X-Mercurial-Series-Id'] = firstpatch
768 768 except TypeError:
769 769 m['Message-Id'] = genmsgid('patchbomb')
770 770 if parent:
771 771 m['In-Reply-To'] = parent
772 772 m['References'] = parent
773 773 if not parent or 'X-Mercurial-Node' not in m:
774 774 parent = m['Message-Id']
775 775
776 776 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
777 777 m['Date'] = eutil.formatdate(start_time[0], localtime=True)
778 778
779 779 start_time = (start_time[0] + 1, start_time[1])
780 780 m['From'] = sender
781 781 m['To'] = ', '.join(to)
782 782 if cc:
783 783 m['Cc'] = ', '.join(cc)
784 784 if bcc:
785 785 m['Bcc'] = ', '.join(bcc)
786 786 if replyto:
787 787 m['Reply-To'] = ', '.join(replyto)
788 788 # Fix up all headers to be native strings.
789 789 # TODO(durin42): this should probably be cleaned up above in the future.
790 790 if pycompat.ispy3:
791 791 for hdr, val in list(m.items()):
792 change = False
792 793 if isinstance(hdr, bytes):
793 794 del m[hdr]
794 795 hdr = pycompat.strurl(hdr)
796 change = True
795 797 if isinstance(val, bytes):
796 798 val = pycompat.strurl(val)
797 m[hdr] = val
799 if not change:
800 # prevent duplicate headers
801 del m[hdr]
802 change = True
803 if change:
804 m[hdr] = val
798 805 if opts.get('test'):
799 806 ui.status(_('displaying '), subj, ' ...\n')
800 807 ui.pager('email')
801 808 generator = emailgen.Generator(_bytesgenerator(ui),
802 809 mangle_from_=False)
803 810 try:
804 811 generator.flatten(m, 0)
805 812 ui.write('\n')
806 813 except IOError as inst:
807 814 if inst.errno != errno.EPIPE:
808 815 raise
809 816 else:
810 817 if not sendmail:
811 818 sendmail = mail.connect(ui, mbox=mbox)
812 819 ui.status(_('sending '), subj, ' ...\n')
813 820 progress.update(i, item=subj)
814 821 if not mbox:
815 822 # Exim does not remove the Bcc field
816 823 del m['Bcc']
817 824 fp = stringio()
818 825 generator = emailgen.Generator(_bytesgenerator(fp),
819 826 mangle_from_=False)
820 827 generator.flatten(m, 0)
821 828 alldests = to + bcc + cc
822 829 alldests = [encoding.strfromlocal(d) for d in alldests]
823 830 sendmail(sender_addr, alldests, fp.getvalue())
824 831
825 832 progress.complete()
General Comments 0
You need to be logged in to leave comments. Login now