##// END OF EJS Templates
py3: port test-bugzilla.t to Python 3...
Augie Fackler -
r41379:7370f302 default
parent child Browse files
Show More
@@ -1,720 +1,721
1 1 test-abort-checkin.t
2 2 test-absorb-edit-lines.t
3 3 test-absorb-filefixupstate.py
4 4 test-absorb-phase.t
5 5 test-absorb-rename.t
6 6 test-absorb-strip.t
7 7 test-absorb.t
8 8 test-add.t
9 9 test-addremove-similar.t
10 10 test-addremove.t
11 11 test-alias.t
12 12 test-amend-subrepo.t
13 13 test-amend.t
14 14 test-ancestor.py
15 15 test-annotate.py
16 16 test-annotate.t
17 17 test-arbitraryfilectx.t
18 18 test-archive-symlinks.t
19 19 test-archive.t
20 20 test-atomictempfile.py
21 21 test-audit-path.t
22 22 test-audit-subrepo.t
23 23 test-automv.t
24 24 test-backout.t
25 25 test-backwards-remove.t
26 26 test-bad-extension.t
27 27 test-bad-pull.t
28 28 test-basic.t
29 29 test-batching.py
30 30 test-bdiff.py
31 31 test-bheads.t
32 32 test-bisect.t
33 33 test-bisect2.t
34 34 test-bisect3.t
35 35 test-blackbox.t
36 36 test-bookflow.t
37 37 test-bookmarks-current.t
38 38 test-bookmarks-merge.t
39 39 test-bookmarks-pushpull.t
40 40 test-bookmarks-rebase.t
41 41 test-bookmarks-strip.t
42 42 test-bookmarks.t
43 43 test-branch-change.t
44 44 test-branch-option.t
45 45 test-branch-tag-confict.t
46 46 test-branches.t
47 test-bugzilla.t
47 48 test-bundle-phases.t
48 49 test-bundle-r.t
49 50 test-bundle-type.t
50 51 test-bundle-vs-outgoing.t
51 52 test-bundle.t
52 53 test-bundle2-exchange.t
53 54 test-bundle2-format.t
54 55 test-bundle2-multiple-changegroups.t
55 56 test-bundle2-pushback.t
56 57 test-bundle2-remote-changegroup.t
57 58 test-cache-abuse.t
58 59 test-cappedreader.py
59 60 test-casecollision.t
60 61 test-cat.t
61 62 test-cbor.py
62 63 test-censor.t
63 64 test-changelog-exec.t
64 65 test-check-code.t
65 66 test-check-commit.t
66 67 test-check-config.py
67 68 test-check-config.t
68 69 test-check-execute.t
69 70 test-check-help.t
70 71 test-check-interfaces.py
71 72 test-check-module-imports.t
72 73 test-check-py3-compat.t
73 74 test-check-pyflakes.t
74 75 test-check-pylint.t
75 76 test-check-shbang.t
76 77 test-children.t
77 78 test-churn.t
78 79 test-clone-cgi.t
79 80 test-clone-pull-corruption.t
80 81 test-clone-r.t
81 82 test-clone-uncompressed.t
82 83 test-clone-update-order.t
83 84 test-clone.t
84 85 test-clonebundles.t
85 86 test-close-head.t
86 87 test-commandserver.t
87 88 test-commit-amend.t
88 89 test-commit-interactive.t
89 90 test-commit-multiple.t
90 91 test-commit-unresolved.t
91 92 test-commit.t
92 93 test-committer.t
93 94 test-completion.t
94 95 test-config-env.py
95 96 test-config.t
96 97 test-conflict.t
97 98 test-confused-revert.t
98 99 test-context-metadata.t
99 100 test-context.py
100 101 test-contrib-check-code.t
101 102 test-contrib-check-commit.t
102 103 test-contrib-dumprevlog.t
103 104 test-contrib-perf.t
104 105 test-contrib-relnotes.t
105 106 test-contrib-testparseutil.t
106 107 test-contrib.t
107 108 test-convert-authormap.t
108 109 test-convert-clonebranches.t
109 110 test-convert-cvs-branch.t
110 111 test-convert-cvs-detectmerge.t
111 112 test-convert-cvs-synthetic.t
112 113 test-convert-cvs.t
113 114 test-convert-cvsnt-mergepoints.t
114 115 test-convert-datesort.t
115 116 test-convert-filemap.t
116 117 test-convert-hg-sink.t
117 118 test-convert-hg-source.t
118 119 test-convert-hg-startrev.t
119 120 test-convert-splicemap.t
120 121 test-convert-svn-sink.t
121 122 test-convert-tagsbranch-topology.t
122 123 test-copy-move-merge.t
123 124 test-copy.t
124 125 test-copytrace-heuristics.t
125 126 test-custom-filters.t
126 127 test-debugbuilddag.t
127 128 test-debugbundle.t
128 129 test-debugcommands.t
129 130 test-debugextensions.t
130 131 test-debugindexdot.t
131 132 test-debugrename.t
132 133 test-default-push.t
133 134 test-diff-antipatience.t
134 135 test-diff-binary-file.t
135 136 test-diff-change.t
136 137 test-diff-color.t
137 138 test-diff-copy-depth.t
138 139 test-diff-hashes.t
139 140 test-diff-ignore-whitespace.t
140 141 test-diff-indent-heuristic.t
141 142 test-diff-issue2761.t
142 143 test-diff-newlines.t
143 144 test-diff-reverse.t
144 145 test-diff-subdir.t
145 146 test-diff-unified.t
146 147 test-diff-upgrade.t
147 148 test-diffdir.t
148 149 test-diffstat.t
149 150 test-directaccess.t
150 151 test-dirstate-backup.t
151 152 test-dirstate-nonnormalset.t
152 153 test-dirstate-race.t
153 154 test-dirstate.t
154 155 test-dispatch.py
155 156 test-doctest.py
156 157 test-double-merge.t
157 158 test-drawdag.t
158 159 test-duplicateoptions.py
159 160 test-editor-filename.t
160 161 test-empty-dir.t
161 162 test-empty-file.t
162 163 test-empty-group.t
163 164 test-empty.t
164 165 test-encode.t
165 166 test-encoding-func.py
166 167 test-encoding-textwrap.t
167 168 test-encoding.t
168 169 test-eol-add.t
169 170 test-eol-clone.t
170 171 test-eol-hook.t
171 172 test-eol-patch.t
172 173 test-eol-tag.t
173 174 test-eol-update.t
174 175 test-eol.t
175 176 test-eolfilename.t
176 177 test-excessive-merge.t
177 178 test-exchange-obsmarkers-case-A1.t
178 179 test-exchange-obsmarkers-case-A2.t
179 180 test-exchange-obsmarkers-case-A3.t
180 181 test-exchange-obsmarkers-case-A4.t
181 182 test-exchange-obsmarkers-case-A5.t
182 183 test-exchange-obsmarkers-case-A6.t
183 184 test-exchange-obsmarkers-case-A7.t
184 185 test-exchange-obsmarkers-case-B1.t
185 186 test-exchange-obsmarkers-case-B2.t
186 187 test-exchange-obsmarkers-case-B3.t
187 188 test-exchange-obsmarkers-case-B4.t
188 189 test-exchange-obsmarkers-case-B5.t
189 190 test-exchange-obsmarkers-case-B6.t
190 191 test-exchange-obsmarkers-case-B7.t
191 192 test-exchange-obsmarkers-case-C1.t
192 193 test-exchange-obsmarkers-case-C2.t
193 194 test-exchange-obsmarkers-case-C3.t
194 195 test-exchange-obsmarkers-case-C4.t
195 196 test-exchange-obsmarkers-case-D1.t
196 197 test-exchange-obsmarkers-case-D2.t
197 198 test-exchange-obsmarkers-case-D3.t
198 199 test-exchange-obsmarkers-case-D4.t
199 200 test-execute-bit.t
200 201 test-export.t
201 202 test-extdata.t
202 203 test-extdiff.t
203 204 test-extension-timing.t
204 205 test-extensions-afterloaded.t
205 206 test-extensions-wrapfunction.py
206 207 test-extra-filelog-entry.t
207 208 test-fastannotate-corrupt.t
208 209 test-fastannotate-diffopts.t
209 210 test-fastannotate-hg.t
210 211 test-fastannotate-perfhack.t
211 212 test-fastannotate-protocol.t
212 213 test-fastannotate-renames.t
213 214 test-fastannotate-revmap.py
214 215 test-fastannotate.t
215 216 test-fetch.t
216 217 test-filebranch.t
217 218 test-filecache.py
218 219 test-filelog.py
219 220 test-fileset-generated.t
220 221 test-fileset.t
221 222 test-fix-topology.t
222 223 test-fix.t
223 224 test-flags.t
224 225 test-fncache.t
225 226 test-gendoc-da.t
226 227 test-gendoc-de.t
227 228 test-gendoc-el.t
228 229 test-gendoc-fr.t
229 230 test-gendoc-it.t
230 231 test-gendoc-ja.t
231 232 test-gendoc-pt_BR.t
232 233 test-gendoc-ro.t
233 234 test-gendoc-ru.t
234 235 test-gendoc-sv.t
235 236 test-gendoc-zh_CN.t
236 237 test-gendoc-zh_TW.t
237 238 test-gendoc.t
238 239 test-generaldelta.t
239 240 test-getbundle.t
240 241 test-git-export.t
241 242 test-globalopts.t
242 243 test-glog-beautifygraph.t
243 244 test-glog-topological.t
244 245 test-glog.t
245 246 test-gpg.t
246 247 test-graft.t
247 248 test-grep.t
248 249 test-hardlinks.t
249 250 test-help-hide.t
250 251 test-help.t
251 252 test-hg-parseurl.py
252 253 test-hghave.t
253 254 test-hgignore.t
254 255 test-hgk.t
255 256 test-hgrc.t
256 257 test-hgweb-annotate-whitespace.t
257 258 test-hgweb-bundle.t
258 259 test-hgweb-commands.t
259 260 test-hgweb-csp.t
260 261 test-hgweb-descend-empties.t
261 262 test-hgweb-diffs.t
262 263 test-hgweb-empty.t
263 264 test-hgweb-filelog.t
264 265 test-hgweb-no-path-info.t
265 266 test-hgweb-no-request-uri.t
266 267 test-hgweb-non-interactive.t
267 268 test-hgweb-raw.t
268 269 test-hgweb-removed.t
269 270 test-hgweb-symrev.t
270 271 test-hgweb.t
271 272 test-hgwebdir-paths.py
272 273 test-hgwebdir.t
273 274 test-hgwebdirsym.t
274 275 test-histedit-arguments.t
275 276 test-histedit-base.t
276 277 test-histedit-bookmark-motion.t
277 278 test-histedit-commute.t
278 279 test-histedit-drop.t
279 280 test-histedit-edit.t
280 281 test-histedit-fold-non-commute.t
281 282 test-histedit-fold.t
282 283 test-histedit-no-backup.t
283 284 test-histedit-no-change.t
284 285 test-histedit-non-commute-abort.t
285 286 test-histedit-non-commute.t
286 287 test-histedit-obsolete.t
287 288 test-histedit-outgoing.t
288 289 test-histedit-templates.t
289 290 test-http-api.t
290 291 test-http-branchmap.t
291 292 test-http-bundle1.t
292 293 test-http-clone-r.t
293 294 test-http-permissions.t
294 295 test-http.t
295 296 test-hybridencode.py
296 297 test-i18n.t
297 298 test-identify.t
298 299 test-impexp-branch.t
299 300 test-import-bypass.t
300 301 test-import-context.t
301 302 test-import-eol.t
302 303 test-import-merge.t
303 304 test-import-unknown.t
304 305 test-import.t
305 306 test-imports-checker.t
306 307 test-incoming-outgoing.t
307 308 test-infinitepush-bundlestore.t
308 309 test-infinitepush-ci.t
309 310 test-infinitepush.t
310 311 test-inherit-mode.t
311 312 test-init.t
312 313 test-install.t
313 314 test-issue1089.t
314 315 test-issue1102.t
315 316 test-issue1175.t
316 317 test-issue1306.t
317 318 test-issue1438.t
318 319 test-issue1502.t
319 320 test-issue1802.t
320 321 test-issue1877.t
321 322 test-issue1993.t
322 323 test-issue2137.t
323 324 test-issue3084.t
324 325 test-issue4074.t
325 326 test-issue522.t
326 327 test-issue586.t
327 328 test-issue5979.t
328 329 test-issue612.t
329 330 test-issue619.t
330 331 test-issue660.t
331 332 test-issue672.t
332 333 test-issue842.t
333 334 test-journal-exists.t
334 335 test-journal-share.t
335 336 test-journal.t
336 337 test-keyword.t
337 338 test-known.t
338 339 test-largefiles-cache.t
339 340 test-largefiles-misc.t
340 341 test-largefiles-small-disk.t
341 342 test-largefiles-update.t
342 343 test-largefiles-wireproto.t
343 344 test-largefiles.t
344 345 test-lfconvert.t
345 346 test-lfs-bundle.t
346 347 test-lfs-largefiles.t
347 348 test-lfs-pointer.py
348 349 test-lfs.t
349 350 test-linelog.py
350 351 test-linerange.py
351 352 test-locate.t
352 353 test-lock-badness.t
353 354 test-log-exthook.t
354 355 test-log-linerange.t
355 356 test-log.t
356 357 test-logexchange.t
357 358 test-logtoprocess.t
358 359 test-lrucachedict.py
359 360 test-mactext.t
360 361 test-mailmap.t
361 362 test-manifest-merging.t
362 363 test-manifest.py
363 364 test-manifest.t
364 365 test-match.py
365 366 test-mdiff.py
366 367 test-merge-changedelete.t
367 368 test-merge-closedheads.t
368 369 test-merge-commit.t
369 370 test-merge-criss-cross.t
370 371 test-merge-default.t
371 372 test-merge-force.t
372 373 test-merge-halt.t
373 374 test-merge-internal-tools-pattern.t
374 375 test-merge-local.t
375 376 test-merge-no-file-change.t
376 377 test-merge-remove.t
377 378 test-merge-revert.t
378 379 test-merge-revert2.t
379 380 test-merge-subrepos.t
380 381 test-merge-symlinks.t
381 382 test-merge-tools.t
382 383 test-merge-types.t
383 384 test-merge1.t
384 385 test-merge10.t
385 386 test-merge2.t
386 387 test-merge4.t
387 388 test-merge5.t
388 389 test-merge6.t
389 390 test-merge7.t
390 391 test-merge8.t
391 392 test-merge9.t
392 393 test-minifileset.py
393 394 test-minirst.py
394 395 test-missing-capability.t
395 396 test-mq-git.t
396 397 test-mq-guards.t
397 398 test-mq-header-date.t
398 399 test-mq-header-from.t
399 400 test-mq-merge.t
400 401 test-mq-missingfiles.t
401 402 test-mq-pull-from-bundle.t
402 403 test-mq-qclone-http.t
403 404 test-mq-qdelete.t
404 405 test-mq-qdiff.t
405 406 test-mq-qfold.t
406 407 test-mq-qgoto.t
407 408 test-mq-qimport-fail-cleanup.t
408 409 test-mq-qimport.t
409 410 test-mq-qnew.t
410 411 test-mq-qpush-exact.t
411 412 test-mq-qpush-fail.t
412 413 test-mq-qqueue.t
413 414 test-mq-qrefresh-interactive.t
414 415 test-mq-qrefresh-replace-log-message.t
415 416 test-mq-qrefresh.t
416 417 test-mq-qrename.t
417 418 test-mq-qsave.t
418 419 test-mq-safety.t
419 420 test-mq-subrepo.t
420 421 test-mq-symlinks.t
421 422 test-mq.t
422 423 test-mv-cp-st-diff.t
423 424 test-narrow-acl.t
424 425 test-narrow-archive.t
425 426 test-narrow-clone-no-ellipsis.t
426 427 test-narrow-clone-non-narrow-server.t
427 428 test-narrow-clone-nonlinear.t
428 429 test-narrow-clone-stream.t
429 430 test-narrow-clone.t
430 431 test-narrow-commit.t
431 432 test-narrow-copies.t
432 433 test-narrow-debugcommands.t
433 434 test-narrow-debugrebuilddirstate.t
434 435 test-narrow-exchange-merges.t
435 436 test-narrow-exchange.t
436 437 test-narrow-expanddirstate.t
437 438 test-narrow-merge.t
438 439 test-narrow-patch.t
439 440 test-narrow-patterns.t
440 441 test-narrow-pull.t
441 442 test-narrow-rebase.t
442 443 test-narrow-shallow-merges.t
443 444 test-narrow-shallow.t
444 445 test-narrow-share.t
445 446 test-narrow-sparse.t
446 447 test-narrow-strip.t
447 448 test-narrow-trackedcmd.t
448 449 test-narrow-update.t
449 450 test-narrow-widen-no-ellipsis.t
450 451 test-narrow-widen.t
451 452 test-narrow.t
452 453 test-nested-repo.t
453 454 test-newbranch.t
454 455 test-newercgi.t
455 456 test-nointerrupt.t
456 457 test-obshistory.t
457 458 test-obsmarker-template.t
458 459 test-obsmarkers-effectflag.t
459 460 test-obsolete-bounds-checking.t
460 461 test-obsolete-bundle-strip.t
461 462 test-obsolete-changeset-exchange.t
462 463 test-obsolete-checkheads.t
463 464 test-obsolete-distributed.t
464 465 test-obsolete-divergent.t
465 466 test-obsolete-tag-cache.t
466 467 test-obsolete.t
467 468 test-origbackup-conflict.t
468 469 test-pager-legacy.t
469 470 test-pager.t
470 471 test-parents.t
471 472 test-parse-date.t
472 473 test-parseindex2.py
473 474 test-patch-offset.t
474 475 test-patch.t
475 476 test-patchbomb-bookmark.t
476 477 test-patchbomb-tls.t
477 478 test-patchbomb.t
478 479 test-pathconflicts-basic.t
479 480 test-pathconflicts-merge.t
480 481 test-pathconflicts-update.t
481 482 test-pathencode.py
482 483 test-pending.t
483 484 test-permissions.t
484 485 test-phases-exchange.t
485 486 test-phases.t
486 487 test-profile.t
487 488 test-progress.t
488 489 test-propertycache.py
489 490 test-pull-branch.t
490 491 test-pull-http.t
491 492 test-pull-permission.t
492 493 test-pull-pull-corruption.t
493 494 test-pull-r.t
494 495 test-pull-update.t
495 496 test-pull.t
496 497 test-purge.t
497 498 test-push-cgi.t
498 499 test-push-checkheads-partial-C1.t
499 500 test-push-checkheads-partial-C2.t
500 501 test-push-checkheads-partial-C3.t
501 502 test-push-checkheads-partial-C4.t
502 503 test-push-checkheads-pruned-B1.t
503 504 test-push-checkheads-pruned-B2.t
504 505 test-push-checkheads-pruned-B3.t
505 506 test-push-checkheads-pruned-B4.t
506 507 test-push-checkheads-pruned-B5.t
507 508 test-push-checkheads-pruned-B6.t
508 509 test-push-checkheads-pruned-B7.t
509 510 test-push-checkheads-pruned-B8.t
510 511 test-push-checkheads-superceed-A1.t
511 512 test-push-checkheads-superceed-A2.t
512 513 test-push-checkheads-superceed-A3.t
513 514 test-push-checkheads-superceed-A4.t
514 515 test-push-checkheads-superceed-A5.t
515 516 test-push-checkheads-superceed-A6.t
516 517 test-push-checkheads-superceed-A7.t
517 518 test-push-checkheads-superceed-A8.t
518 519 test-push-checkheads-unpushed-D1.t
519 520 test-push-checkheads-unpushed-D2.t
520 521 test-push-checkheads-unpushed-D3.t
521 522 test-push-checkheads-unpushed-D4.t
522 523 test-push-checkheads-unpushed-D5.t
523 524 test-push-checkheads-unpushed-D6.t
524 525 test-push-checkheads-unpushed-D7.t
525 526 test-push-http.t
526 527 test-push-race.t
527 528 test-push-warn.t
528 529 test-push.t
529 530 test-pushvars.t
530 531 test-qrecord.t
531 532 test-rebase-abort.t
532 533 test-rebase-backup.t
533 534 test-rebase-base-flag.t
534 535 test-rebase-bookmarks.t
535 536 test-rebase-brute-force.t
536 537 test-rebase-cache.t
537 538 test-rebase-check-restore.t
538 539 test-rebase-collapse.t
539 540 test-rebase-conflicts.t
540 541 test-rebase-dest.t
541 542 test-rebase-detach.t
542 543 test-rebase-emptycommit.t
543 544 test-rebase-inmemory.t
544 545 test-rebase-interruptions.t
545 546 test-rebase-issue-noparam-single-rev.t
546 547 test-rebase-legacy.t
547 548 test-rebase-mq-skip.t
548 549 test-rebase-mq.t
549 550 test-rebase-named-branches.t
550 551 test-rebase-newancestor.t
551 552 test-rebase-obsolete.t
552 553 test-rebase-parameters.t
553 554 test-rebase-partial.t
554 555 test-rebase-pull.t
555 556 test-rebase-rename.t
556 557 test-rebase-scenario-global.t
557 558 test-rebase-templates.t
558 559 test-rebase-transaction.t
559 560 test-rebuildstate.t
560 561 test-record.t
561 562 test-releasenotes-formatting.t
562 563 test-releasenotes-merging.t
563 564 test-releasenotes-parsing.t
564 565 test-relink.t
565 566 test-remotefilelog-bad-configs.t
566 567 test-remotefilelog-bgprefetch.t
567 568 test-remotefilelog-blame.t
568 569 test-remotefilelog-bundle2.t
569 570 test-remotefilelog-bundles.t
570 571 test-remotefilelog-cacheprocess.t
571 572 test-remotefilelog-clone-tree.t
572 573 test-remotefilelog-clone.t
573 574 test-remotefilelog-gcrepack.t
574 575 test-remotefilelog-histpack.py
575 576 test-remotefilelog-http.t
576 577 test-remotefilelog-keepset.t
577 578 test-remotefilelog-local.t
578 579 test-remotefilelog-log.t
579 580 test-remotefilelog-partial-shallow.t
580 581 test-remotefilelog-permissions.t
581 582 test-remotefilelog-permisssions.t
582 583 test-remotefilelog-prefetch.t
583 584 test-remotefilelog-pull-noshallow.t
584 585 test-remotefilelog-share.t
585 586 test-remotefilelog-sparse.t
586 587 test-remotefilelog-tags.t
587 588 test-remotefilelog-wireproto.t
588 589 test-remove.t
589 590 test-removeemptydirs.t
590 591 test-rename-after-merge.t
591 592 test-rename-dir-merge.t
592 593 test-rename-merge1.t
593 594 test-rename-merge2.t
594 595 test-rename.t
595 596 test-repair-strip.t
596 597 test-repo-compengines.t
597 598 test-requires.t
598 599 test-resolve.t
599 600 test-revert-flags.t
600 601 test-revert-interactive.t
601 602 test-revert-unknown.t
602 603 test-revert.t
603 604 test-revisions.t
604 605 test-revlog-ancestry.py
605 606 test-revlog-group-emptyiter.t
606 607 test-revlog-mmapindex.t
607 608 test-revlog-packentry.t
608 609 test-revlog-raw.py
609 610 test-revlog-v2.t
610 611 test-revlog.t
611 612 test-revset-dirstate-parents.t
612 613 test-revset-legacy-lookup.t
613 614 test-revset-outgoing.t
614 615 test-rollback.t
615 616 test-run-tests.py
616 617 test-run-tests.t
617 618 test-rust-ancestor.py
618 619 test-schemes.t
619 620 test-serve.t
620 621 test-setdiscovery.t
621 622 test-share.t
622 623 test-shelve.t
623 624 test-shelve2.t
624 625 test-show-stack.t
625 626 test-show-work.t
626 627 test-show.t
627 628 test-simple-update.t
628 629 test-simplekeyvaluefile.py
629 630 test-simplemerge.py
630 631 test-single-head.t
631 632 test-sparse-clear.t
632 633 test-sparse-clone.t
633 634 test-sparse-import.t
634 635 test-sparse-merges.t
635 636 test-sparse-profiles.t
636 637 test-sparse-requirement.t
637 638 test-sparse-verbose-json.t
638 639 test-sparse.t
639 640 test-split.t
640 641 test-ssh-bundle1.t
641 642 test-ssh-clone-r.t
642 643 test-ssh-proto-unbundle.t
643 644 test-ssh-proto.t
644 645 test-ssh-repoerror.t
645 646 test-ssh.t
646 647 test-sshserver.py
647 648 test-stack.t
648 649 test-status-color.t
649 650 test-status-inprocess.py
650 651 test-status-rev.t
651 652 test-status-terse.t
652 653 test-status.t
653 654 test-storage.py
654 655 test-stream-bundle-v2.t
655 656 test-strict.t
656 657 test-strip-cross.t
657 658 test-strip.t
658 659 test-subrepo-deep-nested-change.t
659 660 test-subrepo-missing.t
660 661 test-subrepo-paths.t
661 662 test-subrepo-recursion.t
662 663 test-subrepo-relative-path.t
663 664 test-subrepo.t
664 665 test-symlink-os-yes-fs-no.py
665 666 test-symlink-placeholder.t
666 667 test-symlinks.t
667 668 test-tag.t
668 669 test-tags.t
669 670 test-template-basic.t
670 671 test-template-functions.t
671 672 test-template-keywords.t
672 673 test-template-map.t
673 674 test-tools.t
674 675 test-transplant.t
675 676 test-treediscovery-legacy.t
676 677 test-treediscovery.t
677 678 test-treemanifest.t
678 679 test-ui-color.py
679 680 test-ui-config.py
680 681 test-ui-verbosity.py
681 682 test-unamend.t
682 683 test-unbundlehash.t
683 684 test-uncommit.t
684 685 test-unified-test.t
685 686 test-unionrepo.t
686 687 test-unrelated-pull.t
687 688 test-up-local-change.t
688 689 test-update-branches.t
689 690 test-update-dest.t
690 691 test-update-issue1456.t
691 692 test-update-names.t
692 693 test-update-reverse.t
693 694 test-upgrade-repo.t
694 695 test-url-download.t
695 696 test-url-rev.t
696 697 test-url.py
697 698 test-username-newline.t
698 699 test-util.py
699 700 test-verify.t
700 701 test-walk.t
701 702 test-walkrepo.py
702 703 test-websub.t
703 704 test-win32text.t
704 705 test-wireproto-clientreactor.py
705 706 test-wireproto-command-branchmap.t
706 707 test-wireproto-command-changesetdata.t
707 708 test-wireproto-command-filedata.t
708 709 test-wireproto-command-filesdata.t
709 710 test-wireproto-command-heads.t
710 711 test-wireproto-command-listkeys.t
711 712 test-wireproto-command-lookup.t
712 713 test-wireproto-command-manifestdata.t
713 714 test-wireproto-command-pushkey.t
714 715 test-wireproto-command-rawstorefiledata.t
715 716 test-wireproto-framing.py
716 717 test-wireproto-serverreactor.py
717 718 test-wireproto.py
718 719 test-wireproto.t
719 720 test-wsgirequest.py
720 721 test-xdg.t
@@ -1,1129 +1,1130
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 error,
304 304 logcmdutil,
305 305 mail,
306 pycompat,
306 307 registrar,
307 308 url,
308 309 util,
309 310 )
310 311 from mercurial.utils import (
311 312 procutil,
312 313 stringutil,
313 314 )
314 315
315 316 xmlrpclib = util.xmlrpclib
316 317
317 318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
318 319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
319 320 # be specifying the version(s) of Mercurial they are tested with, or
320 321 # leave the attribute unspecified.
321 322 testedwith = 'ships-with-hg-core'
322 323
323 324 configtable = {}
324 325 configitem = registrar.configitem(configtable)
325 326
326 327 configitem('bugzilla', 'apikey',
327 328 default='',
328 329 )
329 330 configitem('bugzilla', 'bzdir',
330 331 default='/var/www/html/bugzilla',
331 332 )
332 333 configitem('bugzilla', 'bzemail',
333 334 default=None,
334 335 )
335 336 configitem('bugzilla', 'bzurl',
336 337 default='http://localhost/bugzilla/',
337 338 )
338 339 configitem('bugzilla', 'bzuser',
339 340 default=None,
340 341 )
341 342 configitem('bugzilla', 'db',
342 343 default='bugs',
343 344 )
344 345 configitem('bugzilla', 'fixregexp',
345 default=(r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
346 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
347 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
348 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
346 default=(br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
347 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
348 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
349 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
349 350 )
350 351 configitem('bugzilla', 'fixresolution',
351 352 default='FIXED',
352 353 )
353 354 configitem('bugzilla', 'fixstatus',
354 355 default='RESOLVED',
355 356 )
356 357 configitem('bugzilla', 'host',
357 358 default='localhost',
358 359 )
359 360 configitem('bugzilla', 'notify',
360 361 default=configitem.dynamicdefault,
361 362 )
362 363 configitem('bugzilla', 'password',
363 364 default=None,
364 365 )
365 366 configitem('bugzilla', 'regexp',
366 default=(r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
367 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
368 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
367 default=(br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
368 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
369 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
369 370 )
370 371 configitem('bugzilla', 'strip',
371 372 default=0,
372 373 )
373 374 configitem('bugzilla', 'style',
374 375 default=None,
375 376 )
376 377 configitem('bugzilla', 'template',
377 378 default=None,
378 379 )
379 380 configitem('bugzilla', 'timeout',
380 381 default=5,
381 382 )
382 383 configitem('bugzilla', 'user',
383 384 default='bugs',
384 385 )
385 386 configitem('bugzilla', 'usermap',
386 387 default=None,
387 388 )
388 389 configitem('bugzilla', 'version',
389 390 default=None,
390 391 )
391 392
392 393 class bzaccess(object):
393 394 '''Base class for access to Bugzilla.'''
394 395
395 396 def __init__(self, ui):
396 397 self.ui = ui
397 398 usermap = self.ui.config('bugzilla', 'usermap')
398 399 if usermap:
399 400 self.ui.readconfig(usermap, sections=['usermap'])
400 401
401 402 def map_committer(self, user):
402 403 '''map name of committer to Bugzilla user name.'''
403 404 for committer, bzuser in self.ui.configitems('usermap'):
404 405 if committer.lower() == user.lower():
405 406 return bzuser
406 407 return user
407 408
408 409 # Methods to be implemented by access classes.
409 410 #
410 411 # 'bugs' is a dict keyed on bug id, where values are a dict holding
411 412 # updates to bug state. Recognized dict keys are:
412 413 #
413 414 # 'hours': Value, float containing work hours to be updated.
414 415 # 'fix': If key present, bug is to be marked fixed. Value ignored.
415 416
416 417 def filter_real_bug_ids(self, bugs):
417 418 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
418 419
419 420 def filter_cset_known_bug_ids(self, node, bugs):
420 421 '''remove bug IDs where node occurs in comment text from bugs.'''
421 422
422 423 def updatebug(self, bugid, newstate, text, committer):
423 424 '''update the specified bug. Add comment text and set new states.
424 425
425 426 If possible add the comment as being from the committer of
426 427 the changeset. Otherwise use the default Bugzilla user.
427 428 '''
428 429
429 430 def notify(self, bugs, committer):
430 431 '''Force sending of Bugzilla notification emails.
431 432
432 433 Only required if the access method does not trigger notification
433 434 emails automatically.
434 435 '''
435 436
436 437 # Bugzilla via direct access to MySQL database.
437 438 class bzmysql(bzaccess):
438 439 '''Support for direct MySQL access to Bugzilla.
439 440
440 441 The earliest Bugzilla version this is tested with is version 2.16.
441 442
442 443 If your Bugzilla is version 3.4 or above, you are strongly
443 444 recommended to use the XMLRPC access method instead.
444 445 '''
445 446
446 447 @staticmethod
447 448 def sql_buglist(ids):
448 449 '''return SQL-friendly list of bug ids'''
449 450 return '(' + ','.join(map(str, ids)) + ')'
450 451
451 452 _MySQLdb = None
452 453
453 454 def __init__(self, ui):
454 455 try:
455 456 import MySQLdb as mysql
456 457 bzmysql._MySQLdb = mysql
457 458 except ImportError as err:
458 459 raise error.Abort(_('python mysql support not available: %s') % err)
459 460
460 461 bzaccess.__init__(self, ui)
461 462
462 463 host = self.ui.config('bugzilla', 'host')
463 464 user = self.ui.config('bugzilla', 'user')
464 465 passwd = self.ui.config('bugzilla', 'password')
465 466 db = self.ui.config('bugzilla', 'db')
466 467 timeout = int(self.ui.config('bugzilla', 'timeout'))
467 468 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
468 469 (host, db, user, '*' * len(passwd)))
469 470 self.conn = bzmysql._MySQLdb.connect(host=host,
470 471 user=user, passwd=passwd,
471 472 db=db,
472 473 connect_timeout=timeout)
473 474 self.cursor = self.conn.cursor()
474 475 self.longdesc_id = self.get_longdesc_id()
475 476 self.user_ids = {}
476 477 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
477 478
478 479 def run(self, *args, **kwargs):
479 480 '''run a query.'''
480 481 self.ui.note(_('query: %s %s\n') % (args, kwargs))
481 482 try:
482 483 self.cursor.execute(*args, **kwargs)
483 484 except bzmysql._MySQLdb.MySQLError:
484 485 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
485 486 raise
486 487
487 488 def get_longdesc_id(self):
488 489 '''get identity of longdesc field'''
489 490 self.run('select fieldid from fielddefs where name = "longdesc"')
490 491 ids = self.cursor.fetchall()
491 492 if len(ids) != 1:
492 493 raise error.Abort(_('unknown database schema'))
493 494 return ids[0][0]
494 495
495 496 def filter_real_bug_ids(self, bugs):
496 497 '''filter not-existing bugs from set.'''
497 498 self.run('select bug_id from bugs where bug_id in %s' %
498 499 bzmysql.sql_buglist(bugs.keys()))
499 500 existing = [id for (id,) in self.cursor.fetchall()]
500 501 for id in bugs.keys():
501 502 if id not in existing:
502 503 self.ui.status(_('bug %d does not exist\n') % id)
503 504 del bugs[id]
504 505
505 506 def filter_cset_known_bug_ids(self, node, bugs):
506 507 '''filter bug ids that already refer to this changeset from set.'''
507 508 self.run('''select bug_id from longdescs where
508 509 bug_id in %s and thetext like "%%%s%%"''' %
509 510 (bzmysql.sql_buglist(bugs.keys()), short(node)))
510 511 for (id,) in self.cursor.fetchall():
511 512 self.ui.status(_('bug %d already knows about changeset %s\n') %
512 513 (id, short(node)))
513 514 del bugs[id]
514 515
515 516 def notify(self, bugs, committer):
516 517 '''tell bugzilla to send mail.'''
517 518 self.ui.status(_('telling bugzilla to send mail:\n'))
518 519 (user, userid) = self.get_bugzilla_user(committer)
519 520 for id in bugs.keys():
520 521 self.ui.status(_(' bug %s\n') % id)
521 522 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
522 523 bzdir = self.ui.config('bugzilla', 'bzdir')
523 524 try:
524 525 # Backwards-compatible with old notify string, which
525 526 # took one string. This will throw with a new format
526 527 # string.
527 528 cmd = cmdfmt % id
528 529 except TypeError:
529 530 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
530 531 self.ui.note(_('running notify command %s\n') % cmd)
531 532 fp = procutil.popen('(%s) 2>&1' % cmd, 'rb')
532 533 out = util.fromnativeeol(fp.read())
533 534 ret = fp.close()
534 535 if ret:
535 536 self.ui.warn(out)
536 537 raise error.Abort(_('bugzilla notify command %s') %
537 538 procutil.explainexit(ret))
538 539 self.ui.status(_('done\n'))
539 540
540 541 def get_user_id(self, user):
541 542 '''look up numeric bugzilla user id.'''
542 543 try:
543 544 return self.user_ids[user]
544 545 except KeyError:
545 546 try:
546 547 userid = int(user)
547 548 except ValueError:
548 549 self.ui.note(_('looking up user %s\n') % user)
549 550 self.run('''select userid from profiles
550 551 where login_name like %s''', user)
551 552 all = self.cursor.fetchall()
552 553 if len(all) != 1:
553 554 raise KeyError(user)
554 555 userid = int(all[0][0])
555 556 self.user_ids[user] = userid
556 557 return userid
557 558
558 559 def get_bugzilla_user(self, committer):
559 560 '''See if committer is a registered bugzilla user. Return
560 561 bugzilla username and userid if so. If not, return default
561 562 bugzilla username and userid.'''
562 563 user = self.map_committer(committer)
563 564 try:
564 565 userid = self.get_user_id(user)
565 566 except KeyError:
566 567 try:
567 568 defaultuser = self.ui.config('bugzilla', 'bzuser')
568 569 if not defaultuser:
569 570 raise error.Abort(_('cannot find bugzilla user id for %s') %
570 571 user)
571 572 userid = self.get_user_id(defaultuser)
572 573 user = defaultuser
573 574 except KeyError:
574 575 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
575 576 % (user, defaultuser))
576 577 return (user, userid)
577 578
578 579 def updatebug(self, bugid, newstate, text, committer):
579 580 '''update bug state with comment text.
580 581
581 582 Try adding comment as committer of changeset, otherwise as
582 583 default bugzilla user.'''
583 584 if len(newstate) > 0:
584 585 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
585 586
586 587 (user, userid) = self.get_bugzilla_user(committer)
587 588 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
588 589 self.run('''insert into longdescs
589 590 (bug_id, who, bug_when, thetext)
590 591 values (%s, %s, %s, %s)''',
591 592 (bugid, userid, now, text))
592 593 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
593 594 values (%s, %s, %s, %s)''',
594 595 (bugid, userid, now, self.longdesc_id))
595 596 self.conn.commit()
596 597
597 598 class bzmysql_2_18(bzmysql):
598 599 '''support for bugzilla 2.18 series.'''
599 600
600 601 def __init__(self, ui):
601 602 bzmysql.__init__(self, ui)
602 603 self.default_notify = \
603 604 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
604 605
605 606 class bzmysql_3_0(bzmysql_2_18):
606 607 '''support for bugzilla 3.0 series.'''
607 608
608 609 def __init__(self, ui):
609 610 bzmysql_2_18.__init__(self, ui)
610 611
611 612 def get_longdesc_id(self):
612 613 '''get identity of longdesc field'''
613 614 self.run('select id from fielddefs where name = "longdesc"')
614 615 ids = self.cursor.fetchall()
615 616 if len(ids) != 1:
616 617 raise error.Abort(_('unknown database schema'))
617 618 return ids[0][0]
618 619
619 620 # Bugzilla via XMLRPC interface.
620 621
621 622 class cookietransportrequest(object):
622 623 """A Transport request method that retains cookies over its lifetime.
623 624
624 625 The regular xmlrpclib transports ignore cookies. Which causes
625 626 a bit of a problem when you need a cookie-based login, as with
626 627 the Bugzilla XMLRPC interface prior to 4.4.3.
627 628
628 629 So this is a helper for defining a Transport which looks for
629 630 cookies being set in responses and saves them to add to all future
630 631 requests.
631 632 """
632 633
633 634 # Inspiration drawn from
634 635 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
635 636 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
636 637
637 638 cookies = []
638 639 def send_cookies(self, connection):
639 640 if self.cookies:
640 641 for cookie in self.cookies:
641 642 connection.putheader("Cookie", cookie)
642 643
643 644 def request(self, host, handler, request_body, verbose=0):
644 645 self.verbose = verbose
645 646 self.accept_gzip_encoding = False
646 647
647 648 # issue XML-RPC request
648 649 h = self.make_connection(host)
649 650 if verbose:
650 651 h.set_debuglevel(1)
651 652
652 653 self.send_request(h, handler, request_body)
653 654 self.send_host(h, host)
654 655 self.send_cookies(h)
655 656 self.send_user_agent(h)
656 657 self.send_content(h, request_body)
657 658
658 659 # Deal with differences between Python 2.6 and 2.7.
659 660 # In the former h is a HTTP(S). In the latter it's a
660 661 # HTTP(S)Connection. Luckily, the 2.6 implementation of
661 662 # HTTP(S) has an underlying HTTP(S)Connection, so extract
662 663 # that and use it.
663 664 try:
664 665 response = h.getresponse()
665 666 except AttributeError:
666 667 response = h._conn.getresponse()
667 668
668 669 # Add any cookie definitions to our list.
669 670 for header in response.msg.getallmatchingheaders("Set-Cookie"):
670 671 val = header.split(": ", 1)[1]
671 672 cookie = val.split(";", 1)[0]
672 673 self.cookies.append(cookie)
673 674
674 675 if response.status != 200:
675 676 raise xmlrpclib.ProtocolError(host + handler, response.status,
676 677 response.reason, response.msg.headers)
677 678
678 679 payload = response.read()
679 680 parser, unmarshaller = self.getparser()
680 681 parser.feed(payload)
681 682 parser.close()
682 683
683 684 return unmarshaller.close()
684 685
685 686 # The explicit calls to the underlying xmlrpclib __init__() methods are
686 687 # necessary. The xmlrpclib.Transport classes are old-style classes, and
687 688 # it turns out their __init__() doesn't get called when doing multiple
688 689 # inheritance with a new-style class.
689 690 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
690 691 def __init__(self, use_datetime=0):
691 692 if util.safehasattr(xmlrpclib.Transport, "__init__"):
692 693 xmlrpclib.Transport.__init__(self, use_datetime)
693 694
694 695 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
695 696 def __init__(self, use_datetime=0):
696 697 if util.safehasattr(xmlrpclib.Transport, "__init__"):
697 698 xmlrpclib.SafeTransport.__init__(self, use_datetime)
698 699
699 700 class bzxmlrpc(bzaccess):
700 701 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
701 702
702 703 Requires a minimum Bugzilla version 3.4.
703 704 """
704 705
705 706 def __init__(self, ui):
706 707 bzaccess.__init__(self, ui)
707 708
708 709 bzweb = self.ui.config('bugzilla', 'bzurl')
709 710 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
710 711
711 712 user = self.ui.config('bugzilla', 'user')
712 713 passwd = self.ui.config('bugzilla', 'password')
713 714
714 715 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
715 716 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
716 717
717 718 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
718 719 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
719 720 self.bzvermajor = int(ver[0])
720 721 self.bzverminor = int(ver[1])
721 722 login = self.bzproxy.User.login({'login': user, 'password': passwd,
722 723 'restrict_login': True})
723 724 self.bztoken = login.get('token', '')
724 725
725 726 def transport(self, uri):
726 727 if util.urlreq.urlparse(uri, "http")[0] == "https":
727 728 return cookiesafetransport()
728 729 else:
729 730 return cookietransport()
730 731
731 732 def get_bug_comments(self, id):
732 733 """Return a string with all comment text for a bug."""
733 734 c = self.bzproxy.Bug.comments({'ids': [id],
734 735 'include_fields': ['text'],
735 736 'token': self.bztoken})
736 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
737 return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']])
737 738
738 739 def filter_real_bug_ids(self, bugs):
739 740 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
740 741 'include_fields': [],
741 742 'permissive': True,
742 743 'token': self.bztoken,
743 744 })
744 745 for badbug in probe['faults']:
745 746 id = badbug['id']
746 747 self.ui.status(_('bug %d does not exist\n') % id)
747 748 del bugs[id]
748 749
749 750 def filter_cset_known_bug_ids(self, node, bugs):
750 751 for id in sorted(bugs.keys()):
751 752 if self.get_bug_comments(id).find(short(node)) != -1:
752 753 self.ui.status(_('bug %d already knows about changeset %s\n') %
753 754 (id, short(node)))
754 755 del bugs[id]
755 756
756 757 def updatebug(self, bugid, newstate, text, committer):
757 758 args = {}
758 759 if 'hours' in newstate:
759 760 args['work_time'] = newstate['hours']
760 761
761 762 if self.bzvermajor >= 4:
762 763 args['ids'] = [bugid]
763 764 args['comment'] = {'body' : text}
764 765 if 'fix' in newstate:
765 766 args['status'] = self.fixstatus
766 767 args['resolution'] = self.fixresolution
767 768 args['token'] = self.bztoken
768 769 self.bzproxy.Bug.update(args)
769 770 else:
770 771 if 'fix' in newstate:
771 772 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
772 773 "to mark bugs fixed\n"))
773 774 args['id'] = bugid
774 775 args['comment'] = text
775 776 self.bzproxy.Bug.add_comment(args)
776 777
777 778 class bzxmlrpcemail(bzxmlrpc):
778 779 """Read data from Bugzilla via XMLRPC, send updates via email.
779 780
780 781 Advantages of sending updates via email:
781 782 1. Comments can be added as any user, not just logged in user.
782 783 2. Bug statuses or other fields not accessible via XMLRPC can
783 784 potentially be updated.
784 785
785 786 There is no XMLRPC function to change bug status before Bugzilla
786 787 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
787 788 But bugs can be marked fixed via email from 3.4 onwards.
788 789 """
789 790
790 791 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
791 792 # in-email fields are specified as '@<fieldname> = <value>'. In
792 793 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
793 794 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
794 795 # compatibility, but rather than rely on this use the new format for
795 796 # 4.0 onwards.
796 797
797 798 def __init__(self, ui):
798 799 bzxmlrpc.__init__(self, ui)
799 800
800 801 self.bzemail = self.ui.config('bugzilla', 'bzemail')
801 802 if not self.bzemail:
802 803 raise error.Abort(_("configuration 'bzemail' missing"))
803 804 mail.validateconfig(self.ui)
804 805
805 806 def makecommandline(self, fieldname, value):
806 807 if self.bzvermajor >= 4:
807 return "@%s %s" % (fieldname, str(value))
808 return "@%s %s" % (fieldname, pycompat.bytestr(value))
808 809 else:
809 810 if fieldname == "id":
810 811 fieldname = "bug_id"
811 return "@%s = %s" % (fieldname, str(value))
812 return "@%s = %s" % (fieldname, pycompat.bytestr(value))
812 813
813 814 def send_bug_modify_email(self, bugid, commands, comment, committer):
814 815 '''send modification message to Bugzilla bug via email.
815 816
816 817 The message format is documented in the Bugzilla email_in.pl
817 818 specification. commands is a list of command lines, comment is the
818 819 comment text.
819 820
820 821 To stop users from crafting commit comments with
821 822 Bugzilla commands, specify the bug ID via the message body, rather
822 823 than the subject line, and leave a blank line after it.
823 824 '''
824 825 user = self.map_committer(committer)
825 826 matches = self.bzproxy.User.get({'match': [user],
826 827 'token': self.bztoken})
827 828 if not matches['users']:
828 829 user = self.ui.config('bugzilla', 'user')
829 830 matches = self.bzproxy.User.get({'match': [user],
830 831 'token': self.bztoken})
831 832 if not matches['users']:
832 833 raise error.Abort(_("default bugzilla user %s email not found")
833 834 % user)
834 835 user = matches['users'][0]['email']
835 836 commands.append(self.makecommandline("id", bugid))
836 837
837 838 text = "\n".join(commands) + "\n\n" + comment
838 839
839 840 _charsets = mail._charsets(self.ui)
840 841 user = mail.addressencode(self.ui, user, _charsets)
841 842 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
842 843 msg = mail.mimeencode(self.ui, text, _charsets)
843 844 msg['From'] = user
844 845 msg['To'] = bzemail
845 846 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
846 847 sendmail = mail.connect(self.ui)
847 848 sendmail(user, bzemail, msg.as_string())
848 849
849 850 def updatebug(self, bugid, newstate, text, committer):
850 851 cmds = []
851 852 if 'hours' in newstate:
852 853 cmds.append(self.makecommandline("work_time", newstate['hours']))
853 854 if 'fix' in newstate:
854 855 cmds.append(self.makecommandline("bug_status", self.fixstatus))
855 856 cmds.append(self.makecommandline("resolution", self.fixresolution))
856 857 self.send_bug_modify_email(bugid, cmds, text, committer)
857 858
858 859 class NotFound(LookupError):
859 860 pass
860 861
861 862 class bzrestapi(bzaccess):
862 863 """Read and write bugzilla data using the REST API available since
863 864 Bugzilla 5.0.
864 865 """
865 866 def __init__(self, ui):
866 867 bzaccess.__init__(self, ui)
867 868 bz = self.ui.config('bugzilla', 'bzurl')
868 869 self.bzroot = '/'.join([bz, 'rest'])
869 870 self.apikey = self.ui.config('bugzilla', 'apikey')
870 871 self.user = self.ui.config('bugzilla', 'user')
871 872 self.passwd = self.ui.config('bugzilla', 'password')
872 873 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
873 874 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
874 875
875 876 def apiurl(self, targets, include_fields=None):
876 url = '/'.join([self.bzroot] + [str(t) for t in targets])
877 url = '/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
877 878 qv = {}
878 879 if self.apikey:
879 880 qv['api_key'] = self.apikey
880 881 elif self.user and self.passwd:
881 882 qv['login'] = self.user
882 883 qv['password'] = self.passwd
883 884 if include_fields:
884 885 qv['include_fields'] = include_fields
885 886 if qv:
886 887 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
887 888 return url
888 889
889 890 def _fetch(self, burl):
890 891 try:
891 892 resp = url.open(self.ui, burl)
892 893 return json.loads(resp.read())
893 894 except util.urlerr.httperror as inst:
894 895 if inst.code == 401:
895 896 raise error.Abort(_('authorization failed'))
896 897 if inst.code == 404:
897 898 raise NotFound()
898 899 else:
899 900 raise
900 901
901 902 def _submit(self, burl, data, method='POST'):
902 903 data = json.dumps(data)
903 904 if method == 'PUT':
904 905 class putrequest(util.urlreq.request):
905 906 def get_method(self):
906 907 return 'PUT'
907 908 request_type = putrequest
908 909 else:
909 910 request_type = util.urlreq.request
910 911 req = request_type(burl, data,
911 912 {'Content-Type': 'application/json'})
912 913 try:
913 914 resp = url.opener(self.ui).open(req)
914 915 return json.loads(resp.read())
915 916 except util.urlerr.httperror as inst:
916 917 if inst.code == 401:
917 918 raise error.Abort(_('authorization failed'))
918 919 if inst.code == 404:
919 920 raise NotFound()
920 921 else:
921 922 raise
922 923
923 924 def filter_real_bug_ids(self, bugs):
924 925 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
925 926 badbugs = set()
926 927 for bugid in bugs:
927 928 burl = self.apiurl(('bug', bugid), include_fields='status')
928 929 try:
929 930 self._fetch(burl)
930 931 except NotFound:
931 932 badbugs.add(bugid)
932 933 for bugid in badbugs:
933 934 del bugs[bugid]
934 935
935 936 def filter_cset_known_bug_ids(self, node, bugs):
936 937 '''remove bug IDs where node occurs in comment text from bugs.'''
937 938 sn = short(node)
938 939 for bugid in bugs.keys():
939 940 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
940 941 result = self._fetch(burl)
941 comments = result['bugs'][str(bugid)]['comments']
942 comments = result['bugs'][pycompat.bytestr(bugid)]['comments']
942 943 if any(sn in c['text'] for c in comments):
943 944 self.ui.status(_('bug %d already knows about changeset %s\n') %
944 945 (bugid, sn))
945 946 del bugs[bugid]
946 947
947 948 def updatebug(self, bugid, newstate, text, committer):
948 949 '''update the specified bug. Add comment text and set new states.
949 950
950 951 If possible add the comment as being from the committer of
951 952 the changeset. Otherwise use the default Bugzilla user.
952 953 '''
953 954 bugmod = {}
954 955 if 'hours' in newstate:
955 956 bugmod['work_time'] = newstate['hours']
956 957 if 'fix' in newstate:
957 958 bugmod['status'] = self.fixstatus
958 959 bugmod['resolution'] = self.fixresolution
959 960 if bugmod:
960 961 # if we have to change the bugs state do it here
961 962 bugmod['comment'] = {
962 963 'comment': text,
963 964 'is_private': False,
964 965 'is_markdown': False,
965 966 }
966 967 burl = self.apiurl(('bug', bugid))
967 968 self._submit(burl, bugmod, method='PUT')
968 969 self.ui.debug('updated bug %s\n' % bugid)
969 970 else:
970 971 burl = self.apiurl(('bug', bugid, 'comment'))
971 972 self._submit(burl, {
972 973 'comment': text,
973 974 'is_private': False,
974 975 'is_markdown': False,
975 976 })
976 977 self.ui.debug('added comment to bug %s\n' % bugid)
977 978
978 979 def notify(self, bugs, committer):
979 980 '''Force sending of Bugzilla notification emails.
980 981
981 982 Only required if the access method does not trigger notification
982 983 emails automatically.
983 984 '''
984 985 pass
985 986
986 987 class bugzilla(object):
987 988 # supported versions of bugzilla. different versions have
988 989 # different schemas.
989 990 _versions = {
990 991 '2.16': bzmysql,
991 992 '2.18': bzmysql_2_18,
992 993 '3.0': bzmysql_3_0,
993 994 'xmlrpc': bzxmlrpc,
994 995 'xmlrpc+email': bzxmlrpcemail,
995 996 'restapi': bzrestapi,
996 997 }
997 998
998 999 def __init__(self, ui, repo):
999 1000 self.ui = ui
1000 1001 self.repo = repo
1001 1002
1002 1003 bzversion = self.ui.config('bugzilla', 'version')
1003 1004 try:
1004 1005 bzclass = bugzilla._versions[bzversion]
1005 1006 except KeyError:
1006 1007 raise error.Abort(_('bugzilla version %s not supported') %
1007 1008 bzversion)
1008 1009 self.bzdriver = bzclass(self.ui)
1009 1010
1010 1011 self.bug_re = re.compile(
1011 1012 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1012 1013 self.fix_re = re.compile(
1013 1014 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1014 self.split_re = re.compile(r'\D+')
1015 self.split_re = re.compile(br'\D+')
1015 1016
1016 1017 def find_bugs(self, ctx):
1017 1018 '''return bugs dictionary created from commit comment.
1018 1019
1019 1020 Extract bug info from changeset comments. Filter out any that are
1020 1021 not known to Bugzilla, and any that already have a reference to
1021 1022 the given changeset in their comments.
1022 1023 '''
1023 1024 start = 0
1024 1025 hours = 0.0
1025 1026 bugs = {}
1026 1027 bugmatch = self.bug_re.search(ctx.description(), start)
1027 1028 fixmatch = self.fix_re.search(ctx.description(), start)
1028 1029 while True:
1029 1030 bugattribs = {}
1030 1031 if not bugmatch and not fixmatch:
1031 1032 break
1032 1033 if not bugmatch:
1033 1034 m = fixmatch
1034 1035 elif not fixmatch:
1035 1036 m = bugmatch
1036 1037 else:
1037 1038 if bugmatch.start() < fixmatch.start():
1038 1039 m = bugmatch
1039 1040 else:
1040 1041 m = fixmatch
1041 1042 start = m.end()
1042 1043 if m is bugmatch:
1043 1044 bugmatch = self.bug_re.search(ctx.description(), start)
1044 1045 if 'fix' in bugattribs:
1045 1046 del bugattribs['fix']
1046 1047 else:
1047 1048 fixmatch = self.fix_re.search(ctx.description(), start)
1048 1049 bugattribs['fix'] = None
1049 1050
1050 1051 try:
1051 1052 ids = m.group('ids')
1052 1053 except IndexError:
1053 1054 ids = m.group(1)
1054 1055 try:
1055 1056 hours = float(m.group('hours'))
1056 1057 bugattribs['hours'] = hours
1057 1058 except IndexError:
1058 1059 pass
1059 1060 except TypeError:
1060 1061 pass
1061 1062 except ValueError:
1062 1063 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1063 1064
1064 1065 for id in self.split_re.split(ids):
1065 1066 if not id:
1066 1067 continue
1067 1068 bugs[int(id)] = bugattribs
1068 1069 if bugs:
1069 1070 self.bzdriver.filter_real_bug_ids(bugs)
1070 1071 if bugs:
1071 1072 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1072 1073 return bugs
1073 1074
1074 1075 def update(self, bugid, newstate, ctx):
1075 1076 '''update bugzilla bug with reference to changeset.'''
1076 1077
1077 1078 def webroot(root):
1078 1079 '''strip leading prefix of repo root and turn into
1079 1080 url-safe path.'''
1080 1081 count = int(self.ui.config('bugzilla', 'strip'))
1081 1082 root = util.pconvert(root)
1082 1083 while count > 0:
1083 1084 c = root.find('/')
1084 1085 if c == -1:
1085 1086 break
1086 1087 root = root[c + 1:]
1087 1088 count -= 1
1088 1089 return root
1089 1090
1090 1091 mapfile = None
1091 1092 tmpl = self.ui.config('bugzilla', 'template')
1092 1093 if not tmpl:
1093 1094 mapfile = self.ui.config('bugzilla', 'style')
1094 1095 if not mapfile and not tmpl:
1095 1096 tmpl = _('changeset {node|short} in repo {root} refers '
1096 1097 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1097 1098 spec = logcmdutil.templatespec(tmpl, mapfile)
1098 1099 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1099 1100 self.ui.pushbuffer()
1100 1101 t.show(ctx, changes=ctx.changeset(),
1101 bug=str(bugid),
1102 bug=pycompat.bytestr(bugid),
1102 1103 hgweb=self.ui.config('web', 'baseurl'),
1103 1104 root=self.repo.root,
1104 1105 webroot=webroot(self.repo.root))
1105 1106 data = self.ui.popbuffer()
1106 1107 self.bzdriver.updatebug(bugid, newstate, data,
1107 1108 stringutil.email(ctx.user()))
1108 1109
1109 1110 def notify(self, bugs, committer):
1110 1111 '''ensure Bugzilla users are notified of bug change.'''
1111 1112 self.bzdriver.notify(bugs, committer)
1112 1113
1113 1114 def hook(ui, repo, hooktype, node=None, **kwargs):
1114 1115 '''add comment to bugzilla for each changeset that refers to a
1115 1116 bugzilla bug id. only add a comment once per bug, so same change
1116 1117 seen multiple times does not fill bug with duplicate data.'''
1117 1118 if node is None:
1118 1119 raise error.Abort(_('hook type %s does not pass a changeset id') %
1119 1120 hooktype)
1120 1121 try:
1121 1122 bz = bugzilla(ui, repo)
1122 1123 ctx = repo[node]
1123 1124 bugs = bz.find_bugs(ctx)
1124 1125 if bugs:
1125 1126 for bug in bugs:
1126 1127 bz.update(bug, bugs[bug], ctx)
1127 1128 bz.notify(bugs, stringutil.email(ctx.user()))
1128 1129 except Exception as e:
1129 1130 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,104 +1,109
1 1 mock bugzilla driver for testing template output:
2 2
3 3 $ cat <<EOF > bzmock.py
4 4 > from __future__ import absolute_import
5 5 > from mercurial import extensions
6 > from mercurial import pycompat
6 7 > from mercurial import registrar
8 > from mercurial.utils import stringutil
7 9 >
8 10 > configtable = {}
9 11 > configitem = registrar.configitem(configtable)
10 12 >
11 13 > configitem(b'bugzilla', b'mocklog',
12 14 > default=None,
13 15 > )
14 16 > def extsetup(ui):
15 17 > bugzilla = extensions.find(b'bugzilla')
16 18 > class bzmock(bugzilla.bzaccess):
17 19 > def __init__(self, ui):
18 20 > super(bzmock, self).__init__(ui)
19 21 > self._logfile = ui.config(b'bugzilla', b'mocklog')
20 22 > def updatebug(self, bugid, newstate, text, committer):
21 > with open(self._logfile, 'a') as f:
22 > f.write('update bugid=%r, newstate=%r, committer=%r\n'
23 > % (bugid, newstate, committer))
24 > f.write('----\n' + text + '\n----\n')
23 > with open(pycompat.fsdecode(self._logfile), 'ab') as f:
24 > f.write(b'update bugid=%s, newstate=%s, committer=%s\n'
25 > % (stringutil.pprint(bugid),
26 > stringutil.pprint(newstate),
27 > stringutil.pprint(committer)))
28 > f.write(b'----\n' + text + b'\n----\n')
25 29 > def notify(self, bugs, committer):
26 > with open(self._logfile, 'a') as f:
27 > f.write('notify bugs=%r, committer=%r\n'
28 > % (bugs, committer))
30 > with open(pycompat.fsdecode(self._logfile), 'ab') as f:
31 > f.write(b'notify bugs=%s, committer=%s\n'
32 > % (stringutil.pprint(bugs),
33 > stringutil.pprint(committer)))
29 34 > bugzilla.bugzilla._versions[b'mock'] = bzmock
30 35 > EOF
31 36
32 37 set up mock repository:
33 38
34 39 $ hg init mockremote
35 40 $ cat <<EOF > mockremote/.hg/hgrc
36 41 > [extensions]
37 42 > bugzilla =
38 43 > bzmock = $TESTTMP/bzmock.py
39 44 >
40 45 > [bugzilla]
41 46 > version = mock
42 47 > mocklog = $TESTTMP/bzmock.log
43 48 >
44 49 > [hooks]
45 50 > incoming.bugzilla = python:hgext.bugzilla.hook
46 51 >
47 52 > [web]
48 53 > baseurl=http://example.org/hg
49 54 >
50 55 > %include $TESTTMP/bzstyle.hgrc
51 56 > EOF
52 57
53 58 $ hg clone -q mockremote mocklocal
54 59
55 60 push with default template:
56 61
57 62 $ echo '[bugzilla]' > bzstyle.hgrc
58 63 $ echo foo > mocklocal/foo
59 64 $ hg ci -R mocklocal -Aqm 'Fixes bug 123'
60 65 $ hg -R mocklocal push -q
61 66 $ cat bzmock.log && rm bzmock.log
62 67 update bugid=123, newstate={}, committer='test'
63 68 ----
64 69 changeset 7875a8342c6f in repo $TESTTMP/mockremote refers to bug 123.
65 70 details:
66 71 Fixes bug 123
67 72 ----
68 73 notify bugs={123: {}}, committer='test'
69 74
70 75 push with style:
71 76
72 77 $ cat <<EOF > bzstyle.map
73 78 > changeset = "{node|short} refers to bug {bug}."
74 79 > EOF
75 80 $ echo "style = $TESTTMP/bzstyle.map" >> bzstyle.hgrc
76 81 $ echo foo >> mocklocal/foo
77 82 $ hg ci -R mocklocal -qm 'Fixes bug 456'
78 83 $ hg -R mocklocal push -q
79 84 $ cat bzmock.log && rm bzmock.log
80 85 update bugid=456, newstate={}, committer='test'
81 86 ----
82 87 2808b172464b refers to bug 456.
83 88 ----
84 89 notify bugs={456: {}}, committer='test'
85 90
86 91 push with template (overrides style):
87 92
88 93 $ cat <<EOF >> bzstyle.hgrc
89 94 > template = Changeset {node|short} in {root|basename}.
90 95 > {hgweb}/rev/{node|short}\n
91 96 > {desc}
92 97 > EOF
93 98 $ echo foo >> mocklocal/foo
94 99 $ hg ci -R mocklocal -qm 'Fixes bug 789'
95 100 $ hg -R mocklocal push -q
96 101 $ cat bzmock.log && rm bzmock.log
97 102 update bugid=789, newstate={}, committer='test'
98 103 ----
99 104 Changeset a770f3e409f2 in mockremote.
100 105 http://example.org/hg/rev/a770f3e409f2
101 106
102 107 Fixes bug 789
103 108 ----
104 109 notify bugs={789: {}}, committer='test'
General Comments 0
You need to be logged in to leave comments. Login now