1 # patch.py -- For dealing with packed-style patches.
2 # Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; version 2
7 # of the License or (at your option) a later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 """Classes for dealing with git am-style patches.
21 These patches are basically unified diffs with some extra metadata tacked
25 from difflib import SequenceMatcher
30 from dulwich.objects import (
34 def write_commit_patch(f, commit, contents, progress, version=None):
35 """Write a individual file patch.
37 :param commit: Commit object
38 :param progress: Tuple with current patch number and total.
39 :return: tuple with filename and contents
41 (num, total) = progress
42 f.write("From %s %s\n" % (commit.id, time.ctime(commit.commit_time)))
43 f.write("From: %s\n" % commit.author)
44 f.write("Date: %s\n" % time.strftime("%a, %d %b %Y %H:%M:%S %Z"))
45 f.write("Subject: [PATCH %d/%d] %s\n" % (num, total, commit.message))
49 p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE,
50 stdin=subprocess.PIPE)
52 pass # diffstat not available?
54 (diffstat, _) = p.communicate(contents)
60 from dulwich import __version__ as dulwich_version
61 f.write("Dulwich %d.%d.%d\n" % dulwich_version)
63 f.write("%s\n" % version)
66 def get_summary(commit):
67 """Determine the summary line for use in a filename.
70 :return: Summary string
72 return commit.message.splitlines()[0].replace(" ", "-")
75 def unified_diff(a, b, fromfile='', tofile='', n=3):
76 """difflib.unified_diff that doesn't write any dates or trailing spaces.
78 Based on the same function in Python2.6.5-rc2's difflib.py
81 for group in SequenceMatcher(None, a, b).get_grouped_opcodes(n):
83 yield '--- %s\n' % fromfile
84 yield '+++ %s\n' % tofile
86 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
87 yield "@@ -%d,%d +%d,%d @@\n" % (i1+1, i2-i1, j1+1, j2-j1)
88 for tag, i1, i2, j1, j2 in group:
93 if tag == 'replace' or tag == 'delete':
95 if not line[-1] == '\n':
96 line += '\n\\ No newline at end of file\n'
98 if tag == 'replace' or tag == 'insert':
100 if not line[-1] == '\n':
101 line += '\n\\ No newline at end of file\n'
105 def write_blob_diff(f, (old_path, old_mode, old_blob),
106 (new_path, new_mode, new_blob)):
107 """Write diff file header.
109 :param f: File-like object to write to
110 :param (old_path, old_mode, old_blob): Previous file (None if nonexisting)
111 :param (new_path, new_mode, new_blob): New file (None if nonexisting)
120 return blob.data.splitlines(True)
124 old_path = "/dev/null"
126 old_path = "a/%s" % old_path
128 new_path = "/dev/null"
130 new_path = "b/%s" % new_path
131 f.write("diff --git %s %s\n" % (old_path, new_path))
132 if old_mode != new_mode:
133 if new_mode is not None:
134 if old_mode is not None:
135 f.write("old mode %o\n" % old_mode)
136 f.write("new mode %o\n" % new_mode)
138 f.write("deleted mode %o\n" % old_mode)
139 f.write("index %s..%s %o\n" % (
140 blob_id(old_blob), blob_id(new_blob), new_mode))
141 old_contents = lines(old_blob)
142 new_contents = lines(new_blob)
143 f.writelines(unified_diff(old_contents, new_contents,
147 def git_am_patch_split(f):
148 """Parse a git-am-style patch and split it up into bits.
150 :param f: File-like object to parse
151 :return: Tuple with commit object, diff contents and git version
153 msg = rfc822.Message(f)
155 c.author = msg["from"]
156 c.committer = msg["from"]
157 if msg["subject"].startswith("[PATCH"):
158 subject = msg["subject"].split("]", 1)[1][1:]
160 subject = msg["subject"]
171 version = f.next().rstrip("\n")
172 return c, diff, version