diff options
author | Guido Günther <agx@sigxcpu.org> | 2011-12-24 14:16:49 +0100 |
---|---|---|
committer | Guido Günther <agx@sigxcpu.org> | 2011-12-26 14:02:29 +0100 |
commit | 4770a06ab765931286bc5ab6105c0d7dd6a86811 (patch) | |
tree | 1a99899257091c71b7b1cfd40fe17235a558feac /gbp/patch_series.py | |
parent | 637b87a01a9bdc95c48a6edd85db196f9c4a95a9 (diff) | |
download | git-buildpackage-4770a06ab765931286bc5ab6105c0d7dd6a86811.tar.gz git-buildpackage-4770a06ab765931286bc5ab6105c0d7dd6a86811.tar.bz2 git-buildpackage-4770a06ab765931286bc5ab6105c0d7dd6a86811.zip |
Rename gbp.pq to gbp.patch_series
since that's what it currently handles.
Diffstat (limited to 'gbp/patch_series.py')
-rw-r--r-- | gbp/patch_series.py | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/gbp/patch_series.py b/gbp/patch_series.py new file mode 100644 index 00000000..0ab8bba7 --- /dev/null +++ b/gbp/patch_series.py @@ -0,0 +1,263 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2011 Guido Guenther <agx@sigxcpu.org> +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""Handle Patches and Patch Series""" + +import os +import re +import subprocess +import tempfile +from gbp.errors import GbpError + +class Patch(object): + """ + A patch in a L{PatchSeries} + + @ivar path: path to the patch + @type path: string + @ivar topic: the topic of the patch (the directory component) + @type topic: string + @ivar strip: path components to strip (think patch -p<strip>) + @type strip: integer + @ivar info: Information retrieved from a RFC822 style patch header + @type info: C{dict} with C{str} keys and values + @ivar long_desc: the long description of the patch + """ + patch_exts = ['diff', 'patch'] + + def __init__(self, path, topic=None, strip=None): + self.path = path + self.topic = topic + self.strip = strip + self.info = None + self.long_desc = None + + def __repr__(self): + repr = "<gbp.patch_series.Patch path='%s' " % self.path + if self.topic: + repr += "topic='%s' " % self.topic + if self.strip != None: + repr += "strip=%d " % self.strip + repr += ">" + return repr + + def _read_info(self): + """ + Read patch information into a structured form + + using I{git mailinfo} + """ + self.info = {} + body = tempfile.NamedTemporaryFile(prefix='gbp_') + pipe = subprocess.Popen("git mailinfo '%s' /dev/null < '%s'" % + (body.name, self.path), + shell=True, + stdout=subprocess.PIPE).stdout + for line in pipe: + if ':' in line: + rfc_header, value = line.split(" ", 1) + header = rfc_header[:-1].lower() + self.info[header] = value.strip() + try: + self.long_desc = "".join([ line for line in body ]) + body.close() + except IOError as msg: + raise GbpError("Failed to read patch header of '%s': %s" % + (self.patch, msg)) + finally: + if os.path.exists(body.name): + os.unlink(body.name) + + def _get_subject_from_filename(self): + """ + Determine the patch's subject based on the it's filename + + >>> p = Patch('debian/patches/foo.patch') + >>> p._get_subject_from_filename() + 'foo' + >>> Patch('foo.patch')._get_subject_from_filename() + 'foo' + >>> Patch('debian/patches/foo.bar')._get_subject_from_filename() + 'foo.bar' + >>> p = Patch('debian/patches/foo') + >>> p._get_subject_from_filename() + 'foo' + + @return: the patch's subject + @rtype: C{str} + """ + subject = os.path.basename(self.path) + # Strip of .diff or .patch from patch name + try: + base, ext = subject.rsplit('.', 1) + if ext in self.patch_exts: + subject = base + except ValueError: + pass # No ext so keep subject as is + return subject + + def _get_info_field(self, key, get_val=None): + """ + Return the key I{key} from the info C{dict} + or use val if I{key} is not a valid key. + + Fill self.info if not already done. + + @param key: key to fetch + @type key: C{str} + @param get_val: alternate value if key is not in info dict + @type get_val: C{str} + """ + if self.info == None: + self._read_info() + + if self.info.has_key(key): + return self.info[key] + else: + return get_val() if get_val else None + + + @property + def subject(self): + """ + The patch's subject, either from the patch header or from the filename. + """ + return self._get_info_field('subject', self._get_subject_from_filename) + + @property + def author(self): + """The patch's author""" + return self._get_info_field('author') + + @property + def email(self): + """The patch author's email address""" + return self._get_info_field('email') + + @property + def date(self): + """The patch's modification time""" + return self._get_info_field('date') + + +class PatchSeries(list): + """ + A series of L{Patch}es as read from a quilt series file). + """ + + @classmethod + def read_series_file(klass, seriesfile): + """Read a series file into L{Patch} objects""" + patch_dir = os.path.dirname(seriesfile) + + if not os.path.exists(seriesfile): + return [] + + try: + s = file(seriesfile) + except Exception, err: + raise GbpError("Cannot open series file: %s" % err) + + queue = klass._read_series(s, patch_dir) + s.close() + return queue + + @classmethod + def _read_series(klass, series, patch_dir): + """ + Read patch series + + >>> PatchSeries._read_series(['a/b', \ + 'a -p1', \ + 'a/b -p2'], '.') # doctest:+NORMALIZE_WHITESPACE + [<gbp.patch_series.Patch path='./a/b' topic='a' >, + <gbp.patch_series.Patch path='./a' strip=1 >, + <gbp.patch_series.Patch path='./a/b' topic='a' strip=2 >] + + >>> PatchSeries._read_series(['# foo', 'a/b', '', '# bar'], '.') + [<gbp.patch_series.Patch path='./a/b' topic='a' >] + + @param series: series of patches in quilt format + @type series: iterable of strings + @param patch_dir: path prefix to prepend to each patch path + @type patch_dir: string + """ + + queue = PatchSeries() + for line in series: + try: + if line[0] in [ '\n', '#' ]: + continue + except IndexError: + continue # ignore empty lines + queue.append(klass._parse_line(line, patch_dir)) + return queue + + @staticmethod + def _get_topic(line): + """ + Get the topic from the patch's path + + >>> PatchSeries._get_topic("a/b c") + 'a' + >>> PatchSeries._get_topic("asdf") + >>> PatchSeries._get_topic("/asdf") + """ + topic = os.path.dirname(line) + if topic in [ '', '/' ]: + topic = None + return topic + + @staticmethod + def _split_strip(line): + """ + Separate the -p<num> option from the patch name + + >>> PatchSeries._split_strip("asdf -p1") + ('asdf', 1) + >>> PatchSeries._split_strip("a/nice/patch") + ('a/nice/patch', None) + >>> PatchSeries._split_strip("asdf foo") + ('asdf foo', None) + """ + patch = line + strip = None + + split = line.rsplit(None, 1) + if len(split) > 1: + m = re.match('-p(?P<level>[0-9]+)', split[1]) + if m: + patch = split[0] + strip = int(m.group('level')) + + return (patch, strip) + + @classmethod + def _parse_line(klass, line, patch_dir): + """ + Parse a single line from a series file + + >>> PatchSeries._parse_line("a/b -p1", '/tmp/patches') + <gbp.patch_series.Patch path='/tmp/patches/a/b' topic='a' strip=1 > + >>> PatchSeries._parse_line("a/b", '.') + <gbp.patch_series.Patch path='./a/b' topic='a' > + """ + line = line.rstrip() + topic = klass._get_topic(line) + (patch, split) = klass._split_strip(line) + return Patch(os.path.join(patch_dir, patch), topic, split) + + |