porting code to python3.x with os patch
[tools/git-buildpackage.git] / gbp / patch_series.py
1 # vim: set fileencoding=utf-8 :
2 #
3 # (C) 2011 Guido Guenther <agx@sigxcpu.org>
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 """Handle Patches and Patch Series"""
18
19 import os
20 import re
21 import subprocess
22 import tempfile
23 from gbp.errors import GbpError
24
25 class Patch(object):
26     """
27     A patch in a L{PatchSeries}
28
29     @ivar path: path to the patch
30     @type path: string
31     @ivar topic: the topic of the patch (the directory component)
32     @type topic: string
33     @ivar strip: path components to strip (think patch -p<strip>)
34     @type strip: integer
35     @ivar info: Information retrieved from a RFC822 style patch header
36     @type info: C{dict} with C{str} keys and values
37     @ivar long_desc: the long description of the patch
38     """
39     patch_exts = ['diff', 'patch']
40
41     def __init__(self, path, topic=None, strip=None):
42         self.path = path
43         self.topic = topic
44         self.strip = strip
45         self.info = None
46         self.long_desc = None
47
48     def __repr__(self):
49         repr = "<gbp.patch_series.Patch path='%s' " % self.path
50         if self.topic:
51             repr += "topic='%s' " % self.topic
52         if self.strip is not None:
53             repr += "strip=%d " % self.strip
54         repr += ">"
55         return repr
56
57     def _read_info(self):
58         """
59         Read patch information into a structured form
60
61         using I{git mailinfo}
62         """
63         self.info = {}
64         body = tempfile.NamedTemporaryFile(prefix='gbp_')
65         pipe = subprocess.Popen("git mailinfo -k '%s' /dev/null 2>/dev/null < '%s'" %
66                                 (body.name, self.path),
67                                 shell=True,
68                                 stdout=subprocess.PIPE).stdout
69         for line in pipe:
70             line = line.decode()
71             if ':' in line:
72                 rfc_header, value = line.split(" ", 1)
73                 header = rfc_header[:-1].lower()
74                 self.info[header] = value.strip()
75         try:
76             self.long_desc = "".join([l.decode("utf-8", "backslashreplace") for l in body])
77         except (IOError, UnicodeDecodeError) as msg:
78             raise GbpError("Failed to read patch header of '%s': %s" %
79                            (self.path, msg))
80         finally:
81             body.close()
82             if os.path.exists(body.name):
83                 os.unlink(body.name)
84
85     def _get_subject_from_filename(self):
86         """
87         Determine the patch's subject based on the it's filename
88
89         >>> p = Patch('debian/patches/foo.patch')
90         >>> p._get_subject_from_filename()
91         'foo'
92         >>> Patch('foo.patch')._get_subject_from_filename()
93         'foo'
94         >>> Patch('debian/patches/foo.bar')._get_subject_from_filename()
95         'foo.bar'
96         >>> p = Patch('debian/patches/foo')
97         >>> p._get_subject_from_filename()
98         'foo'
99         >>> Patch('0123-foo.patch')._get_subject_from_filename()
100         'foo'
101         >>> Patch('0123.patch')._get_subject_from_filename()
102         '0123'
103         >>> Patch('0123-foo-0123.patch')._get_subject_from_filename()
104         'foo-0123'
105
106         @return: the patch's subject
107         @rtype: C{str}
108         """
109         subject = os.path.basename(self.path)
110         # Strip of .diff or .patch from patch name
111         try:
112             base, ext = subject.rsplit('.', 1)
113             if ext in self.patch_exts:
114                 subject = base
115         except ValueError:
116                 pass  # No ext so keep subject as is
117         return subject.lstrip('0123456789-') or subject
118
119     def _get_info_field(self, key, get_val=None):
120         """
121         Return the key I{key} from the info C{dict}
122         or use val if I{key} is not a valid key.
123
124         Fill self.info if not already done.
125
126         @param key: key to fetch
127         @type key: C{str}
128         @param get_val: alternate value if key is not in info dict
129         @type get_val: C{()->str}
130         """
131         if self.info is None:
132             self._read_info()
133
134         if key in self.info:
135             return self.info[key]
136         else:
137             return get_val() if get_val else None
138
139
140     @property
141     def subject(self):
142         """
143         The patch's subject, either from the patch header or from the filename.
144         """
145         return self._get_info_field('subject', self._get_subject_from_filename)
146
147     @property
148     def author(self):
149         """The patch's author"""
150         return self._get_info_field('author')
151
152     @property
153     def email(self):
154         """The patch author's email address"""
155         return self._get_info_field('email')
156
157     @property
158     def date(self):
159         """The patch's modification time"""
160         return self._get_info_field('date')
161
162
163 class PatchSeries(list):
164     """
165     A series of L{Patch}es as read from a quilt series file).
166     """
167
168     @classmethod
169     def read_series_file(klass, seriesfile):
170         """Read a series file into L{Patch} objects"""
171         patch_dir = os.path.dirname(seriesfile)
172
173         if not os.path.exists(seriesfile):
174             return []
175
176         try:
177             s = open(seriesfile)
178         except Exception as err:
179             raise GbpError("Cannot open series file: %s" % err)
180
181         queue = klass._read_series(s, patch_dir)
182         s.close()
183         return queue
184
185     @classmethod
186     def _read_series(klass, series, patch_dir):
187         """
188         Read patch series
189
190         >>> PatchSeries._read_series(['a/b', \
191                             'a -p1', \
192                             'a/b -p2'], '.') # doctest:+NORMALIZE_WHITESPACE
193         [<gbp.patch_series.Patch path='./a/b' topic='a' >,
194          <gbp.patch_series.Patch path='./a' strip=1 >,
195          <gbp.patch_series.Patch path='./a/b' topic='a' strip=2 >]
196
197         >>> PatchSeries._read_series(['# foo', 'a/b', '', '# bar'], '.')
198         [<gbp.patch_series.Patch path='./a/b' topic='a' >]
199
200         @param series: series of patches in quilt format
201         @type series: iterable of strings
202         @param patch_dir: path prefix to prepend to each patch path
203         @type patch_dir: string
204         """
205
206         queue = PatchSeries()
207         for line in series:
208             try:
209                 if line[0] in ['\n', '#']:
210                     continue
211             except IndexError:
212                 continue  # ignore empty lines
213             queue.append(cls._parse_line(line, patch_dir))
214         return queue
215
216     @staticmethod
217     def _get_topic(line):
218         """
219         Get the topic from the patch's path
220
221         >>> PatchSeries._get_topic("a/b c")
222         'a'
223         >>> PatchSeries._get_topic("asdf")
224         >>> PatchSeries._get_topic("/asdf")
225         """
226         topic = os.path.dirname(line)
227         if topic in ['', '/']:
228             topic = None
229         return topic
230
231     @staticmethod
232     def _split_strip(line):
233         """
234         Separate the -p<num> option from the patch name
235
236         >>> PatchSeries._split_strip("asdf -p1")
237         ('asdf', 1)
238         >>> PatchSeries._split_strip("a/nice/patch")
239         ('a/nice/patch', None)
240         >>> PatchSeries._split_strip("asdf foo")
241         ('asdf foo', None)
242         """
243         patch = line
244         strip = None
245
246         split = line.rsplit(None, 1)
247         if len(split) > 1:
248             m = re.match('-p(?P<level>[0-9]+)', split[1])
249             if m:
250                 patch = split[0]
251                 strip = int(m.group('level'))
252
253         return (patch, strip)
254
255     @classmethod
256     def _parse_line(klass, line, patch_dir):
257         """
258         Parse a single line from a series file
259
260         >>> PatchSeries._parse_line("a/b -p1", '/tmp/patches')
261         <gbp.patch_series.Patch path='/tmp/patches/a/b' topic='a' strip=1 >
262         >>> PatchSeries._parse_line("a/b", '.')
263         <gbp.patch_series.Patch path='./a/b' topic='a' >
264         """
265         line = line.rstrip()
266         topic = klass._get_topic(line)
267         (patch, split) = klass._split_strip(line)
268         return Patch(os.path.join(patch_dir, patch), topic, split)
269
270