gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / patch.py @ 959
History | View | Annotate | Download (10.6 KB)
1 |
# patch.py -- For dealing with packed-style patches.
|
---|---|
2 |
# Copyright (C) 2009-2013 Jelmer Vernooij <jelmer@samba.org>
|
3 |
#
|
4 |
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
|
5 |
# General Public License as public by the Free Software Foundation; version 2.0
|
6 |
# or (at your option) any later version. You can redistribute it and/or
|
7 |
# modify it under the terms of either of these two licenses.
|
8 |
#
|
9 |
# Unless required by applicable law or agreed to in writing, software
|
10 |
# distributed under the License is distributed on an "AS IS" BASIS,
|
11 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 |
# See the License for the specific language governing permissions and
|
13 |
# limitations under the License.
|
14 |
#
|
15 |
# You should have received a copy of the licenses; if not, see
|
16 |
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
|
17 |
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
|
18 |
# License, Version 2.0.
|
19 |
#
|
20 |
|
21 |
"""Classes for dealing with git am-style patches.
|
22 |
|
23 |
These patches are basically unified diffs with some extra metadata tacked
|
24 |
on.
|
25 |
"""
|
26 |
|
27 |
from difflib import SequenceMatcher |
28 |
import email.parser |
29 |
import time |
30 |
|
31 |
from dulwich.objects import ( |
32 |
Blob, |
33 |
Commit, |
34 |
S_ISGITLINK, |
35 |
) |
36 |
|
37 |
FIRST_FEW_BYTES = 8000
|
38 |
|
39 |
|
40 |
def write_commit_patch(f, commit, contents, progress, version=None, encoding=None): |
41 |
"""Write a individual file patch.
|
42 |
|
43 |
:param commit: Commit object
|
44 |
:param progress: Tuple with current patch number and total.
|
45 |
:return: tuple with filename and contents
|
46 |
"""
|
47 |
encoding = encoding or getattr(f, "encoding", "ascii") |
48 |
if type(contents) is str: |
49 |
contents = contents.encode(encoding) |
50 |
(num, total) = progress |
51 |
f.write(b"From " + commit.id + b" " + time.ctime(commit.commit_time).encode(encoding) + b"\n") |
52 |
f.write(b"From: " + commit.author + b"\n") |
53 |
f.write(b"Date: " + time.strftime("%a, %d %b %Y %H:%M:%S %Z").encode(encoding) + b"\n") |
54 |
f.write(("Subject: [PATCH %d/%d] " % (num, total)).encode(encoding) + commit.message + b"\n") |
55 |
f.write(b"\n")
|
56 |
f.write(b"---\n")
|
57 |
try:
|
58 |
import subprocess |
59 |
p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE,
|
60 |
stdin=subprocess.PIPE) |
61 |
except (ImportError, OSError): |
62 |
pass # diffstat not available? |
63 |
else:
|
64 |
(diffstat, _) = p.communicate(contents) |
65 |
f.write(diffstat) |
66 |
f.write(b"\n")
|
67 |
f.write(contents) |
68 |
f.write(b"-- \n")
|
69 |
if version is None: |
70 |
from dulwich import __version__ as dulwich_version |
71 |
f.write(b"Dulwich %d.%d.%d\n" % dulwich_version)
|
72 |
else:
|
73 |
f.write(version.encode(encoding) + b"\n")
|
74 |
|
75 |
|
76 |
def get_summary(commit): |
77 |
"""Determine the summary line for use in a filename.
|
78 |
|
79 |
:param commit: Commit
|
80 |
:return: Summary string
|
81 |
"""
|
82 |
return commit.message.splitlines()[0].replace(" ", "-") |
83 |
|
84 |
|
85 |
def unified_diff(a, b, fromfile, tofile, n=3): |
86 |
"""difflib.unified_diff that doesn't write any dates or trailing spaces.
|
87 |
|
88 |
Based on the same function in Python2.6.5-rc2's difflib.py
|
89 |
"""
|
90 |
started = False
|
91 |
for group in SequenceMatcher(None, a, b).get_grouped_opcodes(n): |
92 |
if not started: |
93 |
yield b'--- ' + fromfile + b'\n' |
94 |
yield b'+++ ' + tofile + b'\n' |
95 |
started = True
|
96 |
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] |
97 |
sizes = "@@ -%d,%d +%d,%d @@\n" % (i1+1, i2-i1, j1+1, j2-j1) |
98 |
yield sizes.encode('ascii') |
99 |
for tag, i1, i2, j1, j2 in group: |
100 |
if tag == 'equal': |
101 |
for line in a[i1:i2]: |
102 |
yield b' ' + line |
103 |
continue
|
104 |
if tag == 'replace' or tag == 'delete': |
105 |
for line in a[i1:i2]: |
106 |
if not line[-1:] == b'\n': |
107 |
line += b'\n\\ No newline at end of file\n'
|
108 |
yield b'-' + line |
109 |
if tag == 'replace' or tag == 'insert': |
110 |
for line in b[j1:j2]: |
111 |
if not line[-1:] == b'\n': |
112 |
line += b'\n\\ No newline at end of file\n'
|
113 |
yield b'+' + line |
114 |
|
115 |
|
116 |
def is_binary(content): |
117 |
"""See if the first few bytes contain any null characters.
|
118 |
|
119 |
:param content: Bytestring to check for binary content
|
120 |
"""
|
121 |
return b'\0' in content[:FIRST_FEW_BYTES] |
122 |
|
123 |
|
124 |
def shortid(hexsha): |
125 |
if hexsha is None: |
126 |
return b"0" * 7 |
127 |
else:
|
128 |
return hexsha[:7] |
129 |
|
130 |
|
131 |
def patch_filename(p, root): |
132 |
if p is None: |
133 |
return b"/dev/null" |
134 |
else:
|
135 |
return root + b"/" + p |
136 |
|
137 |
|
138 |
def write_object_diff(f, store, old_file, new_file, diff_binary=False): |
139 |
"""Write the diff for an object.
|
140 |
|
141 |
:param f: File-like object to write to
|
142 |
:param store: Store to retrieve objects from, if necessary
|
143 |
:param old_file: (path, mode, hexsha) tuple
|
144 |
:param new_file: (path, mode, hexsha) tuple
|
145 |
:param diff_binary: Whether to diff files even if they
|
146 |
are considered binary files by is_binary().
|
147 |
|
148 |
:note: the tuple elements should be None for nonexistant files
|
149 |
"""
|
150 |
(old_path, old_mode, old_id) = old_file |
151 |
(new_path, new_mode, new_id) = new_file |
152 |
old_path = patch_filename(old_path, b"a")
|
153 |
new_path = patch_filename(new_path, b"b")
|
154 |
def content(mode, hexsha): |
155 |
if hexsha is None: |
156 |
return Blob.from_string(b'') |
157 |
elif S_ISGITLINK(mode):
|
158 |
return Blob.from_string(b"Submodule commit " + hexsha + b"\n") |
159 |
else:
|
160 |
return store[hexsha]
|
161 |
|
162 |
def lines(content): |
163 |
if not content: |
164 |
return []
|
165 |
else:
|
166 |
return content.splitlines()
|
167 |
f.writelines(gen_diff_header( |
168 |
(old_path, new_path), (old_mode, new_mode), (old_id, new_id))) |
169 |
old_content = content(old_mode, old_id) |
170 |
new_content = content(new_mode, new_id) |
171 |
if not diff_binary and ( |
172 |
is_binary(old_content.data) or is_binary(new_content.data)):
|
173 |
f.write(b"Binary files " + old_path + b" and " + new_path + b" differ\n") |
174 |
else:
|
175 |
f.writelines(unified_diff(lines(old_content), lines(new_content), |
176 |
old_path, new_path)) |
177 |
|
178 |
|
179 |
# TODO(jelmer): Support writing unicode, rather than bytes.
|
180 |
def gen_diff_header(paths, modes, shas): |
181 |
"""Write a blob diff header.
|
182 |
|
183 |
:param paths: Tuple with old and new path
|
184 |
:param modes: Tuple with old and new modes
|
185 |
:param shas: Tuple with old and new shas
|
186 |
"""
|
187 |
(old_path, new_path) = paths |
188 |
(old_mode, new_mode) = modes |
189 |
(old_sha, new_sha) = shas |
190 |
yield b"diff --git " + old_path + b" " + new_path + b"\n" |
191 |
if old_mode != new_mode:
|
192 |
if new_mode is not None: |
193 |
if old_mode is not None: |
194 |
yield ("old mode %o\n" % old_mode).encode('ascii') |
195 |
yield ("new mode %o\n" % new_mode).encode('ascii') |
196 |
else:
|
197 |
yield ("deleted mode %o\n" % old_mode).encode('ascii') |
198 |
yield b"index " + shortid(old_sha) + b".." + shortid(new_sha) |
199 |
if new_mode is not None: |
200 |
yield (" %o" % new_mode).encode('ascii') |
201 |
yield b"\n" |
202 |
|
203 |
|
204 |
# TODO(jelmer): Support writing unicode, rather than bytes.
|
205 |
def write_blob_diff(f, old_file, new_file): |
206 |
"""Write blob diff.
|
207 |
|
208 |
:param f: File-like object to write to
|
209 |
:param old_file: (path, mode, hexsha) tuple (None if nonexisting)
|
210 |
:param new_file: (path, mode, hexsha) tuple (None if nonexisting)
|
211 |
|
212 |
:note: The use of write_object_diff is recommended over this function.
|
213 |
"""
|
214 |
(old_path, old_mode, old_blob) = old_file |
215 |
(new_path, new_mode, new_blob) = new_file |
216 |
old_path = patch_filename(old_path, b"a")
|
217 |
new_path = patch_filename(new_path, b"b")
|
218 |
def lines(blob): |
219 |
if blob is not None: |
220 |
return blob.splitlines()
|
221 |
else:
|
222 |
return []
|
223 |
f.writelines(gen_diff_header( |
224 |
(old_path, new_path), (old_mode, new_mode), |
225 |
(getattr(old_blob, "id", None), getattr(new_blob, "id", None)))) |
226 |
old_contents = lines(old_blob) |
227 |
new_contents = lines(new_blob) |
228 |
f.writelines(unified_diff(old_contents, new_contents, |
229 |
old_path, new_path)) |
230 |
|
231 |
|
232 |
# TODO(jelmer): Support writing unicode, rather than bytes.
|
233 |
def write_tree_diff(f, store, old_tree, new_tree, diff_binary=False): |
234 |
"""Write tree diff.
|
235 |
|
236 |
:param f: File-like object to write to.
|
237 |
:param old_tree: Old tree id
|
238 |
:param new_tree: New tree id
|
239 |
:param diff_binary: Whether to diff files even if they
|
240 |
are considered binary files by is_binary().
|
241 |
"""
|
242 |
changes = store.tree_changes(old_tree, new_tree) |
243 |
for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes: |
244 |
write_object_diff(f, store, (oldpath, oldmode, oldsha), |
245 |
(newpath, newmode, newsha), |
246 |
diff_binary=diff_binary) |
247 |
|
248 |
|
249 |
def git_am_patch_split(f, encoding=None): |
250 |
"""Parse a git-am-style patch and split it up into bits.
|
251 |
|
252 |
:param f: File-like object to parse
|
253 |
:param encoding: Encoding to use when creating Git objects
|
254 |
:return: Tuple with commit object, diff contents and git version
|
255 |
"""
|
256 |
encoding = encoding or getattr(f, "encoding", "ascii") |
257 |
contents = f.read() |
258 |
if type(contents) is bytes and getattr(email.parser, "BytesParser", None): |
259 |
parser = email.parser.BytesParser() |
260 |
msg = parser.parsebytes(contents) |
261 |
else:
|
262 |
parser = email.parser.Parser() |
263 |
msg = parser.parsestr(contents) |
264 |
return parse_patch_message(msg, encoding)
|
265 |
|
266 |
|
267 |
def parse_patch_message(msg, encoding=None): |
268 |
"""Extract a Commit object and patch from an e-mail message.
|
269 |
|
270 |
:param msg: An email message (email.message.Message)
|
271 |
:param encoding: Encoding to use to encode Git commits
|
272 |
:return: Tuple with commit object, diff contents and git version
|
273 |
"""
|
274 |
c = Commit() |
275 |
c.author = msg["from"].encode(encoding)
|
276 |
c.committer = msg["from"].encode(encoding)
|
277 |
try:
|
278 |
patch_tag_start = msg["subject"].index("[PATCH") |
279 |
except ValueError: |
280 |
subject = msg["subject"]
|
281 |
else:
|
282 |
close = msg["subject"].index("] ", patch_tag_start) |
283 |
subject = msg["subject"][close+2:] |
284 |
c.message = (subject.replace("\n", "") + "\n").encode(encoding) |
285 |
first = True
|
286 |
|
287 |
body = msg.get_payload(decode=True)
|
288 |
lines = body.splitlines(True)
|
289 |
line_iter = iter(lines)
|
290 |
|
291 |
for l in line_iter: |
292 |
if l == b"---\n": |
293 |
break
|
294 |
if first:
|
295 |
if l.startswith(b"From: "): |
296 |
c.author = l[len(b"From: "):].rstrip() |
297 |
else:
|
298 |
c.message += b"\n" + l
|
299 |
first = False
|
300 |
else:
|
301 |
c.message += l |
302 |
diff = b""
|
303 |
for l in line_iter: |
304 |
if l == b"-- \n": |
305 |
break
|
306 |
diff += l |
307 |
try:
|
308 |
version = next(line_iter).rstrip(b"\n") |
309 |
except StopIteration: |
310 |
version = None
|
311 |
return c, diff, version
|