]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/parser.py
Revert "Decode patch from UTF-8 while parsing from stdin"
[patchwork] / apps / patchwork / parser.py
1 #!/usr/bin/python
2 #
3 # Patchwork - automated patch tracking system
4 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
5 #
6 # This file is part of the Patchwork package.
7 #
8 # Patchwork is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # Patchwork is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Patchwork; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21
22
23 import re
24
25 try:
26     import hashlib
27     sha1_hash = hashlib.sha1
28 except ImportError:
29     import sha
30     sha1_hash = sha.sha
31
32 _hunk_re = re.compile('^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@')
33 _filename_re = re.compile('^(---|\+\+\+) (\S+)')
34
35 def parse_patch(text):
36     patchbuf = ''
37     commentbuf = ''
38     buf = ''
39
40     # state specified the line we just saw, and what to expect next
41     state = 0
42     # 0: text
43     # 1: suspected patch header (diff, ====, Index:)
44     # 2: patch header line 1 (---)
45     # 3: patch header line 2 (+++)
46     # 4: patch hunk header line (@@ line)
47     # 5: patch hunk content
48     #
49     # valid transitions:
50     #  0 -> 1 (diff, ===, Index:)
51     #  0 -> 2 (---)
52     #  1 -> 2 (---)
53     #  2 -> 3 (+++)
54     #  3 -> 4 (@@ line)
55     #  4 -> 5 (patch content)
56     #  5 -> 1 (run out of lines from @@-specifed count)
57     #
58     # Suspected patch header is stored into buf, and appended to
59     # patchbuf if we find a following hunk. Otherwise, append to
60     # comment after parsing.
61
62     # line counts while parsing a patch hunk
63     lc = (0, 0)
64     hunk = 0
65
66
67     for line in text.split('\n'):
68         line += '\n'
69
70         if state == 0:
71             if line.startswith('diff') or line.startswith('===') \
72                     or line.startswith('Index: '):
73                 state = 1
74                 buf += line
75
76             elif line.startswith('--- '):
77                 state = 2
78                 buf += line
79
80             else:
81                 commentbuf += line
82
83         elif state == 1:
84             buf += line
85             if line.startswith('--- '):
86                 state = 2
87
88         elif state == 2:
89             if line.startswith('+++ '):
90                 state = 3
91                 buf += line
92
93             elif hunk:
94                 state = 1
95                 buf += line
96
97             else:
98                 state = 0
99                 commentbuf += buf + line
100                 buf = ''
101
102         elif state == 3:
103             match = _hunk_re.match(line)
104             if match:
105
106                 def fn(x):
107                     if not x:
108                         return 1
109                     return int(x)
110
111                 lc = map(fn, match.groups())
112
113                 state = 4
114                 patchbuf += buf + line
115                 buf = ''
116
117             elif line.startswith('--- '):
118                 patchbuf += buf + line
119                 buf = ''
120                 state = 2
121
122             elif hunk:
123                 state = 1
124                 buf += line
125
126             else:
127                 state = 0
128                 commentbuf += buf + line
129                 buf = ''
130
131         elif state == 4 or state == 5:
132             if line.startswith('-'):
133                 lc[0] -= 1
134             elif line.startswith('+'):
135                 lc[1] -= 1
136             elif line.startswith('\ No newline at end of file'):
137                 # Special case: Not included as part of the hunk's line count
138                 pass
139             else:
140                 lc[0] -= 1
141                 lc[1] -= 1
142
143             patchbuf += line
144
145             if lc[0] <= 0 and lc[1] <= 0:
146                 state = 3
147                 hunk += 1
148             else:
149                 state = 5
150
151         else:
152             raise Exception("Unknown state %d! (line '%s')" % (state, line))
153
154     commentbuf += buf
155
156     if patchbuf == '':
157         patchbuf = None
158
159     if commentbuf == '':
160         commentbuf = None
161
162     return (patchbuf, commentbuf)
163
164 def hash_patch(str):
165     # normalise spaces
166     str = str.replace('\r', '')
167     str = str.strip() + '\n'
168
169     prefixes = ['-', '+', ' ']
170     hash = sha1_hash()
171
172     for line in str.split('\n'):
173
174         if len(line) <= 0:
175             continue
176
177         hunk_match = _hunk_re.match(line)
178         filename_match = _filename_re.match(line)
179
180         if filename_match:
181             # normalise -p1 top-directories
182             if filename_match.group(1) == '---':
183                 filename = 'a/'
184             else:
185                 filename = 'b/'
186             filename += '/'.join(filename_match.group(2).split('/')[1:])
187
188             line = filename_match.group(1) + ' ' + filename
189
190         elif hunk_match:
191             # remove line numbers, but leave line counts
192             def fn(x):
193                 if not x:
194                     return 1
195                 return int(x)
196             line_nos = map(fn, hunk_match.groups())
197             line = '@@ -%d +%d @@' % tuple(line_nos)
198
199         elif line[0] in prefixes:
200             # if we have a +, - or context line, leave as-is
201             pass
202
203         else:
204             # other lines are ignored
205             continue
206
207         hash.update(line.encode('utf-8') + '\n')
208
209     return hash
210
211
212 def main(args):
213     from optparse import OptionParser
214
215     parser = OptionParser()
216     parser.add_option('-p', '--patch', action = 'store_true',
217             dest = 'print_patch', help = 'print parsed patch')
218     parser.add_option('-c', '--comment', action = 'store_true',
219             dest = 'print_comment', help = 'print parsed comment')
220     parser.add_option('-#', '--hash', action = 'store_true',
221             dest = 'print_hash', help = 'print patch hash')
222
223     (options, args) = parser.parse_args()
224
225     # decode from (assumed) UTF-8
226     content = sys.stdin.read().decode('utf-8')
227
228     (patch, comment) = parse_patch(content)
229
230     if options.print_hash and patch:
231         print hash_patch(patch).hexdigest()
232
233     if options.print_patch and patch:
234         print "Patch: ------\n" + patch
235
236     if options.print_comment and comment:
237         print "Comment: ----\n" + comment
238
239 if __name__ == '__main__':
240     import sys
241     sys.exit(main(sys.argv))