Clean up file headers.
[jelmer/dulwich-libgit2.git] / dulwich / patch.py
1 # patch.py -- For dealing with packed-style patches.
2 # Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
3 #
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.
8 #
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.
13 #
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,
17 # MA  02110-1301, USA.
18
19 """Classes for dealing with git am-style patches.
20
21 These patches are basically unified diffs with some extra metadata tacked 
22 on.
23 """
24
25 from difflib import SequenceMatcher
26 import rfc822
27 import subprocess
28 import time
29
30 from dulwich.objects import (
31     Commit,
32     )
33
34 def write_commit_patch(f, commit, contents, progress, version=None):
35     """Write a individual file patch.
36
37     :param commit: Commit object
38     :param progress: Tuple with current patch number and total.
39     :return: tuple with filename and contents
40     """
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))
46     f.write("\n")
47     f.write("---\n")
48     try:
49         p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE, 
50                              stdin=subprocess.PIPE)
51     except OSError, e:
52         pass # diffstat not available?
53     else:
54         (diffstat, _) = p.communicate(contents)
55         f.write(diffstat)
56         f.write("\n")
57     f.write(contents)
58     f.write("-- \n")
59     if version is None:
60         from dulwich import __version__ as dulwich_version
61         f.write("Dulwich %d.%d.%d\n" % dulwich_version)
62     else:
63         f.write("%s\n" % version)
64
65
66 def get_summary(commit):
67     """Determine the summary line for use in a filename.
68     
69     :param commit: Commit
70     :return: Summary string
71     """
72     return commit.message.splitlines()[0].replace(" ", "-")
73
74
75 def unified_diff(a, b, fromfile='', tofile='', n=3):
76     """difflib.unified_diff that doesn't write any dates or trailing spaces.
77
78     Based on the same function in Python2.6.5-rc2's difflib.py
79     """
80     started = False
81     for group in SequenceMatcher(None, a, b).get_grouped_opcodes(n):
82         if not started:
83             yield '--- %s\n' % fromfile
84             yield '+++ %s\n' % tofile
85             started = True
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:
89             if tag == 'equal':
90                 for line in a[i1:i2]:
91                     yield ' ' + line
92                 continue
93             if tag == 'replace' or tag == 'delete':
94                 for line in a[i1:i2]:
95                     if not line[-1] == '\n':
96                         line += '\n\\ No newline at end of file\n'
97                     yield '-' + line
98             if tag == 'replace' or tag == 'insert':
99                 for line in b[j1:j2]:
100                     if not line[-1] == '\n':
101                         line += '\n\\ No newline at end of file\n'
102                     yield '+' + line
103
104
105 def write_blob_diff(f, (old_path, old_mode, old_blob), 
106                        (new_path, new_mode, new_blob)):
107     """Write diff file header.
108
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)
112     """
113     def blob_id(blob):
114         if blob is None:
115             return "0" * 7
116         else:
117             return blob.id[:7]
118     def lines(blob):
119         if blob is not None:
120             return blob.data.splitlines(True)
121         else:
122             return []
123     if old_path is None:
124         old_path = "/dev/null"
125     else:
126         old_path = "a/%s" % old_path
127     if new_path is None:
128         new_path = "/dev/null"
129     else:
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) 
137         else:
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, 
144         old_path, new_path))
145
146
147 def git_am_patch_split(f):
148     """Parse a git-am-style patch and split it up into bits.
149
150     :param f: File-like object to parse
151     :return: Tuple with commit object, diff contents and git version
152     """
153     msg = rfc822.Message(f)
154     c = Commit()
155     c.author = msg["from"]
156     c.committer = msg["from"]
157     if msg["subject"].startswith("[PATCH"):
158         subject = msg["subject"].split("]", 1)[1][1:]
159     else:
160         subject = msg["subject"]
161     c.message = subject
162     for l in f:
163         if l == "---\n":
164             break
165         c.message += l
166     diff = ""
167     for l in f:
168         if l == "-- \n":
169             break
170         diff += l
171     version = f.next().rstrip("\n")
172     return c, diff, version