1 # patch.py -- For dealing wih packed-style patches.
2 # Copryight (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 def write_commit_patch(f, commit, contents, progress, version=None):
31 """Write a individual file patch.
33 :param commit: Commit object
34 :param progress: Tuple with current patch number and total.
35 :return: tuple with filename and contents
37 (num, total) = progress
38 f.write("From %s %s\n" % (commit.id, time.ctime(commit.commit_time)))
39 f.write("From: %s\n" % commit.author)
40 f.write("Date: %s\n" % time.strftime("%a, %d %b %Y %H:%M:%S %Z"))
41 f.write("Subject: [PATCH %d/%d] %s\n" % (num, total, commit.message))
45 p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE,
46 stdin=subprocess.PIPE)
48 pass # diffstat not available?
50 (diffstat, _) = p.communicate(contents)
56 from dulwich import __version__ as dulwich_version
57 f.write("Dulwich %d.%d.%d\n" % dulwich_version)
59 f.write("%s\n" % version)
62 def get_summary(commit):
63 """Determine the summary line for use in a filename.
66 :return: Summary string
68 return commit.message.splitlines()[0].replace(" ", "-")
71 def unified_diff(a, b, fromfile='', tofile='', n=3, lineterm='\n'):
72 """difflib.unified_diff that doesn't write any dates or trailing spaces.
74 Based on the same function in Python2.6.5-rc2's difflib.py
77 for group in SequenceMatcher(None, a, b).get_grouped_opcodes(3):
79 yield '--- %s\n' % fromfile
80 yield '+++ %s\n' % tofile
82 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
83 yield "@@ -%d,%d +%d,%d @@\n" % (i1+1, i2-i1, j1+1, j2-j1)
84 for tag, i1, i2, j1, j2 in group:
89 if tag == 'replace' or tag == 'delete':
92 if tag == 'replace' or tag == 'insert':
97 def write_blob_diff(f, (old_path, old_mode, old_blob),
98 (new_path, new_mode, new_blob)):
99 """Write diff file header.
101 :param f: File-like object to write to
102 :param (old_path, old_mode, old_blob): Previous file (None if nonexisting)
103 :param (new_path, new_mode, new_blob): New file (None if nonexisting)
112 return blob.data.splitlines(True)
116 old_path = "/dev/null"
118 old_path = "a/%s" % old_path
120 new_path = "/dev/null"
122 new_path = "b/%s" % new_path
123 f.write("diff --git %s %s\n" % (old_path, new_path))
124 if old_mode != new_mode:
125 if new_mode is not None:
126 if old_mode is not None:
127 f.write("old mode %o\n" % old_mode)
128 f.write("new mode %o\n" % new_mode)
130 f.write("deleted mode %o\n" % old_mode)
131 f.write("index %s..%s %o\n" % (
132 blob_id(old_blob), blob_id(new_blob), new_mode))
133 old_contents = lines(old_blob)
134 new_contents = lines(new_blob)
135 f.writelines(unified_diff(old_contents, new_contents,