You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.4 KiB
Python
185 lines
5.4 KiB
Python
14 years ago
|
# coding=utf8
|
||
|
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
"""Utility functions to handle patches."""
|
||
|
|
||
|
import re
|
||
|
|
||
|
|
||
|
class UnsupportedPatchFormat(Exception):
|
||
|
def __init__(self, filename, status):
|
||
|
super(UnsupportedPatchFormat, self).__init__(filename, status)
|
||
|
self.filename = filename
|
||
|
self.status = status
|
||
|
|
||
|
def __str__(self):
|
||
|
out = 'Can\'t process patch for file %s.' % self.filename
|
||
|
if self.status:
|
||
|
out += '\n%s' % self.status
|
||
|
return out
|
||
|
|
||
|
|
||
|
class FilePatchBase(object):
|
||
|
"""Defines a single file being modified."""
|
||
|
is_delete = False
|
||
|
is_binary = False
|
||
|
|
||
|
def get(self):
|
||
|
raise NotImplementedError('Nothing to grab')
|
||
|
|
||
|
|
||
|
class FilePatchDelete(FilePatchBase):
|
||
|
"""Deletes a file."""
|
||
|
is_delete = True
|
||
|
|
||
|
def __init__(self, filename, is_binary):
|
||
|
super(FilePatchDelete, self).__init__()
|
||
|
self.filename = filename
|
||
|
self.is_binary = is_binary
|
||
|
|
||
|
def get(self):
|
||
|
raise NotImplementedError('Nothing to grab')
|
||
|
|
||
|
|
||
|
class FilePatchBinary(FilePatchBase):
|
||
|
"""Content of a new binary file."""
|
||
|
is_binary = True
|
||
|
|
||
|
def __init__(self, filename, data, svn_properties):
|
||
|
super(FilePatchBinary, self).__init__()
|
||
|
self.filename = filename
|
||
|
self.data = data
|
||
|
self.svn_properties = svn_properties or []
|
||
|
|
||
|
def get(self):
|
||
|
return self.data
|
||
|
|
||
|
|
||
|
class FilePatchDiff(FilePatchBase):
|
||
|
"""Patch for a single file."""
|
||
|
|
||
|
def __init__(self, filename, diff, svn_properties):
|
||
|
super(FilePatchDiff, self).__init__()
|
||
|
self.filename = filename
|
||
|
self.diff = diff
|
||
|
self.svn_properties = svn_properties or []
|
||
|
self.is_git_diff = self._is_git_diff(diff)
|
||
|
if self.is_git_diff:
|
||
|
self.patchlevel = 1
|
||
|
self._verify_git_patch(filename, diff)
|
||
|
assert not svn_properties
|
||
|
else:
|
||
|
self.patchlevel = 0
|
||
|
self._verify_svn_patch(filename, diff)
|
||
|
|
||
|
def get(self):
|
||
|
return self.diff
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_git_diff(diff):
|
||
|
"""Returns True if the diff for a single files was generated with gt.
|
||
|
|
||
|
Expects the following format:
|
||
|
|
||
|
Index: <filename>
|
||
|
diff --git a/<filename> b/<filename>
|
||
|
<filemode changes>
|
||
|
<index>
|
||
|
--- <filename>
|
||
|
+++ <filename>
|
||
|
<hunks>
|
||
|
|
||
|
Index: is a rietveld specific line.
|
||
|
"""
|
||
|
# Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff
|
||
|
# Rename partial change:
|
||
|
# http://codereview.chromium.org/download/issue6250123_3013_6010.diff
|
||
|
# Rename no change:
|
||
|
# http://codereview.chromium.org/download/issue6287022_3001_4010.diff
|
||
|
return any(l.startswith('diff --git') for l in diff.splitlines()[:3])
|
||
|
|
||
|
@staticmethod
|
||
|
def _verify_git_patch(filename, diff):
|
||
|
lines = diff.splitlines()
|
||
|
# First fine the git diff header:
|
||
|
while lines:
|
||
|
line = lines.pop(0)
|
||
|
match = re.match(r'^diff \-\-git a\/(.*?) b\/(.*)$', line)
|
||
|
if match:
|
||
|
a = match.group(1)
|
||
|
b = match.group(2)
|
||
|
if a != filename and a != 'dev/null':
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff input name.')
|
||
|
if b != filename and b != 'dev/null':
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff output name.')
|
||
|
if a == 'dev/null' and b == 'dev/null':
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected /dev/null git diff.')
|
||
|
break
|
||
|
else:
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff; couldn\'t find git header.')
|
||
|
|
||
|
while lines:
|
||
|
line = lines.pop(0)
|
||
|
match = re.match(r'^--- a/(.*)$', line)
|
||
|
if match:
|
||
|
if a != match.group(1):
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff: %s != %s.' % (a, match.group(1)))
|
||
|
match = re.match(r'^\+\+\+ b/(.*)$', lines.pop(0))
|
||
|
if not match:
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff: --- not following +++.')
|
||
|
if b != match.group(1):
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename, 'Unexpected git diff: %s != %s.' % (b, match.group(1)))
|
||
|
break
|
||
|
# Don't fail if the patch header is not found, the diff could be a
|
||
|
# file-mode-change-only diff.
|
||
|
|
||
|
@staticmethod
|
||
|
def _verify_svn_patch(filename, diff):
|
||
|
lines = diff.splitlines()
|
||
|
while lines:
|
||
|
line = lines.pop(0)
|
||
|
match = re.match(r'^--- ([^\t]+).*$', line)
|
||
|
if match:
|
||
|
if match.group(1) not in (filename, '/dev/null'):
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename,
|
||
|
'Unexpected diff: %s != %s.' % (filename, match.group(1)))
|
||
|
# Grab next line.
|
||
|
line2 = lines.pop(0)
|
||
|
match = re.match(r'^\+\+\+ ([^\t]+).*$', line2)
|
||
|
if not match:
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename,
|
||
|
'Unexpected diff: --- not following +++.:\n%s\n%s' % (
|
||
|
line, line2))
|
||
|
if match.group(1) not in (filename, '/dev/null'):
|
||
|
raise UnsupportedPatchFormat(
|
||
|
filename,
|
||
|
'Unexpected diff: %s != %s.' % (filename, match.group(1)))
|
||
|
break
|
||
|
# Don't fail if the patch header is not found, the diff could be a
|
||
|
# svn-property-change-only diff.
|
||
|
|
||
|
|
||
|
class PatchSet(object):
|
||
|
"""A list of FilePatch* objects."""
|
||
|
|
||
|
def __init__(self, patches):
|
||
|
self.patches = patches
|
||
|
|
||
|
def __iter__(self):
|
||
|
for patch in self.patches:
|
||
|
yield patch
|
||
|
|
||
|
@property
|
||
|
def filenames(self):
|
||
|
return [p.filename for p in self.patches]
|