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