1 # vim: set fileencoding=utf-8 :
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 """Handle Patches and Patch Series"""
23 from gbp.errors import GbpError
27 A patch in a L{PatchSeries}
29 @ivar path: path to the patch
31 @ivar topic: the topic of the patch (the directory component)
33 @ivar strip: path components to strip (think patch -p<strip>)
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
39 patch_exts = ['diff', 'patch']
41 def __init__(self, path, topic=None, strip=None):
49 repr = "<gbp.patch_series.Patch path='%s' " % self.path
51 repr += "topic='%s' " % self.topic
52 if self.strip is not None:
53 repr += "strip=%d " % self.strip
59 Read patch information into a structured form
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),
68 stdout=subprocess.PIPE).stdout
72 rfc_header, value = line.split(" ", 1)
73 header = rfc_header[:-1].lower()
74 self.info[header] = value.strip()
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" %
82 if os.path.exists(body.name):
85 def _get_subject_from_filename(self):
87 Determine the patch's subject based on the it's filename
89 >>> p = Patch('debian/patches/foo.patch')
90 >>> p._get_subject_from_filename()
92 >>> Patch('foo.patch')._get_subject_from_filename()
94 >>> Patch('debian/patches/foo.bar')._get_subject_from_filename()
96 >>> p = Patch('debian/patches/foo')
97 >>> p._get_subject_from_filename()
99 >>> Patch('0123-foo.patch')._get_subject_from_filename()
101 >>> Patch('0123.patch')._get_subject_from_filename()
103 >>> Patch('0123-foo-0123.patch')._get_subject_from_filename()
106 @return: the patch's subject
109 subject = os.path.basename(self.path)
110 # Strip of .diff or .patch from patch name
112 base, ext = subject.rsplit('.', 1)
113 if ext in self.patch_exts:
116 pass # No ext so keep subject as is
117 return subject.lstrip('0123456789-') or subject
119 def _get_info_field(self, key, get_val=None):
121 Return the key I{key} from the info C{dict}
122 or use val if I{key} is not a valid key.
124 Fill self.info if not already done.
126 @param key: key to fetch
128 @param get_val: alternate value if key is not in info dict
129 @type get_val: C{()->str}
131 if self.info is None:
135 return self.info[key]
137 return get_val() if get_val else None
143 The patch's subject, either from the patch header or from the filename.
145 return self._get_info_field('subject', self._get_subject_from_filename)
149 """The patch's author"""
150 return self._get_info_field('author')
154 """The patch author's email address"""
155 return self._get_info_field('email')
159 """The patch's modification time"""
160 return self._get_info_field('date')
163 class PatchSeries(list):
165 A series of L{Patch}es as read from a quilt series file).
169 def read_series_file(klass, seriesfile):
170 """Read a series file into L{Patch} objects"""
171 patch_dir = os.path.dirname(seriesfile)
173 if not os.path.exists(seriesfile):
178 except Exception as err:
179 raise GbpError("Cannot open series file: %s" % err)
181 queue = klass._read_series(s, patch_dir)
186 def _read_series(klass, series, patch_dir):
190 >>> PatchSeries._read_series(['a/b', \
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 >]
197 >>> PatchSeries._read_series(['# foo', 'a/b', '', '# bar'], '.')
198 [<gbp.patch_series.Patch path='./a/b' topic='a' >]
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
206 queue = PatchSeries()
209 if line[0] in ['\n', '#']:
212 continue # ignore empty lines
213 queue.append(cls._parse_line(line, patch_dir))
217 def _get_topic(line):
219 Get the topic from the patch's path
221 >>> PatchSeries._get_topic("a/b c")
223 >>> PatchSeries._get_topic("asdf")
224 >>> PatchSeries._get_topic("/asdf")
226 topic = os.path.dirname(line)
227 if topic in ['', '/']:
232 def _split_strip(line):
234 Separate the -p<num> option from the patch name
236 >>> PatchSeries._split_strip("asdf -p1")
238 >>> PatchSeries._split_strip("a/nice/patch")
239 ('a/nice/patch', None)
240 >>> PatchSeries._split_strip("asdf foo")
246 split = line.rsplit(None, 1)
248 m = re.match('-p(?P<level>[0-9]+)', split[1])
251 strip = int(m.group('level'))
253 return (patch, strip)
256 def _parse_line(klass, line, patch_dir):
258 Parse a single line from a series file
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' >
266 topic = klass._get_topic(line)
267 (patch, split) = klass._split_strip(line)
268 return Patch(os.path.join(patch_dir, patch), topic, split)