summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReinhard Tartler <siretart@tauware.de>2017-08-30 21:53:57 -0400
committerReinhard Tartler <siretart@tauware.de>2017-08-31 08:14:12 -0400
commit9a0b7980ed88c07d291130e57a38bbada705a5e7 (patch)
tree25dd27986240530451bf5dd61660cc61d42543f7
-rw-r--r--LICENSE22
-rw-r--r--MANIFEST.in1
-rw-r--r--PKG-INFO173
-rw-r--r--README.rst145
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control31
-rw-r--r--debian/copyright31
-rw-r--r--debian/python-whatthepatch.lintian-overrides2
-rwxr-xr-xdebian/rules18
-rw-r--r--debian/source/format1
-rw-r--r--debian/source/options1
-rw-r--r--debian/watch6
-rw-r--r--setup.cfg8
-rw-r--r--setup.py48
-rw-r--r--whatthepatch.egg-info/PKG-INFO173
-rw-r--r--whatthepatch.egg-info/SOURCES.txt14
-rw-r--r--whatthepatch.egg-info/dependency_links.txt1
-rw-r--r--whatthepatch.egg-info/pbr.json1
-rw-r--r--whatthepatch.egg-info/top_level.txt1
-rw-r--r--whatthepatch/__init__.py4
-rw-r--r--whatthepatch/apply.py107
-rw-r--r--whatthepatch/patch.py839
-rw-r--r--whatthepatch/snippets.py87
24 files changed, 1720 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3694d4b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2012 -- 2015 Christopher S. Corley
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0c73842
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst LICENSE
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..af243b5
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,173 @@
+Metadata-Version: 1.1
+Name: whatthepatch
+Version: 0.0.5
+Summary: A patch parsing library.
+Home-page: https://github.com/cscorley/whatthepatch
+Author: Christopher S. Corley
+Author-email: cscorley@crimson.ua.edu
+License: MIT
+Description: What The Patch!?
+ ================
+
+ .. image:: https://travis-ci.org/cscorley/whatthepatch.svg?style=flat
+ :target: https://travis-ci.org/cscorley/whatthepatch
+
+ What The Patch!? is a library for parsing patch files. Its only purpose is to
+ read a patch file and get it into some usable form by other programs.
+
+ Features
+ ---------
+
+ - Parsing of almost all ``diff`` formats (except forwarded ed):
+
+ - normal (default, --normal)
+ - copied context (-c, --context)
+ - unified context (-u, --unified)
+ - ed script (-e, --ed)
+ - rcs ed script (-n, --rcs)
+
+ - Parsing of several SCM patches:
+
+ - CVS
+ - SVN
+ - Git
+
+ Installation
+ ------------
+
+ To install What The Patch!?, simply:
+
+ .. code-block:: bash
+
+ $ pip install whatthepatch
+
+ Usage
+ =====
+
+ Let us say we have a patch file containing some changes, aptly named
+ 'somechanges.patch':
+
+ .. code-block:: diff
+
+ --- lao 2012-12-26 23:16:54.000000000 -0600
+ +++ tzu 2012-12-26 23:16:50.000000000 -0600
+ @@ -1,7 +1,6 @@
+ -The Way that can be told of is not the eternal Way;
+ -The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ -The Named is the mother of all things.
+ +The named is the mother of all things.
+ +
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ @@ -9,3 +8,6 @@
+ The two are the same,
+ But after they are produced,
+ they have different names.
+ +They both may be called deep and profound.
+ +Deeper and more profound,
+ +The door of all subtleties!
+
+
+ Parsing
+ -------
+
+ Here is how we would use What The Patch!? in Python to get the changeset for
+ each diff in the patch:
+
+ .. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> for diff in whatthepatch.parse_patch(text):
+ ... print(diff)
+ ...
+ diff(header=header(
+ index_path=None,
+ old_path='lao',
+ old_version='2012-12-26 23:16:54.000000000 -0600',
+ new_path='tzu',
+ new_version='2012-12-26 23:16:50.000000000 -0600'
+ ),
+ changes=[
+ (1, None, 'The Way that can be told of is not the eternal Way;'),
+ (2, None, 'The name that can be named is not the eternal name.'),
+ (3, 1, 'The Nameless is the origin of Heaven and Earth;'),
+ (4, None, 'The Named is the mother of all things.'),
+ (None, 2, 'The named is the mother of all things.'),
+ (None, 3, ''),
+ (5, 4, 'Therefore let there always be non-being,'),
+ (6, 5, ' so we may see their subtlety,'),
+ (7, 6, 'And let there always be being,'),
+ (9, 8, 'The two are the same,'),
+ (10, 9, 'But after they are produced,'),
+ (11, 10, ' they have different names.'),
+ (None, 11, 'They both may be called deep and profound.'),
+ (None, 12, 'Deeper and more profound,'),
+ (None, 13, 'The door of all subtleties!')
+ ]
+ )
+
+ *Edited to show structure of the results*
+
+ The changes are listed as they are in the patch, but instead of the +/- syntax
+ of the patch, we get a tuple of two numbers and the text of the line.
+ What these numbers indicate are as follows:
+
+ #. ``( 1, None, ... )`` indicates line 1 of the file lao was **removed**.
+ #. ``( None, 2, ... )`` indicates line 2 of the file tzu was **inserted**.
+ #. ``( 5, 4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**.
+
+ Please note that not all patch formats provide the actual lines modified, so some
+ results will have the text portion of the tuple set to ``None``.
+
+ Applying
+ --------
+
+ To apply a diff to some lines of text, first read the patch and parse it.
+
+ .. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> with open('lao') as f:
+ ... lao = f.read()
+ ...
+ >>> diff = [x for x in whatthepatch.parse_patch(text)]
+ >>> diff = diff[0]
+ >>> tzu = whatthepatch.apply_diff(diff, lao)
+
+
+ Contribute
+ ==========
+
+ #. Fork this repository
+ #. Create a new branch to work on
+ #. Commit your tests and/or changes
+ #. Push and create a pull request here!
+
+
+Keywords: patch,diff,parser
+Platform: UNKNOWN
+Classifier: Operating System :: OS Independent
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Science/Research
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Version Control
+Classifier: Topic :: Text Processing
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3565fbc
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,145 @@
+What The Patch!?
+================
+
+.. image:: https://travis-ci.org/cscorley/whatthepatch.svg?style=flat
+ :target: https://travis-ci.org/cscorley/whatthepatch
+
+What The Patch!? is a library for parsing patch files. Its only purpose is to
+read a patch file and get it into some usable form by other programs.
+
+Features
+---------
+
+- Parsing of almost all ``diff`` formats (except forwarded ed):
+
+ - normal (default, --normal)
+ - copied context (-c, --context)
+ - unified context (-u, --unified)
+ - ed script (-e, --ed)
+ - rcs ed script (-n, --rcs)
+
+- Parsing of several SCM patches:
+
+ - CVS
+ - SVN
+ - Git
+
+Installation
+------------
+
+To install What The Patch!?, simply:
+
+.. code-block:: bash
+
+ $ pip install whatthepatch
+
+Usage
+=====
+
+Let us say we have a patch file containing some changes, aptly named
+'somechanges.patch':
+
+.. code-block:: diff
+
+ --- lao 2012-12-26 23:16:54.000000000 -0600
+ +++ tzu 2012-12-26 23:16:50.000000000 -0600
+ @@ -1,7 +1,6 @@
+ -The Way that can be told of is not the eternal Way;
+ -The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ -The Named is the mother of all things.
+ +The named is the mother of all things.
+ +
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ @@ -9,3 +8,6 @@
+ The two are the same,
+ But after they are produced,
+ they have different names.
+ +They both may be called deep and profound.
+ +Deeper and more profound,
+ +The door of all subtleties!
+
+
+Parsing
+-------
+
+Here is how we would use What The Patch!? in Python to get the changeset for
+each diff in the patch:
+
+.. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> for diff in whatthepatch.parse_patch(text):
+ ... print(diff)
+ ...
+ diff(header=header(
+ index_path=None,
+ old_path='lao',
+ old_version='2012-12-26 23:16:54.000000000 -0600',
+ new_path='tzu',
+ new_version='2012-12-26 23:16:50.000000000 -0600'
+ ),
+ changes=[
+ (1, None, 'The Way that can be told of is not the eternal Way;'),
+ (2, None, 'The name that can be named is not the eternal name.'),
+ (3, 1, 'The Nameless is the origin of Heaven and Earth;'),
+ (4, None, 'The Named is the mother of all things.'),
+ (None, 2, 'The named is the mother of all things.'),
+ (None, 3, ''),
+ (5, 4, 'Therefore let there always be non-being,'),
+ (6, 5, ' so we may see their subtlety,'),
+ (7, 6, 'And let there always be being,'),
+ (9, 8, 'The two are the same,'),
+ (10, 9, 'But after they are produced,'),
+ (11, 10, ' they have different names.'),
+ (None, 11, 'They both may be called deep and profound.'),
+ (None, 12, 'Deeper and more profound,'),
+ (None, 13, 'The door of all subtleties!')
+ ]
+ )
+
+*Edited to show structure of the results*
+
+The changes are listed as they are in the patch, but instead of the +/- syntax
+of the patch, we get a tuple of two numbers and the text of the line.
+What these numbers indicate are as follows:
+
+#. ``( 1, None, ... )`` indicates line 1 of the file lao was **removed**.
+#. ``( None, 2, ... )`` indicates line 2 of the file tzu was **inserted**.
+#. ``( 5, 4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**.
+
+Please note that not all patch formats provide the actual lines modified, so some
+results will have the text portion of the tuple set to ``None``.
+
+Applying
+--------
+
+To apply a diff to some lines of text, first read the patch and parse it.
+
+.. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> with open('lao') as f:
+ ... lao = f.read()
+ ...
+ >>> diff = [x for x in whatthepatch.parse_patch(text)]
+ >>> diff = diff[0]
+ >>> tzu = whatthepatch.apply_diff(diff, lao)
+
+
+Contribute
+==========
+
+#. Fork this repository
+#. Create a new branch to work on
+#. Commit your tests and/or changes
+#. Push and create a pull request here!
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..4118328
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+whatthepatch (0.0.5-1) unstable; urgency=medium
+
+ * Initial release (Closes: #873777)
+
+ -- Reinhard Tartler <siretart@tauware.de> Wed, 30 Aug 2017 21:38:06 -0400
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..3406549
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,31 @@
+Source: whatthepatch
+Section: python
+Priority: extra
+Maintainer: Reinhard Tartler <siretart@tauware.de>
+Build-Depends: debhelper (>= 9), dh-python, python-all, python-setuptools, python3-all, python3-setuptools
+Standards-Version: 4.0.0
+Homepage: https://github.com/cscorley/whatthepatch
+X-Python-Version: >= 2.6
+X-Python3-Version: >= 3.2
+Testsuite: autopkgtest-pkg-python
+
+Package: python-whatthepatch
+Architecture: all
+Depends: ${python:Depends}, ${misc:Depends}
+Description: Library for parsing patch files (Python 2)
+ What The Patch!? is a library for parsing patch files. Its only purpose
+ is to read a patch file and get it into some usable form by other
+ programs.
+ .
+ This package installs the library for Python 2.
+
+Package: python3-whatthepatch
+Architecture: all
+Depends: ${python3:Depends}, ${misc:Depends}
+Suggests: python-whatthepatch-doc
+Description: Library for parsing patch files(Python 3)
+ What The Patch!? is a library for parsing patch files. Its only purpose
+ is to read a patch file and get it into some usable form by other
+ programs.
+ .
+ This package installs the library for Python 3.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..46d5904
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,31 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: whatthepatch
+Source: https://github.com/cscorley/whatthepatch
+
+Files: *
+Copyright: 2012 2015 Christopher S. Corley
+License: MIT
+
+Files: debian/*
+Copyright: 2017 Reinhard Tartler <siretart@tauware.de>
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/debian/python-whatthepatch.lintian-overrides b/debian/python-whatthepatch.lintian-overrides
new file mode 100644
index 0000000..bb13b12
--- /dev/null
+++ b/debian/python-whatthepatch.lintian-overrides
@@ -0,0 +1,2 @@
+# the only dependency, undertaker, has not been ported to python3 yet
+python-whatthepatch: new-package-should-not-package-python2-module \ No newline at end of file
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..6f0e410
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,18 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#export DH_VERBOSE = 1
+
+export PYBUILD_NAME=whatthepatch
+
+%:
+ dh $@ --with python2,python3 --buildsystem=pybuild
+
+
+# If you need to rebuild the Sphinx documentation
+# Add spinxdoc to the dh --with line
+#override_dh_auto_build:
+# dh_auto_build
+# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator
+# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator
+
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/source/options b/debian/source/options
new file mode 100644
index 0000000..cb61fa5
--- /dev/null
+++ b/debian/source/options
@@ -0,0 +1 @@
+extend-diff-ignore = "^[^/]*[.]egg-info/"
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..8905594
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,6 @@
+# Compulsory line, this is a version 4 file
+version=4
+
+# PyPI
+https://pypi.debian.net/whatthepatch/whatthepatch-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6f08d0e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..97696d3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+
+with open('README.rst') as f:
+ readme = f.read()
+
+setup(
+ name='whatthepatch',
+ version='0.0.5',
+ description='A patch parsing library.',
+ long_description=readme,
+ author='Christopher S. Corley',
+ author_email='cscorley@crimson.ua.edu',
+ url='https://github.com/cscorley/whatthepatch',
+ license='MIT',
+ packages=['whatthepatch'],
+ include_package_data=True,
+ keywords=[
+ "patch",
+ "diff",
+ "parser",
+ ],
+ classifiers=[
+ "Operating System :: OS Independent",
+ "License :: OSI Approved :: MIT License",
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Software Development :: Version Control",
+ "Topic :: Text Processing",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ ],
+)
+
diff --git a/whatthepatch.egg-info/PKG-INFO b/whatthepatch.egg-info/PKG-INFO
new file mode 100644
index 0000000..af243b5
--- /dev/null
+++ b/whatthepatch.egg-info/PKG-INFO
@@ -0,0 +1,173 @@
+Metadata-Version: 1.1
+Name: whatthepatch
+Version: 0.0.5
+Summary: A patch parsing library.
+Home-page: https://github.com/cscorley/whatthepatch
+Author: Christopher S. Corley
+Author-email: cscorley@crimson.ua.edu
+License: MIT
+Description: What The Patch!?
+ ================
+
+ .. image:: https://travis-ci.org/cscorley/whatthepatch.svg?style=flat
+ :target: https://travis-ci.org/cscorley/whatthepatch
+
+ What The Patch!? is a library for parsing patch files. Its only purpose is to
+ read a patch file and get it into some usable form by other programs.
+
+ Features
+ ---------
+
+ - Parsing of almost all ``diff`` formats (except forwarded ed):
+
+ - normal (default, --normal)
+ - copied context (-c, --context)
+ - unified context (-u, --unified)
+ - ed script (-e, --ed)
+ - rcs ed script (-n, --rcs)
+
+ - Parsing of several SCM patches:
+
+ - CVS
+ - SVN
+ - Git
+
+ Installation
+ ------------
+
+ To install What The Patch!?, simply:
+
+ .. code-block:: bash
+
+ $ pip install whatthepatch
+
+ Usage
+ =====
+
+ Let us say we have a patch file containing some changes, aptly named
+ 'somechanges.patch':
+
+ .. code-block:: diff
+
+ --- lao 2012-12-26 23:16:54.000000000 -0600
+ +++ tzu 2012-12-26 23:16:50.000000000 -0600
+ @@ -1,7 +1,6 @@
+ -The Way that can be told of is not the eternal Way;
+ -The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ -The Named is the mother of all things.
+ +The named is the mother of all things.
+ +
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ @@ -9,3 +8,6 @@
+ The two are the same,
+ But after they are produced,
+ they have different names.
+ +They both may be called deep and profound.
+ +Deeper and more profound,
+ +The door of all subtleties!
+
+
+ Parsing
+ -------
+
+ Here is how we would use What The Patch!? in Python to get the changeset for
+ each diff in the patch:
+
+ .. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> for diff in whatthepatch.parse_patch(text):
+ ... print(diff)
+ ...
+ diff(header=header(
+ index_path=None,
+ old_path='lao',
+ old_version='2012-12-26 23:16:54.000000000 -0600',
+ new_path='tzu',
+ new_version='2012-12-26 23:16:50.000000000 -0600'
+ ),
+ changes=[
+ (1, None, 'The Way that can be told of is not the eternal Way;'),
+ (2, None, 'The name that can be named is not the eternal name.'),
+ (3, 1, 'The Nameless is the origin of Heaven and Earth;'),
+ (4, None, 'The Named is the mother of all things.'),
+ (None, 2, 'The named is the mother of all things.'),
+ (None, 3, ''),
+ (5, 4, 'Therefore let there always be non-being,'),
+ (6, 5, ' so we may see their subtlety,'),
+ (7, 6, 'And let there always be being,'),
+ (9, 8, 'The two are the same,'),
+ (10, 9, 'But after they are produced,'),
+ (11, 10, ' they have different names.'),
+ (None, 11, 'They both may be called deep and profound.'),
+ (None, 12, 'Deeper and more profound,'),
+ (None, 13, 'The door of all subtleties!')
+ ]
+ )
+
+ *Edited to show structure of the results*
+
+ The changes are listed as they are in the patch, but instead of the +/- syntax
+ of the patch, we get a tuple of two numbers and the text of the line.
+ What these numbers indicate are as follows:
+
+ #. ``( 1, None, ... )`` indicates line 1 of the file lao was **removed**.
+ #. ``( None, 2, ... )`` indicates line 2 of the file tzu was **inserted**.
+ #. ``( 5, 4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**.
+
+ Please note that not all patch formats provide the actual lines modified, so some
+ results will have the text portion of the tuple set to ``None``.
+
+ Applying
+ --------
+
+ To apply a diff to some lines of text, first read the patch and parse it.
+
+ .. code-block:: python
+
+ >>> import whatthepatch
+ >>> with open('somechanges.patch') as f:
+ ... text = f.read()
+ ...
+ >>> with open('lao') as f:
+ ... lao = f.read()
+ ...
+ >>> diff = [x for x in whatthepatch.parse_patch(text)]
+ >>> diff = diff[0]
+ >>> tzu = whatthepatch.apply_diff(diff, lao)
+
+
+ Contribute
+ ==========
+
+ #. Fork this repository
+ #. Create a new branch to work on
+ #. Commit your tests and/or changes
+ #. Push and create a pull request here!
+
+
+Keywords: patch,diff,parser
+Platform: UNKNOWN
+Classifier: Operating System :: OS Independent
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Science/Research
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Version Control
+Classifier: Topic :: Text Processing
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
diff --git a/whatthepatch.egg-info/SOURCES.txt b/whatthepatch.egg-info/SOURCES.txt
new file mode 100644
index 0000000..1a6fb34
--- /dev/null
+++ b/whatthepatch.egg-info/SOURCES.txt
@@ -0,0 +1,14 @@
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+whatthepatch/__init__.py
+whatthepatch/apply.py
+whatthepatch/patch.py
+whatthepatch/snippets.py
+whatthepatch.egg-info/PKG-INFO
+whatthepatch.egg-info/SOURCES.txt
+whatthepatch.egg-info/dependency_links.txt
+whatthepatch.egg-info/pbr.json
+whatthepatch.egg-info/top_level.txt \ No newline at end of file
diff --git a/whatthepatch.egg-info/dependency_links.txt b/whatthepatch.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/whatthepatch.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/whatthepatch.egg-info/pbr.json b/whatthepatch.egg-info/pbr.json
new file mode 100644
index 0000000..ae7b94f
--- /dev/null
+++ b/whatthepatch.egg-info/pbr.json
@@ -0,0 +1 @@
+{"is_release": false, "git_version": "39c8edd"} \ No newline at end of file
diff --git a/whatthepatch.egg-info/top_level.txt b/whatthepatch.egg-info/top_level.txt
new file mode 100644
index 0000000..30630f2
--- /dev/null
+++ b/whatthepatch.egg-info/top_level.txt
@@ -0,0 +1 @@
+whatthepatch
diff --git a/whatthepatch/__init__.py b/whatthepatch/__init__.py
new file mode 100644
index 0000000..d9e8088
--- /dev/null
+++ b/whatthepatch/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from .patch import parse_patch
+from .apply import apply_diff
diff --git a/whatthepatch/apply.py b/whatthepatch/apply.py
new file mode 100644
index 0000000..ed74467
--- /dev/null
+++ b/whatthepatch/apply.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+import re
+import subprocess
+
+from . import patch
+from .snippets import which, remove
+
+def apply_patch(diffs):
+ """ Not ready for use yet """
+ pass
+
+ if isinstance(diffs, patch.diff):
+ diffs = [diffs]
+
+ for diff in diffs:
+ if diff.header.old_path == '/dev/null':
+ text = []
+ else:
+ with open(diff.header.old_path) as f:
+ text = f.read()
+
+ new_text = apply_diff(diff, text)
+ with open(diff.header.new_path, 'w') as f:
+ f.write(new_text)
+
+def apply_diff(diff, text, use_patch=False):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = list(text)
+
+ if use_patch:
+ # call out to patch program
+ patchexec = which('patch')
+ assert patchexec # patch program does not exist
+
+ filepath = '/tmp/wtp-' + str(hash(diff.header))
+ oldfilepath = filepath + '.old'
+ newfilepath = filepath + '.new'
+ rejfilepath = filepath + '.rej'
+ patchfilepath = filepath + '.patch'
+ with open(oldfilepath, 'w') as f:
+ f.write('\n'.join(lines) + '\n')
+
+ with open(patchfilepath, 'w') as f:
+ f.write(diff.text)
+
+ args = [patchexec,
+ '--quiet',
+ '-o', newfilepath,
+ '-i', patchfilepath,
+ '-r', rejfilepath,
+ oldfilepath
+ ]
+ ret = subprocess.call(args)
+
+
+ with open(newfilepath) as f:
+ lines = f.read().splitlines()
+
+ try:
+ with open(rejfilepath) as f:
+ rejlines = f.read().splitlines()
+ except IOError:
+ rejlines = None
+
+ remove(oldfilepath)
+ remove(newfilepath)
+ remove(rejfilepath)
+ remove(patchfilepath)
+
+ # do this last to ensure files get cleaned up
+ assert ret == 0 # patch return code is success
+
+ return lines, rejlines
+
+ # check that the source text matches the context of the diff
+ for old, new, line in diff.changes:
+ # might have to check for line is None here for ed scripts
+ if old is not None and line is not None:
+ assert len(lines) >= old
+ assert lines[old-1] == line
+
+ # for calculating the old line
+ r = 0
+ i = 0
+
+ for old, new, line in diff.changes:
+ if old is not None and new is None:
+ del lines[old-1-r+i]
+ r += 1
+ elif old is None and new is not None:
+ lines.insert(new-1, line)
+ i += 1
+ elif old is not None and new is not None:
+ # are we crazy?
+ #assert new == old - r + i
+
+ # Sometimes, people remove hunks from patches, making these
+ # numbers completely unreliable. Because they're jerks.
+ pass
+
+ return lines
+
+
+
diff --git a/whatthepatch/patch.py b/whatthepatch/patch.py
new file mode 100644
index 0000000..1a06836
--- /dev/null
+++ b/whatthepatch/patch.py
@@ -0,0 +1,839 @@
+# -*- coding: utf-8 -*-
+
+import re
+from collections import namedtuple
+
+from .snippets import split_by_regex, findall_regex
+
+header = namedtuple('header',
+ 'index_path old_path old_version new_path new_version')
+
+diffobj = namedtuple('diff', 'header changes text')
+
+file_timestamp_str = '(.+?)(?:\t|:| +)(.*)'
+# .+? was previously [^:\t\n\r\f\v]+
+
+# general diff regex
+diffcmd_header = re.compile('^diff.* (.+) (.+)$')
+unified_header_index = re.compile('^Index: (.+)$')
+unified_header_old_line = re.compile('^--- ' + file_timestamp_str + '$')
+unified_header_new_line = re.compile('^\+\+\+ ' + file_timestamp_str + '$')
+unified_hunk_start = re.compile('^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$')
+unified_change = re.compile('^([-+ ])(.*)$')
+
+context_header_old_line = re.compile('^\*\*\* ' + file_timestamp_str + '$')
+context_header_new_line = re.compile('^--- ' + file_timestamp_str + '$')
+context_hunk_start = re.compile('^\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*$')
+context_hunk_old = re.compile('^\*\*\* (\d+),?(\d*) \*\*\*\*$')
+context_hunk_new = re.compile('^--- (\d+),?(\d*) ----$')
+context_change = re.compile('^([-+ !]) (.*)$')
+
+ed_hunk_start = re.compile('^(\d+),?(\d*)([acd])$')
+ed_hunk_end = re.compile('^.$')
+# much like forward ed, but no 'c' type
+rcs_ed_hunk_start = re.compile('^([ad])(\d+) ?(\d*)$')
+
+default_hunk_start = re.compile('^(\d+),?(\d*)([acd])(\d+),?(\d*)$')
+default_hunk_mid = re.compile('^---$')
+default_change = re.compile('^([><]) (.*)$')
+
+# Headers
+
+# git has a special index header and no end part
+git_diffcmd_header = re.compile('^diff --git a/(.+) b/(.+)$')
+git_header_index = re.compile('^index ([a-f0-9]+)..([a-f0-9]+) ?(\d*)$')
+git_header_old_line = re.compile('^--- (.+)$')
+git_header_new_line = re.compile('^\+\+\+ (.+)$')
+git_header_file_mode = re.compile('^(new|deleted) file mode \d{6}$')
+git_header_binary_file = re.compile('^Binary files (.+) and (.+) differ')
+
+bzr_header_index = re.compile("=== (.+)")
+bzr_header_old_line = unified_header_old_line
+bzr_header_new_line = unified_header_new_line
+
+svn_header_index = unified_header_index
+svn_header_timestamp_version = re.compile('\((?:working copy|revision (\d+))\)')
+svn_header_timestamp = re.compile('.*(\(.*\))$')
+
+cvs_header_index = unified_header_index
+cvs_header_rcs = re.compile('^RCS file: (.+)(?:,\w{1}$|$)')
+cvs_header_timestamp = re.compile('(.+)\t([\d.]+)')
+cvs_header_timestamp_colon = re.compile(':([\d.]+)\t(.+)')
+old_cvs_diffcmd_header = re.compile('^diff.* (.+):(.*) (.+):(.*)$')
+
+
+def parse_patch(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ # maybe use this to nuke all of those line endings?
+ #lines = [x.splitlines()[0] for x in lines]
+ lines = [x if len(x) == 0 else x.splitlines()[0] for x in lines]
+
+ check = [
+ unified_header_index,
+ diffcmd_header,
+ cvs_header_rcs,
+ git_header_index,
+ context_header_old_line,
+ unified_header_old_line,
+ ]
+
+ for c in check:
+ diffs = split_by_regex(lines, c)
+ if len(diffs) > 1:
+ break
+
+ for diff in diffs:
+ difftext = '\n'.join(diff) + '\n'
+ h = parse_header(diff)
+ d = parse_diff(diff)
+ if h or d:
+ yield diffobj(header=h, changes=d, text=difftext)
+
+def parse_header(text):
+ h = parse_scm_header(text)
+ if h is None:
+ h = parse_diff_header(text)
+ return h
+
+def parse_scm_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ check = [
+ (git_header_index, parse_git_header),
+ (old_cvs_diffcmd_header, parse_cvs_header),
+ (cvs_header_rcs, parse_cvs_header),
+ (svn_header_index, parse_svn_header),
+ ]
+
+ for regex, parser in check:
+ diffs = findall_regex(lines, regex)
+ if len(diffs) > 0:
+ git_opt = findall_regex(lines, git_diffcmd_header)
+ if len(git_opt) > 0:
+ res = parser(lines)
+ if res:
+ old_path = res.old_path
+ new_path = res.new_path
+ if old_path.startswith('a/'):
+ old_path = old_path[2:]
+
+ if new_path.startswith('b/'):
+ new_path = new_path[2:]
+
+ return header(
+ index_path=res.index_path,
+ old_path = old_path,
+ old_version = res.old_version,
+ new_path = new_path,
+ new_version = res.new_version
+ )
+ else:
+ res = parser(lines)
+
+ return res
+
+ return None
+
+def parse_diff_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ check = [
+ (unified_header_new_line, parse_unified_header),
+ (context_header_old_line, parse_context_header),
+ (diffcmd_header, parse_diffcmd_header),
+ # TODO:
+ # git_header can handle version-less unified headers, but
+ # will trim a/ and b/ in the paths if they exist...
+ (git_header_new_line, parse_git_header),
+ ]
+
+ for regex, parser in check:
+ diffs = findall_regex(lines, regex)
+ if len(diffs) > 0:
+ return parser(lines)
+
+ return None # no header?
+
+
+def parse_diff(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ check = [
+ (unified_hunk_start, parse_unified_diff),
+ (context_hunk_start, parse_context_diff),
+ (default_hunk_start, parse_default_diff),
+ (ed_hunk_start, parse_ed_diff),
+ (rcs_ed_hunk_start, parse_rcs_ed_diff),
+ ]
+
+ for hunk, parser in check:
+ diffs = findall_regex(lines, hunk)
+ if len(diffs) > 0:
+ return parser(lines)
+
+def parse_git_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ # headers = findall_regex(lines, git_header_old_line)
+ # if len(headers) == 0:
+ # return None
+
+ over = nver = old_path = new_path = None
+ while len(lines) > 1:
+ g = git_header_index.match(lines[0])
+ # git always has it's own special headers
+ o = git_header_old_line.match(lines[0])
+ del lines[0]
+ if g:
+ over = g.group(1)
+ nver = g.group(2)
+ g = None
+ if o:
+ n = git_header_new_line.match(lines[0])
+ del lines[0]
+ if n:
+ old_path = o.group(1)
+ new_path = n.group(1)
+ binary = git_header_binary_file.match(lines[0])
+ if binary:
+ old_path = binary.group(1)
+ new_path = binary.group(2)
+ if old_path and new_path:
+ if old_path.startswith('a/'):
+ old_path = old_path[2:]
+
+ if new_path.startswith('b/'):
+ new_path = new_path[2:]
+ return header(
+ index_path = None,
+ old_path = old_path,
+ old_version = over,
+ new_path = new_path,
+ new_version = nver)
+
+ return None
+
+def parse_svn_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ headers = findall_regex(lines, svn_header_index)
+ if len(headers) == 0:
+ return None
+
+ while len(lines) > 0:
+ i = svn_header_index.match(lines[0])
+ del lines[0]
+ if i:
+ diff_header = parse_diff_header(lines)
+ if diff_header:
+ opath = diff_header.old_path
+ over = diff_header.old_version
+ if over:
+ oend = svn_header_timestamp_version.match(over)
+ if oend and oend.group(1):
+ over = int(oend.group(1))
+ elif opath:
+ ts = svn_header_timestamp.match(opath)
+ if ts:
+ opath = opath[:-len(ts.group(1))]
+ oend = svn_header_timestamp_version.match(ts.group(1))
+ if oend and oend.group(1):
+ over = int(oend.group(1))
+
+ npath = diff_header.new_path
+ nver = diff_header.new_version
+ if nver:
+ nend = svn_header_timestamp_version.match(diff_header.new_version)
+ if nend and nend.group(1):
+ nver = int(nend.group(1))
+ elif npath:
+ ts = svn_header_timestamp.match(npath)
+ if ts:
+ npath = npath[:-len(ts.group(1))]
+ nend = svn_header_timestamp_version.match(ts.group(1))
+ if nend and nend.group(1):
+ nver = int(nend.group(1))
+
+ if type(over) != int:
+ over = None
+
+ if type(nver) != int:
+ nver = None
+
+ return header(
+ index_path = i.group(1),
+ old_path = opath,
+ old_version = over,
+ new_path = npath,
+ new_version = nver,
+ )
+ return header(
+ index_path = i.group(1),
+ old_path = i.group(1),
+ old_version = None,
+ new_path = i.group(1),
+ new_version = None,
+ )
+
+ return None
+
+def parse_cvs_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ headers = findall_regex(lines, cvs_header_rcs)
+ headers_old = findall_regex(lines, old_cvs_diffcmd_header)
+
+ if headers:
+ # parse rcs style headers
+ while len(lines) > 0:
+ i = cvs_header_index.match(lines[0])
+ del lines[0]
+ if i:
+ diff_header = parse_diff_header(lines)
+ if diff_header:
+ over = diff_header.old_version
+ if over:
+ oend = cvs_header_timestamp.match(over)
+ oend_c = cvs_header_timestamp_colon.match(over)
+ if oend:
+ over = oend.group(2)
+ elif oend_c:
+ over = oend_c.group(1)
+
+ nver = diff_header.new_version
+ if nver:
+ nend = cvs_header_timestamp.match(nver)
+ nend_c = cvs_header_timestamp_colon.match(nver)
+ if nend:
+ nver = nend.group(2)
+ elif nend_c:
+ nver = nend_c.group(1)
+
+ return header(
+ index_path = i.group(1),
+ old_path = diff_header.old_path,
+ old_version = over,
+ new_path = diff_header.new_path,
+ new_version = nver,
+ )
+ return header(
+ index_path = i.group(1),
+ old_path = i.group(1),
+ old_version = None,
+ new_path = i.group(1),
+ new_version = None,
+ )
+ elif headers_old:
+ # parse old style headers
+ while len(lines) > 0:
+ i = cvs_header_index.match(lines[0])
+ del lines[0]
+ if i:
+ d = old_cvs_diffcmd_header.match(lines[0])
+ if d:
+ _ = parse_diff_header(lines) # will get rid of the useless stuff for us
+ over = d.group(2)
+ if not over:
+ over = None
+
+ nver = d.group(4)
+ if not nver:
+ nver = None
+ return header(
+ index_path = i.group(1),
+ old_path = d.group(1),
+ old_version = over,
+ new_path = d.group(3),
+ new_version = nver,
+ )
+
+ return header(
+ index_path = i.group(1),
+ old_path = i.group(1),
+ old_version = None,
+ new_path = i.group(1),
+ new_version = None,
+ )
+
+ return None
+
+def parse_diffcmd_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ headers = findall_regex(lines, diffcmd_header)
+ if len(headers) == 0:
+ return None
+
+ while len(lines) > 0:
+ d = diffcmd_header.match(lines[0])
+ del lines[0]
+ if d:
+ return header(
+ index_path = None,
+ old_path = d.group(1),
+ old_version = None,
+ new_path = d.group(2),
+ new_version = None,
+ )
+
+ return None
+
+def parse_unified_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ headers = findall_regex(lines, unified_header_new_line)
+ if len(headers) == 0:
+ return None
+
+ while len(lines) > 1:
+ o = unified_header_old_line.match(lines[0])
+ del lines[0]
+ if o:
+ n = unified_header_new_line.match(lines[0])
+ del lines[0]
+ if n:
+ over = o.group(2)
+ if len(over) == 0:
+ over = None
+
+ nver = n.group(2)
+ if len(nver) == 0:
+ never = None
+
+ return header(
+ index_path = None,
+ old_path = o.group(1),
+ old_version = over,
+ new_path = n.group(1),
+ new_version = nver,
+ )
+
+ return None
+
+def parse_context_header(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ headers = findall_regex(lines, context_header_old_line)
+ if len(headers) == 0:
+ return None
+
+ while len(lines) > 1:
+ o = context_header_old_line.match(lines[0])
+ del lines[0]
+ if o:
+ n = context_header_new_line.match(lines[0])
+ del lines[0]
+ if n:
+ over = o.group(2)
+ if len(over) == 0:
+ over = None
+
+ nver = n.group(2)
+ if len(nver) == 0:
+ never = None
+
+ return header(
+ index_path = None,
+ old_path = o.group(1),
+ old_version = over,
+ new_path = n.group(1),
+ new_version = nver,
+ )
+
+ return None
+
+
+def parse_default_diff(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ old = 0
+ new = 0
+ old_len = 0
+ new_len = 0
+ r = 0
+ i = 0
+
+ changes = list()
+
+ hunks = split_by_regex(lines, default_hunk_start)
+ for hunk in hunks:
+ if len(hunk):
+ r = 0
+ i = 0
+ while len(hunk) > 0:
+ h = default_hunk_start.match(hunk[0])
+ c = default_change.match(hunk[0])
+ del hunk[0]
+ if h:
+ old = int(h.group(1))
+ if len(h.group(2)) > 0:
+ old_len = int(h.group(2)) - old + 1
+ else:
+ old_len = 0
+
+ new = int(h.group(4))
+ if len(h.group(5)) > 0:
+ new_len = int(h.group(5)) - new + 1
+ else:
+ new_len = 0
+
+ hunk_kind = h.group(3)
+ elif c:
+ kind = c.group(1)
+ line = c.group(2)
+
+ if kind == '<' and (r != old_len or r == 0):
+ changes.append((old + r, None, line))
+ r += 1
+ elif kind == '>' and (i != new_len or i == 0):
+ changes.append((None, new + i, line))
+ i += 1
+
+ if len(changes) > 0:
+ return changes
+
+ return None
+
+def parse_unified_diff(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ old = 0
+ new = 0
+ r = 0
+ i = 0
+
+ changes = list()
+
+ hunks = split_by_regex(lines, unified_hunk_start)
+ for hunk in hunks:
+ # reset counters
+ r = 0
+ i = 0
+ while len(hunk) > 0:
+ h = unified_hunk_start.match(hunk[0])
+ del hunk[0]
+ if h:
+ old = int(h.group(1))
+ if len(h.group(2)) > 0:
+ old_len = int(h.group(2))
+ else:
+ old_len = 0
+
+ new = int(h.group(3))
+ if len(h.group(4)) > 0:
+ new_len = int(h.group(4))
+ else:
+ new_len = 0
+
+ h = None
+ break
+
+ while len(hunk) > 0:
+ c = unified_change.match(hunk[0])
+ if c:
+ kind = c.group(1)
+ line = c.group(2)
+ c = None
+
+ if kind == '-' and (r != old_len or r == 0):
+ changes.append((old + r, None, line))
+ r += 1
+ elif kind == '+' and (i != new_len or i == 0):
+ changes.append((None, new + i, line))
+ i += 1
+ elif kind == ' ' and r != old_len and i != new_len:
+ changes.append((old + r, new + i, line))
+ r += 1
+ i += 1
+
+ del hunk[0]
+
+ if len(changes) > 0:
+ return changes
+
+ return None
+
+
+
+def parse_context_diff(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ old = 0
+ new = 0
+ j = 0
+ k = 0
+
+ changes = list()
+ old_lines = list()
+ new_lines = list()
+
+ hunks = split_by_regex(lines, context_hunk_start)
+ for hunk in hunks:
+ if len(hunk):
+ j = 0
+ k = 0
+ parts = split_by_regex(hunk, context_hunk_new)
+ if len(parts) != 2:
+ raise ValueError("Context diff invalid")
+
+ old_hunk = parts[0]
+ new_hunk = parts[1]
+
+ while len(old_hunk) > 0:
+ o = context_hunk_old.match(old_hunk[0])
+ del old_hunk[0]
+ if o:
+ old = int(o.group(1))
+ old_len = int(o.group(2)) + 1 - old
+ while len(new_hunk) > 0:
+ n = context_hunk_new.match(new_hunk[0])
+ del new_hunk[0]
+ if n:
+ new = int(n.group(1))
+ new_len = int(n.group(2)) + 1 - new
+ break
+ break
+
+ # now have old and new set, can start processing?
+ if len(old_hunk) > 0 and len(new_hunk) == 0:
+ # only removes left?
+ while len(old_hunk) > 0:
+ c = context_change.match(old_hunk[0])
+ del old_hunk[0]
+ if c:
+ kind = c.group(1)
+ line = c.group(2)
+
+ if kind == '-' and (j != old_len or j == 0):
+ changes.append((old + j, None, line))
+ j += 1
+ elif kind == ' ' and ((j != old_len and k != new_len)
+ or (j == 0 or k == 0)):
+ changes.append((old + j, new + k, line))
+ j += 1
+ k += 1
+ elif kind == '+' or kind == '!':
+ raise ValueError("Got unexpected change in removal hunk: " + kind)
+
+ elif len(old_hunk) == 0 and len(new_hunk) > 0:
+ # only insertions left?
+ while len(new_hunk) > 0:
+ c = context_change.match(new_hunk[0])
+ del new_hunk[0]
+ if c:
+ kind = c.group(1)
+ line = c.group(2)
+
+ if kind == '+' and (k != new_len or k == 0):
+ changes.append((None, new + k, line))
+ k += 1
+ elif kind == ' ' and ((j != old_len and k != new_len)
+ or (j == 0 or k == 0)):
+ changes.append((old + j, new + k, line))
+ j += 1
+ k += 1
+ elif kind == '-' or kind == '!':
+ raise ValueError("Got unexpected change in insertion hunk: " + kind)
+ else:
+ # both
+ while len(old_hunk) > 0 and len(new_hunk) > 0:
+ oc = context_change.match(old_hunk[0])
+ nc = context_change.match(new_hunk[0])
+ okind = None
+ nkind = None
+
+ if oc:
+ okind = oc.group(1)
+ oline = oc.group(2)
+
+ if nc:
+ nkind = nc.group(1)
+ nline = nc.group(2)
+
+ if not (oc or nc):
+ del old_hunk[0]
+ del new_hunk[0]
+ elif okind == ' ' and nkind == ' ' and oline == nline:
+ changes.append((old + j, new + k, oline))
+ j += 1
+ k += 1
+ del old_hunk[0]
+ del new_hunk[0]
+ elif okind == '-' or okind == '!' and (j != old_len or j == 0):
+ changes.append((old + j, None, oline))
+ j += 1
+ del old_hunk[0]
+ elif nkind == '+' or nkind == '!' and (k != old_len or k == 0):
+ changes.append((None, new + k, nline))
+ k += 1
+ del new_hunk[0]
+ else:
+ return None
+
+ if len(changes) > 0:
+ return changes
+
+ return None
+
+
+def parse_ed_diff(text):
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ old = 0
+ new = 0
+ j = 0
+ k = 0
+
+ r = 0
+ i = 0
+
+ changes = list()
+
+ hunks = split_by_regex(lines, ed_hunk_start)
+ hunks.reverse()
+ for hunk in hunks:
+ if len(hunk):
+ j = 0
+ k = 0
+ while len(hunk) > 0:
+ o = ed_hunk_start.match(hunk[0])
+ del hunk[0]
+ if o:
+ old = int(o.group(1))
+ if len(o.group(2)):
+ old_end = int(o.group(2))
+ else:
+ old_end = old
+
+ hunk_kind = o.group(3)
+ if hunk_kind == 'd':
+ k = 0
+ while old_end >= old:
+ changes.append((old + k, None, None))
+ r += 1
+ k += 1
+ old_end -= 1
+ else:
+ while len(hunk) > 0:
+ e = ed_hunk_end.match(hunk[0])
+ if e:
+ pass
+ elif hunk_kind == 'c':
+ k = 0
+ while old_end >= old:
+ changes.append((old + k, None, None))
+ r += 1
+ k += 1
+ old_end -= 1
+
+ # I basically have no idea why this works
+ # for these tests.
+ changes.append((None, old - r + i + k + j, hunk[0]))
+ i += 1
+ j += 1
+ elif hunk_kind == 'a':
+ changes.append((None, old - r + i + 1, hunk[0]))
+ i += 1
+
+ del hunk[0]
+
+
+
+ if len(changes) > 0:
+ return changes
+
+ return None
+
+def parse_rcs_ed_diff(text):
+ # much like forward ed, but no 'c' type
+ try:
+ lines = text.splitlines()
+ except AttributeError:
+ lines = text
+
+ old = 0
+ new = 0
+ j = 0
+ size = 0
+ total_change_size = 0
+
+ changes = list()
+
+
+ hunks = split_by_regex(lines, rcs_ed_hunk_start)
+ for hunk in hunks:
+ if len(hunk):
+ j = 0
+ while len(hunk) > 0:
+ o = rcs_ed_hunk_start.match(hunk[0])
+ del hunk[0]
+ if o:
+ hunk_kind = o.group(1)
+ old = int(o.group(2))
+ size = int(o.group(3))
+
+
+ if hunk_kind == 'a':
+ old += total_change_size + 1
+ total_change_size += size
+ while size > 0 and len(hunk) > 0:
+ changes.append((None, old + j, hunk[0]))
+ j += 1
+ size -= 1
+
+ del hunk[0]
+
+ elif hunk_kind == 'd':
+ total_change_size -= size
+ while size > 0:
+ changes.append((old + j, None, None))
+ j += 1
+ size -= 1
+
+ if len(changes) > 0:
+ return changes
+
+ return None
diff --git a/whatthepatch/snippets.py b/whatthepatch/snippets.py
new file mode 100644
index 0000000..18f86f0
--- /dev/null
+++ b/whatthepatch/snippets.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+import os
+from shutil import rmtree
+
+
+# order preserving uniq for lists
+def uniq(L):
+ seen = {}
+ result = []
+ for item in L:
+ if item in seen:
+ continue
+ seen[item] = 1
+ result.append(item)
+ return result
+
+
+# exception handling mkdir -p
+def make_dir(dir):
+ try:
+ os.makedirs(dir)
+ except os.error as e:
+ if 17 == e.errno:
+ # the directory already exists
+ pass
+ else:
+ raise e
+
+def remove(path):
+ if os.path.exists(path):
+ if os.path.isdir(path):
+ rmtree(path)
+ else:
+ os.remove(path)
+
+# file line length
+def file_len(fname):
+ with open(fname) as f:
+ for i, l in enumerate(f):
+ pass
+ return i + 1
+
+# find all indices of a list of strings that match a regex
+def findall_regex(l, r):
+ found = list()
+ for i in range(0, len(l)):
+ k = r.match(l[i])
+ if k:
+ found.append(i)
+ k = None
+
+ return found
+
+def split_by_regex(l, r):
+ splits = list()
+ indices = findall_regex(l, r)
+ k = None
+ for i in indices:
+ if k is None:
+ splits.append(l[0:i])
+ k = i
+ else:
+ splits.append(l[k:i])
+ k = i
+
+ splits.append(l[k:])
+
+ return splits
+
+# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
+def which(program):
+ def is_exe(fpath):
+ return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
+
+ fpath, fname = os.path.split(program)
+ if fpath:
+ if is_exe(program):
+ return program
+ else:
+ for path in os.environ["PATH"].split(os.pathsep):
+ path = path.strip('"')
+ exe_file = os.path.join(path, program)
+ if is_exe(exe_file):
+ return exe_file
+
+ return None