123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- # vim: set fileencoding=utf-8 :
- #
- # (C) 2011,2015 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, please see
- # <http://www.gnu.org/licenses/>
- """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 is not 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 2>/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 its 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'
- >>> Patch('0123-foo.patch')._get_subject_from_filename()
- 'foo'
- >>> Patch('0123.patch')._get_subject_from_filename()
- '0123'
- >>> Patch('0123-foo-0123.patch')._get_subject_from_filename()
- 'foo-0123'
- @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.lstrip('0123456789-') or 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 is None:
- self._read_info()
- if key in self.info:
- 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 = open(seriesfile)
- except Exception as 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)
|