##// END OF EJS Templates
merge with stable
Augie Fackler -
r32652:9929af2b merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,53 +1,54 b''
1 Source: mercurial
1 Source: mercurial
2 Section: vcs
2 Section: vcs
3 Priority: optional
3 Priority: optional
4 Maintainer: Mercurial Developers <mercurial-devel@mercurial-scm.org>
4 Maintainer: Mercurial Developers <mercurial-devel@mercurial-scm.org>
5 Build-Depends:
5 Build-Depends:
6 debhelper (>= 9),
6 debhelper (>= 9),
7 dh-python,
7 dh-python,
8 less,
8 netbase,
9 netbase,
9 python-all,
10 python-all,
10 python-all-dev,
11 python-all-dev,
11 python-docutils,
12 python-docutils,
12 unzip,
13 unzip,
13 zip
14 zip
14 Standards-Version: 3.9.4
15 Standards-Version: 3.9.4
15 X-Python-Version: >= 2.7
16 X-Python-Version: >= 2.7
16
17
17 Package: mercurial
18 Package: mercurial
18 Depends:
19 Depends:
19 python,
20 python,
20 ${shlibs:Depends},
21 ${shlibs:Depends},
21 ${misc:Depends},
22 ${misc:Depends},
22 ${python:Depends},
23 ${python:Depends},
23 mercurial-common (= ${source:Version})
24 mercurial-common (= ${source:Version})
24 Architecture: any
25 Architecture: any
25 Description: fast, easy to use, distributed revision control tool.
26 Description: fast, easy to use, distributed revision control tool.
26 Mercurial is a fast, lightweight Source Control Management system designed
27 Mercurial is a fast, lightweight Source Control Management system designed
27 for efficient handling of very large distributed projects.
28 for efficient handling of very large distributed projects.
28 .
29 .
29 Its features include:
30 Its features include:
30 * O(1) delta-compressed file storage and retrieval scheme
31 * O(1) delta-compressed file storage and retrieval scheme
31 * Complete cross-indexing of files and changesets for efficient exploration
32 * Complete cross-indexing of files and changesets for efficient exploration
32 of project history
33 of project history
33 * Robust SHA1-based integrity checking and append-only storage model
34 * Robust SHA1-based integrity checking and append-only storage model
34 * Decentralized development model with arbitrary merging between trees
35 * Decentralized development model with arbitrary merging between trees
35 * Easy-to-use command-line interface
36 * Easy-to-use command-line interface
36 * Integrated stand-alone web interface
37 * Integrated stand-alone web interface
37 * Small Python codebase
38 * Small Python codebase
38
39
39 Package: mercurial-common
40 Package: mercurial-common
40 Architecture: all
41 Architecture: all
41 Depends:
42 Depends:
42 ${misc:Depends},
43 ${misc:Depends},
43 ${python:Depends},
44 ${python:Depends},
44 Recommends: mercurial (= ${source:Version}), ca-certificates
45 Recommends: mercurial (= ${source:Version}), ca-certificates
45 Suggests: wish
46 Suggests: wish
46 Breaks: mercurial (<< ${source:Version})
47 Breaks: mercurial (<< ${source:Version})
47 Replaces: mercurial (<< 2.6.3)
48 Replaces: mercurial (<< 2.6.3)
48 Description: easy-to-use, scalable distributed version control system (common files)
49 Description: easy-to-use, scalable distributed version control system (common files)
49 Mercurial is a fast, lightweight Source Control Management system designed
50 Mercurial is a fast, lightweight Source Control Management system designed
50 for efficient handling of very large distributed projects.
51 for efficient handling of very large distributed projects.
51 .
52 .
52 This package contains the architecture independent components of Mercurial,
53 This package contains the architecture independent components of Mercurial,
53 and is generally useless without the mercurial package.
54 and is generally useless without the mercurial package.
@@ -1,35 +1,36 b''
1 <?xml version="1.0" encoding="utf-8"?>
1 <?xml version="1.0" encoding="utf-8"?>
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3
3
4 <?include guids.wxi ?>
4 <?include guids.wxi ?>
5 <?include defines.wxi ?>
5 <?include defines.wxi ?>
6
6
7 <Fragment>
7 <Fragment>
8 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
8 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
9 <Component Id="distOutput" Guid="$(var.dist.guid)" Win64='$(var.IsX64)'>
9 <Component Id="distOutput" Guid="$(var.dist.guid)" Win64='$(var.IsX64)'>
10 <File Name="python27.dll" KeyPath="yes" />
10 <File Name="python27.dll" KeyPath="yes" />
11 </Component>
11 </Component>
12 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
12 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
13 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
13 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
14 <File Name="library.zip" KeyPath="yes" />
14 <File Name="library.zip" KeyPath="yes" />
15 <File Name="mercurial.cext.base85.pyd" />
15 <File Name="mercurial.cext.base85.pyd" />
16 <File Name="mercurial.cext.bdiff.pyd" />
16 <File Name="mercurial.cext.bdiff.pyd" />
17 <File Name="mercurial.cext.diffhelpers.pyd" />
17 <File Name="mercurial.cext.diffhelpers.pyd" />
18 <File Name="mercurial.cext.mpatch.pyd" />
18 <File Name="mercurial.cext.mpatch.pyd" />
19 <File Name="mercurial.cext.osutil.pyd" />
19 <File Name="mercurial.cext.osutil.pyd" />
20 <File Name="mercurial.cext.parsers.pyd" />
20 <File Name="mercurial.cext.parsers.pyd" />
21 <File Name="mercurial.zstd.pyd" />
21 <File Name="pyexpat.pyd" />
22 <File Name="pyexpat.pyd" />
22 <File Name="bz2.pyd" />
23 <File Name="bz2.pyd" />
23 <File Name="select.pyd" />
24 <File Name="select.pyd" />
24 <File Name="unicodedata.pyd" />
25 <File Name="unicodedata.pyd" />
25 <File Name="_ctypes.pyd" />
26 <File Name="_ctypes.pyd" />
26 <File Name="_elementtree.pyd" />
27 <File Name="_elementtree.pyd" />
27 <File Name="_hashlib.pyd" />
28 <File Name="_hashlib.pyd" />
28 <File Name="_socket.pyd" />
29 <File Name="_socket.pyd" />
29 <File Name="_ssl.pyd" />
30 <File Name="_ssl.pyd" />
30 </Component>
31 </Component>
31 </Directory>
32 </Directory>
32 </DirectoryRef>
33 </DirectoryRef>
33 </Fragment>
34 </Fragment>
34
35
35 </Wix>
36 </Wix>
@@ -1,34 +1,34 b''
1 <?xml version="1.0" encoding="utf-8"?>
1 <?xml version="1.0" encoding="utf-8"?>
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3
3
4 <?include defines.wxi ?>
4 <?include defines.wxi ?>
5
5
6 <?define hglocales =
6 <?define hglocales =
7 da;de;el;fr;it;ja;pt_BR;sv;zh_CN;zh_TW
7 da;de;el;fr;it;ja;pt_BR;ro;ru;sv;zh_CN;zh_TW
8 ?>
8 ?>
9
9
10 <Fragment>
10 <Fragment>
11 <ComponentGroup Id="localeFolder">
11 <ComponentGroup Id="localeFolder">
12 <?foreach LOC in $(var.hglocales) ?>
12 <?foreach LOC in $(var.hglocales) ?>
13 <ComponentRef Id="hg.locale.$(var.LOC)"/>
13 <ComponentRef Id="hg.locale.$(var.LOC)"/>
14 <?endforeach?>
14 <?endforeach?>
15 </ComponentGroup>
15 </ComponentGroup>
16 </Fragment>
16 </Fragment>
17
17
18 <Fragment>
18 <Fragment>
19 <DirectoryRef Id="INSTALLDIR">
19 <DirectoryRef Id="INSTALLDIR">
20 <Directory Id="localedir" Name="locale" FileSource="$(var.SourceDir)">
20 <Directory Id="localedir" Name="locale" FileSource="$(var.SourceDir)">
21 <?foreach LOC in $(var.hglocales) ?>
21 <?foreach LOC in $(var.hglocales) ?>
22 <Directory Id="hg.locale.$(var.LOC)" Name="$(var.LOC)">
22 <Directory Id="hg.locale.$(var.LOC)" Name="$(var.LOC)">
23 <Directory Id="hg.locale.$(var.LOC).LC_MESSAGES" Name="LC_MESSAGES">
23 <Directory Id="hg.locale.$(var.LOC).LC_MESSAGES" Name="LC_MESSAGES">
24 <Component Id="hg.locale.$(var.LOC)" Guid="*" Win64='$(var.IsX64)'>
24 <Component Id="hg.locale.$(var.LOC)" Guid="*" Win64='$(var.IsX64)'>
25 <File Id="hg.mo.$(var.LOC)" Name="hg.mo" KeyPath="yes" />
25 <File Id="hg.mo.$(var.LOC)" Name="hg.mo" KeyPath="yes" />
26 </Component>
26 </Component>
27 </Directory>
27 </Directory>
28 </Directory>
28 </Directory>
29 <?endforeach?>
29 <?endforeach?>
30 </Directory>
30 </Directory>
31 </DirectoryRef>
31 </DirectoryRef>
32 </Fragment>
32 </Fragment>
33
33
34 </Wix>
34 </Wix>
@@ -1,1074 +1,1074 b''
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 assoicated 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 cmdutil,
303 cmdutil,
304 error,
304 error,
305 mail,
305 mail,
306 url,
306 url,
307 util,
307 util,
308 )
308 )
309
309
310 xmlrpclib = util.xmlrpclib
310 xmlrpclib = util.xmlrpclib
311
311
312 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
312 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
313 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
313 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
314 # be specifying the version(s) of Mercurial they are tested with, or
314 # be specifying the version(s) of Mercurial they are tested with, or
315 # leave the attribute unspecified.
315 # leave the attribute unspecified.
316 testedwith = 'ships-with-hg-core'
316 testedwith = 'ships-with-hg-core'
317
317
318 class bzaccess(object):
318 class bzaccess(object):
319 '''Base class for access to Bugzilla.'''
319 '''Base class for access to Bugzilla.'''
320
320
321 def __init__(self, ui):
321 def __init__(self, ui):
322 self.ui = ui
322 self.ui = ui
323 usermap = self.ui.config('bugzilla', 'usermap')
323 usermap = self.ui.config('bugzilla', 'usermap')
324 if usermap:
324 if usermap:
325 self.ui.readconfig(usermap, sections=['usermap'])
325 self.ui.readconfig(usermap, sections=['usermap'])
326
326
327 def map_committer(self, user):
327 def map_committer(self, user):
328 '''map name of committer to Bugzilla user name.'''
328 '''map name of committer to Bugzilla user name.'''
329 for committer, bzuser in self.ui.configitems('usermap'):
329 for committer, bzuser in self.ui.configitems('usermap'):
330 if committer.lower() == user.lower():
330 if committer.lower() == user.lower():
331 return bzuser
331 return bzuser
332 return user
332 return user
333
333
334 # Methods to be implemented by access classes.
334 # Methods to be implemented by access classes.
335 #
335 #
336 # 'bugs' is a dict keyed on bug id, where values are a dict holding
336 # 'bugs' is a dict keyed on bug id, where values are a dict holding
337 # updates to bug state. Recognized dict keys are:
337 # updates to bug state. Recognized dict keys are:
338 #
338 #
339 # 'hours': Value, float containing work hours to be updated.
339 # 'hours': Value, float containing work hours to be updated.
340 # 'fix': If key present, bug is to be marked fixed. Value ignored.
340 # 'fix': If key present, bug is to be marked fixed. Value ignored.
341
341
342 def filter_real_bug_ids(self, bugs):
342 def filter_real_bug_ids(self, bugs):
343 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
343 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
344 pass
344 pass
345
345
346 def filter_cset_known_bug_ids(self, node, bugs):
346 def filter_cset_known_bug_ids(self, node, bugs):
347 '''remove bug IDs where node occurs in comment text from bugs.'''
347 '''remove bug IDs where node occurs in comment text from bugs.'''
348 pass
348 pass
349
349
350 def updatebug(self, bugid, newstate, text, committer):
350 def updatebug(self, bugid, newstate, text, committer):
351 '''update the specified bug. Add comment text and set new states.
351 '''update the specified bug. Add comment text and set new states.
352
352
353 If possible add the comment as being from the committer of
353 If possible add the comment as being from the committer of
354 the changeset. Otherwise use the default Bugzilla user.
354 the changeset. Otherwise use the default Bugzilla user.
355 '''
355 '''
356 pass
356 pass
357
357
358 def notify(self, bugs, committer):
358 def notify(self, bugs, committer):
359 '''Force sending of Bugzilla notification emails.
359 '''Force sending of Bugzilla notification emails.
360
360
361 Only required if the access method does not trigger notification
361 Only required if the access method does not trigger notification
362 emails automatically.
362 emails automatically.
363 '''
363 '''
364 pass
364 pass
365
365
366 # Bugzilla via direct access to MySQL database.
366 # Bugzilla via direct access to MySQL database.
367 class bzmysql(bzaccess):
367 class bzmysql(bzaccess):
368 '''Support for direct MySQL access to Bugzilla.
368 '''Support for direct MySQL access to Bugzilla.
369
369
370 The earliest Bugzilla version this is tested with is version 2.16.
370 The earliest Bugzilla version this is tested with is version 2.16.
371
371
372 If your Bugzilla is version 3.4 or above, you are strongly
372 If your Bugzilla is version 3.4 or above, you are strongly
373 recommended to use the XMLRPC access method instead.
373 recommended to use the XMLRPC access method instead.
374 '''
374 '''
375
375
376 @staticmethod
376 @staticmethod
377 def sql_buglist(ids):
377 def sql_buglist(ids):
378 '''return SQL-friendly list of bug ids'''
378 '''return SQL-friendly list of bug ids'''
379 return '(' + ','.join(map(str, ids)) + ')'
379 return '(' + ','.join(map(str, ids)) + ')'
380
380
381 _MySQLdb = None
381 _MySQLdb = None
382
382
383 def __init__(self, ui):
383 def __init__(self, ui):
384 try:
384 try:
385 import MySQLdb as mysql
385 import MySQLdb as mysql
386 bzmysql._MySQLdb = mysql
386 bzmysql._MySQLdb = mysql
387 except ImportError as err:
387 except ImportError as err:
388 raise error.Abort(_('python mysql support not available: %s') % err)
388 raise error.Abort(_('python mysql support not available: %s') % err)
389
389
390 bzaccess.__init__(self, ui)
390 bzaccess.__init__(self, ui)
391
391
392 host = self.ui.config('bugzilla', 'host', 'localhost')
392 host = self.ui.config('bugzilla', 'host', 'localhost')
393 user = self.ui.config('bugzilla', 'user', 'bugs')
393 user = self.ui.config('bugzilla', 'user', 'bugs')
394 passwd = self.ui.config('bugzilla', 'password')
394 passwd = self.ui.config('bugzilla', 'password')
395 db = self.ui.config('bugzilla', 'db', 'bugs')
395 db = self.ui.config('bugzilla', 'db', 'bugs')
396 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
396 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
397 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
397 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
398 (host, db, user, '*' * len(passwd)))
398 (host, db, user, '*' * len(passwd)))
399 self.conn = bzmysql._MySQLdb.connect(host=host,
399 self.conn = bzmysql._MySQLdb.connect(host=host,
400 user=user, passwd=passwd,
400 user=user, passwd=passwd,
401 db=db,
401 db=db,
402 connect_timeout=timeout)
402 connect_timeout=timeout)
403 self.cursor = self.conn.cursor()
403 self.cursor = self.conn.cursor()
404 self.longdesc_id = self.get_longdesc_id()
404 self.longdesc_id = self.get_longdesc_id()
405 self.user_ids = {}
405 self.user_ids = {}
406 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
406 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
407
407
408 def run(self, *args, **kwargs):
408 def run(self, *args, **kwargs):
409 '''run a query.'''
409 '''run a query.'''
410 self.ui.note(_('query: %s %s\n') % (args, kwargs))
410 self.ui.note(_('query: %s %s\n') % (args, kwargs))
411 try:
411 try:
412 self.cursor.execute(*args, **kwargs)
412 self.cursor.execute(*args, **kwargs)
413 except bzmysql._MySQLdb.MySQLError:
413 except bzmysql._MySQLdb.MySQLError:
414 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
414 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
415 raise
415 raise
416
416
417 def get_longdesc_id(self):
417 def get_longdesc_id(self):
418 '''get identity of longdesc field'''
418 '''get identity of longdesc field'''
419 self.run('select fieldid from fielddefs where name = "longdesc"')
419 self.run('select fieldid from fielddefs where name = "longdesc"')
420 ids = self.cursor.fetchall()
420 ids = self.cursor.fetchall()
421 if len(ids) != 1:
421 if len(ids) != 1:
422 raise error.Abort(_('unknown database schema'))
422 raise error.Abort(_('unknown database schema'))
423 return ids[0][0]
423 return ids[0][0]
424
424
425 def filter_real_bug_ids(self, bugs):
425 def filter_real_bug_ids(self, bugs):
426 '''filter not-existing bugs from set.'''
426 '''filter not-existing bugs from set.'''
427 self.run('select bug_id from bugs where bug_id in %s' %
427 self.run('select bug_id from bugs where bug_id in %s' %
428 bzmysql.sql_buglist(bugs.keys()))
428 bzmysql.sql_buglist(bugs.keys()))
429 existing = [id for (id,) in self.cursor.fetchall()]
429 existing = [id for (id,) in self.cursor.fetchall()]
430 for id in bugs.keys():
430 for id in bugs.keys():
431 if id not in existing:
431 if id not in existing:
432 self.ui.status(_('bug %d does not exist\n') % id)
432 self.ui.status(_('bug %d does not exist\n') % id)
433 del bugs[id]
433 del bugs[id]
434
434
435 def filter_cset_known_bug_ids(self, node, bugs):
435 def filter_cset_known_bug_ids(self, node, bugs):
436 '''filter bug ids that already refer to this changeset from set.'''
436 '''filter bug ids that already refer to this changeset from set.'''
437 self.run('''select bug_id from longdescs where
437 self.run('''select bug_id from longdescs where
438 bug_id in %s and thetext like "%%%s%%"''' %
438 bug_id in %s and thetext like "%%%s%%"''' %
439 (bzmysql.sql_buglist(bugs.keys()), short(node)))
439 (bzmysql.sql_buglist(bugs.keys()), short(node)))
440 for (id,) in self.cursor.fetchall():
440 for (id,) in self.cursor.fetchall():
441 self.ui.status(_('bug %d already knows about changeset %s\n') %
441 self.ui.status(_('bug %d already knows about changeset %s\n') %
442 (id, short(node)))
442 (id, short(node)))
443 del bugs[id]
443 del bugs[id]
444
444
445 def notify(self, bugs, committer):
445 def notify(self, bugs, committer):
446 '''tell bugzilla to send mail.'''
446 '''tell bugzilla to send mail.'''
447 self.ui.status(_('telling bugzilla to send mail:\n'))
447 self.ui.status(_('telling bugzilla to send mail:\n'))
448 (user, userid) = self.get_bugzilla_user(committer)
448 (user, userid) = self.get_bugzilla_user(committer)
449 for id in bugs.keys():
449 for id in bugs.keys():
450 self.ui.status(_(' bug %s\n') % id)
450 self.ui.status(_(' bug %s\n') % id)
451 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
451 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
452 bzdir = self.ui.config('bugzilla', 'bzdir',
452 bzdir = self.ui.config('bugzilla', 'bzdir',
453 '/var/www/html/bugzilla')
453 '/var/www/html/bugzilla')
454 try:
454 try:
455 # Backwards-compatible with old notify string, which
455 # Backwards-compatible with old notify string, which
456 # took one string. This will throw with a new format
456 # took one string. This will throw with a new format
457 # string.
457 # string.
458 cmd = cmdfmt % id
458 cmd = cmdfmt % id
459 except TypeError:
459 except TypeError:
460 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
460 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
461 self.ui.note(_('running notify command %s\n') % cmd)
461 self.ui.note(_('running notify command %s\n') % cmd)
462 fp = util.popen('(%s) 2>&1' % cmd)
462 fp = util.popen('(%s) 2>&1' % cmd)
463 out = fp.read()
463 out = fp.read()
464 ret = fp.close()
464 ret = fp.close()
465 if ret:
465 if ret:
466 self.ui.warn(out)
466 self.ui.warn(out)
467 raise error.Abort(_('bugzilla notify command %s') %
467 raise error.Abort(_('bugzilla notify command %s') %
468 util.explainexit(ret)[0])
468 util.explainexit(ret)[0])
469 self.ui.status(_('done\n'))
469 self.ui.status(_('done\n'))
470
470
471 def get_user_id(self, user):
471 def get_user_id(self, user):
472 '''look up numeric bugzilla user id.'''
472 '''look up numeric bugzilla user id.'''
473 try:
473 try:
474 return self.user_ids[user]
474 return self.user_ids[user]
475 except KeyError:
475 except KeyError:
476 try:
476 try:
477 userid = int(user)
477 userid = int(user)
478 except ValueError:
478 except ValueError:
479 self.ui.note(_('looking up user %s\n') % user)
479 self.ui.note(_('looking up user %s\n') % user)
480 self.run('''select userid from profiles
480 self.run('''select userid from profiles
481 where login_name like %s''', user)
481 where login_name like %s''', user)
482 all = self.cursor.fetchall()
482 all = self.cursor.fetchall()
483 if len(all) != 1:
483 if len(all) != 1:
484 raise KeyError(user)
484 raise KeyError(user)
485 userid = int(all[0][0])
485 userid = int(all[0][0])
486 self.user_ids[user] = userid
486 self.user_ids[user] = userid
487 return userid
487 return userid
488
488
489 def get_bugzilla_user(self, committer):
489 def get_bugzilla_user(self, committer):
490 '''See if committer is a registered bugzilla user. Return
490 '''See if committer is a registered bugzilla user. Return
491 bugzilla username and userid if so. If not, return default
491 bugzilla username and userid if so. If not, return default
492 bugzilla username and userid.'''
492 bugzilla username and userid.'''
493 user = self.map_committer(committer)
493 user = self.map_committer(committer)
494 try:
494 try:
495 userid = self.get_user_id(user)
495 userid = self.get_user_id(user)
496 except KeyError:
496 except KeyError:
497 try:
497 try:
498 defaultuser = self.ui.config('bugzilla', 'bzuser')
498 defaultuser = self.ui.config('bugzilla', 'bzuser')
499 if not defaultuser:
499 if not defaultuser:
500 raise error.Abort(_('cannot find bugzilla user id for %s') %
500 raise error.Abort(_('cannot find bugzilla user id for %s') %
501 user)
501 user)
502 userid = self.get_user_id(defaultuser)
502 userid = self.get_user_id(defaultuser)
503 user = defaultuser
503 user = defaultuser
504 except KeyError:
504 except KeyError:
505 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
505 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
506 % (user, defaultuser))
506 % (user, defaultuser))
507 return (user, userid)
507 return (user, userid)
508
508
509 def updatebug(self, bugid, newstate, text, committer):
509 def updatebug(self, bugid, newstate, text, committer):
510 '''update bug state with comment text.
510 '''update bug state with comment text.
511
511
512 Try adding comment as committer of changeset, otherwise as
512 Try adding comment as committer of changeset, otherwise as
513 default bugzilla user.'''
513 default bugzilla user.'''
514 if len(newstate) > 0:
514 if len(newstate) > 0:
515 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
515 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
516
516
517 (user, userid) = self.get_bugzilla_user(committer)
517 (user, userid) = self.get_bugzilla_user(committer)
518 now = time.strftime('%Y-%m-%d %H:%M:%S')
518 now = time.strftime('%Y-%m-%d %H:%M:%S')
519 self.run('''insert into longdescs
519 self.run('''insert into longdescs
520 (bug_id, who, bug_when, thetext)
520 (bug_id, who, bug_when, thetext)
521 values (%s, %s, %s, %s)''',
521 values (%s, %s, %s, %s)''',
522 (bugid, userid, now, text))
522 (bugid, userid, now, text))
523 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
523 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
524 values (%s, %s, %s, %s)''',
524 values (%s, %s, %s, %s)''',
525 (bugid, userid, now, self.longdesc_id))
525 (bugid, userid, now, self.longdesc_id))
526 self.conn.commit()
526 self.conn.commit()
527
527
528 class bzmysql_2_18(bzmysql):
528 class bzmysql_2_18(bzmysql):
529 '''support for bugzilla 2.18 series.'''
529 '''support for bugzilla 2.18 series.'''
530
530
531 def __init__(self, ui):
531 def __init__(self, ui):
532 bzmysql.__init__(self, ui)
532 bzmysql.__init__(self, ui)
533 self.default_notify = \
533 self.default_notify = \
534 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
534 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
535
535
536 class bzmysql_3_0(bzmysql_2_18):
536 class bzmysql_3_0(bzmysql_2_18):
537 '''support for bugzilla 3.0 series.'''
537 '''support for bugzilla 3.0 series.'''
538
538
539 def __init__(self, ui):
539 def __init__(self, ui):
540 bzmysql_2_18.__init__(self, ui)
540 bzmysql_2_18.__init__(self, ui)
541
541
542 def get_longdesc_id(self):
542 def get_longdesc_id(self):
543 '''get identity of longdesc field'''
543 '''get identity of longdesc field'''
544 self.run('select id from fielddefs where name = "longdesc"')
544 self.run('select id from fielddefs where name = "longdesc"')
545 ids = self.cursor.fetchall()
545 ids = self.cursor.fetchall()
546 if len(ids) != 1:
546 if len(ids) != 1:
547 raise error.Abort(_('unknown database schema'))
547 raise error.Abort(_('unknown database schema'))
548 return ids[0][0]
548 return ids[0][0]
549
549
550 # Bugzilla via XMLRPC interface.
550 # Bugzilla via XMLRPC interface.
551
551
552 class cookietransportrequest(object):
552 class cookietransportrequest(object):
553 """A Transport request method that retains cookies over its lifetime.
553 """A Transport request method that retains cookies over its lifetime.
554
554
555 The regular xmlrpclib transports ignore cookies. Which causes
555 The regular xmlrpclib transports ignore cookies. Which causes
556 a bit of a problem when you need a cookie-based login, as with
556 a bit of a problem when you need a cookie-based login, as with
557 the Bugzilla XMLRPC interface prior to 4.4.3.
557 the Bugzilla XMLRPC interface prior to 4.4.3.
558
558
559 So this is a helper for defining a Transport which looks for
559 So this is a helper for defining a Transport which looks for
560 cookies being set in responses and saves them to add to all future
560 cookies being set in responses and saves them to add to all future
561 requests.
561 requests.
562 """
562 """
563
563
564 # Inspiration drawn from
564 # Inspiration drawn from
565 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
565 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
566 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
566 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
567
567
568 cookies = []
568 cookies = []
569 def send_cookies(self, connection):
569 def send_cookies(self, connection):
570 if self.cookies:
570 if self.cookies:
571 for cookie in self.cookies:
571 for cookie in self.cookies:
572 connection.putheader("Cookie", cookie)
572 connection.putheader("Cookie", cookie)
573
573
574 def request(self, host, handler, request_body, verbose=0):
574 def request(self, host, handler, request_body, verbose=0):
575 self.verbose = verbose
575 self.verbose = verbose
576 self.accept_gzip_encoding = False
576 self.accept_gzip_encoding = False
577
577
578 # issue XML-RPC request
578 # issue XML-RPC request
579 h = self.make_connection(host)
579 h = self.make_connection(host)
580 if verbose:
580 if verbose:
581 h.set_debuglevel(1)
581 h.set_debuglevel(1)
582
582
583 self.send_request(h, handler, request_body)
583 self.send_request(h, handler, request_body)
584 self.send_host(h, host)
584 self.send_host(h, host)
585 self.send_cookies(h)
585 self.send_cookies(h)
586 self.send_user_agent(h)
586 self.send_user_agent(h)
587 self.send_content(h, request_body)
587 self.send_content(h, request_body)
588
588
589 # Deal with differences between Python 2.6 and 2.7.
589 # Deal with differences between Python 2.6 and 2.7.
590 # In the former h is a HTTP(S). In the latter it's a
590 # In the former h is a HTTP(S). In the latter it's a
591 # HTTP(S)Connection. Luckily, the 2.6 implementation of
591 # HTTP(S)Connection. Luckily, the 2.6 implementation of
592 # HTTP(S) has an underlying HTTP(S)Connection, so extract
592 # HTTP(S) has an underlying HTTP(S)Connection, so extract
593 # that and use it.
593 # that and use it.
594 try:
594 try:
595 response = h.getresponse()
595 response = h.getresponse()
596 except AttributeError:
596 except AttributeError:
597 response = h._conn.getresponse()
597 response = h._conn.getresponse()
598
598
599 # Add any cookie definitions to our list.
599 # Add any cookie definitions to our list.
600 for header in response.msg.getallmatchingheaders("Set-Cookie"):
600 for header in response.msg.getallmatchingheaders("Set-Cookie"):
601 val = header.split(": ", 1)[1]
601 val = header.split(": ", 1)[1]
602 cookie = val.split(";", 1)[0]
602 cookie = val.split(";", 1)[0]
603 self.cookies.append(cookie)
603 self.cookies.append(cookie)
604
604
605 if response.status != 200:
605 if response.status != 200:
606 raise xmlrpclib.ProtocolError(host + handler, response.status,
606 raise xmlrpclib.ProtocolError(host + handler, response.status,
607 response.reason, response.msg.headers)
607 response.reason, response.msg.headers)
608
608
609 payload = response.read()
609 payload = response.read()
610 parser, unmarshaller = self.getparser()
610 parser, unmarshaller = self.getparser()
611 parser.feed(payload)
611 parser.feed(payload)
612 parser.close()
612 parser.close()
613
613
614 return unmarshaller.close()
614 return unmarshaller.close()
615
615
616 # The explicit calls to the underlying xmlrpclib __init__() methods are
616 # The explicit calls to the underlying xmlrpclib __init__() methods are
617 # necessary. The xmlrpclib.Transport classes are old-style classes, and
617 # necessary. The xmlrpclib.Transport classes are old-style classes, and
618 # it turns out their __init__() doesn't get called when doing multiple
618 # it turns out their __init__() doesn't get called when doing multiple
619 # inheritance with a new-style class.
619 # inheritance with a new-style class.
620 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
620 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
621 def __init__(self, use_datetime=0):
621 def __init__(self, use_datetime=0):
622 if util.safehasattr(xmlrpclib.Transport, "__init__"):
622 if util.safehasattr(xmlrpclib.Transport, "__init__"):
623 xmlrpclib.Transport.__init__(self, use_datetime)
623 xmlrpclib.Transport.__init__(self, use_datetime)
624
624
625 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
625 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
626 def __init__(self, use_datetime=0):
626 def __init__(self, use_datetime=0):
627 if util.safehasattr(xmlrpclib.Transport, "__init__"):
627 if util.safehasattr(xmlrpclib.Transport, "__init__"):
628 xmlrpclib.SafeTransport.__init__(self, use_datetime)
628 xmlrpclib.SafeTransport.__init__(self, use_datetime)
629
629
630 class bzxmlrpc(bzaccess):
630 class bzxmlrpc(bzaccess):
631 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
631 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
632
632
633 Requires a minimum Bugzilla version 3.4.
633 Requires a minimum Bugzilla version 3.4.
634 """
634 """
635
635
636 def __init__(self, ui):
636 def __init__(self, ui):
637 bzaccess.__init__(self, ui)
637 bzaccess.__init__(self, ui)
638
638
639 bzweb = self.ui.config('bugzilla', 'bzurl',
639 bzweb = self.ui.config('bugzilla', 'bzurl',
640 'http://localhost/bugzilla/')
640 'http://localhost/bugzilla/')
641 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
641 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
642
642
643 user = self.ui.config('bugzilla', 'user', 'bugs')
643 user = self.ui.config('bugzilla', 'user', 'bugs')
644 passwd = self.ui.config('bugzilla', 'password')
644 passwd = self.ui.config('bugzilla', 'password')
645
645
646 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
646 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
647 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
647 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
648 'FIXED')
648 'FIXED')
649
649
650 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
650 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
651 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
651 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
652 self.bzvermajor = int(ver[0])
652 self.bzvermajor = int(ver[0])
653 self.bzverminor = int(ver[1])
653 self.bzverminor = int(ver[1])
654 login = self.bzproxy.User.login({'login': user, 'password': passwd,
654 login = self.bzproxy.User.login({'login': user, 'password': passwd,
655 'restrict_login': True})
655 'restrict_login': True})
656 self.bztoken = login.get('token', '')
656 self.bztoken = login.get('token', '')
657
657
658 def transport(self, uri):
658 def transport(self, uri):
659 if util.urlreq.urlparse(uri, "http")[0] == "https":
659 if util.urlreq.urlparse(uri, "http")[0] == "https":
660 return cookiesafetransport()
660 return cookiesafetransport()
661 else:
661 else:
662 return cookietransport()
662 return cookietransport()
663
663
664 def get_bug_comments(self, id):
664 def get_bug_comments(self, id):
665 """Return a string with all comment text for a bug."""
665 """Return a string with all comment text for a bug."""
666 c = self.bzproxy.Bug.comments({'ids': [id],
666 c = self.bzproxy.Bug.comments({'ids': [id],
667 'include_fields': ['text'],
667 'include_fields': ['text'],
668 'token': self.bztoken})
668 'token': self.bztoken})
669 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
669 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
670
670
671 def filter_real_bug_ids(self, bugs):
671 def filter_real_bug_ids(self, bugs):
672 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
672 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
673 'include_fields': [],
673 'include_fields': [],
674 'permissive': True,
674 'permissive': True,
675 'token': self.bztoken,
675 'token': self.bztoken,
676 })
676 })
677 for badbug in probe['faults']:
677 for badbug in probe['faults']:
678 id = badbug['id']
678 id = badbug['id']
679 self.ui.status(_('bug %d does not exist\n') % id)
679 self.ui.status(_('bug %d does not exist\n') % id)
680 del bugs[id]
680 del bugs[id]
681
681
682 def filter_cset_known_bug_ids(self, node, bugs):
682 def filter_cset_known_bug_ids(self, node, bugs):
683 for id in sorted(bugs.keys()):
683 for id in sorted(bugs.keys()):
684 if self.get_bug_comments(id).find(short(node)) != -1:
684 if self.get_bug_comments(id).find(short(node)) != -1:
685 self.ui.status(_('bug %d already knows about changeset %s\n') %
685 self.ui.status(_('bug %d already knows about changeset %s\n') %
686 (id, short(node)))
686 (id, short(node)))
687 del bugs[id]
687 del bugs[id]
688
688
689 def updatebug(self, bugid, newstate, text, committer):
689 def updatebug(self, bugid, newstate, text, committer):
690 args = {}
690 args = {}
691 if 'hours' in newstate:
691 if 'hours' in newstate:
692 args['work_time'] = newstate['hours']
692 args['work_time'] = newstate['hours']
693
693
694 if self.bzvermajor >= 4:
694 if self.bzvermajor >= 4:
695 args['ids'] = [bugid]
695 args['ids'] = [bugid]
696 args['comment'] = {'body' : text}
696 args['comment'] = {'body' : text}
697 if 'fix' in newstate:
697 if 'fix' in newstate:
698 args['status'] = self.fixstatus
698 args['status'] = self.fixstatus
699 args['resolution'] = self.fixresolution
699 args['resolution'] = self.fixresolution
700 args['token'] = self.bztoken
700 args['token'] = self.bztoken
701 self.bzproxy.Bug.update(args)
701 self.bzproxy.Bug.update(args)
702 else:
702 else:
703 if 'fix' in newstate:
703 if 'fix' in newstate:
704 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
704 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
705 "to mark bugs fixed\n"))
705 "to mark bugs fixed\n"))
706 args['id'] = bugid
706 args['id'] = bugid
707 args['comment'] = text
707 args['comment'] = text
708 self.bzproxy.Bug.add_comment(args)
708 self.bzproxy.Bug.add_comment(args)
709
709
710 class bzxmlrpcemail(bzxmlrpc):
710 class bzxmlrpcemail(bzxmlrpc):
711 """Read data from Bugzilla via XMLRPC, send updates via email.
711 """Read data from Bugzilla via XMLRPC, send updates via email.
712
712
713 Advantages of sending updates via email:
713 Advantages of sending updates via email:
714 1. Comments can be added as any user, not just logged in user.
714 1. Comments can be added as any user, not just logged in user.
715 2. Bug statuses or other fields not accessible via XMLRPC can
715 2. Bug statuses or other fields not accessible via XMLRPC can
716 potentially be updated.
716 potentially be updated.
717
717
718 There is no XMLRPC function to change bug status before Bugzilla
718 There is no XMLRPC function to change bug status before Bugzilla
719 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
719 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
720 But bugs can be marked fixed via email from 3.4 onwards.
720 But bugs can be marked fixed via email from 3.4 onwards.
721 """
721 """
722
722
723 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
723 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
724 # in-email fields are specified as '@<fieldname> = <value>'. In
724 # in-email fields are specified as '@<fieldname> = <value>'. In
725 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
725 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
726 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
726 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
727 # compatibility, but rather than rely on this use the new format for
727 # compatibility, but rather than rely on this use the new format for
728 # 4.0 onwards.
728 # 4.0 onwards.
729
729
730 def __init__(self, ui):
730 def __init__(self, ui):
731 bzxmlrpc.__init__(self, ui)
731 bzxmlrpc.__init__(self, ui)
732
732
733 self.bzemail = self.ui.config('bugzilla', 'bzemail')
733 self.bzemail = self.ui.config('bugzilla', 'bzemail')
734 if not self.bzemail:
734 if not self.bzemail:
735 raise error.Abort(_("configuration 'bzemail' missing"))
735 raise error.Abort(_("configuration 'bzemail' missing"))
736 mail.validateconfig(self.ui)
736 mail.validateconfig(self.ui)
737
737
738 def makecommandline(self, fieldname, value):
738 def makecommandline(self, fieldname, value):
739 if self.bzvermajor >= 4:
739 if self.bzvermajor >= 4:
740 return "@%s %s" % (fieldname, str(value))
740 return "@%s %s" % (fieldname, str(value))
741 else:
741 else:
742 if fieldname == "id":
742 if fieldname == "id":
743 fieldname = "bug_id"
743 fieldname = "bug_id"
744 return "@%s = %s" % (fieldname, str(value))
744 return "@%s = %s" % (fieldname, str(value))
745
745
746 def send_bug_modify_email(self, bugid, commands, comment, committer):
746 def send_bug_modify_email(self, bugid, commands, comment, committer):
747 '''send modification message to Bugzilla bug via email.
747 '''send modification message to Bugzilla bug via email.
748
748
749 The message format is documented in the Bugzilla email_in.pl
749 The message format is documented in the Bugzilla email_in.pl
750 specification. commands is a list of command lines, comment is the
750 specification. commands is a list of command lines, comment is the
751 comment text.
751 comment text.
752
752
753 To stop users from crafting commit comments with
753 To stop users from crafting commit comments with
754 Bugzilla commands, specify the bug ID via the message body, rather
754 Bugzilla commands, specify the bug ID via the message body, rather
755 than the subject line, and leave a blank line after it.
755 than the subject line, and leave a blank line after it.
756 '''
756 '''
757 user = self.map_committer(committer)
757 user = self.map_committer(committer)
758 matches = self.bzproxy.User.get({'match': [user],
758 matches = self.bzproxy.User.get({'match': [user],
759 'token': self.bztoken})
759 'token': self.bztoken})
760 if not matches['users']:
760 if not matches['users']:
761 user = self.ui.config('bugzilla', 'user', 'bugs')
761 user = self.ui.config('bugzilla', 'user', 'bugs')
762 matches = self.bzproxy.User.get({'match': [user],
762 matches = self.bzproxy.User.get({'match': [user],
763 'token': self.bztoken})
763 'token': self.bztoken})
764 if not matches['users']:
764 if not matches['users']:
765 raise error.Abort(_("default bugzilla user %s email not found")
765 raise error.Abort(_("default bugzilla user %s email not found")
766 % user)
766 % user)
767 user = matches['users'][0]['email']
767 user = matches['users'][0]['email']
768 commands.append(self.makecommandline("id", bugid))
768 commands.append(self.makecommandline("id", bugid))
769
769
770 text = "\n".join(commands) + "\n\n" + comment
770 text = "\n".join(commands) + "\n\n" + comment
771
771
772 _charsets = mail._charsets(self.ui)
772 _charsets = mail._charsets(self.ui)
773 user = mail.addressencode(self.ui, user, _charsets)
773 user = mail.addressencode(self.ui, user, _charsets)
774 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
774 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
775 msg = mail.mimeencode(self.ui, text, _charsets)
775 msg = mail.mimeencode(self.ui, text, _charsets)
776 msg['From'] = user
776 msg['From'] = user
777 msg['To'] = bzemail
777 msg['To'] = bzemail
778 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
778 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
779 sendmail = mail.connect(self.ui)
779 sendmail = mail.connect(self.ui)
780 sendmail(user, bzemail, msg.as_string())
780 sendmail(user, bzemail, msg.as_string())
781
781
782 def updatebug(self, bugid, newstate, text, committer):
782 def updatebug(self, bugid, newstate, text, committer):
783 cmds = []
783 cmds = []
784 if 'hours' in newstate:
784 if 'hours' in newstate:
785 cmds.append(self.makecommandline("work_time", newstate['hours']))
785 cmds.append(self.makecommandline("work_time", newstate['hours']))
786 if 'fix' in newstate:
786 if 'fix' in newstate:
787 cmds.append(self.makecommandline("bug_status", self.fixstatus))
787 cmds.append(self.makecommandline("bug_status", self.fixstatus))
788 cmds.append(self.makecommandline("resolution", self.fixresolution))
788 cmds.append(self.makecommandline("resolution", self.fixresolution))
789 self.send_bug_modify_email(bugid, cmds, text, committer)
789 self.send_bug_modify_email(bugid, cmds, text, committer)
790
790
791 class NotFound(LookupError):
791 class NotFound(LookupError):
792 pass
792 pass
793
793
794 class bzrestapi(bzaccess):
794 class bzrestapi(bzaccess):
795 """Read and write bugzilla data using the REST API available since
795 """Read and write bugzilla data using the REST API available since
796 Bugzilla 5.0.
796 Bugzilla 5.0.
797 """
797 """
798 def __init__(self, ui):
798 def __init__(self, ui):
799 bzaccess.__init__(self, ui)
799 bzaccess.__init__(self, ui)
800 bz = self.ui.config('bugzilla', 'bzurl',
800 bz = self.ui.config('bugzilla', 'bzurl',
801 'http://localhost/bugzilla/')
801 'http://localhost/bugzilla/')
802 self.bzroot = '/'.join([bz, 'rest'])
802 self.bzroot = '/'.join([bz, 'rest'])
803 self.apikey = self.ui.config('bugzilla', 'apikey', '')
803 self.apikey = self.ui.config('bugzilla', 'apikey', '')
804 self.user = self.ui.config('bugzilla', 'user', 'bugs')
804 self.user = self.ui.config('bugzilla', 'user', 'bugs')
805 self.passwd = self.ui.config('bugzilla', 'password')
805 self.passwd = self.ui.config('bugzilla', 'password')
806 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
806 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
807 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
807 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
808 'FIXED')
808 'FIXED')
809
809
810 def apiurl(self, targets, include_fields=None):
810 def apiurl(self, targets, include_fields=None):
811 url = '/'.join([self.bzroot] + [str(t) for t in targets])
811 url = '/'.join([self.bzroot] + [str(t) for t in targets])
812 qv = {}
812 qv = {}
813 if self.apikey:
813 if self.apikey:
814 qv['api_key'] = self.apikey
814 qv['api_key'] = self.apikey
815 elif self.user and self.passwd:
815 elif self.user and self.passwd:
816 qv['login'] = self.user
816 qv['login'] = self.user
817 qv['password'] = self.passwd
817 qv['password'] = self.passwd
818 if include_fields:
818 if include_fields:
819 qv['include_fields'] = include_fields
819 qv['include_fields'] = include_fields
820 if qv:
820 if qv:
821 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
821 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
822 return url
822 return url
823
823
824 def _fetch(self, burl):
824 def _fetch(self, burl):
825 try:
825 try:
826 resp = url.open(self.ui, burl)
826 resp = url.open(self.ui, burl)
827 return json.loads(resp.read())
827 return json.loads(resp.read())
828 except util.urlerr.httperror as inst:
828 except util.urlerr.httperror as inst:
829 if inst.code == 401:
829 if inst.code == 401:
830 raise error.Abort(_('authorization failed'))
830 raise error.Abort(_('authorization failed'))
831 if inst.code == 404:
831 if inst.code == 404:
832 raise NotFound()
832 raise NotFound()
833 else:
833 else:
834 raise
834 raise
835
835
836 def _submit(self, burl, data, method='POST'):
836 def _submit(self, burl, data, method='POST'):
837 data = json.dumps(data)
837 data = json.dumps(data)
838 if method == 'PUT':
838 if method == 'PUT':
839 class putrequest(util.urlreq.request):
839 class putrequest(util.urlreq.request):
840 def get_method(self):
840 def get_method(self):
841 return 'PUT'
841 return 'PUT'
842 request_type = putrequest
842 request_type = putrequest
843 else:
843 else:
844 request_type = util.urlreq.request
844 request_type = util.urlreq.request
845 req = request_type(burl, data,
845 req = request_type(burl, data,
846 {'Content-Type': 'application/json'})
846 {'Content-Type': 'application/json'})
847 try:
847 try:
848 resp = url.opener(self.ui).open(req)
848 resp = url.opener(self.ui).open(req)
849 return json.loads(resp.read())
849 return json.loads(resp.read())
850 except util.urlerr.httperror as inst:
850 except util.urlerr.httperror as inst:
851 if inst.code == 401:
851 if inst.code == 401:
852 raise error.Abort(_('authorization failed'))
852 raise error.Abort(_('authorization failed'))
853 if inst.code == 404:
853 if inst.code == 404:
854 raise NotFound()
854 raise NotFound()
855 else:
855 else:
856 raise
856 raise
857
857
858 def filter_real_bug_ids(self, bugs):
858 def filter_real_bug_ids(self, bugs):
859 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
859 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
860 badbugs = set()
860 badbugs = set()
861 for bugid in bugs:
861 for bugid in bugs:
862 burl = self.apiurl(('bug', bugid), include_fields='status')
862 burl = self.apiurl(('bug', bugid), include_fields='status')
863 try:
863 try:
864 self._fetch(burl)
864 self._fetch(burl)
865 except NotFound:
865 except NotFound:
866 badbugs.add(bugid)
866 badbugs.add(bugid)
867 for bugid in badbugs:
867 for bugid in badbugs:
868 del bugs[bugid]
868 del bugs[bugid]
869
869
870 def filter_cset_known_bug_ids(self, node, bugs):
870 def filter_cset_known_bug_ids(self, node, bugs):
871 '''remove bug IDs where node occurs in comment text from bugs.'''
871 '''remove bug IDs where node occurs in comment text from bugs.'''
872 sn = short(node)
872 sn = short(node)
873 for bugid in bugs.keys():
873 for bugid in bugs.keys():
874 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
874 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
875 result = self._fetch(burl)
875 result = self._fetch(burl)
876 comments = result['bugs'][str(bugid)]['comments']
876 comments = result['bugs'][str(bugid)]['comments']
877 if any(sn in c['text'] for c in comments):
877 if any(sn in c['text'] for c in comments):
878 self.ui.status(_('bug %d already knows about changeset %s\n') %
878 self.ui.status(_('bug %d already knows about changeset %s\n') %
879 (bugid, sn))
879 (bugid, sn))
880 del bugs[bugid]
880 del bugs[bugid]
881
881
882 def updatebug(self, bugid, newstate, text, committer):
882 def updatebug(self, bugid, newstate, text, committer):
883 '''update the specified bug. Add comment text and set new states.
883 '''update the specified bug. Add comment text and set new states.
884
884
885 If possible add the comment as being from the committer of
885 If possible add the comment as being from the committer of
886 the changeset. Otherwise use the default Bugzilla user.
886 the changeset. Otherwise use the default Bugzilla user.
887 '''
887 '''
888 bugmod = {}
888 bugmod = {}
889 if 'hours' in newstate:
889 if 'hours' in newstate:
890 bugmod['work_time'] = newstate['hours']
890 bugmod['work_time'] = newstate['hours']
891 if 'fix' in newstate:
891 if 'fix' in newstate:
892 bugmod['status'] = self.fixstatus
892 bugmod['status'] = self.fixstatus
893 bugmod['resolution'] = self.fixresolution
893 bugmod['resolution'] = self.fixresolution
894 if bugmod:
894 if bugmod:
895 # if we have to change the bugs state do it here
895 # if we have to change the bugs state do it here
896 bugmod['comment'] = {
896 bugmod['comment'] = {
897 'comment': text,
897 'comment': text,
898 'is_private': False,
898 'is_private': False,
899 'is_markdown': False,
899 'is_markdown': False,
900 }
900 }
901 burl = self.apiurl(('bug', bugid))
901 burl = self.apiurl(('bug', bugid))
902 self._submit(burl, bugmod, method='PUT')
902 self._submit(burl, bugmod, method='PUT')
903 self.ui.debug('updated bug %s\n' % bugid)
903 self.ui.debug('updated bug %s\n' % bugid)
904 else:
904 else:
905 burl = self.apiurl(('bug', bugid, 'comment'))
905 burl = self.apiurl(('bug', bugid, 'comment'))
906 self._submit(burl, {
906 self._submit(burl, {
907 'comment': text,
907 'comment': text,
908 'is_private': False,
908 'is_private': False,
909 'is_markdown': False,
909 'is_markdown': False,
910 })
910 })
911 self.ui.debug('added comment to bug %s\n' % bugid)
911 self.ui.debug('added comment to bug %s\n' % bugid)
912
912
913 def notify(self, bugs, committer):
913 def notify(self, bugs, committer):
914 '''Force sending of Bugzilla notification emails.
914 '''Force sending of Bugzilla notification emails.
915
915
916 Only required if the access method does not trigger notification
916 Only required if the access method does not trigger notification
917 emails automatically.
917 emails automatically.
918 '''
918 '''
919 pass
919 pass
920
920
921 class bugzilla(object):
921 class bugzilla(object):
922 # supported versions of bugzilla. different versions have
922 # supported versions of bugzilla. different versions have
923 # different schemas.
923 # different schemas.
924 _versions = {
924 _versions = {
925 '2.16': bzmysql,
925 '2.16': bzmysql,
926 '2.18': bzmysql_2_18,
926 '2.18': bzmysql_2_18,
927 '3.0': bzmysql_3_0,
927 '3.0': bzmysql_3_0,
928 'xmlrpc': bzxmlrpc,
928 'xmlrpc': bzxmlrpc,
929 'xmlrpc+email': bzxmlrpcemail,
929 'xmlrpc+email': bzxmlrpcemail,
930 'restapi': bzrestapi,
930 'restapi': bzrestapi,
931 }
931 }
932
932
933 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
933 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
934 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
934 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
935 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
935 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
936
936
937 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
937 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
938 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
938 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
939 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
939 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
940 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
940 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
941
941
942 def __init__(self, ui, repo):
942 def __init__(self, ui, repo):
943 self.ui = ui
943 self.ui = ui
944 self.repo = repo
944 self.repo = repo
945
945
946 bzversion = self.ui.config('bugzilla', 'version')
946 bzversion = self.ui.config('bugzilla', 'version')
947 try:
947 try:
948 bzclass = bugzilla._versions[bzversion]
948 bzclass = bugzilla._versions[bzversion]
949 except KeyError:
949 except KeyError:
950 raise error.Abort(_('bugzilla version %s not supported') %
950 raise error.Abort(_('bugzilla version %s not supported') %
951 bzversion)
951 bzversion)
952 self.bzdriver = bzclass(self.ui)
952 self.bzdriver = bzclass(self.ui)
953
953
954 self.bug_re = re.compile(
954 self.bug_re = re.compile(
955 self.ui.config('bugzilla', 'regexp',
955 self.ui.config('bugzilla', 'regexp',
956 bugzilla._default_bug_re), re.IGNORECASE)
956 bugzilla._default_bug_re), re.IGNORECASE)
957 self.fix_re = re.compile(
957 self.fix_re = re.compile(
958 self.ui.config('bugzilla', 'fixregexp',
958 self.ui.config('bugzilla', 'fixregexp',
959 bugzilla._default_fix_re), re.IGNORECASE)
959 bugzilla._default_fix_re), re.IGNORECASE)
960 self.split_re = re.compile(r'\D+')
960 self.split_re = re.compile(r'\D+')
961
961
962 def find_bugs(self, ctx):
962 def find_bugs(self, ctx):
963 '''return bugs dictionary created from commit comment.
963 '''return bugs dictionary created from commit comment.
964
964
965 Extract bug info from changeset comments. Filter out any that are
965 Extract bug info from changeset comments. Filter out any that are
966 not known to Bugzilla, and any that already have a reference to
966 not known to Bugzilla, and any that already have a reference to
967 the given changeset in their comments.
967 the given changeset in their comments.
968 '''
968 '''
969 start = 0
969 start = 0
970 hours = 0.0
970 hours = 0.0
971 bugs = {}
971 bugs = {}
972 bugmatch = self.bug_re.search(ctx.description(), start)
972 bugmatch = self.bug_re.search(ctx.description(), start)
973 fixmatch = self.fix_re.search(ctx.description(), start)
973 fixmatch = self.fix_re.search(ctx.description(), start)
974 while True:
974 while True:
975 bugattribs = {}
975 bugattribs = {}
976 if not bugmatch and not fixmatch:
976 if not bugmatch and not fixmatch:
977 break
977 break
978 if not bugmatch:
978 if not bugmatch:
979 m = fixmatch
979 m = fixmatch
980 elif not fixmatch:
980 elif not fixmatch:
981 m = bugmatch
981 m = bugmatch
982 else:
982 else:
983 if bugmatch.start() < fixmatch.start():
983 if bugmatch.start() < fixmatch.start():
984 m = bugmatch
984 m = bugmatch
985 else:
985 else:
986 m = fixmatch
986 m = fixmatch
987 start = m.end()
987 start = m.end()
988 if m is bugmatch:
988 if m is bugmatch:
989 bugmatch = self.bug_re.search(ctx.description(), start)
989 bugmatch = self.bug_re.search(ctx.description(), start)
990 if 'fix' in bugattribs:
990 if 'fix' in bugattribs:
991 del bugattribs['fix']
991 del bugattribs['fix']
992 else:
992 else:
993 fixmatch = self.fix_re.search(ctx.description(), start)
993 fixmatch = self.fix_re.search(ctx.description(), start)
994 bugattribs['fix'] = None
994 bugattribs['fix'] = None
995
995
996 try:
996 try:
997 ids = m.group('ids')
997 ids = m.group('ids')
998 except IndexError:
998 except IndexError:
999 ids = m.group(1)
999 ids = m.group(1)
1000 try:
1000 try:
1001 hours = float(m.group('hours'))
1001 hours = float(m.group('hours'))
1002 bugattribs['hours'] = hours
1002 bugattribs['hours'] = hours
1003 except IndexError:
1003 except IndexError:
1004 pass
1004 pass
1005 except TypeError:
1005 except TypeError:
1006 pass
1006 pass
1007 except ValueError:
1007 except ValueError:
1008 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1008 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1009
1009
1010 for id in self.split_re.split(ids):
1010 for id in self.split_re.split(ids):
1011 if not id:
1011 if not id:
1012 continue
1012 continue
1013 bugs[int(id)] = bugattribs
1013 bugs[int(id)] = bugattribs
1014 if bugs:
1014 if bugs:
1015 self.bzdriver.filter_real_bug_ids(bugs)
1015 self.bzdriver.filter_real_bug_ids(bugs)
1016 if bugs:
1016 if bugs:
1017 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1017 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1018 return bugs
1018 return bugs
1019
1019
1020 def update(self, bugid, newstate, ctx):
1020 def update(self, bugid, newstate, ctx):
1021 '''update bugzilla bug with reference to changeset.'''
1021 '''update bugzilla bug with reference to changeset.'''
1022
1022
1023 def webroot(root):
1023 def webroot(root):
1024 '''strip leading prefix of repo root and turn into
1024 '''strip leading prefix of repo root and turn into
1025 url-safe path.'''
1025 url-safe path.'''
1026 count = int(self.ui.config('bugzilla', 'strip', 0))
1026 count = int(self.ui.config('bugzilla', 'strip', 0))
1027 root = util.pconvert(root)
1027 root = util.pconvert(root)
1028 while count > 0:
1028 while count > 0:
1029 c = root.find('/')
1029 c = root.find('/')
1030 if c == -1:
1030 if c == -1:
1031 break
1031 break
1032 root = root[c + 1:]
1032 root = root[c + 1:]
1033 count -= 1
1033 count -= 1
1034 return root
1034 return root
1035
1035
1036 mapfile = None
1036 mapfile = None
1037 tmpl = self.ui.config('bugzilla', 'template')
1037 tmpl = self.ui.config('bugzilla', 'template')
1038 if not tmpl:
1038 if not tmpl:
1039 mapfile = self.ui.config('bugzilla', 'style')
1039 mapfile = self.ui.config('bugzilla', 'style')
1040 if not mapfile and not tmpl:
1040 if not mapfile and not tmpl:
1041 tmpl = _('changeset {node|short} in repo {root} refers '
1041 tmpl = _('changeset {node|short} in repo {root} refers '
1042 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1042 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1043 t = cmdutil.changeset_templater(self.ui, self.repo,
1043 t = cmdutil.changeset_templater(self.ui, self.repo,
1044 False, None, tmpl, mapfile, False)
1044 False, None, tmpl, mapfile, False)
1045 self.ui.pushbuffer()
1045 self.ui.pushbuffer()
1046 t.show(ctx, changes=ctx.changeset(),
1046 t.show(ctx, changes=ctx.changeset(),
1047 bug=str(bugid),
1047 bug=str(bugid),
1048 hgweb=self.ui.config('web', 'baseurl'),
1048 hgweb=self.ui.config('web', 'baseurl'),
1049 root=self.repo.root,
1049 root=self.repo.root,
1050 webroot=webroot(self.repo.root))
1050 webroot=webroot(self.repo.root))
1051 data = self.ui.popbuffer()
1051 data = self.ui.popbuffer()
1052 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1052 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1053
1053
1054 def notify(self, bugs, committer):
1054 def notify(self, bugs, committer):
1055 '''ensure Bugzilla users are notified of bug change.'''
1055 '''ensure Bugzilla users are notified of bug change.'''
1056 self.bzdriver.notify(bugs, committer)
1056 self.bzdriver.notify(bugs, committer)
1057
1057
1058 def hook(ui, repo, hooktype, node=None, **kwargs):
1058 def hook(ui, repo, hooktype, node=None, **kwargs):
1059 '''add comment to bugzilla for each changeset that refers to a
1059 '''add comment to bugzilla for each changeset that refers to a
1060 bugzilla bug id. only add a comment once per bug, so same change
1060 bugzilla bug id. only add a comment once per bug, so same change
1061 seen multiple times does not fill bug with duplicate data.'''
1061 seen multiple times does not fill bug with duplicate data.'''
1062 if node is None:
1062 if node is None:
1063 raise error.Abort(_('hook type %s does not pass a changeset id') %
1063 raise error.Abort(_('hook type %s does not pass a changeset id') %
1064 hooktype)
1064 hooktype)
1065 try:
1065 try:
1066 bz = bugzilla(ui, repo)
1066 bz = bugzilla(ui, repo)
1067 ctx = repo[node]
1067 ctx = repo[node]
1068 bugs = bz.find_bugs(ctx)
1068 bugs = bz.find_bugs(ctx)
1069 if bugs:
1069 if bugs:
1070 for bug in bugs:
1070 for bug in bugs:
1071 bz.update(bug, bugs[bug], ctx)
1071 bz.update(bug, bugs[bug], ctx)
1072 bz.notify(bugs, util.email(ctx.user()))
1072 bz.notify(bugs, util.email(ctx.user()))
1073 except Exception as e:
1073 except Exception as e:
1074 raise error.Abort(_('Bugzilla error: %s') % e)
1074 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,186 +1,186 b''
1 # This software may be used and distributed according to the terms of the
1 # This software may be used and distributed according to the terms of the
2 # GNU General Public License version 2 or any later version.
2 # GNU General Public License version 2 or any later version.
3
3
4 """advertise pre-generated bundles to seed clones
4 """advertise pre-generated bundles to seed clones
5
5
6 "clonebundles" is a server-side extension used to advertise the existence
6 "clonebundles" is a server-side extension used to advertise the existence
7 of pre-generated, externally hosted bundle files to clients that are
7 of pre-generated, externally hosted bundle files to clients that are
8 cloning so that cloning can be faster, more reliable, and require less
8 cloning so that cloning can be faster, more reliable, and require less
9 resources on the server.
9 resources on the server.
10
10
11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
12 the server, in response to a client's request to clone, dynamically generates
12 the server, in response to a client's request to clone, dynamically generates
13 a bundle containing the entire repository content and sends it to the client.
13 a bundle containing the entire repository content and sends it to the client.
14 There is no caching on the server and the server will have to redundantly
14 There is no caching on the server and the server will have to redundantly
15 generate the same outgoing bundle in response to each clone request. For
15 generate the same outgoing bundle in response to each clone request. For
16 servers with large repositories or with high clone volume, the load from
16 servers with large repositories or with high clone volume, the load from
17 clones can make scaling the server challenging and costly.
17 clones can make scaling the server challenging and costly.
18
18
19 This extension provides server operators the ability to offload potentially
19 This extension provides server operators the ability to offload potentially
20 expensive clone load to an external service. Here's how it works.
20 expensive clone load to an external service. Here's how it works.
21
21
22 1. A server operator establishes a mechanism for making bundle files available
22 1. A server operator establishes a mechanism for making bundle files available
23 on a hosting service where Mercurial clients can fetch them.
23 on a hosting service where Mercurial clients can fetch them.
24 2. A manifest file listing available bundle URLs and some optional metadata
24 2. A manifest file listing available bundle URLs and some optional metadata
25 is added to the Mercurial repository on the server.
25 is added to the Mercurial repository on the server.
26 3. A client initiates a clone against a clone bundles aware server.
26 3. A client initiates a clone against a clone bundles aware server.
27 4. The client sees the server is advertising clone bundles and fetches the
27 4. The client sees the server is advertising clone bundles and fetches the
28 manifest listing available bundles.
28 manifest listing available bundles.
29 5. The client filters and sorts the available bundles based on what it
29 5. The client filters and sorts the available bundles based on what it
30 supports and prefers.
30 supports and prefers.
31 6. The client downloads and applies an available bundle from the
31 6. The client downloads and applies an available bundle from the
32 server-specified URL.
32 server-specified URL.
33 7. The client reconnects to the original server and performs the equivalent
33 7. The client reconnects to the original server and performs the equivalent
34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
35 repository could have been updated between when the bundle was created
35 repository could have been updated between when the bundle was created
36 and when the client started the clone.)
36 and when the client started the clone.)
37
37
38 Instead of the server generating full repository bundles for every clone
38 Instead of the server generating full repository bundles for every clone
39 request, it generates full bundles once and they are subsequently reused to
39 request, it generates full bundles once and they are subsequently reused to
40 bootstrap new clones. The server may still transfer data at clone time.
40 bootstrap new clones. The server may still transfer data at clone time.
41 However, this is only data that has been added/changed since the bundle was
41 However, this is only data that has been added/changed since the bundle was
42 created. For large, established repositories, this can reduce server load for
42 created. For large, established repositories, this can reduce server load for
43 clones to less than 1% of original.
43 clones to less than 1% of original.
44
44
45 To work, this extension requires the following of server operators:
45 To work, this extension requires the following of server operators:
46
46
47 * Generating bundle files of repository content (typically periodically,
47 * Generating bundle files of repository content (typically periodically,
48 such as once per day).
48 such as once per day).
49 * A file server that clients have network access to and that Python knows
49 * A file server that clients have network access to and that Python knows
50 how to talk to through its normal URL handling facility (typically an
50 how to talk to through its normal URL handling facility (typically an
51 HTTP server).
51 HTTP server).
52 * A process for keeping the bundles manifest in sync with available bundle
52 * A process for keeping the bundles manifest in sync with available bundle
53 files.
53 files.
54
54
55 Strictly speaking, using a static file hosting server isn't required: a server
55 Strictly speaking, using a static file hosting server isn't required: a server
56 operator could use a dynamic service for retrieving bundle data. However,
56 operator could use a dynamic service for retrieving bundle data. However,
57 static file hosting services are simple and scalable and should be sufficient
57 static file hosting services are simple and scalable and should be sufficient
58 for most needs.
58 for most needs.
59
59
60 Bundle files can be generated with the :hg:`bundle` command. Typically
60 Bundle files can be generated with the :hg:`bundle` command. Typically
61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
62
62
63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
64 *streaming clone bundle*. These are bundle files that are extremely efficient
64 *streaming clone bundle*. These are bundle files that are extremely efficient
65 to produce and consume (read: fast). However, they are larger than
65 to produce and consume (read: fast). However, they are larger than
66 traditional bundle formats and require that clients support the exact set
66 traditional bundle formats and require that clients support the exact set
67 of repository data store formats in use by the repository that created them.
67 of repository data store formats in use by the repository that created them.
68 Typically, a newer server can serve data that is compatible with older clients.
68 Typically, a newer server can serve data that is compatible with older clients.
69 However, *streaming clone bundles* don't have this guarantee. **Server
69 However, *streaming clone bundles* don't have this guarantee. **Server
70 operators need to be aware that newer versions of Mercurial may produce
70 operators need to be aware that newer versions of Mercurial may produce
71 streaming clone bundles incompatible with older Mercurial versions.**
71 streaming clone bundles incompatible with older Mercurial versions.**
72
72
73 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
73 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
74 file containing the list of available bundle files suitable for seeding
74 file containing the list of available bundle files suitable for seeding
75 clones. If this file does not exist, the repository will not advertise the
75 clones. If this file does not exist, the repository will not advertise the
76 existence of clone bundles when clients connect.
76 existence of clone bundles when clients connect.
77
77
78 The manifest file contains a newline (\n) delimited list of entries.
78 The manifest file contains a newline (\\n) delimited list of entries.
79
79
80 Each line in this file defines an available bundle. Lines have the format:
80 Each line in this file defines an available bundle. Lines have the format:
81
81
82 <URL> [<key>=<value>[ <key>=<value>]]
82 <URL> [<key>=<value>[ <key>=<value>]]
83
83
84 That is, a URL followed by an optional, space-delimited list of key=value
84 That is, a URL followed by an optional, space-delimited list of key=value
85 pairs describing additional properties of this bundle. Both keys and values
85 pairs describing additional properties of this bundle. Both keys and values
86 are URI encoded.
86 are URI encoded.
87
87
88 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
88 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
89 All non-uppercase keys can be used by site installations. An example use
89 All non-uppercase keys can be used by site installations. An example use
90 for custom properties is to use the *datacenter* attribute to define which
90 for custom properties is to use the *datacenter* attribute to define which
91 data center a file is hosted in. Clients could then prefer a server in the
91 data center a file is hosted in. Clients could then prefer a server in the
92 data center closest to them.
92 data center closest to them.
93
93
94 The following reserved keys are currently defined:
94 The following reserved keys are currently defined:
95
95
96 BUNDLESPEC
96 BUNDLESPEC
97 A "bundle specification" string that describes the type of the bundle.
97 A "bundle specification" string that describes the type of the bundle.
98
98
99 These are string values that are accepted by the "--type" argument of
99 These are string values that are accepted by the "--type" argument of
100 :hg:`bundle`.
100 :hg:`bundle`.
101
101
102 The values are parsed in strict mode, which means they must be of the
102 The values are parsed in strict mode, which means they must be of the
103 "<compression>-<type>" form. See
103 "<compression>-<type>" form. See
104 mercurial.exchange.parsebundlespec() for more details.
104 mercurial.exchange.parsebundlespec() for more details.
105
105
106 :hg:`debugbundle --spec` can be used to print the bundle specification
106 :hg:`debugbundle --spec` can be used to print the bundle specification
107 string for a bundle file. The output of this command can be used verbatim
107 string for a bundle file. The output of this command can be used verbatim
108 for the value of ``BUNDLESPEC`` (it is already escaped).
108 for the value of ``BUNDLESPEC`` (it is already escaped).
109
109
110 Clients will automatically filter out specifications that are unknown or
110 Clients will automatically filter out specifications that are unknown or
111 unsupported so they won't attempt to download something that likely won't
111 unsupported so they won't attempt to download something that likely won't
112 apply.
112 apply.
113
113
114 The actual value doesn't impact client behavior beyond filtering:
114 The actual value doesn't impact client behavior beyond filtering:
115 clients will still sniff the bundle type from the header of downloaded
115 clients will still sniff the bundle type from the header of downloaded
116 files.
116 files.
117
117
118 **Use of this key is highly recommended**, as it allows clients to
118 **Use of this key is highly recommended**, as it allows clients to
119 easily skip unsupported bundles. If this key is not defined, an old
119 easily skip unsupported bundles. If this key is not defined, an old
120 client may attempt to apply a bundle that it is incapable of reading.
120 client may attempt to apply a bundle that it is incapable of reading.
121
121
122 REQUIRESNI
122 REQUIRESNI
123 Whether Server Name Indication (SNI) is required to connect to the URL.
123 Whether Server Name Indication (SNI) is required to connect to the URL.
124 SNI allows servers to use multiple certificates on the same IP. It is
124 SNI allows servers to use multiple certificates on the same IP. It is
125 somewhat common in CDNs and other hosting providers. Older Python
125 somewhat common in CDNs and other hosting providers. Older Python
126 versions do not support SNI. Defining this attribute enables clients
126 versions do not support SNI. Defining this attribute enables clients
127 with older Python versions to filter this entry without experiencing
127 with older Python versions to filter this entry without experiencing
128 an opaque SSL failure at connection time.
128 an opaque SSL failure at connection time.
129
129
130 If this is defined, it is important to advertise a non-SNI fallback
130 If this is defined, it is important to advertise a non-SNI fallback
131 URL or clients running old Python releases may not be able to clone
131 URL or clients running old Python releases may not be able to clone
132 with the clonebundles facility.
132 with the clonebundles facility.
133
133
134 Value should be "true".
134 Value should be "true".
135
135
136 Manifests can contain multiple entries. Assuming metadata is defined, clients
136 Manifests can contain multiple entries. Assuming metadata is defined, clients
137 will filter entries from the manifest that they don't support. The remaining
137 will filter entries from the manifest that they don't support. The remaining
138 entries are optionally sorted by client preferences
138 entries are optionally sorted by client preferences
139 (``experimental.clonebundleprefers`` config option). The client then attempts
139 (``experimental.clonebundleprefers`` config option). The client then attempts
140 to fetch the bundle at the first URL in the remaining list.
140 to fetch the bundle at the first URL in the remaining list.
141
141
142 **Errors when downloading a bundle will fail the entire clone operation:
142 **Errors when downloading a bundle will fail the entire clone operation:
143 clients do not automatically fall back to a traditional clone.** The reason
143 clients do not automatically fall back to a traditional clone.** The reason
144 for this is that if a server is using clone bundles, it is probably doing so
144 for this is that if a server is using clone bundles, it is probably doing so
145 because the feature is necessary to help it scale. In other words, there
145 because the feature is necessary to help it scale. In other words, there
146 is an assumption that clone load will be offloaded to another service and
146 is an assumption that clone load will be offloaded to another service and
147 that the Mercurial server isn't responsible for serving this clone load.
147 that the Mercurial server isn't responsible for serving this clone load.
148 If that other service experiences issues and clients start mass falling back to
148 If that other service experiences issues and clients start mass falling back to
149 the original Mercurial server, the added clone load could overwhelm the server
149 the original Mercurial server, the added clone load could overwhelm the server
150 due to unexpected load and effectively take it offline. Not having clients
150 due to unexpected load and effectively take it offline. Not having clients
151 automatically fall back to cloning from the original server mitigates this
151 automatically fall back to cloning from the original server mitigates this
152 scenario.
152 scenario.
153
153
154 Because there is no automatic Mercurial server fallback on failure of the
154 Because there is no automatic Mercurial server fallback on failure of the
155 bundle hosting service, it is important for server operators to view the bundle
155 bundle hosting service, it is important for server operators to view the bundle
156 hosting service as an extension of the Mercurial server in terms of
156 hosting service as an extension of the Mercurial server in terms of
157 availability and service level agreements: if the bundle hosting service goes
157 availability and service level agreements: if the bundle hosting service goes
158 down, so does the ability for clients to clone. Note: clients will see a
158 down, so does the ability for clients to clone. Note: clients will see a
159 message informing them how to bypass the clone bundles facility when a failure
159 message informing them how to bypass the clone bundles facility when a failure
160 occurs. So server operators should prepare for some people to follow these
160 occurs. So server operators should prepare for some people to follow these
161 instructions when a failure occurs, thus driving more load to the original
161 instructions when a failure occurs, thus driving more load to the original
162 Mercurial server when the bundle hosting service fails.
162 Mercurial server when the bundle hosting service fails.
163 """
163 """
164
164
165 from __future__ import absolute_import
165 from __future__ import absolute_import
166
166
167 from mercurial import (
167 from mercurial import (
168 extensions,
168 extensions,
169 wireproto,
169 wireproto,
170 )
170 )
171
171
172 testedwith = 'ships-with-hg-core'
172 testedwith = 'ships-with-hg-core'
173
173
174 def capabilities(orig, repo, proto):
174 def capabilities(orig, repo, proto):
175 caps = orig(repo, proto)
175 caps = orig(repo, proto)
176
176
177 # Only advertise if a manifest exists. This does add some I/O to requests.
177 # Only advertise if a manifest exists. This does add some I/O to requests.
178 # But this should be cheaper than a wasted network round trip due to
178 # But this should be cheaper than a wasted network round trip due to
179 # missing file.
179 # missing file.
180 if repo.vfs.exists('clonebundles.manifest'):
180 if repo.vfs.exists('clonebundles.manifest'):
181 caps.append('clonebundles')
181 caps.append('clonebundles')
182
182
183 return caps
183 return caps
184
184
185 def extsetup(ui):
185 def extsetup(ui):
186 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
186 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
@@ -1,196 +1,196 b''
1 # win32mbcs.py -- MBCS filename support for Mercurial
1 # win32mbcs.py -- MBCS filename support for Mercurial
2 #
2 #
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 #
4 #
5 # Version: 0.3
5 # Version: 0.3
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10 #
10 #
11
11
12 '''allow the use of MBCS paths with problematic encodings
12 '''allow the use of MBCS paths with problematic encodings
13
13
14 Some MBCS encodings are not good for some path operations (i.e.
14 Some MBCS encodings are not good for some path operations (i.e.
15 splitting path, case conversion, etc.) with its encoded bytes. We call
15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 This extension can be used to fix the issue with those encodings by
17 This extension can be used to fix the issue with those encodings by
18 wrapping some functions to convert to Unicode string before path
18 wrapping some functions to convert to Unicode string before path
19 operation.
19 operation.
20
20
21 This extension is useful for:
21 This extension is useful for:
22
22
23 - Japanese Windows users using shift_jis encoding.
23 - Japanese Windows users using shift_jis encoding.
24 - Chinese Windows users using big5 encoding.
24 - Chinese Windows users using big5 encoding.
25 - All users who use a repository with one of problematic encodings on
25 - All users who use a repository with one of problematic encodings on
26 case-insensitive file system.
26 case-insensitive file system.
27
27
28 This extension is not needed for:
28 This extension is not needed for:
29
29
30 - Any user who use only ASCII chars in path.
30 - Any user who use only ASCII chars in path.
31 - Any user who do not use any of problematic encodings.
31 - Any user who do not use any of problematic encodings.
32
32
33 Note that there are some limitations on using this extension:
33 Note that there are some limitations on using this extension:
34
34
35 - You should use single encoding in one repository.
35 - You should use single encoding in one repository.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 - win32mbcs is not compatible with fixutf8 extension.
37 - win32mbcs is not compatible with fixutf8 extension.
38
38
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 You can specify the encoding by config option::
40 You can specify the encoding by config option::
41
41
42 [win32mbcs]
42 [win32mbcs]
43 encoding = sjis
43 encoding = sjis
44
44
45 It is useful for the users who want to commit with UTF-8 log message.
45 It is useful for the users who want to commit with UTF-8 log message.
46 '''
46 '''
47 from __future__ import absolute_import
47 from __future__ import absolute_import
48
48
49 import os
49 import os
50 import sys
50 import sys
51
51
52 from mercurial.i18n import _
52 from mercurial.i18n import _
53 from mercurial import (
53 from mercurial import (
54 encoding,
54 encoding,
55 error,
55 error,
56 pycompat,
56 pycompat,
57 )
57 )
58
58
59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
61 # be specifying the version(s) of Mercurial they are tested with, or
61 # be specifying the version(s) of Mercurial they are tested with, or
62 # leave the attribute unspecified.
62 # leave the attribute unspecified.
63 testedwith = 'ships-with-hg-core'
63 testedwith = 'ships-with-hg-core'
64
64
65 _encoding = None # see extsetup
65 _encoding = None # see extsetup
66
66
67 def decode(arg):
67 def decode(arg):
68 if isinstance(arg, str):
68 if isinstance(arg, str):
69 uarg = arg.decode(_encoding)
69 uarg = arg.decode(_encoding)
70 if arg == uarg.encode(_encoding):
70 if arg == uarg.encode(_encoding):
71 return uarg
71 return uarg
72 raise UnicodeError("Not local encoding")
72 raise UnicodeError("Not local encoding")
73 elif isinstance(arg, tuple):
73 elif isinstance(arg, tuple):
74 return tuple(map(decode, arg))
74 return tuple(map(decode, arg))
75 elif isinstance(arg, list):
75 elif isinstance(arg, list):
76 return map(decode, arg)
76 return map(decode, arg)
77 elif isinstance(arg, dict):
77 elif isinstance(arg, dict):
78 for k, v in arg.items():
78 for k, v in arg.items():
79 arg[k] = decode(v)
79 arg[k] = decode(v)
80 return arg
80 return arg
81
81
82 def encode(arg):
82 def encode(arg):
83 if isinstance(arg, unicode):
83 if isinstance(arg, unicode):
84 return arg.encode(_encoding)
84 return arg.encode(_encoding)
85 elif isinstance(arg, tuple):
85 elif isinstance(arg, tuple):
86 return tuple(map(encode, arg))
86 return tuple(map(encode, arg))
87 elif isinstance(arg, list):
87 elif isinstance(arg, list):
88 return map(encode, arg)
88 return map(encode, arg)
89 elif isinstance(arg, dict):
89 elif isinstance(arg, dict):
90 for k, v in arg.items():
90 for k, v in arg.items():
91 arg[k] = encode(v)
91 arg[k] = encode(v)
92 return arg
92 return arg
93
93
94 def appendsep(s):
94 def appendsep(s):
95 # ensure the path ends with os.sep, appending it if necessary.
95 # ensure the path ends with os.sep, appending it if necessary.
96 try:
96 try:
97 us = decode(s)
97 us = decode(s)
98 except UnicodeError:
98 except UnicodeError:
99 us = s
99 us = s
100 if us and us[-1] not in ':/\\':
100 if us and us[-1] not in ':/\\':
101 s += pycompat.ossep
101 s += pycompat.ossep
102 return s
102 return s
103
103
104
104
105 def basewrapper(func, argtype, enc, dec, args, kwds):
105 def basewrapper(func, argtype, enc, dec, args, kwds):
106 # check check already converted, then call original
106 # check check already converted, then call original
107 for arg in args:
107 for arg in args:
108 if isinstance(arg, argtype):
108 if isinstance(arg, argtype):
109 return func(*args, **kwds)
109 return func(*args, **kwds)
110
110
111 try:
111 try:
112 # convert string arguments, call func, then convert back the
112 # convert string arguments, call func, then convert back the
113 # return value.
113 # return value.
114 return enc(func(*dec(args), **dec(kwds)))
114 return enc(func(*dec(args), **dec(kwds)))
115 except UnicodeError:
115 except UnicodeError:
116 raise error.Abort(_("[win32mbcs] filename conversion failed with"
116 raise error.Abort(_("[win32mbcs] filename conversion failed with"
117 " %s encoding\n") % (_encoding))
117 " %s encoding\n") % (_encoding))
118
118
119 def wrapper(func, args, kwds):
119 def wrapper(func, args, kwds):
120 return basewrapper(func, unicode, encode, decode, args, kwds)
120 return basewrapper(func, unicode, encode, decode, args, kwds)
121
121
122
122
123 def reversewrapper(func, args, kwds):
123 def reversewrapper(func, args, kwds):
124 return basewrapper(func, str, decode, encode, args, kwds)
124 return basewrapper(func, str, decode, encode, args, kwds)
125
125
126 def wrapperforlistdir(func, args, kwds):
126 def wrapperforlistdir(func, args, kwds):
127 # Ensure 'path' argument ends with os.sep to avoids
127 # Ensure 'path' argument ends with os.sep to avoids
128 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
128 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
129 if args:
129 if args:
130 args = list(args)
130 args = list(args)
131 args[0] = appendsep(args[0])
131 args[0] = appendsep(args[0])
132 if 'path' in kwds:
132 if 'path' in kwds:
133 kwds['path'] = appendsep(kwds['path'])
133 kwds['path'] = appendsep(kwds['path'])
134 return func(*args, **kwds)
134 return func(*args, **kwds)
135
135
136 def wrapname(name, wrapper):
136 def wrapname(name, wrapper):
137 module, name = name.rsplit('.', 1)
137 module, name = name.rsplit('.', 1)
138 module = sys.modules[module]
138 module = sys.modules[module]
139 func = getattr(module, name)
139 func = getattr(module, name)
140 def f(*args, **kwds):
140 def f(*args, **kwds):
141 return wrapper(func, args, kwds)
141 return wrapper(func, args, kwds)
142 f.__name__ = func.__name__
142 f.__name__ = func.__name__
143 setattr(module, name, f)
143 setattr(module, name, f)
144
144
145 # List of functions to be wrapped.
145 # List of functions to be wrapped.
146 # NOTE: os.path.dirname() and os.path.basename() are safe because
146 # NOTE: os.path.dirname() and os.path.basename() are safe because
147 # they use result of os.path.split()
147 # they use result of os.path.split()
148 funcs = '''os.path.join os.path.split os.path.splitext
148 funcs = '''os.path.join os.path.split os.path.splitext
149 os.path.normpath os.makedirs mercurial.util.endswithsep
149 os.path.normpath os.makedirs mercurial.util.endswithsep
150 mercurial.util.splitpath mercurial.util.fscasesensitive
150 mercurial.util.splitpath mercurial.util.fscasesensitive
151 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
151 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
152 mercurial.util.checkwinfilename mercurial.util.checkosfilename
152 mercurial.util.checkwinfilename mercurial.util.checkosfilename
153 mercurial.util.split'''
153 mercurial.util.split'''
154
154
155 # These functions are required to be called with local encoded string
155 # These functions are required to be called with local encoded string
156 # because they expects argument is local encoded string and cause
156 # because they expects argument is local encoded string and cause
157 # problem with unicode string.
157 # problem with unicode string.
158 rfuncs = '''mercurial.encoding.upper mercurial.encoding.lower
158 rfuncs = '''mercurial.encoding.upper mercurial.encoding.lower
159 mercurial.pycompat.bytestr'''
159 mercurial.util._filenamebytestr'''
160
160
161 # List of Windows specific functions to be wrapped.
161 # List of Windows specific functions to be wrapped.
162 winfuncs = '''os.path.splitunc'''
162 winfuncs = '''os.path.splitunc'''
163
163
164 # codec and alias names of sjis and big5 to be faked.
164 # codec and alias names of sjis and big5 to be faked.
165 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
165 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
166 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
166 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
167 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
167 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
168 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
168 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
169
169
170 def extsetup(ui):
170 def extsetup(ui):
171 # TODO: decide use of config section for this extension
171 # TODO: decide use of config section for this extension
172 if ((not os.path.supports_unicode_filenames) and
172 if ((not os.path.supports_unicode_filenames) and
173 (pycompat.sysplatform != 'cygwin')):
173 (pycompat.sysplatform != 'cygwin')):
174 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
174 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
175 return
175 return
176 # determine encoding for filename
176 # determine encoding for filename
177 global _encoding
177 global _encoding
178 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
178 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
179 # fake is only for relevant environment.
179 # fake is only for relevant environment.
180 if _encoding.lower() in problematic_encodings.split():
180 if _encoding.lower() in problematic_encodings.split():
181 for f in funcs.split():
181 for f in funcs.split():
182 wrapname(f, wrapper)
182 wrapname(f, wrapper)
183 if pycompat.osname == 'nt':
183 if pycompat.osname == 'nt':
184 for f in winfuncs.split():
184 for f in winfuncs.split():
185 wrapname(f, wrapper)
185 wrapname(f, wrapper)
186 wrapname("mercurial.util.listdir", wrapperforlistdir)
186 wrapname("mercurial.util.listdir", wrapperforlistdir)
187 wrapname("mercurial.windows.listdir", wrapperforlistdir)
187 wrapname("mercurial.windows.listdir", wrapperforlistdir)
188 # wrap functions to be called with local byte string arguments
188 # wrap functions to be called with local byte string arguments
189 for f in rfuncs.split():
189 for f in rfuncs.split():
190 wrapname(f, reversewrapper)
190 wrapname(f, reversewrapper)
191 # Check sys.args manually instead of using ui.debug() because
191 # Check sys.args manually instead of using ui.debug() because
192 # command line options is not yet applied when
192 # command line options is not yet applied when
193 # extensions.loadall() is called.
193 # extensions.loadall() is called.
194 if '--debug' in sys.argv:
194 if '--debug' in sys.argv:
195 ui.write(("[win32mbcs] activated with encoding: %s\n")
195 ui.write(("[win32mbcs] activated with encoding: %s\n")
196 % _encoding)
196 % _encoding)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now