-
-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathpatch.py
More file actions
147 lines (118 loc) · 4.64 KB
/
patch.py
File metadata and controls
147 lines (118 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# ScanCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/nexB/extractcode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import posixpath
import logging
import os.path
from commoncode import paths
from commoncode import fileutils
from commoncode import text
import typecode.contenttype
import extractcode
from extractcode import ExtractErrorFailedToExtract
"""
Low level utilities to parse patch files and treat them as if they were
archives containing files. Patches do not really contain files but changes to
files that would patched.
This way of dealing with patches helps handling patches with mixed origins
more conveniently.
"""
LOG = logging.getLogger(__name__)
def extract(location, target_dir):
"""
Extract each patch of a patch file at `location` as files in a target_dir
directory tree mimicking the directory in which the patches would be
applied with the patch command.
This treats a patch file as if it were an archive containing one file for
each patch applied to a file to be patched.
Return a list of warning messages. Raise Exceptionon errors.
"""
for source, target, text in patch_info(location):
# prefer the target path for writing the patch text to a subfile
# unless target is /dev/null (a deletion)
if '/dev/null' in target:
patch_subfile_path = source
else:
patch_subfile_path = target
# make the path safe to use as a subfile path
# ensure this a good and clean posix relative path
patch_subfile_path = paths.safe_path(patch_subfile_path, preserve_spaces=True)
# create directories
parent_dir = posixpath.dirname(patch_subfile_path)
parent_target_dir = os.path.join(target_dir, parent_dir)
fileutils.create_dir(parent_target_dir)
# find a unique name using a simple counter
base_subfile_path = os.path.join(target_dir, patch_subfile_path)
counter = 0
fp = base_subfile_path
while os.path.exists(fp + extractcode.EXTRACT_SUFFIX):
fp = base_subfile_path + '_%d' % counter
counter += 1
base_subfile_path = fp
# write the location proper, with a suffix extension to avoid
# recursive extraction
subfile_path = base_subfile_path + extractcode.EXTRACT_SUFFIX
with open(subfile_path, 'w') as subfile:
subfile.write('\n'.join(text))
return []
def is_patch(location, include_extracted=False):
"""
Test if a file is a possible patch file. May return True for some files
that are not patches. Extracted patch files are ignored by default.
"""
T = typecode.contenttype.get_type(location)
file_name = fileutils.file_name(location)
patch_like = (
'diff ' in T.filetype_file.lower()
or '.diff' in file_name
or '.patch' in file_name
)
if not patch_like:
return False
if extractcode.is_extraction_path(file_name) and not include_extracted:
return False
return True
def patch_text(ptch):
"""
Return the patch text content as an iterable of lines given a ptch 'patch
item'.
The content is re-formatted as unified diff.
"""
for head in ptch.header:
yield head
yield '--- ' + fileutils.as_posixpath(ptch.source)
yield '+++ ' + fileutils.as_posixpath(ptch.target)
hk = '@@ -%(startsrc)d,%(linessrc)d +%(starttgt)d,%(linestgt)d @@ %(desc)s'
def hunk_data(hnk):
return dict(
startsrc=hnk.startsrc,
linessrc=hnk.linessrc,
starttgt=hnk.starttgt,
linestgt=hnk.linestgt,
desc=text.as_unicode(hnk.desc),
)
for hunk in ptch.hunks:
yield hk % hunk_data(hunk)
for line in hunk.text:
yield line
def patch_info(location):
"""
Yield an iterable of tuples of (src_path, target_path, patch_text)
for each patch segment of a patch file at location.
Raise an exception if the file is not a patch file or cannot be parsed.
"""
import patch as pythonpatch
patchset = pythonpatch.fromfile(location)
if not patchset:
msg = 'Unable to parse patch file: %(location)s' % locals()
raise ExtractErrorFailedToExtract(msg)
for ptch in patchset.items:
src = fileutils.as_posixpath(ptch.source.strip())
tgt = fileutils.as_posixpath(ptch.target.strip())
text = [l.strip() for l in patch_text(ptch) if l]
yield src, tgt, text