Change parse_tree to return a list rather than a dict.
[jelmer/dulwich-libgit2.git] / dulwich / patch.py
1 # patch.py -- For dealing wih packed-style patches.
2 # Copryight (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 subprocess
27 import time
28
29
30 def write_commit_patch(f, commit, contents, progress, version=None):
31     """Write a individual file patch.
32
33     :param commit: Commit object
34     :param progress: Tuple with current patch number and total.
35     :return: tuple with filename and contents
36     """
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))
42     f.write("\n")
43     f.write("---\n")
44     try:
45         p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE, 
46                              stdin=subprocess.PIPE)
47     except OSError, e:
48         pass # diffstat not available?
49     else:
50         (diffstat, _) = p.communicate(contents)
51         f.write(diffstat)
52         f.write("\n")
53     f.write(contents)
54     f.write("-- \n")
55     if version is None:
56         from dulwich import __version__ as dulwich_version
57         f.write("Dulwich %d.%d.%d\n" % dulwich_version)
58     else:
59         f.write("%s\n" % version)
60
61
62 def get_summary(commit):
63     """Determine the summary line for use in a filename.
64     
65     :param commit: Commit
66     :return: Summary string
67     """
68     return commit.message.splitlines()[0].replace(" ", "-")
69
70
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.
73
74     Based on the same function in Python2.6.5-rc2's difflib.py
75     """
76     started = False
77     for group in SequenceMatcher(None, a, b).get_grouped_opcodes(3):
78         if not started:
79             yield '--- %s\n' % fromfile
80             yield '+++ %s\n' % tofile
81             started = True
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:
85             if tag == 'equal':
86                 for line in a[i1:i2]:
87                     yield ' ' + line
88                 continue
89             if tag == 'replace' or tag == 'delete':
90                 for line in a[i1:i2]:
91                     yield '-' + line
92             if tag == 'replace' or tag == 'insert':
93                 for line in b[j1:j2]:
94                     yield '+' + line
95
96
97 def write_blob_diff(f, (old_path, old_mode, old_blob), 
98                        (new_path, new_mode, new_blob)):
99     """Write diff file header.
100
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)
104     """
105     def blob_id(blob):
106         if blob is None:
107             return "0" * 7
108         else:
109             return blob.id[:7]
110     def lines(blob):
111         if blob is not None:
112             return blob.data.splitlines(True)
113         else:
114             return []
115     if old_path is None:
116         old_path = "/dev/null"
117     else:
118         old_path = "a/%s" % old_path
119     if new_path is None:
120         new_path = "/dev/null"
121     else:
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) 
129         else:
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, 
136         old_path, new_path))