| Index: patch.py
|
| diff --git a/patch.py b/patch.py
|
| index c627f48c85b79a8ab1c65da9b30e654d012d4db1..9c54924df3dc8ea1a192cd9d0a4bc39ca51e763b 100644
|
| --- a/patch.py
|
| +++ b/patch.py
|
| @@ -29,6 +29,7 @@ class FilePatchBase(object):
|
| """
|
| is_delete = False
|
| is_binary = False
|
| + is_new = False
|
|
|
| def __init__(self, filename):
|
| self.filename = None
|
| @@ -44,7 +45,7 @@ class FilePatchBase(object):
|
| if self.filename.startswith(i):
|
| self._fail('Filename can\'t start with \'%s\'.' % i)
|
|
|
| - def get(self):
|
| + def get(self): # pragma: no coverage
|
| raise NotImplementedError('Nothing to grab')
|
|
|
| def set_relpath(self, relpath):
|
| @@ -75,10 +76,11 @@ class FilePatchBinary(FilePatchBase):
|
| """Content of a new binary file."""
|
| is_binary = True
|
|
|
| - def __init__(self, filename, data, svn_properties):
|
| + def __init__(self, filename, data, svn_properties, is_new):
|
| super(FilePatchBinary, self).__init__(filename)
|
| self.data = data
|
| self.svn_properties = svn_properties or []
|
| + self.is_new = is_new
|
|
|
| def get(self):
|
| return self.data
|
| @@ -195,52 +197,86 @@ class FilePatchDiff(FilePatchBase):
|
| if not old or not new:
|
| self._fail('Unexpected git diff; couldn\'t find git header.')
|
|
|
| - # Handle these:
|
| - # new file mode \d{6}
|
| - # rename from <>
|
| - # rename to <>
|
| - # copy from <>
|
| - # copy to <>
|
| + last_line = ''
|
| +
|
| while lines:
|
| - if lines[0].startswith('--- '):
|
| - break
|
| line = lines.pop(0)
|
| - match = re.match(r'^(rename|copy) from (.+)$', line)
|
| - if match:
|
| - if old != match.group(2):
|
| - self._fail('Unexpected git diff input name for %s.' % match.group(1))
|
| - if not lines:
|
| - self._fail('Missing git diff output name for %s.' % match.group(1))
|
| - match = re.match(r'^(rename|copy) to (.+)$', lines.pop(0))
|
| - if not match:
|
| - self._fail('Missing git diff output name for %s.' % match.group(1))
|
| - if new != match.group(2):
|
| - self._fail('Unexpected git diff output name for %s.' % match.group(1))
|
| - continue
|
| + # TODO(maruel): old should be replace with self.source_file
|
| + # TODO(maruel): new == self.filename and remove new
|
| + self._verify_git_header_process_line(lines, line, last_line, old, new)
|
| + last_line = line
|
|
|
| - match = re.match(r'^new file mode (\d{6})$', line)
|
| - if match:
|
| - mode = match.group(1)
|
| - # Only look at owner ACL for executable.
|
| - if bool(int(mode[4]) & 4):
|
| - self.svn_properties.append(('svn:executable', '*'))
|
| + # Cheap check to make sure the file name is at least mentioned in the
|
| + # 'diff' header. That the only remaining invariant.
|
| + if not self.filename in self.diff_header:
|
| + self._fail('Diff seems corrupted.')
|
|
|
| - # Handle ---/+++
|
| - while lines:
|
| - match = re.match(r'^--- (.*)$', lines.pop(0))
|
| - if not match:
|
| - continue
|
| + def _verify_git_header_process_line(self, lines, line, last_line, old, new):
|
| + """Processes a single line of the header.
|
| +
|
| + Returns True if it should continue looping.
|
| + """
|
| + # Handle these:
|
| + # rename from <>
|
| + # copy from <>
|
| + match = re.match(r'^(rename|copy) from (.+)$', line)
|
| + if match:
|
| + if old != match.group(2):
|
| + self._fail('Unexpected git diff input name for line %s.' % line)
|
| + if not lines or not lines[0].startswith('%s to ' % match.group(1)):
|
| + self._fail(
|
| + 'Confused %s from/to git diff for line %s.' %
|
| + (match.group(1), line))
|
| + return
|
| +
|
| + # Handle these:
|
| + # rename to <>
|
| + # copy to <>
|
| + match = re.match(r'^(rename|copy) to (.+)$', line)
|
| + if match:
|
| + if new != match.group(2):
|
| + self._fail('Unexpected git diff output name for line %s.' % line)
|
| + if not last_line.startswith('%s from ' % match.group(1)):
|
| + self._fail(
|
| + 'Confused %s from/to git diff for line %s.' %
|
| + (match.group(1), line))
|
| + return
|
| +
|
| + # Handle "new file mode \d{6}"
|
| + match = re.match(r'^new file mode (\d{6})$', line)
|
| + if match:
|
| + mode = match.group(1)
|
| + # Only look at owner ACL for executable.
|
| + if bool(int(mode[4]) & 4):
|
| + self.svn_properties.append(('svn:executable', '*'))
|
| +
|
| + # Handle "--- "
|
| + match = re.match(r'^--- (.*)$', line)
|
| + if match:
|
| + if last_line[:3] in ('---', '+++'):
|
| + self._fail('--- and +++ are reversed')
|
| + self.is_new = match.group(1) == '/dev/null'
|
| + # TODO(maruel): Use self.source_file.
|
| if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null':
|
| self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
|
| - if not lines:
|
| + if not lines or not lines[0].startswith('+++'):
|
| self._fail('Missing git diff output name.')
|
| - match = re.match(r'^\+\+\+ (.*)$', lines.pop(0))
|
| - if not match:
|
| + return
|
| +
|
| + # Handle "+++ "
|
| + match = re.match(r'^\+\+\+ (.*)$', line)
|
| + if match:
|
| + if not last_line.startswith('---'):
|
| self._fail('Unexpected git diff: --- not following +++.')
|
| + # TODO(maruel): new == self.filename.
|
| if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1):
|
| + # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete ==
|
| + # True.
|
| self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1)))
|
| - assert not lines, '_split_header() is broken'
|
| - break
|
| + if lines:
|
| + self._fail('Crap after +++')
|
| + # We're done.
|
| + return
|
|
|
| def _verify_svn_header(self):
|
| """Sanity checks the header.
|
| @@ -250,30 +286,52 @@ class FilePatchDiff(FilePatchBase):
|
| localized.
|
| """
|
| lines = self.diff_header.splitlines()
|
| + last_line = ''
|
| +
|
| while lines:
|
| - match = re.match(r'^--- ([^\t]+).*$', lines.pop(0))
|
| - if not match:
|
| - continue
|
| - # For copy and renames, it's possible that the -- line doesn't match +++,
|
| - # so don't check match.group(1) to match self.filename or '/dev/null', it
|
| - # can be anything else.
|
| + line = lines.pop(0)
|
| + self._verify_svn_header_process_line(lines, line, last_line)
|
| + last_line = line
|
| +
|
| + # Cheap check to make sure the file name is at least mentioned in the
|
| + # 'diff' header. That the only remaining invariant.
|
| + if not self.filename in self.diff_header:
|
| + self._fail('Diff seems corrupted.')
|
| +
|
| + def _verify_svn_header_process_line(self, lines, line, last_line):
|
| + """Processes a single line of the header.
|
| +
|
| + Returns True if it should continue looping.
|
| + """
|
| + match = re.match(r'^--- ([^\t]+).*$', line)
|
| + if match:
|
| + if last_line[:3] in ('---', '+++'):
|
| + self._fail('--- and +++ are reversed')
|
| + self.is_new = match.group(1) == '/dev/null'
|
| + # For copy and renames, it's possible that the -- line doesn't match
|
| + # +++, so don't check match.group(1) to match self.filename or
|
| + # '/dev/null', it can be anything else.
|
| # TODO(maruel): Handle rename/copy explicitly.
|
| - # if match.group(1) not in (self.filename, '/dev/null'):
|
| + # if (self.mangle(match.group(1)) != self.filename and
|
| + # match.group(1) != '/dev/null'):
|
| # self.source_file = match.group(1)
|
| - if not lines:
|
| + if not lines or not lines[0].startswith('+++'):
|
| self._fail('Nothing after header.')
|
| - match = re.match(r'^\+\+\+ ([^\t]+).*$', lines.pop(0))
|
| - if not match:
|
| + return
|
| +
|
| + match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
|
| + if match:
|
| + if not last_line.startswith('---'):
|
| self._fail('Unexpected diff: --- not following +++.')
|
| - if match.group(1) not in (self.filename, '/dev/null'):
|
| + if (self.mangle(match.group(1)) != self.filename and
|
| + match.group(1) != '/dev/null'):
|
| + # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete ==
|
| + # True.
|
| self._fail('Unexpected diff: %s.' % match.group(1))
|
| - assert not lines, '_split_header() is broken'
|
| - break
|
| - else:
|
| - # Cheap check to make sure the file name is at least mentioned in the
|
| - # 'diff' header. That the only remaining invariant.
|
| - if not self.filename in self.diff_header:
|
| - self._fail('Diff seems corrupted.')
|
| + if lines:
|
| + self._fail('Crap after +++')
|
| + # We're done.
|
| + return
|
|
|
|
|
| class PatchSet(object):
|
|
|