Statistics
| Revision:

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