Move patch.py and rietveld.py from commit-queue.

It will be used for:
- git cl patch so binary files can be patched
- try jobs instead of doing a curl <url> | patch

BUG=
TEST=

Review URL: http://codereview.chromium.org/6792028

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@80355 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/patch.py b/patch.py
new file mode 100644
index 0000000..b22e32e
--- /dev/null
+++ b/patch.py
@@ -0,0 +1,184 @@
+# 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]