Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/dev/patch.py
8815 views
1
r"""
2
Handling of deprecated hg patch files.
3
4
This file contains the methods of :class:`sagedev.SageDev` which are related to
5
mercurial patches.
6
7
AUTHORS:
8
9
- David Roe, Frej Drejhammar, Julian Rueth, Martin Raum, Nicolas M. Thiery, R.
10
Andrew Ohana, Robert Bradshaw, Timo Kluck: initial version
11
12
"""
13
#*****************************************************************************
14
# Copyright (C) 2013 David Roe <[email protected]>
15
# Frej Drejhammar <[email protected]>
16
# Julian Rueth <[email protected]>
17
# Martin Raum <[email protected]>
18
# Nicolas M. Thiery <[email protected]>
19
# R. Andrew Ohana <[email protected]>
20
# Robert Bradshaw <[email protected]>
21
# Timo Kluck <[email protected]>
22
# Volker Braun <[email protected]>
23
#
24
# Distributed under the terms of the GNU General Public License (GPL)
25
# as published by the Free Software Foundation; either version 2 of
26
# the License, or (at your option) any later version.
27
# http://www.gnu.org/licenses/
28
#*****************************************************************************
29
30
import os
31
import re
32
33
# regular expressions to parse mercurial patches
34
HG_HEADER_REGEX = re.compile(r"^# HG changeset patch$")
35
HG_USER_REGEX = re.compile(r"^# User (.*)$")
36
HG_DATE_REGEX = re.compile(r"^# Date (\d+) (-?\d+)$")
37
HG_NODE_REGEX = re.compile(r"^# Node ID ([0-9a-f]+)$")
38
HG_PARENT_REGEX = re.compile(r"^# Parent +([0-9a-f]+)$")
39
HG_DIFF_REGEX = re.compile(r"^diff (?:-r [0-9a-f]+ ){1,2}(.*)$")
40
PM_DIFF_REGEX = re.compile(r"^(?:(?:\+\+\+)|(?:---)) [ab]/([^ ]*)(?: .*)?$")
41
MV_DIFF_REGEX = re.compile(r"^rename (?:(?:to)|(?:from)) (.*)$")
42
CP_DIFF_REGEX = re.compile(r"^copy (?:(?:to)|(?:from)) (.*)$")
43
44
# regular expressions to parse git patches -- at least those created by us
45
GIT_FROM_REGEX = re.compile(r"^From: (.*)$")
46
GIT_SUBJECT_REGEX = re.compile(r"^Subject: (.*)$")
47
GIT_DATE_REGEX = re.compile(r"^Date: (.*)$")
48
GIT_DIFF_REGEX = re.compile(r"^diff --git a/(.*) b/(.*)$") # this regex should work for our patches
49
# since we do not have spaces in file names
50
51
# regular expressions to determine whether a path was written for the new git
52
# repository or for the old hg repositoryw
53
HG_PATH_REGEX = re.compile(r"^(?=sage/)|(?=doc/)|(?=module_list\.py)|(?=setup\.py)|(?=c_lib/)")
54
GIT_PATH_REGEX = re.compile(r"^(?=src/)")
55
56
from user_interface_error import OperationCancelledError
57
from git_error import GitError
58
59
60
class MercurialPatchMixin(object):
61
62
def import_patch(self, patchname=None, url=None, local_file=None,
63
diff_format=None, header_format=None, path_format=None):
64
r"""
65
Legacy support: Import a patch into the current branch.
66
67
If no arguments are given, then all patches from the ticket are
68
downloaded and applied using :meth:`download_patch`.
69
70
If ``local_file`` is specified, apply the file it points to.
71
72
Otherwise, download the patch using :meth:`download_patch` and apply
73
it.
74
75
INPUT:
76
77
- ``patchname`` -- a string or ``None`` (default: ``None``), passed on
78
to :meth:`download_patch`
79
80
- ``url`` -- a string or ``None`` (default: ``None``), passed on to
81
:meth:`download_patch`
82
83
- ``local_file`` -- a string or ``None`` (default: ``None``), if
84
specified, ``url`` and ``patchname`` must be ``None``; instead of
85
downloading the patch, apply this patch file.
86
87
- ``diff_format`` -- a string or ``None`` (default: ``None``), per
88
default the format of the patch file is autodetected; it can be
89
specified explicitly with this parameter
90
91
- ``header_format`` -- a string or ``None`` (default: ``None``), per
92
default the format of the patch header is autodetected; it can be
93
specified explicitly with this parameter
94
95
- ``path_format`` -- a string or ``None`` (default: ``None``), per
96
default the format of the paths is autodetected; it can be specified
97
explicitly with this parameter
98
99
.. NOTE::
100
101
This method calls :meth:`_rewrite_patch` if necessary to rewrite
102
patches which were created for sage before the move to git
103
happened. In other words, this is not just a simple wrapper for
104
``git am``.
105
106
.. SEEALSO::
107
108
- :meth:`download_patch` -- download a patch to a local file.
109
110
- :meth:`download` -- merges in changes from a git branch
111
rather than a patch.
112
113
TESTS:
114
115
Set up a single user for doctesting::
116
117
sage: from sage.dev.test.sagedev import single_user_setup
118
sage: dev, config, UI, server = single_user_setup()
119
120
Create a patch::
121
122
sage: open("tracked", "w").close()
123
sage: open("tracked2", "w").close()
124
sage: patchfile = os.path.join(dev._sagedev.tmp_dir,"tracked.patch")
125
sage: dev.git.silent.add("tracked", "tracked2")
126
sage: with open(patchfile, "w") as f: f.write(dev.git.diff(cached=True))
127
sage: dev.git.silent.reset()
128
129
Applying this patch fails since we are not in a sage repository::
130
131
sage: dev.import_patch(local_file=patchfile, path_format="new")
132
There are untracked files in your working directory:
133
tracked
134
tracked2
135
The patch cannot be imported unless these files are removed.
136
137
After moving away ``tracked`` and ``tracked2``, this works::
138
139
sage: os.unlink("tracked")
140
sage: os.unlink("tracked2")
141
sage: dev.import_patch(local_file=patchfile, path_format="new")
142
Applying: No Subject. Modified: tracked, tracked2
143
144
We create a patch which does not apply::
145
146
sage: with open("tracked", "w") as f: f.write("foo")
147
sage: dev.git.silent.add("tracked")
148
sage: with open("tracked", "w") as f: f.write("boo")
149
sage: with open("tracked2", "w") as f: f.write("boo")
150
sage: with open(patchfile, "w") as f: f.write(dev.git.diff())
151
sage: dev.git.clean_wrapper()
152
sage: open("tracked").read()
153
''
154
155
The import fails::
156
157
sage: UI.append("abort")
158
sage: UI.append("y")
159
sage: dev.import_patch(local_file=patchfile, path_format="new")
160
Applying: No Subject. Modified: tracked, tracked2
161
Patch failed at 0001 No Subject. Modified: tracked, tracked2
162
The copy of the patch that failed is found in:
163
.../rebase-apply/patch
164
<BLANKLINE>
165
The patch does not apply cleanly. Reject files will be created for the parts
166
that do not apply if you proceed.
167
Apply it anyway? [yes/No] y
168
The patch did not apply cleanly. Please integrate the `.rej` files that were
169
created and resolve conflicts. After you do, type `resolved`. If you want to
170
abort this process, type `abort`. [resolved/abort] abort
171
Removing tracked.rej
172
sage: open("tracked").read()
173
''
174
175
sage: UI.append("resolved")
176
sage: UI.append("y")
177
sage: dev.import_patch(local_file=patchfile, path_format="new")
178
Applying: No Subject. Modified: tracked, tracked2
179
Patch failed at 0001 No Subject. Modified: tracked, tracked2
180
The copy of the patch that failed is found in:
181
.../rebase-apply/patch
182
<BLANKLINE>
183
The patch does not apply cleanly. Reject files will be created for the parts
184
that do not apply if you proceed.
185
Apply it anyway? [yes/No] y
186
The patch did not apply cleanly. Please integrate the `.rej` files that were
187
created and resolve conflicts. After you do, type `resolved`. If you want to
188
abort this process, type `abort`. [resolved/abort] resolved
189
Removing tracked.rej
190
sage: open("tracked").read() # we did not actually incorporate the .rej files in this doctest, so nothing has changed
191
''
192
sage: open("tracked2").read()
193
'boo'
194
"""
195
try:
196
self.reset_to_clean_state()
197
self.clean()
198
except OperationCancelledError:
199
self._UI.error("Cannot import patch. Your working directory is not in a clean state.")
200
raise
201
202
untracked = self.git.untracked_files()
203
# do not exclude .patch files here: they would be deleted by clean() later
204
if untracked:
205
self._UI.error("There are untracked files in your working directory:\n{0}\nThe patch cannot be imported unless these files are removed.".format("\n".join(untracked)))
206
raise OperationCancelledError("untracked files make import impossible")
207
208
if not local_file:
209
local_files = self.download_patch(patchname=patchname, url=url)
210
try:
211
for local_file in local_files:
212
lines = open(local_file).read().splitlines()
213
if not self._UI.confirm("I will apply a patch which starts with the following lines:\n{0}\n\nIt modifies the following files:\n{1}\nIs this what you want?".format("\n".join(lines[:8]),", ".join(self._detect_patch_modified_files(lines))), default=True):
214
self._UI.show("Skipping this patch.")
215
else:
216
self.import_patch(
217
local_file=local_file,
218
diff_format=diff_format, header_format=header_format, path_format=path_format)
219
finally:
220
for local_file in local_files:
221
self._UI.debug("Deleting {0}.".format(local_file))
222
os.unlink(local_file)
223
elif patchname or url:
224
from sagedev import SageDevValueError
225
raise SageDevValueError("if local_file is specified, patchname and url must not be specified")
226
else:
227
lines = open(local_file).read().splitlines()
228
lines = self._rewrite_patch(lines, to_header_format="git",
229
to_path_format="new", from_diff_format=diff_format,
230
from_header_format=header_format,
231
from_path_format=path_format)
232
233
from sage.dev.misc import tmp_filename
234
outfile = tmp_filename()
235
with open(outfile, 'w') as f:
236
f.write("\n".join(lines)+"\n")
237
238
self._UI.debug("Trying to apply reformatted patch `%s`"%outfile)
239
# am, apply and add need to be in the root directory
240
curdir = os.getcwd()
241
os.chdir(self.git._src)
242
try:
243
try:
244
self.git.echo.am(outfile, "--resolvemsg= ", ignore_whitespace=True)
245
except GitError as err:
246
self._UI.error([err.stdout, ''])
247
self._UI.warning("The patch does not apply cleanly. Reject files will be"
248
" created for the parts that do not apply if you proceed.")
249
if not self._UI.confirm("Apply it anyway?", default=False):
250
self._UI.debug("Not applying patch.")
251
self.git.reset_to_clean_state()
252
self.git.clean_wrapper(remove_untracked_files=True)
253
raise OperationCancelledError("User requested to cancel the apply.")
254
255
try:
256
try:
257
self.git.silent.apply(outfile, ignore_whitespace=True, reject=True)
258
except GitError:
259
if self._UI.select("The patch did not apply cleanly. Please integrate the `.rej` files that were created and resolve conflicts. After you do, type `resolved`. If you want to abort this process, type `abort`.", ("resolved","abort")) == "abort":
260
raise OperationCancelledError("User requested to cancel the apply.")
261
else:
262
self._UI.show("It seemed that the patch would not apply, but in fact it did.")
263
264
self.git.super_silent.add(update=True)
265
untracked = [fname for fname in self.git.untracked_files() if not fname.endswith(".rej")]
266
if untracked and self._UI.confirm("The patch will introduce the following new files to the repository:\n{0}\nIs this correct?".format("\n".join(untracked)), default=True):
267
self.git.super_silent.add(*untracked)
268
self.git.am('--resolvemsg= ', resolved=True)
269
self._UI.debug("A commit on the current branch has been created from the patch.")
270
finally:
271
self.git.reset_to_clean_state()
272
self.git.clean_wrapper(remove_untracked_files=True, remove_untracked_directories=True)
273
finally:
274
os.chdir(curdir)
275
276
def download_patch(self, ticket=None, patchname=None, url=None):
277
r"""
278
Legacy support: Download a patch to a temporary directory.
279
280
If only ``ticket`` is specified, then try to make sense of the
281
``apply`` statements in the comments on the ticket to download the
282
tickets in the right order just like the patchbot would do.
283
284
If no ``ticket`` is specified, use the current ticket.
285
286
If ``ticket`` and ``patchname`` are specified, download the
287
patch ``patchname`` attached to ``ticket``.
288
289
If ``url`` is specified, download ``url``.
290
291
Raise an error on any other combination of parameters.
292
293
INPUT:
294
295
- ``ticket`` -- an integer or string identifying a ticket or ``None``
296
(default: ``None``)
297
298
- ``patchname`` -- a string or ``None`` (default: ``None``)
299
300
- ``url`` -- a string or ``None`` (default: ``None``)
301
302
OUTPUT:
303
304
Otherwise, returns a tuple of the names of the local patch files that
305
have been downloaded.
306
307
.. SEEALSO::
308
309
- :meth:`import_patch` -- also creates a commit on the current
310
branch from the patch.
311
312
EXAMPLES::
313
314
sage: from sage.env import SAGE_ROOT
315
sage: os.chdir(SAGE_ROOT) # silence possible warnings about not being in SAGE_ROOT
316
sage: dev.download_patch(ticket=14882, # optional: internet
317
....: patchname='trac_14882-backtrack_longtime-dg.patch')
318
Downloading "https://trac.sagemath.org/raw-attachment/ticket/14882/trac_14882
319
-backtrack_longtime-dg.patch"...
320
Downloaded "https://trac.sagemath.org/raw-attachment/ticket/14882/trac_14882
321
-backtrack_longtime-dg.patch" to "/...patch".
322
('/...patch',)
323
324
TESTS:
325
326
Set up a single user for doctesting::
327
328
sage: from sage.dev.test.sagedev import single_user_setup
329
sage: dev, config, UI, server = single_user_setup()
330
331
Create a new ticket::
332
333
sage: UI.append("Summary: summary1\ndescription")
334
sage: dev.create_ticket()
335
Created ticket #1 at https://trac.sagemath.org/1.
336
<BLANKLINE>
337
# (use "sage --dev checkout --ticket=1" to create a new local branch)
338
1
339
340
There are no attachments to download yet::
341
342
sage: dev.download_patch(ticket=1) # optional - internet
343
Ticket #1 has no attachments.
344
345
After adding one attachment, this works::
346
347
sage: server.tickets[1].attachments['first.patch'] = ''
348
sage: dev.download_patch(ticket=1) # not tested, just an example
349
350
After adding another attachment, this does not work anymore, one needs
351
to specify which attachment should be downloaded::
352
353
sage: server.tickets[1].attachments['second.patch'] = ''
354
sage: dev.download_patch(ticket=1) # optional - internet
355
I could not understand the comments on ticket #1. To apply use one of the
356
patches on the ticket, set the parameter `patchname` to one of: first.patch,
357
second.patch
358
sage: dev.download_patch(ticket=1, # not tested, just an example
359
....: patchname='second.patch')
360
361
It is an error not to specify any parameters if not on a ticket::
362
363
sage: dev.vanilla()
364
sage: dev.download_patch()
365
ticket or url must be specified if not currently on a ticket
366
367
Check that the parser for the rss stream works::
368
369
sage: UI.append("n")
370
sage: dev._sagedev.trac = sage.all.dev.trac
371
sage: dev.download_patch(ticket=12415) # optional: internet
372
It seems that the following patches have to be applied in this order:
373
12415_spkg_bin_sage.patch
374
12415_script.patch
375
12415_framework.patch
376
12415_doctest_review.patch
377
12415_script_review.patch
378
12415_review_review.patch
379
12415_doc.patch
380
12415_test.patch
381
12415_review.patch
382
12415_review3.patch
383
12415_doctest_fixes.patch
384
12415_manifest.patch
385
12415_rebase_58.patch
386
Should I download these patches? [Yes/no] n
387
Ticket #12415 has more than one attachment but you chose not to download
388
them in the proposed order. To use only one of these patches set the
389
parameter `patchname` to one of: 12415_doc.patch, 12415_doctest_fixes.patch,
390
12415_doctest_review.patch, 12415_framework.patch, 12415_manifest.patch,
391
12415_rebase_58.patch, 12415_review.patch, 12415_review3.patch,
392
12415_review_review.patch, 12415_script.patch, 12415_script_review.patch,
393
12415_spkg_bin_sage.patch, 12415_test.patch
394
"""
395
if url is not None:
396
if ticket or patchname:
397
raise ValueError('If "url" is specifed then neither "ticket" nor "patchname"'
398
' may be specified.')
399
import urllib
400
self._UI.show('Downloading "{0}"...', url)
401
ret = urllib.urlretrieve(url)[0]
402
self._UI.show('Downloaded "{0}" to "{1}".', url, ret)
403
return (ret,)
404
405
if ticket is None:
406
ticket = self._current_ticket()
407
408
if ticket is None:
409
from sagedev import SageDevValueError
410
raise SageDevValueError("ticket or url must be specified if not currently on a ticket")
411
412
ticket = self._ticket_from_ticket_name(ticket)
413
414
if patchname:
415
from sage.env import TRAC_SERVER_URI
416
url = TRAC_SERVER_URI+"/raw-attachment/ticket/%s/%s"%(ticket,patchname)
417
if url.startswith("https://"):
418
try:
419
import ssl
420
except ImportError:
421
# python is not build with ssl support by default. to make
422
# downloading patches work even if ssl is not present, we try
423
# to access trac through http
424
url = url.replace("https","http",1)
425
return self.download_patch(url = url)
426
else:
427
attachments = self.trac.attachment_names(ticket)
428
if len(attachments) == 0:
429
from sagedev import SageDevValueError
430
raise SageDevValueError("Ticket #%s has no attachments."%ticket)
431
if len(attachments) == 1:
432
ret = self.download_patch(ticket = ticket, patchname = attachments[0])
433
self._UI.show('Attachment "{0}" for ticket #{1} has been downloaded to "{2}".'
434
.format(attachments[0], ticket, ret[0]))
435
return ret
436
else:
437
from sage.env import TRAC_SERVER_URI
438
rss = TRAC_SERVER_URI+"/ticket/%s?format=rss"%ticket
439
self._UI.debug('There is more than one attachment on ticket #{0}. '
440
'Reading "{1}" to try to find out in which order they must be applied.',
441
ticket, rss)
442
import urllib2
443
rss = urllib2.urlopen(rss).read()
444
445
# the following block has been copied from the patchbot
446
all_patches = []
447
patches = []
448
import re
449
folded_regex = re.compile('all.*(folded|combined|merged)')
450
subsequent_regex = re.compile('second|third|fourth|next|on top|after')
451
attachment_regex = re.compile(r"<strong>attachment</strong>\s*set to <em>(.*)</em>", re.M)
452
rebased_regex = re.compile(r"([-.]?rebased?)|(-v\d)")
453
def extract_tag(sgml, tag):
454
"""
455
Find the first occurance of the tag start (including
456
attributes) and return the contents of that tag (really, up
457
until the next end tag of that type).
458
459
Crude but fast.
460
"""
461
tag_name = tag[1:-1]
462
if ' ' in tag_name:
463
tag_name = tag_name[:tag_name.index(' ')]
464
end = "</%s>" % tag_name
465
start_ix = sgml.find(tag)
466
if start_ix == -1:
467
return None
468
end_ix = sgml.find(end, start_ix)
469
if end_ix == -1:
470
return None
471
return sgml[start_ix + len(tag) : end_ix].strip()
472
for item in rss.split('<item>'):
473
description = extract_tag(item, '<description>').replace('&lt;', '<').replace('&gt;', '>')
474
m = attachment_regex.search(description)
475
comments = description[description.find('</ul>') + 1:]
476
# Look for apply... followed by patch names
477
for line in comments.split('\n'):
478
if 'apply' in line.lower():
479
new_patches = []
480
for p in line[line.lower().index('apply') + 5:].split(','):
481
for pp in p.strip().split():
482
if pp in all_patches:
483
new_patches.append(pp)
484
if new_patches or (m and not subsequent_regex.search(line)):
485
patches = new_patches
486
elif m and folded_regex.search(line):
487
patches = [] # will add this patch below
488
if m is not None:
489
attachment = m.group(1)
490
import os.path
491
base, ext = os.path.splitext(attachment)
492
if '.' in base:
493
try:
494
base2, ext2 = os.path.splitext(base)
495
count = int(ext2[1:])
496
for i in range(count):
497
if i:
498
older = "%s.%s%s" % (base2, i, ext)
499
else:
500
older = "%s%s" % (base2, ext)
501
if older in patches:
502
patches.remove(older)
503
except:
504
pass
505
if rebased_regex.search(attachment):
506
older = rebased_regex.sub('', attachment)
507
if older in patches:
508
patches.remove(older)
509
if ext in ('.patch', '.diff'):
510
all_patches.append(attachment)
511
patches.append(attachment)
512
513
# now patches contains the list of patches to apply
514
if patches:
515
if self._UI.confirm("It seems that the following patches have to be applied in this order: \n{0}\nShould I download these patches?".format("\n".join(patches)),default=True):
516
ret = []
517
for patch in patches:
518
ret.extend(self.download_patch(ticket=ticket, patchname=patch))
519
return ret
520
else:
521
self._UI.error("Ticket #{0} has more than one attachment but you chose not to download them in the proposed order. To use only one of these patches set the parameter `patchname` to one of: {1}".format(ticket, ", ".join(sorted(attachments))))
522
raise OperationCancelledError("user requested")
523
else:
524
self._UI.error("I could not understand the comments on ticket #{0}. To apply use one of the patches on the ticket, set the parameter `patchname` to one of: {1}".format(ticket, ", ".join(sorted(attachments))))
525
raise OperationCancelledError("user requested")
526
527
def _detect_patch_diff_format(self, lines):
528
r"""
529
Determine the format of the ``diff`` lines in ``lines``.
530
531
INPUT:
532
533
- ``lines`` -- a list of strings
534
535
OUTPUT:
536
537
Either ``git`` (for ``diff --git`` lines) or ``hg`` (for ``diff -r`` lines).
538
539
.. NOTE::
540
541
Most Sage developpers have configured mercurial to export
542
patches in git format.
543
544
TESTS::
545
546
sage: dev = dev._sagedev
547
sage: dev._detect_patch_diff_format(
548
....: ["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"])
549
'hg'
550
sage: dev._detect_patch_diff_format(
551
....: ["diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"])
552
'git'
553
554
sage: from sage.env import SAGE_SRC
555
sage: dev._detect_patch_diff_format(
556
....: open(os.path.join(
557
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
558
....: )).read().splitlines())
559
'git'
560
sage: dev._detect_patch_diff_format(
561
....: open(os.path.join(
562
....: SAGE_SRC,"sage","dev","test","data","diff.patch"
563
....: )).read().splitlines())
564
'hg'
565
566
sage: dev._detect_patch_diff_format(["# HG changeset patch"])
567
Traceback (most recent call last):
568
...
569
NotImplementedError: Failed to detect diff format.
570
sage: dev._detect_patch_diff_format(
571
... ["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py",
572
... "diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"])
573
Traceback (most recent call last):
574
...
575
SageDevValueError: File appears to have mixed diff formats.
576
"""
577
format = None
578
regexs = { "hg" : HG_DIFF_REGEX, "git" : GIT_DIFF_REGEX }
579
580
for line in lines:
581
for name,regex in regexs.items():
582
if regex.match(line):
583
if format is None:
584
format = name
585
if format != name:
586
from sagedev import SageDevValueError
587
raise SageDevValueError("File appears to have mixed diff formats.")
588
589
if format is None:
590
raise NotImplementedError("Failed to detect diff format.")
591
else:
592
return format
593
594
def _detect_patch_path_format(self, lines, diff_format=None):
595
r"""
596
Determine the format of the paths in the patch given in ``lines``.
597
598
INPUT:
599
600
- ``lines`` -- a list (or iterable) of strings
601
602
- ``diff_format`` -- ``'hg'``,``'git'``, or ``None`` (default:
603
``None``), the format of the ``diff`` lines in the patch. If
604
``None``, the format will be determined by
605
:meth:`_detect_patch_diff_format`.
606
607
OUTPUT:
608
609
A string, ``'new'`` (new repository layout) or ``'old'`` (old
610
repository layout).
611
612
EXAMPLES::
613
614
sage: dev._wrap("_detect_patch_path_format")
615
sage: dev._detect_patch_path_format(
616
....: ["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"])
617
'old'
618
sage: dev._detect_patch_path_format(
619
....: ["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"],
620
....: diff_format="git")
621
Traceback (most recent call last):
622
...
623
NotImplementedError: Failed to detect path format.
624
sage: dev._detect_patch_path_format(
625
....: ["diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"])
626
'old'
627
sage: dev._detect_patch_path_format(
628
....: ["diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi"])
629
'new'
630
sage: dev._detect_patch_path_format(
631
....: ["rename to sage/rings/number_field/totallyreal.pyx"], diff_format='hg')
632
'old'
633
sage: dev._detect_patch_path_format(
634
....: ["rename from src/sage/rings/number_field/totalyreal.pyx"], diff_format='git')
635
'new'
636
637
sage: from sage.env import SAGE_SRC
638
sage: dev._detect_patch_path_format(
639
....: open(os.path.join(
640
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
641
....: )).read().splitlines())
642
'old'
643
"""
644
lines = list(lines)
645
if diff_format is None:
646
diff_format = self._detect_patch_diff_format(lines)
647
648
path_format = None
649
650
if diff_format == "git":
651
diff_regexs = (GIT_DIFF_REGEX, PM_DIFF_REGEX, MV_DIFF_REGEX, CP_DIFF_REGEX)
652
elif diff_format == "hg":
653
diff_regexs = (HG_DIFF_REGEX, PM_DIFF_REGEX, MV_DIFF_REGEX, CP_DIFF_REGEX)
654
else:
655
raise NotImplementedError(diff_format)
656
657
regexs = { "old" : HG_PATH_REGEX, "new" : GIT_PATH_REGEX }
658
659
for line in lines:
660
for regex in diff_regexs:
661
match = regex.match(line)
662
if match:
663
for group in match.groups():
664
for name, regex in regexs.items():
665
if regex.match(group):
666
if path_format is None:
667
path_format = name
668
if path_format != name:
669
from sagedev import SageDevValueError
670
raise SageDevValueError("File appears to have mixed path formats.")
671
672
if path_format is None:
673
raise NotImplementedError("Failed to detect path format.")
674
else:
675
return path_format
676
677
def _rewrite_patch_diff_paths(self, lines, to_format, from_format=None, diff_format=None):
678
r"""
679
Rewrite the ``diff`` lines in ``lines`` to use ``to_format``.
680
681
INPUT:
682
683
- ``lines`` -- a list or iterable of strings
684
685
- ``to_format`` -- ``'old'`` or ``'new'``
686
687
- ``from_format`` -- ``'old'``, ``'new'``, or ``None`` (default:
688
``None``), the current formatting of the paths; detected
689
automatically if ``None``
690
691
- ``diff_format`` -- ``'git'``, ``'hg'``, or ``None`` (default:
692
``None``), the format of the ``diff`` lines; detected automatically
693
if ``None``
694
695
OUTPUT:
696
697
A list of string, ``lines`` rewritten to conform to ``lines``.
698
699
EXAMPLES:
700
701
Paths in the old format::
702
703
sage: dev._wrap("_rewrite_patch_diff_paths")
704
sage: dev._rewrite_patch_diff_paths(
705
....: ['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'],
706
....: to_format="old")
707
['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py']
708
sage: dev._rewrite_patch_diff_paths(
709
....: ['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'],
710
....: to_format="old")
711
['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi']
712
sage: dev._rewrite_patch_diff_paths(
713
....: ['--- a/sage/rings/padics/pow_computer_ext.pxd',
714
....: '+++ b/sage/rings/padics/pow_computer_ext.pxd'],
715
....: to_format="old", diff_format="git")
716
['--- a/sage/rings/padics/pow_computer_ext.pxd',
717
'+++ b/sage/rings/padics/pow_computer_ext.pxd']
718
sage: dev._rewrite_patch_diff_paths(
719
....: ['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'],
720
....: to_format="new")
721
['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py']
722
sage: dev._rewrite_patch_diff_paths(
723
....: ['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'],
724
....: to_format="new")
725
['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi']
726
sage: dev._rewrite_patch_diff_paths(
727
....: ['--- a/sage/rings/padics/pow_computer_ext.pxd',
728
....: '+++ b/sage/rings/padics/pow_computer_ext.pxd'],
729
....: to_format="new", diff_format="git")
730
['--- a/src/sage/rings/padics/pow_computer_ext.pxd',
731
'+++ b/src/sage/rings/padics/pow_computer_ext.pxd']
732
733
Paths in the new format::
734
735
sage: dev._rewrite_patch_diff_paths(
736
....: ['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'],
737
....: to_format="old")
738
['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py']
739
sage: dev._rewrite_patch_diff_paths(
740
....: ['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'],
741
....: to_format="old")
742
['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi']
743
sage: dev._rewrite_patch_diff_paths(
744
....: ['--- a/src/sage/rings/padics/pow_computer_ext.pxd',
745
....: '+++ b/src/sage/rings/padics/pow_computer_ext.pxd'],
746
....: to_format="old", diff_format="git")
747
['--- a/sage/rings/padics/pow_computer_ext.pxd',
748
'+++ b/sage/rings/padics/pow_computer_ext.pxd']
749
sage: dev._rewrite_patch_diff_paths(
750
....: ['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'],
751
....: to_format="new")
752
['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py']
753
sage: dev._rewrite_patch_diff_paths(
754
....: ['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'],
755
....: to_format="new")
756
['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi']
757
sage: dev._rewrite_patch_diff_paths(
758
....: ['--- a/src/sage/rings/padics/pow_computer_ext.pxd',
759
....: '+++ b/src/sage/rings/padics/pow_computer_ext.pxd'],
760
....: to_format="new", diff_format="git")
761
['--- a/src/sage/rings/padics/pow_computer_ext.pxd',
762
'+++ b/src/sage/rings/padics/pow_computer_ext.pxd']
763
764
sage: dev._rewrite_patch_diff_paths(
765
....: ['rename from sage/combinat/crystals/letters.py',
766
....: 'rename to sage/combinat/crystals/letters.pyx'],
767
....: to_format="new", diff_format="hg")
768
['rename from src/sage/combinat/crystals/letters.py',
769
'rename to src/sage/combinat/crystals/letters.pyx']
770
sage: dev._rewrite_patch_diff_paths(
771
....: ['rename from src/sage/combinat/crystals/letters.py',
772
....: 'rename to src/sage/combinat/crystals/letters.pyx'],
773
....: to_format="old", diff_format="git")
774
['rename from sage/combinat/crystals/letters.py',
775
'rename to sage/combinat/crystals/letters.pyx']
776
777
sage: from sage.env import SAGE_SRC
778
sage: result = dev._rewrite_patch_diff_paths(
779
....: open(os.path.join(
780
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
781
....: )).read().splitlines(),
782
....: to_format="new", diff_format="git")
783
sage: len(result)
784
2980
785
sage: result[0]
786
'#8703: Enumerated sets and data structure for ordered and binary trees'
787
sage: result[12]
788
'diff --git a/src/doc/en/reference/combinat/index.rst b/src/doc/en/reference/combinat/index.rst'
789
"""
790
lines = list(lines)
791
if diff_format is None:
792
diff_format = self._detect_patch_diff_format(lines)
793
794
if from_format is None:
795
from_format = self._detect_patch_path_format(lines, diff_format=diff_format)
796
797
if to_format == from_format:
798
return lines
799
800
def hg_path_to_git_path(path):
801
if any([path.startswith(p) for p in
802
"module_list.py", "setup.py", "c_lib/", "sage/", "doc/"]):
803
return "src/%s"%path
804
else:
805
raise NotImplementedError('mapping hg path "%s"'%path)
806
807
def git_path_to_hg_path(path):
808
if any([path.startswith(p) for p in
809
"src/module_list.py", "src/setup.py", "src/c_lib/", "src/sage/", "src/doc/"]):
810
return path[4:]
811
else:
812
raise NotImplementedError('mapping git path "%s"'%path)
813
814
def apply_replacements(lines, diff_regexs, replacement):
815
ret = []
816
for line in lines:
817
for diff_regex in diff_regexs:
818
m = diff_regex.match(line)
819
if m:
820
line = line[:m.start(1)] + \
821
("".join([ line[m.end(i-1):m.start(i)]+replacement(m.group(i))
822
for i in range(1,m.lastindex+1) ])) + \
823
line[m.end(m.lastindex):]
824
ret.append(line)
825
return ret
826
827
diff_regex = None
828
if diff_format == "hg":
829
diff_regex = (HG_DIFF_REGEX, PM_DIFF_REGEX, MV_DIFF_REGEX, CP_DIFF_REGEX)
830
elif diff_format == "git":
831
diff_regex = (GIT_DIFF_REGEX, PM_DIFF_REGEX, MV_DIFF_REGEX, CP_DIFF_REGEX)
832
else:
833
raise NotImplementedError(diff_format)
834
835
if from_format == "old":
836
return self._rewrite_patch_diff_paths(
837
apply_replacements(lines, diff_regex, hg_path_to_git_path),
838
from_format="new", to_format=to_format, diff_format=diff_format)
839
elif from_format == "new":
840
if to_format == "old":
841
return apply_replacements(lines, diff_regex, git_path_to_hg_path)
842
else:
843
raise NotImplementedError(to_format)
844
else:
845
raise NotImplementedError(from_format)
846
847
def _detect_patch_header_format(self, lines):
848
r"""
849
Detect the format of the patch header in ``lines``.
850
851
INPUT:
852
853
- ``lines`` -- a list (or iterable) of strings
854
855
OUTPUT:
856
857
A string, ``'hg-export'`` (mercurial export header), ``'hg'``
858
(mercurial header), ``'git'`` (git mailbox header), ``'diff'`` (no
859
header)
860
861
EXAMPLES::
862
863
sage: dev._wrap("_detect_patch_header_format")
864
sage: dev._detect_patch_header_format(
865
... ['# HG changeset patch','# Parent 05fca316b08fe56c8eec85151d9a6dde6f435d46'])
866
'hg'
867
sage: dev._detect_patch_header_format(
868
... ['# HG changeset patch','# User [email protected]'])
869
'hg-export'
870
sage: dev._detect_patch_header_format(
871
... ['From: foo@bar'])
872
'git'
873
874
sage: from sage.env import SAGE_SRC
875
sage: dev._detect_patch_header_format(
876
....: open(os.path.join(
877
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
878
....: )).read().splitlines())
879
'diff'
880
sage: dev._detect_patch_header_format(
881
....: open(os.path.join(
882
....: SAGE_SRC,"sage","dev","test","data","diff.patch"
883
....: )).read().splitlines())
884
'diff'
885
"""
886
lines = list(lines)
887
if not lines:
888
from sagedev import SageDevValueError
889
raise SageDevValueError("patch is empty")
890
891
if HG_HEADER_REGEX.match(lines[0]):
892
if HG_USER_REGEX.match(lines[1]):
893
return "hg-export"
894
elif HG_PARENT_REGEX.match(lines[1]):
895
return "hg"
896
elif GIT_FROM_REGEX.match(lines[0]):
897
return "git"
898
return "diff"
899
900
def _detect_patch_modified_files(self, lines, diff_format = None):
901
r"""
902
Return a list of files which are modified by the patch in ``lines``.
903
904
TESTS::
905
906
sage: dev._wrap("_detect_patch_modified_files")
907
sage: from sage.env import SAGE_SRC
908
sage: dev._detect_patch_modified_files(
909
....: open(os.path.join(
910
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
911
....: )).read().splitlines())
912
['ordered_tree.py', 'binary_tree.pyx', 'list_clone.pyx', 'permutation.py',
913
'index.rst', 'abstract_tree.py', 'all.py', 'binary_tree.py']
914
"""
915
if diff_format is None:
916
diff_format = self._detect_patch_diff_format(lines)
917
918
if diff_format == "hg":
919
regex = HG_DIFF_REGEX
920
elif diff_format == "git":
921
regex = GIT_DIFF_REGEX
922
else:
923
raise NotImplementedError(diff_format)
924
925
ret = set()
926
for line in lines:
927
m = regex.match(line)
928
if m:
929
for group in m.groups():
930
split = group.split('/')
931
if split:
932
ret.add(split[-1])
933
return list(ret)
934
935
def _rewrite_patch_header(self, lines, to_format, from_format = None, diff_format = None):
936
r"""
937
Rewrite ``lines`` to match ``to_format``.
938
939
INPUT:
940
941
- ``lines`` -- a list of strings, the lines of the patch file
942
943
- ``to_format`` -- one of ``'hg'``, ``'hg-export'``, ``'diff'``,
944
``'git'``, the format of the resulting patch file.
945
946
- ``from_format`` -- one of ``None``, ``'hg'``, ``'hg-export'``, ``'diff'``, ``'git'``
947
(default: ``None``), the format of the patch file. The format is
948
determined automatically if ``format`` is ``None``.
949
950
OUTPUT:
951
952
A list of lines, in the format specified by ``to_format``.
953
954
Some sample patch files are in data/, in hg and git
955
format. Since the translation is not perfect, the resulting
956
file is also put there for comparison.
957
958
EXAMPLES::
959
960
sage: from sage.env import SAGE_SRC
961
sage: hg_lines = open(
962
....: os.path.join(SAGE_SRC, "sage", "dev", "test", "data", "hg.patch")
963
....: ).read().splitlines()
964
sage: hg_output_lines = open(
965
....: os.path.join(SAGE_SRC, "sage", "dev", "test", "data", "hg-output.patch")
966
....: ).read().splitlines()
967
sage: git_lines = open(
968
....: os.path.join(SAGE_SRC, "sage", "dev", "test", "data", "git.patch")
969
....: ).read().splitlines()
970
sage: git_output_lines = open(
971
....: os.path.join(SAGE_SRC, "sage", "dev", "test", "data", "git-output.patch")
972
....: ).read().splitlines()
973
974
sage: dev._wrap("_rewrite_patch_header")
975
sage: dev._rewrite_patch_header(git_lines, 'git') == git_lines
976
True
977
sage: dev._rewrite_patch_header(hg_lines, 'hg-export') == hg_lines
978
True
979
980
sage: dev._rewrite_patch_header(git_lines, 'hg-export') == hg_output_lines
981
True
982
sage: dev._rewrite_patch_header(hg_lines, 'git') == git_output_lines
983
True
984
985
sage: dev._rewrite_patch_header(
986
....: open(os.path.join(
987
....: SAGE_SRC,"sage","dev","test","data","trac_8703-trees-fh.patch"
988
....: )).read().splitlines(), 'git')[:5]
989
['From: "Unknown User" <[email protected]>',
990
'Subject: #8703: Enumerated sets and data structure for ordered and binary trees',
991
'Date: ...',
992
'',
993
'- The Class Abstract[Labelled]Tree allows for inheritance from different']
994
"""
995
import email.utils, time
996
997
lines = list(lines)
998
if not lines:
999
from sagedev import SageDevValueError
1000
raise SageDevValueError("empty patch file")
1001
1002
if from_format is None:
1003
from_format = self._detect_patch_header_format(lines)
1004
1005
if from_format == to_format:
1006
return lines
1007
1008
def parse_header(lines, regexs, mandatory=False):
1009
header = {}
1010
i = 0
1011
for (key, regex) in regexs:
1012
if i > len(lines):
1013
if mandatory:
1014
from sagedev import SageDevValueError
1015
raise SageDevValueError('Malformed patch. Missing line for regular expression "%s".'
1016
%(regex.pattern))
1017
else:
1018
return
1019
match = regex.match(lines[i])
1020
if match is not None:
1021
if len(match.groups()) > 0:
1022
header[key] = match.groups()[0]
1023
i += 1
1024
elif mandatory:
1025
from sagedev import SageDevValueError
1026
raise SageDevValueError('Malformed patch. Line "%s" does not match regular expression "%s".'
1027
%(lines[i],regex.pattern))
1028
1029
message = []
1030
for i in range(i,len(lines)):
1031
if lines[i].startswith("diff -"):
1032
break
1033
else:
1034
message.append(lines[i])
1035
1036
header["message"] = message
1037
return header, lines[i:]
1038
1039
if from_format == "git":
1040
header, diff = parse_header(lines,
1041
(("user", GIT_FROM_REGEX), ("subject", GIT_SUBJECT_REGEX), ("date", GIT_DATE_REGEX)),
1042
mandatory=True)
1043
1044
if to_format == "hg-export":
1045
ret = []
1046
ret.append('# HG changeset patch')
1047
ret.append('# User %s'%(header["user"]))
1048
old_TZ = os.environ.get('TZ')
1049
try:
1050
os.environ['TZ'] = 'UTC'
1051
time.tzset()
1052
t = time.mktime(email.utils.parsedate(header["date"]))
1053
ret.append('# Date %s 00000'%int(t)) # this is not portable
1054
finally:
1055
if old_TZ:
1056
os.environ['TZ'] = old_TZ
1057
else:
1058
del os.environ['TZ']
1059
time.tzset()
1060
ret.append('# Node ID 0000000000000000000000000000000000000000')
1061
ret.append('# Parent 0000000000000000000000000000000000000000')
1062
ret.append(header["subject"])
1063
ret.extend(header["message"])
1064
ret.extend(diff)
1065
return ret
1066
else:
1067
raise NotImplementedError(to_format)
1068
elif from_format in ["hg", "diff", "hg-export"]:
1069
header, diff = parse_header(lines,
1070
(("hg_header", HG_HEADER_REGEX),
1071
("user", HG_USER_REGEX),
1072
("date", HG_DATE_REGEX),
1073
("node", HG_NODE_REGEX),
1074
("parent", HG_PARENT_REGEX)))
1075
user = header.get("user", '"Unknown User" <[email protected]>')
1076
date = email.utils.formatdate(int(header.get("date", time.time())))
1077
message = header.get("message", [])
1078
if message:
1079
subject = message[0]
1080
message = message[1:]
1081
else:
1082
subject = 'No Subject. Modified: %s'%(", ".join(
1083
sorted(self._detect_patch_modified_files(lines))))
1084
ret = []
1085
ret.append('From: %s'%user)
1086
ret.append('Subject: %s'%subject)
1087
ret.append('Date: %s'%date)
1088
ret.append('')
1089
if message and message != ['']: # avoid a double empty line
1090
ret.extend(message)
1091
ret.extend(diff)
1092
return self._rewrite_patch_header(ret, to_format=to_format, from_format="git",
1093
diff_format=diff_format)
1094
else:
1095
raise NotImplementedError(from_format)
1096
1097
def _rewrite_patch(self, lines, to_path_format, to_header_format, from_diff_format=None,
1098
from_path_format=None, from_header_format=None):
1099
r"""
1100
Rewrite the patch in ``lines`` to the path format given in
1101
``to_path_format`` and the header format given in ``to_header_format``.
1102
1103
TESTS::
1104
1105
sage: dev._wrap("_rewrite_patch")
1106
sage: from sage.env import SAGE_SRC
1107
sage: git_lines = open(
1108
....: os.path.join(SAGE_SRC, "sage", "dev", "test", "data", "git.patch")
1109
....: ).read().splitlines()
1110
sage: dev._rewrite_patch(git_lines, "old", "git") == git_lines
1111
True
1112
"""
1113
return self._rewrite_patch_diff_paths(
1114
self._rewrite_patch_header(lines,
1115
to_format=to_header_format,
1116
from_format=from_header_format,
1117
diff_format=from_diff_format),
1118
to_format=to_path_format,
1119
diff_format=from_diff_format,
1120
from_format=from_path_format)
1121
1122