From 240a39d2adf794ee3355db4ed346538ce5c48643 Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Mon, 6 Jun 2016 00:36:43 +0100 Subject: [PATCH 1/6] Python 3 debug fixes --- patch.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/patch.py b/patch.py index 45c5a48..dfaeaf2 100755 --- a/patch.py +++ b/patch.py @@ -689,25 +689,24 @@ def _normalize_filenames(self): [x] always use forward slashes to be crossplatform (diff/patch were born as a unix utility after all) - + return None """ if debugmode: debug("normalize filenames") for i,p in enumerate(self.items): if debugmode: - debug(" patch type = " + p.type) - debug(" source = " + p.source) - debug(" target = " + p.target) + debug(" patch type = %s" % p.type) + debug(" source = %s" % p.source) + debug(" target = %s" % p.target) if p.type in (HG, GIT): - # TODO: figure out how to deal with /dev/null entries debug("stripping a/ and b/ prefixes") - if p.source != '/dev/null': + if p.source != b'/dev/null': if not p.source.startswith(b"a/"): warning("invalid source filename") else: p.source = p.source[2:] - if p.target != '/dev/null': + if p.target != b'/dev/null': if not p.target.startswith(b"b/"): warning("invalid target filename") else: From d5b90be1ffd115f7921a6ba3e476008485a7414d Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Mon, 6 Jun 2016 00:48:10 +0100 Subject: [PATCH 2/6] Add support for creating and removing files --- patch.py | 122 ++++++++++++++++++++----------- tests/08create/08create.patch | 5 ++ tests/08create/[result]/created | 1 + tests/09delete/09delete.patch | 5 ++ tests/09delete/[result]/.gitkeep | 1 + tests/09delete/deleted | 1 + tests/run_tests.py | 16 +++- 7 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 tests/08create/08create.patch create mode 100644 tests/08create/[result]/created create mode 100644 tests/09delete/09delete.patch create mode 100644 tests/09delete/[result]/.gitkeep create mode 100644 tests/09delete/deleted diff --git a/patch.py b/patch.py index dfaeaf2..8428aed 100755 --- a/patch.py +++ b/patch.py @@ -18,6 +18,7 @@ import copy import logging import re +import tempfile # cStringIO doesn't support unicode in 2.5 try: @@ -477,11 +478,26 @@ def lineno(self): # XXX header += srcname # double source filename line is encountered # attempt to restart from this second line - re_filename = b"^--- ([^\t]+)" - match = re.match(re_filename, line) + + # Files dated at Unix epoch don't exist, e.g.: + # '1970-01-01 01:00:00.000000000 +0100' + # They include timezone offsets. + # .. which can be parsed (if we remove the nanoseconds) + # .. by strptime() with: + # '%Y-%m-%d %H:%M:%S %z' + # .. but unfortunately this relies on the OSes libc + # strptime function and %z support is patchy, so we drop + # everything from the . onwards and group the year and time + # separately. + re_filename_date_time = b"^--- ([^\t]+)(?:\s([0-9-]+)\s([0-9:]+)|.*)" + match = re.match(re_filename_date_time, line) # todo: support spaces in filenames if match: srcname = match.group(1).strip() + date = match.group(2) + time = match.group(3) + if (date == b'1970-01-01' or date == b'1969-12-31') and time.split(b':',1)[1] == b'00:00': + srcname = b'/dev/null' else: warning("skipping invalid filename at line %d" % (lineno+1)) self.errors += 1 @@ -516,8 +532,8 @@ def lineno(self): filenames = False headscan = True else: - re_filename = b"^\+\+\+ ([^\t]+)" - match = re.match(re_filename, line) + re_filename_date_time = b"^\+\+\+ ([^\t]+)(?:\s([0-9-]+)\s([0-9:]+)|.*)" + match = re.match(re_filename_date_time, line) if not match: warning("skipping invalid patch - no target filename at line %d" % (lineno+1)) self.errors += 1 @@ -526,12 +542,18 @@ def lineno(self): filenames = False headscan = True else: + tgtname = match.group(1).strip() + date = match.group(2) + time = match.group(3) + if (date == b'1970-01-01' or date == b'1969-12-31') and time.split(b':',1)[1] == b'00:00': + tgtname = b'/dev/null' if p: # for the first run p is None self.items.append(p) p = Patch() p.source = srcname srcname = None - p.target = match.group(1).strip() + p.target = tgtname + tgtname = None p.header = header header = [] # switch to hunkhead state @@ -654,7 +676,7 @@ def _detect_type(self, p): break if p.header[idx].startswith(b'diff --git a/'): if (idx+1 < len(p.header) - and re.match(b'index \\w{7}..\\w{7} \\d{6}', p.header[idx+1])): + and re.match(b'(?:index \\w{7}..\\w{7} \\d{6}|new file mode \\d*)', p.header[idx+1])): if DVCS: return GIT @@ -729,16 +751,17 @@ def _normalize_filenames(self): while p.target.startswith(b".." + sep): p.target = p.target.partition(sep)[2] # absolute paths are not allowed - if xisabs(p.source) or xisabs(p.target): + if (xisabs(p.source) and p.source != b'/dev/null') or \ + (xisabs(p.target) and p.target != b'/dev/null'): warning("error: absolute paths are not allowed - file no.%d" % (i+1)) self.warnings += 1 - if xisabs(p.source): + if xisabs(p.source) and p.source != b'/dev/null': warning("stripping absolute path from source name '%s'" % p.source) p.source = xstrip(p.source) - if xisabs(p.target): + if xisabs(p.target) and p.target != b'/dev/null': warning("stripping absolute path from target name '%s'" % p.target) p.target = xstrip(p.target) - + self.items[i].source = p.source self.items[i].target = p.target @@ -800,12 +823,24 @@ def diffstat(self): return output - def findfile(self, old, new): - """ return name of file to be patched or None """ - if exists(old): - return old + def findfiles(self, old, new): + """ return tuple of source file, target file """ + if old == b'/dev/null': + handle, abspath = tempfile.mkstemp(suffix='pypatch') + abspath = abspath.encode() + # The source file must contain a line for the hunk matching to succeed. + os.write(handle, b' ') + os.close(handle) + if not exists(new): + handle = open(new, 'wb') + handle.close() + return abspath, new + elif exists(old): + return old, old elif exists(new): - return new + return new, new + elif new == b'/dev/null': + return None, None else: # [w] Google Code generates broken patches with its online editor debug("broken patch from Google Code, stripping prefixes..") @@ -814,10 +849,10 @@ def findfile(self, old, new): debug(" %s" % old) debug(" %s" % new) if exists(old): - return old + return old, old elif exists(new): - return new - return None + return new, new + return None, None def apply(self, strip=0, root=None): @@ -848,27 +883,27 @@ def apply(self, strip=0, root=None): debug("stripping %s leading component(s) from:" % strip) debug(" %s" % p.source) debug(" %s" % p.target) - old = pathstrip(p.source, strip) - new = pathstrip(p.target, strip) + old = p.source if p.source == b'/dev/null' else pathstrip(p.source, strip) + new = p.target if p.target == b'/dev/null' else pathstrip(p.target, strip) else: old, new = p.source, p.target - filename = self.findfile(old, new) + filenameo, filenamen = self.findfiles(old, new) - if not filename: + if not filenameo or not filenamen: warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) errors += 1 continue - if not isfile(filename): - warning("not a file - %s" % filename) + if not isfile(filenameo): + warning("not a file - %s" % filenameo) errors += 1 continue # [ ] check absolute paths security here - debug("processing %d/%d:\t %s" % (i+1, total, filename)) + debug("processing %d/%d:\t %s" % (i+1, total, filenamen)) # validate before patching - f2fp = open(filename, 'rb') + f2fp = open(filenameo, 'rb') hunkno = 0 hunk = p.hunks[hunkno] hunkfind = [] @@ -891,7 +926,7 @@ def apply(self, strip=0, root=None): if line.rstrip(b"\r\n") == hunkfind[hunklineno]: hunklineno+=1 else: - info("file %d/%d:\t %s" % (i+1, total, filename)) + info("file %d/%d:\t %s" % (i+1, total, filenamen)) info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1)) info(" expected: %s" % hunkfind[hunklineno]) info(" actual : %s" % line.rstrip(b"\r\n")) @@ -911,8 +946,8 @@ def apply(self, strip=0, root=None): break # check if processed line is the last line - if lineno+1 == hunk.startsrc+len(hunkfind)-1: - debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filename)) + if len(hunkfind) == 0 or lineno+1 == hunk.startsrc+len(hunkfind)-1: + debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filenamen)) hunkno+=1 validhunks+=1 if hunkno < len(p.hunks): @@ -924,34 +959,39 @@ def apply(self, strip=0, root=None): break else: if hunkno < len(p.hunks): - warning("premature end of source file %s at hunk %d" % (filename, hunkno+1)) + warning("premature end of source file %s at hunk %d" % (filenameo, hunkno+1)) errors += 1 f2fp.close() if validhunks < len(p.hunks): - if self._match_file_hunks(filename, p.hunks): - warning("already patched %s" % filename) + if self._match_file_hunks(filenameo, p.hunks): + warning("already patched %s" % filenameo) else: - warning("source file is different - %s" % filename) + warning("source file is different - %s" % filenameo) errors += 1 if canpatch: - backupname = filename+b".orig" + backupname = filenamen+b".orig" if exists(backupname): warning("can't backup original file to %s - aborting" % backupname) else: import shutil - shutil.move(filename, backupname) - if self.write_hunks(backupname, filename, p.hunks): - info("successfully patched %d/%d:\t %s" % (i+1, total, filename)) + shutil.move(filenamen, backupname) + if self.write_hunks(backupname if filenameo == filenamen else filenameo, filenamen, p.hunks): + info("successfully patched %d/%d:\t %s" % (i+1, total, filenamen)) os.unlink(backupname) + if new == b'/dev/null': + # check that filename is of size 0 and delete it. + if os.path.getsize(filenamen) > 0: + warning("expected patched file to be empty as it's marked as deletion:\t %s" % filenamen) + os.unlink(filenamen) else: errors += 1 - warning("error patching file %s" % filename) - shutil.copy(filename, filename+".invalid") - warning("invalid version is saved to %s" % filename+".invalid") + warning("error patching file %s" % filenamen) + shutil.copy(filenamen, filenamen+".invalid") + warning("invalid version is saved to %s" % filenamen+".invalid") # todo: proper rejects - shutil.move(backupname, filename) + shutil.move(backupname, filenamen) if root: os.chdir(prevdir) diff --git a/tests/08create/08create.patch b/tests/08create/08create.patch new file mode 100644 index 0000000..af91725 --- /dev/null +++ b/tests/08create/08create.patch @@ -0,0 +1,5 @@ +diff -urN from/created to/created +--- created 1970-01-01 01:00:00.000000000 +0100 ++++ created 2016-06-07 00:43:08.701304500 +0100 +@@ -0,0 +1 @@ ++Created by patch diff --git a/tests/08create/[result]/created b/tests/08create/[result]/created new file mode 100644 index 0000000..439f92b --- /dev/null +++ b/tests/08create/[result]/created @@ -0,0 +1 @@ +Created by patch diff --git a/tests/09delete/09delete.patch b/tests/09delete/09delete.patch new file mode 100644 index 0000000..b8d4890 --- /dev/null +++ b/tests/09delete/09delete.patch @@ -0,0 +1,5 @@ +diff -urN from/deleted to/deleted +--- deleted 2016-06-07 00:44:19.093323300 +0100 ++++ deleted 1970-01-01 01:00:00.000000000 +0100 +@@ -1 +0,0 @@ +-Deleted by patch diff --git a/tests/09delete/[result]/.gitkeep b/tests/09delete/[result]/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/09delete/[result]/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/09delete/deleted b/tests/09delete/deleted new file mode 100644 index 0000000..0faeb37 --- /dev/null +++ b/tests/09delete/deleted @@ -0,0 +1 @@ +Deleted by patch diff --git a/tests/run_tests.py b/tests/run_tests.py index 876aeae..ea244a9 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -169,7 +169,7 @@ def _run_test(self, testname): # recursive comparison self._assert_dirs_equal(join(basepath, "[result]"), tmpdir, - ignore=["%s.patch" % testname, ".svn", "[result]"]) + ignore=["%s.patch" % testname, ".svn", ".gitkeep", "[result]"]) shutil.rmtree(tmpdir) @@ -410,6 +410,20 @@ def test_apply_strip(self): p.target = b'nasty/prefix/' + p.target self.assertTrue(pto.apply(strip=2, root=treeroot)) + def test_create_file(self): + treeroot = join(self.tmpdir, 'rootparent') + os.makedirs(treeroot) + pto = patch.fromfile(join(TESTS, '08create/08create.patch')) + pto.apply(strip=0, root=treeroot) + self.assertTrue(os.path.exists(os.path.join(treeroot, 'created'))) + + def test_delete_file(self): + treeroot = join(self.tmpdir, 'rootparent') + shutil.copytree(join(TESTS, '09delete'), treeroot) + pto = patch.fromfile(join(TESTS, '09delete/09delete.patch')) + pto.apply(strip=0, root=treeroot) + self.assertFalse(os.path.exists(os.path.join(treeroot, 'deleted'))) + class TestHelpers(unittest.TestCase): # unittest setting From 9d166dc1f02874707932b01403ef9185565a77f6 Mon Sep 17 00:00:00 2001 From: memsharded Date: Tue, 10 Oct 2017 15:49:56 +0200 Subject: [PATCH 3/6] managing dev/null file add and removal --- .gitignore | 1 + example/example.py | 42 ++++++++++++++++++++++++++++++++++++++++++ patch.py | 24 ++++++++++++++++++++---- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 example/example.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/example/example.py b/example/example.py new file mode 100644 index 0000000..b9db850 --- /dev/null +++ b/example/example.py @@ -0,0 +1,42 @@ +import os +import logging +from patch import fromfile, fromstring + +class PatchLogHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self, logging.DEBUG) + + def emit(self, record): + logstr = self.format(record) + print logstr + +patchlog = logging.getLogger("patch") +patchlog.handlers = [] +patchlog.addHandler(PatchLogHandler()) + +patch = fromstring("""--- /dev/null ++++ b/newfile +@@ -0,0 +0,3 @@ ++New file1 ++New file2 ++New file3 +""") + +patch.apply(root=os.getcwd(), strip=0) + + +with open("newfile", "rb") as f: + newfile = f.read() + assert "New file1\nNew file2\nNew file3\n" == newfile + +patch = fromstring("""--- a/newfile ++++ /dev/null +@@ -0,3 +0,0 @@ +-New file1 +-New file2 +-New file3 +""") + +result = patch.apply(root=os.getcwd(), strip=0) + +assert os.path.exists("newfile") is False \ No newline at end of file diff --git a/patch.py b/patch.py index 4b82af0..4857121 100755 --- a/patch.py +++ b/patch.py @@ -800,7 +800,6 @@ def diffstat(self): % (len(names), sum(insert), sum(delete), delta)) return output - def findfile(self, old, new): """ return name of file to be patched or None """ if exists(old): @@ -820,6 +819,10 @@ def findfile(self, old, new): return new return None + def _strip_prefix(self, filename): + if filename.startswith(b'a/') or filename.startswith(b'b/'): + return filename[2:] + return filename def apply(self, strip=0, root=None): """ Apply parsed patch, optionally stripping leading components @@ -857,9 +860,22 @@ def apply(self, strip=0, root=None): filename = self.findfile(old, new) if not filename: - warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) - errors += 1 - continue + if "dev/null" in old: + # this is a file creation + filename = self._strip_prefix(new) + # I wish there would be something more clean to get the full contents + new_file = "".join(s[1:] for s in p.hunks[0].text) + with open(filename, "wb") as f: + f.write(new_file) + continue + elif "dev/null" in new: + # this is a file removal + os.remove(self._strip_prefix(old)) + continue + else: + warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) + errors += 1 + continue if not isfile(filename): warning("not a file - %s" % filename) errors += 1 From 6000f33fba5680299f4fe41fbf68bb385bcf24f4 Mon Sep 17 00:00:00 2001 From: Francois Marier Date: Wed, 17 Apr 2019 17:34:36 -0700 Subject: [PATCH 4/6] Return an error when aborting due to existing .orig file (fixes #60) --- patch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/patch.py b/patch.py index 4b82af0..da3bcf7 100755 --- a/patch.py +++ b/patch.py @@ -940,6 +940,7 @@ def apply(self, strip=0, root=None): backupname = filename+b".orig" if exists(backupname): warning("can't backup original file to %s - aborting" % backupname) + errors += 1 else: import shutil shutil.move(filename, backupname) From 942aab570caf00e1ff4aa7ad6746168d49336607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Klitzing?= Date: Wed, 31 Jul 2019 10:23:12 +0200 Subject: [PATCH 5/6] Fix some issues found by lgtm.com * This import of module shutil is redundant, as it was previously imported on line 35. * Testing for None should use the 'is' operator. * The value assigned to local variable 'hno' is never used. --- patch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/patch.py b/patch.py index 4b82af0..79b1540 100755 --- a/patch.py +++ b/patch.py @@ -368,7 +368,7 @@ def lineno(self): header.append(fe.line) fe.next() if fe.is_empty: - if p == None: + if p is None: debug("no patch data found") # error is shown later self.errors += 1 else: @@ -941,7 +941,6 @@ def apply(self, strip=0, root=None): if exists(backupname): warning("can't backup original file to %s - aborting" % backupname) else: - import shutil shutil.move(filename, backupname) if self.write_hunks(backupname, filename, p.hunks): info("successfully patched %d/%d:\t %s" % (i+1, total, filename)) @@ -1005,7 +1004,6 @@ class NoMatch(Exception): lineno = 1 line = fp.readline() - hno = None try: for hno, h in enumerate(hunks): # skip to first line of the hunk From 7057c3c360d0b4a6856fec45a127c9f9bf35160d Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Fri, 27 Sep 2019 16:33:06 -0300 Subject: [PATCH 6/6] Add setup file - Add setup.py (setuptools) - Update LICENSE with Conan MIT - Fix patch - Update README file with fork description Signed-off-by: Uilian Ries --- doc/LICENSE => LICENSE | 31 ++++++++++ README.md | 27 ++++++--- patch.py | 51 ++++++++++------- setup.py | 126 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 30 deletions(-) rename doc/LICENSE => LICENSE (50%) create mode 100644 setup.py diff --git a/doc/LICENSE b/LICENSE similarity index 50% rename from doc/LICENSE rename to LICENSE index e172f7a..54060ca 100644 --- a/doc/LICENSE +++ b/LICENSE @@ -20,3 +20,34 @@ 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. + +---- + +The MIT License (MIT) + +Copyright (c) 2019 JFrog LTD + + + +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/README.md b/README.md index 6cca3fd..e848d44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ -Library to parse and apply unified diffs. +[![PyPI](https://img.shields.io/pypi/v/patch-ng)](https://pypi.python.org/pypi/patch-ng) -[![Build Status](https://img.shields.io/travis/techtonik/python-patch/master)](https://travis-ci.org/techtonik/python-patch/branches) [![PyPI](https://img.shields.io/pypi/v/patch)](https://pypi.python.org/pypi/patch) +## Patch NG (New Generation) + +#### Library to parse and apply unified diffs. + +#### Why did we fork this project? + +This project is a fork from the original [python-patch](https://github.com/techtonik/python-patch) project. + +As any other project, bugs are common during the development process, the combination of issues + pull requests are +able to keep the constant improvement of a project. However, both community and author need to be aligned. When users, +developers, the community, needs a fix which are important for their projects, but there is no answer from the author, +or the time for response is not enough, then the most plausible way is forking and continuing a parallel development. + +That's way we forked the original and accepted most of PRs waiting for review since jun/2019 (5 months from now). ### Features @@ -32,8 +45,8 @@ module without external dependencies. patch.py diff.patch You can also run the .zip file. - - python patch-1.16.zip diff.patch + + python patch-1.17.zip diff.patch ### Installation @@ -42,13 +55,11 @@ and use it from here. This setup will always be repeatable. But if you need to add `patch` module as a dependency, make sure to use strict specifiers to avoid hitting an API break when version 2 is released: - pip install "patch==1.*" + pip install "patch-ng" ### Other stuff * [CHANGES](doc/CHANGES.md) -* [LICENSE](doc/LICENSE) +* [LICENSE: MIT](LICENSE) * [CREDITS](doc/CREDITS) - -* [test coverage](http://techtonik.github.io/python-patch/tests/coverage/) diff --git a/patch.py b/patch.py index f2f688c..5e932cf 100755 --- a/patch.py +++ b/patch.py @@ -7,13 +7,33 @@ Copyright (c) 2008-2016 anatoly techtonik Available under the terms of MIT license +--- + The MIT License (MIT) + + Copyright (c) 2019 JFrog LTD + + 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. """ from __future__ import print_function -__author__ = "anatoly techtonik " -__version__ = "1.16" +__author__ = "Conan.io " +__version__ = "1.17" __license__ = "MIT" -__url__ = "https://github.com/techtonik/python-patch" +__url__ = "https://github.com/conan-io/python-patch" import copy import logging @@ -894,25 +914,12 @@ def apply(self, strip=0, root=None): filenameo, filenamen = self.findfiles(old, new) - if not filename: - if "dev/null" in old: - # this is a file creation - filename = self._strip_prefix(new) - # I wish there would be something more clean to get the full contents - new_file = "".join(s[1:] for s in p.hunks[0].text) - with open(filename, "wb") as f: - f.write(new_file) - continue - elif "dev/null" in new: - # this is a file removal - os.remove(self._strip_prefix(old)) - continue - else: - warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) - errors += 1 - continue - if not isfile(filename): - warning("not a file - %s" % filename) + if not filenameo or not filenamen: + warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new)) + errors += 1 + continue + if not isfile(filenameo): + warning("not a file - %s" % filenameo) errors += 1 continue diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..35a0902 --- /dev/null +++ b/setup.py @@ -0,0 +1,126 @@ +"""A setuptools based setup module. +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +# Always prefer setuptools over distutils +import re +import os +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open + + +here = os.path.abspath(os.path.dirname(__file__)) + +# Get the long description from the README file +with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + + +def get_requires(filename): + requirements = [] + with open(filename) as req_file: + for line in req_file.read().splitlines(): + if not line.strip().startswith("#"): + requirements.append(line) + return requirements + + +def load_version(): + """Loads a file content""" + filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "patch.py")) + with open(filename, "rt") as version_file: + content = version_file.read() + version = re.search('__version__ = "([0-9a-z.-]+)"', content).group(1) + return version + +setup( + name='patch-ng', + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version=load_version(), + + # This is an optional longer description of your project that represents + # the body of text which users will see when they visit PyPI. + # + # Often, this is the same as your README, so you can just read it in from + # that file directly (as we have already done above) + # + # This field corresponds to the "Description" metadata field: + # https://packaging.python.org/specifications/core-metadata/#description-optional + long_description=long_description, # Optional + + description='Library to parse and apply unified diffs.', + + # The project's main homepage. + url='https://github.com/conan-io/python-patch', + + # Author details + author='Conan.io', + author_email='info@conan.io', + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + ], + + # What does your project relate to? + keywords=['patch', 'parse', 'diff', 'strip', 'diffstat'], + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + # packages=find_packages(exclude=['tests']), + + # Alternatively, if you want to distribute just a my_module.py, uncomment + # this: + py_modules=["patch"], + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + # install_requires=get_requires('requirements.txt'), + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + #extras_require={ + # 'test': get_requires(os.path.join('tests', 'requirements_test.txt')) + #}, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + package_data={ + '': ['*.md'], + }, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + # data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + #entry_points={ + # 'console_scripts': [ + # 'patch.py=patch', + # ], + #}, +)