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