##// END OF EJS Templates
merge with stable
Matt Mackall -
r16662:ea7bf1d4 merge default
parent child Browse files
Show More
@@ -1,910 +1,912 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-2 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Three basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21 21
22 22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24 24
25 25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 26 using MySQL are supported. Requires Python MySQLdb.
27 27
28 28 Writing directly to the database is susceptible to schema changes, and
29 29 relies on a Bugzilla contrib script to send out bug change
30 30 notification emails. This script runs as the user running Mercurial,
31 31 must be run on the host with the Bugzilla install, and requires
32 32 permission to read Bugzilla configuration details and the necessary
33 33 MySQL user and password to have full access rights to the Bugzilla
34 34 database. For these reasons this access mode is now considered
35 35 deprecated, and will not be updated for new Bugzilla versions going
36 36 forward. Only adding comments is supported in this access mode.
37 37
38 38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 39 in the configuration. Comments are added under that username. Since the
40 40 configuration must be readable by all Mercurial users, it is recommended
41 41 that the rights of that user are restricted in Bugzilla to the minimum
42 42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43 43
44 44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 45 email to the Bugzilla email interface to submit comments to bugs.
46 46 The From: address in the email is set to the email address of the Mercurial
47 47 user, so the comment appears to come from the Mercurial user. In the event
48 48 that the Mercurial user email is not recognised by Bugzilla as a Bugzilla
49 49 user, the email associated with the Bugzilla username used to log into
50 50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 51 works on all supported Bugzilla versions.
52 52
53 53 Configuration items common to all access modes:
54 54
55 55 bugzilla.version
56 56 This access type to use. Values recognised are:
57 57
58 58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 62 including 3.0.
63 63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 64 including 2.18.
65 65
66 66 bugzilla.regexp
67 67 Regular expression to match bug IDs for update in changeset commit message.
68 68 It must contain one "()" named group ``<ids>`` containing the bug
69 69 IDs separated by non-digit characters. It may also contain
70 70 a named group ``<hours>`` with a floating-point number giving the
71 71 hours worked on the bug. If no named groups are present, the first
72 72 "()" group is assumed to contain the bug IDs, and work time is not
73 73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 75 variations thereof, followed by an hours number prefixed by ``h`` or
76 76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77 77
78 78 bugzilla.fixregexp
79 79 Regular expression to match bug IDs for marking fixed in changeset
80 80 commit message. This must contain a "()" named group ``<ids>` containing
81 81 the bug IDs separated by non-digit characters. It may also contain
82 82 a named group ``<hours>`` with a floating-point number giving the
83 83 hours worked on the bug. If no named groups are present, the first
84 84 "()" group is assumed to contain the bug IDs, and work time is not
85 85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 87 variations thereof, followed by an hours number prefixed by ``h`` or
88 88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89 89
90 90 bugzilla.fixstatus
91 91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92 92
93 93 bugzilla.fixresolution
94 94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95 95
96 96 bugzilla.style
97 97 The style file to use when formatting comments.
98 98
99 99 bugzilla.template
100 100 Template to use when formatting comments. Overrides style if
101 101 specified. In addition to the usual Mercurial keywords, the
102 102 extension specifies:
103 103
104 104 :``{bug}``: The Bugzilla bug ID.
105 105 :``{root}``: The full pathname of the Mercurial repository.
106 106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108 108
109 109 Default ``changeset {node|short} in repo {root} refers to bug
110 110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111 111
112 112 bugzilla.strip
113 113 The number of path separator characters to strip from the front of
114 114 the Mercurial repository path (``{root}`` in templates) to produce
115 115 ``{webroot}``. For example, a repository with ``{root}``
116 116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 117 ``{webroot}`` of ``my-project``. Default 0.
118 118
119 119 web.baseurl
120 120 Base URL for browsing Mercurial repositories. Referenced from
121 121 templates as ``{hgweb}``.
122 122
123 123 Configuration items common to XMLRPC+email and MySQL access modes:
124 124
125 125 bugzilla.usermap
126 126 Path of file containing Mercurial committer email to Bugzilla user email
127 127 mappings. If specified, the file should contain one mapping per
128 128 line::
129 129
130 130 committer = Bugzilla user
131 131
132 132 See also the ``[usermap]`` section.
133 133
134 134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 136 Contains entries of the form ``committer = Bugzilla user``.
137 137
138 138 XMLRPC access mode configuration:
139 139
140 140 bugzilla.bzurl
141 141 The base URL for the Bugzilla installation.
142 142 Default ``http://localhost/bugzilla``.
143 143
144 144 bugzilla.user
145 145 The username to use to log into Bugzilla via XMLRPC. Default
146 146 ``bugs``.
147 147
148 148 bugzilla.password
149 149 The password for Bugzilla login.
150 150
151 151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 152 and also:
153 153
154 154 bugzilla.bzemail
155 155 The Bugzilla email address.
156 156
157 157 In addition, the Mercurial email settings must be configured. See the
158 158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159 159
160 160 MySQL access mode configuration:
161 161
162 162 bugzilla.host
163 163 Hostname of the MySQL server holding the Bugzilla database.
164 164 Default ``localhost``.
165 165
166 166 bugzilla.db
167 167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168 168
169 169 bugzilla.user
170 170 Username to use to access MySQL server. Default ``bugs``.
171 171
172 172 bugzilla.password
173 173 Password to use to access MySQL server.
174 174
175 175 bugzilla.timeout
176 176 Database connection timeout (seconds). Default 5.
177 177
178 178 bugzilla.bzuser
179 179 Fallback Bugzilla user name to record comments with, if changeset
180 180 committer cannot be found as a Bugzilla user.
181 181
182 182 bugzilla.bzdir
183 183 Bugzilla install directory. Used by default notify. Default
184 184 ``/var/www/html/bugzilla``.
185 185
186 186 bugzilla.notify
187 187 The command to run to get Bugzilla to send bug change notification
188 188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 189 id) and ``user`` (committer bugzilla email). Default depends on
190 190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 191 contrib/sendbugmail.pl %(id)s %(user)s".
192 192
193 193 Activating the extension::
194 194
195 195 [extensions]
196 196 bugzilla =
197 197
198 198 [hooks]
199 199 # run bugzilla hook on every change pulled or pushed in here
200 200 incoming.bugzilla = python:hgext.bugzilla.hook
201 201
202 202 Example configurations:
203 203
204 204 XMLRPC example configuration. This uses the Bugzilla at
205 205 ``http://my-project.org/bugzilla``, logging in as user
206 206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 208 with a web interface at ``http://my-project.org/hg``. ::
209 209
210 210 [bugzilla]
211 211 bzurl=http://my-project.org/bugzilla
212 212 user=bugmail@my-project.org
213 213 password=plugh
214 214 version=xmlrpc
215 215 template=Changeset {node|short} in {root|basename}.
216 216 {hgweb}/{webroot}/rev/{node|short}\\n
217 217 {desc}\\n
218 218 strip=5
219 219
220 220 [web]
221 221 baseurl=http://my-project.org/hg
222 222
223 223 XMLRPC+email example configuration. This uses the Bugzilla at
224 224 ``http://my-project.org/bugzilla``, logging in as user
225 225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 228 are sent to the Bugzilla email address
229 229 ``bugzilla@my-project.org``. ::
230 230
231 231 [bugzilla]
232 232 bzurl=http://my-project.org/bugzilla
233 233 user=bugmail@my-project.org
234 234 password=plugh
235 235 version=xmlrpc
236 236 bzemail=bugzilla@my-project.org
237 237 template=Changeset {node|short} in {root|basename}.
238 238 {hgweb}/{webroot}/rev/{node|short}\\n
239 239 {desc}\\n
240 240 strip=5
241 241
242 242 [web]
243 243 baseurl=http://my-project.org/hg
244 244
245 245 [usermap]
246 246 user@emaildomain.com=user.name@bugzilladomain.com
247 247
248 248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 250 the Bugzilla database name is ``bugs`` and MySQL is
251 251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 253 with a web interface at ``http://my-project.org/hg``. ::
254 254
255 255 [bugzilla]
256 256 host=localhost
257 257 password=XYZZY
258 258 version=3.0
259 259 bzuser=unknown@domain.com
260 260 bzdir=/opt/bugzilla-3.2
261 261 template=Changeset {node|short} in {root|basename}.
262 262 {hgweb}/{webroot}/rev/{node|short}\\n
263 263 {desc}\\n
264 264 strip=5
265 265
266 266 [web]
267 267 baseurl=http://my-project.org/hg
268 268
269 269 [usermap]
270 270 user@emaildomain.com=user.name@bugzilladomain.com
271 271
272 272 All the above add a comment to the Bugzilla bug record of the form::
273 273
274 274 Changeset 3b16791d6642 in repository-name.
275 275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276 276
277 277 Changeset commit comment. Bug 1234.
278 278 '''
279 279
280 280 from mercurial.i18n import _
281 281 from mercurial.node import short
282 282 from mercurial import cmdutil, mail, templater, util
283 283 import re, time, urlparse, xmlrpclib
284 284
285 285 class bzaccess(object):
286 286 '''Base class for access to Bugzilla.'''
287 287
288 288 def __init__(self, ui):
289 289 self.ui = ui
290 290 usermap = self.ui.config('bugzilla', 'usermap')
291 291 if usermap:
292 292 self.ui.readconfig(usermap, sections=['usermap'])
293 293
294 294 def map_committer(self, user):
295 295 '''map name of committer to Bugzilla user name.'''
296 296 for committer, bzuser in self.ui.configitems('usermap'):
297 297 if committer.lower() == user.lower():
298 298 return bzuser
299 299 return user
300 300
301 301 # Methods to be implemented by access classes.
302 302 #
303 303 # 'bugs' is a dict keyed on bug id, where values are a dict holding
304 304 # updates to bug state. Recognised dict keys are:
305 305 #
306 306 # 'hours': Value, float containing work hours to be updated.
307 307 # 'fix': If key present, bug is to be marked fixed. Value ignored.
308 308
309 309 def filter_real_bug_ids(self, bugs):
310 310 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
311 311 pass
312 312
313 313 def filter_cset_known_bug_ids(self, node, bugs):
314 314 '''remove bug IDs where node occurs in comment text from bugs.'''
315 315 pass
316 316
317 317 def updatebug(self, bugid, newstate, text, committer):
318 318 '''update the specified bug. Add comment text and set new states.
319 319
320 320 If possible add the comment as being from the committer of
321 321 the changeset. Otherwise use the default Bugzilla user.
322 322 '''
323 323 pass
324 324
325 325 def notify(self, bugs, committer):
326 326 '''Force sending of Bugzilla notification emails.
327 327
328 328 Only required if the access method does not trigger notification
329 329 emails automatically.
330 330 '''
331 331 pass
332 332
333 333 # Bugzilla via direct access to MySQL database.
334 334 class bzmysql(bzaccess):
335 335 '''Support for direct MySQL access to Bugzilla.
336 336
337 337 The earliest Bugzilla version this is tested with is version 2.16.
338 338
339 339 If your Bugzilla is version 3.4 or above, you are strongly
340 340 recommended to use the XMLRPC access method instead.
341 341 '''
342 342
343 343 @staticmethod
344 344 def sql_buglist(ids):
345 345 '''return SQL-friendly list of bug ids'''
346 346 return '(' + ','.join(map(str, ids)) + ')'
347 347
348 348 _MySQLdb = None
349 349
350 350 def __init__(self, ui):
351 351 try:
352 352 import MySQLdb as mysql
353 353 bzmysql._MySQLdb = mysql
354 354 except ImportError, err:
355 355 raise util.Abort(_('python mysql support not available: %s') % err)
356 356
357 357 bzaccess.__init__(self, ui)
358 358
359 359 host = self.ui.config('bugzilla', 'host', 'localhost')
360 360 user = self.ui.config('bugzilla', 'user', 'bugs')
361 361 passwd = self.ui.config('bugzilla', 'password')
362 362 db = self.ui.config('bugzilla', 'db', 'bugs')
363 363 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
364 364 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
365 365 (host, db, user, '*' * len(passwd)))
366 366 self.conn = bzmysql._MySQLdb.connect(host=host,
367 367 user=user, passwd=passwd,
368 368 db=db,
369 369 connect_timeout=timeout)
370 370 self.cursor = self.conn.cursor()
371 371 self.longdesc_id = self.get_longdesc_id()
372 372 self.user_ids = {}
373 373 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
374 374
375 375 def run(self, *args, **kwargs):
376 376 '''run a query.'''
377 377 self.ui.note(_('query: %s %s\n') % (args, kwargs))
378 378 try:
379 379 self.cursor.execute(*args, **kwargs)
380 380 except bzmysql._MySQLdb.MySQLError:
381 381 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
382 382 raise
383 383
384 384 def get_longdesc_id(self):
385 385 '''get identity of longdesc field'''
386 386 self.run('select fieldid from fielddefs where name = "longdesc"')
387 387 ids = self.cursor.fetchall()
388 388 if len(ids) != 1:
389 389 raise util.Abort(_('unknown database schema'))
390 390 return ids[0][0]
391 391
392 392 def filter_real_bug_ids(self, bugs):
393 393 '''filter not-existing bugs from set.'''
394 394 self.run('select bug_id from bugs where bug_id in %s' %
395 395 bzmysql.sql_buglist(bugs.keys()))
396 396 existing = [id for (id,) in self.cursor.fetchall()]
397 397 for id in bugs.keys():
398 398 if id not in existing:
399 399 self.ui.status(_('bug %d does not exist\n') % id)
400 400 del bugs[id]
401 401
402 402 def filter_cset_known_bug_ids(self, node, bugs):
403 403 '''filter bug ids that already refer to this changeset from set.'''
404 404 self.run('''select bug_id from longdescs where
405 405 bug_id in %s and thetext like "%%%s%%"''' %
406 406 (bzmysql.sql_buglist(bugs.keys()), short(node)))
407 407 for (id,) in self.cursor.fetchall():
408 408 self.ui.status(_('bug %d already knows about changeset %s\n') %
409 409 (id, short(node)))
410 410 del bugs[id]
411 411
412 412 def notify(self, bugs, committer):
413 413 '''tell bugzilla to send mail.'''
414 414 self.ui.status(_('telling bugzilla to send mail:\n'))
415 415 (user, userid) = self.get_bugzilla_user(committer)
416 416 for id in bugs.keys():
417 417 self.ui.status(_(' bug %s\n') % id)
418 418 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
419 419 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
420 420 try:
421 421 # Backwards-compatible with old notify string, which
422 422 # took one string. This will throw with a new format
423 423 # string.
424 424 cmd = cmdfmt % id
425 425 except TypeError:
426 426 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
427 427 self.ui.note(_('running notify command %s\n') % cmd)
428 428 fp = util.popen('(%s) 2>&1' % cmd)
429 429 out = fp.read()
430 430 ret = fp.close()
431 431 if ret:
432 432 self.ui.warn(out)
433 433 raise util.Abort(_('bugzilla notify command %s') %
434 434 util.explainexit(ret)[0])
435 435 self.ui.status(_('done\n'))
436 436
437 437 def get_user_id(self, user):
438 438 '''look up numeric bugzilla user id.'''
439 439 try:
440 440 return self.user_ids[user]
441 441 except KeyError:
442 442 try:
443 443 userid = int(user)
444 444 except ValueError:
445 445 self.ui.note(_('looking up user %s\n') % user)
446 446 self.run('''select userid from profiles
447 447 where login_name like %s''', user)
448 448 all = self.cursor.fetchall()
449 449 if len(all) != 1:
450 450 raise KeyError(user)
451 451 userid = int(all[0][0])
452 452 self.user_ids[user] = userid
453 453 return userid
454 454
455 455 def get_bugzilla_user(self, committer):
456 456 '''See if committer is a registered bugzilla user. Return
457 457 bugzilla username and userid if so. If not, return default
458 458 bugzilla username and userid.'''
459 459 user = self.map_committer(committer)
460 460 try:
461 461 userid = self.get_user_id(user)
462 462 except KeyError:
463 463 try:
464 464 defaultuser = self.ui.config('bugzilla', 'bzuser')
465 465 if not defaultuser:
466 466 raise util.Abort(_('cannot find bugzilla user id for %s') %
467 467 user)
468 468 userid = self.get_user_id(defaultuser)
469 469 user = defaultuser
470 470 except KeyError:
471 471 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
472 472 (user, defaultuser))
473 473 return (user, userid)
474 474
475 475 def updatebug(self, bugid, newstate, text, committer):
476 476 '''update bug state with comment text.
477 477
478 478 Try adding comment as committer of changeset, otherwise as
479 479 default bugzilla user.'''
480 480 if len(newstate) > 0:
481 481 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
482 482
483 483 (user, userid) = self.get_bugzilla_user(committer)
484 484 now = time.strftime('%Y-%m-%d %H:%M:%S')
485 485 self.run('''insert into longdescs
486 486 (bug_id, who, bug_when, thetext)
487 487 values (%s, %s, %s, %s)''',
488 488 (bugid, userid, now, text))
489 489 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
490 490 values (%s, %s, %s, %s)''',
491 491 (bugid, userid, now, self.longdesc_id))
492 492 self.conn.commit()
493 493
494 494 class bzmysql_2_18(bzmysql):
495 495 '''support for bugzilla 2.18 series.'''
496 496
497 497 def __init__(self, ui):
498 498 bzmysql.__init__(self, ui)
499 499 self.default_notify = \
500 500 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
501 501
502 502 class bzmysql_3_0(bzmysql_2_18):
503 503 '''support for bugzilla 3.0 series.'''
504 504
505 505 def __init__(self, ui):
506 506 bzmysql_2_18.__init__(self, ui)
507 507
508 508 def get_longdesc_id(self):
509 509 '''get identity of longdesc field'''
510 510 self.run('select id from fielddefs where name = "longdesc"')
511 511 ids = self.cursor.fetchall()
512 512 if len(ids) != 1:
513 513 raise util.Abort(_('unknown database schema'))
514 514 return ids[0][0]
515 515
516 516 # Buzgilla via XMLRPC interface.
517 517
518 518 class cookietransportrequest(object):
519 519 """A Transport request method that retains cookies over its lifetime.
520 520
521 521 The regular xmlrpclib transports ignore cookies. Which causes
522 522 a bit of a problem when you need a cookie-based login, as with
523 523 the Bugzilla XMLRPC interface.
524 524
525 525 So this is a helper for defining a Transport which looks for
526 526 cookies being set in responses and saves them to add to all future
527 527 requests.
528 528 """
529 529
530 530 # Inspiration drawn from
531 531 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
532 532 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
533 533
534 534 cookies = []
535 535 def send_cookies(self, connection):
536 536 if self.cookies:
537 537 for cookie in self.cookies:
538 538 connection.putheader("Cookie", cookie)
539 539
540 540 def request(self, host, handler, request_body, verbose=0):
541 541 self.verbose = verbose
542 542 self.accept_gzip_encoding = False
543 543
544 544 # issue XML-RPC request
545 545 h = self.make_connection(host)
546 546 if verbose:
547 547 h.set_debuglevel(1)
548 548
549 549 self.send_request(h, handler, request_body)
550 550 self.send_host(h, host)
551 551 self.send_cookies(h)
552 552 self.send_user_agent(h)
553 553 self.send_content(h, request_body)
554 554
555 555 # Deal with differences between Python 2.4-2.6 and 2.7.
556 556 # In the former h is a HTTP(S). In the latter it's a
557 557 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
558 558 # HTTP(S) has an underlying HTTP(S)Connection, so extract
559 559 # that and use it.
560 560 try:
561 561 response = h.getresponse()
562 562 except AttributeError:
563 563 response = h._conn.getresponse()
564 564
565 565 # Add any cookie definitions to our list.
566 566 for header in response.msg.getallmatchingheaders("Set-Cookie"):
567 567 val = header.split(": ", 1)[1]
568 568 cookie = val.split(";", 1)[0]
569 569 self.cookies.append(cookie)
570 570
571 571 if response.status != 200:
572 572 raise xmlrpclib.ProtocolError(host + handler, response.status,
573 573 response.reason, response.msg.headers)
574 574
575 575 payload = response.read()
576 576 parser, unmarshaller = self.getparser()
577 577 parser.feed(payload)
578 578 parser.close()
579 579
580 580 return unmarshaller.close()
581 581
582 582 # The explicit calls to the underlying xmlrpclib __init__() methods are
583 583 # necessary. The xmlrpclib.Transport classes are old-style classes, and
584 584 # it turns out their __init__() doesn't get called when doing multiple
585 585 # inheritance with a new-style class.
586 586 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
587 587 def __init__(self, use_datetime=0):
588 xmlrpclib.Transport.__init__(self, use_datetime)
588 if util.safehasattr(xmlrpclib.Transport, "__init__"):
589 xmlrpclib.Transport.__init__(self, use_datetime)
589 590
590 591 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
591 592 def __init__(self, use_datetime=0):
592 xmlrpclib.SafeTransport.__init__(self, use_datetime)
593 if util.safehasattr(xmlrpclib.Transport, "__init__"):
594 xmlrpclib.SafeTransport.__init__(self, use_datetime)
593 595
594 596 class bzxmlrpc(bzaccess):
595 597 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
596 598
597 599 Requires a minimum Bugzilla version 3.4.
598 600 """
599 601
600 602 def __init__(self, ui):
601 603 bzaccess.__init__(self, ui)
602 604
603 605 bzweb = self.ui.config('bugzilla', 'bzurl',
604 606 'http://localhost/bugzilla/')
605 607 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
606 608
607 609 user = self.ui.config('bugzilla', 'user', 'bugs')
608 610 passwd = self.ui.config('bugzilla', 'password')
609 611
610 612 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
611 613 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
612 614 'FIXED')
613 615
614 616 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
615 617 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
616 618 self.bzvermajor = int(ver[0])
617 619 self.bzverminor = int(ver[1])
618 620 self.bzproxy.User.login(dict(login=user, password=passwd))
619 621
620 622 def transport(self, uri):
621 623 if urlparse.urlparse(uri, "http")[0] == "https":
622 624 return cookiesafetransport()
623 625 else:
624 626 return cookietransport()
625 627
626 628 def get_bug_comments(self, id):
627 629 """Return a string with all comment text for a bug."""
628 630 c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text']))
629 631 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
630 632
631 633 def filter_real_bug_ids(self, bugs):
632 634 probe = self.bzproxy.Bug.get(dict(ids=sorted(bugs.keys()),
633 635 include_fields=[],
634 636 permissive=True))
635 637 for badbug in probe['faults']:
636 638 id = badbug['id']
637 639 self.ui.status(_('bug %d does not exist\n') % id)
638 640 del bugs[id]
639 641
640 642 def filter_cset_known_bug_ids(self, node, bugs):
641 643 for id in sorted(bugs.keys()):
642 644 if self.get_bug_comments(id).find(short(node)) != -1:
643 645 self.ui.status(_('bug %d already knows about changeset %s\n') %
644 646 (id, short(node)))
645 647 del bugs[id]
646 648
647 649 def updatebug(self, bugid, newstate, text, committer):
648 650 args = {}
649 651 if 'hours' in newstate:
650 652 args['work_time'] = newstate['hours']
651 653
652 654 if self.bzvermajor >= 4:
653 655 args['ids'] = [bugid]
654 656 args['comment'] = {'body' : text}
655 657 args['status'] = self.fixstatus
656 658 args['resolution'] = self.fixresolution
657 659 self.bzproxy.Bug.update(args)
658 660 else:
659 661 if 'fix' in newstate:
660 662 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
661 663 "to mark bugs fixed\n"))
662 664 args['id'] = bugid
663 665 args['comment'] = text
664 666 self.bzproxy.Bug.add_comment(args)
665 667
666 668 class bzxmlrpcemail(bzxmlrpc):
667 669 """Read data from Bugzilla via XMLRPC, send updates via email.
668 670
669 671 Advantages of sending updates via email:
670 672 1. Comments can be added as any user, not just logged in user.
671 673 2. Bug statuses or other fields not accessible via XMLRPC can
672 674 potentially be updated.
673 675
674 676 There is no XMLRPC function to change bug status before Bugzilla
675 677 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
676 678 But bugs can be marked fixed via email from 3.4 onwards.
677 679 """
678 680
679 681 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
680 682 # in-email fields are specified as '@<fieldname> = <value>'. In
681 683 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
682 684 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
683 685 # compatibility, but rather than rely on this use the new format for
684 686 # 4.0 onwards.
685 687
686 688 def __init__(self, ui):
687 689 bzxmlrpc.__init__(self, ui)
688 690
689 691 self.bzemail = self.ui.config('bugzilla', 'bzemail')
690 692 if not self.bzemail:
691 693 raise util.Abort(_("configuration 'bzemail' missing"))
692 694 mail.validateconfig(self.ui)
693 695
694 696 def makecommandline(self, fieldname, value):
695 697 if self.bzvermajor >= 4:
696 698 return "@%s %s" % (fieldname, str(value))
697 699 else:
698 700 if fieldname == "id":
699 701 fieldname = "bug_id"
700 702 return "@%s = %s" % (fieldname, str(value))
701 703
702 704 def send_bug_modify_email(self, bugid, commands, comment, committer):
703 705 '''send modification message to Bugzilla bug via email.
704 706
705 707 The message format is documented in the Bugzilla email_in.pl
706 708 specification. commands is a list of command lines, comment is the
707 709 comment text.
708 710
709 711 To stop users from crafting commit comments with
710 712 Bugzilla commands, specify the bug ID via the message body, rather
711 713 than the subject line, and leave a blank line after it.
712 714 '''
713 715 user = self.map_committer(committer)
714 716 matches = self.bzproxy.User.get(dict(match=[user]))
715 717 if not matches['users']:
716 718 user = self.ui.config('bugzilla', 'user', 'bugs')
717 719 matches = self.bzproxy.User.get(dict(match=[user]))
718 720 if not matches['users']:
719 721 raise util.Abort(_("default bugzilla user %s email not found") %
720 722 user)
721 723 user = matches['users'][0]['email']
722 724 commands.append(self.makecommandline("id", bugid))
723 725
724 726 text = "\n".join(commands) + "\n\n" + comment
725 727
726 728 _charsets = mail._charsets(self.ui)
727 729 user = mail.addressencode(self.ui, user, _charsets)
728 730 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
729 731 msg = mail.mimeencode(self.ui, text, _charsets)
730 732 msg['From'] = user
731 733 msg['To'] = bzemail
732 734 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
733 735 sendmail = mail.connect(self.ui)
734 736 sendmail(user, bzemail, msg.as_string())
735 737
736 738 def updatebug(self, bugid, newstate, text, committer):
737 739 cmds = []
738 740 if 'hours' in newstate:
739 741 cmds.append(self.makecommandline("work_time", newstate['hours']))
740 742 if 'fix' in newstate:
741 743 cmds.append(self.makecommandline("bug_status", self.fixstatus))
742 744 cmds.append(self.makecommandline("resolution", self.fixresolution))
743 745 self.send_bug_modify_email(bugid, cmds, text, committer)
744 746
745 747 class bugzilla(object):
746 748 # supported versions of bugzilla. different versions have
747 749 # different schemas.
748 750 _versions = {
749 751 '2.16': bzmysql,
750 752 '2.18': bzmysql_2_18,
751 753 '3.0': bzmysql_3_0,
752 754 'xmlrpc': bzxmlrpc,
753 755 'xmlrpc+email': bzxmlrpcemail
754 756 }
755 757
756 758 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
757 759 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
758 760 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
759 761
760 762 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
761 763 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
762 764 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
763 765 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
764 766
765 767 _bz = None
766 768
767 769 def __init__(self, ui, repo):
768 770 self.ui = ui
769 771 self.repo = repo
770 772
771 773 def bz(self):
772 774 '''return object that knows how to talk to bugzilla version in
773 775 use.'''
774 776
775 777 if bugzilla._bz is None:
776 778 bzversion = self.ui.config('bugzilla', 'version')
777 779 try:
778 780 bzclass = bugzilla._versions[bzversion]
779 781 except KeyError:
780 782 raise util.Abort(_('bugzilla version %s not supported') %
781 783 bzversion)
782 784 bugzilla._bz = bzclass(self.ui)
783 785 return bugzilla._bz
784 786
785 787 def __getattr__(self, key):
786 788 return getattr(self.bz(), key)
787 789
788 790 _bug_re = None
789 791 _fix_re = None
790 792 _split_re = None
791 793
792 794 def find_bugs(self, ctx):
793 795 '''return bugs dictionary created from commit comment.
794 796
795 797 Extract bug info from changeset comments. Filter out any that are
796 798 not known to Bugzilla, and any that already have a reference to
797 799 the given changeset in their comments.
798 800 '''
799 801 if bugzilla._bug_re is None:
800 802 bugzilla._bug_re = re.compile(
801 803 self.ui.config('bugzilla', 'regexp',
802 804 bugzilla._default_bug_re), re.IGNORECASE)
803 805 bugzilla._fix_re = re.compile(
804 806 self.ui.config('bugzilla', 'fixregexp',
805 807 bugzilla._default_fix_re), re.IGNORECASE)
806 808 bugzilla._split_re = re.compile(r'\D+')
807 809 start = 0
808 810 hours = 0.0
809 811 bugs = {}
810 812 bugmatch = bugzilla._bug_re.search(ctx.description(), start)
811 813 fixmatch = bugzilla._fix_re.search(ctx.description(), start)
812 814 while True:
813 815 bugattribs = {}
814 816 if not bugmatch and not fixmatch:
815 817 break
816 818 if not bugmatch:
817 819 m = fixmatch
818 820 elif not fixmatch:
819 821 m = bugmatch
820 822 else:
821 823 if bugmatch.start() < fixmatch.start():
822 824 m = bugmatch
823 825 else:
824 826 m = fixmatch
825 827 start = m.end()
826 828 if m is bugmatch:
827 829 bugmatch = bugzilla._bug_re.search(ctx.description(), start)
828 830 if 'fix' in bugattribs:
829 831 del bugattribs['fix']
830 832 else:
831 833 fixmatch = bugzilla._fix_re.search(ctx.description(), start)
832 834 bugattribs['fix'] = None
833 835
834 836 try:
835 837 ids = m.group('ids')
836 838 except IndexError:
837 839 ids = m.group(1)
838 840 try:
839 841 hours = float(m.group('hours'))
840 842 bugattribs['hours'] = hours
841 843 except IndexError:
842 844 pass
843 845 except TypeError:
844 846 pass
845 847 except ValueError:
846 848 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
847 849
848 850 for id in bugzilla._split_re.split(ids):
849 851 if not id:
850 852 continue
851 853 bugs[int(id)] = bugattribs
852 854 if bugs:
853 855 self.filter_real_bug_ids(bugs)
854 856 if bugs:
855 857 self.filter_cset_known_bug_ids(ctx.node(), bugs)
856 858 return bugs
857 859
858 860 def update(self, bugid, newstate, ctx):
859 861 '''update bugzilla bug with reference to changeset.'''
860 862
861 863 def webroot(root):
862 864 '''strip leading prefix of repo root and turn into
863 865 url-safe path.'''
864 866 count = int(self.ui.config('bugzilla', 'strip', 0))
865 867 root = util.pconvert(root)
866 868 while count > 0:
867 869 c = root.find('/')
868 870 if c == -1:
869 871 break
870 872 root = root[c + 1:]
871 873 count -= 1
872 874 return root
873 875
874 876 mapfile = self.ui.config('bugzilla', 'style')
875 877 tmpl = self.ui.config('bugzilla', 'template')
876 878 t = cmdutil.changeset_templater(self.ui, self.repo,
877 879 False, None, mapfile, False)
878 880 if not mapfile and not tmpl:
879 881 tmpl = _('changeset {node|short} in repo {root} refers '
880 882 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
881 883 if tmpl:
882 884 tmpl = templater.parsestring(tmpl, quoted=False)
883 885 t.use_template(tmpl)
884 886 self.ui.pushbuffer()
885 887 t.show(ctx, changes=ctx.changeset(),
886 888 bug=str(bugid),
887 889 hgweb=self.ui.config('web', 'baseurl'),
888 890 root=self.repo.root,
889 891 webroot=webroot(self.repo.root))
890 892 data = self.ui.popbuffer()
891 893 self.updatebug(bugid, newstate, data, util.email(ctx.user()))
892 894
893 895 def hook(ui, repo, hooktype, node=None, **kwargs):
894 896 '''add comment to bugzilla for each changeset that refers to a
895 897 bugzilla bug id. only add a comment once per bug, so same change
896 898 seen multiple times does not fill bug with duplicate data.'''
897 899 if node is None:
898 900 raise util.Abort(_('hook type %s does not pass a changeset id') %
899 901 hooktype)
900 902 try:
901 903 bz = bugzilla(ui, repo)
902 904 ctx = repo[node]
903 905 bugs = bz.find_bugs(ctx)
904 906 if bugs:
905 907 for bug in bugs:
906 908 bz.update(bug, bugs[bug], ctx)
907 909 bz.notify(bugs, util.email(ctx.user()))
908 910 except Exception, e:
909 911 raise util.Abort(_('Bugzilla error: %s') % e)
910 912
@@ -1,103 +1,101 b''
1 1 # pager.py - display output using a pager
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 # To load the extension, add it to your configuration file:
9 9 #
10 10 # [extension]
11 11 # pager =
12 12 #
13 13 # Run "hg help pager" to get info on configuration.
14 14
15 15 '''browse command output with an external pager
16 16
17 17 To set the pager that should be used, set the application variable::
18 18
19 19 [pager]
20 20 pager = less -FRSX
21 21
22 22 If no pager is set, the pager extensions uses the environment variable
23 23 $PAGER. If neither pager.pager, nor $PAGER is set, no pager is used.
24 24
25 25 You can disable the pager for certain commands by adding them to the
26 26 pager.ignore list::
27 27
28 28 [pager]
29 29 ignore = version, help, update
30 30
31 31 You can also enable the pager only for certain commands using
32 32 pager.attend. Below is the default list of commands to be paged::
33 33
34 34 [pager]
35 35 attend = annotate, cat, diff, export, glog, log, qdiff
36 36
37 37 Setting pager.attend to an empty value will cause all commands to be
38 38 paged.
39 39
40 40 If pager.attend is present, pager.ignore will be ignored.
41 41
42 42 To ignore global commands like :hg:`version` or :hg:`help`, you have
43 43 to specify them in your user configuration file.
44 44
45 45 The --pager=... option can also be used to control when the pager is
46 46 used. Use a boolean value like yes, no, on, off, or use auto for
47 47 normal behavior.
48 48 '''
49 49
50 50 import atexit, sys, os, signal, subprocess
51 51 from mercurial import commands, dispatch, util, extensions
52 52 from mercurial.i18n import _
53 53
54 54 def _runpager(p):
55 55 pager = subprocess.Popen(p, shell=True, bufsize=-1,
56 56 close_fds=util.closefds, stdin=subprocess.PIPE,
57 57 stdout=sys.stdout, stderr=sys.stderr)
58 58
59 59 stdout = os.dup(sys.stdout.fileno())
60 60 stderr = os.dup(sys.stderr.fileno())
61 61 os.dup2(pager.stdin.fileno(), sys.stdout.fileno())
62 62 if util.isatty(sys.stderr):
63 63 os.dup2(pager.stdin.fileno(), sys.stderr.fileno())
64 64
65 65 @atexit.register
66 66 def killpager():
67 67 pager.stdin.close()
68 68 os.dup2(stdout, sys.stdout.fileno())
69 69 os.dup2(stderr, sys.stderr.fileno())
70 70 pager.wait()
71 71
72 72 def uisetup(ui):
73 73 if ui.plain() or '--debugger' in sys.argv or not util.isatty(sys.stdout):
74 74 return
75 75
76 76 def pagecmd(orig, ui, options, cmd, cmdfunc):
77 77 p = ui.config("pager", "pager", os.environ.get("PAGER"))
78 78
79 79 if p:
80 80 attend = ui.configlist('pager', 'attend', attended)
81 81 auto = options['pager'] == 'auto'
82 82 always = util.parsebool(options['pager'])
83 83 if (always or auto and
84 84 (cmd in attend or
85 85 (cmd not in ui.configlist('pager', 'ignore') and not attend))):
86 86 ui.setconfig('ui', 'formatted', ui.formatted())
87 87 ui.setconfig('ui', 'interactive', False)
88 try:
88 if util.safehasattr(signal, "SIGPIPE"):
89 89 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
90 except ValueError:
91 pass
92 90 _runpager(p)
93 91 return orig(ui, options, cmd, cmdfunc)
94 92
95 93 extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
96 94
97 95 def extsetup(ui):
98 96 commands.globalopts.append(
99 97 ('', 'pager', 'auto',
100 98 _("when to paginate (boolean, always, auto, or never)"),
101 99 _('TYPE')))
102 100
103 101 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
@@ -1,190 +1,190 b''
1 1 /*
2 2 * diffhelpers.c - helper routines for mpatch
3 3 *
4 4 * Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 *
6 6 * This software may be used and distributed according to the terms
7 7 * of the GNU General Public License v2, incorporated herein by reference.
8 8 */
9 9
10 10 #include <Python.h>
11 11 #include <stdlib.h>
12 12 #include <string.h>
13 13
14 14 #include "util.h"
15 15
16 16 static char diffhelpers_doc[] = "Efficient diff parsing";
17 17 static PyObject *diffhelpers_Error;
18 18
19 19
20 20 /* fixup the last lines of a and b when the patch has no newline at eof */
21 21 static void _fix_newline(PyObject *hunk, PyObject *a, PyObject *b)
22 22 {
23 23 int hunksz = PyList_Size(hunk);
24 24 PyObject *s = PyList_GET_ITEM(hunk, hunksz-1);
25 25 char *l = PyBytes_AsString(s);
26 26 int alen = PyList_Size(a);
27 27 int blen = PyList_Size(b);
28 28 char c = l[0];
29 29 PyObject *hline;
30 30 int sz = PyBytes_GET_SIZE(s);
31 31
32 32 if (sz > 1 && l[sz-2] == '\r')
33 33 /* tolerate CRLF in last line */
34 34 sz -= 1;
35 35
36 36 hline = PyBytes_FromStringAndSize(l, sz-1);
37 37
38 38 if (c == ' ' || c == '+') {
39 39 PyObject *rline = PyBytes_FromStringAndSize(l + 1, sz - 2);
40 40 PyList_SetItem(b, blen-1, rline);
41 41 }
42 42 if (c == ' ' || c == '-') {
43 43 Py_INCREF(hline);
44 44 PyList_SetItem(a, alen-1, hline);
45 45 }
46 46 PyList_SetItem(hunk, hunksz-1, hline);
47 47 }
48 48
49 49 /* python callable form of _fix_newline */
50 50 static PyObject *
51 51 fix_newline(PyObject *self, PyObject *args)
52 52 {
53 53 PyObject *hunk, *a, *b;
54 54 if (!PyArg_ParseTuple(args, "OOO", &hunk, &a, &b))
55 55 return NULL;
56 56 _fix_newline(hunk, a, b);
57 57 return Py_BuildValue("l", 0);
58 58 }
59 59
60 60 /*
61 61 * read lines from fp into the hunk. The hunk is parsed into two arrays
62 62 * a and b. a gets the old state of the text, b gets the new state
63 63 * The control char from the hunk is saved when inserting into a, but not b
64 64 * (for performance while deleting files)
65 65 */
66 66 static PyObject *
67 67 addlines(PyObject *self, PyObject *args)
68 68 {
69 69
70 70 PyObject *fp, *hunk, *a, *b, *x;
71 71 int i;
72 72 int lena, lenb;
73 73 int num;
74 74 int todoa, todob;
75 75 char *s, c;
76 76 PyObject *l;
77 77 if (!PyArg_ParseTuple(args, "OOiiOO", &fp, &hunk, &lena, &lenb, &a, &b))
78 78 return NULL;
79 79
80 80 while (1) {
81 81 todoa = lena - PyList_Size(a);
82 82 todob = lenb - PyList_Size(b);
83 83 num = todoa > todob ? todoa : todob;
84 84 if (num == 0)
85 85 break;
86 86 for (i = 0; i < num; i++) {
87 87 x = PyFile_GetLine(fp, 0);
88 88 s = PyBytes_AsString(x);
89 89 c = *s;
90 90 if (strcmp(s, "\\ No newline at end of file\n") == 0) {
91 91 _fix_newline(hunk, a, b);
92 92 continue;
93 93 }
94 94 if (c == '\n') {
95 95 /* Some patches may be missing the control char
96 96 * on empty lines. Supply a leading space. */
97 97 Py_DECREF(x);
98 98 x = PyBytes_FromString(" \n");
99 99 }
100 100 PyList_Append(hunk, x);
101 101 if (c == '+') {
102 102 l = PyBytes_FromString(s + 1);
103 103 PyList_Append(b, l);
104 104 Py_DECREF(l);
105 105 } else if (c == '-') {
106 106 PyList_Append(a, x);
107 107 } else {
108 108 l = PyBytes_FromString(s + 1);
109 109 PyList_Append(b, l);
110 110 Py_DECREF(l);
111 111 PyList_Append(a, x);
112 112 }
113 113 Py_DECREF(x);
114 114 }
115 115 }
116 116 return Py_BuildValue("l", 0);
117 117 }
118 118
119 119 /*
120 120 * compare the lines in a with the lines in b. a is assumed to have
121 121 * a control char at the start of each line, this char is ignored in the
122 122 * compare
123 123 */
124 124 static PyObject *
125 125 testhunk(PyObject *self, PyObject *args)
126 126 {
127 127
128 128 PyObject *a, *b;
129 129 long bstart;
130 130 int alen, blen;
131 131 int i;
132 132 char *sa, *sb;
133 133
134 134 if (!PyArg_ParseTuple(args, "OOl", &a, &b, &bstart))
135 135 return NULL;
136 136 alen = PyList_Size(a);
137 137 blen = PyList_Size(b);
138 if (alen > blen - bstart) {
138 if (alen > blen - bstart || bstart < 0) {
139 139 return Py_BuildValue("l", -1);
140 140 }
141 141 for (i = 0; i < alen; i++) {
142 142 sa = PyBytes_AsString(PyList_GET_ITEM(a, i));
143 143 sb = PyBytes_AsString(PyList_GET_ITEM(b, i + bstart));
144 144 if (strcmp(sa + 1, sb) != 0)
145 145 return Py_BuildValue("l", -1);
146 146 }
147 147 return Py_BuildValue("l", 0);
148 148 }
149 149
150 150 static PyMethodDef methods[] = {
151 151 {"addlines", addlines, METH_VARARGS, "add lines to a hunk\n"},
152 152 {"fix_newline", fix_newline, METH_VARARGS, "fixup newline counters\n"},
153 153 {"testhunk", testhunk, METH_VARARGS, "test lines in a hunk\n"},
154 154 {NULL, NULL}
155 155 };
156 156
157 157 #ifdef IS_PY3K
158 158 static struct PyModuleDef diffhelpers_module = {
159 159 PyModuleDef_HEAD_INIT,
160 160 "diffhelpers",
161 161 diffhelpers_doc,
162 162 -1,
163 163 methods
164 164 };
165 165
166 166 PyMODINIT_FUNC PyInit_diffhelpers(void)
167 167 {
168 168 PyObject *m;
169 169
170 170 m = PyModule_Create(&diffhelpers_module);
171 171 if (m == NULL)
172 172 return NULL;
173 173
174 174 diffhelpers_Error = PyErr_NewException("diffhelpers.diffhelpersError",
175 175 NULL, NULL);
176 176 Py_INCREF(diffhelpers_Error);
177 177 PyModule_AddObject(m, "diffhelpersError", diffhelpers_Error);
178 178
179 179 return m;
180 180 }
181 181 #else
182 182 PyMODINIT_FUNC
183 183 initdiffhelpers(void)
184 184 {
185 185 Py_InitModule3("diffhelpers", methods, diffhelpers_doc);
186 186 diffhelpers_Error = PyErr_NewException("diffhelpers.diffhelpersError",
187 187 NULL, NULL);
188 188 }
189 189 #endif
190 190
@@ -1,1887 +1,1887 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
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 import cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
15 15 import context
16 16
17 17 gitre = re.compile('diff --git a/(.*) b/(.*)')
18 18
19 19 class PatchError(Exception):
20 20 pass
21 21
22 22
23 23 # public functions
24 24
25 25 def split(stream):
26 26 '''return an iterator of individual patches from a stream'''
27 27 def isheader(line, inheader):
28 28 if inheader and line[0] in (' ', '\t'):
29 29 # continuation
30 30 return True
31 31 if line[0] in (' ', '-', '+'):
32 32 # diff line - don't check for header pattern in there
33 33 return False
34 34 l = line.split(': ', 1)
35 35 return len(l) == 2 and ' ' not in l[0]
36 36
37 37 def chunk(lines):
38 38 return cStringIO.StringIO(''.join(lines))
39 39
40 40 def hgsplit(stream, cur):
41 41 inheader = True
42 42
43 43 for line in stream:
44 44 if not line.strip():
45 45 inheader = False
46 46 if not inheader and line.startswith('# HG changeset patch'):
47 47 yield chunk(cur)
48 48 cur = []
49 49 inheader = True
50 50
51 51 cur.append(line)
52 52
53 53 if cur:
54 54 yield chunk(cur)
55 55
56 56 def mboxsplit(stream, cur):
57 57 for line in stream:
58 58 if line.startswith('From '):
59 59 for c in split(chunk(cur[1:])):
60 60 yield c
61 61 cur = []
62 62
63 63 cur.append(line)
64 64
65 65 if cur:
66 66 for c in split(chunk(cur[1:])):
67 67 yield c
68 68
69 69 def mimesplit(stream, cur):
70 70 def msgfp(m):
71 71 fp = cStringIO.StringIO()
72 72 g = email.Generator.Generator(fp, mangle_from_=False)
73 73 g.flatten(m)
74 74 fp.seek(0)
75 75 return fp
76 76
77 77 for line in stream:
78 78 cur.append(line)
79 79 c = chunk(cur)
80 80
81 81 m = email.Parser.Parser().parse(c)
82 82 if not m.is_multipart():
83 83 yield msgfp(m)
84 84 else:
85 85 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
86 86 for part in m.walk():
87 87 ct = part.get_content_type()
88 88 if ct not in ok_types:
89 89 continue
90 90 yield msgfp(part)
91 91
92 92 def headersplit(stream, cur):
93 93 inheader = False
94 94
95 95 for line in stream:
96 96 if not inheader and isheader(line, inheader):
97 97 yield chunk(cur)
98 98 cur = []
99 99 inheader = True
100 100 if inheader and not isheader(line, inheader):
101 101 inheader = False
102 102
103 103 cur.append(line)
104 104
105 105 if cur:
106 106 yield chunk(cur)
107 107
108 108 def remainder(cur):
109 109 yield chunk(cur)
110 110
111 111 class fiter(object):
112 112 def __init__(self, fp):
113 113 self.fp = fp
114 114
115 115 def __iter__(self):
116 116 return self
117 117
118 118 def next(self):
119 119 l = self.fp.readline()
120 120 if not l:
121 121 raise StopIteration
122 122 return l
123 123
124 124 inheader = False
125 125 cur = []
126 126
127 127 mimeheaders = ['content-type']
128 128
129 129 if not util.safehasattr(stream, 'next'):
130 130 # http responses, for example, have readline but not next
131 131 stream = fiter(stream)
132 132
133 133 for line in stream:
134 134 cur.append(line)
135 135 if line.startswith('# HG changeset patch'):
136 136 return hgsplit(stream, cur)
137 137 elif line.startswith('From '):
138 138 return mboxsplit(stream, cur)
139 139 elif isheader(line, inheader):
140 140 inheader = True
141 141 if line.split(':', 1)[0].lower() in mimeheaders:
142 142 # let email parser handle this
143 143 return mimesplit(stream, cur)
144 144 elif line.startswith('--- ') and inheader:
145 145 # No evil headers seen by diff start, split by hand
146 146 return headersplit(stream, cur)
147 147 # Not enough info, keep reading
148 148
149 149 # if we are here, we have a very plain patch
150 150 return remainder(cur)
151 151
152 152 def extract(ui, fileobj):
153 153 '''extract patch from data read from fileobj.
154 154
155 155 patch can be a normal patch or contained in an email message.
156 156
157 157 return tuple (filename, message, user, date, branch, node, p1, p2).
158 158 Any item in the returned tuple can be None. If filename is None,
159 159 fileobj did not contain a patch. Caller must unlink filename when done.'''
160 160
161 161 # attempt to detect the start of a patch
162 162 # (this heuristic is borrowed from quilt)
163 163 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
164 164 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
165 165 r'---[ \t].*?^\+\+\+[ \t]|'
166 166 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
167 167
168 168 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
169 169 tmpfp = os.fdopen(fd, 'w')
170 170 try:
171 171 msg = email.Parser.Parser().parse(fileobj)
172 172
173 173 subject = msg['Subject']
174 174 user = msg['From']
175 175 if not subject and not user:
176 176 # Not an email, restore parsed headers if any
177 177 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
178 178
179 179 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
180 180 # should try to parse msg['Date']
181 181 date = None
182 182 nodeid = None
183 183 branch = None
184 184 parents = []
185 185
186 186 if subject:
187 187 if subject.startswith('[PATCH'):
188 188 pend = subject.find(']')
189 189 if pend >= 0:
190 190 subject = subject[pend + 1:].lstrip()
191 191 subject = re.sub(r'\n[ \t]+', ' ', subject)
192 192 ui.debug('Subject: %s\n' % subject)
193 193 if user:
194 194 ui.debug('From: %s\n' % user)
195 195 diffs_seen = 0
196 196 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
197 197 message = ''
198 198 for part in msg.walk():
199 199 content_type = part.get_content_type()
200 200 ui.debug('Content-Type: %s\n' % content_type)
201 201 if content_type not in ok_types:
202 202 continue
203 203 payload = part.get_payload(decode=True)
204 204 m = diffre.search(payload)
205 205 if m:
206 206 hgpatch = False
207 207 hgpatchheader = False
208 208 ignoretext = False
209 209
210 210 ui.debug('found patch at byte %d\n' % m.start(0))
211 211 diffs_seen += 1
212 212 cfp = cStringIO.StringIO()
213 213 for line in payload[:m.start(0)].splitlines():
214 214 if line.startswith('# HG changeset patch') and not hgpatch:
215 215 ui.debug('patch generated by hg export\n')
216 216 hgpatch = True
217 217 hgpatchheader = True
218 218 # drop earlier commit message content
219 219 cfp.seek(0)
220 220 cfp.truncate()
221 221 subject = None
222 222 elif hgpatchheader:
223 223 if line.startswith('# User '):
224 224 user = line[7:]
225 225 ui.debug('From: %s\n' % user)
226 226 elif line.startswith("# Date "):
227 227 date = line[7:]
228 228 elif line.startswith("# Branch "):
229 229 branch = line[9:]
230 230 elif line.startswith("# Node ID "):
231 231 nodeid = line[10:]
232 232 elif line.startswith("# Parent "):
233 233 parents.append(line[9:].lstrip())
234 234 elif not line.startswith("# "):
235 235 hgpatchheader = False
236 236 elif line == '---' and gitsendmail:
237 237 ignoretext = True
238 238 if not hgpatchheader and not ignoretext:
239 239 cfp.write(line)
240 240 cfp.write('\n')
241 241 message = cfp.getvalue()
242 242 if tmpfp:
243 243 tmpfp.write(payload)
244 244 if not payload.endswith('\n'):
245 245 tmpfp.write('\n')
246 246 elif not diffs_seen and message and content_type == 'text/plain':
247 247 message += '\n' + payload
248 248 except:
249 249 tmpfp.close()
250 250 os.unlink(tmpname)
251 251 raise
252 252
253 253 if subject and not message.startswith(subject):
254 254 message = '%s\n%s' % (subject, message)
255 255 tmpfp.close()
256 256 if not diffs_seen:
257 257 os.unlink(tmpname)
258 258 return None, message, user, date, branch, None, None, None
259 259 p1 = parents and parents.pop(0) or None
260 260 p2 = parents and parents.pop(0) or None
261 261 return tmpname, message, user, date, branch, nodeid, p1, p2
262 262
263 263 class patchmeta(object):
264 264 """Patched file metadata
265 265
266 266 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
267 267 or COPY. 'path' is patched file path. 'oldpath' is set to the
268 268 origin file when 'op' is either COPY or RENAME, None otherwise. If
269 269 file mode is changed, 'mode' is a tuple (islink, isexec) where
270 270 'islink' is True if the file is a symlink and 'isexec' is True if
271 271 the file is executable. Otherwise, 'mode' is None.
272 272 """
273 273 def __init__(self, path):
274 274 self.path = path
275 275 self.oldpath = None
276 276 self.mode = None
277 277 self.op = 'MODIFY'
278 278 self.binary = False
279 279
280 280 def setmode(self, mode):
281 281 islink = mode & 020000
282 282 isexec = mode & 0100
283 283 self.mode = (islink, isexec)
284 284
285 285 def copy(self):
286 286 other = patchmeta(self.path)
287 287 other.oldpath = self.oldpath
288 288 other.mode = self.mode
289 289 other.op = self.op
290 290 other.binary = self.binary
291 291 return other
292 292
293 293 def _ispatchinga(self, afile):
294 294 if afile == '/dev/null':
295 295 return self.op == 'ADD'
296 296 return afile == 'a/' + (self.oldpath or self.path)
297 297
298 298 def _ispatchingb(self, bfile):
299 299 if bfile == '/dev/null':
300 300 return self.op == 'DELETE'
301 301 return bfile == 'b/' + self.path
302 302
303 303 def ispatching(self, afile, bfile):
304 304 return self._ispatchinga(afile) and self._ispatchingb(bfile)
305 305
306 306 def __repr__(self):
307 307 return "<patchmeta %s %r>" % (self.op, self.path)
308 308
309 309 def readgitpatch(lr):
310 310 """extract git-style metadata about patches from <patchname>"""
311 311
312 312 # Filter patch for git information
313 313 gp = None
314 314 gitpatches = []
315 315 for line in lr:
316 316 line = line.rstrip(' \r\n')
317 317 if line.startswith('diff --git'):
318 318 m = gitre.match(line)
319 319 if m:
320 320 if gp:
321 321 gitpatches.append(gp)
322 322 dst = m.group(2)
323 323 gp = patchmeta(dst)
324 324 elif gp:
325 325 if line.startswith('--- '):
326 326 gitpatches.append(gp)
327 327 gp = None
328 328 continue
329 329 if line.startswith('rename from '):
330 330 gp.op = 'RENAME'
331 331 gp.oldpath = line[12:]
332 332 elif line.startswith('rename to '):
333 333 gp.path = line[10:]
334 334 elif line.startswith('copy from '):
335 335 gp.op = 'COPY'
336 336 gp.oldpath = line[10:]
337 337 elif line.startswith('copy to '):
338 338 gp.path = line[8:]
339 339 elif line.startswith('deleted file'):
340 340 gp.op = 'DELETE'
341 341 elif line.startswith('new file mode '):
342 342 gp.op = 'ADD'
343 343 gp.setmode(int(line[-6:], 8))
344 344 elif line.startswith('new mode '):
345 345 gp.setmode(int(line[-6:], 8))
346 346 elif line.startswith('GIT binary patch'):
347 347 gp.binary = True
348 348 if gp:
349 349 gitpatches.append(gp)
350 350
351 351 return gitpatches
352 352
353 353 class linereader(object):
354 354 # simple class to allow pushing lines back into the input stream
355 355 def __init__(self, fp):
356 356 self.fp = fp
357 357 self.buf = []
358 358
359 359 def push(self, line):
360 360 if line is not None:
361 361 self.buf.append(line)
362 362
363 363 def readline(self):
364 364 if self.buf:
365 365 l = self.buf[0]
366 366 del self.buf[0]
367 367 return l
368 368 return self.fp.readline()
369 369
370 370 def __iter__(self):
371 371 while True:
372 372 l = self.readline()
373 373 if not l:
374 374 break
375 375 yield l
376 376
377 377 class abstractbackend(object):
378 378 def __init__(self, ui):
379 379 self.ui = ui
380 380
381 381 def getfile(self, fname):
382 382 """Return target file data and flags as a (data, (islink,
383 383 isexec)) tuple.
384 384 """
385 385 raise NotImplementedError
386 386
387 387 def setfile(self, fname, data, mode, copysource):
388 388 """Write data to target file fname and set its mode. mode is a
389 389 (islink, isexec) tuple. If data is None, the file content should
390 390 be left unchanged. If the file is modified after being copied,
391 391 copysource is set to the original file name.
392 392 """
393 393 raise NotImplementedError
394 394
395 395 def unlink(self, fname):
396 396 """Unlink target file."""
397 397 raise NotImplementedError
398 398
399 399 def writerej(self, fname, failed, total, lines):
400 400 """Write rejected lines for fname. total is the number of hunks
401 401 which failed to apply and total the total number of hunks for this
402 402 files.
403 403 """
404 404 pass
405 405
406 406 def exists(self, fname):
407 407 raise NotImplementedError
408 408
409 409 class fsbackend(abstractbackend):
410 410 def __init__(self, ui, basedir):
411 411 super(fsbackend, self).__init__(ui)
412 412 self.opener = scmutil.opener(basedir)
413 413
414 414 def _join(self, f):
415 415 return os.path.join(self.opener.base, f)
416 416
417 417 def getfile(self, fname):
418 418 path = self._join(fname)
419 419 if os.path.islink(path):
420 420 return (os.readlink(path), (True, False))
421 421 isexec = False
422 422 try:
423 423 isexec = os.lstat(path).st_mode & 0100 != 0
424 424 except OSError, e:
425 425 if e.errno != errno.ENOENT:
426 426 raise
427 427 return (self.opener.read(fname), (False, isexec))
428 428
429 429 def setfile(self, fname, data, mode, copysource):
430 430 islink, isexec = mode
431 431 if data is None:
432 432 util.setflags(self._join(fname), islink, isexec)
433 433 return
434 434 if islink:
435 435 self.opener.symlink(data, fname)
436 436 else:
437 437 self.opener.write(fname, data)
438 438 if isexec:
439 439 util.setflags(self._join(fname), False, True)
440 440
441 441 def unlink(self, fname):
442 442 try:
443 443 util.unlinkpath(self._join(fname))
444 444 except OSError, inst:
445 445 if inst.errno != errno.ENOENT:
446 446 raise
447 447
448 448 def writerej(self, fname, failed, total, lines):
449 449 fname = fname + ".rej"
450 450 self.ui.warn(
451 451 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
452 452 (failed, total, fname))
453 453 fp = self.opener(fname, 'w')
454 454 fp.writelines(lines)
455 455 fp.close()
456 456
457 457 def exists(self, fname):
458 458 return os.path.lexists(self._join(fname))
459 459
460 460 class workingbackend(fsbackend):
461 461 def __init__(self, ui, repo, similarity):
462 462 super(workingbackend, self).__init__(ui, repo.root)
463 463 self.repo = repo
464 464 self.similarity = similarity
465 465 self.removed = set()
466 466 self.changed = set()
467 467 self.copied = []
468 468
469 469 def _checkknown(self, fname):
470 470 if self.repo.dirstate[fname] == '?' and self.exists(fname):
471 471 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
472 472
473 473 def setfile(self, fname, data, mode, copysource):
474 474 self._checkknown(fname)
475 475 super(workingbackend, self).setfile(fname, data, mode, copysource)
476 476 if copysource is not None:
477 477 self.copied.append((copysource, fname))
478 478 self.changed.add(fname)
479 479
480 480 def unlink(self, fname):
481 481 self._checkknown(fname)
482 482 super(workingbackend, self).unlink(fname)
483 483 self.removed.add(fname)
484 484 self.changed.add(fname)
485 485
486 486 def close(self):
487 487 wctx = self.repo[None]
488 488 addremoved = set(self.changed)
489 489 for src, dst in self.copied:
490 490 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
491 491 if self.removed:
492 492 wctx.forget(sorted(self.removed))
493 493 for f in self.removed:
494 494 if f not in self.repo.dirstate:
495 495 # File was deleted and no longer belongs to the
496 496 # dirstate, it was probably marked added then
497 497 # deleted, and should not be considered by
498 498 # addremove().
499 499 addremoved.discard(f)
500 500 if addremoved:
501 501 cwd = self.repo.getcwd()
502 502 if cwd:
503 503 addremoved = [util.pathto(self.repo.root, cwd, f)
504 504 for f in addremoved]
505 505 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
506 506 return sorted(self.changed)
507 507
508 508 class filestore(object):
509 509 def __init__(self, maxsize=None):
510 510 self.opener = None
511 511 self.files = {}
512 512 self.created = 0
513 513 self.maxsize = maxsize
514 514 if self.maxsize is None:
515 515 self.maxsize = 4*(2**20)
516 516 self.size = 0
517 517 self.data = {}
518 518
519 519 def setfile(self, fname, data, mode, copied=None):
520 520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
521 521 self.data[fname] = (data, mode, copied)
522 522 self.size += len(data)
523 523 else:
524 524 if self.opener is None:
525 525 root = tempfile.mkdtemp(prefix='hg-patch-')
526 526 self.opener = scmutil.opener(root)
527 527 # Avoid filename issues with these simple names
528 528 fn = str(self.created)
529 529 self.opener.write(fn, data)
530 530 self.created += 1
531 531 self.files[fname] = (fn, mode, copied)
532 532
533 533 def getfile(self, fname):
534 534 if fname in self.data:
535 535 return self.data[fname]
536 536 if not self.opener or fname not in self.files:
537 537 raise IOError()
538 538 fn, mode, copied = self.files[fname]
539 539 return self.opener.read(fn), mode, copied
540 540
541 541 def close(self):
542 542 if self.opener:
543 543 shutil.rmtree(self.opener.base)
544 544
545 545 class repobackend(abstractbackend):
546 546 def __init__(self, ui, repo, ctx, store):
547 547 super(repobackend, self).__init__(ui)
548 548 self.repo = repo
549 549 self.ctx = ctx
550 550 self.store = store
551 551 self.changed = set()
552 552 self.removed = set()
553 553 self.copied = {}
554 554
555 555 def _checkknown(self, fname):
556 556 if fname not in self.ctx:
557 557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
558 558
559 559 def getfile(self, fname):
560 560 try:
561 561 fctx = self.ctx[fname]
562 562 except error.LookupError:
563 563 raise IOError()
564 564 flags = fctx.flags()
565 565 return fctx.data(), ('l' in flags, 'x' in flags)
566 566
567 567 def setfile(self, fname, data, mode, copysource):
568 568 if copysource:
569 569 self._checkknown(copysource)
570 570 if data is None:
571 571 data = self.ctx[fname].data()
572 572 self.store.setfile(fname, data, mode, copysource)
573 573 self.changed.add(fname)
574 574 if copysource:
575 575 self.copied[fname] = copysource
576 576
577 577 def unlink(self, fname):
578 578 self._checkknown(fname)
579 579 self.removed.add(fname)
580 580
581 581 def exists(self, fname):
582 582 return fname in self.ctx
583 583
584 584 def close(self):
585 585 return self.changed | self.removed
586 586
587 587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
588 588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
589 589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
590 590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
591 591
592 592 class patchfile(object):
593 593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
594 594 self.fname = gp.path
595 595 self.eolmode = eolmode
596 596 self.eol = None
597 597 self.backend = backend
598 598 self.ui = ui
599 599 self.lines = []
600 600 self.exists = False
601 601 self.missing = True
602 602 self.mode = gp.mode
603 603 self.copysource = gp.oldpath
604 604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
605 605 self.remove = gp.op == 'DELETE'
606 606 try:
607 607 if self.copysource is None:
608 608 data, mode = backend.getfile(self.fname)
609 609 self.exists = True
610 610 else:
611 611 data, mode = store.getfile(self.copysource)[:2]
612 612 self.exists = backend.exists(self.fname)
613 613 self.missing = False
614 614 if data:
615 615 self.lines = mdiff.splitnewlines(data)
616 616 if self.mode is None:
617 617 self.mode = mode
618 618 if self.lines:
619 619 # Normalize line endings
620 620 if self.lines[0].endswith('\r\n'):
621 621 self.eol = '\r\n'
622 622 elif self.lines[0].endswith('\n'):
623 623 self.eol = '\n'
624 624 if eolmode != 'strict':
625 625 nlines = []
626 626 for l in self.lines:
627 627 if l.endswith('\r\n'):
628 628 l = l[:-2] + '\n'
629 629 nlines.append(l)
630 630 self.lines = nlines
631 631 except IOError:
632 632 if self.create:
633 633 self.missing = False
634 634 if self.mode is None:
635 635 self.mode = (False, False)
636 636 if self.missing:
637 637 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
638 638
639 639 self.hash = {}
640 640 self.dirty = 0
641 641 self.offset = 0
642 642 self.skew = 0
643 643 self.rej = []
644 644 self.fileprinted = False
645 645 self.printfile(False)
646 646 self.hunks = 0
647 647
648 648 def writelines(self, fname, lines, mode):
649 649 if self.eolmode == 'auto':
650 650 eol = self.eol
651 651 elif self.eolmode == 'crlf':
652 652 eol = '\r\n'
653 653 else:
654 654 eol = '\n'
655 655
656 656 if self.eolmode != 'strict' and eol and eol != '\n':
657 657 rawlines = []
658 658 for l in lines:
659 659 if l and l[-1] == '\n':
660 660 l = l[:-1] + eol
661 661 rawlines.append(l)
662 662 lines = rawlines
663 663
664 664 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
665 665
666 666 def printfile(self, warn):
667 667 if self.fileprinted:
668 668 return
669 669 if warn or self.ui.verbose:
670 670 self.fileprinted = True
671 671 s = _("patching file %s\n") % self.fname
672 672 if warn:
673 673 self.ui.warn(s)
674 674 else:
675 675 self.ui.note(s)
676 676
677 677
678 678 def findlines(self, l, linenum):
679 679 # looks through the hash and finds candidate lines. The
680 680 # result is a list of line numbers sorted based on distance
681 681 # from linenum
682 682
683 683 cand = self.hash.get(l, [])
684 684 if len(cand) > 1:
685 685 # resort our list of potentials forward then back.
686 686 cand.sort(key=lambda x: abs(x - linenum))
687 687 return cand
688 688
689 689 def write_rej(self):
690 690 # our rejects are a little different from patch(1). This always
691 691 # creates rejects in the same form as the original patch. A file
692 692 # header is inserted so that you can run the reject through patch again
693 693 # without having to type the filename.
694 694 if not self.rej:
695 695 return
696 696 base = os.path.basename(self.fname)
697 697 lines = ["--- %s\n+++ %s\n" % (base, base)]
698 698 for x in self.rej:
699 699 for l in x.hunk:
700 700 lines.append(l)
701 701 if l[-1] != '\n':
702 702 lines.append("\n\ No newline at end of file\n")
703 703 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
704 704
705 705 def apply(self, h):
706 706 if not h.complete():
707 707 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
708 708 (h.number, h.desc, len(h.a), h.lena, len(h.b),
709 709 h.lenb))
710 710
711 711 self.hunks += 1
712 712
713 713 if self.missing:
714 714 self.rej.append(h)
715 715 return -1
716 716
717 717 if self.exists and self.create:
718 718 if self.copysource:
719 719 self.ui.warn(_("cannot create %s: destination already "
720 720 "exists\n" % self.fname))
721 721 else:
722 722 self.ui.warn(_("file %s already exists\n") % self.fname)
723 723 self.rej.append(h)
724 724 return -1
725 725
726 726 if isinstance(h, binhunk):
727 727 if self.remove:
728 728 self.backend.unlink(self.fname)
729 729 else:
730 730 self.lines[:] = h.new()
731 731 self.offset += len(h.new())
732 732 self.dirty = True
733 733 return 0
734 734
735 735 horig = h
736 736 if (self.eolmode in ('crlf', 'lf')
737 737 or self.eolmode == 'auto' and self.eol):
738 738 # If new eols are going to be normalized, then normalize
739 739 # hunk data before patching. Otherwise, preserve input
740 740 # line-endings.
741 741 h = h.getnormalized()
742 742
743 743 # fast case first, no offsets, no fuzz
744 744 old, oldstart, new, newstart = h.fuzzit(0, False)
745 745 oldstart += self.offset
746 746 orig_start = oldstart
747 747 # if there's skew we want to emit the "(offset %d lines)" even
748 748 # when the hunk cleanly applies at start + skew, so skip the
749 749 # fast case code
750 750 if (self.skew == 0 and
751 751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
752 752 if self.remove:
753 753 self.backend.unlink(self.fname)
754 754 else:
755 755 self.lines[oldstart:oldstart + len(old)] = new
756 756 self.offset += len(new) - len(old)
757 757 self.dirty = True
758 758 return 0
759 759
760 760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
761 761 self.hash = {}
762 762 for x, s in enumerate(self.lines):
763 763 self.hash.setdefault(s, []).append(x)
764 764
765 765 for fuzzlen in xrange(3):
766 766 for toponly in [True, False]:
767 767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
768 768 oldstart = oldstart + self.offset + self.skew
769 769 oldstart = min(oldstart, len(self.lines))
770 770 if old:
771 771 cand = self.findlines(old[0][1:], oldstart)
772 772 else:
773 773 # Only adding lines with no or fuzzed context, just
774 774 # take the skew in account
775 775 cand = [oldstart]
776 776
777 777 for l in cand:
778 778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
779 779 self.lines[l : l + len(old)] = new
780 780 self.offset += len(new) - len(old)
781 781 self.skew = l - orig_start
782 782 self.dirty = True
783 783 offset = l - orig_start - fuzzlen
784 784 if fuzzlen:
785 785 msg = _("Hunk #%d succeeded at %d "
786 786 "with fuzz %d "
787 787 "(offset %d lines).\n")
788 788 self.printfile(True)
789 789 self.ui.warn(msg %
790 790 (h.number, l + 1, fuzzlen, offset))
791 791 else:
792 792 msg = _("Hunk #%d succeeded at %d "
793 793 "(offset %d lines).\n")
794 794 self.ui.note(msg % (h.number, l + 1, offset))
795 795 return fuzzlen
796 796 self.printfile(True)
797 797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
798 798 self.rej.append(horig)
799 799 return -1
800 800
801 801 def close(self):
802 802 if self.dirty:
803 803 self.writelines(self.fname, self.lines, self.mode)
804 804 self.write_rej()
805 805 return len(self.rej)
806 806
807 807 class hunk(object):
808 808 def __init__(self, desc, num, lr, context):
809 809 self.number = num
810 810 self.desc = desc
811 811 self.hunk = [desc]
812 812 self.a = []
813 813 self.b = []
814 814 self.starta = self.lena = None
815 815 self.startb = self.lenb = None
816 816 if lr is not None:
817 817 if context:
818 818 self.read_context_hunk(lr)
819 819 else:
820 820 self.read_unified_hunk(lr)
821 821
822 822 def getnormalized(self):
823 823 """Return a copy with line endings normalized to LF."""
824 824
825 825 def normalize(lines):
826 826 nlines = []
827 827 for line in lines:
828 828 if line.endswith('\r\n'):
829 829 line = line[:-2] + '\n'
830 830 nlines.append(line)
831 831 return nlines
832 832
833 833 # Dummy object, it is rebuilt manually
834 834 nh = hunk(self.desc, self.number, None, None)
835 835 nh.number = self.number
836 836 nh.desc = self.desc
837 837 nh.hunk = self.hunk
838 838 nh.a = normalize(self.a)
839 839 nh.b = normalize(self.b)
840 840 nh.starta = self.starta
841 841 nh.startb = self.startb
842 842 nh.lena = self.lena
843 843 nh.lenb = self.lenb
844 844 return nh
845 845
846 846 def read_unified_hunk(self, lr):
847 847 m = unidesc.match(self.desc)
848 848 if not m:
849 849 raise PatchError(_("bad hunk #%d") % self.number)
850 850 self.starta, self.lena, self.startb, self.lenb = m.groups()
851 851 if self.lena is None:
852 852 self.lena = 1
853 853 else:
854 854 self.lena = int(self.lena)
855 855 if self.lenb is None:
856 856 self.lenb = 1
857 857 else:
858 858 self.lenb = int(self.lenb)
859 859 self.starta = int(self.starta)
860 860 self.startb = int(self.startb)
861 861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
862 862 # if we hit eof before finishing out the hunk, the last line will
863 863 # be zero length. Lets try to fix it up.
864 864 while len(self.hunk[-1]) == 0:
865 865 del self.hunk[-1]
866 866 del self.a[-1]
867 867 del self.b[-1]
868 868 self.lena -= 1
869 869 self.lenb -= 1
870 870 self._fixnewline(lr)
871 871
872 872 def read_context_hunk(self, lr):
873 873 self.desc = lr.readline()
874 874 m = contextdesc.match(self.desc)
875 875 if not m:
876 876 raise PatchError(_("bad hunk #%d") % self.number)
877 877 self.starta, aend = m.groups()
878 878 self.starta = int(self.starta)
879 879 if aend is None:
880 880 aend = self.starta
881 881 self.lena = int(aend) - self.starta
882 882 if self.starta:
883 883 self.lena += 1
884 884 for x in xrange(self.lena):
885 885 l = lr.readline()
886 886 if l.startswith('---'):
887 887 # lines addition, old block is empty
888 888 lr.push(l)
889 889 break
890 890 s = l[2:]
891 891 if l.startswith('- ') or l.startswith('! '):
892 892 u = '-' + s
893 893 elif l.startswith(' '):
894 894 u = ' ' + s
895 895 else:
896 896 raise PatchError(_("bad hunk #%d old text line %d") %
897 897 (self.number, x))
898 898 self.a.append(u)
899 899 self.hunk.append(u)
900 900
901 901 l = lr.readline()
902 902 if l.startswith('\ '):
903 903 s = self.a[-1][:-1]
904 904 self.a[-1] = s
905 905 self.hunk[-1] = s
906 906 l = lr.readline()
907 907 m = contextdesc.match(l)
908 908 if not m:
909 909 raise PatchError(_("bad hunk #%d") % self.number)
910 910 self.startb, bend = m.groups()
911 911 self.startb = int(self.startb)
912 912 if bend is None:
913 913 bend = self.startb
914 914 self.lenb = int(bend) - self.startb
915 915 if self.startb:
916 916 self.lenb += 1
917 917 hunki = 1
918 918 for x in xrange(self.lenb):
919 919 l = lr.readline()
920 920 if l.startswith('\ '):
921 921 # XXX: the only way to hit this is with an invalid line range.
922 922 # The no-eol marker is not counted in the line range, but I
923 923 # guess there are diff(1) out there which behave differently.
924 924 s = self.b[-1][:-1]
925 925 self.b[-1] = s
926 926 self.hunk[hunki - 1] = s
927 927 continue
928 928 if not l:
929 929 # line deletions, new block is empty and we hit EOF
930 930 lr.push(l)
931 931 break
932 932 s = l[2:]
933 933 if l.startswith('+ ') or l.startswith('! '):
934 934 u = '+' + s
935 935 elif l.startswith(' '):
936 936 u = ' ' + s
937 937 elif len(self.b) == 0:
938 938 # line deletions, new block is empty
939 939 lr.push(l)
940 940 break
941 941 else:
942 942 raise PatchError(_("bad hunk #%d old text line %d") %
943 943 (self.number, x))
944 944 self.b.append(s)
945 945 while True:
946 946 if hunki >= len(self.hunk):
947 947 h = ""
948 948 else:
949 949 h = self.hunk[hunki]
950 950 hunki += 1
951 951 if h == u:
952 952 break
953 953 elif h.startswith('-'):
954 954 continue
955 955 else:
956 956 self.hunk.insert(hunki - 1, u)
957 957 break
958 958
959 959 if not self.a:
960 960 # this happens when lines were only added to the hunk
961 961 for x in self.hunk:
962 962 if x.startswith('-') or x.startswith(' '):
963 963 self.a.append(x)
964 964 if not self.b:
965 965 # this happens when lines were only deleted from the hunk
966 966 for x in self.hunk:
967 967 if x.startswith('+') or x.startswith(' '):
968 968 self.b.append(x[1:])
969 969 # @@ -start,len +start,len @@
970 970 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
971 971 self.startb, self.lenb)
972 972 self.hunk[0] = self.desc
973 973 self._fixnewline(lr)
974 974
975 975 def _fixnewline(self, lr):
976 976 l = lr.readline()
977 977 if l.startswith('\ '):
978 978 diffhelpers.fix_newline(self.hunk, self.a, self.b)
979 979 else:
980 980 lr.push(l)
981 981
982 982 def complete(self):
983 983 return len(self.a) == self.lena and len(self.b) == self.lenb
984 984
985 985 def _fuzzit(self, old, new, fuzz, toponly):
986 986 # this removes context lines from the top and bottom of list 'l'. It
987 987 # checks the hunk to make sure only context lines are removed, and then
988 988 # returns a new shortened list of lines.
989 989 fuzz = min(fuzz, len(old))
990 990 if fuzz:
991 991 top = 0
992 992 bot = 0
993 993 hlen = len(self.hunk)
994 994 for x in xrange(hlen - 1):
995 995 # the hunk starts with the @@ line, so use x+1
996 996 if self.hunk[x + 1][0] == ' ':
997 997 top += 1
998 998 else:
999 999 break
1000 1000 if not toponly:
1001 1001 for x in xrange(hlen - 1):
1002 1002 if self.hunk[hlen - bot - 1][0] == ' ':
1003 1003 bot += 1
1004 1004 else:
1005 1005 break
1006 1006
1007 1007 bot = min(fuzz, bot)
1008 1008 top = min(fuzz, top)
1009 1009 return old[top:len(old)-bot], new[top:len(new)-bot], top
1010 1010 return old, new, 0
1011 1011
1012 1012 def fuzzit(self, fuzz, toponly):
1013 1013 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1014 1014 oldstart = self.starta + top
1015 1015 newstart = self.startb + top
1016 1016 # zero length hunk ranges already have their start decremented
1017 if self.lena:
1017 if self.lena and oldstart > 0:
1018 1018 oldstart -= 1
1019 if self.lenb:
1019 if self.lenb and newstart > 0:
1020 1020 newstart -= 1
1021 1021 return old, oldstart, new, newstart
1022 1022
1023 1023 class binhunk(object):
1024 1024 'A binary patch file. Only understands literals so far.'
1025 1025 def __init__(self, lr, fname):
1026 1026 self.text = None
1027 1027 self.hunk = ['GIT binary patch\n']
1028 1028 self._fname = fname
1029 1029 self._read(lr)
1030 1030
1031 1031 def complete(self):
1032 1032 return self.text is not None
1033 1033
1034 1034 def new(self):
1035 1035 return [self.text]
1036 1036
1037 1037 def _read(self, lr):
1038 1038 def getline(lr, hunk):
1039 1039 l = lr.readline()
1040 1040 hunk.append(l)
1041 1041 return l.rstrip('\r\n')
1042 1042
1043 1043 while True:
1044 1044 line = getline(lr, self.hunk)
1045 1045 if not line:
1046 1046 raise PatchError(_('could not extract "%s" binary data')
1047 1047 % self._fname)
1048 1048 if line.startswith('literal '):
1049 1049 break
1050 1050 size = int(line[8:].rstrip())
1051 1051 dec = []
1052 1052 line = getline(lr, self.hunk)
1053 1053 while len(line) > 1:
1054 1054 l = line[0]
1055 1055 if l <= 'Z' and l >= 'A':
1056 1056 l = ord(l) - ord('A') + 1
1057 1057 else:
1058 1058 l = ord(l) - ord('a') + 27
1059 1059 try:
1060 1060 dec.append(base85.b85decode(line[1:])[:l])
1061 1061 except ValueError, e:
1062 1062 raise PatchError(_('could not decode "%s" binary patch: %s')
1063 1063 % (self._fname, str(e)))
1064 1064 line = getline(lr, self.hunk)
1065 1065 text = zlib.decompress(''.join(dec))
1066 1066 if len(text) != size:
1067 1067 raise PatchError(_('"%s" length is %d bytes, should be %d')
1068 1068 % (self._fname, len(text), size))
1069 1069 self.text = text
1070 1070
1071 1071 def parsefilename(str):
1072 1072 # --- filename \t|space stuff
1073 1073 s = str[4:].rstrip('\r\n')
1074 1074 i = s.find('\t')
1075 1075 if i < 0:
1076 1076 i = s.find(' ')
1077 1077 if i < 0:
1078 1078 return s
1079 1079 return s[:i]
1080 1080
1081 1081 def pathstrip(path, strip):
1082 1082 pathlen = len(path)
1083 1083 i = 0
1084 1084 if strip == 0:
1085 1085 return '', path.rstrip()
1086 1086 count = strip
1087 1087 while count > 0:
1088 1088 i = path.find('/', i)
1089 1089 if i == -1:
1090 1090 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1091 1091 (count, strip, path))
1092 1092 i += 1
1093 1093 # consume '//' in the path
1094 1094 while i < pathlen - 1 and path[i] == '/':
1095 1095 i += 1
1096 1096 count -= 1
1097 1097 return path[:i].lstrip(), path[i:].rstrip()
1098 1098
1099 1099 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1100 1100 nulla = afile_orig == "/dev/null"
1101 1101 nullb = bfile_orig == "/dev/null"
1102 1102 create = nulla and hunk.starta == 0 and hunk.lena == 0
1103 1103 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1104 1104 abase, afile = pathstrip(afile_orig, strip)
1105 1105 gooda = not nulla and backend.exists(afile)
1106 1106 bbase, bfile = pathstrip(bfile_orig, strip)
1107 1107 if afile == bfile:
1108 1108 goodb = gooda
1109 1109 else:
1110 1110 goodb = not nullb and backend.exists(bfile)
1111 1111 missing = not goodb and not gooda and not create
1112 1112
1113 1113 # some diff programs apparently produce patches where the afile is
1114 1114 # not /dev/null, but afile starts with bfile
1115 1115 abasedir = afile[:afile.rfind('/') + 1]
1116 1116 bbasedir = bfile[:bfile.rfind('/') + 1]
1117 1117 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1118 1118 and hunk.starta == 0 and hunk.lena == 0):
1119 1119 create = True
1120 1120 missing = False
1121 1121
1122 1122 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1123 1123 # diff is between a file and its backup. In this case, the original
1124 1124 # file should be patched (see original mpatch code).
1125 1125 isbackup = (abase == bbase and bfile.startswith(afile))
1126 1126 fname = None
1127 1127 if not missing:
1128 1128 if gooda and goodb:
1129 1129 fname = isbackup and afile or bfile
1130 1130 elif gooda:
1131 1131 fname = afile
1132 1132
1133 1133 if not fname:
1134 1134 if not nullb:
1135 1135 fname = isbackup and afile or bfile
1136 1136 elif not nulla:
1137 1137 fname = afile
1138 1138 else:
1139 1139 raise PatchError(_("undefined source and destination files"))
1140 1140
1141 1141 gp = patchmeta(fname)
1142 1142 if create:
1143 1143 gp.op = 'ADD'
1144 1144 elif remove:
1145 1145 gp.op = 'DELETE'
1146 1146 return gp
1147 1147
1148 1148 def scangitpatch(lr, firstline):
1149 1149 """
1150 1150 Git patches can emit:
1151 1151 - rename a to b
1152 1152 - change b
1153 1153 - copy a to c
1154 1154 - change c
1155 1155
1156 1156 We cannot apply this sequence as-is, the renamed 'a' could not be
1157 1157 found for it would have been renamed already. And we cannot copy
1158 1158 from 'b' instead because 'b' would have been changed already. So
1159 1159 we scan the git patch for copy and rename commands so we can
1160 1160 perform the copies ahead of time.
1161 1161 """
1162 1162 pos = 0
1163 1163 try:
1164 1164 pos = lr.fp.tell()
1165 1165 fp = lr.fp
1166 1166 except IOError:
1167 1167 fp = cStringIO.StringIO(lr.fp.read())
1168 1168 gitlr = linereader(fp)
1169 1169 gitlr.push(firstline)
1170 1170 gitpatches = readgitpatch(gitlr)
1171 1171 fp.seek(pos)
1172 1172 return gitpatches
1173 1173
1174 1174 def iterhunks(fp):
1175 1175 """Read a patch and yield the following events:
1176 1176 - ("file", afile, bfile, firsthunk): select a new target file.
1177 1177 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1178 1178 "file" event.
1179 1179 - ("git", gitchanges): current diff is in git format, gitchanges
1180 1180 maps filenames to gitpatch records. Unique event.
1181 1181 """
1182 1182 afile = ""
1183 1183 bfile = ""
1184 1184 state = None
1185 1185 hunknum = 0
1186 1186 emitfile = newfile = False
1187 1187 gitpatches = None
1188 1188
1189 1189 # our states
1190 1190 BFILE = 1
1191 1191 context = None
1192 1192 lr = linereader(fp)
1193 1193
1194 1194 while True:
1195 1195 x = lr.readline()
1196 1196 if not x:
1197 1197 break
1198 1198 if state == BFILE and (
1199 1199 (not context and x[0] == '@')
1200 1200 or (context is not False and x.startswith('***************'))
1201 1201 or x.startswith('GIT binary patch')):
1202 1202 gp = None
1203 1203 if (gitpatches and
1204 1204 gitpatches[-1].ispatching(afile, bfile)):
1205 1205 gp = gitpatches.pop()
1206 1206 if x.startswith('GIT binary patch'):
1207 1207 h = binhunk(lr, gp.path)
1208 1208 else:
1209 1209 if context is None and x.startswith('***************'):
1210 1210 context = True
1211 1211 h = hunk(x, hunknum + 1, lr, context)
1212 1212 hunknum += 1
1213 1213 if emitfile:
1214 1214 emitfile = False
1215 1215 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1216 1216 yield 'hunk', h
1217 1217 elif x.startswith('diff --git'):
1218 1218 m = gitre.match(x.rstrip(' \r\n'))
1219 1219 if not m:
1220 1220 continue
1221 1221 if gitpatches is None:
1222 1222 # scan whole input for git metadata
1223 1223 gitpatches = scangitpatch(lr, x)
1224 1224 yield 'git', [g.copy() for g in gitpatches
1225 1225 if g.op in ('COPY', 'RENAME')]
1226 1226 gitpatches.reverse()
1227 1227 afile = 'a/' + m.group(1)
1228 1228 bfile = 'b/' + m.group(2)
1229 1229 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1230 1230 gp = gitpatches.pop()
1231 1231 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1232 1232 if not gitpatches:
1233 1233 raise PatchError(_('failed to synchronize metadata for "%s"')
1234 1234 % afile[2:])
1235 1235 gp = gitpatches[-1]
1236 1236 newfile = True
1237 1237 elif x.startswith('---'):
1238 1238 # check for a unified diff
1239 1239 l2 = lr.readline()
1240 1240 if not l2.startswith('+++'):
1241 1241 lr.push(l2)
1242 1242 continue
1243 1243 newfile = True
1244 1244 context = False
1245 1245 afile = parsefilename(x)
1246 1246 bfile = parsefilename(l2)
1247 1247 elif x.startswith('***'):
1248 1248 # check for a context diff
1249 1249 l2 = lr.readline()
1250 1250 if not l2.startswith('---'):
1251 1251 lr.push(l2)
1252 1252 continue
1253 1253 l3 = lr.readline()
1254 1254 lr.push(l3)
1255 1255 if not l3.startswith("***************"):
1256 1256 lr.push(l2)
1257 1257 continue
1258 1258 newfile = True
1259 1259 context = True
1260 1260 afile = parsefilename(x)
1261 1261 bfile = parsefilename(l2)
1262 1262
1263 1263 if newfile:
1264 1264 newfile = False
1265 1265 emitfile = True
1266 1266 state = BFILE
1267 1267 hunknum = 0
1268 1268
1269 1269 while gitpatches:
1270 1270 gp = gitpatches.pop()
1271 1271 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1272 1272
1273 1273 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1274 1274 """Reads a patch from fp and tries to apply it.
1275 1275
1276 1276 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1277 1277 there was any fuzz.
1278 1278
1279 1279 If 'eolmode' is 'strict', the patch content and patched file are
1280 1280 read in binary mode. Otherwise, line endings are ignored when
1281 1281 patching then normalized according to 'eolmode'.
1282 1282 """
1283 1283 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1284 1284 eolmode=eolmode)
1285 1285
1286 1286 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1287 1287 eolmode='strict'):
1288 1288
1289 1289 def pstrip(p):
1290 1290 return pathstrip(p, strip - 1)[1]
1291 1291
1292 1292 rejects = 0
1293 1293 err = 0
1294 1294 current_file = None
1295 1295
1296 1296 for state, values in iterhunks(fp):
1297 1297 if state == 'hunk':
1298 1298 if not current_file:
1299 1299 continue
1300 1300 ret = current_file.apply(values)
1301 1301 if ret > 0:
1302 1302 err = 1
1303 1303 elif state == 'file':
1304 1304 if current_file:
1305 1305 rejects += current_file.close()
1306 1306 current_file = None
1307 1307 afile, bfile, first_hunk, gp = values
1308 1308 if gp:
1309 1309 gp.path = pstrip(gp.path)
1310 1310 if gp.oldpath:
1311 1311 gp.oldpath = pstrip(gp.oldpath)
1312 1312 else:
1313 1313 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1314 1314 if gp.op == 'RENAME':
1315 1315 backend.unlink(gp.oldpath)
1316 1316 if not first_hunk:
1317 1317 if gp.op == 'DELETE':
1318 1318 backend.unlink(gp.path)
1319 1319 continue
1320 1320 data, mode = None, None
1321 1321 if gp.op in ('RENAME', 'COPY'):
1322 1322 data, mode = store.getfile(gp.oldpath)[:2]
1323 1323 if gp.mode:
1324 1324 mode = gp.mode
1325 1325 if gp.op == 'ADD':
1326 1326 # Added files without content have no hunk and
1327 1327 # must be created
1328 1328 data = ''
1329 1329 if data or mode:
1330 1330 if (gp.op in ('ADD', 'RENAME', 'COPY')
1331 1331 and backend.exists(gp.path)):
1332 1332 raise PatchError(_("cannot create %s: destination "
1333 1333 "already exists") % gp.path)
1334 1334 backend.setfile(gp.path, data, mode, gp.oldpath)
1335 1335 continue
1336 1336 try:
1337 1337 current_file = patcher(ui, gp, backend, store,
1338 1338 eolmode=eolmode)
1339 1339 except PatchError, inst:
1340 1340 ui.warn(str(inst) + '\n')
1341 1341 current_file = None
1342 1342 rejects += 1
1343 1343 continue
1344 1344 elif state == 'git':
1345 1345 for gp in values:
1346 1346 path = pstrip(gp.oldpath)
1347 1347 data, mode = backend.getfile(path)
1348 1348 store.setfile(path, data, mode)
1349 1349 else:
1350 1350 raise util.Abort(_('unsupported parser state: %s') % state)
1351 1351
1352 1352 if current_file:
1353 1353 rejects += current_file.close()
1354 1354
1355 1355 if rejects:
1356 1356 return -1
1357 1357 return err
1358 1358
1359 1359 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1360 1360 similarity):
1361 1361 """use <patcher> to apply <patchname> to the working directory.
1362 1362 returns whether patch was applied with fuzz factor."""
1363 1363
1364 1364 fuzz = False
1365 1365 args = []
1366 1366 cwd = repo.root
1367 1367 if cwd:
1368 1368 args.append('-d %s' % util.shellquote(cwd))
1369 1369 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1370 1370 util.shellquote(patchname)))
1371 1371 try:
1372 1372 for line in fp:
1373 1373 line = line.rstrip()
1374 1374 ui.note(line + '\n')
1375 1375 if line.startswith('patching file '):
1376 1376 pf = util.parsepatchoutput(line)
1377 1377 printed_file = False
1378 1378 files.add(pf)
1379 1379 elif line.find('with fuzz') >= 0:
1380 1380 fuzz = True
1381 1381 if not printed_file:
1382 1382 ui.warn(pf + '\n')
1383 1383 printed_file = True
1384 1384 ui.warn(line + '\n')
1385 1385 elif line.find('saving rejects to file') >= 0:
1386 1386 ui.warn(line + '\n')
1387 1387 elif line.find('FAILED') >= 0:
1388 1388 if not printed_file:
1389 1389 ui.warn(pf + '\n')
1390 1390 printed_file = True
1391 1391 ui.warn(line + '\n')
1392 1392 finally:
1393 1393 if files:
1394 1394 cfiles = list(files)
1395 1395 cwd = repo.getcwd()
1396 1396 if cwd:
1397 1397 cfiles = [util.pathto(repo.root, cwd, f)
1398 1398 for f in cfiles]
1399 1399 scmutil.addremove(repo, cfiles, similarity=similarity)
1400 1400 code = fp.close()
1401 1401 if code:
1402 1402 raise PatchError(_("patch command failed: %s") %
1403 1403 util.explainexit(code)[0])
1404 1404 return fuzz
1405 1405
1406 1406 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1407 1407 if files is None:
1408 1408 files = set()
1409 1409 if eolmode is None:
1410 1410 eolmode = ui.config('patch', 'eol', 'strict')
1411 1411 if eolmode.lower() not in eolmodes:
1412 1412 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1413 1413 eolmode = eolmode.lower()
1414 1414
1415 1415 store = filestore()
1416 1416 try:
1417 1417 fp = open(patchobj, 'rb')
1418 1418 except TypeError:
1419 1419 fp = patchobj
1420 1420 try:
1421 1421 ret = applydiff(ui, fp, backend, store, strip=strip,
1422 1422 eolmode=eolmode)
1423 1423 finally:
1424 1424 if fp != patchobj:
1425 1425 fp.close()
1426 1426 files.update(backend.close())
1427 1427 store.close()
1428 1428 if ret < 0:
1429 1429 raise PatchError(_('patch failed to apply'))
1430 1430 return ret > 0
1431 1431
1432 1432 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1433 1433 similarity=0):
1434 1434 """use builtin patch to apply <patchobj> to the working directory.
1435 1435 returns whether patch was applied with fuzz factor."""
1436 1436 backend = workingbackend(ui, repo, similarity)
1437 1437 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1438 1438
1439 1439 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1440 1440 eolmode='strict'):
1441 1441 backend = repobackend(ui, repo, ctx, store)
1442 1442 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1443 1443
1444 1444 def makememctx(repo, parents, text, user, date, branch, files, store,
1445 1445 editor=None):
1446 1446 def getfilectx(repo, memctx, path):
1447 1447 data, (islink, isexec), copied = store.getfile(path)
1448 1448 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1449 1449 copied=copied)
1450 1450 extra = {}
1451 1451 if branch:
1452 1452 extra['branch'] = encoding.fromlocal(branch)
1453 1453 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1454 1454 date, extra)
1455 1455 if editor:
1456 1456 ctx._text = editor(repo, ctx, [])
1457 1457 return ctx
1458 1458
1459 1459 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1460 1460 similarity=0):
1461 1461 """Apply <patchname> to the working directory.
1462 1462
1463 1463 'eolmode' specifies how end of lines should be handled. It can be:
1464 1464 - 'strict': inputs are read in binary mode, EOLs are preserved
1465 1465 - 'crlf': EOLs are ignored when patching and reset to CRLF
1466 1466 - 'lf': EOLs are ignored when patching and reset to LF
1467 1467 - None: get it from user settings, default to 'strict'
1468 1468 'eolmode' is ignored when using an external patcher program.
1469 1469
1470 1470 Returns whether patch was applied with fuzz factor.
1471 1471 """
1472 1472 patcher = ui.config('ui', 'patch')
1473 1473 if files is None:
1474 1474 files = set()
1475 1475 try:
1476 1476 if patcher:
1477 1477 return _externalpatch(ui, repo, patcher, patchname, strip,
1478 1478 files, similarity)
1479 1479 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1480 1480 similarity)
1481 1481 except PatchError, err:
1482 1482 raise util.Abort(str(err))
1483 1483
1484 1484 def changedfiles(ui, repo, patchpath, strip=1):
1485 1485 backend = fsbackend(ui, repo.root)
1486 1486 fp = open(patchpath, 'rb')
1487 1487 try:
1488 1488 changed = set()
1489 1489 for state, values in iterhunks(fp):
1490 1490 if state == 'file':
1491 1491 afile, bfile, first_hunk, gp = values
1492 1492 if gp:
1493 1493 gp.path = pathstrip(gp.path, strip - 1)[1]
1494 1494 if gp.oldpath:
1495 1495 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1496 1496 else:
1497 1497 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1498 1498 changed.add(gp.path)
1499 1499 if gp.op == 'RENAME':
1500 1500 changed.add(gp.oldpath)
1501 1501 elif state not in ('hunk', 'git'):
1502 1502 raise util.Abort(_('unsupported parser state: %s') % state)
1503 1503 return changed
1504 1504 finally:
1505 1505 fp.close()
1506 1506
1507 1507 def b85diff(to, tn):
1508 1508 '''print base85-encoded binary diff'''
1509 1509 def gitindex(text):
1510 1510 if not text:
1511 1511 return hex(nullid)
1512 1512 l = len(text)
1513 1513 s = util.sha1('blob %d\0' % l)
1514 1514 s.update(text)
1515 1515 return s.hexdigest()
1516 1516
1517 1517 def fmtline(line):
1518 1518 l = len(line)
1519 1519 if l <= 26:
1520 1520 l = chr(ord('A') + l - 1)
1521 1521 else:
1522 1522 l = chr(l - 26 + ord('a') - 1)
1523 1523 return '%c%s\n' % (l, base85.b85encode(line, True))
1524 1524
1525 1525 def chunk(text, csize=52):
1526 1526 l = len(text)
1527 1527 i = 0
1528 1528 while i < l:
1529 1529 yield text[i:i + csize]
1530 1530 i += csize
1531 1531
1532 1532 tohash = gitindex(to)
1533 1533 tnhash = gitindex(tn)
1534 1534 if tohash == tnhash:
1535 1535 return ""
1536 1536
1537 1537 # TODO: deltas
1538 1538 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1539 1539 (tohash, tnhash, len(tn))]
1540 1540 for l in chunk(zlib.compress(tn)):
1541 1541 ret.append(fmtline(l))
1542 1542 ret.append('\n')
1543 1543 return ''.join(ret)
1544 1544
1545 1545 class GitDiffRequired(Exception):
1546 1546 pass
1547 1547
1548 1548 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1549 1549 def get(key, name=None, getter=ui.configbool):
1550 1550 return ((opts and opts.get(key)) or
1551 1551 getter(section, name or key, None, untrusted=untrusted))
1552 1552 return mdiff.diffopts(
1553 1553 text=opts and opts.get('text'),
1554 1554 git=get('git'),
1555 1555 nodates=get('nodates'),
1556 1556 showfunc=get('show_function', 'showfunc'),
1557 1557 ignorews=get('ignore_all_space', 'ignorews'),
1558 1558 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1559 1559 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1560 1560 context=get('unified', getter=ui.config))
1561 1561
1562 1562 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1563 1563 losedatafn=None, prefix=''):
1564 1564 '''yields diff of changes to files between two nodes, or node and
1565 1565 working directory.
1566 1566
1567 1567 if node1 is None, use first dirstate parent instead.
1568 1568 if node2 is None, compare node1 with working directory.
1569 1569
1570 1570 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1571 1571 every time some change cannot be represented with the current
1572 1572 patch format. Return False to upgrade to git patch format, True to
1573 1573 accept the loss or raise an exception to abort the diff. It is
1574 1574 called with the name of current file being diffed as 'fn'. If set
1575 1575 to None, patches will always be upgraded to git format when
1576 1576 necessary.
1577 1577
1578 1578 prefix is a filename prefix that is prepended to all filenames on
1579 1579 display (used for subrepos).
1580 1580 '''
1581 1581
1582 1582 if opts is None:
1583 1583 opts = mdiff.defaultopts
1584 1584
1585 1585 if not node1 and not node2:
1586 1586 node1 = repo.dirstate.p1()
1587 1587
1588 1588 def lrugetfilectx():
1589 1589 cache = {}
1590 1590 order = []
1591 1591 def getfilectx(f, ctx):
1592 1592 fctx = ctx.filectx(f, filelog=cache.get(f))
1593 1593 if f not in cache:
1594 1594 if len(cache) > 20:
1595 1595 del cache[order.pop(0)]
1596 1596 cache[f] = fctx.filelog()
1597 1597 else:
1598 1598 order.remove(f)
1599 1599 order.append(f)
1600 1600 return fctx
1601 1601 return getfilectx
1602 1602 getfilectx = lrugetfilectx()
1603 1603
1604 1604 ctx1 = repo[node1]
1605 1605 ctx2 = repo[node2]
1606 1606
1607 1607 if not changes:
1608 1608 changes = repo.status(ctx1, ctx2, match=match)
1609 1609 modified, added, removed = changes[:3]
1610 1610
1611 1611 if not modified and not added and not removed:
1612 1612 return []
1613 1613
1614 1614 revs = None
1615 1615 if not repo.ui.quiet:
1616 1616 hexfunc = repo.ui.debugflag and hex or short
1617 1617 revs = [hexfunc(node) for node in [node1, node2] if node]
1618 1618
1619 1619 copy = {}
1620 1620 if opts.git or opts.upgrade:
1621 1621 copy = copies.pathcopies(ctx1, ctx2)
1622 1622
1623 1623 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1624 1624 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1625 1625 if opts.upgrade and not opts.git:
1626 1626 try:
1627 1627 def losedata(fn):
1628 1628 if not losedatafn or not losedatafn(fn=fn):
1629 1629 raise GitDiffRequired()
1630 1630 # Buffer the whole output until we are sure it can be generated
1631 1631 return list(difffn(opts.copy(git=False), losedata))
1632 1632 except GitDiffRequired:
1633 1633 return difffn(opts.copy(git=True), None)
1634 1634 else:
1635 1635 return difffn(opts, None)
1636 1636
1637 1637 def difflabel(func, *args, **kw):
1638 1638 '''yields 2-tuples of (output, label) based on the output of func()'''
1639 1639 headprefixes = [('diff', 'diff.diffline'),
1640 1640 ('copy', 'diff.extended'),
1641 1641 ('rename', 'diff.extended'),
1642 1642 ('old', 'diff.extended'),
1643 1643 ('new', 'diff.extended'),
1644 1644 ('deleted', 'diff.extended'),
1645 1645 ('---', 'diff.file_a'),
1646 1646 ('+++', 'diff.file_b')]
1647 1647 textprefixes = [('@', 'diff.hunk'),
1648 1648 ('-', 'diff.deleted'),
1649 1649 ('+', 'diff.inserted')]
1650 1650 head = False
1651 1651 for chunk in func(*args, **kw):
1652 1652 lines = chunk.split('\n')
1653 1653 for i, line in enumerate(lines):
1654 1654 if i != 0:
1655 1655 yield ('\n', '')
1656 1656 if head:
1657 1657 if line.startswith('@'):
1658 1658 head = False
1659 1659 else:
1660 1660 if line and not line[0] in ' +-@\\':
1661 1661 head = True
1662 1662 stripline = line
1663 1663 if not head and line and line[0] in '+-':
1664 1664 # highlight trailing whitespace, but only in changed lines
1665 1665 stripline = line.rstrip()
1666 1666 prefixes = textprefixes
1667 1667 if head:
1668 1668 prefixes = headprefixes
1669 1669 for prefix, label in prefixes:
1670 1670 if stripline.startswith(prefix):
1671 1671 yield (stripline, label)
1672 1672 break
1673 1673 else:
1674 1674 yield (line, '')
1675 1675 if line != stripline:
1676 1676 yield (line[len(stripline):], 'diff.trailingwhitespace')
1677 1677
1678 1678 def diffui(*args, **kw):
1679 1679 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1680 1680 return difflabel(diff, *args, **kw)
1681 1681
1682 1682
1683 1683 def _addmodehdr(header, omode, nmode):
1684 1684 if omode != nmode:
1685 1685 header.append('old mode %s\n' % omode)
1686 1686 header.append('new mode %s\n' % nmode)
1687 1687
1688 1688 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1689 1689 copy, getfilectx, opts, losedatafn, prefix):
1690 1690
1691 1691 def join(f):
1692 1692 return os.path.join(prefix, f)
1693 1693
1694 1694 date1 = util.datestr(ctx1.date())
1695 1695 man1 = ctx1.manifest()
1696 1696
1697 1697 gone = set()
1698 1698 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1699 1699
1700 1700 copyto = dict([(v, k) for k, v in copy.items()])
1701 1701
1702 1702 if opts.git:
1703 1703 revs = None
1704 1704
1705 1705 for f in sorted(modified + added + removed):
1706 1706 to = None
1707 1707 tn = None
1708 1708 dodiff = True
1709 1709 header = []
1710 1710 if f in man1:
1711 1711 to = getfilectx(f, ctx1).data()
1712 1712 if f not in removed:
1713 1713 tn = getfilectx(f, ctx2).data()
1714 1714 a, b = f, f
1715 1715 if opts.git or losedatafn:
1716 1716 if f in added:
1717 1717 mode = gitmode[ctx2.flags(f)]
1718 1718 if f in copy or f in copyto:
1719 1719 if opts.git:
1720 1720 if f in copy:
1721 1721 a = copy[f]
1722 1722 else:
1723 1723 a = copyto[f]
1724 1724 omode = gitmode[man1.flags(a)]
1725 1725 _addmodehdr(header, omode, mode)
1726 1726 if a in removed and a not in gone:
1727 1727 op = 'rename'
1728 1728 gone.add(a)
1729 1729 else:
1730 1730 op = 'copy'
1731 1731 header.append('%s from %s\n' % (op, join(a)))
1732 1732 header.append('%s to %s\n' % (op, join(f)))
1733 1733 to = getfilectx(a, ctx1).data()
1734 1734 else:
1735 1735 losedatafn(f)
1736 1736 else:
1737 1737 if opts.git:
1738 1738 header.append('new file mode %s\n' % mode)
1739 1739 elif ctx2.flags(f):
1740 1740 losedatafn(f)
1741 1741 # In theory, if tn was copied or renamed we should check
1742 1742 # if the source is binary too but the copy record already
1743 1743 # forces git mode.
1744 1744 if util.binary(tn):
1745 1745 if opts.git:
1746 1746 dodiff = 'binary'
1747 1747 else:
1748 1748 losedatafn(f)
1749 1749 if not opts.git and not tn:
1750 1750 # regular diffs cannot represent new empty file
1751 1751 losedatafn(f)
1752 1752 elif f in removed:
1753 1753 if opts.git:
1754 1754 # have we already reported a copy above?
1755 1755 if ((f in copy and copy[f] in added
1756 1756 and copyto[copy[f]] == f) or
1757 1757 (f in copyto and copyto[f] in added
1758 1758 and copy[copyto[f]] == f)):
1759 1759 dodiff = False
1760 1760 else:
1761 1761 header.append('deleted file mode %s\n' %
1762 1762 gitmode[man1.flags(f)])
1763 1763 elif not to or util.binary(to):
1764 1764 # regular diffs cannot represent empty file deletion
1765 1765 losedatafn(f)
1766 1766 else:
1767 1767 oflag = man1.flags(f)
1768 1768 nflag = ctx2.flags(f)
1769 1769 binary = util.binary(to) or util.binary(tn)
1770 1770 if opts.git:
1771 1771 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1772 1772 if binary:
1773 1773 dodiff = 'binary'
1774 1774 elif binary or nflag != oflag:
1775 1775 losedatafn(f)
1776 1776 if opts.git:
1777 1777 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1778 1778
1779 1779 if dodiff:
1780 1780 if dodiff == 'binary':
1781 1781 text = b85diff(to, tn)
1782 1782 else:
1783 1783 text = mdiff.unidiff(to, date1,
1784 1784 # ctx2 date may be dynamic
1785 1785 tn, util.datestr(ctx2.date()),
1786 1786 join(a), join(b), revs, opts=opts)
1787 1787 if header and (text or len(header) > 1):
1788 1788 yield ''.join(header)
1789 1789 if text:
1790 1790 yield text
1791 1791
1792 1792 def diffstatsum(stats):
1793 1793 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1794 1794 for f, a, r, b in stats:
1795 1795 maxfile = max(maxfile, encoding.colwidth(f))
1796 1796 maxtotal = max(maxtotal, a + r)
1797 1797 addtotal += a
1798 1798 removetotal += r
1799 1799 binary = binary or b
1800 1800
1801 1801 return maxfile, maxtotal, addtotal, removetotal, binary
1802 1802
1803 1803 def diffstatdata(lines):
1804 1804 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1805 1805
1806 1806 results = []
1807 1807 filename, adds, removes, isbinary = None, 0, 0, False
1808 1808
1809 1809 def addresult():
1810 1810 if filename:
1811 1811 results.append((filename, adds, removes, isbinary))
1812 1812
1813 1813 for line in lines:
1814 1814 if line.startswith('diff'):
1815 1815 addresult()
1816 1816 # set numbers to 0 anyway when starting new file
1817 1817 adds, removes, isbinary = 0, 0, False
1818 1818 if line.startswith('diff --git'):
1819 1819 filename = gitre.search(line).group(1)
1820 1820 elif line.startswith('diff -r'):
1821 1821 # format: "diff -r ... -r ... filename"
1822 1822 filename = diffre.search(line).group(1)
1823 1823 elif line.startswith('+') and not line.startswith('+++ '):
1824 1824 adds += 1
1825 1825 elif line.startswith('-') and not line.startswith('--- '):
1826 1826 removes += 1
1827 1827 elif (line.startswith('GIT binary patch') or
1828 1828 line.startswith('Binary file')):
1829 1829 isbinary = True
1830 1830 addresult()
1831 1831 return results
1832 1832
1833 1833 def diffstat(lines, width=80, git=False):
1834 1834 output = []
1835 1835 stats = diffstatdata(lines)
1836 1836 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1837 1837
1838 1838 countwidth = len(str(maxtotal))
1839 1839 if hasbinary and countwidth < 3:
1840 1840 countwidth = 3
1841 1841 graphwidth = width - countwidth - maxname - 6
1842 1842 if graphwidth < 10:
1843 1843 graphwidth = 10
1844 1844
1845 1845 def scale(i):
1846 1846 if maxtotal <= graphwidth:
1847 1847 return i
1848 1848 # If diffstat runs out of room it doesn't print anything,
1849 1849 # which isn't very useful, so always print at least one + or -
1850 1850 # if there were at least some changes.
1851 1851 return max(i * graphwidth // maxtotal, int(bool(i)))
1852 1852
1853 1853 for filename, adds, removes, isbinary in stats:
1854 1854 if isbinary:
1855 1855 count = 'Bin'
1856 1856 else:
1857 1857 count = adds + removes
1858 1858 pluses = '+' * scale(adds)
1859 1859 minuses = '-' * scale(removes)
1860 1860 output.append(' %s%s | %*s %s%s\n' %
1861 1861 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1862 1862 countwidth, count, pluses, minuses))
1863 1863
1864 1864 if stats:
1865 1865 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1866 1866 % (len(stats), totaladds, totalremoves))
1867 1867
1868 1868 return ''.join(output)
1869 1869
1870 1870 def diffstatui(*args, **kw):
1871 1871 '''like diffstat(), but yields 2-tuples of (output, label) for
1872 1872 ui.write()
1873 1873 '''
1874 1874
1875 1875 for line in diffstat(*args, **kw).splitlines():
1876 1876 if line and line[-1] in '+-':
1877 1877 name, graph = line.rsplit(' ', 1)
1878 1878 yield (name + ' ', '')
1879 1879 m = re.search(r'\++', graph)
1880 1880 if m:
1881 1881 yield (m.group(0), 'diffstat.inserted')
1882 1882 m = re.search(r'-+', graph)
1883 1883 if m:
1884 1884 yield (m.group(0), 'diffstat.deleted')
1885 1885 else:
1886 1886 yield (line, '')
1887 1887 yield ('\n', '')
@@ -1,1097 +1,1117 b''
1 1 $ "$TESTDIR/hghave" unix-permissions || exit 80
2 2
3 3 $ hg init a
4 4 $ mkdir a/d1
5 5 $ mkdir a/d1/d2
6 6 $ echo line 1 > a/a
7 7 $ echo line 1 > a/d1/d2/a
8 8 $ hg --cwd a ci -Ama
9 9 adding a
10 10 adding d1/d2/a
11 11
12 12 $ echo line 2 >> a/a
13 13 $ hg --cwd a ci -u someone -d '1 0' -m'second change'
14 14
15 15 import with no args:
16 16
17 17 $ hg --cwd a import
18 18 abort: need at least one patch to import
19 19 [255]
20 20
21 21 generate patches for the test
22 22
23 23 $ hg --cwd a export tip > exported-tip.patch
24 24 $ hg --cwd a diff -r0:1 > diffed-tip.patch
25 25
26 26
27 27 import exported patch
28 28
29 29 $ hg clone -r0 a b
30 30 adding changesets
31 31 adding manifests
32 32 adding file changes
33 33 added 1 changesets with 2 changes to 2 files
34 34 updating to branch default
35 35 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 36 $ hg --cwd b import ../exported-tip.patch
37 37 applying ../exported-tip.patch
38 38
39 39 message and committer should be same
40 40
41 41 $ hg --cwd b tip
42 42 changeset: 1:1d4bd90af0e4
43 43 tag: tip
44 44 user: someone
45 45 date: Thu Jan 01 00:00:01 1970 +0000
46 46 summary: second change
47 47
48 48 $ rm -r b
49 49
50 50
51 51 import exported patch with external patcher
52 52
53 53 $ cat > dummypatch.py <<EOF
54 54 > print 'patching file a'
55 55 > file('a', 'wb').write('line2\n')
56 56 > EOF
57 57 $ chmod +x dummypatch.py
58 58 $ hg clone -r0 a b
59 59 adding changesets
60 60 adding manifests
61 61 adding file changes
62 62 added 1 changesets with 2 changes to 2 files
63 63 updating to branch default
64 64 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 65 $ hg --config ui.patch='python ../dummypatch.py' --cwd b import ../exported-tip.patch
66 66 applying ../exported-tip.patch
67 67 $ cat b/a
68 68 line2
69 69 $ rm -r b
70 70
71 71
72 72 import of plain diff should fail without message
73 73
74 74 $ hg clone -r0 a b
75 75 adding changesets
76 76 adding manifests
77 77 adding file changes
78 78 added 1 changesets with 2 changes to 2 files
79 79 updating to branch default
80 80 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
81 81 $ hg --cwd b import ../diffed-tip.patch
82 82 applying ../diffed-tip.patch
83 83 abort: empty commit message
84 84 [255]
85 85 $ rm -r b
86 86
87 87
88 88 import of plain diff should be ok with message
89 89
90 90 $ hg clone -r0 a b
91 91 adding changesets
92 92 adding manifests
93 93 adding file changes
94 94 added 1 changesets with 2 changes to 2 files
95 95 updating to branch default
96 96 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
97 97 $ hg --cwd b import -mpatch ../diffed-tip.patch
98 98 applying ../diffed-tip.patch
99 99 $ rm -r b
100 100
101 101
102 102 import of plain diff with specific date and user
103 103
104 104 $ hg clone -r0 a b
105 105 adding changesets
106 106 adding manifests
107 107 adding file changes
108 108 added 1 changesets with 2 changes to 2 files
109 109 updating to branch default
110 110 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 111 $ hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../diffed-tip.patch
112 112 applying ../diffed-tip.patch
113 113 $ hg -R b tip -pv
114 114 changeset: 1:ca68f19f3a40
115 115 tag: tip
116 116 user: user@nowhere.net
117 117 date: Thu Jan 01 00:00:01 1970 +0000
118 118 files: a
119 119 description:
120 120 patch
121 121
122 122
123 123 diff -r 80971e65b431 -r ca68f19f3a40 a
124 124 --- a/a Thu Jan 01 00:00:00 1970 +0000
125 125 +++ b/a Thu Jan 01 00:00:01 1970 +0000
126 126 @@ -1,1 +1,2 @@
127 127 line 1
128 128 +line 2
129 129
130 130 $ rm -r b
131 131
132 132
133 133 import of plain diff should be ok with --no-commit
134 134
135 135 $ hg clone -r0 a b
136 136 adding changesets
137 137 adding manifests
138 138 adding file changes
139 139 added 1 changesets with 2 changes to 2 files
140 140 updating to branch default
141 141 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 142 $ hg --cwd b import --no-commit ../diffed-tip.patch
143 143 applying ../diffed-tip.patch
144 144 $ hg --cwd b diff --nodates
145 145 diff -r 80971e65b431 a
146 146 --- a/a
147 147 +++ b/a
148 148 @@ -1,1 +1,2 @@
149 149 line 1
150 150 +line 2
151 151 $ rm -r b
152 152
153 153
154 154 import of malformed plain diff should fail
155 155
156 156 $ hg clone -r0 a b
157 157 adding changesets
158 158 adding manifests
159 159 adding file changes
160 160 added 1 changesets with 2 changes to 2 files
161 161 updating to branch default
162 162 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
163 163 $ sed 's/1,1/foo/' < diffed-tip.patch > broken.patch
164 164 $ hg --cwd b import -mpatch ../broken.patch
165 165 applying ../broken.patch
166 166 abort: bad hunk #1
167 167 [255]
168 168 $ rm -r b
169 169
170 170
171 171 hg -R repo import
172 172 put the clone in a subdir - having a directory named "a"
173 173 used to hide a bug.
174 174
175 175 $ mkdir dir
176 176 $ hg clone -r0 a dir/b
177 177 adding changesets
178 178 adding manifests
179 179 adding file changes
180 180 added 1 changesets with 2 changes to 2 files
181 181 updating to branch default
182 182 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
183 183 $ cd dir
184 184 $ hg -R b import ../exported-tip.patch
185 185 applying ../exported-tip.patch
186 186 $ cd ..
187 187 $ rm -r dir
188 188
189 189
190 190 import from stdin
191 191
192 192 $ hg clone -r0 a b
193 193 adding changesets
194 194 adding manifests
195 195 adding file changes
196 196 added 1 changesets with 2 changes to 2 files
197 197 updating to branch default
198 198 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
199 199 $ hg --cwd b import - < exported-tip.patch
200 200 applying patch from stdin
201 201 $ rm -r b
202 202
203 203
204 204 import two patches in one stream
205 205
206 206 $ hg init b
207 207 $ hg --cwd a export 0:tip | hg --cwd b import -
208 208 applying patch from stdin
209 209 $ hg --cwd a id
210 210 1d4bd90af0e4 tip
211 211 $ hg --cwd b id
212 212 1d4bd90af0e4 tip
213 213 $ rm -r b
214 214
215 215
216 216 override commit message
217 217
218 218 $ hg clone -r0 a b
219 219 adding changesets
220 220 adding manifests
221 221 adding file changes
222 222 added 1 changesets with 2 changes to 2 files
223 223 updating to branch default
224 224 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
225 225 $ hg --cwd b import -m 'override' - < exported-tip.patch
226 226 applying patch from stdin
227 227 $ hg --cwd b tip | grep override
228 228 summary: override
229 229 $ rm -r b
230 230
231 231 $ cat > mkmsg.py <<EOF
232 232 > import email.Message, sys
233 233 > msg = email.Message.Message()
234 234 > patch = open(sys.argv[1], 'rb').read()
235 235 > msg.set_payload('email commit message\n' + patch)
236 236 > msg['Subject'] = 'email patch'
237 237 > msg['From'] = 'email patcher'
238 238 > file(sys.argv[2], 'wb').write(msg.as_string())
239 239 > EOF
240 240
241 241
242 242 plain diff in email, subject, message body
243 243
244 244 $ hg clone -r0 a b
245 245 adding changesets
246 246 adding manifests
247 247 adding file changes
248 248 added 1 changesets with 2 changes to 2 files
249 249 updating to branch default
250 250 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
251 251 $ python mkmsg.py diffed-tip.patch msg.patch
252 252 $ hg --cwd b import ../msg.patch
253 253 applying ../msg.patch
254 254 $ hg --cwd b tip | grep email
255 255 user: email patcher
256 256 summary: email patch
257 257 $ rm -r b
258 258
259 259
260 260 plain diff in email, no subject, message body
261 261
262 262 $ hg clone -r0 a b
263 263 adding changesets
264 264 adding manifests
265 265 adding file changes
266 266 added 1 changesets with 2 changes to 2 files
267 267 updating to branch default
268 268 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
269 269 $ grep -v '^Subject:' msg.patch | hg --cwd b import -
270 270 applying patch from stdin
271 271 $ rm -r b
272 272
273 273
274 274 plain diff in email, subject, no message body
275 275
276 276 $ hg clone -r0 a b
277 277 adding changesets
278 278 adding manifests
279 279 adding file changes
280 280 added 1 changesets with 2 changes to 2 files
281 281 updating to branch default
282 282 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
283 283 $ grep -v '^email ' msg.patch | hg --cwd b import -
284 284 applying patch from stdin
285 285 $ rm -r b
286 286
287 287
288 288 plain diff in email, no subject, no message body, should fail
289 289
290 290 $ hg clone -r0 a b
291 291 adding changesets
292 292 adding manifests
293 293 adding file changes
294 294 added 1 changesets with 2 changes to 2 files
295 295 updating to branch default
296 296 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
297 297 $ egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
298 298 applying patch from stdin
299 299 abort: empty commit message
300 300 [255]
301 301 $ rm -r b
302 302
303 303
304 304 hg export in email, should use patch header
305 305
306 306 $ hg clone -r0 a b
307 307 adding changesets
308 308 adding manifests
309 309 adding file changes
310 310 added 1 changesets with 2 changes to 2 files
311 311 updating to branch default
312 312 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
313 313 $ python mkmsg.py exported-tip.patch msg.patch
314 314 $ cat msg.patch | hg --cwd b import -
315 315 applying patch from stdin
316 316 $ hg --cwd b tip | grep second
317 317 summary: second change
318 318 $ rm -r b
319 319
320 320
321 321 subject: duplicate detection, removal of [PATCH]
322 322 The '---' tests the gitsendmail handling without proper mail headers
323 323
324 324 $ cat > mkmsg2.py <<EOF
325 325 > import email.Message, sys
326 326 > msg = email.Message.Message()
327 327 > patch = open(sys.argv[1], 'rb').read()
328 328 > msg.set_payload('email patch\n\nnext line\n---\n' + patch)
329 329 > msg['Subject'] = '[PATCH] email patch'
330 330 > msg['From'] = 'email patcher'
331 331 > file(sys.argv[2], 'wb').write(msg.as_string())
332 332 > EOF
333 333
334 334
335 335 plain diff in email, [PATCH] subject, message body with subject
336 336
337 337 $ hg clone -r0 a b
338 338 adding changesets
339 339 adding manifests
340 340 adding file changes
341 341 added 1 changesets with 2 changes to 2 files
342 342 updating to branch default
343 343 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 344 $ python mkmsg2.py diffed-tip.patch msg.patch
345 345 $ cat msg.patch | hg --cwd b import -
346 346 applying patch from stdin
347 347 $ hg --cwd b tip --template '{desc}\n'
348 348 email patch
349 349
350 350 next line
351 351 ---
352 352 $ rm -r b
353 353
354 354
355 355 Issue963: Parent of working dir incorrect after import of multiple
356 356 patches and rollback
357 357
358 358 We weren't backing up the correct dirstate file when importing many
359 359 patches: import patch1 patch2; rollback
360 360
361 361 $ echo line 3 >> a/a
362 362 $ hg --cwd a ci -m'third change'
363 363 $ hg --cwd a export -o '../patch%R' 1 2
364 364 $ hg clone -qr0 a b
365 365 $ hg --cwd b parents --template 'parent: {rev}\n'
366 366 parent: 0
367 367 $ hg --cwd b import -v ../patch1 ../patch2
368 368 applying ../patch1
369 369 patching file a
370 370 a
371 371 created 1d4bd90af0e4
372 372 applying ../patch2
373 373 patching file a
374 374 a
375 375 created 6d019af21222
376 376 $ hg --cwd b rollback
377 377 repository tip rolled back to revision 0 (undo import)
378 378 working directory now based on revision 0
379 379 $ hg --cwd b parents --template 'parent: {rev}\n'
380 380 parent: 0
381 381 $ rm -r b
382 382
383 383
384 384 importing a patch in a subdirectory failed at the commit stage
385 385
386 386 $ echo line 2 >> a/d1/d2/a
387 387 $ hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
388 388
389 389 hg import in a subdirectory
390 390
391 391 $ hg clone -r0 a b
392 392 adding changesets
393 393 adding manifests
394 394 adding file changes
395 395 added 1 changesets with 2 changes to 2 files
396 396 updating to branch default
397 397 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
398 398 $ hg --cwd a export tip > tmp
399 399 $ sed -e 's/d1\/d2\///' < tmp > subdir-tip.patch
400 400 $ dir=`pwd`
401 401 $ cd b/d1/d2 2>&1 > /dev/null
402 402 $ hg import ../../../subdir-tip.patch
403 403 applying ../../../subdir-tip.patch
404 404 $ cd "$dir"
405 405
406 406 message should be 'subdir change'
407 407 committer should be 'someoneelse'
408 408
409 409 $ hg --cwd b tip
410 410 changeset: 1:3577f5aea227
411 411 tag: tip
412 412 user: someoneelse
413 413 date: Thu Jan 01 00:00:01 1970 +0000
414 414 summary: subdir change
415 415
416 416
417 417 should be empty
418 418
419 419 $ hg --cwd b status
420 420
421 421
422 422 Test fuzziness (ambiguous patch location, fuzz=2)
423 423
424 424 $ hg init fuzzy
425 425 $ cd fuzzy
426 426 $ echo line1 > a
427 427 $ echo line0 >> a
428 428 $ echo line3 >> a
429 429 $ hg ci -Am adda
430 430 adding a
431 431 $ echo line1 > a
432 432 $ echo line2 >> a
433 433 $ echo line0 >> a
434 434 $ echo line3 >> a
435 435 $ hg ci -m change a
436 436 $ hg export tip > fuzzy-tip.patch
437 437 $ hg up -C 0
438 438 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
439 439 $ echo line1 > a
440 440 $ echo line0 >> a
441 441 $ echo line1 >> a
442 442 $ echo line0 >> a
443 443 $ hg ci -m brancha
444 444 created new head
445 445 $ hg import --no-commit -v fuzzy-tip.patch
446 446 applying fuzzy-tip.patch
447 447 patching file a
448 448 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
449 449 applied to working directory
450 450 $ hg revert -a
451 451 reverting a
452 452
453 453
454 454 import with --no-commit should have written .hg/last-message.txt
455 455
456 456 $ cat .hg/last-message.txt
457 457 change (no-eol)
458 458
459 459
460 460 test fuzziness with eol=auto
461 461
462 462 $ hg --config patch.eol=auto import --no-commit -v fuzzy-tip.patch
463 463 applying fuzzy-tip.patch
464 464 patching file a
465 465 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
466 466 applied to working directory
467 467 $ cd ..
468 468
469 469
470 470 Test hunk touching empty files (issue906)
471 471
472 472 $ hg init empty
473 473 $ cd empty
474 474 $ touch a
475 475 $ touch b1
476 476 $ touch c1
477 477 $ echo d > d
478 478 $ hg ci -Am init
479 479 adding a
480 480 adding b1
481 481 adding c1
482 482 adding d
483 483 $ echo a > a
484 484 $ echo b > b1
485 485 $ hg mv b1 b2
486 486 $ echo c > c1
487 487 $ hg copy c1 c2
488 488 $ rm d
489 489 $ touch d
490 490 $ hg diff --git
491 491 diff --git a/a b/a
492 492 --- a/a
493 493 +++ b/a
494 494 @@ -0,0 +1,1 @@
495 495 +a
496 496 diff --git a/b1 b/b2
497 497 rename from b1
498 498 rename to b2
499 499 --- a/b1
500 500 +++ b/b2
501 501 @@ -0,0 +1,1 @@
502 502 +b
503 503 diff --git a/c1 b/c1
504 504 --- a/c1
505 505 +++ b/c1
506 506 @@ -0,0 +1,1 @@
507 507 +c
508 508 diff --git a/c1 b/c2
509 509 copy from c1
510 510 copy to c2
511 511 --- a/c1
512 512 +++ b/c2
513 513 @@ -0,0 +1,1 @@
514 514 +c
515 515 diff --git a/d b/d
516 516 --- a/d
517 517 +++ b/d
518 518 @@ -1,1 +0,0 @@
519 519 -d
520 520 $ hg ci -m empty
521 521 $ hg export --git tip > empty.diff
522 522 $ hg up -C 0
523 523 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
524 524 $ hg import empty.diff
525 525 applying empty.diff
526 526 $ for name in a b1 b2 c1 c2 d; do
527 527 > echo % $name file
528 528 > test -f $name && cat $name
529 529 > done
530 530 % a file
531 531 a
532 532 % b1 file
533 533 % b2 file
534 534 b
535 535 % c1 file
536 536 c
537 537 % c2 file
538 538 c
539 539 % d file
540 540 $ cd ..
541 541
542 542
543 543 Test importing a patch ending with a binary file removal
544 544
545 545 $ hg init binaryremoval
546 546 $ cd binaryremoval
547 547 $ echo a > a
548 548 $ python -c "file('b', 'wb').write('a\x00b')"
549 549 $ hg ci -Am addall
550 550 adding a
551 551 adding b
552 552 $ hg rm a
553 553 $ hg rm b
554 554 $ hg st
555 555 R a
556 556 R b
557 557 $ hg ci -m remove
558 558 $ hg export --git . > remove.diff
559 559 $ cat remove.diff | grep git
560 560 diff --git a/a b/a
561 561 diff --git a/b b/b
562 562 $ hg up -C 0
563 563 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
564 564 $ hg import remove.diff
565 565 applying remove.diff
566 566 $ hg manifest
567 567 $ cd ..
568 568
569 569
570 570 Issue927: test update+rename with common name
571 571
572 572 $ hg init t
573 573 $ cd t
574 574 $ touch a
575 575 $ hg ci -Am t
576 576 adding a
577 577 $ echo a > a
578 578
579 579 Here, bfile.startswith(afile)
580 580
581 581 $ hg copy a a2
582 582 $ hg ci -m copya
583 583 $ hg export --git tip > copy.diff
584 584 $ hg up -C 0
585 585 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
586 586 $ hg import copy.diff
587 587 applying copy.diff
588 588
589 589 a should contain an 'a'
590 590
591 591 $ cat a
592 592 a
593 593
594 594 and a2 should have duplicated it
595 595
596 596 $ cat a2
597 597 a
598 598 $ cd ..
599 599
600 600
601 601 test -p0
602 602
603 603 $ hg init p0
604 604 $ cd p0
605 605 $ echo a > a
606 606 $ hg ci -Am t
607 607 adding a
608 608 $ hg import -p0 - << EOF
609 609 > foobar
610 610 > --- a Sat Apr 12 22:43:58 2008 -0400
611 611 > +++ a Sat Apr 12 22:44:05 2008 -0400
612 612 > @@ -1,1 +1,1 @@
613 613 > -a
614 614 > +bb
615 615 > EOF
616 616 applying patch from stdin
617 617 $ hg status
618 618 $ cat a
619 619 bb
620 620 $ cd ..
621 621
622 622
623 623 test paths outside repo root
624 624
625 625 $ mkdir outside
626 626 $ touch outside/foo
627 627 $ hg init inside
628 628 $ cd inside
629 629 $ hg import - <<EOF
630 630 > diff --git a/a b/b
631 631 > rename from ../outside/foo
632 632 > rename to bar
633 633 > EOF
634 634 applying patch from stdin
635 635 abort: path contains illegal component: ../outside/foo
636 636 [255]
637 637 $ cd ..
638 638
639 639
640 640 test import with similarity and git and strip (issue295 et al.)
641 641
642 642 $ hg init sim
643 643 $ cd sim
644 644 $ echo 'this is a test' > a
645 645 $ hg ci -Ama
646 646 adding a
647 647 $ cat > ../rename.diff <<EOF
648 648 > diff --git a/foo/a b/foo/a
649 649 > deleted file mode 100644
650 650 > --- a/foo/a
651 651 > +++ /dev/null
652 652 > @@ -1,1 +0,0 @@
653 653 > -this is a test
654 654 > diff --git a/foo/b b/foo/b
655 655 > new file mode 100644
656 656 > --- /dev/null
657 657 > +++ b/foo/b
658 658 > @@ -0,0 +1,2 @@
659 659 > +this is a test
660 660 > +foo
661 661 > EOF
662 662 $ hg import --no-commit -v -s 1 ../rename.diff -p2
663 663 applying ../rename.diff
664 664 patching file a
665 665 patching file b
666 666 adding b
667 667 recording removal of a as rename to b (88% similar)
668 668 applied to working directory
669 669 $ hg st -C
670 670 A b
671 671 a
672 672 R a
673 673 $ hg revert -a
674 674 undeleting a
675 675 forgetting b
676 676 $ rm b
677 677 $ hg import --no-commit -v -s 100 ../rename.diff -p2
678 678 applying ../rename.diff
679 679 patching file a
680 680 patching file b
681 681 adding b
682 682 applied to working directory
683 683 $ hg st -C
684 684 A b
685 685 R a
686 686 $ cd ..
687 687
688 688
689 689 Issue1495: add empty file from the end of patch
690 690
691 691 $ hg init addemptyend
692 692 $ cd addemptyend
693 693 $ touch a
694 694 $ hg addremove
695 695 adding a
696 696 $ hg ci -m "commit"
697 697 $ cat > a.patch <<EOF
698 698 > add a, b
699 699 > diff --git a/a b/a
700 700 > --- a/a
701 701 > +++ b/a
702 702 > @@ -0,0 +1,1 @@
703 703 > +a
704 704 > diff --git a/b b/b
705 705 > new file mode 100644
706 706 > EOF
707 707 $ hg import --no-commit a.patch
708 708 applying a.patch
709 709
710 710 apply a good patch followed by an empty patch (mainly to ensure
711 711 that dirstate is *not* updated when import crashes)
712 712 $ hg update -q -C .
713 713 $ rm b
714 714 $ touch empty.patch
715 715 $ hg import a.patch empty.patch
716 716 applying a.patch
717 717 applying empty.patch
718 718 transaction abort!
719 719 rollback completed
720 720 abort: empty.patch: no diffs found
721 721 [255]
722 722 $ hg tip --template '{rev} {desc|firstline}\n'
723 723 0 commit
724 724 $ hg -q status
725 725 M a
726 726 $ cd ..
727 727
728 728 create file when source is not /dev/null
729 729
730 730 $ cat > create.patch <<EOF
731 731 > diff -Naur proj-orig/foo proj-new/foo
732 732 > --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
733 733 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
734 734 > @@ -0,0 +1,1 @@
735 735 > +a
736 736 > EOF
737 737
738 738 some people have patches like the following too
739 739
740 740 $ cat > create2.patch <<EOF
741 741 > diff -Naur proj-orig/foo proj-new/foo
742 742 > --- proj-orig/foo.orig 1969-12-31 16:00:00.000000000 -0800
743 743 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
744 744 > @@ -0,0 +1,1 @@
745 745 > +a
746 746 > EOF
747 747 $ hg init oddcreate
748 748 $ cd oddcreate
749 749 $ hg import --no-commit ../create.patch
750 750 applying ../create.patch
751 751 $ cat foo
752 752 a
753 753 $ rm foo
754 754 $ hg revert foo
755 755 $ hg import --no-commit ../create2.patch
756 756 applying ../create2.patch
757 757 $ cat foo
758 758 a
759 759
760 760
761 761 Issue1859: first line mistaken for email headers
762 762
763 763 $ hg init emailconfusion
764 764 $ cd emailconfusion
765 765 $ cat > a.patch <<EOF
766 766 > module: summary
767 767 >
768 768 > description
769 769 >
770 770 >
771 771 > diff -r 000000000000 -r 9b4c1e343b55 test.txt
772 772 > --- /dev/null
773 773 > +++ b/a
774 774 > @@ -0,0 +1,1 @@
775 775 > +a
776 776 > EOF
777 777 $ hg import -d '0 0' a.patch
778 778 applying a.patch
779 779 $ hg parents -v
780 780 changeset: 0:5a681217c0ad
781 781 tag: tip
782 782 user: test
783 783 date: Thu Jan 01 00:00:00 1970 +0000
784 784 files: a
785 785 description:
786 786 module: summary
787 787
788 788 description
789 789
790 790
791 791 $ cd ..
792 792
793 793
794 794 --- in commit message
795 795
796 796 $ hg init commitconfusion
797 797 $ cd commitconfusion
798 798 $ cat > a.patch <<EOF
799 799 > module: summary
800 800 >
801 801 > --- description
802 802 >
803 803 > diff --git a/a b/a
804 804 > new file mode 100644
805 805 > --- /dev/null
806 806 > +++ b/a
807 807 > @@ -0,0 +1,1 @@
808 808 > +a
809 809 > EOF
810 810 > hg import -d '0 0' a.patch
811 811 > hg parents -v
812 812 > cd ..
813 813 >
814 814 > echo '% tricky header splitting'
815 815 > cat > trickyheaders.patch <<EOF
816 816 > From: User A <user@a>
817 817 > Subject: [PATCH] from: tricky!
818 818 >
819 819 > # HG changeset patch
820 820 > # User User B
821 821 > # Date 1266264441 18000
822 822 > # Branch stable
823 823 > # Node ID f2be6a1170ac83bf31cb4ae0bad00d7678115bc0
824 824 > # Parent 0000000000000000000000000000000000000000
825 825 > from: tricky!
826 826 >
827 827 > That is not a header.
828 828 >
829 829 > diff -r 000000000000 -r f2be6a1170ac foo
830 830 > --- /dev/null
831 831 > +++ b/foo
832 832 > @@ -0,0 +1,1 @@
833 833 > +foo
834 834 > EOF
835 835 applying a.patch
836 836 changeset: 0:f34d9187897d
837 837 tag: tip
838 838 user: test
839 839 date: Thu Jan 01 00:00:00 1970 +0000
840 840 files: a
841 841 description:
842 842 module: summary
843 843
844 844
845 845 % tricky header splitting
846 846
847 847 $ hg init trickyheaders
848 848 $ cd trickyheaders
849 849 $ hg import -d '0 0' ../trickyheaders.patch
850 850 applying ../trickyheaders.patch
851 851 $ hg export --git tip
852 852 # HG changeset patch
853 853 # User User B
854 854 # Date 0 0
855 855 # Node ID eb56ab91903632294ac504838508cb370c0901d2
856 856 # Parent 0000000000000000000000000000000000000000
857 857 from: tricky!
858 858
859 859 That is not a header.
860 860
861 861 diff --git a/foo b/foo
862 862 new file mode 100644
863 863 --- /dev/null
864 864 +++ b/foo
865 865 @@ -0,0 +1,1 @@
866 866 +foo
867 867 $ cd ..
868 868
869 869
870 870 Issue2102: hg export and hg import speak different languages
871 871
872 872 $ hg init issue2102
873 873 $ cd issue2102
874 874 $ mkdir -p src/cmd/gc
875 875 $ touch src/cmd/gc/mksys.bash
876 876 $ hg ci -Am init
877 877 adding src/cmd/gc/mksys.bash
878 878 $ hg import - <<EOF
879 879 > # HG changeset patch
880 880 > # User Rob Pike
881 881 > # Date 1216685449 25200
882 882 > # Node ID 03aa2b206f499ad6eb50e6e207b9e710d6409c98
883 883 > # Parent 93d10138ad8df586827ca90b4ddb5033e21a3a84
884 884 > help management of empty pkg and lib directories in perforce
885 885 >
886 886 > R=gri
887 887 > DELTA=4 (4 added, 0 deleted, 0 changed)
888 888 > OCL=13328
889 889 > CL=13328
890 890 >
891 891 > diff --git a/lib/place-holder b/lib/place-holder
892 892 > new file mode 100644
893 893 > --- /dev/null
894 894 > +++ b/lib/place-holder
895 895 > @@ -0,0 +1,2 @@
896 896 > +perforce does not maintain empty directories.
897 897 > +this file helps.
898 898 > diff --git a/pkg/place-holder b/pkg/place-holder
899 899 > new file mode 100644
900 900 > --- /dev/null
901 901 > +++ b/pkg/place-holder
902 902 > @@ -0,0 +1,2 @@
903 903 > +perforce does not maintain empty directories.
904 904 > +this file helps.
905 905 > diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
906 906 > old mode 100644
907 907 > new mode 100755
908 908 > EOF
909 909 applying patch from stdin
910 910 $ hg sum
911 911 parent: 1:d59915696727 tip
912 912 help management of empty pkg and lib directories in perforce
913 913 branch: default
914 914 commit: (clean)
915 915 update: (current)
916 916 $ hg diff --git -c tip
917 917 diff --git a/lib/place-holder b/lib/place-holder
918 918 new file mode 100644
919 919 --- /dev/null
920 920 +++ b/lib/place-holder
921 921 @@ -0,0 +1,2 @@
922 922 +perforce does not maintain empty directories.
923 923 +this file helps.
924 924 diff --git a/pkg/place-holder b/pkg/place-holder
925 925 new file mode 100644
926 926 --- /dev/null
927 927 +++ b/pkg/place-holder
928 928 @@ -0,0 +1,2 @@
929 929 +perforce does not maintain empty directories.
930 930 +this file helps.
931 931 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
932 932 old mode 100644
933 933 new mode 100755
934 934 $ cd ..
935 935
936 936
937 937 diff lines looking like headers
938 938
939 939 $ hg init difflineslikeheaders
940 940 $ cd difflineslikeheaders
941 941 $ echo a >a
942 942 $ echo b >b
943 943 $ echo c >c
944 944 $ hg ci -Am1
945 945 adding a
946 946 adding b
947 947 adding c
948 948
949 949 $ echo "key: value" >>a
950 950 $ echo "key: value" >>b
951 951 $ echo "foo" >>c
952 952 $ hg ci -m2
953 953
954 954 $ hg up -C 0
955 955 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
956 956 $ hg diff --git -c1 >want
957 957 $ hg diff -c1 | hg import --no-commit -
958 958 applying patch from stdin
959 959 $ hg diff --git >have
960 960 $ diff want have
961 961 $ cd ..
962 962
963 963 import a unified diff with no lines of context (diff -U0)
964 964
965 965 $ hg init diffzero
966 966 $ cd diffzero
967 967 $ cat > f << EOF
968 968 > c2
969 969 > c4
970 970 > c5
971 971 > EOF
972 972 $ hg commit -Am0
973 973 adding f
974 974
975 975 $ hg import --no-commit - << EOF
976 976 > # HG changeset patch
977 977 > # User test
978 978 > # Date 0 0
979 979 > # Node ID f4974ab632f3dee767567b0576c0ec9a4508575c
980 980 > # Parent 8679a12a975b819fae5f7ad3853a2886d143d794
981 981 > 1
982 982 > diff -r 8679a12a975b -r f4974ab632f3 f
983 983 > --- a/f Thu Jan 01 00:00:00 1970 +0000
984 984 > +++ b/f Thu Jan 01 00:00:00 1970 +0000
985 985 > @@ -0,0 +1,1 @@
986 986 > +c1
987 987 > @@ -1,0 +3,1 @@
988 988 > +c3
989 989 > @@ -3,1 +4,0 @@
990 990 > -c5
991 991 > EOF
992 992 applying patch from stdin
993 993
994 994 $ cat f
995 995 c1
996 996 c2
997 997 c3
998 998 c4
999 999
1000 no segfault while importing a unified diff which start line is zero but chunk
1001 size is non-zero
1002
1003 $ hg init startlinezero
1004 $ cd startlinezero
1005 $ echo foo > foo
1006 $ hg commit -Amfoo
1007 adding foo
1008
1009 $ hg import --no-commit - << EOF
1010 > diff a/foo b/foo
1011 > --- a/foo
1012 > +++ b/foo
1013 > @@ -0,1 +0,1 @@
1014 > foo
1015 > EOF
1016 applying patch from stdin
1017
1018 $ cd ..
1019
1000 1020 Test corner case involving fuzz and skew
1001 1021
1002 1022 $ hg init morecornercases
1003 1023 $ cd morecornercases
1004 1024
1005 1025 $ cat > 01-no-context-beginning-of-file.diff <<EOF
1006 1026 > diff --git a/a b/a
1007 1027 > --- a/a
1008 1028 > +++ b/a
1009 1029 > @@ -1,0 +1,1 @@
1010 1030 > +line
1011 1031 > EOF
1012 1032
1013 1033 $ cat > 02-no-context-middle-of-file.diff <<EOF
1014 1034 > diff --git a/a b/a
1015 1035 > --- a/a
1016 1036 > +++ b/a
1017 1037 > @@ -1,1 +1,1 @@
1018 1038 > -2
1019 1039 > +add some skew
1020 1040 > @@ -2,0 +2,1 @@
1021 1041 > +line
1022 1042 > EOF
1023 1043
1024 1044 $ cat > 03-no-context-end-of-file.diff <<EOF
1025 1045 > diff --git a/a b/a
1026 1046 > --- a/a
1027 1047 > +++ b/a
1028 1048 > @@ -10,0 +10,1 @@
1029 1049 > +line
1030 1050 > EOF
1031 1051
1032 1052 $ cat > 04-middle-of-file-completely-fuzzed.diff <<EOF
1033 1053 > diff --git a/a b/a
1034 1054 > --- a/a
1035 1055 > +++ b/a
1036 1056 > @@ -1,1 +1,1 @@
1037 1057 > -2
1038 1058 > +add some skew
1039 1059 > @@ -2,2 +2,3 @@
1040 1060 > not matching, should fuzz
1041 1061 > ... a bit
1042 1062 > +line
1043 1063 > EOF
1044 1064
1045 1065 $ cat > a <<EOF
1046 1066 > 1
1047 1067 > 2
1048 1068 > 3
1049 1069 > 4
1050 1070 > EOF
1051 1071 $ hg ci -Am adda a
1052 1072 $ for p in *.diff; do
1053 1073 > hg import -v --no-commit $p
1054 1074 > cat a
1055 1075 > hg revert -aqC a
1056 1076 > # patch -p1 < $p
1057 1077 > # cat a
1058 1078 > # hg revert -aC a
1059 1079 > done
1060 1080 applying 01-no-context-beginning-of-file.diff
1061 1081 patching file a
1062 1082 applied to working directory
1063 1083 1
1064 1084 line
1065 1085 2
1066 1086 3
1067 1087 4
1068 1088 applying 02-no-context-middle-of-file.diff
1069 1089 patching file a
1070 1090 Hunk #1 succeeded at 2 (offset 1 lines).
1071 1091 Hunk #2 succeeded at 4 (offset 1 lines).
1072 1092 applied to working directory
1073 1093 1
1074 1094 add some skew
1075 1095 3
1076 1096 line
1077 1097 4
1078 1098 applying 03-no-context-end-of-file.diff
1079 1099 patching file a
1080 1100 Hunk #1 succeeded at 5 (offset -6 lines).
1081 1101 applied to working directory
1082 1102 1
1083 1103 2
1084 1104 3
1085 1105 4
1086 1106 line
1087 1107 applying 04-middle-of-file-completely-fuzzed.diff
1088 1108 patching file a
1089 1109 Hunk #1 succeeded at 2 (offset 1 lines).
1090 1110 Hunk #2 succeeded at 5 with fuzz 2 (offset 1 lines).
1091 1111 applied to working directory
1092 1112 1
1093 1113 add some skew
1094 1114 3
1095 1115 4
1096 1116 line
1097 1117
General Comments 0
You need to be logged in to leave comments. Login now