##// END OF EJS Templates
merge with stable
Matt Mackall -
r16197:0196c437 merge default
parent child Browse files
Show More
@@ -1,773 +1,774
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 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011 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 hook does not change bug status.
15 The hook does not change bug status.
16
16
17 Three basic modes of access to Bugzilla are provided:
17 Three basic modes of access to Bugzilla are provided:
18
18
19 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
19 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
20
20
21 2. Check data via the Bugzilla XMLRPC interface and submit bug change
21 2. Check data via the Bugzilla XMLRPC interface and submit bug change
22 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
22 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
23
23
24 3. Writing directly to the Bugzilla database. Only Bugzilla installations
24 3. Writing directly to the Bugzilla database. Only Bugzilla installations
25 using MySQL are supported. Requires Python MySQLdb.
25 using MySQL are supported. Requires Python MySQLdb.
26
26
27 Writing directly to the database is susceptible to schema changes, and
27 Writing directly to the database is susceptible to schema changes, and
28 relies on a Bugzilla contrib script to send out bug change
28 relies on a Bugzilla contrib script to send out bug change
29 notification emails. This script runs as the user running Mercurial,
29 notification emails. This script runs as the user running Mercurial,
30 must be run on the host with the Bugzilla install, and requires
30 must be run on the host with the Bugzilla install, and requires
31 permission to read Bugzilla configuration details and the necessary
31 permission to read Bugzilla configuration details and the necessary
32 MySQL user and password to have full access rights to the Bugzilla
32 MySQL user and password to have full access rights to the Bugzilla
33 database. For these reasons this access mode is now considered
33 database. For these reasons this access mode is now considered
34 deprecated, and will not be updated for new Bugzilla versions going
34 deprecated, and will not be updated for new Bugzilla versions going
35 forward.
35 forward.
36
36
37 Access via XMLRPC needs a Bugzilla username and password to be specified
37 Access via XMLRPC needs a Bugzilla username and password to be specified
38 in the configuration. Comments are added under that username. Since the
38 in the configuration. Comments are added under that username. Since the
39 configuration must be readable by all Mercurial users, it is recommended
39 configuration must be readable by all Mercurial users, it is recommended
40 that the rights of that user are restricted in Bugzilla to the minimum
40 that the rights of that user are restricted in Bugzilla to the minimum
41 necessary to add comments.
41 necessary to add comments.
42
42
43 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
43 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
44 email to the Bugzilla email interface to submit comments to bugs.
44 email to the Bugzilla email interface to submit comments to bugs.
45 The From: address in the email is set to the email address of the Mercurial
45 The From: address in the email is set to the email address of the Mercurial
46 user, so the comment appears to come from the Mercurial user. In the event
46 user, so the comment appears to come from the Mercurial user. In the event
47 that the Mercurial user email is not recognised by Bugzilla as a Bugzilla
47 that the Mercurial user email is not recognised by Bugzilla as a Bugzilla
48 user, the email associated with the Bugzilla username used to log into
48 user, the email associated with the Bugzilla username used to log into
49 Bugzilla is used instead as the source of the comment.
49 Bugzilla is used instead as the source of the comment.
50
50
51 Configuration items common to all access modes:
51 Configuration items common to all access modes:
52
52
53 bugzilla.version
53 bugzilla.version
54 This access type to use. Values recognised are:
54 This access type to use. Values recognised are:
55
55
56 :``xmlrpc``: Bugzilla XMLRPC interface.
56 :``xmlrpc``: Bugzilla XMLRPC interface.
57 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
57 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
58 :``3.0``: MySQL access, Bugzilla 3.0 and later.
58 :``3.0``: MySQL access, Bugzilla 3.0 and later.
59 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
59 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
60 including 3.0.
60 including 3.0.
61 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
61 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
62 including 2.18.
62 including 2.18.
63
63
64 bugzilla.regexp
64 bugzilla.regexp
65 Regular expression to match bug IDs in changeset commit message.
65 Regular expression to match bug IDs in changeset commit message.
66 Must contain one "()" group. The default expression matches ``Bug
66 Must contain one "()" group. The default expression matches ``Bug
67 1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``,
67 1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``,
68 ``Bug 1234 and 5678`` and variations thereof. Matching is case
68 ``Bug 1234 and 5678`` and variations thereof. Matching is case
69 insensitive.
69 insensitive.
70
70
71 bugzilla.style
71 bugzilla.style
72 The style file to use when formatting comments.
72 The style file to use when formatting comments.
73
73
74 bugzilla.template
74 bugzilla.template
75 Template to use when formatting comments. Overrides style if
75 Template to use when formatting comments. Overrides style if
76 specified. In addition to the usual Mercurial keywords, the
76 specified. In addition to the usual Mercurial keywords, the
77 extension specifies:
77 extension specifies:
78
78
79 :``{bug}``: The Bugzilla bug ID.
79 :``{bug}``: The Bugzilla bug ID.
80 :``{root}``: The full pathname of the Mercurial repository.
80 :``{root}``: The full pathname of the Mercurial repository.
81 :``{webroot}``: Stripped pathname of the Mercurial repository.
81 :``{webroot}``: Stripped pathname of the Mercurial repository.
82 :``{hgweb}``: Base URL for browsing Mercurial repositories.
82 :``{hgweb}``: Base URL for browsing Mercurial repositories.
83
83
84 Default ``changeset {node|short} in repo {root} refers to bug
84 Default ``changeset {node|short} in repo {root} refers to bug
85 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
85 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
86
86
87 bugzilla.strip
87 bugzilla.strip
88 The number of path separator characters to strip from the front of
88 The number of path separator characters to strip from the front of
89 the Mercurial repository path (``{root}`` in templates) to produce
89 the Mercurial repository path (``{root}`` in templates) to produce
90 ``{webroot}``. For example, a repository with ``{root}``
90 ``{webroot}``. For example, a repository with ``{root}``
91 ``/var/local/my-project`` with a strip of 2 gives a value for
91 ``/var/local/my-project`` with a strip of 2 gives a value for
92 ``{webroot}`` of ``my-project``. Default 0.
92 ``{webroot}`` of ``my-project``. Default 0.
93
93
94 web.baseurl
94 web.baseurl
95 Base URL for browsing Mercurial repositories. Referenced from
95 Base URL for browsing Mercurial repositories. Referenced from
96 templates as ``{hgweb}``.
96 templates as ``{hgweb}``.
97
97
98 Configuration items common to XMLRPC+email and MySQL access modes:
98 Configuration items common to XMLRPC+email and MySQL access modes:
99
99
100 bugzilla.usermap
100 bugzilla.usermap
101 Path of file containing Mercurial committer email to Bugzilla user email
101 Path of file containing Mercurial committer email to Bugzilla user email
102 mappings. If specified, the file should contain one mapping per
102 mappings. If specified, the file should contain one mapping per
103 line::
103 line::
104
104
105 committer = Bugzilla user
105 committer = Bugzilla user
106
106
107 See also the ``[usermap]`` section.
107 See also the ``[usermap]`` section.
108
108
109 The ``[usermap]`` section is used to specify mappings of Mercurial
109 The ``[usermap]`` section is used to specify mappings of Mercurial
110 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
110 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
111 Contains entries of the form ``committer = Bugzilla user``.
111 Contains entries of the form ``committer = Bugzilla user``.
112
112
113 XMLRPC access mode configuration:
113 XMLRPC access mode configuration:
114
114
115 bugzilla.bzurl
115 bugzilla.bzurl
116 The base URL for the Bugzilla installation.
116 The base URL for the Bugzilla installation.
117 Default ``http://localhost/bugzilla``.
117 Default ``http://localhost/bugzilla``.
118
118
119 bugzilla.user
119 bugzilla.user
120 The username to use to log into Bugzilla via XMLRPC. Default
120 The username to use to log into Bugzilla via XMLRPC. Default
121 ``bugs``.
121 ``bugs``.
122
122
123 bugzilla.password
123 bugzilla.password
124 The password for Bugzilla login.
124 The password for Bugzilla login.
125
125
126 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
126 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
127 and also:
127 and also:
128
128
129 bugzilla.bzemail
129 bugzilla.bzemail
130 The Bugzilla email address.
130 The Bugzilla email address.
131
131
132 In addition, the Mercurial email settings must be configured. See the
132 In addition, the Mercurial email settings must be configured. See the
133 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
133 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
134
134
135 MySQL access mode configuration:
135 MySQL access mode configuration:
136
136
137 bugzilla.host
137 bugzilla.host
138 Hostname of the MySQL server holding the Bugzilla database.
138 Hostname of the MySQL server holding the Bugzilla database.
139 Default ``localhost``.
139 Default ``localhost``.
140
140
141 bugzilla.db
141 bugzilla.db
142 Name of the Bugzilla database in MySQL. Default ``bugs``.
142 Name of the Bugzilla database in MySQL. Default ``bugs``.
143
143
144 bugzilla.user
144 bugzilla.user
145 Username to use to access MySQL server. Default ``bugs``.
145 Username to use to access MySQL server. Default ``bugs``.
146
146
147 bugzilla.password
147 bugzilla.password
148 Password to use to access MySQL server.
148 Password to use to access MySQL server.
149
149
150 bugzilla.timeout
150 bugzilla.timeout
151 Database connection timeout (seconds). Default 5.
151 Database connection timeout (seconds). Default 5.
152
152
153 bugzilla.bzuser
153 bugzilla.bzuser
154 Fallback Bugzilla user name to record comments with, if changeset
154 Fallback Bugzilla user name to record comments with, if changeset
155 committer cannot be found as a Bugzilla user.
155 committer cannot be found as a Bugzilla user.
156
156
157 bugzilla.bzdir
157 bugzilla.bzdir
158 Bugzilla install directory. Used by default notify. Default
158 Bugzilla install directory. Used by default notify. Default
159 ``/var/www/html/bugzilla``.
159 ``/var/www/html/bugzilla``.
160
160
161 bugzilla.notify
161 bugzilla.notify
162 The command to run to get Bugzilla to send bug change notification
162 The command to run to get Bugzilla to send bug change notification
163 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
163 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
164 id) and ``user`` (committer bugzilla email). Default depends on
164 id) and ``user`` (committer bugzilla email). Default depends on
165 version; from 2.18 it is "cd %(bzdir)s && perl -T
165 version; from 2.18 it is "cd %(bzdir)s && perl -T
166 contrib/sendbugmail.pl %(id)s %(user)s".
166 contrib/sendbugmail.pl %(id)s %(user)s".
167
167
168 Activating the extension::
168 Activating the extension::
169
169
170 [extensions]
170 [extensions]
171 bugzilla =
171 bugzilla =
172
172
173 [hooks]
173 [hooks]
174 # run bugzilla hook on every change pulled or pushed in here
174 # run bugzilla hook on every change pulled or pushed in here
175 incoming.bugzilla = python:hgext.bugzilla.hook
175 incoming.bugzilla = python:hgext.bugzilla.hook
176
176
177 Example configurations:
177 Example configurations:
178
178
179 XMLRPC example configuration. This uses the Bugzilla at
179 XMLRPC example configuration. This uses the Bugzilla at
180 ``http://my-project.org/bugzilla``, logging in as user
180 ``http://my-project.org/bugzilla``, logging in as user
181 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
181 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
182 collection of Mercurial repositories in ``/var/local/hg/repos/``,
182 collection of Mercurial repositories in ``/var/local/hg/repos/``,
183 with a web interface at ``http://my-project.org/hg``. ::
183 with a web interface at ``http://my-project.org/hg``. ::
184
184
185 [bugzilla]
185 [bugzilla]
186 bzurl=http://my-project.org/bugzilla
186 bzurl=http://my-project.org/bugzilla
187 user=bugmail@my-project.org
187 user=bugmail@my-project.org
188 password=plugh
188 password=plugh
189 version=xmlrpc
189 version=xmlrpc
190 template=Changeset {node|short} in {root|basename}.
190 template=Changeset {node|short} in {root|basename}.
191 {hgweb}/{webroot}/rev/{node|short}\\n
191 {hgweb}/{webroot}/rev/{node|short}\\n
192 {desc}\\n
192 {desc}\\n
193 strip=5
193 strip=5
194
194
195 [web]
195 [web]
196 baseurl=http://my-project.org/hg
196 baseurl=http://my-project.org/hg
197
197
198 XMLRPC+email example configuration. This uses the Bugzilla at
198 XMLRPC+email example configuration. This uses the Bugzilla at
199 ``http://my-project.org/bugzilla``, logging in as user
199 ``http://my-project.org/bugzilla``, logging in as user
200 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
200 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
201 collection of Mercurial repositories in ``/var/local/hg/repos/``,
201 collection of Mercurial repositories in ``/var/local/hg/repos/``,
202 with a web interface at ``http://my-project.org/hg``. Bug comments
202 with a web interface at ``http://my-project.org/hg``. Bug comments
203 are sent to the Bugzilla email address
203 are sent to the Bugzilla email address
204 ``bugzilla@my-project.org``. ::
204 ``bugzilla@my-project.org``. ::
205
205
206 [bugzilla]
206 [bugzilla]
207 bzurl=http://my-project.org/bugzilla
207 bzurl=http://my-project.org/bugzilla
208 user=bugmail@my-project.org
208 user=bugmail@my-project.org
209 password=plugh
209 password=plugh
210 version=xmlrpc
210 version=xmlrpc
211 bzemail=bugzilla@my-project.org
211 bzemail=bugzilla@my-project.org
212 template=Changeset {node|short} in {root|basename}.
212 template=Changeset {node|short} in {root|basename}.
213 {hgweb}/{webroot}/rev/{node|short}\\n
213 {hgweb}/{webroot}/rev/{node|short}\\n
214 {desc}\\n
214 {desc}\\n
215 strip=5
215 strip=5
216
216
217 [web]
217 [web]
218 baseurl=http://my-project.org/hg
218 baseurl=http://my-project.org/hg
219
219
220 [usermap]
220 [usermap]
221 user@emaildomain.com=user.name@bugzilladomain.com
221 user@emaildomain.com=user.name@bugzilladomain.com
222
222
223 MySQL example configuration. This has a local Bugzilla 3.2 installation
223 MySQL example configuration. This has a local Bugzilla 3.2 installation
224 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
224 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
225 the Bugzilla database name is ``bugs`` and MySQL is
225 the Bugzilla database name is ``bugs`` and MySQL is
226 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
226 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
227 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
228 with a web interface at ``http://my-project.org/hg``. ::
228 with a web interface at ``http://my-project.org/hg``. ::
229
229
230 [bugzilla]
230 [bugzilla]
231 host=localhost
231 host=localhost
232 password=XYZZY
232 password=XYZZY
233 version=3.0
233 version=3.0
234 bzuser=unknown@domain.com
234 bzuser=unknown@domain.com
235 bzdir=/opt/bugzilla-3.2
235 bzdir=/opt/bugzilla-3.2
236 template=Changeset {node|short} in {root|basename}.
236 template=Changeset {node|short} in {root|basename}.
237 {hgweb}/{webroot}/rev/{node|short}\\n
237 {hgweb}/{webroot}/rev/{node|short}\\n
238 {desc}\\n
238 {desc}\\n
239 strip=5
239 strip=5
240
240
241 [web]
241 [web]
242 baseurl=http://my-project.org/hg
242 baseurl=http://my-project.org/hg
243
243
244 [usermap]
244 [usermap]
245 user@emaildomain.com=user.name@bugzilladomain.com
245 user@emaildomain.com=user.name@bugzilladomain.com
246
246
247 All the above add a comment to the Bugzilla bug record of the form::
247 All the above add a comment to the Bugzilla bug record of the form::
248
248
249 Changeset 3b16791d6642 in repository-name.
249 Changeset 3b16791d6642 in repository-name.
250 http://my-project.org/hg/repository-name/rev/3b16791d6642
250 http://my-project.org/hg/repository-name/rev/3b16791d6642
251
251
252 Changeset commit comment. Bug 1234.
252 Changeset commit comment. Bug 1234.
253 '''
253 '''
254
254
255 from mercurial.i18n import _
255 from mercurial.i18n import _
256 from mercurial.node import short
256 from mercurial.node import short
257 from mercurial import cmdutil, mail, templater, util
257 from mercurial import cmdutil, mail, templater, util
258 import re, time, urlparse, xmlrpclib
258 import re, time, urlparse, xmlrpclib
259
259
260 class bzaccess(object):
260 class bzaccess(object):
261 '''Base class for access to Bugzilla.'''
261 '''Base class for access to Bugzilla.'''
262
262
263 def __init__(self, ui):
263 def __init__(self, ui):
264 self.ui = ui
264 self.ui = ui
265 usermap = self.ui.config('bugzilla', 'usermap')
265 usermap = self.ui.config('bugzilla', 'usermap')
266 if usermap:
266 if usermap:
267 self.ui.readconfig(usermap, sections=['usermap'])
267 self.ui.readconfig(usermap, sections=['usermap'])
268
268
269 def map_committer(self, user):
269 def map_committer(self, user):
270 '''map name of committer to Bugzilla user name.'''
270 '''map name of committer to Bugzilla user name.'''
271 for committer, bzuser in self.ui.configitems('usermap'):
271 for committer, bzuser in self.ui.configitems('usermap'):
272 if committer.lower() == user.lower():
272 if committer.lower() == user.lower():
273 return bzuser
273 return bzuser
274 return user
274 return user
275
275
276 # Methods to be implemented by access classes.
276 # Methods to be implemented by access classes.
277 def filter_real_bug_ids(self, ids):
277 def filter_real_bug_ids(self, ids):
278 '''remove bug IDs that do not exist in Bugzilla from set.'''
278 '''remove bug IDs that do not exist in Bugzilla from set.'''
279 pass
279 pass
280
280
281 def filter_cset_known_bug_ids(self, node, ids):
281 def filter_cset_known_bug_ids(self, node, ids):
282 '''remove bug IDs where node occurs in comment text from set.'''
282 '''remove bug IDs where node occurs in comment text from set.'''
283 pass
283 pass
284
284
285 def add_comment(self, bugid, text, committer):
285 def add_comment(self, bugid, text, committer):
286 '''add comment to bug.
286 '''add comment to bug.
287
287
288 If possible add the comment as being from the committer of
288 If possible add the comment as being from the committer of
289 the changeset. Otherwise use the default Bugzilla user.
289 the changeset. Otherwise use the default Bugzilla user.
290 '''
290 '''
291 pass
291 pass
292
292
293 def notify(self, ids, committer):
293 def notify(self, ids, committer):
294 '''Force sending of Bugzilla notification emails.'''
294 '''Force sending of Bugzilla notification emails.'''
295 pass
295 pass
296
296
297 # Bugzilla via direct access to MySQL database.
297 # Bugzilla via direct access to MySQL database.
298 class bzmysql(bzaccess):
298 class bzmysql(bzaccess):
299 '''Support for direct MySQL access to Bugzilla.
299 '''Support for direct MySQL access to Bugzilla.
300
300
301 The earliest Bugzilla version this is tested with is version 2.16.
301 The earliest Bugzilla version this is tested with is version 2.16.
302
302
303 If your Bugzilla is version 3.2 or above, you are strongly
303 If your Bugzilla is version 3.2 or above, you are strongly
304 recommended to use the XMLRPC access method instead.
304 recommended to use the XMLRPC access method instead.
305 '''
305 '''
306
306
307 @staticmethod
307 @staticmethod
308 def sql_buglist(ids):
308 def sql_buglist(ids):
309 '''return SQL-friendly list of bug ids'''
309 '''return SQL-friendly list of bug ids'''
310 return '(' + ','.join(map(str, ids)) + ')'
310 return '(' + ','.join(map(str, ids)) + ')'
311
311
312 _MySQLdb = None
312 _MySQLdb = None
313
313
314 def __init__(self, ui):
314 def __init__(self, ui):
315 try:
315 try:
316 import MySQLdb as mysql
316 import MySQLdb as mysql
317 bzmysql._MySQLdb = mysql
317 bzmysql._MySQLdb = mysql
318 except ImportError, err:
318 except ImportError, err:
319 raise util.Abort(_('python mysql support not available: %s') % err)
319 raise util.Abort(_('python mysql support not available: %s') % err)
320
320
321 bzaccess.__init__(self, ui)
321 bzaccess.__init__(self, ui)
322
322
323 host = self.ui.config('bugzilla', 'host', 'localhost')
323 host = self.ui.config('bugzilla', 'host', 'localhost')
324 user = self.ui.config('bugzilla', 'user', 'bugs')
324 user = self.ui.config('bugzilla', 'user', 'bugs')
325 passwd = self.ui.config('bugzilla', 'password')
325 passwd = self.ui.config('bugzilla', 'password')
326 db = self.ui.config('bugzilla', 'db', 'bugs')
326 db = self.ui.config('bugzilla', 'db', 'bugs')
327 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
327 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
328 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
328 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
329 (host, db, user, '*' * len(passwd)))
329 (host, db, user, '*' * len(passwd)))
330 self.conn = bzmysql._MySQLdb.connect(host=host,
330 self.conn = bzmysql._MySQLdb.connect(host=host,
331 user=user, passwd=passwd,
331 user=user, passwd=passwd,
332 db=db,
332 db=db,
333 connect_timeout=timeout)
333 connect_timeout=timeout)
334 self.cursor = self.conn.cursor()
334 self.cursor = self.conn.cursor()
335 self.longdesc_id = self.get_longdesc_id()
335 self.longdesc_id = self.get_longdesc_id()
336 self.user_ids = {}
336 self.user_ids = {}
337 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
337 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
338
338
339 def run(self, *args, **kwargs):
339 def run(self, *args, **kwargs):
340 '''run a query.'''
340 '''run a query.'''
341 self.ui.note(_('query: %s %s\n') % (args, kwargs))
341 self.ui.note(_('query: %s %s\n') % (args, kwargs))
342 try:
342 try:
343 self.cursor.execute(*args, **kwargs)
343 self.cursor.execute(*args, **kwargs)
344 except bzmysql._MySQLdb.MySQLError:
344 except bzmysql._MySQLdb.MySQLError:
345 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
345 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
346 raise
346 raise
347
347
348 def get_longdesc_id(self):
348 def get_longdesc_id(self):
349 '''get identity of longdesc field'''
349 '''get identity of longdesc field'''
350 self.run('select fieldid from fielddefs where name = "longdesc"')
350 self.run('select fieldid from fielddefs where name = "longdesc"')
351 ids = self.cursor.fetchall()
351 ids = self.cursor.fetchall()
352 if len(ids) != 1:
352 if len(ids) != 1:
353 raise util.Abort(_('unknown database schema'))
353 raise util.Abort(_('unknown database schema'))
354 return ids[0][0]
354 return ids[0][0]
355
355
356 def filter_real_bug_ids(self, ids):
356 def filter_real_bug_ids(self, ids):
357 '''filter not-existing bug ids from set.'''
357 '''filter not-existing bug ids from set.'''
358 self.run('select bug_id from bugs where bug_id in %s' %
358 self.run('select bug_id from bugs where bug_id in %s' %
359 bzmysql.sql_buglist(ids))
359 bzmysql.sql_buglist(ids))
360 return set([c[0] for c in self.cursor.fetchall()])
360 return set([c[0] for c in self.cursor.fetchall()])
361
361
362 def filter_cset_known_bug_ids(self, node, ids):
362 def filter_cset_known_bug_ids(self, node, ids):
363 '''filter bug ids that already refer to this changeset from set.'''
363 '''filter bug ids that already refer to this changeset from set.'''
364
364
365 self.run('''select bug_id from longdescs where
365 self.run('''select bug_id from longdescs where
366 bug_id in %s and thetext like "%%%s%%"''' %
366 bug_id in %s and thetext like "%%%s%%"''' %
367 (bzmysql.sql_buglist(ids), short(node)))
367 (bzmysql.sql_buglist(ids), short(node)))
368 for (id,) in self.cursor.fetchall():
368 for (id,) in self.cursor.fetchall():
369 self.ui.status(_('bug %d already knows about changeset %s\n') %
369 self.ui.status(_('bug %d already knows about changeset %s\n') %
370 (id, short(node)))
370 (id, short(node)))
371 ids.discard(id)
371 ids.discard(id)
372 return ids
372 return ids
373
373
374 def notify(self, ids, committer):
374 def notify(self, ids, committer):
375 '''tell bugzilla to send mail.'''
375 '''tell bugzilla to send mail.'''
376
376
377 self.ui.status(_('telling bugzilla to send mail:\n'))
377 self.ui.status(_('telling bugzilla to send mail:\n'))
378 (user, userid) = self.get_bugzilla_user(committer)
378 (user, userid) = self.get_bugzilla_user(committer)
379 for id in ids:
379 for id in ids:
380 self.ui.status(_(' bug %s\n') % id)
380 self.ui.status(_(' bug %s\n') % id)
381 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
381 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
382 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
382 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
383 try:
383 try:
384 # Backwards-compatible with old notify string, which
384 # Backwards-compatible with old notify string, which
385 # took one string. This will throw with a new format
385 # took one string. This will throw with a new format
386 # string.
386 # string.
387 cmd = cmdfmt % id
387 cmd = cmdfmt % id
388 except TypeError:
388 except TypeError:
389 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
389 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
390 self.ui.note(_('running notify command %s\n') % cmd)
390 self.ui.note(_('running notify command %s\n') % cmd)
391 fp = util.popen('(%s) 2>&1' % cmd)
391 fp = util.popen('(%s) 2>&1' % cmd)
392 out = fp.read()
392 out = fp.read()
393 ret = fp.close()
393 ret = fp.close()
394 if ret:
394 if ret:
395 self.ui.warn(out)
395 self.ui.warn(out)
396 raise util.Abort(_('bugzilla notify command %s') %
396 raise util.Abort(_('bugzilla notify command %s') %
397 util.explainexit(ret)[0])
397 util.explainexit(ret)[0])
398 self.ui.status(_('done\n'))
398 self.ui.status(_('done\n'))
399
399
400 def get_user_id(self, user):
400 def get_user_id(self, user):
401 '''look up numeric bugzilla user id.'''
401 '''look up numeric bugzilla user id.'''
402 try:
402 try:
403 return self.user_ids[user]
403 return self.user_ids[user]
404 except KeyError:
404 except KeyError:
405 try:
405 try:
406 userid = int(user)
406 userid = int(user)
407 except ValueError:
407 except ValueError:
408 self.ui.note(_('looking up user %s\n') % user)
408 self.ui.note(_('looking up user %s\n') % user)
409 self.run('''select userid from profiles
409 self.run('''select userid from profiles
410 where login_name like %s''', user)
410 where login_name like %s''', user)
411 all = self.cursor.fetchall()
411 all = self.cursor.fetchall()
412 if len(all) != 1:
412 if len(all) != 1:
413 raise KeyError(user)
413 raise KeyError(user)
414 userid = int(all[0][0])
414 userid = int(all[0][0])
415 self.user_ids[user] = userid
415 self.user_ids[user] = userid
416 return userid
416 return userid
417
417
418 def get_bugzilla_user(self, committer):
418 def get_bugzilla_user(self, committer):
419 '''See if committer is a registered bugzilla user. Return
419 '''See if committer is a registered bugzilla user. Return
420 bugzilla username and userid if so. If not, return default
420 bugzilla username and userid if so. If not, return default
421 bugzilla username and userid.'''
421 bugzilla username and userid.'''
422 user = self.map_committer(committer)
422 user = self.map_committer(committer)
423 try:
423 try:
424 userid = self.get_user_id(user)
424 userid = self.get_user_id(user)
425 except KeyError:
425 except KeyError:
426 try:
426 try:
427 defaultuser = self.ui.config('bugzilla', 'bzuser')
427 defaultuser = self.ui.config('bugzilla', 'bzuser')
428 if not defaultuser:
428 if not defaultuser:
429 raise util.Abort(_('cannot find bugzilla user id for %s') %
429 raise util.Abort(_('cannot find bugzilla user id for %s') %
430 user)
430 user)
431 userid = self.get_user_id(defaultuser)
431 userid = self.get_user_id(defaultuser)
432 user = defaultuser
432 user = defaultuser
433 except KeyError:
433 except KeyError:
434 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
434 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
435 (user, defaultuser))
435 (user, defaultuser))
436 return (user, userid)
436 return (user, userid)
437
437
438 def add_comment(self, bugid, text, committer):
438 def add_comment(self, bugid, text, committer):
439 '''add comment to bug. try adding comment as committer of
439 '''add comment to bug. try adding comment as committer of
440 changeset, otherwise as default bugzilla user.'''
440 changeset, otherwise as default bugzilla user.'''
441 (user, userid) = self.get_bugzilla_user(committer)
441 (user, userid) = self.get_bugzilla_user(committer)
442 now = time.strftime('%Y-%m-%d %H:%M:%S')
442 now = time.strftime('%Y-%m-%d %H:%M:%S')
443 self.run('''insert into longdescs
443 self.run('''insert into longdescs
444 (bug_id, who, bug_when, thetext)
444 (bug_id, who, bug_when, thetext)
445 values (%s, %s, %s, %s)''',
445 values (%s, %s, %s, %s)''',
446 (bugid, userid, now, text))
446 (bugid, userid, now, text))
447 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
447 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
448 values (%s, %s, %s, %s)''',
448 values (%s, %s, %s, %s)''',
449 (bugid, userid, now, self.longdesc_id))
449 (bugid, userid, now, self.longdesc_id))
450 self.conn.commit()
450 self.conn.commit()
451
451
452 class bzmysql_2_18(bzmysql):
452 class bzmysql_2_18(bzmysql):
453 '''support for bugzilla 2.18 series.'''
453 '''support for bugzilla 2.18 series.'''
454
454
455 def __init__(self, ui):
455 def __init__(self, ui):
456 bzmysql.__init__(self, ui)
456 bzmysql.__init__(self, ui)
457 self.default_notify = \
457 self.default_notify = \
458 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
458 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
459
459
460 class bzmysql_3_0(bzmysql_2_18):
460 class bzmysql_3_0(bzmysql_2_18):
461 '''support for bugzilla 3.0 series.'''
461 '''support for bugzilla 3.0 series.'''
462
462
463 def __init__(self, ui):
463 def __init__(self, ui):
464 bzmysql_2_18.__init__(self, ui)
464 bzmysql_2_18.__init__(self, ui)
465
465
466 def get_longdesc_id(self):
466 def get_longdesc_id(self):
467 '''get identity of longdesc field'''
467 '''get identity of longdesc field'''
468 self.run('select id from fielddefs where name = "longdesc"')
468 self.run('select id from fielddefs where name = "longdesc"')
469 ids = self.cursor.fetchall()
469 ids = self.cursor.fetchall()
470 if len(ids) != 1:
470 if len(ids) != 1:
471 raise util.Abort(_('unknown database schema'))
471 raise util.Abort(_('unknown database schema'))
472 return ids[0][0]
472 return ids[0][0]
473
473
474 # Buzgilla via XMLRPC interface.
474 # Buzgilla via XMLRPC interface.
475
475
476 class cookietransportrequest(object):
476 class cookietransportrequest(object):
477 """A Transport request method that retains cookies over its lifetime.
477 """A Transport request method that retains cookies over its lifetime.
478
478
479 The regular xmlrpclib transports ignore cookies. Which causes
479 The regular xmlrpclib transports ignore cookies. Which causes
480 a bit of a problem when you need a cookie-based login, as with
480 a bit of a problem when you need a cookie-based login, as with
481 the Bugzilla XMLRPC interface.
481 the Bugzilla XMLRPC interface.
482
482
483 So this is a helper for defining a Transport which looks for
483 So this is a helper for defining a Transport which looks for
484 cookies being set in responses and saves them to add to all future
484 cookies being set in responses and saves them to add to all future
485 requests.
485 requests.
486 """
486 """
487
487
488 # Inspiration drawn from
488 # Inspiration drawn from
489 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
489 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
490 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
490 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
491
491
492 cookies = []
492 cookies = []
493 def send_cookies(self, connection):
493 def send_cookies(self, connection):
494 if self.cookies:
494 if self.cookies:
495 for cookie in self.cookies:
495 for cookie in self.cookies:
496 connection.putheader("Cookie", cookie)
496 connection.putheader("Cookie", cookie)
497
497
498 def request(self, host, handler, request_body, verbose=0):
498 def request(self, host, handler, request_body, verbose=0):
499 self.verbose = verbose
499 self.verbose = verbose
500 self.accept_gzip_encoding = False
500
501
501 # issue XML-RPC request
502 # issue XML-RPC request
502 h = self.make_connection(host)
503 h = self.make_connection(host)
503 if verbose:
504 if verbose:
504 h.set_debuglevel(1)
505 h.set_debuglevel(1)
505
506
506 self.send_request(h, handler, request_body)
507 self.send_request(h, handler, request_body)
507 self.send_host(h, host)
508 self.send_host(h, host)
508 self.send_cookies(h)
509 self.send_cookies(h)
509 self.send_user_agent(h)
510 self.send_user_agent(h)
510 self.send_content(h, request_body)
511 self.send_content(h, request_body)
511
512
512 # Deal with differences between Python 2.4-2.6 and 2.7.
513 # Deal with differences between Python 2.4-2.6 and 2.7.
513 # In the former h is a HTTP(S). In the latter it's a
514 # In the former h is a HTTP(S). In the latter it's a
514 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
515 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
515 # HTTP(S) has an underlying HTTP(S)Connection, so extract
516 # HTTP(S) has an underlying HTTP(S)Connection, so extract
516 # that and use it.
517 # that and use it.
517 try:
518 try:
518 response = h.getresponse()
519 response = h.getresponse()
519 except AttributeError:
520 except AttributeError:
520 response = h._conn.getresponse()
521 response = h._conn.getresponse()
521
522
522 # Add any cookie definitions to our list.
523 # Add any cookie definitions to our list.
523 for header in response.msg.getallmatchingheaders("Set-Cookie"):
524 for header in response.msg.getallmatchingheaders("Set-Cookie"):
524 val = header.split(": ", 1)[1]
525 val = header.split(": ", 1)[1]
525 cookie = val.split(";", 1)[0]
526 cookie = val.split(";", 1)[0]
526 self.cookies.append(cookie)
527 self.cookies.append(cookie)
527
528
528 if response.status != 200:
529 if response.status != 200:
529 raise xmlrpclib.ProtocolError(host + handler, response.status,
530 raise xmlrpclib.ProtocolError(host + handler, response.status,
530 response.reason, response.msg.headers)
531 response.reason, response.msg.headers)
531
532
532 payload = response.read()
533 payload = response.read()
533 parser, unmarshaller = self.getparser()
534 parser, unmarshaller = self.getparser()
534 parser.feed(payload)
535 parser.feed(payload)
535 parser.close()
536 parser.close()
536
537
537 return unmarshaller.close()
538 return unmarshaller.close()
538
539
539 # The explicit calls to the underlying xmlrpclib __init__() methods are
540 # The explicit calls to the underlying xmlrpclib __init__() methods are
540 # necessary. The xmlrpclib.Transport classes are old-style classes, and
541 # necessary. The xmlrpclib.Transport classes are old-style classes, and
541 # it turns out their __init__() doesn't get called when doing multiple
542 # it turns out their __init__() doesn't get called when doing multiple
542 # inheritance with a new-style class.
543 # inheritance with a new-style class.
543 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
544 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
544 def __init__(self, use_datetime=0):
545 def __init__(self, use_datetime=0):
545 xmlrpclib.Transport.__init__(self, use_datetime)
546 xmlrpclib.Transport.__init__(self, use_datetime)
546
547
547 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
548 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
548 def __init__(self, use_datetime=0):
549 def __init__(self, use_datetime=0):
549 xmlrpclib.SafeTransport.__init__(self, use_datetime)
550 xmlrpclib.SafeTransport.__init__(self, use_datetime)
550
551
551 class bzxmlrpc(bzaccess):
552 class bzxmlrpc(bzaccess):
552 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
553 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
553
554
554 Requires a minimum Bugzilla version 3.4.
555 Requires a minimum Bugzilla version 3.4.
555 """
556 """
556
557
557 def __init__(self, ui):
558 def __init__(self, ui):
558 bzaccess.__init__(self, ui)
559 bzaccess.__init__(self, ui)
559
560
560 bzweb = self.ui.config('bugzilla', 'bzurl',
561 bzweb = self.ui.config('bugzilla', 'bzurl',
561 'http://localhost/bugzilla/')
562 'http://localhost/bugzilla/')
562 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
563 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
563
564
564 user = self.ui.config('bugzilla', 'user', 'bugs')
565 user = self.ui.config('bugzilla', 'user', 'bugs')
565 passwd = self.ui.config('bugzilla', 'password')
566 passwd = self.ui.config('bugzilla', 'password')
566
567
567 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
568 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
568 self.bzproxy.User.login(dict(login=user, password=passwd))
569 self.bzproxy.User.login(dict(login=user, password=passwd))
569
570
570 def transport(self, uri):
571 def transport(self, uri):
571 if urlparse.urlparse(uri, "http")[0] == "https":
572 if urlparse.urlparse(uri, "http")[0] == "https":
572 return cookiesafetransport()
573 return cookiesafetransport()
573 else:
574 else:
574 return cookietransport()
575 return cookietransport()
575
576
576 def get_bug_comments(self, id):
577 def get_bug_comments(self, id):
577 """Return a string with all comment text for a bug."""
578 """Return a string with all comment text for a bug."""
578 c = self.bzproxy.Bug.comments(dict(ids=[id]))
579 c = self.bzproxy.Bug.comments(dict(ids=[id]))
579 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
580 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
580
581
581 def filter_real_bug_ids(self, ids):
582 def filter_real_bug_ids(self, ids):
582 res = set()
583 res = set()
583 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
584 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
584 for bug in bugs['bugs']:
585 for bug in bugs['bugs']:
585 res.add(bug['id'])
586 res.add(bug['id'])
586 return res
587 return res
587
588
588 def filter_cset_known_bug_ids(self, node, ids):
589 def filter_cset_known_bug_ids(self, node, ids):
589 for id in sorted(ids):
590 for id in sorted(ids):
590 if self.get_bug_comments(id).find(short(node)) != -1:
591 if self.get_bug_comments(id).find(short(node)) != -1:
591 self.ui.status(_('bug %d already knows about changeset %s\n') %
592 self.ui.status(_('bug %d already knows about changeset %s\n') %
592 (id, short(node)))
593 (id, short(node)))
593 ids.discard(id)
594 ids.discard(id)
594 return ids
595 return ids
595
596
596 def add_comment(self, bugid, text, committer):
597 def add_comment(self, bugid, text, committer):
597 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
598 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
598
599
599 class bzxmlrpcemail(bzxmlrpc):
600 class bzxmlrpcemail(bzxmlrpc):
600 """Read data from Bugzilla via XMLRPC, send updates via email.
601 """Read data from Bugzilla via XMLRPC, send updates via email.
601
602
602 Advantages of sending updates via email:
603 Advantages of sending updates via email:
603 1. Comments can be added as any user, not just logged in user.
604 1. Comments can be added as any user, not just logged in user.
604 2. Bug statuses and other fields not accessible via XMLRPC can
605 2. Bug statuses and other fields not accessible via XMLRPC can
605 be updated. This is not currently used.
606 be updated. This is not currently used.
606 """
607 """
607
608
608 def __init__(self, ui):
609 def __init__(self, ui):
609 bzxmlrpc.__init__(self, ui)
610 bzxmlrpc.__init__(self, ui)
610
611
611 self.bzemail = self.ui.config('bugzilla', 'bzemail')
612 self.bzemail = self.ui.config('bugzilla', 'bzemail')
612 if not self.bzemail:
613 if not self.bzemail:
613 raise util.Abort(_("configuration 'bzemail' missing"))
614 raise util.Abort(_("configuration 'bzemail' missing"))
614 mail.validateconfig(self.ui)
615 mail.validateconfig(self.ui)
615
616
616 def send_bug_modify_email(self, bugid, commands, comment, committer):
617 def send_bug_modify_email(self, bugid, commands, comment, committer):
617 '''send modification message to Bugzilla bug via email.
618 '''send modification message to Bugzilla bug via email.
618
619
619 The message format is documented in the Bugzilla email_in.pl
620 The message format is documented in the Bugzilla email_in.pl
620 specification. commands is a list of command lines, comment is the
621 specification. commands is a list of command lines, comment is the
621 comment text.
622 comment text.
622
623
623 To stop users from crafting commit comments with
624 To stop users from crafting commit comments with
624 Bugzilla commands, specify the bug ID via the message body, rather
625 Bugzilla commands, specify the bug ID via the message body, rather
625 than the subject line, and leave a blank line after it.
626 than the subject line, and leave a blank line after it.
626 '''
627 '''
627 user = self.map_committer(committer)
628 user = self.map_committer(committer)
628 matches = self.bzproxy.User.get(dict(match=[user]))
629 matches = self.bzproxy.User.get(dict(match=[user]))
629 if not matches['users']:
630 if not matches['users']:
630 user = self.ui.config('bugzilla', 'user', 'bugs')
631 user = self.ui.config('bugzilla', 'user', 'bugs')
631 matches = self.bzproxy.User.get(dict(match=[user]))
632 matches = self.bzproxy.User.get(dict(match=[user]))
632 if not matches['users']:
633 if not matches['users']:
633 raise util.Abort(_("default bugzilla user %s email not found") %
634 raise util.Abort(_("default bugzilla user %s email not found") %
634 user)
635 user)
635 user = matches['users'][0]['email']
636 user = matches['users'][0]['email']
636
637
637 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
638 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
638
639
639 _charsets = mail._charsets(self.ui)
640 _charsets = mail._charsets(self.ui)
640 user = mail.addressencode(self.ui, user, _charsets)
641 user = mail.addressencode(self.ui, user, _charsets)
641 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
642 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
642 msg = mail.mimeencode(self.ui, text, _charsets)
643 msg = mail.mimeencode(self.ui, text, _charsets)
643 msg['From'] = user
644 msg['From'] = user
644 msg['To'] = bzemail
645 msg['To'] = bzemail
645 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
646 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
646 sendmail = mail.connect(self.ui)
647 sendmail = mail.connect(self.ui)
647 sendmail(user, bzemail, msg.as_string())
648 sendmail(user, bzemail, msg.as_string())
648
649
649 def add_comment(self, bugid, text, committer):
650 def add_comment(self, bugid, text, committer):
650 self.send_bug_modify_email(bugid, [], text, committer)
651 self.send_bug_modify_email(bugid, [], text, committer)
651
652
652 class bugzilla(object):
653 class bugzilla(object):
653 # supported versions of bugzilla. different versions have
654 # supported versions of bugzilla. different versions have
654 # different schemas.
655 # different schemas.
655 _versions = {
656 _versions = {
656 '2.16': bzmysql,
657 '2.16': bzmysql,
657 '2.18': bzmysql_2_18,
658 '2.18': bzmysql_2_18,
658 '3.0': bzmysql_3_0,
659 '3.0': bzmysql_3_0,
659 'xmlrpc': bzxmlrpc,
660 'xmlrpc': bzxmlrpc,
660 'xmlrpc+email': bzxmlrpcemail
661 'xmlrpc+email': bzxmlrpcemail
661 }
662 }
662
663
663 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
664 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
664 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
665 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
665
666
666 _bz = None
667 _bz = None
667
668
668 def __init__(self, ui, repo):
669 def __init__(self, ui, repo):
669 self.ui = ui
670 self.ui = ui
670 self.repo = repo
671 self.repo = repo
671
672
672 def bz(self):
673 def bz(self):
673 '''return object that knows how to talk to bugzilla version in
674 '''return object that knows how to talk to bugzilla version in
674 use.'''
675 use.'''
675
676
676 if bugzilla._bz is None:
677 if bugzilla._bz is None:
677 bzversion = self.ui.config('bugzilla', 'version')
678 bzversion = self.ui.config('bugzilla', 'version')
678 try:
679 try:
679 bzclass = bugzilla._versions[bzversion]
680 bzclass = bugzilla._versions[bzversion]
680 except KeyError:
681 except KeyError:
681 raise util.Abort(_('bugzilla version %s not supported') %
682 raise util.Abort(_('bugzilla version %s not supported') %
682 bzversion)
683 bzversion)
683 bugzilla._bz = bzclass(self.ui)
684 bugzilla._bz = bzclass(self.ui)
684 return bugzilla._bz
685 return bugzilla._bz
685
686
686 def __getattr__(self, key):
687 def __getattr__(self, key):
687 return getattr(self.bz(), key)
688 return getattr(self.bz(), key)
688
689
689 _bug_re = None
690 _bug_re = None
690 _split_re = None
691 _split_re = None
691
692
692 def find_bug_ids(self, ctx):
693 def find_bug_ids(self, ctx):
693 '''return set of integer bug IDs from commit comment.
694 '''return set of integer bug IDs from commit comment.
694
695
695 Extract bug IDs from changeset comments. Filter out any that are
696 Extract bug IDs from changeset comments. Filter out any that are
696 not known to Bugzilla, and any that already have a reference to
697 not known to Bugzilla, and any that already have a reference to
697 the given changeset in their comments.
698 the given changeset in their comments.
698 '''
699 '''
699 if bugzilla._bug_re is None:
700 if bugzilla._bug_re is None:
700 bugzilla._bug_re = re.compile(
701 bugzilla._bug_re = re.compile(
701 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
702 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
702 re.IGNORECASE)
703 re.IGNORECASE)
703 bugzilla._split_re = re.compile(r'\D+')
704 bugzilla._split_re = re.compile(r'\D+')
704 start = 0
705 start = 0
705 ids = set()
706 ids = set()
706 while True:
707 while True:
707 m = bugzilla._bug_re.search(ctx.description(), start)
708 m = bugzilla._bug_re.search(ctx.description(), start)
708 if not m:
709 if not m:
709 break
710 break
710 start = m.end()
711 start = m.end()
711 for id in bugzilla._split_re.split(m.group(1)):
712 for id in bugzilla._split_re.split(m.group(1)):
712 if not id:
713 if not id:
713 continue
714 continue
714 ids.add(int(id))
715 ids.add(int(id))
715 if ids:
716 if ids:
716 ids = self.filter_real_bug_ids(ids)
717 ids = self.filter_real_bug_ids(ids)
717 if ids:
718 if ids:
718 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
719 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
719 return ids
720 return ids
720
721
721 def update(self, bugid, ctx):
722 def update(self, bugid, ctx):
722 '''update bugzilla bug with reference to changeset.'''
723 '''update bugzilla bug with reference to changeset.'''
723
724
724 def webroot(root):
725 def webroot(root):
725 '''strip leading prefix of repo root and turn into
726 '''strip leading prefix of repo root and turn into
726 url-safe path.'''
727 url-safe path.'''
727 count = int(self.ui.config('bugzilla', 'strip', 0))
728 count = int(self.ui.config('bugzilla', 'strip', 0))
728 root = util.pconvert(root)
729 root = util.pconvert(root)
729 while count > 0:
730 while count > 0:
730 c = root.find('/')
731 c = root.find('/')
731 if c == -1:
732 if c == -1:
732 break
733 break
733 root = root[c + 1:]
734 root = root[c + 1:]
734 count -= 1
735 count -= 1
735 return root
736 return root
736
737
737 mapfile = self.ui.config('bugzilla', 'style')
738 mapfile = self.ui.config('bugzilla', 'style')
738 tmpl = self.ui.config('bugzilla', 'template')
739 tmpl = self.ui.config('bugzilla', 'template')
739 t = cmdutil.changeset_templater(self.ui, self.repo,
740 t = cmdutil.changeset_templater(self.ui, self.repo,
740 False, None, mapfile, False)
741 False, None, mapfile, False)
741 if not mapfile and not tmpl:
742 if not mapfile and not tmpl:
742 tmpl = _('changeset {node|short} in repo {root} refers '
743 tmpl = _('changeset {node|short} in repo {root} refers '
743 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
744 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
744 if tmpl:
745 if tmpl:
745 tmpl = templater.parsestring(tmpl, quoted=False)
746 tmpl = templater.parsestring(tmpl, quoted=False)
746 t.use_template(tmpl)
747 t.use_template(tmpl)
747 self.ui.pushbuffer()
748 self.ui.pushbuffer()
748 t.show(ctx, changes=ctx.changeset(),
749 t.show(ctx, changes=ctx.changeset(),
749 bug=str(bugid),
750 bug=str(bugid),
750 hgweb=self.ui.config('web', 'baseurl'),
751 hgweb=self.ui.config('web', 'baseurl'),
751 root=self.repo.root,
752 root=self.repo.root,
752 webroot=webroot(self.repo.root))
753 webroot=webroot(self.repo.root))
753 data = self.ui.popbuffer()
754 data = self.ui.popbuffer()
754 self.add_comment(bugid, data, util.email(ctx.user()))
755 self.add_comment(bugid, data, util.email(ctx.user()))
755
756
756 def hook(ui, repo, hooktype, node=None, **kwargs):
757 def hook(ui, repo, hooktype, node=None, **kwargs):
757 '''add comment to bugzilla for each changeset that refers to a
758 '''add comment to bugzilla for each changeset that refers to a
758 bugzilla bug id. only add a comment once per bug, so same change
759 bugzilla bug id. only add a comment once per bug, so same change
759 seen multiple times does not fill bug with duplicate data.'''
760 seen multiple times does not fill bug with duplicate data.'''
760 if node is None:
761 if node is None:
761 raise util.Abort(_('hook type %s does not pass a changeset id') %
762 raise util.Abort(_('hook type %s does not pass a changeset id') %
762 hooktype)
763 hooktype)
763 try:
764 try:
764 bz = bugzilla(ui, repo)
765 bz = bugzilla(ui, repo)
765 ctx = repo[node]
766 ctx = repo[node]
766 ids = bz.find_bug_ids(ctx)
767 ids = bz.find_bug_ids(ctx)
767 if ids:
768 if ids:
768 for id in ids:
769 for id in ids:
769 bz.update(id, ctx)
770 bz.update(id, ctx)
770 bz.notify(ids, util.email(ctx.user()))
771 bz.notify(ids, util.email(ctx.user()))
771 except Exception, e:
772 except Exception, e:
772 raise util.Abort(_('Bugzilla error: %s') % e)
773 raise util.Abort(_('Bugzilla error: %s') % e)
773
774
@@ -1,241 +1,242
1 # Mercurial bookmark support code
1 # Mercurial bookmark support code
2 #
2 #
3 # Copyright 2008 David Soria Parra <dsp@php.net>
3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from mercurial.i18n import _
8 from mercurial.i18n import _
9 from mercurial.node import hex
9 from mercurial.node import hex
10 from mercurial import encoding, error, util
10 from mercurial import encoding, error, util
11 import errno, os
11 import errno, os
12
12
13 def valid(mark):
13 def valid(mark):
14 for c in (':', '\0', '\n', '\r'):
14 for c in (':', '\0', '\n', '\r'):
15 if c in mark:
15 if c in mark:
16 return False
16 return False
17 return True
17 return True
18
18
19 def read(repo):
19 def read(repo):
20 '''Parse .hg/bookmarks file and return a dictionary
20 '''Parse .hg/bookmarks file and return a dictionary
21
21
22 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
22 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
23 in the .hg/bookmarks file.
23 in the .hg/bookmarks file.
24 Read the file and return a (name=>nodeid) dictionary
24 Read the file and return a (name=>nodeid) dictionary
25 '''
25 '''
26 bookmarks = {}
26 bookmarks = {}
27 try:
27 try:
28 for line in repo.opener('bookmarks'):
28 for line in repo.opener('bookmarks'):
29 line = line.strip()
29 line = line.strip()
30 if not line:
30 if not line:
31 continue
31 continue
32 if ' ' not in line:
32 if ' ' not in line:
33 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n') % line)
33 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n') % line)
34 continue
34 continue
35 sha, refspec = line.split(' ', 1)
35 sha, refspec = line.split(' ', 1)
36 refspec = encoding.tolocal(refspec)
36 refspec = encoding.tolocal(refspec)
37 try:
37 try:
38 bookmarks[refspec] = repo.changelog.lookup(sha)
38 bookmarks[refspec] = repo.changelog.lookup(sha)
39 except error.RepoLookupError:
39 except error.RepoLookupError:
40 pass
40 pass
41 except IOError, inst:
41 except IOError, inst:
42 if inst.errno != errno.ENOENT:
42 if inst.errno != errno.ENOENT:
43 raise
43 raise
44 return bookmarks
44 return bookmarks
45
45
46 def readcurrent(repo):
46 def readcurrent(repo):
47 '''Get the current bookmark
47 '''Get the current bookmark
48
48
49 If we use gittishsh branches we have a current bookmark that
49 If we use gittishsh branches we have a current bookmark that
50 we are on. This function returns the name of the bookmark. It
50 we are on. This function returns the name of the bookmark. It
51 is stored in .hg/bookmarks.current
51 is stored in .hg/bookmarks.current
52 '''
52 '''
53 mark = None
53 mark = None
54 try:
54 try:
55 file = repo.opener('bookmarks.current')
55 file = repo.opener('bookmarks.current')
56 except IOError, inst:
56 except IOError, inst:
57 if inst.errno != errno.ENOENT:
57 if inst.errno != errno.ENOENT:
58 raise
58 raise
59 return None
59 return None
60 try:
60 try:
61 # No readline() in posixfile_nt, reading everything is cheap
61 # No readline() in posixfile_nt, reading everything is cheap
62 mark = encoding.tolocal((file.readlines() or [''])[0])
62 mark = encoding.tolocal((file.readlines() or [''])[0])
63 if mark == '' or mark not in repo._bookmarks:
63 if mark == '' or mark not in repo._bookmarks:
64 mark = None
64 mark = None
65 finally:
65 finally:
66 file.close()
66 file.close()
67 return mark
67 return mark
68
68
69 def write(repo):
69 def write(repo):
70 '''Write bookmarks
70 '''Write bookmarks
71
71
72 Write the given bookmark => hash dictionary to the .hg/bookmarks file
72 Write the given bookmark => hash dictionary to the .hg/bookmarks file
73 in a format equal to those of localtags.
73 in a format equal to those of localtags.
74
74
75 We also store a backup of the previous state in undo.bookmarks that
75 We also store a backup of the previous state in undo.bookmarks that
76 can be copied back on rollback.
76 can be copied back on rollback.
77 '''
77 '''
78 refs = repo._bookmarks
78 refs = repo._bookmarks
79
79
80 if repo._bookmarkcurrent not in refs:
80 if repo._bookmarkcurrent not in refs:
81 setcurrent(repo, None)
81 setcurrent(repo, None)
82 for mark in refs.keys():
82 for mark in refs.keys():
83 if not valid(mark):
83 if not valid(mark):
84 raise util.Abort(_("bookmark '%s' contains illegal "
84 raise util.Abort(_("bookmark '%s' contains illegal "
85 "character" % mark))
85 "character" % mark))
86
86
87 wlock = repo.wlock()
87 wlock = repo.wlock()
88 try:
88 try:
89
89
90 file = repo.opener('bookmarks', 'w', atomictemp=True)
90 file = repo.opener('bookmarks', 'w', atomictemp=True)
91 for refspec, node in refs.iteritems():
91 for refspec, node in refs.iteritems():
92 file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec)))
92 file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec)))
93 file.close()
93 file.close()
94
94
95 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
95 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
96 try:
96 try:
97 os.utime(repo.sjoin('00changelog.i'), None)
97 os.utime(repo.sjoin('00changelog.i'), None)
98 except OSError:
98 except OSError:
99 pass
99 pass
100
100
101 finally:
101 finally:
102 wlock.release()
102 wlock.release()
103
103
104 def setcurrent(repo, mark):
104 def setcurrent(repo, mark):
105 '''Set the name of the bookmark that we are currently on
105 '''Set the name of the bookmark that we are currently on
106
106
107 Set the name of the bookmark that we are on (hg update <bookmark>).
107 Set the name of the bookmark that we are on (hg update <bookmark>).
108 The name is recorded in .hg/bookmarks.current
108 The name is recorded in .hg/bookmarks.current
109 '''
109 '''
110 current = repo._bookmarkcurrent
110 current = repo._bookmarkcurrent
111 if current == mark:
111 if current == mark:
112 return
112 return
113
113
114 if mark not in repo._bookmarks:
114 if mark not in repo._bookmarks:
115 mark = ''
115 mark = ''
116 if not valid(mark):
116 if not valid(mark):
117 raise util.Abort(_("bookmark '%s' contains illegal "
117 raise util.Abort(_("bookmark '%s' contains illegal "
118 "character" % mark))
118 "character" % mark))
119
119
120 wlock = repo.wlock()
120 wlock = repo.wlock()
121 try:
121 try:
122 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
122 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
123 file.write(encoding.fromlocal(mark))
123 file.write(encoding.fromlocal(mark))
124 file.close()
124 file.close()
125 finally:
125 finally:
126 wlock.release()
126 wlock.release()
127 repo._bookmarkcurrent = mark
127 repo._bookmarkcurrent = mark
128
128
129 def unsetcurrent(repo):
129 def unsetcurrent(repo):
130 wlock = repo.wlock()
130 wlock = repo.wlock()
131 try:
131 try:
132 util.unlink(repo.join('bookmarks.current'))
132 try:
133 repo._bookmarkcurrent = None
133 util.unlink(repo.join('bookmarks.current'))
134 except OSError, inst:
134 repo._bookmarkcurrent = None
135 if inst.errno != errno.ENOENT:
135 except OSError, inst:
136 raise
136 if inst.errno != errno.ENOENT:
137 raise
137 finally:
138 finally:
138 wlock.release()
139 wlock.release()
139
140
140 def updatecurrentbookmark(repo, oldnode, curbranch):
141 def updatecurrentbookmark(repo, oldnode, curbranch):
141 try:
142 try:
142 return update(repo, oldnode, repo.branchtags()[curbranch])
143 return update(repo, oldnode, repo.branchtags()[curbranch])
143 except KeyError:
144 except KeyError:
144 if curbranch == "default": # no default branch!
145 if curbranch == "default": # no default branch!
145 return update(repo, oldnode, repo.lookup("tip"))
146 return update(repo, oldnode, repo.lookup("tip"))
146 else:
147 else:
147 raise util.Abort(_("branch %s not found") % curbranch)
148 raise util.Abort(_("branch %s not found") % curbranch)
148
149
149 def update(repo, parents, node):
150 def update(repo, parents, node):
150 marks = repo._bookmarks
151 marks = repo._bookmarks
151 update = False
152 update = False
152 mark = repo._bookmarkcurrent
153 mark = repo._bookmarkcurrent
153 if mark and marks[mark] in parents:
154 if mark and marks[mark] in parents:
154 old = repo[marks[mark]]
155 old = repo[marks[mark]]
155 new = repo[node]
156 new = repo[node]
156 if new in old.descendants():
157 if new in old.descendants():
157 marks[mark] = new.node()
158 marks[mark] = new.node()
158 update = True
159 update = True
159 if update:
160 if update:
160 repo._writebookmarks(marks)
161 repo._writebookmarks(marks)
161 return update
162 return update
162
163
163 def listbookmarks(repo):
164 def listbookmarks(repo):
164 # We may try to list bookmarks on a repo type that does not
165 # We may try to list bookmarks on a repo type that does not
165 # support it (e.g., statichttprepository).
166 # support it (e.g., statichttprepository).
166 marks = getattr(repo, '_bookmarks', {})
167 marks = getattr(repo, '_bookmarks', {})
167
168
168 d = {}
169 d = {}
169 for k, v in marks.iteritems():
170 for k, v in marks.iteritems():
170 # don't expose local divergent bookmarks
171 # don't expose local divergent bookmarks
171 if '@' not in k and not k.endswith('@'):
172 if '@' not in k and not k.endswith('@'):
172 d[k] = hex(v)
173 d[k] = hex(v)
173 return d
174 return d
174
175
175 def pushbookmark(repo, key, old, new):
176 def pushbookmark(repo, key, old, new):
176 w = repo.wlock()
177 w = repo.wlock()
177 try:
178 try:
178 marks = repo._bookmarks
179 marks = repo._bookmarks
179 if hex(marks.get(key, '')) != old:
180 if hex(marks.get(key, '')) != old:
180 return False
181 return False
181 if new == '':
182 if new == '':
182 del marks[key]
183 del marks[key]
183 else:
184 else:
184 if new not in repo:
185 if new not in repo:
185 return False
186 return False
186 marks[key] = repo[new].node()
187 marks[key] = repo[new].node()
187 write(repo)
188 write(repo)
188 return True
189 return True
189 finally:
190 finally:
190 w.release()
191 w.release()
191
192
192 def updatefromremote(ui, repo, remote, path):
193 def updatefromremote(ui, repo, remote, path):
193 ui.debug("checking for updated bookmarks\n")
194 ui.debug("checking for updated bookmarks\n")
194 rb = remote.listkeys('bookmarks')
195 rb = remote.listkeys('bookmarks')
195 changed = False
196 changed = False
196 for k in rb.keys():
197 for k in rb.keys():
197 if k in repo._bookmarks:
198 if k in repo._bookmarks:
198 nr, nl = rb[k], repo._bookmarks[k]
199 nr, nl = rb[k], repo._bookmarks[k]
199 if nr in repo:
200 if nr in repo:
200 cr = repo[nr]
201 cr = repo[nr]
201 cl = repo[nl]
202 cl = repo[nl]
202 if cl.rev() >= cr.rev():
203 if cl.rev() >= cr.rev():
203 continue
204 continue
204 if cr in cl.descendants():
205 if cr in cl.descendants():
205 repo._bookmarks[k] = cr.node()
206 repo._bookmarks[k] = cr.node()
206 changed = True
207 changed = True
207 ui.status(_("updating bookmark %s\n") % k)
208 ui.status(_("updating bookmark %s\n") % k)
208 else:
209 else:
209 # find a unique @ suffix
210 # find a unique @ suffix
210 for x in range(1, 100):
211 for x in range(1, 100):
211 n = '%s@%d' % (k, x)
212 n = '%s@%d' % (k, x)
212 if n not in repo._bookmarks:
213 if n not in repo._bookmarks:
213 break
214 break
214 # try to use an @pathalias suffix
215 # try to use an @pathalias suffix
215 # if an @pathalias already exists, we overwrite (update) it
216 # if an @pathalias already exists, we overwrite (update) it
216 for p, u in ui.configitems("paths"):
217 for p, u in ui.configitems("paths"):
217 if path == u:
218 if path == u:
218 n = '%s@%s' % (k, p)
219 n = '%s@%s' % (k, p)
219
220
220 repo._bookmarks[n] = cr.node()
221 repo._bookmarks[n] = cr.node()
221 changed = True
222 changed = True
222 ui.warn(_("divergent bookmark %s stored as %s\n") % (k, n))
223 ui.warn(_("divergent bookmark %s stored as %s\n") % (k, n))
223
224
224 if changed:
225 if changed:
225 write(repo)
226 write(repo)
226
227
227 def diff(ui, repo, remote):
228 def diff(ui, repo, remote):
228 ui.status(_("searching for changed bookmarks\n"))
229 ui.status(_("searching for changed bookmarks\n"))
229
230
230 lmarks = repo.listkeys('bookmarks')
231 lmarks = repo.listkeys('bookmarks')
231 rmarks = remote.listkeys('bookmarks')
232 rmarks = remote.listkeys('bookmarks')
232
233
233 diff = sorted(set(rmarks) - set(lmarks))
234 diff = sorted(set(rmarks) - set(lmarks))
234 for k in diff:
235 for k in diff:
235 mark = ui.debugflag and rmarks[k] or rmarks[k][:12]
236 mark = ui.debugflag and rmarks[k] or rmarks[k][:12]
236 ui.write(" %-25s %s\n" % (k, mark))
237 ui.write(" %-25s %s\n" % (k, mark))
237
238
238 if len(diff) <= 0:
239 if len(diff) <= 0:
239 ui.status(_("no changed bookmarks found\n"))
240 ui.status(_("no changed bookmarks found\n"))
240 return 1
241 return 1
241 return 0
242 return 0
@@ -1,368 +1,369
1 # bundlerepo.py - repository class for viewing uncompressed bundles
1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 #
2 #
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Repository class for viewing uncompressed bundles.
8 """Repository class for viewing uncompressed bundles.
9
9
10 This provides a read-only repository interface to bundles as if they
10 This provides a read-only repository interface to bundles as if they
11 were part of the actual repository.
11 were part of the actual repository.
12 """
12 """
13
13
14 from node import nullid
14 from node import nullid
15 from i18n import _
15 from i18n import _
16 import os, tempfile, shutil
16 import os, tempfile, shutil
17 import changegroup, util, mdiff, discovery, cmdutil
17 import changegroup, util, mdiff, discovery, cmdutil
18 import localrepo, changelog, manifest, filelog, revlog, error
18 import localrepo, changelog, manifest, filelog, revlog, error
19
19
20 class bundlerevlog(revlog.revlog):
20 class bundlerevlog(revlog.revlog):
21 def __init__(self, opener, indexfile, bundle, linkmapper):
21 def __init__(self, opener, indexfile, bundle, linkmapper):
22 # How it works:
22 # How it works:
23 # to retrieve a revision, we need to know the offset of
23 # to retrieve a revision, we need to know the offset of
24 # the revision in the bundle (an unbundle object).
24 # the revision in the bundle (an unbundle object).
25 #
25 #
26 # We store this offset in the index (start), to differentiate a
26 # We store this offset in the index (start), to differentiate a
27 # rev in the bundle and from a rev in the revlog, we check
27 # rev in the bundle and from a rev in the revlog, we check
28 # len(index[r]). If the tuple is bigger than 7, it is a bundle
28 # len(index[r]). If the tuple is bigger than 7, it is a bundle
29 # (it is bigger since we store the node to which the delta is)
29 # (it is bigger since we store the node to which the delta is)
30 #
30 #
31 revlog.revlog.__init__(self, opener, indexfile)
31 revlog.revlog.__init__(self, opener, indexfile)
32 self.bundle = bundle
32 self.bundle = bundle
33 self.basemap = {}
33 self.basemap = {}
34 n = len(self)
34 n = len(self)
35 chain = None
35 chain = None
36 while True:
36 while True:
37 chunkdata = bundle.deltachunk(chain)
37 chunkdata = bundle.deltachunk(chain)
38 if not chunkdata:
38 if not chunkdata:
39 break
39 break
40 node = chunkdata['node']
40 node = chunkdata['node']
41 p1 = chunkdata['p1']
41 p1 = chunkdata['p1']
42 p2 = chunkdata['p2']
42 p2 = chunkdata['p2']
43 cs = chunkdata['cs']
43 cs = chunkdata['cs']
44 deltabase = chunkdata['deltabase']
44 deltabase = chunkdata['deltabase']
45 delta = chunkdata['delta']
45 delta = chunkdata['delta']
46
46
47 size = len(delta)
47 size = len(delta)
48 start = bundle.tell() - size
48 start = bundle.tell() - size
49
49
50 link = linkmapper(cs)
50 link = linkmapper(cs)
51 if node in self.nodemap:
51 if node in self.nodemap:
52 # this can happen if two branches make the same change
52 # this can happen if two branches make the same change
53 chain = node
53 chain = node
54 continue
54 continue
55
55
56 for p in (p1, p2):
56 for p in (p1, p2):
57 if not p in self.nodemap:
57 if not p in self.nodemap:
58 raise error.LookupError(p, self.indexfile,
58 raise error.LookupError(p, self.indexfile,
59 _("unknown parent"))
59 _("unknown parent"))
60 # start, size, full unc. size, base (unused), link, p1, p2, node
60 # start, size, full unc. size, base (unused), link, p1, p2, node
61 e = (revlog.offset_type(start, 0), size, -1, -1, link,
61 e = (revlog.offset_type(start, 0), size, -1, -1, link,
62 self.rev(p1), self.rev(p2), node)
62 self.rev(p1), self.rev(p2), node)
63 self.basemap[n] = deltabase
63 self.basemap[n] = deltabase
64 self.index.insert(-1, e)
64 self.index.insert(-1, e)
65 self.nodemap[node] = n
65 self.nodemap[node] = n
66 chain = node
66 chain = node
67 n += 1
67 n += 1
68
68
69 def inbundle(self, rev):
69 def inbundle(self, rev):
70 """is rev from the bundle"""
70 """is rev from the bundle"""
71 if rev < 0:
71 if rev < 0:
72 return False
72 return False
73 return rev in self.basemap
73 return rev in self.basemap
74 def bundlebase(self, rev):
74 def bundlebase(self, rev):
75 return self.basemap[rev]
75 return self.basemap[rev]
76 def _chunk(self, rev):
76 def _chunk(self, rev):
77 # Warning: in case of bundle, the diff is against bundlebase,
77 # Warning: in case of bundle, the diff is against bundlebase,
78 # not against rev - 1
78 # not against rev - 1
79 # XXX: could use some caching
79 # XXX: could use some caching
80 if not self.inbundle(rev):
80 if not self.inbundle(rev):
81 return revlog.revlog._chunk(self, rev)
81 return revlog.revlog._chunk(self, rev)
82 self.bundle.seek(self.start(rev))
82 self.bundle.seek(self.start(rev))
83 return self.bundle.read(self.length(rev))
83 return self.bundle.read(self.length(rev))
84
84
85 def revdiff(self, rev1, rev2):
85 def revdiff(self, rev1, rev2):
86 """return or calculate a delta between two revisions"""
86 """return or calculate a delta between two revisions"""
87 if self.inbundle(rev1) and self.inbundle(rev2):
87 if self.inbundle(rev1) and self.inbundle(rev2):
88 # hot path for bundle
88 # hot path for bundle
89 revb = self.rev(self.bundlebase(rev2))
89 revb = self.rev(self.bundlebase(rev2))
90 if revb == rev1:
90 if revb == rev1:
91 return self._chunk(rev2)
91 return self._chunk(rev2)
92 elif not self.inbundle(rev1) and not self.inbundle(rev2):
92 elif not self.inbundle(rev1) and not self.inbundle(rev2):
93 return revlog.revlog.revdiff(self, rev1, rev2)
93 return revlog.revlog.revdiff(self, rev1, rev2)
94
94
95 return mdiff.textdiff(self.revision(self.node(rev1)),
95 return mdiff.textdiff(self.revision(self.node(rev1)),
96 self.revision(self.node(rev2)))
96 self.revision(self.node(rev2)))
97
97
98 def revision(self, node):
98 def revision(self, node):
99 """return an uncompressed revision of a given"""
99 """return an uncompressed revision of a given"""
100 if node == nullid:
100 if node == nullid:
101 return ""
101 return ""
102
102
103 text = None
103 text = None
104 chain = []
104 chain = []
105 iter_node = node
105 iter_node = node
106 rev = self.rev(iter_node)
106 rev = self.rev(iter_node)
107 # reconstruct the revision if it is from a changegroup
107 # reconstruct the revision if it is from a changegroup
108 while self.inbundle(rev):
108 while self.inbundle(rev):
109 if self._cache and self._cache[0] == iter_node:
109 if self._cache and self._cache[0] == iter_node:
110 text = self._cache[2]
110 text = self._cache[2]
111 break
111 break
112 chain.append(rev)
112 chain.append(rev)
113 iter_node = self.bundlebase(rev)
113 iter_node = self.bundlebase(rev)
114 rev = self.rev(iter_node)
114 rev = self.rev(iter_node)
115 if text is None:
115 if text is None:
116 text = revlog.revlog.revision(self, iter_node)
116 text = revlog.revlog.revision(self, iter_node)
117
117
118 while chain:
118 while chain:
119 delta = self._chunk(chain.pop())
119 delta = self._chunk(chain.pop())
120 text = mdiff.patches(text, [delta])
120 text = mdiff.patches(text, [delta])
121
121
122 p1, p2 = self.parents(node)
122 p1, p2 = self.parents(node)
123 if node != revlog.hash(text, p1, p2):
123 if node != revlog.hash(text, p1, p2):
124 raise error.RevlogError(_("integrity check failed on %s:%d")
124 raise error.RevlogError(_("integrity check failed on %s:%d")
125 % (self.datafile, self.rev(node)))
125 % (self.datafile, self.rev(node)))
126
126
127 self._cache = (node, self.rev(node), text)
127 self._cache = (node, self.rev(node), text)
128 return text
128 return text
129
129
130 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
130 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
131 raise NotImplementedError
131 raise NotImplementedError
132 def addgroup(self, revs, linkmapper, transaction):
132 def addgroup(self, revs, linkmapper, transaction):
133 raise NotImplementedError
133 raise NotImplementedError
134 def strip(self, rev, minlink):
134 def strip(self, rev, minlink):
135 raise NotImplementedError
135 raise NotImplementedError
136 def checksize(self):
136 def checksize(self):
137 raise NotImplementedError
137 raise NotImplementedError
138
138
139 class bundlechangelog(bundlerevlog, changelog.changelog):
139 class bundlechangelog(bundlerevlog, changelog.changelog):
140 def __init__(self, opener, bundle):
140 def __init__(self, opener, bundle):
141 changelog.changelog.__init__(self, opener)
141 changelog.changelog.__init__(self, opener)
142 linkmapper = lambda x: x
142 linkmapper = lambda x: x
143 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
143 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
144 linkmapper)
144 linkmapper)
145
145
146 class bundlemanifest(bundlerevlog, manifest.manifest):
146 class bundlemanifest(bundlerevlog, manifest.manifest):
147 def __init__(self, opener, bundle, linkmapper):
147 def __init__(self, opener, bundle, linkmapper):
148 manifest.manifest.__init__(self, opener)
148 manifest.manifest.__init__(self, opener)
149 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
149 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
150 linkmapper)
150 linkmapper)
151
151
152 class bundlefilelog(bundlerevlog, filelog.filelog):
152 class bundlefilelog(bundlerevlog, filelog.filelog):
153 def __init__(self, opener, path, bundle, linkmapper, repo):
153 def __init__(self, opener, path, bundle, linkmapper, repo):
154 filelog.filelog.__init__(self, opener, path)
154 filelog.filelog.__init__(self, opener, path)
155 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
155 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
156 linkmapper)
156 linkmapper)
157 self._repo = repo
157 self._repo = repo
158
158
159 def _file(self, f):
159 def _file(self, f):
160 self._repo.file(f)
160 self._repo.file(f)
161
161
162 class bundlerepository(localrepo.localrepository):
162 class bundlerepository(localrepo.localrepository):
163 def __init__(self, ui, path, bundlename):
163 def __init__(self, ui, path, bundlename):
164 self._tempparent = None
164 self._tempparent = None
165 try:
165 try:
166 localrepo.localrepository.__init__(self, ui, path)
166 localrepo.localrepository.__init__(self, ui, path)
167 except error.RepoError:
167 except error.RepoError:
168 self._tempparent = tempfile.mkdtemp()
168 self._tempparent = tempfile.mkdtemp()
169 localrepo.instance(ui, self._tempparent, 1)
169 localrepo.instance(ui, self._tempparent, 1)
170 localrepo.localrepository.__init__(self, ui, self._tempparent)
170 localrepo.localrepository.__init__(self, ui, self._tempparent)
171 self.ui.setconfig('phases', 'publish', False)
171
172
172 if path:
173 if path:
173 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
174 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
174 else:
175 else:
175 self._url = 'bundle:' + bundlename
176 self._url = 'bundle:' + bundlename
176
177
177 self.tempfile = None
178 self.tempfile = None
178 f = util.posixfile(bundlename, "rb")
179 f = util.posixfile(bundlename, "rb")
179 self.bundle = changegroup.readbundle(f, bundlename)
180 self.bundle = changegroup.readbundle(f, bundlename)
180 if self.bundle.compressed():
181 if self.bundle.compressed():
181 fdtemp, temp = tempfile.mkstemp(prefix="hg-bundle-",
182 fdtemp, temp = tempfile.mkstemp(prefix="hg-bundle-",
182 suffix=".hg10un", dir=self.path)
183 suffix=".hg10un", dir=self.path)
183 self.tempfile = temp
184 self.tempfile = temp
184 fptemp = os.fdopen(fdtemp, 'wb')
185 fptemp = os.fdopen(fdtemp, 'wb')
185
186
186 try:
187 try:
187 fptemp.write("HG10UN")
188 fptemp.write("HG10UN")
188 while True:
189 while True:
189 chunk = self.bundle.read(2**18)
190 chunk = self.bundle.read(2**18)
190 if not chunk:
191 if not chunk:
191 break
192 break
192 fptemp.write(chunk)
193 fptemp.write(chunk)
193 finally:
194 finally:
194 fptemp.close()
195 fptemp.close()
195
196
196 f = util.posixfile(self.tempfile, "rb")
197 f = util.posixfile(self.tempfile, "rb")
197 self.bundle = changegroup.readbundle(f, bundlename)
198 self.bundle = changegroup.readbundle(f, bundlename)
198
199
199 # dict with the mapping 'filename' -> position in the bundle
200 # dict with the mapping 'filename' -> position in the bundle
200 self.bundlefilespos = {}
201 self.bundlefilespos = {}
201
202
202 @util.propertycache
203 @util.propertycache
203 def changelog(self):
204 def changelog(self):
204 # consume the header if it exists
205 # consume the header if it exists
205 self.bundle.changelogheader()
206 self.bundle.changelogheader()
206 c = bundlechangelog(self.sopener, self.bundle)
207 c = bundlechangelog(self.sopener, self.bundle)
207 self.manstart = self.bundle.tell()
208 self.manstart = self.bundle.tell()
208 return c
209 return c
209
210
210 @util.propertycache
211 @util.propertycache
211 def manifest(self):
212 def manifest(self):
212 self.bundle.seek(self.manstart)
213 self.bundle.seek(self.manstart)
213 # consume the header if it exists
214 # consume the header if it exists
214 self.bundle.manifestheader()
215 self.bundle.manifestheader()
215 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
216 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
216 self.filestart = self.bundle.tell()
217 self.filestart = self.bundle.tell()
217 return m
218 return m
218
219
219 @util.propertycache
220 @util.propertycache
220 def manstart(self):
221 def manstart(self):
221 self.changelog
222 self.changelog
222 return self.manstart
223 return self.manstart
223
224
224 @util.propertycache
225 @util.propertycache
225 def filestart(self):
226 def filestart(self):
226 self.manifest
227 self.manifest
227 return self.filestart
228 return self.filestart
228
229
229 def url(self):
230 def url(self):
230 return self._url
231 return self._url
231
232
232 def file(self, f):
233 def file(self, f):
233 if not self.bundlefilespos:
234 if not self.bundlefilespos:
234 self.bundle.seek(self.filestart)
235 self.bundle.seek(self.filestart)
235 while True:
236 while True:
236 chunkdata = self.bundle.filelogheader()
237 chunkdata = self.bundle.filelogheader()
237 if not chunkdata:
238 if not chunkdata:
238 break
239 break
239 fname = chunkdata['filename']
240 fname = chunkdata['filename']
240 self.bundlefilespos[fname] = self.bundle.tell()
241 self.bundlefilespos[fname] = self.bundle.tell()
241 while True:
242 while True:
242 c = self.bundle.deltachunk(None)
243 c = self.bundle.deltachunk(None)
243 if not c:
244 if not c:
244 break
245 break
245
246
246 if f[0] == '/':
247 if f[0] == '/':
247 f = f[1:]
248 f = f[1:]
248 if f in self.bundlefilespos:
249 if f in self.bundlefilespos:
249 self.bundle.seek(self.bundlefilespos[f])
250 self.bundle.seek(self.bundlefilespos[f])
250 return bundlefilelog(self.sopener, f, self.bundle,
251 return bundlefilelog(self.sopener, f, self.bundle,
251 self.changelog.rev, self)
252 self.changelog.rev, self)
252 else:
253 else:
253 return filelog.filelog(self.sopener, f)
254 return filelog.filelog(self.sopener, f)
254
255
255 def close(self):
256 def close(self):
256 """Close assigned bundle file immediately."""
257 """Close assigned bundle file immediately."""
257 self.bundle.close()
258 self.bundle.close()
258 if self.tempfile is not None:
259 if self.tempfile is not None:
259 os.unlink(self.tempfile)
260 os.unlink(self.tempfile)
260 if self._tempparent:
261 if self._tempparent:
261 shutil.rmtree(self._tempparent, True)
262 shutil.rmtree(self._tempparent, True)
262
263
263 def cancopy(self):
264 def cancopy(self):
264 return False
265 return False
265
266
266 def getcwd(self):
267 def getcwd(self):
267 return os.getcwd() # always outside the repo
268 return os.getcwd() # always outside the repo
268
269
269 def _writebranchcache(self, branches, tip, tiprev):
270 def _writebranchcache(self, branches, tip, tiprev):
270 # don't overwrite the disk cache with bundle-augmented data
271 # don't overwrite the disk cache with bundle-augmented data
271 pass
272 pass
272
273
273 def instance(ui, path, create):
274 def instance(ui, path, create):
274 if create:
275 if create:
275 raise util.Abort(_('cannot create new bundle repository'))
276 raise util.Abort(_('cannot create new bundle repository'))
276 parentpath = ui.config("bundle", "mainreporoot", "")
277 parentpath = ui.config("bundle", "mainreporoot", "")
277 if not parentpath:
278 if not parentpath:
278 # try to find the correct path to the working directory repo
279 # try to find the correct path to the working directory repo
279 parentpath = cmdutil.findrepo(os.getcwd())
280 parentpath = cmdutil.findrepo(os.getcwd())
280 if parentpath is None:
281 if parentpath is None:
281 parentpath = ''
282 parentpath = ''
282 if parentpath:
283 if parentpath:
283 # Try to make the full path relative so we get a nice, short URL.
284 # Try to make the full path relative so we get a nice, short URL.
284 # In particular, we don't want temp dir names in test outputs.
285 # In particular, we don't want temp dir names in test outputs.
285 cwd = os.getcwd()
286 cwd = os.getcwd()
286 if parentpath == cwd:
287 if parentpath == cwd:
287 parentpath = ''
288 parentpath = ''
288 else:
289 else:
289 cwd = os.path.join(cwd,'')
290 cwd = os.path.join(cwd,'')
290 if parentpath.startswith(cwd):
291 if parentpath.startswith(cwd):
291 parentpath = parentpath[len(cwd):]
292 parentpath = parentpath[len(cwd):]
292 u = util.url(path)
293 u = util.url(path)
293 path = u.localpath()
294 path = u.localpath()
294 if u.scheme == 'bundle':
295 if u.scheme == 'bundle':
295 s = path.split("+", 1)
296 s = path.split("+", 1)
296 if len(s) == 1:
297 if len(s) == 1:
297 repopath, bundlename = parentpath, s[0]
298 repopath, bundlename = parentpath, s[0]
298 else:
299 else:
299 repopath, bundlename = s
300 repopath, bundlename = s
300 else:
301 else:
301 repopath, bundlename = parentpath, path
302 repopath, bundlename = parentpath, path
302 return bundlerepository(ui, repopath, bundlename)
303 return bundlerepository(ui, repopath, bundlename)
303
304
304 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
305 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
305 force=False):
306 force=False):
306 '''obtains a bundle of changes incoming from other
307 '''obtains a bundle of changes incoming from other
307
308
308 "onlyheads" restricts the returned changes to those reachable from the
309 "onlyheads" restricts the returned changes to those reachable from the
309 specified heads.
310 specified heads.
310 "bundlename", if given, stores the bundle to this file path permanently;
311 "bundlename", if given, stores the bundle to this file path permanently;
311 otherwise it's stored to a temp file and gets deleted again when you call
312 otherwise it's stored to a temp file and gets deleted again when you call
312 the returned "cleanupfn".
313 the returned "cleanupfn".
313 "force" indicates whether to proceed on unrelated repos.
314 "force" indicates whether to proceed on unrelated repos.
314
315
315 Returns a tuple (local, csets, cleanupfn):
316 Returns a tuple (local, csets, cleanupfn):
316
317
317 "local" is a local repo from which to obtain the actual incoming changesets; it
318 "local" is a local repo from which to obtain the actual incoming changesets; it
318 is a bundlerepo for the obtained bundle when the original "other" is remote.
319 is a bundlerepo for the obtained bundle when the original "other" is remote.
319 "csets" lists the incoming changeset node ids.
320 "csets" lists the incoming changeset node ids.
320 "cleanupfn" must be called without arguments when you're done processing the
321 "cleanupfn" must be called without arguments when you're done processing the
321 changes; it closes both the original "other" and the one returned here.
322 changes; it closes both the original "other" and the one returned here.
322 '''
323 '''
323 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads, force=force)
324 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads, force=force)
324 common, incoming, rheads = tmp
325 common, incoming, rheads = tmp
325 if not incoming:
326 if not incoming:
326 try:
327 try:
327 if bundlename:
328 if bundlename:
328 os.unlink(bundlename)
329 os.unlink(bundlename)
329 except OSError:
330 except OSError:
330 pass
331 pass
331 return other, [], other.close
332 return other, [], other.close
332
333
333 bundle = None
334 bundle = None
334 bundlerepo = None
335 bundlerepo = None
335 localrepo = other
336 localrepo = other
336 if bundlename or not other.local():
337 if bundlename or not other.local():
337 # create a bundle (uncompressed if other repo is not local)
338 # create a bundle (uncompressed if other repo is not local)
338
339
339 if other.capable('getbundle'):
340 if other.capable('getbundle'):
340 cg = other.getbundle('incoming', common=common, heads=rheads)
341 cg = other.getbundle('incoming', common=common, heads=rheads)
341 elif onlyheads is None and not other.capable('changegroupsubset'):
342 elif onlyheads is None and not other.capable('changegroupsubset'):
342 # compat with older servers when pulling all remote heads
343 # compat with older servers when pulling all remote heads
343 cg = other.changegroup(incoming, "incoming")
344 cg = other.changegroup(incoming, "incoming")
344 rheads = None
345 rheads = None
345 else:
346 else:
346 cg = other.changegroupsubset(incoming, rheads, 'incoming')
347 cg = other.changegroupsubset(incoming, rheads, 'incoming')
347 bundletype = other.local() and "HG10BZ" or "HG10UN"
348 bundletype = other.local() and "HG10BZ" or "HG10UN"
348 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
349 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
349 # keep written bundle?
350 # keep written bundle?
350 if bundlename:
351 if bundlename:
351 bundle = None
352 bundle = None
352 if not other.local():
353 if not other.local():
353 # use the created uncompressed bundlerepo
354 # use the created uncompressed bundlerepo
354 localrepo = bundlerepo = bundlerepository(ui, repo.root, fname)
355 localrepo = bundlerepo = bundlerepository(ui, repo.root, fname)
355 # this repo contains local and other now, so filter out local again
356 # this repo contains local and other now, so filter out local again
356 common = repo.heads()
357 common = repo.heads()
357
358
358 csets = localrepo.changelog.findmissing(common, rheads)
359 csets = localrepo.changelog.findmissing(common, rheads)
359
360
360 def cleanup():
361 def cleanup():
361 if bundlerepo:
362 if bundlerepo:
362 bundlerepo.close()
363 bundlerepo.close()
363 if bundle:
364 if bundle:
364 os.unlink(bundle)
365 os.unlink(bundle)
365 other.close()
366 other.close()
366
367
367 return (localrepo, csets, cleanup)
368 return (localrepo, csets, cleanup)
368
369
@@ -1,1163 +1,1163
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, scmutil, util, node, error, cmdutil, bookmarks
11 import config, scmutil, util, node, error, cmdutil, bookmarks
12 hg = None
12 hg = None
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 nullstate = ('', '', 'empty')
15 nullstate = ('', '', 'empty')
16
16
17 def state(ctx, ui):
17 def state(ctx, ui):
18 """return a state dict, mapping subrepo paths configured in .hgsub
18 """return a state dict, mapping subrepo paths configured in .hgsub
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 (key in types dict))
20 (key in types dict))
21 """
21 """
22 p = config.config()
22 p = config.config()
23 def read(f, sections=None, remap=None):
23 def read(f, sections=None, remap=None):
24 if f in ctx:
24 if f in ctx:
25 try:
25 try:
26 data = ctx[f].data()
26 data = ctx[f].data()
27 except IOError, err:
27 except IOError, err:
28 if err.errno != errno.ENOENT:
28 if err.errno != errno.ENOENT:
29 raise
29 raise
30 # handle missing subrepo spec files as removed
30 # handle missing subrepo spec files as removed
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 return
32 return
33 p.parse(f, data, sections, remap, read)
33 p.parse(f, data, sections, remap, read)
34 else:
34 else:
35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 raise util.Abort(_("subrepo spec file %s not found") % f)
36
36
37 if '.hgsub' in ctx:
37 if '.hgsub' in ctx:
38 read('.hgsub')
38 read('.hgsub')
39
39
40 for path, src in ui.configitems('subpaths'):
40 for path, src in ui.configitems('subpaths'):
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42
42
43 rev = {}
43 rev = {}
44 if '.hgsubstate' in ctx:
44 if '.hgsubstate' in ctx:
45 try:
45 try:
46 for l in ctx['.hgsubstate'].data().splitlines():
46 for l in ctx['.hgsubstate'].data().splitlines():
47 revision, path = l.split(" ", 1)
47 revision, path = l.split(" ", 1)
48 rev[path] = revision
48 rev[path] = revision
49 except IOError, err:
49 except IOError, err:
50 if err.errno != errno.ENOENT:
50 if err.errno != errno.ENOENT:
51 raise
51 raise
52
52
53 def remap(src):
53 def remap(src):
54 for pattern, repl in p.items('subpaths'):
54 for pattern, repl in p.items('subpaths'):
55 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
55 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
56 # does a string decode.
56 # does a string decode.
57 repl = repl.encode('string-escape')
57 repl = repl.encode('string-escape')
58 # However, we still want to allow back references to go
58 # However, we still want to allow back references to go
59 # through unharmed, so we turn r'\\1' into r'\1'. Again,
59 # through unharmed, so we turn r'\\1' into r'\1'. Again,
60 # extra escapes are needed because re.sub string decodes.
60 # extra escapes are needed because re.sub string decodes.
61 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
61 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
62 try:
62 try:
63 src = re.sub(pattern, repl, src, 1)
63 src = re.sub(pattern, repl, src, 1)
64 except re.error, e:
64 except re.error, e:
65 raise util.Abort(_("bad subrepository pattern in %s: %s")
65 raise util.Abort(_("bad subrepository pattern in %s: %s")
66 % (p.source('subpaths', pattern), e))
66 % (p.source('subpaths', pattern), e))
67 return src
67 return src
68
68
69 state = {}
69 state = {}
70 for path, src in p[''].items():
70 for path, src in p[''].items():
71 kind = 'hg'
71 kind = 'hg'
72 if src.startswith('['):
72 if src.startswith('['):
73 if ']' not in src:
73 if ']' not in src:
74 raise util.Abort(_('missing ] in subrepo source'))
74 raise util.Abort(_('missing ] in subrepo source'))
75 kind, src = src.split(']', 1)
75 kind, src = src.split(']', 1)
76 kind = kind[1:]
76 kind = kind[1:]
77 src = src.lstrip() # strip any extra whitespace after ']'
77 src = src.lstrip() # strip any extra whitespace after ']'
78
78
79 if not util.url(src).isabs():
79 if not util.url(src).isabs():
80 parent = _abssource(ctx._repo, abort=False)
80 parent = _abssource(ctx._repo, abort=False)
81 if parent:
81 if parent:
82 parent = util.url(parent)
82 parent = util.url(parent)
83 parent.path = posixpath.join(parent.path or '', src)
83 parent.path = posixpath.join(parent.path or '', src)
84 parent.path = posixpath.normpath(parent.path)
84 parent.path = posixpath.normpath(parent.path)
85 joined = str(parent)
85 joined = str(parent)
86 # Remap the full joined path and use it if it changes,
86 # Remap the full joined path and use it if it changes,
87 # else remap the original source.
87 # else remap the original source.
88 remapped = remap(joined)
88 remapped = remap(joined)
89 if remapped == joined:
89 if remapped == joined:
90 src = remap(src)
90 src = remap(src)
91 else:
91 else:
92 src = remapped
92 src = remapped
93
93
94 src = remap(src)
94 src = remap(src)
95 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
95 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
96
96
97 return state
97 return state
98
98
99 def writestate(repo, state):
99 def writestate(repo, state):
100 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
100 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
101 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
101 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
102 repo.wwrite('.hgsubstate', ''.join(lines), '')
102 repo.wwrite('.hgsubstate', ''.join(lines), '')
103
103
104 def submerge(repo, wctx, mctx, actx, overwrite):
104 def submerge(repo, wctx, mctx, actx, overwrite):
105 """delegated from merge.applyupdates: merging of .hgsubstate file
105 """delegated from merge.applyupdates: merging of .hgsubstate file
106 in working context, merging context and ancestor context"""
106 in working context, merging context and ancestor context"""
107 if mctx == actx: # backwards?
107 if mctx == actx: # backwards?
108 actx = wctx.p1()
108 actx = wctx.p1()
109 s1 = wctx.substate
109 s1 = wctx.substate
110 s2 = mctx.substate
110 s2 = mctx.substate
111 sa = actx.substate
111 sa = actx.substate
112 sm = {}
112 sm = {}
113
113
114 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
114 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
115
115
116 def debug(s, msg, r=""):
116 def debug(s, msg, r=""):
117 if r:
117 if r:
118 r = "%s:%s:%s" % r
118 r = "%s:%s:%s" % r
119 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
119 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
120
120
121 for s, l in s1.items():
121 for s, l in s1.items():
122 a = sa.get(s, nullstate)
122 a = sa.get(s, nullstate)
123 ld = l # local state with possible dirty flag for compares
123 ld = l # local state with possible dirty flag for compares
124 if wctx.sub(s).dirty():
124 if wctx.sub(s).dirty():
125 ld = (l[0], l[1] + "+")
125 ld = (l[0], l[1] + "+")
126 if wctx == actx: # overwrite
126 if wctx == actx: # overwrite
127 a = ld
127 a = ld
128
128
129 if s in s2:
129 if s in s2:
130 r = s2[s]
130 r = s2[s]
131 if ld == r or r == a: # no change or local is newer
131 if ld == r or r == a: # no change or local is newer
132 sm[s] = l
132 sm[s] = l
133 continue
133 continue
134 elif ld == a: # other side changed
134 elif ld == a: # other side changed
135 debug(s, "other changed, get", r)
135 debug(s, "other changed, get", r)
136 wctx.sub(s).get(r, overwrite)
136 wctx.sub(s).get(r, overwrite)
137 sm[s] = r
137 sm[s] = r
138 elif ld[0] != r[0]: # sources differ
138 elif ld[0] != r[0]: # sources differ
139 if repo.ui.promptchoice(
139 if repo.ui.promptchoice(
140 _(' subrepository sources for %s differ\n'
140 _(' subrepository sources for %s differ\n'
141 'use (l)ocal source (%s) or (r)emote source (%s)?')
141 'use (l)ocal source (%s) or (r)emote source (%s)?')
142 % (s, l[0], r[0]),
142 % (s, l[0], r[0]),
143 (_('&Local'), _('&Remote')), 0):
143 (_('&Local'), _('&Remote')), 0):
144 debug(s, "prompt changed, get", r)
144 debug(s, "prompt changed, get", r)
145 wctx.sub(s).get(r, overwrite)
145 wctx.sub(s).get(r, overwrite)
146 sm[s] = r
146 sm[s] = r
147 elif ld[1] == a[1]: # local side is unchanged
147 elif ld[1] == a[1]: # local side is unchanged
148 debug(s, "other side changed, get", r)
148 debug(s, "other side changed, get", r)
149 wctx.sub(s).get(r, overwrite)
149 wctx.sub(s).get(r, overwrite)
150 sm[s] = r
150 sm[s] = r
151 else:
151 else:
152 debug(s, "both sides changed, merge with", r)
152 debug(s, "both sides changed, merge with", r)
153 wctx.sub(s).merge(r)
153 wctx.sub(s).merge(r)
154 sm[s] = l
154 sm[s] = l
155 elif ld == a: # remote removed, local unchanged
155 elif ld == a: # remote removed, local unchanged
156 debug(s, "remote removed, remove")
156 debug(s, "remote removed, remove")
157 wctx.sub(s).remove()
157 wctx.sub(s).remove()
158 elif a == nullstate: # not present in remote or ancestor
158 elif a == nullstate: # not present in remote or ancestor
159 debug(s, "local added, keep")
159 debug(s, "local added, keep")
160 sm[s] = l
160 sm[s] = l
161 continue
161 continue
162 else:
162 else:
163 if repo.ui.promptchoice(
163 if repo.ui.promptchoice(
164 _(' local changed subrepository %s which remote removed\n'
164 _(' local changed subrepository %s which remote removed\n'
165 'use (c)hanged version or (d)elete?') % s,
165 'use (c)hanged version or (d)elete?') % s,
166 (_('&Changed'), _('&Delete')), 0):
166 (_('&Changed'), _('&Delete')), 0):
167 debug(s, "prompt remove")
167 debug(s, "prompt remove")
168 wctx.sub(s).remove()
168 wctx.sub(s).remove()
169
169
170 for s, r in sorted(s2.items()):
170 for s, r in sorted(s2.items()):
171 if s in s1:
171 if s in s1:
172 continue
172 continue
173 elif s not in sa:
173 elif s not in sa:
174 debug(s, "remote added, get", r)
174 debug(s, "remote added, get", r)
175 mctx.sub(s).get(r)
175 mctx.sub(s).get(r)
176 sm[s] = r
176 sm[s] = r
177 elif r != sa[s]:
177 elif r != sa[s]:
178 if repo.ui.promptchoice(
178 if repo.ui.promptchoice(
179 _(' remote changed subrepository %s which local removed\n'
179 _(' remote changed subrepository %s which local removed\n'
180 'use (c)hanged version or (d)elete?') % s,
180 'use (c)hanged version or (d)elete?') % s,
181 (_('&Changed'), _('&Delete')), 0) == 0:
181 (_('&Changed'), _('&Delete')), 0) == 0:
182 debug(s, "prompt recreate", r)
182 debug(s, "prompt recreate", r)
183 wctx.sub(s).get(r)
183 wctx.sub(s).get(r)
184 sm[s] = r
184 sm[s] = r
185
185
186 # record merged .hgsubstate
186 # record merged .hgsubstate
187 writestate(repo, sm)
187 writestate(repo, sm)
188
188
189 def _updateprompt(ui, sub, dirty, local, remote):
189 def _updateprompt(ui, sub, dirty, local, remote):
190 if dirty:
190 if dirty:
191 msg = (_(' subrepository sources for %s differ\n'
191 msg = (_(' subrepository sources for %s differ\n'
192 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
192 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
193 % (subrelpath(sub), local, remote))
193 % (subrelpath(sub), local, remote))
194 else:
194 else:
195 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
195 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
196 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
196 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
197 % (subrelpath(sub), local, remote))
197 % (subrelpath(sub), local, remote))
198 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
198 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
199
199
200 def reporelpath(repo):
200 def reporelpath(repo):
201 """return path to this (sub)repo as seen from outermost repo"""
201 """return path to this (sub)repo as seen from outermost repo"""
202 parent = repo
202 parent = repo
203 while util.safehasattr(parent, '_subparent'):
203 while util.safehasattr(parent, '_subparent'):
204 parent = parent._subparent
204 parent = parent._subparent
205 p = parent.root.rstrip(os.sep)
205 p = parent.root.rstrip(os.sep)
206 return repo.root[len(p) + 1:]
206 return repo.root[len(p) + 1:]
207
207
208 def subrelpath(sub):
208 def subrelpath(sub):
209 """return path to this subrepo as seen from outermost repo"""
209 """return path to this subrepo as seen from outermost repo"""
210 if util.safehasattr(sub, '_relpath'):
210 if util.safehasattr(sub, '_relpath'):
211 return sub._relpath
211 return sub._relpath
212 if not util.safehasattr(sub, '_repo'):
212 if not util.safehasattr(sub, '_repo'):
213 return sub._path
213 return sub._path
214 return reporelpath(sub._repo)
214 return reporelpath(sub._repo)
215
215
216 def _abssource(repo, push=False, abort=True):
216 def _abssource(repo, push=False, abort=True):
217 """return pull/push path of repo - either based on parent repo .hgsub info
217 """return pull/push path of repo - either based on parent repo .hgsub info
218 or on the top repo config. Abort or return None if no source found."""
218 or on the top repo config. Abort or return None if no source found."""
219 if util.safehasattr(repo, '_subparent'):
219 if util.safehasattr(repo, '_subparent'):
220 source = util.url(repo._subsource)
220 source = util.url(repo._subsource)
221 if source.isabs():
221 if source.isabs():
222 return str(source)
222 return str(source)
223 source.path = posixpath.normpath(source.path)
223 source.path = posixpath.normpath(source.path)
224 parent = _abssource(repo._subparent, push, abort=False)
224 parent = _abssource(repo._subparent, push, abort=False)
225 if parent:
225 if parent:
226 parent = util.url(util.pconvert(parent))
226 parent = util.url(util.pconvert(parent))
227 parent.path = posixpath.join(parent.path or '', source.path)
227 parent.path = posixpath.join(parent.path or '', source.path)
228 parent.path = posixpath.normpath(parent.path)
228 parent.path = posixpath.normpath(parent.path)
229 return str(parent)
229 return str(parent)
230 else: # recursion reached top repo
230 else: # recursion reached top repo
231 if util.safehasattr(repo, '_subtoppath'):
231 if util.safehasattr(repo, '_subtoppath'):
232 return repo._subtoppath
232 return repo._subtoppath
233 if push and repo.ui.config('paths', 'default-push'):
233 if push and repo.ui.config('paths', 'default-push'):
234 return repo.ui.config('paths', 'default-push')
234 return repo.ui.config('paths', 'default-push')
235 if repo.ui.config('paths', 'default'):
235 if repo.ui.config('paths', 'default'):
236 return repo.ui.config('paths', 'default')
236 return repo.ui.config('paths', 'default')
237 if abort:
237 if abort:
238 raise util.Abort(_("default path for subrepository %s not found") %
238 raise util.Abort(_("default path for subrepository %s not found") %
239 reporelpath(repo))
239 reporelpath(repo))
240
240
241 def itersubrepos(ctx1, ctx2):
241 def itersubrepos(ctx1, ctx2):
242 """find subrepos in ctx1 or ctx2"""
242 """find subrepos in ctx1 or ctx2"""
243 # Create a (subpath, ctx) mapping where we prefer subpaths from
243 # Create a (subpath, ctx) mapping where we prefer subpaths from
244 # ctx1. The subpaths from ctx2 are important when the .hgsub file
244 # ctx1. The subpaths from ctx2 are important when the .hgsub file
245 # has been modified (in ctx2) but not yet committed (in ctx1).
245 # has been modified (in ctx2) but not yet committed (in ctx1).
246 subpaths = dict.fromkeys(ctx2.substate, ctx2)
246 subpaths = dict.fromkeys(ctx2.substate, ctx2)
247 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
247 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
248 for subpath, ctx in sorted(subpaths.iteritems()):
248 for subpath, ctx in sorted(subpaths.iteritems()):
249 yield subpath, ctx.sub(subpath)
249 yield subpath, ctx.sub(subpath)
250
250
251 def subrepo(ctx, path):
251 def subrepo(ctx, path):
252 """return instance of the right subrepo class for subrepo in path"""
252 """return instance of the right subrepo class for subrepo in path"""
253 # subrepo inherently violates our import layering rules
253 # subrepo inherently violates our import layering rules
254 # because it wants to make repo objects from deep inside the stack
254 # because it wants to make repo objects from deep inside the stack
255 # so we manually delay the circular imports to not break
255 # so we manually delay the circular imports to not break
256 # scripts that don't use our demand-loading
256 # scripts that don't use our demand-loading
257 global hg
257 global hg
258 import hg as h
258 import hg as h
259 hg = h
259 hg = h
260
260
261 scmutil.pathauditor(ctx._repo.root)(path)
261 scmutil.pathauditor(ctx._repo.root)(path)
262 state = ctx.substate.get(path, nullstate)
262 state = ctx.substate.get(path, nullstate)
263 if state[2] not in types:
263 if state[2] not in types:
264 raise util.Abort(_('unknown subrepo type %s') % state[2])
264 raise util.Abort(_('unknown subrepo type %s') % state[2])
265 return types[state[2]](ctx, path, state[:2])
265 return types[state[2]](ctx, path, state[:2])
266
266
267 # subrepo classes need to implement the following abstract class:
267 # subrepo classes need to implement the following abstract class:
268
268
269 class abstractsubrepo(object):
269 class abstractsubrepo(object):
270
270
271 def dirty(self, ignoreupdate=False):
271 def dirty(self, ignoreupdate=False):
272 """returns true if the dirstate of the subrepo is dirty or does not
272 """returns true if the dirstate of the subrepo is dirty or does not
273 match current stored state. If ignoreupdate is true, only check
273 match current stored state. If ignoreupdate is true, only check
274 whether the subrepo has uncommitted changes in its dirstate.
274 whether the subrepo has uncommitted changes in its dirstate.
275 """
275 """
276 raise NotImplementedError
276 raise NotImplementedError
277
277
278 def basestate(self):
278 def basestate(self):
279 """current working directory base state, disregarding .hgsubstate
279 """current working directory base state, disregarding .hgsubstate
280 state and working directory modifications"""
280 state and working directory modifications"""
281 raise NotImplementedError
281 raise NotImplementedError
282
282
283 def checknested(self, path):
283 def checknested(self, path):
284 """check if path is a subrepository within this repository"""
284 """check if path is a subrepository within this repository"""
285 return False
285 return False
286
286
287 def commit(self, text, user, date):
287 def commit(self, text, user, date):
288 """commit the current changes to the subrepo with the given
288 """commit the current changes to the subrepo with the given
289 log message. Use given user and date if possible. Return the
289 log message. Use given user and date if possible. Return the
290 new state of the subrepo.
290 new state of the subrepo.
291 """
291 """
292 raise NotImplementedError
292 raise NotImplementedError
293
293
294 def remove(self):
294 def remove(self):
295 """remove the subrepo
295 """remove the subrepo
296
296
297 (should verify the dirstate is not dirty first)
297 (should verify the dirstate is not dirty first)
298 """
298 """
299 raise NotImplementedError
299 raise NotImplementedError
300
300
301 def get(self, state, overwrite=False):
301 def get(self, state, overwrite=False):
302 """run whatever commands are needed to put the subrepo into
302 """run whatever commands are needed to put the subrepo into
303 this state
303 this state
304 """
304 """
305 raise NotImplementedError
305 raise NotImplementedError
306
306
307 def merge(self, state):
307 def merge(self, state):
308 """merge currently-saved state with the new state."""
308 """merge currently-saved state with the new state."""
309 raise NotImplementedError
309 raise NotImplementedError
310
310
311 def push(self, opts):
311 def push(self, opts):
312 """perform whatever action is analogous to 'hg push'
312 """perform whatever action is analogous to 'hg push'
313
313
314 This may be a no-op on some systems.
314 This may be a no-op on some systems.
315 """
315 """
316 raise NotImplementedError
316 raise NotImplementedError
317
317
318 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
318 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
319 return []
319 return []
320
320
321 def status(self, rev2, **opts):
321 def status(self, rev2, **opts):
322 return [], [], [], [], [], [], []
322 return [], [], [], [], [], [], []
323
323
324 def diff(self, diffopts, node2, match, prefix, **opts):
324 def diff(self, diffopts, node2, match, prefix, **opts):
325 pass
325 pass
326
326
327 def outgoing(self, ui, dest, opts):
327 def outgoing(self, ui, dest, opts):
328 return 1
328 return 1
329
329
330 def incoming(self, ui, source, opts):
330 def incoming(self, ui, source, opts):
331 return 1
331 return 1
332
332
333 def files(self):
333 def files(self):
334 """return filename iterator"""
334 """return filename iterator"""
335 raise NotImplementedError
335 raise NotImplementedError
336
336
337 def filedata(self, name):
337 def filedata(self, name):
338 """return file data"""
338 """return file data"""
339 raise NotImplementedError
339 raise NotImplementedError
340
340
341 def fileflags(self, name):
341 def fileflags(self, name):
342 """return file flags"""
342 """return file flags"""
343 return ''
343 return ''
344
344
345 def archive(self, ui, archiver, prefix):
345 def archive(self, ui, archiver, prefix):
346 files = self.files()
346 files = self.files()
347 total = len(files)
347 total = len(files)
348 relpath = subrelpath(self)
348 relpath = subrelpath(self)
349 ui.progress(_('archiving (%s)') % relpath, 0,
349 ui.progress(_('archiving (%s)') % relpath, 0,
350 unit=_('files'), total=total)
350 unit=_('files'), total=total)
351 for i, name in enumerate(files):
351 for i, name in enumerate(files):
352 flags = self.fileflags(name)
352 flags = self.fileflags(name)
353 mode = 'x' in flags and 0755 or 0644
353 mode = 'x' in flags and 0755 or 0644
354 symlink = 'l' in flags
354 symlink = 'l' in flags
355 archiver.addfile(os.path.join(prefix, self._path, name),
355 archiver.addfile(os.path.join(prefix, self._path, name),
356 mode, symlink, self.filedata(name))
356 mode, symlink, self.filedata(name))
357 ui.progress(_('archiving (%s)') % relpath, i + 1,
357 ui.progress(_('archiving (%s)') % relpath, i + 1,
358 unit=_('files'), total=total)
358 unit=_('files'), total=total)
359 ui.progress(_('archiving (%s)') % relpath, None)
359 ui.progress(_('archiving (%s)') % relpath, None)
360
360
361 def walk(self, match):
361 def walk(self, match):
362 '''
362 '''
363 walk recursively through the directory tree, finding all files
363 walk recursively through the directory tree, finding all files
364 matched by the match function
364 matched by the match function
365 '''
365 '''
366 pass
366 pass
367
367
368 def forget(self, ui, match, prefix):
368 def forget(self, ui, match, prefix):
369 return []
369 return []
370
370
371 class hgsubrepo(abstractsubrepo):
371 class hgsubrepo(abstractsubrepo):
372 def __init__(self, ctx, path, state):
372 def __init__(self, ctx, path, state):
373 self._path = path
373 self._path = path
374 self._state = state
374 self._state = state
375 r = ctx._repo
375 r = ctx._repo
376 root = r.wjoin(path)
376 root = r.wjoin(path)
377 create = False
377 create = False
378 if not os.path.exists(os.path.join(root, '.hg')):
378 if not os.path.exists(os.path.join(root, '.hg')):
379 create = True
379 create = True
380 util.makedirs(root)
380 util.makedirs(root)
381 self._repo = hg.repository(r.ui, root, create=create)
381 self._repo = hg.repository(r.ui, root, create=create)
382 self._initrepo(r, state[0], create)
382 self._initrepo(r, state[0], create)
383
383
384 def _initrepo(self, parentrepo, source, create):
384 def _initrepo(self, parentrepo, source, create):
385 self._repo._subparent = parentrepo
385 self._repo._subparent = parentrepo
386 self._repo._subsource = source
386 self._repo._subsource = source
387
387
388 if create:
388 if create:
389 fp = self._repo.opener("hgrc", "w", text=True)
389 fp = self._repo.opener("hgrc", "w", text=True)
390 fp.write('[paths]\n')
390 fp.write('[paths]\n')
391
391
392 def addpathconfig(key, value):
392 def addpathconfig(key, value):
393 if value:
393 if value:
394 fp.write('%s = %s\n' % (key, value))
394 fp.write('%s = %s\n' % (key, value))
395 self._repo.ui.setconfig('paths', key, value)
395 self._repo.ui.setconfig('paths', key, value)
396
396
397 defpath = _abssource(self._repo, abort=False)
397 defpath = _abssource(self._repo, abort=False)
398 defpushpath = _abssource(self._repo, True, abort=False)
398 defpushpath = _abssource(self._repo, True, abort=False)
399 addpathconfig('default', defpath)
399 addpathconfig('default', defpath)
400 if defpath != defpushpath:
400 if defpath != defpushpath:
401 addpathconfig('default-push', defpushpath)
401 addpathconfig('default-push', defpushpath)
402 fp.close()
402 fp.close()
403
403
404 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
404 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
405 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
405 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
406 os.path.join(prefix, self._path), explicitonly)
406 os.path.join(prefix, self._path), explicitonly)
407
407
408 def status(self, rev2, **opts):
408 def status(self, rev2, **opts):
409 try:
409 try:
410 rev1 = self._state[1]
410 rev1 = self._state[1]
411 ctx1 = self._repo[rev1]
411 ctx1 = self._repo[rev1]
412 ctx2 = self._repo[rev2]
412 ctx2 = self._repo[rev2]
413 return self._repo.status(ctx1, ctx2, **opts)
413 return self._repo.status(ctx1, ctx2, **opts)
414 except error.RepoLookupError, inst:
414 except error.RepoLookupError, inst:
415 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
415 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
416 % (inst, subrelpath(self)))
416 % (inst, subrelpath(self)))
417 return [], [], [], [], [], [], []
417 return [], [], [], [], [], [], []
418
418
419 def diff(self, diffopts, node2, match, prefix, **opts):
419 def diff(self, diffopts, node2, match, prefix, **opts):
420 try:
420 try:
421 node1 = node.bin(self._state[1])
421 node1 = node.bin(self._state[1])
422 # We currently expect node2 to come from substate and be
422 # We currently expect node2 to come from substate and be
423 # in hex format
423 # in hex format
424 if node2 is not None:
424 if node2 is not None:
425 node2 = node.bin(node2)
425 node2 = node.bin(node2)
426 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
426 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
427 node1, node2, match,
427 node1, node2, match,
428 prefix=os.path.join(prefix, self._path),
428 prefix=os.path.join(prefix, self._path),
429 listsubrepos=True, **opts)
429 listsubrepos=True, **opts)
430 except error.RepoLookupError, inst:
430 except error.RepoLookupError, inst:
431 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
431 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
432 % (inst, subrelpath(self)))
432 % (inst, subrelpath(self)))
433
433
434 def archive(self, ui, archiver, prefix):
434 def archive(self, ui, archiver, prefix):
435 self._get(self._state + ('hg',))
435 self._get(self._state + ('hg',))
436 abstractsubrepo.archive(self, ui, archiver, prefix)
436 abstractsubrepo.archive(self, ui, archiver, prefix)
437
437
438 rev = self._state[1]
438 rev = self._state[1]
439 ctx = self._repo[rev]
439 ctx = self._repo[rev]
440 for subpath in ctx.substate:
440 for subpath in ctx.substate:
441 s = subrepo(ctx, subpath)
441 s = subrepo(ctx, subpath)
442 s.archive(ui, archiver, os.path.join(prefix, self._path))
442 s.archive(ui, archiver, os.path.join(prefix, self._path))
443
443
444 def dirty(self, ignoreupdate=False):
444 def dirty(self, ignoreupdate=False):
445 r = self._state[1]
445 r = self._state[1]
446 if r == '' and not ignoreupdate: # no state recorded
446 if r == '' and not ignoreupdate: # no state recorded
447 return True
447 return True
448 w = self._repo[None]
448 w = self._repo[None]
449 if r != w.p1().hex() and not ignoreupdate:
449 if r != w.p1().hex() and not ignoreupdate:
450 # different version checked out
450 # different version checked out
451 return True
451 return True
452 return w.dirty() # working directory changed
452 return w.dirty() # working directory changed
453
453
454 def basestate(self):
454 def basestate(self):
455 return self._repo['.'].hex()
455 return self._repo['.'].hex()
456
456
457 def checknested(self, path):
457 def checknested(self, path):
458 return self._repo._checknested(self._repo.wjoin(path))
458 return self._repo._checknested(self._repo.wjoin(path))
459
459
460 def commit(self, text, user, date):
460 def commit(self, text, user, date):
461 # don't bother committing in the subrepo if it's only been
461 # don't bother committing in the subrepo if it's only been
462 # updated
462 # updated
463 if not self.dirty(True):
463 if not self.dirty(True):
464 return self._repo['.'].hex()
464 return self._repo['.'].hex()
465 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
465 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
466 n = self._repo.commit(text, user, date)
466 n = self._repo.commit(text, user, date)
467 if not n:
467 if not n:
468 return self._repo['.'].hex() # different version checked out
468 return self._repo['.'].hex() # different version checked out
469 return node.hex(n)
469 return node.hex(n)
470
470
471 def remove(self):
471 def remove(self):
472 # we can't fully delete the repository as it may contain
472 # we can't fully delete the repository as it may contain
473 # local-only history
473 # local-only history
474 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
474 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
475 hg.clean(self._repo, node.nullid, False)
475 hg.clean(self._repo, node.nullid, False)
476
476
477 def _get(self, state):
477 def _get(self, state):
478 source, revision, kind = state
478 source, revision, kind = state
479 if revision not in self._repo:
479 if revision not in self._repo:
480 self._repo._subsource = source
480 self._repo._subsource = source
481 srcurl = _abssource(self._repo)
481 srcurl = _abssource(self._repo)
482 other = hg.peer(self._repo.ui, {}, srcurl)
482 other = hg.peer(self._repo.ui, {}, srcurl)
483 if len(self._repo) == 0:
483 if len(self._repo) == 0:
484 self._repo.ui.status(_('cloning subrepo %s from %s\n')
484 self._repo.ui.status(_('cloning subrepo %s from %s\n')
485 % (subrelpath(self), srcurl))
485 % (subrelpath(self), srcurl))
486 parentrepo = self._repo._subparent
486 parentrepo = self._repo._subparent
487 shutil.rmtree(self._repo.path)
487 shutil.rmtree(self._repo.path)
488 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
488 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
489 self._repo.root, update=False)
489 self._repo.root, update=False)
490 self._initrepo(parentrepo, source, create=True)
490 self._initrepo(parentrepo, source, create=True)
491 else:
491 else:
492 self._repo.ui.status(_('pulling subrepo %s from %s\n')
492 self._repo.ui.status(_('pulling subrepo %s from %s\n')
493 % (subrelpath(self), srcurl))
493 % (subrelpath(self), srcurl))
494 self._repo.pull(other)
494 self._repo.pull(other)
495 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
495 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
496 srcurl)
496 srcurl)
497
497
498 def get(self, state, overwrite=False):
498 def get(self, state, overwrite=False):
499 self._get(state)
499 self._get(state)
500 source, revision, kind = state
500 source, revision, kind = state
501 self._repo.ui.debug("getting subrepo %s\n" % self._path)
501 self._repo.ui.debug("getting subrepo %s\n" % self._path)
502 hg.clean(self._repo, revision, False)
502 hg.clean(self._repo, revision, False)
503
503
504 def merge(self, state):
504 def merge(self, state):
505 self._get(state)
505 self._get(state)
506 cur = self._repo['.']
506 cur = self._repo['.']
507 dst = self._repo[state[1]]
507 dst = self._repo[state[1]]
508 anc = dst.ancestor(cur)
508 anc = dst.ancestor(cur)
509
509
510 def mergefunc():
510 def mergefunc():
511 if anc == cur:
511 if anc == cur and dst.branch() == cur.branch():
512 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
512 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
513 hg.update(self._repo, state[1])
513 hg.update(self._repo, state[1])
514 elif anc == dst:
514 elif anc == dst:
515 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
515 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
516 else:
516 else:
517 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
517 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
518 hg.merge(self._repo, state[1], remind=False)
518 hg.merge(self._repo, state[1], remind=False)
519
519
520 wctx = self._repo[None]
520 wctx = self._repo[None]
521 if self.dirty():
521 if self.dirty():
522 if anc != dst:
522 if anc != dst:
523 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
523 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
524 mergefunc()
524 mergefunc()
525 else:
525 else:
526 mergefunc()
526 mergefunc()
527 else:
527 else:
528 mergefunc()
528 mergefunc()
529
529
530 def push(self, opts):
530 def push(self, opts):
531 force = opts.get('force')
531 force = opts.get('force')
532 newbranch = opts.get('new_branch')
532 newbranch = opts.get('new_branch')
533 ssh = opts.get('ssh')
533 ssh = opts.get('ssh')
534
534
535 # push subrepos depth-first for coherent ordering
535 # push subrepos depth-first for coherent ordering
536 c = self._repo['']
536 c = self._repo['']
537 subs = c.substate # only repos that are committed
537 subs = c.substate # only repos that are committed
538 for s in sorted(subs):
538 for s in sorted(subs):
539 if c.sub(s).push(opts) == 0:
539 if c.sub(s).push(opts) == 0:
540 return False
540 return False
541
541
542 dsturl = _abssource(self._repo, True)
542 dsturl = _abssource(self._repo, True)
543 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
543 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
544 (subrelpath(self), dsturl))
544 (subrelpath(self), dsturl))
545 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
545 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
546 return self._repo.push(other, force, newbranch=newbranch)
546 return self._repo.push(other, force, newbranch=newbranch)
547
547
548 def outgoing(self, ui, dest, opts):
548 def outgoing(self, ui, dest, opts):
549 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
549 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
550
550
551 def incoming(self, ui, source, opts):
551 def incoming(self, ui, source, opts):
552 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
552 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
553
553
554 def files(self):
554 def files(self):
555 rev = self._state[1]
555 rev = self._state[1]
556 ctx = self._repo[rev]
556 ctx = self._repo[rev]
557 return ctx.manifest()
557 return ctx.manifest()
558
558
559 def filedata(self, name):
559 def filedata(self, name):
560 rev = self._state[1]
560 rev = self._state[1]
561 return self._repo[rev][name].data()
561 return self._repo[rev][name].data()
562
562
563 def fileflags(self, name):
563 def fileflags(self, name):
564 rev = self._state[1]
564 rev = self._state[1]
565 ctx = self._repo[rev]
565 ctx = self._repo[rev]
566 return ctx.flags(name)
566 return ctx.flags(name)
567
567
568 def walk(self, match):
568 def walk(self, match):
569 ctx = self._repo[None]
569 ctx = self._repo[None]
570 return ctx.walk(match)
570 return ctx.walk(match)
571
571
572 def forget(self, ui, match, prefix):
572 def forget(self, ui, match, prefix):
573 return cmdutil.forget(ui, self._repo, match,
573 return cmdutil.forget(ui, self._repo, match,
574 os.path.join(prefix, self._path), True)
574 os.path.join(prefix, self._path), True)
575
575
576 class svnsubrepo(abstractsubrepo):
576 class svnsubrepo(abstractsubrepo):
577 def __init__(self, ctx, path, state):
577 def __init__(self, ctx, path, state):
578 self._path = path
578 self._path = path
579 self._state = state
579 self._state = state
580 self._ctx = ctx
580 self._ctx = ctx
581 self._ui = ctx._repo.ui
581 self._ui = ctx._repo.ui
582 self._exe = util.findexe('svn')
582 self._exe = util.findexe('svn')
583 if not self._exe:
583 if not self._exe:
584 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
584 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
585 % self._path)
585 % self._path)
586
586
587 def _svncommand(self, commands, filename='', failok=False):
587 def _svncommand(self, commands, filename='', failok=False):
588 cmd = [self._exe]
588 cmd = [self._exe]
589 extrakw = {}
589 extrakw = {}
590 if not self._ui.interactive():
590 if not self._ui.interactive():
591 # Making stdin be a pipe should prevent svn from behaving
591 # Making stdin be a pipe should prevent svn from behaving
592 # interactively even if we can't pass --non-interactive.
592 # interactively even if we can't pass --non-interactive.
593 extrakw['stdin'] = subprocess.PIPE
593 extrakw['stdin'] = subprocess.PIPE
594 # Starting in svn 1.5 --non-interactive is a global flag
594 # Starting in svn 1.5 --non-interactive is a global flag
595 # instead of being per-command, but we need to support 1.4 so
595 # instead of being per-command, but we need to support 1.4 so
596 # we have to be intelligent about what commands take
596 # we have to be intelligent about what commands take
597 # --non-interactive.
597 # --non-interactive.
598 if commands[0] in ('update', 'checkout', 'commit'):
598 if commands[0] in ('update', 'checkout', 'commit'):
599 cmd.append('--non-interactive')
599 cmd.append('--non-interactive')
600 cmd.extend(commands)
600 cmd.extend(commands)
601 if filename is not None:
601 if filename is not None:
602 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
602 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
603 cmd.append(path)
603 cmd.append(path)
604 env = dict(os.environ)
604 env = dict(os.environ)
605 # Avoid localized output, preserve current locale for everything else.
605 # Avoid localized output, preserve current locale for everything else.
606 env['LC_MESSAGES'] = 'C'
606 env['LC_MESSAGES'] = 'C'
607 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
607 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
608 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
608 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
609 universal_newlines=True, env=env, **extrakw)
609 universal_newlines=True, env=env, **extrakw)
610 stdout, stderr = p.communicate()
610 stdout, stderr = p.communicate()
611 stderr = stderr.strip()
611 stderr = stderr.strip()
612 if not failok:
612 if not failok:
613 if p.returncode:
613 if p.returncode:
614 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
614 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
615 if stderr:
615 if stderr:
616 self._ui.warn(stderr + '\n')
616 self._ui.warn(stderr + '\n')
617 return stdout, stderr
617 return stdout, stderr
618
618
619 @propertycache
619 @propertycache
620 def _svnversion(self):
620 def _svnversion(self):
621 output, err = self._svncommand(['--version'], filename=None)
621 output, err = self._svncommand(['--version'], filename=None)
622 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
622 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
623 if not m:
623 if not m:
624 raise util.Abort(_('cannot retrieve svn tool version'))
624 raise util.Abort(_('cannot retrieve svn tool version'))
625 return (int(m.group(1)), int(m.group(2)))
625 return (int(m.group(1)), int(m.group(2)))
626
626
627 def _wcrevs(self):
627 def _wcrevs(self):
628 # Get the working directory revision as well as the last
628 # Get the working directory revision as well as the last
629 # commit revision so we can compare the subrepo state with
629 # commit revision so we can compare the subrepo state with
630 # both. We used to store the working directory one.
630 # both. We used to store the working directory one.
631 output, err = self._svncommand(['info', '--xml'])
631 output, err = self._svncommand(['info', '--xml'])
632 doc = xml.dom.minidom.parseString(output)
632 doc = xml.dom.minidom.parseString(output)
633 entries = doc.getElementsByTagName('entry')
633 entries = doc.getElementsByTagName('entry')
634 lastrev, rev = '0', '0'
634 lastrev, rev = '0', '0'
635 if entries:
635 if entries:
636 rev = str(entries[0].getAttribute('revision')) or '0'
636 rev = str(entries[0].getAttribute('revision')) or '0'
637 commits = entries[0].getElementsByTagName('commit')
637 commits = entries[0].getElementsByTagName('commit')
638 if commits:
638 if commits:
639 lastrev = str(commits[0].getAttribute('revision')) or '0'
639 lastrev = str(commits[0].getAttribute('revision')) or '0'
640 return (lastrev, rev)
640 return (lastrev, rev)
641
641
642 def _wcrev(self):
642 def _wcrev(self):
643 return self._wcrevs()[0]
643 return self._wcrevs()[0]
644
644
645 def _wcchanged(self):
645 def _wcchanged(self):
646 """Return (changes, extchanges) where changes is True
646 """Return (changes, extchanges) where changes is True
647 if the working directory was changed, and extchanges is
647 if the working directory was changed, and extchanges is
648 True if any of these changes concern an external entry.
648 True if any of these changes concern an external entry.
649 """
649 """
650 output, err = self._svncommand(['status', '--xml'])
650 output, err = self._svncommand(['status', '--xml'])
651 externals, changes = [], []
651 externals, changes = [], []
652 doc = xml.dom.minidom.parseString(output)
652 doc = xml.dom.minidom.parseString(output)
653 for e in doc.getElementsByTagName('entry'):
653 for e in doc.getElementsByTagName('entry'):
654 s = e.getElementsByTagName('wc-status')
654 s = e.getElementsByTagName('wc-status')
655 if not s:
655 if not s:
656 continue
656 continue
657 item = s[0].getAttribute('item')
657 item = s[0].getAttribute('item')
658 props = s[0].getAttribute('props')
658 props = s[0].getAttribute('props')
659 path = e.getAttribute('path')
659 path = e.getAttribute('path')
660 if item == 'external':
660 if item == 'external':
661 externals.append(path)
661 externals.append(path)
662 if (item not in ('', 'normal', 'unversioned', 'external')
662 if (item not in ('', 'normal', 'unversioned', 'external')
663 or props not in ('', 'none', 'normal')):
663 or props not in ('', 'none', 'normal')):
664 changes.append(path)
664 changes.append(path)
665 for path in changes:
665 for path in changes:
666 for ext in externals:
666 for ext in externals:
667 if path == ext or path.startswith(ext + os.sep):
667 if path == ext or path.startswith(ext + os.sep):
668 return True, True
668 return True, True
669 return bool(changes), False
669 return bool(changes), False
670
670
671 def dirty(self, ignoreupdate=False):
671 def dirty(self, ignoreupdate=False):
672 if not self._wcchanged()[0]:
672 if not self._wcchanged()[0]:
673 if self._state[1] in self._wcrevs() or ignoreupdate:
673 if self._state[1] in self._wcrevs() or ignoreupdate:
674 return False
674 return False
675 return True
675 return True
676
676
677 def basestate(self):
677 def basestate(self):
678 return self._wcrev()
678 return self._wcrev()
679
679
680 def commit(self, text, user, date):
680 def commit(self, text, user, date):
681 # user and date are out of our hands since svn is centralized
681 # user and date are out of our hands since svn is centralized
682 changed, extchanged = self._wcchanged()
682 changed, extchanged = self._wcchanged()
683 if not changed:
683 if not changed:
684 return self._wcrev()
684 return self._wcrev()
685 if extchanged:
685 if extchanged:
686 # Do not try to commit externals
686 # Do not try to commit externals
687 raise util.Abort(_('cannot commit svn externals'))
687 raise util.Abort(_('cannot commit svn externals'))
688 commitinfo, err = self._svncommand(['commit', '-m', text])
688 commitinfo, err = self._svncommand(['commit', '-m', text])
689 self._ui.status(commitinfo)
689 self._ui.status(commitinfo)
690 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
690 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
691 if not newrev:
691 if not newrev:
692 raise util.Abort(commitinfo.splitlines()[-1])
692 raise util.Abort(commitinfo.splitlines()[-1])
693 newrev = newrev.groups()[0]
693 newrev = newrev.groups()[0]
694 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
694 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
695 return newrev
695 return newrev
696
696
697 def remove(self):
697 def remove(self):
698 if self.dirty():
698 if self.dirty():
699 self._ui.warn(_('not removing repo %s because '
699 self._ui.warn(_('not removing repo %s because '
700 'it has changes.\n' % self._path))
700 'it has changes.\n' % self._path))
701 return
701 return
702 self._ui.note(_('removing subrepo %s\n') % self._path)
702 self._ui.note(_('removing subrepo %s\n') % self._path)
703
703
704 def onerror(function, path, excinfo):
704 def onerror(function, path, excinfo):
705 if function is not os.remove:
705 if function is not os.remove:
706 raise
706 raise
707 # read-only files cannot be unlinked under Windows
707 # read-only files cannot be unlinked under Windows
708 s = os.stat(path)
708 s = os.stat(path)
709 if (s.st_mode & stat.S_IWRITE) != 0:
709 if (s.st_mode & stat.S_IWRITE) != 0:
710 raise
710 raise
711 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
711 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
712 os.remove(path)
712 os.remove(path)
713
713
714 path = self._ctx._repo.wjoin(self._path)
714 path = self._ctx._repo.wjoin(self._path)
715 shutil.rmtree(path, onerror=onerror)
715 shutil.rmtree(path, onerror=onerror)
716 try:
716 try:
717 os.removedirs(os.path.dirname(path))
717 os.removedirs(os.path.dirname(path))
718 except OSError:
718 except OSError:
719 pass
719 pass
720
720
721 def get(self, state, overwrite=False):
721 def get(self, state, overwrite=False):
722 if overwrite:
722 if overwrite:
723 self._svncommand(['revert', '--recursive'])
723 self._svncommand(['revert', '--recursive'])
724 args = ['checkout']
724 args = ['checkout']
725 if self._svnversion >= (1, 5):
725 if self._svnversion >= (1, 5):
726 args.append('--force')
726 args.append('--force')
727 # The revision must be specified at the end of the URL to properly
727 # The revision must be specified at the end of the URL to properly
728 # update to a directory which has since been deleted and recreated.
728 # update to a directory which has since been deleted and recreated.
729 args.append('%s@%s' % (state[0], state[1]))
729 args.append('%s@%s' % (state[0], state[1]))
730 status, err = self._svncommand(args, failok=True)
730 status, err = self._svncommand(args, failok=True)
731 if not re.search('Checked out revision [0-9]+.', status):
731 if not re.search('Checked out revision [0-9]+.', status):
732 if ('is already a working copy for a different URL' in err
732 if ('is already a working copy for a different URL' in err
733 and (self._wcchanged() == (False, False))):
733 and (self._wcchanged() == (False, False))):
734 # obstructed but clean working copy, so just blow it away.
734 # obstructed but clean working copy, so just blow it away.
735 self.remove()
735 self.remove()
736 self.get(state, overwrite=False)
736 self.get(state, overwrite=False)
737 return
737 return
738 raise util.Abort((status or err).splitlines()[-1])
738 raise util.Abort((status or err).splitlines()[-1])
739 self._ui.status(status)
739 self._ui.status(status)
740
740
741 def merge(self, state):
741 def merge(self, state):
742 old = self._state[1]
742 old = self._state[1]
743 new = state[1]
743 new = state[1]
744 if new != self._wcrev():
744 if new != self._wcrev():
745 dirty = old == self._wcrev() or self._wcchanged()[0]
745 dirty = old == self._wcrev() or self._wcchanged()[0]
746 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
746 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
747 self.get(state, False)
747 self.get(state, False)
748
748
749 def push(self, opts):
749 def push(self, opts):
750 # push is a no-op for SVN
750 # push is a no-op for SVN
751 return True
751 return True
752
752
753 def files(self):
753 def files(self):
754 output = self._svncommand(['list'])
754 output = self._svncommand(['list'])
755 # This works because svn forbids \n in filenames.
755 # This works because svn forbids \n in filenames.
756 return output.splitlines()
756 return output.splitlines()
757
757
758 def filedata(self, name):
758 def filedata(self, name):
759 return self._svncommand(['cat'], name)
759 return self._svncommand(['cat'], name)
760
760
761
761
762 class gitsubrepo(abstractsubrepo):
762 class gitsubrepo(abstractsubrepo):
763 def __init__(self, ctx, path, state):
763 def __init__(self, ctx, path, state):
764 # TODO add git version check.
764 # TODO add git version check.
765 self._state = state
765 self._state = state
766 self._ctx = ctx
766 self._ctx = ctx
767 self._path = path
767 self._path = path
768 self._relpath = os.path.join(reporelpath(ctx._repo), path)
768 self._relpath = os.path.join(reporelpath(ctx._repo), path)
769 self._abspath = ctx._repo.wjoin(path)
769 self._abspath = ctx._repo.wjoin(path)
770 self._subparent = ctx._repo
770 self._subparent = ctx._repo
771 self._ui = ctx._repo.ui
771 self._ui = ctx._repo.ui
772
772
773 def _gitcommand(self, commands, env=None, stream=False):
773 def _gitcommand(self, commands, env=None, stream=False):
774 return self._gitdir(commands, env=env, stream=stream)[0]
774 return self._gitdir(commands, env=env, stream=stream)[0]
775
775
776 def _gitdir(self, commands, env=None, stream=False):
776 def _gitdir(self, commands, env=None, stream=False):
777 return self._gitnodir(commands, env=env, stream=stream,
777 return self._gitnodir(commands, env=env, stream=stream,
778 cwd=self._abspath)
778 cwd=self._abspath)
779
779
780 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
780 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
781 """Calls the git command
781 """Calls the git command
782
782
783 The methods tries to call the git command. versions previor to 1.6.0
783 The methods tries to call the git command. versions previor to 1.6.0
784 are not supported and very probably fail.
784 are not supported and very probably fail.
785 """
785 """
786 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
786 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
787 # unless ui.quiet is set, print git's stderr,
787 # unless ui.quiet is set, print git's stderr,
788 # which is mostly progress and useful info
788 # which is mostly progress and useful info
789 errpipe = None
789 errpipe = None
790 if self._ui.quiet:
790 if self._ui.quiet:
791 errpipe = open(os.devnull, 'w')
791 errpipe = open(os.devnull, 'w')
792 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
792 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
793 close_fds=util.closefds,
793 close_fds=util.closefds,
794 stdout=subprocess.PIPE, stderr=errpipe)
794 stdout=subprocess.PIPE, stderr=errpipe)
795 if stream:
795 if stream:
796 return p.stdout, None
796 return p.stdout, None
797
797
798 retdata = p.stdout.read().strip()
798 retdata = p.stdout.read().strip()
799 # wait for the child to exit to avoid race condition.
799 # wait for the child to exit to avoid race condition.
800 p.wait()
800 p.wait()
801
801
802 if p.returncode != 0 and p.returncode != 1:
802 if p.returncode != 0 and p.returncode != 1:
803 # there are certain error codes that are ok
803 # there are certain error codes that are ok
804 command = commands[0]
804 command = commands[0]
805 if command in ('cat-file', 'symbolic-ref'):
805 if command in ('cat-file', 'symbolic-ref'):
806 return retdata, p.returncode
806 return retdata, p.returncode
807 # for all others, abort
807 # for all others, abort
808 raise util.Abort('git %s error %d in %s' %
808 raise util.Abort('git %s error %d in %s' %
809 (command, p.returncode, self._relpath))
809 (command, p.returncode, self._relpath))
810
810
811 return retdata, p.returncode
811 return retdata, p.returncode
812
812
813 def _gitmissing(self):
813 def _gitmissing(self):
814 return not os.path.exists(os.path.join(self._abspath, '.git'))
814 return not os.path.exists(os.path.join(self._abspath, '.git'))
815
815
816 def _gitstate(self):
816 def _gitstate(self):
817 return self._gitcommand(['rev-parse', 'HEAD'])
817 return self._gitcommand(['rev-parse', 'HEAD'])
818
818
819 def _gitcurrentbranch(self):
819 def _gitcurrentbranch(self):
820 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
820 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
821 if err:
821 if err:
822 current = None
822 current = None
823 return current
823 return current
824
824
825 def _gitremote(self, remote):
825 def _gitremote(self, remote):
826 out = self._gitcommand(['remote', 'show', '-n', remote])
826 out = self._gitcommand(['remote', 'show', '-n', remote])
827 line = out.split('\n')[1]
827 line = out.split('\n')[1]
828 i = line.index('URL: ') + len('URL: ')
828 i = line.index('URL: ') + len('URL: ')
829 return line[i:]
829 return line[i:]
830
830
831 def _githavelocally(self, revision):
831 def _githavelocally(self, revision):
832 out, code = self._gitdir(['cat-file', '-e', revision])
832 out, code = self._gitdir(['cat-file', '-e', revision])
833 return code == 0
833 return code == 0
834
834
835 def _gitisancestor(self, r1, r2):
835 def _gitisancestor(self, r1, r2):
836 base = self._gitcommand(['merge-base', r1, r2])
836 base = self._gitcommand(['merge-base', r1, r2])
837 return base == r1
837 return base == r1
838
838
839 def _gitisbare(self):
839 def _gitisbare(self):
840 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
840 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
841
841
842 def _gitupdatestat(self):
842 def _gitupdatestat(self):
843 """This must be run before git diff-index.
843 """This must be run before git diff-index.
844 diff-index only looks at changes to file stat;
844 diff-index only looks at changes to file stat;
845 this command looks at file contents and updates the stat."""
845 this command looks at file contents and updates the stat."""
846 self._gitcommand(['update-index', '-q', '--refresh'])
846 self._gitcommand(['update-index', '-q', '--refresh'])
847
847
848 def _gitbranchmap(self):
848 def _gitbranchmap(self):
849 '''returns 2 things:
849 '''returns 2 things:
850 a map from git branch to revision
850 a map from git branch to revision
851 a map from revision to branches'''
851 a map from revision to branches'''
852 branch2rev = {}
852 branch2rev = {}
853 rev2branch = {}
853 rev2branch = {}
854
854
855 out = self._gitcommand(['for-each-ref', '--format',
855 out = self._gitcommand(['for-each-ref', '--format',
856 '%(objectname) %(refname)'])
856 '%(objectname) %(refname)'])
857 for line in out.split('\n'):
857 for line in out.split('\n'):
858 revision, ref = line.split(' ')
858 revision, ref = line.split(' ')
859 if (not ref.startswith('refs/heads/') and
859 if (not ref.startswith('refs/heads/') and
860 not ref.startswith('refs/remotes/')):
860 not ref.startswith('refs/remotes/')):
861 continue
861 continue
862 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
862 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
863 continue # ignore remote/HEAD redirects
863 continue # ignore remote/HEAD redirects
864 branch2rev[ref] = revision
864 branch2rev[ref] = revision
865 rev2branch.setdefault(revision, []).append(ref)
865 rev2branch.setdefault(revision, []).append(ref)
866 return branch2rev, rev2branch
866 return branch2rev, rev2branch
867
867
868 def _gittracking(self, branches):
868 def _gittracking(self, branches):
869 'return map of remote branch to local tracking branch'
869 'return map of remote branch to local tracking branch'
870 # assumes no more than one local tracking branch for each remote
870 # assumes no more than one local tracking branch for each remote
871 tracking = {}
871 tracking = {}
872 for b in branches:
872 for b in branches:
873 if b.startswith('refs/remotes/'):
873 if b.startswith('refs/remotes/'):
874 continue
874 continue
875 bname = b.split('/', 2)[2]
875 bname = b.split('/', 2)[2]
876 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
876 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
877 if remote:
877 if remote:
878 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
878 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
879 tracking['refs/remotes/%s/%s' %
879 tracking['refs/remotes/%s/%s' %
880 (remote, ref.split('/', 2)[2])] = b
880 (remote, ref.split('/', 2)[2])] = b
881 return tracking
881 return tracking
882
882
883 def _abssource(self, source):
883 def _abssource(self, source):
884 if '://' not in source:
884 if '://' not in source:
885 # recognize the scp syntax as an absolute source
885 # recognize the scp syntax as an absolute source
886 colon = source.find(':')
886 colon = source.find(':')
887 if colon != -1 and '/' not in source[:colon]:
887 if colon != -1 and '/' not in source[:colon]:
888 return source
888 return source
889 self._subsource = source
889 self._subsource = source
890 return _abssource(self)
890 return _abssource(self)
891
891
892 def _fetch(self, source, revision):
892 def _fetch(self, source, revision):
893 if self._gitmissing():
893 if self._gitmissing():
894 source = self._abssource(source)
894 source = self._abssource(source)
895 self._ui.status(_('cloning subrepo %s from %s\n') %
895 self._ui.status(_('cloning subrepo %s from %s\n') %
896 (self._relpath, source))
896 (self._relpath, source))
897 self._gitnodir(['clone', source, self._abspath])
897 self._gitnodir(['clone', source, self._abspath])
898 if self._githavelocally(revision):
898 if self._githavelocally(revision):
899 return
899 return
900 self._ui.status(_('pulling subrepo %s from %s\n') %
900 self._ui.status(_('pulling subrepo %s from %s\n') %
901 (self._relpath, self._gitremote('origin')))
901 (self._relpath, self._gitremote('origin')))
902 # try only origin: the originally cloned repo
902 # try only origin: the originally cloned repo
903 self._gitcommand(['fetch'])
903 self._gitcommand(['fetch'])
904 if not self._githavelocally(revision):
904 if not self._githavelocally(revision):
905 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
905 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
906 (revision, self._relpath))
906 (revision, self._relpath))
907
907
908 def dirty(self, ignoreupdate=False):
908 def dirty(self, ignoreupdate=False):
909 if self._gitmissing():
909 if self._gitmissing():
910 return self._state[1] != ''
910 return self._state[1] != ''
911 if self._gitisbare():
911 if self._gitisbare():
912 return True
912 return True
913 if not ignoreupdate and self._state[1] != self._gitstate():
913 if not ignoreupdate and self._state[1] != self._gitstate():
914 # different version checked out
914 # different version checked out
915 return True
915 return True
916 # check for staged changes or modified files; ignore untracked files
916 # check for staged changes or modified files; ignore untracked files
917 self._gitupdatestat()
917 self._gitupdatestat()
918 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
918 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
919 return code == 1
919 return code == 1
920
920
921 def basestate(self):
921 def basestate(self):
922 return self._gitstate()
922 return self._gitstate()
923
923
924 def get(self, state, overwrite=False):
924 def get(self, state, overwrite=False):
925 source, revision, kind = state
925 source, revision, kind = state
926 if not revision:
926 if not revision:
927 self.remove()
927 self.remove()
928 return
928 return
929 self._fetch(source, revision)
929 self._fetch(source, revision)
930 # if the repo was set to be bare, unbare it
930 # if the repo was set to be bare, unbare it
931 if self._gitisbare():
931 if self._gitisbare():
932 self._gitcommand(['config', 'core.bare', 'false'])
932 self._gitcommand(['config', 'core.bare', 'false'])
933 if self._gitstate() == revision:
933 if self._gitstate() == revision:
934 self._gitcommand(['reset', '--hard', 'HEAD'])
934 self._gitcommand(['reset', '--hard', 'HEAD'])
935 return
935 return
936 elif self._gitstate() == revision:
936 elif self._gitstate() == revision:
937 if overwrite:
937 if overwrite:
938 # first reset the index to unmark new files for commit, because
938 # first reset the index to unmark new files for commit, because
939 # reset --hard will otherwise throw away files added for commit,
939 # reset --hard will otherwise throw away files added for commit,
940 # not just unmark them.
940 # not just unmark them.
941 self._gitcommand(['reset', 'HEAD'])
941 self._gitcommand(['reset', 'HEAD'])
942 self._gitcommand(['reset', '--hard', 'HEAD'])
942 self._gitcommand(['reset', '--hard', 'HEAD'])
943 return
943 return
944 branch2rev, rev2branch = self._gitbranchmap()
944 branch2rev, rev2branch = self._gitbranchmap()
945
945
946 def checkout(args):
946 def checkout(args):
947 cmd = ['checkout']
947 cmd = ['checkout']
948 if overwrite:
948 if overwrite:
949 # first reset the index to unmark new files for commit, because
949 # first reset the index to unmark new files for commit, because
950 # the -f option will otherwise throw away files added for
950 # the -f option will otherwise throw away files added for
951 # commit, not just unmark them.
951 # commit, not just unmark them.
952 self._gitcommand(['reset', 'HEAD'])
952 self._gitcommand(['reset', 'HEAD'])
953 cmd.append('-f')
953 cmd.append('-f')
954 self._gitcommand(cmd + args)
954 self._gitcommand(cmd + args)
955
955
956 def rawcheckout():
956 def rawcheckout():
957 # no branch to checkout, check it out with no branch
957 # no branch to checkout, check it out with no branch
958 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
958 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
959 self._relpath)
959 self._relpath)
960 self._ui.warn(_('check out a git branch if you intend '
960 self._ui.warn(_('check out a git branch if you intend '
961 'to make changes\n'))
961 'to make changes\n'))
962 checkout(['-q', revision])
962 checkout(['-q', revision])
963
963
964 if revision not in rev2branch:
964 if revision not in rev2branch:
965 rawcheckout()
965 rawcheckout()
966 return
966 return
967 branches = rev2branch[revision]
967 branches = rev2branch[revision]
968 firstlocalbranch = None
968 firstlocalbranch = None
969 for b in branches:
969 for b in branches:
970 if b == 'refs/heads/master':
970 if b == 'refs/heads/master':
971 # master trumps all other branches
971 # master trumps all other branches
972 checkout(['refs/heads/master'])
972 checkout(['refs/heads/master'])
973 return
973 return
974 if not firstlocalbranch and not b.startswith('refs/remotes/'):
974 if not firstlocalbranch and not b.startswith('refs/remotes/'):
975 firstlocalbranch = b
975 firstlocalbranch = b
976 if firstlocalbranch:
976 if firstlocalbranch:
977 checkout([firstlocalbranch])
977 checkout([firstlocalbranch])
978 return
978 return
979
979
980 tracking = self._gittracking(branch2rev.keys())
980 tracking = self._gittracking(branch2rev.keys())
981 # choose a remote branch already tracked if possible
981 # choose a remote branch already tracked if possible
982 remote = branches[0]
982 remote = branches[0]
983 if remote not in tracking:
983 if remote not in tracking:
984 for b in branches:
984 for b in branches:
985 if b in tracking:
985 if b in tracking:
986 remote = b
986 remote = b
987 break
987 break
988
988
989 if remote not in tracking:
989 if remote not in tracking:
990 # create a new local tracking branch
990 # create a new local tracking branch
991 local = remote.split('/', 2)[2]
991 local = remote.split('/', 2)[2]
992 checkout(['-b', local, remote])
992 checkout(['-b', local, remote])
993 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
993 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
994 # When updating to a tracked remote branch,
994 # When updating to a tracked remote branch,
995 # if the local tracking branch is downstream of it,
995 # if the local tracking branch is downstream of it,
996 # a normal `git pull` would have performed a "fast-forward merge"
996 # a normal `git pull` would have performed a "fast-forward merge"
997 # which is equivalent to updating the local branch to the remote.
997 # which is equivalent to updating the local branch to the remote.
998 # Since we are only looking at branching at update, we need to
998 # Since we are only looking at branching at update, we need to
999 # detect this situation and perform this action lazily.
999 # detect this situation and perform this action lazily.
1000 if tracking[remote] != self._gitcurrentbranch():
1000 if tracking[remote] != self._gitcurrentbranch():
1001 checkout([tracking[remote]])
1001 checkout([tracking[remote]])
1002 self._gitcommand(['merge', '--ff', remote])
1002 self._gitcommand(['merge', '--ff', remote])
1003 else:
1003 else:
1004 # a real merge would be required, just checkout the revision
1004 # a real merge would be required, just checkout the revision
1005 rawcheckout()
1005 rawcheckout()
1006
1006
1007 def commit(self, text, user, date):
1007 def commit(self, text, user, date):
1008 if self._gitmissing():
1008 if self._gitmissing():
1009 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1009 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1010 cmd = ['commit', '-a', '-m', text]
1010 cmd = ['commit', '-a', '-m', text]
1011 env = os.environ.copy()
1011 env = os.environ.copy()
1012 if user:
1012 if user:
1013 cmd += ['--author', user]
1013 cmd += ['--author', user]
1014 if date:
1014 if date:
1015 # git's date parser silently ignores when seconds < 1e9
1015 # git's date parser silently ignores when seconds < 1e9
1016 # convert to ISO8601
1016 # convert to ISO8601
1017 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1017 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1018 '%Y-%m-%dT%H:%M:%S %1%2')
1018 '%Y-%m-%dT%H:%M:%S %1%2')
1019 self._gitcommand(cmd, env=env)
1019 self._gitcommand(cmd, env=env)
1020 # make sure commit works otherwise HEAD might not exist under certain
1020 # make sure commit works otherwise HEAD might not exist under certain
1021 # circumstances
1021 # circumstances
1022 return self._gitstate()
1022 return self._gitstate()
1023
1023
1024 def merge(self, state):
1024 def merge(self, state):
1025 source, revision, kind = state
1025 source, revision, kind = state
1026 self._fetch(source, revision)
1026 self._fetch(source, revision)
1027 base = self._gitcommand(['merge-base', revision, self._state[1]])
1027 base = self._gitcommand(['merge-base', revision, self._state[1]])
1028 self._gitupdatestat()
1028 self._gitupdatestat()
1029 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1029 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1030
1030
1031 def mergefunc():
1031 def mergefunc():
1032 if base == revision:
1032 if base == revision:
1033 self.get(state) # fast forward merge
1033 self.get(state) # fast forward merge
1034 elif base != self._state[1]:
1034 elif base != self._state[1]:
1035 self._gitcommand(['merge', '--no-commit', revision])
1035 self._gitcommand(['merge', '--no-commit', revision])
1036
1036
1037 if self.dirty():
1037 if self.dirty():
1038 if self._gitstate() != revision:
1038 if self._gitstate() != revision:
1039 dirty = self._gitstate() == self._state[1] or code != 0
1039 dirty = self._gitstate() == self._state[1] or code != 0
1040 if _updateprompt(self._ui, self, dirty,
1040 if _updateprompt(self._ui, self, dirty,
1041 self._state[1][:7], revision[:7]):
1041 self._state[1][:7], revision[:7]):
1042 mergefunc()
1042 mergefunc()
1043 else:
1043 else:
1044 mergefunc()
1044 mergefunc()
1045
1045
1046 def push(self, opts):
1046 def push(self, opts):
1047 force = opts.get('force')
1047 force = opts.get('force')
1048
1048
1049 if not self._state[1]:
1049 if not self._state[1]:
1050 return True
1050 return True
1051 if self._gitmissing():
1051 if self._gitmissing():
1052 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1052 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1053 # if a branch in origin contains the revision, nothing to do
1053 # if a branch in origin contains the revision, nothing to do
1054 branch2rev, rev2branch = self._gitbranchmap()
1054 branch2rev, rev2branch = self._gitbranchmap()
1055 if self._state[1] in rev2branch:
1055 if self._state[1] in rev2branch:
1056 for b in rev2branch[self._state[1]]:
1056 for b in rev2branch[self._state[1]]:
1057 if b.startswith('refs/remotes/origin/'):
1057 if b.startswith('refs/remotes/origin/'):
1058 return True
1058 return True
1059 for b, revision in branch2rev.iteritems():
1059 for b, revision in branch2rev.iteritems():
1060 if b.startswith('refs/remotes/origin/'):
1060 if b.startswith('refs/remotes/origin/'):
1061 if self._gitisancestor(self._state[1], revision):
1061 if self._gitisancestor(self._state[1], revision):
1062 return True
1062 return True
1063 # otherwise, try to push the currently checked out branch
1063 # otherwise, try to push the currently checked out branch
1064 cmd = ['push']
1064 cmd = ['push']
1065 if force:
1065 if force:
1066 cmd.append('--force')
1066 cmd.append('--force')
1067
1067
1068 current = self._gitcurrentbranch()
1068 current = self._gitcurrentbranch()
1069 if current:
1069 if current:
1070 # determine if the current branch is even useful
1070 # determine if the current branch is even useful
1071 if not self._gitisancestor(self._state[1], current):
1071 if not self._gitisancestor(self._state[1], current):
1072 self._ui.warn(_('unrelated git branch checked out '
1072 self._ui.warn(_('unrelated git branch checked out '
1073 'in subrepo %s\n') % self._relpath)
1073 'in subrepo %s\n') % self._relpath)
1074 return False
1074 return False
1075 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1075 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1076 (current.split('/', 2)[2], self._relpath))
1076 (current.split('/', 2)[2], self._relpath))
1077 self._gitcommand(cmd + ['origin', current])
1077 self._gitcommand(cmd + ['origin', current])
1078 return True
1078 return True
1079 else:
1079 else:
1080 self._ui.warn(_('no branch checked out in subrepo %s\n'
1080 self._ui.warn(_('no branch checked out in subrepo %s\n'
1081 'cannot push revision %s') %
1081 'cannot push revision %s') %
1082 (self._relpath, self._state[1]))
1082 (self._relpath, self._state[1]))
1083 return False
1083 return False
1084
1084
1085 def remove(self):
1085 def remove(self):
1086 if self._gitmissing():
1086 if self._gitmissing():
1087 return
1087 return
1088 if self.dirty():
1088 if self.dirty():
1089 self._ui.warn(_('not removing repo %s because '
1089 self._ui.warn(_('not removing repo %s because '
1090 'it has changes.\n') % self._relpath)
1090 'it has changes.\n') % self._relpath)
1091 return
1091 return
1092 # we can't fully delete the repository as it may contain
1092 # we can't fully delete the repository as it may contain
1093 # local-only history
1093 # local-only history
1094 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1094 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1095 self._gitcommand(['config', 'core.bare', 'true'])
1095 self._gitcommand(['config', 'core.bare', 'true'])
1096 for f in os.listdir(self._abspath):
1096 for f in os.listdir(self._abspath):
1097 if f == '.git':
1097 if f == '.git':
1098 continue
1098 continue
1099 path = os.path.join(self._abspath, f)
1099 path = os.path.join(self._abspath, f)
1100 if os.path.isdir(path) and not os.path.islink(path):
1100 if os.path.isdir(path) and not os.path.islink(path):
1101 shutil.rmtree(path)
1101 shutil.rmtree(path)
1102 else:
1102 else:
1103 os.remove(path)
1103 os.remove(path)
1104
1104
1105 def archive(self, ui, archiver, prefix):
1105 def archive(self, ui, archiver, prefix):
1106 source, revision = self._state
1106 source, revision = self._state
1107 if not revision:
1107 if not revision:
1108 return
1108 return
1109 self._fetch(source, revision)
1109 self._fetch(source, revision)
1110
1110
1111 # Parse git's native archive command.
1111 # Parse git's native archive command.
1112 # This should be much faster than manually traversing the trees
1112 # This should be much faster than manually traversing the trees
1113 # and objects with many subprocess calls.
1113 # and objects with many subprocess calls.
1114 tarstream = self._gitcommand(['archive', revision], stream=True)
1114 tarstream = self._gitcommand(['archive', revision], stream=True)
1115 tar = tarfile.open(fileobj=tarstream, mode='r|')
1115 tar = tarfile.open(fileobj=tarstream, mode='r|')
1116 relpath = subrelpath(self)
1116 relpath = subrelpath(self)
1117 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1117 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1118 for i, info in enumerate(tar):
1118 for i, info in enumerate(tar):
1119 if info.isdir():
1119 if info.isdir():
1120 continue
1120 continue
1121 if info.issym():
1121 if info.issym():
1122 data = info.linkname
1122 data = info.linkname
1123 else:
1123 else:
1124 data = tar.extractfile(info).read()
1124 data = tar.extractfile(info).read()
1125 archiver.addfile(os.path.join(prefix, self._path, info.name),
1125 archiver.addfile(os.path.join(prefix, self._path, info.name),
1126 info.mode, info.issym(), data)
1126 info.mode, info.issym(), data)
1127 ui.progress(_('archiving (%s)') % relpath, i + 1,
1127 ui.progress(_('archiving (%s)') % relpath, i + 1,
1128 unit=_('files'))
1128 unit=_('files'))
1129 ui.progress(_('archiving (%s)') % relpath, None)
1129 ui.progress(_('archiving (%s)') % relpath, None)
1130
1130
1131
1131
1132 def status(self, rev2, **opts):
1132 def status(self, rev2, **opts):
1133 rev1 = self._state[1]
1133 rev1 = self._state[1]
1134 if self._gitmissing() or not rev1:
1134 if self._gitmissing() or not rev1:
1135 # if the repo is missing, return no results
1135 # if the repo is missing, return no results
1136 return [], [], [], [], [], [], []
1136 return [], [], [], [], [], [], []
1137 modified, added, removed = [], [], []
1137 modified, added, removed = [], [], []
1138 self._gitupdatestat()
1138 self._gitupdatestat()
1139 if rev2:
1139 if rev2:
1140 command = ['diff-tree', rev1, rev2]
1140 command = ['diff-tree', rev1, rev2]
1141 else:
1141 else:
1142 command = ['diff-index', rev1]
1142 command = ['diff-index', rev1]
1143 out = self._gitcommand(command)
1143 out = self._gitcommand(command)
1144 for line in out.split('\n'):
1144 for line in out.split('\n'):
1145 tab = line.find('\t')
1145 tab = line.find('\t')
1146 if tab == -1:
1146 if tab == -1:
1147 continue
1147 continue
1148 status, f = line[tab - 1], line[tab + 1:]
1148 status, f = line[tab - 1], line[tab + 1:]
1149 if status == 'M':
1149 if status == 'M':
1150 modified.append(f)
1150 modified.append(f)
1151 elif status == 'A':
1151 elif status == 'A':
1152 added.append(f)
1152 added.append(f)
1153 elif status == 'D':
1153 elif status == 'D':
1154 removed.append(f)
1154 removed.append(f)
1155
1155
1156 deleted = unknown = ignored = clean = []
1156 deleted = unknown = ignored = clean = []
1157 return modified, added, removed, deleted, unknown, ignored, clean
1157 return modified, added, removed, deleted, unknown, ignored, clean
1158
1158
1159 types = {
1159 types = {
1160 'hg': hgsubrepo,
1160 'hg': hgsubrepo,
1161 'svn': svnsubrepo,
1161 'svn': svnsubrepo,
1162 'git': gitsubrepo,
1162 'git': gitsubrepo,
1163 }
1163 }
@@ -1,1023 +1,1055
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > graphlog=
3 > graphlog=
4 > EOF
4 > EOF
5 $ hgph() { hg log -G --template "{rev} {phase} {desc} - {node|short}\n" $*; }
5 $ hgph() { hg log -G --template "{rev} {phase} {desc} - {node|short}\n" $*; }
6
6
7 $ mkcommit() {
7 $ mkcommit() {
8 > echo "$1" > "$1"
8 > echo "$1" > "$1"
9 > hg add "$1"
9 > hg add "$1"
10 > message="$1"
10 > message="$1"
11 > shift
11 > shift
12 > hg ci -m "$message" $*
12 > hg ci -m "$message" $*
13 > }
13 > }
14
14
15 $ hg init alpha
15 $ hg init alpha
16 $ cd alpha
16 $ cd alpha
17 $ mkcommit a-A
17 $ mkcommit a-A
18 $ mkcommit a-B
18 $ mkcommit a-B
19 $ mkcommit a-C
19 $ mkcommit a-C
20 $ mkcommit a-D
20 $ mkcommit a-D
21 $ hgph
21 $ hgph
22 @ 3 draft a-D - b555f63b6063
22 @ 3 draft a-D - b555f63b6063
23 |
23 |
24 o 2 draft a-C - 54acac6f23ab
24 o 2 draft a-C - 54acac6f23ab
25 |
25 |
26 o 1 draft a-B - 548a3d25dbf0
26 o 1 draft a-B - 548a3d25dbf0
27 |
27 |
28 o 0 draft a-A - 054250a37db4
28 o 0 draft a-A - 054250a37db4
29
29
30
30
31 $ hg init ../beta
31 $ hg init ../beta
32 $ hg push -r 1 ../beta
32 $ hg push -r 1 ../beta
33 pushing to ../beta
33 pushing to ../beta
34 searching for changes
34 searching for changes
35 adding changesets
35 adding changesets
36 adding manifests
36 adding manifests
37 adding file changes
37 adding file changes
38 added 2 changesets with 2 changes to 2 files
38 added 2 changesets with 2 changes to 2 files
39 $ hgph
39 $ hgph
40 @ 3 draft a-D - b555f63b6063
40 @ 3 draft a-D - b555f63b6063
41 |
41 |
42 o 2 draft a-C - 54acac6f23ab
42 o 2 draft a-C - 54acac6f23ab
43 |
43 |
44 o 1 public a-B - 548a3d25dbf0
44 o 1 public a-B - 548a3d25dbf0
45 |
45 |
46 o 0 public a-A - 054250a37db4
46 o 0 public a-A - 054250a37db4
47
47
48
48
49 $ cd ../beta
49 $ cd ../beta
50 $ hgph
50 $ hgph
51 o 1 public a-B - 548a3d25dbf0
51 o 1 public a-B - 548a3d25dbf0
52 |
52 |
53 o 0 public a-A - 054250a37db4
53 o 0 public a-A - 054250a37db4
54
54
55 $ hg up -q
55 $ hg up -q
56 $ mkcommit b-A
56 $ mkcommit b-A
57 $ hgph
57 $ hgph
58 @ 2 draft b-A - f54f1bb90ff3
58 @ 2 draft b-A - f54f1bb90ff3
59 |
59 |
60 o 1 public a-B - 548a3d25dbf0
60 o 1 public a-B - 548a3d25dbf0
61 |
61 |
62 o 0 public a-A - 054250a37db4
62 o 0 public a-A - 054250a37db4
63
63
64 $ hg pull ../alpha
64 $ hg pull ../alpha
65 pulling from ../alpha
65 pulling from ../alpha
66 searching for changes
66 searching for changes
67 adding changesets
67 adding changesets
68 adding manifests
68 adding manifests
69 adding file changes
69 adding file changes
70 added 2 changesets with 2 changes to 2 files (+1 heads)
70 added 2 changesets with 2 changes to 2 files (+1 heads)
71 (run 'hg heads' to see heads, 'hg merge' to merge)
71 (run 'hg heads' to see heads, 'hg merge' to merge)
72 $ hgph
72 $ hgph
73 o 4 public a-D - b555f63b6063
73 o 4 public a-D - b555f63b6063
74 |
74 |
75 o 3 public a-C - 54acac6f23ab
75 o 3 public a-C - 54acac6f23ab
76 |
76 |
77 | @ 2 draft b-A - f54f1bb90ff3
77 | @ 2 draft b-A - f54f1bb90ff3
78 |/
78 |/
79 o 1 public a-B - 548a3d25dbf0
79 o 1 public a-B - 548a3d25dbf0
80 |
80 |
81 o 0 public a-A - 054250a37db4
81 o 0 public a-A - 054250a37db4
82
82
83
83
84 pull did not updated ../alpha state.
84 pull did not updated ../alpha state.
85 push from alpha to beta should update phase even if nothing is transfered
85 push from alpha to beta should update phase even if nothing is transfered
86
86
87 $ cd ../alpha
87 $ cd ../alpha
88 $ hgph # not updated by remote pull
88 $ hgph # not updated by remote pull
89 @ 3 draft a-D - b555f63b6063
89 @ 3 draft a-D - b555f63b6063
90 |
90 |
91 o 2 draft a-C - 54acac6f23ab
91 o 2 draft a-C - 54acac6f23ab
92 |
92 |
93 o 1 public a-B - 548a3d25dbf0
93 o 1 public a-B - 548a3d25dbf0
94 |
94 |
95 o 0 public a-A - 054250a37db4
95 o 0 public a-A - 054250a37db4
96
96
97 $ hg push ../beta
97 $ hg push ../beta
98 pushing to ../beta
98 pushing to ../beta
99 searching for changes
99 searching for changes
100 no changes found
100 no changes found
101 [1]
101 [1]
102 $ hgph
102 $ hgph
103 @ 3 public a-D - b555f63b6063
103 @ 3 public a-D - b555f63b6063
104 |
104 |
105 o 2 public a-C - 54acac6f23ab
105 o 2 public a-C - 54acac6f23ab
106 |
106 |
107 o 1 public a-B - 548a3d25dbf0
107 o 1 public a-B - 548a3d25dbf0
108 |
108 |
109 o 0 public a-A - 054250a37db4
109 o 0 public a-A - 054250a37db4
110
110
111
111
112 update must update phase of common changeset too
112 update must update phase of common changeset too
113
113
114 $ hg pull ../beta # getting b-A
114 $ hg pull ../beta # getting b-A
115 pulling from ../beta
115 pulling from ../beta
116 searching for changes
116 searching for changes
117 adding changesets
117 adding changesets
118 adding manifests
118 adding manifests
119 adding file changes
119 adding file changes
120 added 1 changesets with 1 changes to 1 files (+1 heads)
120 added 1 changesets with 1 changes to 1 files (+1 heads)
121 (run 'hg heads' to see heads, 'hg merge' to merge)
121 (run 'hg heads' to see heads, 'hg merge' to merge)
122
122
123 $ cd ../beta
123 $ cd ../beta
124 $ hgph # not updated by remote pull
124 $ hgph # not updated by remote pull
125 o 4 public a-D - b555f63b6063
125 o 4 public a-D - b555f63b6063
126 |
126 |
127 o 3 public a-C - 54acac6f23ab
127 o 3 public a-C - 54acac6f23ab
128 |
128 |
129 | @ 2 draft b-A - f54f1bb90ff3
129 | @ 2 draft b-A - f54f1bb90ff3
130 |/
130 |/
131 o 1 public a-B - 548a3d25dbf0
131 o 1 public a-B - 548a3d25dbf0
132 |
132 |
133 o 0 public a-A - 054250a37db4
133 o 0 public a-A - 054250a37db4
134
134
135 $ hg pull ../alpha
135 $ hg pull ../alpha
136 pulling from ../alpha
136 pulling from ../alpha
137 searching for changes
137 searching for changes
138 no changes found
138 no changes found
139 $ hgph
139 $ hgph
140 o 4 public a-D - b555f63b6063
140 o 4 public a-D - b555f63b6063
141 |
141 |
142 o 3 public a-C - 54acac6f23ab
142 o 3 public a-C - 54acac6f23ab
143 |
143 |
144 | @ 2 public b-A - f54f1bb90ff3
144 | @ 2 public b-A - f54f1bb90ff3
145 |/
145 |/
146 o 1 public a-B - 548a3d25dbf0
146 o 1 public a-B - 548a3d25dbf0
147 |
147 |
148 o 0 public a-A - 054250a37db4
148 o 0 public a-A - 054250a37db4
149
149
150
150
151 Publish configuration option
151 Publish configuration option
152 ----------------------------
152 ----------------------------
153
153
154 Pull
154 Pull
155 ````
155 ````
156
156
157 changegroup are added without phase movement
157 changegroup are added without phase movement
158
158
159 $ hg bundle -a ../base.bundle
159 $ hg bundle -a ../base.bundle
160 5 changesets found
160 5 changesets found
161 $ cd ..
161 $ cd ..
162 $ hg init mu
162 $ hg init mu
163 $ cd mu
163 $ cd mu
164 $ cat > .hg/hgrc << EOF
164 $ cat > .hg/hgrc << EOF
165 > [phases]
165 > [phases]
166 > publish=0
166 > publish=0
167 > EOF
167 > EOF
168 $ hg unbundle ../base.bundle
168 $ hg unbundle ../base.bundle
169 adding changesets
169 adding changesets
170 adding manifests
170 adding manifests
171 adding file changes
171 adding file changes
172 added 5 changesets with 5 changes to 5 files (+1 heads)
172 added 5 changesets with 5 changes to 5 files (+1 heads)
173 (run 'hg heads' to see heads, 'hg merge' to merge)
173 (run 'hg heads' to see heads, 'hg merge' to merge)
174 $ hgph
174 $ hgph
175 o 4 draft a-D - b555f63b6063
175 o 4 draft a-D - b555f63b6063
176 |
176 |
177 o 3 draft a-C - 54acac6f23ab
177 o 3 draft a-C - 54acac6f23ab
178 |
178 |
179 | o 2 draft b-A - f54f1bb90ff3
179 | o 2 draft b-A - f54f1bb90ff3
180 |/
180 |/
181 o 1 draft a-B - 548a3d25dbf0
181 o 1 draft a-B - 548a3d25dbf0
182 |
182 |
183 o 0 draft a-A - 054250a37db4
183 o 0 draft a-A - 054250a37db4
184
184
185 $ cd ..
185 $ cd ..
186
186
187 Pulling from publish=False to publish=False does not move boundary.
187 Pulling from publish=False to publish=False does not move boundary.
188
188
189 $ hg init nu
189 $ hg init nu
190 $ cd nu
190 $ cd nu
191 $ cat > .hg/hgrc << EOF
191 $ cat > .hg/hgrc << EOF
192 > [phases]
192 > [phases]
193 > publish=0
193 > publish=0
194 > EOF
194 > EOF
195 $ hg pull ../mu -r 54acac6f23ab
195 $ hg pull ../mu -r 54acac6f23ab
196 pulling from ../mu
196 pulling from ../mu
197 adding changesets
197 adding changesets
198 adding manifests
198 adding manifests
199 adding file changes
199 adding file changes
200 added 3 changesets with 3 changes to 3 files
200 added 3 changesets with 3 changes to 3 files
201 (run 'hg update' to get a working copy)
201 (run 'hg update' to get a working copy)
202 $ hgph
202 $ hgph
203 o 2 draft a-C - 54acac6f23ab
203 o 2 draft a-C - 54acac6f23ab
204 |
204 |
205 o 1 draft a-B - 548a3d25dbf0
205 o 1 draft a-B - 548a3d25dbf0
206 |
206 |
207 o 0 draft a-A - 054250a37db4
207 o 0 draft a-A - 054250a37db4
208
208
209
209
210 Even for common
210 Even for common
211
211
212 $ hg pull ../mu -r f54f1bb90ff3
212 $ hg pull ../mu -r f54f1bb90ff3
213 pulling from ../mu
213 pulling from ../mu
214 searching for changes
214 searching for changes
215 adding changesets
215 adding changesets
216 adding manifests
216 adding manifests
217 adding file changes
217 adding file changes
218 added 1 changesets with 1 changes to 1 files (+1 heads)
218 added 1 changesets with 1 changes to 1 files (+1 heads)
219 (run 'hg heads' to see heads, 'hg merge' to merge)
219 (run 'hg heads' to see heads, 'hg merge' to merge)
220 $ hgph
220 $ hgph
221 o 3 draft b-A - f54f1bb90ff3
221 o 3 draft b-A - f54f1bb90ff3
222 |
222 |
223 | o 2 draft a-C - 54acac6f23ab
223 | o 2 draft a-C - 54acac6f23ab
224 |/
224 |/
225 o 1 draft a-B - 548a3d25dbf0
225 o 1 draft a-B - 548a3d25dbf0
226 |
226 |
227 o 0 draft a-A - 054250a37db4
227 o 0 draft a-A - 054250a37db4
228
228
229
229
230
230
231 Pulling from Publish=True to Publish=False move boundary in common set.
231 Pulling from Publish=True to Publish=False move boundary in common set.
232 we are in nu
232 we are in nu
233
233
234 $ hg pull ../alpha -r b555f63b6063
234 $ hg pull ../alpha -r b555f63b6063
235 pulling from ../alpha
235 pulling from ../alpha
236 searching for changes
236 searching for changes
237 adding changesets
237 adding changesets
238 adding manifests
238 adding manifests
239 adding file changes
239 adding file changes
240 added 1 changesets with 1 changes to 1 files
240 added 1 changesets with 1 changes to 1 files
241 (run 'hg update' to get a working copy)
241 (run 'hg update' to get a working copy)
242 $ hgph # f54f1bb90ff3 stay draft, not ancestor of -r
242 $ hgph # f54f1bb90ff3 stay draft, not ancestor of -r
243 o 4 public a-D - b555f63b6063
243 o 4 public a-D - b555f63b6063
244 |
244 |
245 | o 3 draft b-A - f54f1bb90ff3
245 | o 3 draft b-A - f54f1bb90ff3
246 | |
246 | |
247 o | 2 public a-C - 54acac6f23ab
247 o | 2 public a-C - 54acac6f23ab
248 |/
248 |/
249 o 1 public a-B - 548a3d25dbf0
249 o 1 public a-B - 548a3d25dbf0
250 |
250 |
251 o 0 public a-A - 054250a37db4
251 o 0 public a-A - 054250a37db4
252
252
253
253
254 pulling from Publish=False to publish=False with some public
254 pulling from Publish=False to publish=False with some public
255
255
256 $ hg up -q f54f1bb90ff3
256 $ hg up -q f54f1bb90ff3
257 $ mkcommit n-A
257 $ mkcommit n-A
258 $ mkcommit n-B
258 $ mkcommit n-B
259 $ hgph
259 $ hgph
260 @ 6 draft n-B - 145e75495359
260 @ 6 draft n-B - 145e75495359
261 |
261 |
262 o 5 draft n-A - d6bcb4f74035
262 o 5 draft n-A - d6bcb4f74035
263 |
263 |
264 | o 4 public a-D - b555f63b6063
264 | o 4 public a-D - b555f63b6063
265 | |
265 | |
266 o | 3 draft b-A - f54f1bb90ff3
266 o | 3 draft b-A - f54f1bb90ff3
267 | |
267 | |
268 | o 2 public a-C - 54acac6f23ab
268 | o 2 public a-C - 54acac6f23ab
269 |/
269 |/
270 o 1 public a-B - 548a3d25dbf0
270 o 1 public a-B - 548a3d25dbf0
271 |
271 |
272 o 0 public a-A - 054250a37db4
272 o 0 public a-A - 054250a37db4
273
273
274 $ cd ../mu
274 $ cd ../mu
275 $ hg pull ../nu
275 $ hg pull ../nu
276 pulling from ../nu
276 pulling from ../nu
277 searching for changes
277 searching for changes
278 adding changesets
278 adding changesets
279 adding manifests
279 adding manifests
280 adding file changes
280 adding file changes
281 added 2 changesets with 2 changes to 2 files
281 added 2 changesets with 2 changes to 2 files
282 (run 'hg update' to get a working copy)
282 (run 'hg update' to get a working copy)
283 $ hgph
283 $ hgph
284 o 6 draft n-B - 145e75495359
284 o 6 draft n-B - 145e75495359
285 |
285 |
286 o 5 draft n-A - d6bcb4f74035
286 o 5 draft n-A - d6bcb4f74035
287 |
287 |
288 | o 4 public a-D - b555f63b6063
288 | o 4 public a-D - b555f63b6063
289 | |
289 | |
290 | o 3 public a-C - 54acac6f23ab
290 | o 3 public a-C - 54acac6f23ab
291 | |
291 | |
292 o | 2 draft b-A - f54f1bb90ff3
292 o | 2 draft b-A - f54f1bb90ff3
293 |/
293 |/
294 o 1 public a-B - 548a3d25dbf0
294 o 1 public a-B - 548a3d25dbf0
295 |
295 |
296 o 0 public a-A - 054250a37db4
296 o 0 public a-A - 054250a37db4
297
297
298 $ cd ..
298 $ cd ..
299
299
300 pulling into publish=True
300 pulling into publish=True
301
301
302 $ cd alpha
302 $ cd alpha
303 $ hgph
303 $ hgph
304 o 4 public b-A - f54f1bb90ff3
304 o 4 public b-A - f54f1bb90ff3
305 |
305 |
306 | @ 3 public a-D - b555f63b6063
306 | @ 3 public a-D - b555f63b6063
307 | |
307 | |
308 | o 2 public a-C - 54acac6f23ab
308 | o 2 public a-C - 54acac6f23ab
309 |/
309 |/
310 o 1 public a-B - 548a3d25dbf0
310 o 1 public a-B - 548a3d25dbf0
311 |
311 |
312 o 0 public a-A - 054250a37db4
312 o 0 public a-A - 054250a37db4
313
313
314 $ hg pull ../mu
314 $ hg pull ../mu
315 pulling from ../mu
315 pulling from ../mu
316 searching for changes
316 searching for changes
317 adding changesets
317 adding changesets
318 adding manifests
318 adding manifests
319 adding file changes
319 adding file changes
320 added 2 changesets with 2 changes to 2 files
320 added 2 changesets with 2 changes to 2 files
321 (run 'hg update' to get a working copy)
321 (run 'hg update' to get a working copy)
322 $ hgph
322 $ hgph
323 o 6 draft n-B - 145e75495359
323 o 6 draft n-B - 145e75495359
324 |
324 |
325 o 5 draft n-A - d6bcb4f74035
325 o 5 draft n-A - d6bcb4f74035
326 |
326 |
327 o 4 public b-A - f54f1bb90ff3
327 o 4 public b-A - f54f1bb90ff3
328 |
328 |
329 | @ 3 public a-D - b555f63b6063
329 | @ 3 public a-D - b555f63b6063
330 | |
330 | |
331 | o 2 public a-C - 54acac6f23ab
331 | o 2 public a-C - 54acac6f23ab
332 |/
332 |/
333 o 1 public a-B - 548a3d25dbf0
333 o 1 public a-B - 548a3d25dbf0
334 |
334 |
335 o 0 public a-A - 054250a37db4
335 o 0 public a-A - 054250a37db4
336
336
337 $ cd ..
337 $ cd ..
338
338
339 pulling back into original repo
339 pulling back into original repo
340
340
341 $ cd nu
341 $ cd nu
342 $ hg pull ../alpha
342 $ hg pull ../alpha
343 pulling from ../alpha
343 pulling from ../alpha
344 searching for changes
344 searching for changes
345 no changes found
345 no changes found
346 $ hgph
346 $ hgph
347 @ 6 public n-B - 145e75495359
347 @ 6 public n-B - 145e75495359
348 |
348 |
349 o 5 public n-A - d6bcb4f74035
349 o 5 public n-A - d6bcb4f74035
350 |
350 |
351 | o 4 public a-D - b555f63b6063
351 | o 4 public a-D - b555f63b6063
352 | |
352 | |
353 o | 3 public b-A - f54f1bb90ff3
353 o | 3 public b-A - f54f1bb90ff3
354 | |
354 | |
355 | o 2 public a-C - 54acac6f23ab
355 | o 2 public a-C - 54acac6f23ab
356 |/
356 |/
357 o 1 public a-B - 548a3d25dbf0
357 o 1 public a-B - 548a3d25dbf0
358 |
358 |
359 o 0 public a-A - 054250a37db4
359 o 0 public a-A - 054250a37db4
360
360
361
361
362 Push
362 Push
363 ````
363 ````
364
364
365 (inserted)
365 (inserted)
366
366
367 Test that phase are pushed even when they are nothing to pus
367 Test that phase are pushed even when they are nothing to pus
368 (this might be tested later bu are very convenient to not alter too much test)
368 (this might be tested later bu are very convenient to not alter too much test)
369
369
370 Push back to alpha
370 Push back to alpha
371
371
372 $ hg push ../alpha # from nu
372 $ hg push ../alpha # from nu
373 pushing to ../alpha
373 pushing to ../alpha
374 searching for changes
374 searching for changes
375 no changes found
375 no changes found
376 [1]
376 [1]
377 $ cd ..
377 $ cd ..
378 $ cd alpha
378 $ cd alpha
379 $ hgph
379 $ hgph
380 o 6 public n-B - 145e75495359
380 o 6 public n-B - 145e75495359
381 |
381 |
382 o 5 public n-A - d6bcb4f74035
382 o 5 public n-A - d6bcb4f74035
383 |
383 |
384 o 4 public b-A - f54f1bb90ff3
384 o 4 public b-A - f54f1bb90ff3
385 |
385 |
386 | @ 3 public a-D - b555f63b6063
386 | @ 3 public a-D - b555f63b6063
387 | |
387 | |
388 | o 2 public a-C - 54acac6f23ab
388 | o 2 public a-C - 54acac6f23ab
389 |/
389 |/
390 o 1 public a-B - 548a3d25dbf0
390 o 1 public a-B - 548a3d25dbf0
391 |
391 |
392 o 0 public a-A - 054250a37db4
392 o 0 public a-A - 054250a37db4
393
393
394
394
395 (end insertion)
395 (end insertion)
396
396
397
397
398 initial setup
398 initial setup
399
399
400 $ hg glog # of alpha
400 $ hg glog # of alpha
401 o changeset: 6:145e75495359
401 o changeset: 6:145e75495359
402 | tag: tip
402 | tag: tip
403 | user: test
403 | user: test
404 | date: Thu Jan 01 00:00:00 1970 +0000
404 | date: Thu Jan 01 00:00:00 1970 +0000
405 | summary: n-B
405 | summary: n-B
406 |
406 |
407 o changeset: 5:d6bcb4f74035
407 o changeset: 5:d6bcb4f74035
408 | user: test
408 | user: test
409 | date: Thu Jan 01 00:00:00 1970 +0000
409 | date: Thu Jan 01 00:00:00 1970 +0000
410 | summary: n-A
410 | summary: n-A
411 |
411 |
412 o changeset: 4:f54f1bb90ff3
412 o changeset: 4:f54f1bb90ff3
413 | parent: 1:548a3d25dbf0
413 | parent: 1:548a3d25dbf0
414 | user: test
414 | user: test
415 | date: Thu Jan 01 00:00:00 1970 +0000
415 | date: Thu Jan 01 00:00:00 1970 +0000
416 | summary: b-A
416 | summary: b-A
417 |
417 |
418 | @ changeset: 3:b555f63b6063
418 | @ changeset: 3:b555f63b6063
419 | | user: test
419 | | user: test
420 | | date: Thu Jan 01 00:00:00 1970 +0000
420 | | date: Thu Jan 01 00:00:00 1970 +0000
421 | | summary: a-D
421 | | summary: a-D
422 | |
422 | |
423 | o changeset: 2:54acac6f23ab
423 | o changeset: 2:54acac6f23ab
424 |/ user: test
424 |/ user: test
425 | date: Thu Jan 01 00:00:00 1970 +0000
425 | date: Thu Jan 01 00:00:00 1970 +0000
426 | summary: a-C
426 | summary: a-C
427 |
427 |
428 o changeset: 1:548a3d25dbf0
428 o changeset: 1:548a3d25dbf0
429 | user: test
429 | user: test
430 | date: Thu Jan 01 00:00:00 1970 +0000
430 | date: Thu Jan 01 00:00:00 1970 +0000
431 | summary: a-B
431 | summary: a-B
432 |
432 |
433 o changeset: 0:054250a37db4
433 o changeset: 0:054250a37db4
434 user: test
434 user: test
435 date: Thu Jan 01 00:00:00 1970 +0000
435 date: Thu Jan 01 00:00:00 1970 +0000
436 summary: a-A
436 summary: a-A
437
437
438 $ mkcommit a-E
438 $ mkcommit a-E
439 $ mkcommit a-F
439 $ mkcommit a-F
440 $ mkcommit a-G
440 $ mkcommit a-G
441 $ hg up d6bcb4f74035 -q
441 $ hg up d6bcb4f74035 -q
442 $ mkcommit a-H
442 $ mkcommit a-H
443 created new head
443 created new head
444 $ hgph
444 $ hgph
445 @ 10 draft a-H - 967b449fbc94
445 @ 10 draft a-H - 967b449fbc94
446 |
446 |
447 | o 9 draft a-G - 3e27b6f1eee1
447 | o 9 draft a-G - 3e27b6f1eee1
448 | |
448 | |
449 | o 8 draft a-F - b740e3e5c05d
449 | o 8 draft a-F - b740e3e5c05d
450 | |
450 | |
451 | o 7 draft a-E - e9f537e46dea
451 | o 7 draft a-E - e9f537e46dea
452 | |
452 | |
453 +---o 6 public n-B - 145e75495359
453 +---o 6 public n-B - 145e75495359
454 | |
454 | |
455 o | 5 public n-A - d6bcb4f74035
455 o | 5 public n-A - d6bcb4f74035
456 | |
456 | |
457 o | 4 public b-A - f54f1bb90ff3
457 o | 4 public b-A - f54f1bb90ff3
458 | |
458 | |
459 | o 3 public a-D - b555f63b6063
459 | o 3 public a-D - b555f63b6063
460 | |
460 | |
461 | o 2 public a-C - 54acac6f23ab
461 | o 2 public a-C - 54acac6f23ab
462 |/
462 |/
463 o 1 public a-B - 548a3d25dbf0
463 o 1 public a-B - 548a3d25dbf0
464 |
464 |
465 o 0 public a-A - 054250a37db4
465 o 0 public a-A - 054250a37db4
466
466
467
467
468 Pulling from bundle does not alter phases of changeset not present in the bundle
469
470 $ hg bundle --base 1 -r 6 -r 3 ../partial-bundle.hg
471 5 changesets found
472 $ hg pull ../partial-bundle.hg
473 pulling from ../partial-bundle.hg
474 searching for changes
475 no changes found
476 $ hgph
477 @ 10 draft a-H - 967b449fbc94
478 |
479 | o 9 draft a-G - 3e27b6f1eee1
480 | |
481 | o 8 draft a-F - b740e3e5c05d
482 | |
483 | o 7 draft a-E - e9f537e46dea
484 | |
485 +---o 6 public n-B - 145e75495359
486 | |
487 o | 5 public n-A - d6bcb4f74035
488 | |
489 o | 4 public b-A - f54f1bb90ff3
490 | |
491 | o 3 public a-D - b555f63b6063
492 | |
493 | o 2 public a-C - 54acac6f23ab
494 |/
495 o 1 public a-B - 548a3d25dbf0
496 |
497 o 0 public a-A - 054250a37db4
498
499
468 Pushing to Publish=False (unknown changeset)
500 Pushing to Publish=False (unknown changeset)
469
501
470 $ hg push ../mu -r b740e3e5c05d # a-F
502 $ hg push ../mu -r b740e3e5c05d # a-F
471 pushing to ../mu
503 pushing to ../mu
472 searching for changes
504 searching for changes
473 adding changesets
505 adding changesets
474 adding manifests
506 adding manifests
475 adding file changes
507 adding file changes
476 added 2 changesets with 2 changes to 2 files
508 added 2 changesets with 2 changes to 2 files
477 $ hgph
509 $ hgph
478 @ 10 draft a-H - 967b449fbc94
510 @ 10 draft a-H - 967b449fbc94
479 |
511 |
480 | o 9 draft a-G - 3e27b6f1eee1
512 | o 9 draft a-G - 3e27b6f1eee1
481 | |
513 | |
482 | o 8 draft a-F - b740e3e5c05d
514 | o 8 draft a-F - b740e3e5c05d
483 | |
515 | |
484 | o 7 draft a-E - e9f537e46dea
516 | o 7 draft a-E - e9f537e46dea
485 | |
517 | |
486 +---o 6 public n-B - 145e75495359
518 +---o 6 public n-B - 145e75495359
487 | |
519 | |
488 o | 5 public n-A - d6bcb4f74035
520 o | 5 public n-A - d6bcb4f74035
489 | |
521 | |
490 o | 4 public b-A - f54f1bb90ff3
522 o | 4 public b-A - f54f1bb90ff3
491 | |
523 | |
492 | o 3 public a-D - b555f63b6063
524 | o 3 public a-D - b555f63b6063
493 | |
525 | |
494 | o 2 public a-C - 54acac6f23ab
526 | o 2 public a-C - 54acac6f23ab
495 |/
527 |/
496 o 1 public a-B - 548a3d25dbf0
528 o 1 public a-B - 548a3d25dbf0
497 |
529 |
498 o 0 public a-A - 054250a37db4
530 o 0 public a-A - 054250a37db4
499
531
500
532
501 $ cd ../mu
533 $ cd ../mu
502 $ hgph # again f54f1bb90ff3, d6bcb4f74035 and 145e75495359 stay draft,
534 $ hgph # again f54f1bb90ff3, d6bcb4f74035 and 145e75495359 stay draft,
503 > # not ancestor of -r
535 > # not ancestor of -r
504 o 8 draft a-F - b740e3e5c05d
536 o 8 draft a-F - b740e3e5c05d
505 |
537 |
506 o 7 draft a-E - e9f537e46dea
538 o 7 draft a-E - e9f537e46dea
507 |
539 |
508 | o 6 draft n-B - 145e75495359
540 | o 6 draft n-B - 145e75495359
509 | |
541 | |
510 | o 5 draft n-A - d6bcb4f74035
542 | o 5 draft n-A - d6bcb4f74035
511 | |
543 | |
512 o | 4 public a-D - b555f63b6063
544 o | 4 public a-D - b555f63b6063
513 | |
545 | |
514 o | 3 public a-C - 54acac6f23ab
546 o | 3 public a-C - 54acac6f23ab
515 | |
547 | |
516 | o 2 draft b-A - f54f1bb90ff3
548 | o 2 draft b-A - f54f1bb90ff3
517 |/
549 |/
518 o 1 public a-B - 548a3d25dbf0
550 o 1 public a-B - 548a3d25dbf0
519 |
551 |
520 o 0 public a-A - 054250a37db4
552 o 0 public a-A - 054250a37db4
521
553
522
554
523 Pushing to Publish=True (unknown changeset)
555 Pushing to Publish=True (unknown changeset)
524
556
525 $ hg push ../beta -r b740e3e5c05d
557 $ hg push ../beta -r b740e3e5c05d
526 pushing to ../beta
558 pushing to ../beta
527 searching for changes
559 searching for changes
528 adding changesets
560 adding changesets
529 adding manifests
561 adding manifests
530 adding file changes
562 adding file changes
531 added 2 changesets with 2 changes to 2 files
563 added 2 changesets with 2 changes to 2 files
532 $ hgph # again f54f1bb90ff3, d6bcb4f74035 and 145e75495359 stay draft,
564 $ hgph # again f54f1bb90ff3, d6bcb4f74035 and 145e75495359 stay draft,
533 > # not ancestor of -r
565 > # not ancestor of -r
534 o 8 public a-F - b740e3e5c05d
566 o 8 public a-F - b740e3e5c05d
535 |
567 |
536 o 7 public a-E - e9f537e46dea
568 o 7 public a-E - e9f537e46dea
537 |
569 |
538 | o 6 draft n-B - 145e75495359
570 | o 6 draft n-B - 145e75495359
539 | |
571 | |
540 | o 5 draft n-A - d6bcb4f74035
572 | o 5 draft n-A - d6bcb4f74035
541 | |
573 | |
542 o | 4 public a-D - b555f63b6063
574 o | 4 public a-D - b555f63b6063
543 | |
575 | |
544 o | 3 public a-C - 54acac6f23ab
576 o | 3 public a-C - 54acac6f23ab
545 | |
577 | |
546 | o 2 draft b-A - f54f1bb90ff3
578 | o 2 draft b-A - f54f1bb90ff3
547 |/
579 |/
548 o 1 public a-B - 548a3d25dbf0
580 o 1 public a-B - 548a3d25dbf0
549 |
581 |
550 o 0 public a-A - 054250a37db4
582 o 0 public a-A - 054250a37db4
551
583
552
584
553 Pushing to Publish=True (common changeset)
585 Pushing to Publish=True (common changeset)
554
586
555 $ cd ../beta
587 $ cd ../beta
556 $ hg push ../alpha
588 $ hg push ../alpha
557 pushing to ../alpha
589 pushing to ../alpha
558 searching for changes
590 searching for changes
559 no changes found
591 no changes found
560 [1]
592 [1]
561 $ hgph
593 $ hgph
562 o 6 public a-F - b740e3e5c05d
594 o 6 public a-F - b740e3e5c05d
563 |
595 |
564 o 5 public a-E - e9f537e46dea
596 o 5 public a-E - e9f537e46dea
565 |
597 |
566 o 4 public a-D - b555f63b6063
598 o 4 public a-D - b555f63b6063
567 |
599 |
568 o 3 public a-C - 54acac6f23ab
600 o 3 public a-C - 54acac6f23ab
569 |
601 |
570 | @ 2 public b-A - f54f1bb90ff3
602 | @ 2 public b-A - f54f1bb90ff3
571 |/
603 |/
572 o 1 public a-B - 548a3d25dbf0
604 o 1 public a-B - 548a3d25dbf0
573 |
605 |
574 o 0 public a-A - 054250a37db4
606 o 0 public a-A - 054250a37db4
575
607
576 $ cd ../alpha
608 $ cd ../alpha
577 $ hgph
609 $ hgph
578 @ 10 draft a-H - 967b449fbc94
610 @ 10 draft a-H - 967b449fbc94
579 |
611 |
580 | o 9 draft a-G - 3e27b6f1eee1
612 | o 9 draft a-G - 3e27b6f1eee1
581 | |
613 | |
582 | o 8 public a-F - b740e3e5c05d
614 | o 8 public a-F - b740e3e5c05d
583 | |
615 | |
584 | o 7 public a-E - e9f537e46dea
616 | o 7 public a-E - e9f537e46dea
585 | |
617 | |
586 +---o 6 public n-B - 145e75495359
618 +---o 6 public n-B - 145e75495359
587 | |
619 | |
588 o | 5 public n-A - d6bcb4f74035
620 o | 5 public n-A - d6bcb4f74035
589 | |
621 | |
590 o | 4 public b-A - f54f1bb90ff3
622 o | 4 public b-A - f54f1bb90ff3
591 | |
623 | |
592 | o 3 public a-D - b555f63b6063
624 | o 3 public a-D - b555f63b6063
593 | |
625 | |
594 | o 2 public a-C - 54acac6f23ab
626 | o 2 public a-C - 54acac6f23ab
595 |/
627 |/
596 o 1 public a-B - 548a3d25dbf0
628 o 1 public a-B - 548a3d25dbf0
597 |
629 |
598 o 0 public a-A - 054250a37db4
630 o 0 public a-A - 054250a37db4
599
631
600
632
601 Pushing to Publish=False (common changeset that change phase + unknown one)
633 Pushing to Publish=False (common changeset that change phase + unknown one)
602
634
603 $ hg push ../mu -r 967b449fbc94 -f
635 $ hg push ../mu -r 967b449fbc94 -f
604 pushing to ../mu
636 pushing to ../mu
605 searching for changes
637 searching for changes
606 adding changesets
638 adding changesets
607 adding manifests
639 adding manifests
608 adding file changes
640 adding file changes
609 added 1 changesets with 1 changes to 1 files (+1 heads)
641 added 1 changesets with 1 changes to 1 files (+1 heads)
610 $ hgph
642 $ hgph
611 @ 10 draft a-H - 967b449fbc94
643 @ 10 draft a-H - 967b449fbc94
612 |
644 |
613 | o 9 draft a-G - 3e27b6f1eee1
645 | o 9 draft a-G - 3e27b6f1eee1
614 | |
646 | |
615 | o 8 public a-F - b740e3e5c05d
647 | o 8 public a-F - b740e3e5c05d
616 | |
648 | |
617 | o 7 public a-E - e9f537e46dea
649 | o 7 public a-E - e9f537e46dea
618 | |
650 | |
619 +---o 6 public n-B - 145e75495359
651 +---o 6 public n-B - 145e75495359
620 | |
652 | |
621 o | 5 public n-A - d6bcb4f74035
653 o | 5 public n-A - d6bcb4f74035
622 | |
654 | |
623 o | 4 public b-A - f54f1bb90ff3
655 o | 4 public b-A - f54f1bb90ff3
624 | |
656 | |
625 | o 3 public a-D - b555f63b6063
657 | o 3 public a-D - b555f63b6063
626 | |
658 | |
627 | o 2 public a-C - 54acac6f23ab
659 | o 2 public a-C - 54acac6f23ab
628 |/
660 |/
629 o 1 public a-B - 548a3d25dbf0
661 o 1 public a-B - 548a3d25dbf0
630 |
662 |
631 o 0 public a-A - 054250a37db4
663 o 0 public a-A - 054250a37db4
632
664
633 $ cd ../mu
665 $ cd ../mu
634 $ hgph # d6bcb4f74035 should have changed phase
666 $ hgph # d6bcb4f74035 should have changed phase
635 > # 145e75495359 is still draft. not ancestor of -r
667 > # 145e75495359 is still draft. not ancestor of -r
636 o 9 draft a-H - 967b449fbc94
668 o 9 draft a-H - 967b449fbc94
637 |
669 |
638 | o 8 public a-F - b740e3e5c05d
670 | o 8 public a-F - b740e3e5c05d
639 | |
671 | |
640 | o 7 public a-E - e9f537e46dea
672 | o 7 public a-E - e9f537e46dea
641 | |
673 | |
642 +---o 6 draft n-B - 145e75495359
674 +---o 6 draft n-B - 145e75495359
643 | |
675 | |
644 o | 5 public n-A - d6bcb4f74035
676 o | 5 public n-A - d6bcb4f74035
645 | |
677 | |
646 | o 4 public a-D - b555f63b6063
678 | o 4 public a-D - b555f63b6063
647 | |
679 | |
648 | o 3 public a-C - 54acac6f23ab
680 | o 3 public a-C - 54acac6f23ab
649 | |
681 | |
650 o | 2 public b-A - f54f1bb90ff3
682 o | 2 public b-A - f54f1bb90ff3
651 |/
683 |/
652 o 1 public a-B - 548a3d25dbf0
684 o 1 public a-B - 548a3d25dbf0
653 |
685 |
654 o 0 public a-A - 054250a37db4
686 o 0 public a-A - 054250a37db4
655
687
656
688
657
689
658 Pushing to Publish=True (common changeset from publish=False)
690 Pushing to Publish=True (common changeset from publish=False)
659
691
660 (in mu)
692 (in mu)
661 $ hg push ../alpha
693 $ hg push ../alpha
662 pushing to ../alpha
694 pushing to ../alpha
663 searching for changes
695 searching for changes
664 no changes found
696 no changes found
665 [1]
697 [1]
666 $ hgph
698 $ hgph
667 o 9 public a-H - 967b449fbc94
699 o 9 public a-H - 967b449fbc94
668 |
700 |
669 | o 8 public a-F - b740e3e5c05d
701 | o 8 public a-F - b740e3e5c05d
670 | |
702 | |
671 | o 7 public a-E - e9f537e46dea
703 | o 7 public a-E - e9f537e46dea
672 | |
704 | |
673 +---o 6 public n-B - 145e75495359
705 +---o 6 public n-B - 145e75495359
674 | |
706 | |
675 o | 5 public n-A - d6bcb4f74035
707 o | 5 public n-A - d6bcb4f74035
676 | |
708 | |
677 | o 4 public a-D - b555f63b6063
709 | o 4 public a-D - b555f63b6063
678 | |
710 | |
679 | o 3 public a-C - 54acac6f23ab
711 | o 3 public a-C - 54acac6f23ab
680 | |
712 | |
681 o | 2 public b-A - f54f1bb90ff3
713 o | 2 public b-A - f54f1bb90ff3
682 |/
714 |/
683 o 1 public a-B - 548a3d25dbf0
715 o 1 public a-B - 548a3d25dbf0
684 |
716 |
685 o 0 public a-A - 054250a37db4
717 o 0 public a-A - 054250a37db4
686
718
687 $ hgph -R ../alpha # a-H should have been synced to 0
719 $ hgph -R ../alpha # a-H should have been synced to 0
688 @ 10 public a-H - 967b449fbc94
720 @ 10 public a-H - 967b449fbc94
689 |
721 |
690 | o 9 draft a-G - 3e27b6f1eee1
722 | o 9 draft a-G - 3e27b6f1eee1
691 | |
723 | |
692 | o 8 public a-F - b740e3e5c05d
724 | o 8 public a-F - b740e3e5c05d
693 | |
725 | |
694 | o 7 public a-E - e9f537e46dea
726 | o 7 public a-E - e9f537e46dea
695 | |
727 | |
696 +---o 6 public n-B - 145e75495359
728 +---o 6 public n-B - 145e75495359
697 | |
729 | |
698 o | 5 public n-A - d6bcb4f74035
730 o | 5 public n-A - d6bcb4f74035
699 | |
731 | |
700 o | 4 public b-A - f54f1bb90ff3
732 o | 4 public b-A - f54f1bb90ff3
701 | |
733 | |
702 | o 3 public a-D - b555f63b6063
734 | o 3 public a-D - b555f63b6063
703 | |
735 | |
704 | o 2 public a-C - 54acac6f23ab
736 | o 2 public a-C - 54acac6f23ab
705 |/
737 |/
706 o 1 public a-B - 548a3d25dbf0
738 o 1 public a-B - 548a3d25dbf0
707 |
739 |
708 o 0 public a-A - 054250a37db4
740 o 0 public a-A - 054250a37db4
709
741
710
742
711
743
712 Discovery locally secret changeset on a remote repository:
744 Discovery locally secret changeset on a remote repository:
713
745
714 - should make it non-secret
746 - should make it non-secret
715
747
716 $ cd ../alpha
748 $ cd ../alpha
717 $ mkcommit A-secret --config phases.new-commit=2
749 $ mkcommit A-secret --config phases.new-commit=2
718 $ hgph
750 $ hgph
719 @ 11 secret A-secret - 435b5d83910c
751 @ 11 secret A-secret - 435b5d83910c
720 |
752 |
721 o 10 public a-H - 967b449fbc94
753 o 10 public a-H - 967b449fbc94
722 |
754 |
723 | o 9 draft a-G - 3e27b6f1eee1
755 | o 9 draft a-G - 3e27b6f1eee1
724 | |
756 | |
725 | o 8 public a-F - b740e3e5c05d
757 | o 8 public a-F - b740e3e5c05d
726 | |
758 | |
727 | o 7 public a-E - e9f537e46dea
759 | o 7 public a-E - e9f537e46dea
728 | |
760 | |
729 +---o 6 public n-B - 145e75495359
761 +---o 6 public n-B - 145e75495359
730 | |
762 | |
731 o | 5 public n-A - d6bcb4f74035
763 o | 5 public n-A - d6bcb4f74035
732 | |
764 | |
733 o | 4 public b-A - f54f1bb90ff3
765 o | 4 public b-A - f54f1bb90ff3
734 | |
766 | |
735 | o 3 public a-D - b555f63b6063
767 | o 3 public a-D - b555f63b6063
736 | |
768 | |
737 | o 2 public a-C - 54acac6f23ab
769 | o 2 public a-C - 54acac6f23ab
738 |/
770 |/
739 o 1 public a-B - 548a3d25dbf0
771 o 1 public a-B - 548a3d25dbf0
740 |
772 |
741 o 0 public a-A - 054250a37db4
773 o 0 public a-A - 054250a37db4
742
774
743 $ hg bundle --base 'parents(.)' -r . ../secret-bundle.hg
775 $ hg bundle --base 'parents(.)' -r . ../secret-bundle.hg
744 1 changesets found
776 1 changesets found
745 $ hg -R ../mu unbundle ../secret-bundle.hg
777 $ hg -R ../mu unbundle ../secret-bundle.hg
746 adding changesets
778 adding changesets
747 adding manifests
779 adding manifests
748 adding file changes
780 adding file changes
749 added 1 changesets with 1 changes to 1 files
781 added 1 changesets with 1 changes to 1 files
750 (run 'hg update' to get a working copy)
782 (run 'hg update' to get a working copy)
751 $ hgph -R ../mu
783 $ hgph -R ../mu
752 o 10 draft A-secret - 435b5d83910c
784 o 10 draft A-secret - 435b5d83910c
753 |
785 |
754 o 9 public a-H - 967b449fbc94
786 o 9 public a-H - 967b449fbc94
755 |
787 |
756 | o 8 public a-F - b740e3e5c05d
788 | o 8 public a-F - b740e3e5c05d
757 | |
789 | |
758 | o 7 public a-E - e9f537e46dea
790 | o 7 public a-E - e9f537e46dea
759 | |
791 | |
760 +---o 6 public n-B - 145e75495359
792 +---o 6 public n-B - 145e75495359
761 | |
793 | |
762 o | 5 public n-A - d6bcb4f74035
794 o | 5 public n-A - d6bcb4f74035
763 | |
795 | |
764 | o 4 public a-D - b555f63b6063
796 | o 4 public a-D - b555f63b6063
765 | |
797 | |
766 | o 3 public a-C - 54acac6f23ab
798 | o 3 public a-C - 54acac6f23ab
767 | |
799 | |
768 o | 2 public b-A - f54f1bb90ff3
800 o | 2 public b-A - f54f1bb90ff3
769 |/
801 |/
770 o 1 public a-B - 548a3d25dbf0
802 o 1 public a-B - 548a3d25dbf0
771 |
803 |
772 o 0 public a-A - 054250a37db4
804 o 0 public a-A - 054250a37db4
773
805
774 $ hg pull ../mu
806 $ hg pull ../mu
775 pulling from ../mu
807 pulling from ../mu
776 searching for changes
808 searching for changes
777 no changes found
809 no changes found
778 $ hgph
810 $ hgph
779 @ 11 draft A-secret - 435b5d83910c
811 @ 11 draft A-secret - 435b5d83910c
780 |
812 |
781 o 10 public a-H - 967b449fbc94
813 o 10 public a-H - 967b449fbc94
782 |
814 |
783 | o 9 draft a-G - 3e27b6f1eee1
815 | o 9 draft a-G - 3e27b6f1eee1
784 | |
816 | |
785 | o 8 public a-F - b740e3e5c05d
817 | o 8 public a-F - b740e3e5c05d
786 | |
818 | |
787 | o 7 public a-E - e9f537e46dea
819 | o 7 public a-E - e9f537e46dea
788 | |
820 | |
789 +---o 6 public n-B - 145e75495359
821 +---o 6 public n-B - 145e75495359
790 | |
822 | |
791 o | 5 public n-A - d6bcb4f74035
823 o | 5 public n-A - d6bcb4f74035
792 | |
824 | |
793 o | 4 public b-A - f54f1bb90ff3
825 o | 4 public b-A - f54f1bb90ff3
794 | |
826 | |
795 | o 3 public a-D - b555f63b6063
827 | o 3 public a-D - b555f63b6063
796 | |
828 | |
797 | o 2 public a-C - 54acac6f23ab
829 | o 2 public a-C - 54acac6f23ab
798 |/
830 |/
799 o 1 public a-B - 548a3d25dbf0
831 o 1 public a-B - 548a3d25dbf0
800 |
832 |
801 o 0 public a-A - 054250a37db4
833 o 0 public a-A - 054250a37db4
802
834
803
835
804 pushing a locally public and draft changesets remotly secret should make them appear on the remote side
836 pushing a locally public and draft changesets remotly secret should make them appear on the remote side
805
837
806 $ hg -R ../mu phase --secret --force 967b449fbc94
838 $ hg -R ../mu phase --secret --force 967b449fbc94
807 $ hg push -r 435b5d83910c ../mu
839 $ hg push -r 435b5d83910c ../mu
808 pushing to ../mu
840 pushing to ../mu
809 searching for changes
841 searching for changes
810 adding changesets
842 adding changesets
811 adding manifests
843 adding manifests
812 adding file changes
844 adding file changes
813 added 0 changesets with 0 changes to 2 files
845 added 0 changesets with 0 changes to 2 files
814 $ hgph -R ../mu
846 $ hgph -R ../mu
815 o 10 draft A-secret - 435b5d83910c
847 o 10 draft A-secret - 435b5d83910c
816 |
848 |
817 o 9 public a-H - 967b449fbc94
849 o 9 public a-H - 967b449fbc94
818 |
850 |
819 | o 8 public a-F - b740e3e5c05d
851 | o 8 public a-F - b740e3e5c05d
820 | |
852 | |
821 | o 7 public a-E - e9f537e46dea
853 | o 7 public a-E - e9f537e46dea
822 | |
854 | |
823 +---o 6 public n-B - 145e75495359
855 +---o 6 public n-B - 145e75495359
824 | |
856 | |
825 o | 5 public n-A - d6bcb4f74035
857 o | 5 public n-A - d6bcb4f74035
826 | |
858 | |
827 | o 4 public a-D - b555f63b6063
859 | o 4 public a-D - b555f63b6063
828 | |
860 | |
829 | o 3 public a-C - 54acac6f23ab
861 | o 3 public a-C - 54acac6f23ab
830 | |
862 | |
831 o | 2 public b-A - f54f1bb90ff3
863 o | 2 public b-A - f54f1bb90ff3
832 |/
864 |/
833 o 1 public a-B - 548a3d25dbf0
865 o 1 public a-B - 548a3d25dbf0
834 |
866 |
835 o 0 public a-A - 054250a37db4
867 o 0 public a-A - 054250a37db4
836
868
837
869
838 pull new changeset with common draft locally
870 pull new changeset with common draft locally
839
871
840 $ hg up -q 967b449fbc94 # create a new root for draft
872 $ hg up -q 967b449fbc94 # create a new root for draft
841 $ mkcommit 'alpha-more'
873 $ mkcommit 'alpha-more'
842 created new head
874 created new head
843 $ hg push -fr . ../mu
875 $ hg push -fr . ../mu
844 pushing to ../mu
876 pushing to ../mu
845 searching for changes
877 searching for changes
846 adding changesets
878 adding changesets
847 adding manifests
879 adding manifests
848 adding file changes
880 adding file changes
849 added 1 changesets with 1 changes to 1 files (+1 heads)
881 added 1 changesets with 1 changes to 1 files (+1 heads)
850 $ cd ../mu
882 $ cd ../mu
851 $ hg phase --secret --force 1c5cfd894796
883 $ hg phase --secret --force 1c5cfd894796
852 $ hg up -q 435b5d83910c
884 $ hg up -q 435b5d83910c
853 $ mkcommit 'mu-more'
885 $ mkcommit 'mu-more'
854 $ cd ../alpha
886 $ cd ../alpha
855 $ hg pull ../mu
887 $ hg pull ../mu
856 pulling from ../mu
888 pulling from ../mu
857 searching for changes
889 searching for changes
858 adding changesets
890 adding changesets
859 adding manifests
891 adding manifests
860 adding file changes
892 adding file changes
861 added 1 changesets with 1 changes to 1 files
893 added 1 changesets with 1 changes to 1 files
862 (run 'hg update' to get a working copy)
894 (run 'hg update' to get a working copy)
863 $ hgph
895 $ hgph
864 o 13 draft mu-more - 5237fb433fc8
896 o 13 draft mu-more - 5237fb433fc8
865 |
897 |
866 | @ 12 draft alpha-more - 1c5cfd894796
898 | @ 12 draft alpha-more - 1c5cfd894796
867 | |
899 | |
868 o | 11 draft A-secret - 435b5d83910c
900 o | 11 draft A-secret - 435b5d83910c
869 |/
901 |/
870 o 10 public a-H - 967b449fbc94
902 o 10 public a-H - 967b449fbc94
871 |
903 |
872 | o 9 draft a-G - 3e27b6f1eee1
904 | o 9 draft a-G - 3e27b6f1eee1
873 | |
905 | |
874 | o 8 public a-F - b740e3e5c05d
906 | o 8 public a-F - b740e3e5c05d
875 | |
907 | |
876 | o 7 public a-E - e9f537e46dea
908 | o 7 public a-E - e9f537e46dea
877 | |
909 | |
878 +---o 6 public n-B - 145e75495359
910 +---o 6 public n-B - 145e75495359
879 | |
911 | |
880 o | 5 public n-A - d6bcb4f74035
912 o | 5 public n-A - d6bcb4f74035
881 | |
913 | |
882 o | 4 public b-A - f54f1bb90ff3
914 o | 4 public b-A - f54f1bb90ff3
883 | |
915 | |
884 | o 3 public a-D - b555f63b6063
916 | o 3 public a-D - b555f63b6063
885 | |
917 | |
886 | o 2 public a-C - 54acac6f23ab
918 | o 2 public a-C - 54acac6f23ab
887 |/
919 |/
888 o 1 public a-B - 548a3d25dbf0
920 o 1 public a-B - 548a3d25dbf0
889 |
921 |
890 o 0 public a-A - 054250a37db4
922 o 0 public a-A - 054250a37db4
891
923
892
924
893 Test that test are properly ignored on remote event when existing locally
925 Test that test are properly ignored on remote event when existing locally
894
926
895 $ cd ..
927 $ cd ..
896 $ hg clone -qU -r b555f63b6063 -r f54f1bb90ff3 beta gamma
928 $ hg clone -qU -r b555f63b6063 -r f54f1bb90ff3 beta gamma
897
929
898 # pathological case are
930 # pathological case are
899 #
931 #
900 # * secret remotely
932 # * secret remotely
901 # * known locally
933 # * known locally
902 # * repo have uncommon changeset
934 # * repo have uncommon changeset
903
935
904 $ hg -R beta phase --secret --force f54f1bb90ff3
936 $ hg -R beta phase --secret --force f54f1bb90ff3
905 $ hg -R gamma phase --draft --force f54f1bb90ff3
937 $ hg -R gamma phase --draft --force f54f1bb90ff3
906
938
907 $ cd gamma
939 $ cd gamma
908 $ hg pull ../beta
940 $ hg pull ../beta
909 pulling from ../beta
941 pulling from ../beta
910 searching for changes
942 searching for changes
911 adding changesets
943 adding changesets
912 adding manifests
944 adding manifests
913 adding file changes
945 adding file changes
914 added 2 changesets with 2 changes to 2 files
946 added 2 changesets with 2 changes to 2 files
915 (run 'hg update' to get a working copy)
947 (run 'hg update' to get a working copy)
916 $ hg phase f54f1bb90ff3
948 $ hg phase f54f1bb90ff3
917 2: draft
949 2: draft
918
950
919 same over the wire
951 same over the wire
920
952
921 $ cd ../beta
953 $ cd ../beta
922 $ hg serve -p $HGPORT -d --pid-file=../beta.pid -E ../beta-error.log
954 $ hg serve -p $HGPORT -d --pid-file=../beta.pid -E ../beta-error.log
923 $ cat ../beta.pid >> $DAEMON_PIDS
955 $ cat ../beta.pid >> $DAEMON_PIDS
924 $ cd ../gamma
956 $ cd ../gamma
925
957
926 $ hg pull http://localhost:$HGPORT/
958 $ hg pull http://localhost:$HGPORT/
927 pulling from http://localhost:$HGPORT/
959 pulling from http://localhost:$HGPORT/
928 searching for changes
960 searching for changes
929 no changes found
961 no changes found
930 $ hg phase f54f1bb90ff3
962 $ hg phase f54f1bb90ff3
931 2: draft
963 2: draft
932
964
933 check that secret local on both side are not synced to public
965 check that secret local on both side are not synced to public
934
966
935 $ hg push -r b555f63b6063 http://localhost:$HGPORT/
967 $ hg push -r b555f63b6063 http://localhost:$HGPORT/
936 pushing to http://localhost:$HGPORT/
968 pushing to http://localhost:$HGPORT/
937 searching for changes
969 searching for changes
938 no changes found
970 no changes found
939 [1]
971 [1]
940 $ hg phase f54f1bb90ff3
972 $ hg phase f54f1bb90ff3
941 2: draft
973 2: draft
942
974
943 put the changeset in the draft state again
975 put the changeset in the draft state again
944 (first test after this one expect to be able to copy)
976 (first test after this one expect to be able to copy)
945
977
946 $ cd ..
978 $ cd ..
947
979
948
980
949 Test Clone behavior
981 Test Clone behavior
950
982
951 A. Clone without secret changeset
983 A. Clone without secret changeset
952
984
953 1. cloning non-publishing repository
985 1. cloning non-publishing repository
954 (Phase should be preserved)
986 (Phase should be preserved)
955
987
956 # make sure there is no secret so we can use a copy clone
988 # make sure there is no secret so we can use a copy clone
957
989
958 $ hg -R mu phase --draft 'secret()'
990 $ hg -R mu phase --draft 'secret()'
959
991
960 $ hg clone -U mu Tau
992 $ hg clone -U mu Tau
961 $ hgph -R Tau
993 $ hgph -R Tau
962 o 12 draft mu-more - 5237fb433fc8
994 o 12 draft mu-more - 5237fb433fc8
963 |
995 |
964 | o 11 draft alpha-more - 1c5cfd894796
996 | o 11 draft alpha-more - 1c5cfd894796
965 | |
997 | |
966 o | 10 draft A-secret - 435b5d83910c
998 o | 10 draft A-secret - 435b5d83910c
967 |/
999 |/
968 o 9 public a-H - 967b449fbc94
1000 o 9 public a-H - 967b449fbc94
969 |
1001 |
970 | o 8 public a-F - b740e3e5c05d
1002 | o 8 public a-F - b740e3e5c05d
971 | |
1003 | |
972 | o 7 public a-E - e9f537e46dea
1004 | o 7 public a-E - e9f537e46dea
973 | |
1005 | |
974 +---o 6 public n-B - 145e75495359
1006 +---o 6 public n-B - 145e75495359
975 | |
1007 | |
976 o | 5 public n-A - d6bcb4f74035
1008 o | 5 public n-A - d6bcb4f74035
977 | |
1009 | |
978 | o 4 public a-D - b555f63b6063
1010 | o 4 public a-D - b555f63b6063
979 | |
1011 | |
980 | o 3 public a-C - 54acac6f23ab
1012 | o 3 public a-C - 54acac6f23ab
981 | |
1013 | |
982 o | 2 public b-A - f54f1bb90ff3
1014 o | 2 public b-A - f54f1bb90ff3
983 |/
1015 |/
984 o 1 public a-B - 548a3d25dbf0
1016 o 1 public a-B - 548a3d25dbf0
985 |
1017 |
986 o 0 public a-A - 054250a37db4
1018 o 0 public a-A - 054250a37db4
987
1019
988
1020
989 2. cloning publishing repository
1021 2. cloning publishing repository
990
1022
991 (everything should be public)
1023 (everything should be public)
992
1024
993 $ hg clone -U alpha Upsilon
1025 $ hg clone -U alpha Upsilon
994 $ hgph -R Upsilon
1026 $ hgph -R Upsilon
995 o 13 public mu-more - 5237fb433fc8
1027 o 13 public mu-more - 5237fb433fc8
996 |
1028 |
997 | o 12 public alpha-more - 1c5cfd894796
1029 | o 12 public alpha-more - 1c5cfd894796
998 | |
1030 | |
999 o | 11 public A-secret - 435b5d83910c
1031 o | 11 public A-secret - 435b5d83910c
1000 |/
1032 |/
1001 o 10 public a-H - 967b449fbc94
1033 o 10 public a-H - 967b449fbc94
1002 |
1034 |
1003 | o 9 public a-G - 3e27b6f1eee1
1035 | o 9 public a-G - 3e27b6f1eee1
1004 | |
1036 | |
1005 | o 8 public a-F - b740e3e5c05d
1037 | o 8 public a-F - b740e3e5c05d
1006 | |
1038 | |
1007 | o 7 public a-E - e9f537e46dea
1039 | o 7 public a-E - e9f537e46dea
1008 | |
1040 | |
1009 +---o 6 public n-B - 145e75495359
1041 +---o 6 public n-B - 145e75495359
1010 | |
1042 | |
1011 o | 5 public n-A - d6bcb4f74035
1043 o | 5 public n-A - d6bcb4f74035
1012 | |
1044 | |
1013 o | 4 public b-A - f54f1bb90ff3
1045 o | 4 public b-A - f54f1bb90ff3
1014 | |
1046 | |
1015 | o 3 public a-D - b555f63b6063
1047 | o 3 public a-D - b555f63b6063
1016 | |
1048 | |
1017 | o 2 public a-C - 54acac6f23ab
1049 | o 2 public a-C - 54acac6f23ab
1018 |/
1050 |/
1019 o 1 public a-B - 548a3d25dbf0
1051 o 1 public a-B - 548a3d25dbf0
1020 |
1052 |
1021 o 0 public a-A - 054250a37db4
1053 o 0 public a-A - 054250a37db4
1022
1054
1023
1055
General Comments 0
You need to be logged in to leave comments. Login now