Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-patch-ng for openSUSE:Factory
checked in at 2025-11-21 16:56:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-patch-ng (Old)
and /work/SRC/openSUSE:Factory/.python-patch-ng.new.2061 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-patch-ng"
Fri Nov 21 16:56:38 2025 rev:4 rq:1318843 version:1.19.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-patch-ng/python-patch-ng.changes
2024-11-07 16:29:16.882376130 +0100
+++
/work/SRC/openSUSE:Factory/.python-patch-ng.new.2061/python-patch-ng.changes
2025-11-21 16:57:31.520501794 +0100
@@ -1,0 +2,11 @@
+Thu Nov 20 10:13:00 UTC 2025 - John Paul Adrian Glaubitz
<[email protected]>
+
+- Update to 1.19.0
+ * Fix: File move/rename is not supported when applying Git patch (#24)
+ * Fix: Changing file permissions is ignored when applying Git patch (#23)
+ * Fix: Git patch full index format is not recognized (#45)
+ * Fix: Better handling for big patch files (#34)
+ * Fix: Quoted file path is not parsed correctly
+- Use Python 3.11 on SLE-15 by default
+
+-------------------------------------------------------------------
Old:
----
python-patch-ng-1.18.1.tar.gz
New:
----
python-patch-ng-1.19.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-patch-ng.spec ++++++
--- /var/tmp/diff_new_pack.pAEN46/_old 2025-11-21 16:57:32.876558938 +0100
+++ /var/tmp/diff_new_pack.pAEN46/_new 2025-11-21 16:57:32.880559106 +0100
@@ -15,9 +15,9 @@
# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
-
+%{?sle15_python_module_pythons}
Name: python-patch-ng
-Version: 1.18.1
+Version: 1.19.0
Release: 0
Summary: Library to parse and apply unified diffs
License: MIT
++++++ python-patch-ng-1.18.1.tar.gz -> python-patch-ng-1.19.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/bug.yml
new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/bug.yml
--- old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/bug.yml 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/bug.yml 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,23 @@
+name: Bug Report
+description: Report a bug, something does not work as it's supposed to
+title: '[bug]'
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Describe the bug
+ description: Include the bug description and environment details
+ placeholder: |
+ Environment details: OS, Python version, patch-ng version, etc.
+ Description: xxxx
+ validations:
+ required: false
+ - type: textarea
+ id: steps
+ attributes:
+ label: How to reproduce it
+ description: It would be great to know how to reproduce it locally
+ placeholder: |
+ Commands to reproduce it, remote repository to use it locally, etc.
Every detail is more than welcome!
+ validations:
+ required: false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/feature_request.yml
new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/feature_request.yml
--- old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/feature_request.yml
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/feature_request.yml
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,17 @@
+name: Feature Request
+description: Request a new feature or suggest a change
+title: '[feature] SHORT DESCRIPTION'
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to submit a request.
+ **Please don't forget to update the issue title.**
+ - type: textarea
+ id: suggestion
+ attributes:
+ label: What is your suggestion?
+ description: Please be as specific as possible!
+ placeholder: Hi! I would like for patch-ng to be able to ...
+ validations:
+ required: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/question.yml
new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/question.yml
--- old/python-patch-ng-1.18.1/.github/ISSUE_TEMPLATE/question.yml
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/ISSUE_TEMPLATE/question.yml
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,17 @@
+name: Question
+description: If something needs clarification
+title: '[question] SHORT DESCRIPTION'
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill your question.
+ **Please don't forget to update the issue title.**
+ - type: textarea
+ id: question
+ attributes:
+ label: What is your question?
+ description: Please be as specific as possible!
+ placeholder: Hi! I have a question regarding ...
+ validations:
+ required: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/PULL_REQUEST_TEMPLATE.md
new/python-patch-ng-1.19.0/.github/PULL_REQUEST_TEMPLATE.md
--- old/python-patch-ng-1.18.1/.github/PULL_REQUEST_TEMPLATE.md 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/PULL_REQUEST_TEMPLATE.md 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,8 @@
+### Description of your Pull Request
+
+<!-- Replace this text with a description of your pull request. -->
+
+- [ ] Refer to the issue that supports this Pull Request.
+- [ ] If the issue has missing info, explain the purpose/use case/pain/need
that covers this Pull Request.
+- [ ] I've tested locally that my code works.
+- [ ] I've added relevant tests to verify that my code works.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/scripts/validate-cci-patch-ng.sh
new/python-patch-ng-1.19.0/.github/scripts/validate-cci-patch-ng.sh
--- old/python-patch-ng-1.18.1/.github/scripts/validate-cci-patch-ng.sh
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/scripts/validate-cci-patch-ng.sh
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Get TEST_ALL_RECIPES from environment variable, default to 0 (false)
+PYTHON_NG_TEST_ALL_RECIPES=${PYTHON_NG_TEST_ALL_RECIPES:-0}
+
+SAMPLE_RECIPES_NUM=30
+RECIPES_BUILD_NUM=10
+RECIPES_BUILT_COUNT=0
+
+# Ensure required tools are installed
+COMMANDS=("conan" "yq" "jq")
+for cmd in "${COMMANDS[@]}"; do
+ if ! which $cmd &> /dev/null; then
+ echo "ERROR: $cmd is not installed. Please install $cmd to proceed."
+ exit 1
+ fi
+done
+
+# Find all conanfile.py files that use apply_conandata_patches
+RECIPES=$(find . -type f -name "conanfile.py" -exec grep -l
"apply_conandata_patches(self)" {} + | sort | uniq)
+# And does not need system requirement
+RECIPES=$(grep -L "/system" $RECIPES)
+# And does not contain Conan 1 imports
+RECIPES=$(grep -L "from conans" $RECIPES)
+
+echo "Found $(echo "$RECIPES" | wc -l) recipes using apply_conandata_patches."
+
+if [ "${PYTHON_NG_TEST_ALL_RECIPES}" -eq "1" ]; then
+ SAMPLE_RECIPES_NUM=$(echo "$RECIPES" | wc -l)
+ RECIPES_BUILD_NUM=$SAMPLE_RECIPES_NUM
+ echo "PYTHON_NG_TEST_ALL_RECIPES is set to 1, testing all
$SAMPLE_RECIPES_NUM recipes."
+else
+ RECIPES=$(shuf -e ${RECIPES[@]} -n $SAMPLE_RECIPES_NUM)
+ echo "Pick $SAMPLE_RECIPES_NUM random recipes to test:"
+ echo "$RECIPES"
+fi
+
+# Run conan create for each sampled recipe
+for it in $RECIPES; do
+
+ if [ $RECIPES_BUILT_COUNT -ge $RECIPES_BUILD_NUM ]; then
+ echo "Reached the limit of $RECIPES_BUILD_NUM recipes built, stopping.
All done."
+ break
+ fi
+
+ recipe_dir=$(dirname "${it}")
+ pushd "$recipe_dir" > /dev/null
+ echo "Testing recipe in directory: ${recipe_dir}"
+ # Get a version from conandata.yml that uses a patch
+ version=$(yq '.patches | keys | .[0]' conandata.yml 2>/dev/null)
+ if [ -z "$version" ]; then
+ echo "ERROR: No patches found in conandata.yml for $recipe_dir,
skipping."
+ popd > /dev/null
+ continue
+ fi
+ version=$(echo ${version} | tr -d '"')
+ # Replace apply_conandata_patches to exit just after applying patches
+ sed -i -e 's/apply_conandata_patches(self)/apply_conandata_patches(self);
import sys; sys.exit(0)/g' conanfile.py
+
+ # Allow conan create to fail without stopping the script, we will handle
errors manually
+ set +e
+
+ # Create the package with the specified version
+ output=$(conan create . --version=${version} 2>&1)
+ # Accept some errors as non-fatal
+ if [ $? -ne 0 ]; then
+ echo "WARNING: conan create failed for $recipe_dir"
+ allowed_errors=(
+ "ERROR: There are invalid packages"
+ "ERROR: Version conflict"
+ "ERROR: Missing binary"
+ "Failed to establish a new connection"
+ "ConanException: sha256 signature failed"
+ "ConanException: Error downloading file"
+ "ConanException: Cannot find"
+ "certificate verify failed: certificate has expired"
+ "NotFoundException: Not found"
+ )
+ # check if any allowed error is in the output
+ if printf '%s\n' "${allowed_errors[@]}" | grep -q -f - <(echo
"$output"); then
+ echo "WARNING: Could not apply patches, skipping build:"
+ echo "$output" | tail -n 10
+ echo "-------------------------------------------------------"
+ else
+ echo "ERROR: Fatal error during conan create command execution:"
+ echo "$output"
+ popd > /dev/null
+ exit 1
+ fi
+ else
+ echo "INFO: Successfully patched $recipe_dir."
+ echo "$output" | tail -n 10
+ echo "-------------------------------------------------------"
+ RECIPES_BUILT_COUNT=$((RECIPES_BUILT_COUNT + 1))
+ fi
+ popd > /dev/null
+done
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/workflows/conan-center-index.yml
new/python-patch-ng-1.19.0/.github/workflows/conan-center-index.yml
--- old/python-patch-ng-1.18.1/.github/workflows/conan-center-index.yml
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/workflows/conan-center-index.yml
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,47 @@
+name: Validate Applying Patch in Conan Center Index
+
+on:
+ push:
+ paths-ignore:
+ - 'doc/**'
+ - '**/*.md'
+ - 'LICENSE'
+ - 'example/**'
+ - '.gitignore'
+ - 'tests/**'
+ workflow_dispatch:
+ pull_request:
+ paths-ignore:
+ - 'doc/**'
+ - '**/*.md'
+ - 'LICENSE'
+ - 'example/**'
+ - '.gitignore'
+ - 'tests/**'
+
+jobs:
+ conan-center-index-validate:
+ name: "Validate Patche-NG in Conan Center Index"
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ path: python-ng
+
+ - name: Setup Conan client
+ uses: conan-io/setup-conan@v1
+
+ - name: Setup Python NG
+ run: pip install -e ./python-ng
+
+ - name: Checkout conan-center-index
+ uses: actions/checkout@v5
+ with:
+ repository: conan-io/conan-center-index
+ path: conan-center-index
+ ref: 'd8efbb6f3c51b134205f01d3f8d90bdad1a67fe6'
+
+ - name: Validate Python-NG patch application
+ working-directory: conan-center-index/recipes
+ run: bash
"${GITHUB_WORKSPACE}/python-ng/.github/scripts/validate-cci-patch-ng.sh"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-patch-ng-1.18.1/.github/workflows/conan.yml
new/python-patch-ng-1.19.0/.github/workflows/conan.yml
--- old/python-patch-ng-1.18.1/.github/workflows/conan.yml 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/.github/workflows/conan.yml 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,48 @@
+name: Validate Conan client
+
+on:
+ push:
+ paths-ignore:
+ - 'doc/**'
+ - '**/*.md'
+ - 'LICENSE'
+ - 'example/**'
+ - '.gitignore'
+ - 'tests/**'
+ workflow_dispatch:
+ pull_request:
+ paths-ignore:
+ - 'doc/**'
+ - '**/*.md'
+ - 'LICENSE'
+ - 'example/**'
+ - '.gitignore'
+ - 'tests/**'
+
+jobs:
+ conan-client-validate:
+ name: "Validate Conan client"
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Setup python
+ uses: actions/setup-python@v6
+ with:
+ python-version: 3.12
+ architecture: x64
+
+ - name: Clone conan client
+ run: |
+ git clone --depth 1 https://github.com/conan-io/conan.git
+ cd conan/
+ pip install -r conans/requirements_dev.txt
+ pip install -r conans/requirements.txt
+
+ - name: Run Conan client tests involving patch-ng
+ run: |
+ pip install -e .
+ cd conan/
+ pytest -v test/functional/test_third_party_patch_flow.py
+ pytest -v test/functional/tools/test_files.py
+ pytest -v test/unittests/tools/files/test_patches.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/.github/workflows/workflow.yml
new/python-patch-ng-1.19.0/.github/workflows/workflow.yml
--- old/python-patch-ng-1.18.1/.github/workflows/workflow.yml 2024-10-25
14:05:56.000000000 +0200
+++ new/python-patch-ng-1.19.0/.github/workflows/workflow.yml 2025-10-08
16:28:08.000000000 +0200
@@ -1,4 +1,4 @@
-name: Main workflow
+name: Python Patch-NG Tests
on:
push:
@@ -21,15 +21,15 @@
jobs:
linux-validate:
name: Validate on Linux - Python ${{ matrix.python }}
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
- python: [ '3.6', '3.8', '3.12' ]
+ python: [ '3.7', '3.8', '3.12' ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Setup python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
architecture: x64
@@ -42,12 +42,12 @@
runs-on: windows-latest
strategy:
matrix:
- python: [ '3.6', '3.8', '3.12' ]
+ python: [ '3.7', '3.8', '3.12' ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Setup python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
architecture: x64
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-patch-ng-1.18.1/.gitignore
new/python-patch-ng-1.19.0/.gitignore
--- old/python-patch-ng-1.18.1/.gitignore 2024-10-25 14:05:56.000000000
+0200
+++ new/python-patch-ng-1.19.0/.gitignore 2025-10-08 16:28:08.000000000
+0200
@@ -1,5 +1,113 @@
-*.pyc
-build
-dist
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+venv/
+.venv/
+*.egg-info/
+.installed.cfg
+*.egg
+pip-wheel-metadata/
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+Icon?
+ehthumbs.db
+Thumbs.db
+
+.pydev*
+.project
+
+# IDEs #
+########
+.metadata
.idea
-patch_ng.egg-info
+.history/
+
+conan.conf
+*default_package_folder
+
+#Eclipse folder
+.settings
+
+#VScode folder
+.vscode
+
+#Generated certificate file
+cacert.pem
+
+#linux backup and vim files
+*~
+.*.sw?
+Session.vim
+
+#Pyinstaller generated binaries
+/pyinstaller
+
+# Run tests in docker in current dir
+.bash_history
+.conan_server/
+.sudo_as_admin_successful
+.noseids
+
+# add excluded
+!conans/client/build
+!conan/tools/build
+!test/unittests/client/build
+!test/unittests/tools/build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-patch-ng-1.18.1/example/example.py
new/python-patch-ng-1.19.0/example/example.py
--- old/python-patch-ng-1.18.1/example/example.py 2024-10-25
14:05:56.000000000 +0200
+++ new/python-patch-ng-1.19.0/example/example.py 2025-10-08
16:28:08.000000000 +0200
@@ -8,7 +8,7 @@
def emit(self, record):
logstr = self.format(record)
- print logstr
+ print(logstr)
patchlog = logging.getLogger("patch")
patchlog.handlers = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-patch-ng-1.18.1/patch_ng.py
new/python-patch-ng-1.19.0/patch_ng.py
--- old/python-patch-ng-1.18.1/patch_ng.py 2024-10-25 14:05:56.000000000
+0200
+++ new/python-patch-ng-1.19.0/patch_ng.py 2025-10-08 16:28:08.000000000
+0200
@@ -28,56 +28,23 @@
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__ = "Conan.io <[email protected]>"
-__version__ = "1.18.1"
+__version__ = "1.19.0"
__license__ = "MIT"
__url__ = "https://github.com/conan-io/python-patch"
+import codecs
import copy
+import io
import logging
-import re
-import tempfile
-import codecs
-
-# cStringIO doesn't support unicode in 2.5
-try:
- from StringIO import StringIO
-except ImportError:
- from io import BytesIO as StringIO # python 3
-try:
- import urllib2 as urllib_request
-except ImportError:
- import urllib.request as urllib_request
-
-from os.path import exists, isfile, abspath
import os
import posixpath
+import re
import shutil
-import sys
import stat
-
-
-PY3K = sys.version_info >= (3, 0)
-
-# PEP 3114
-if not PY3K:
- compat_next = lambda gen: gen.next()
-else:
- compat_next = lambda gen: gen.__next__()
-
-def tostr(b):
- """ Python 3 bytes encoder. Used to print filename in
- diffstat output. Assumes that filenames are in utf-8.
- """
- if not PY3K:
- return b
-
- # [ ] figure out how to print non-utf-8 filenames without
- # information loss
- return b.decode('utf-8')
-
+import tempfile
+import urllib.request
+from os.path import exists, isfile, abspath
#------------------------------------------------
# Logging is controlled by logger named after the
@@ -90,22 +57,10 @@
warning = logger.warning
error = logger.error
-class NullHandler(logging.Handler):
- """ Copied from Python 2.7 to avoid getting
- `No handlers could be found for logger "patch"`
- http://bugs.python.org/issue16539
- """
- def handle(self, record):
- pass
- def emit(self, record):
- pass
- def createLock(self):
- self.lock = None
-
streamhandler = logging.StreamHandler()
# initialize logger itself
-logger.addHandler(NullHandler())
+logger.addHandler(logging.NullHandler())
debugmode = False
@@ -194,11 +149,10 @@
"""
patchset = PatchSet()
debug("reading %s" % filename)
- fp = open(filename, "rb")
- res = patchset.parse(fp)
- fp.close()
- if res == True:
- return patchset
+ with open(filename, "rb") as fp:
+ res = patchset.parse(fp)
+ if res == True:
+ return patchset
return False
@@ -206,7 +160,7 @@
""" Parse text string and return PatchSet()
object (or False if parsing fails)
"""
- ps = PatchSet( StringIO(s) )
+ ps = PatchSet( io.BytesIO(s) )
if ps.errors == 0:
return ps
return False
@@ -217,7 +171,7 @@
if an error occured. Note that this also
can throw urlopen() exceptions.
"""
- ps = PatchSet( urllib_request.urlopen(url) )
+ ps = PatchSet( urllib.request.urlopen(url) )
if ps.errors == 0:
return ps
return False
@@ -261,15 +215,6 @@
return text.decode("utf-8", "ignore") # Ignore not compatible characters
-def to_file_bytes(content):
- if PY3K:
- if not isinstance(content, bytes):
- content = bytes(content, "utf-8")
- elif isinstance(content, unicode):
- content = content.encode("utf-8")
- return content
-
-
def load(path, binary=False):
""" Loads a file content """
with open(path, 'rb') as handle:
@@ -290,7 +235,9 @@
except Exception:
pass
- new_content = to_file_bytes(content)
+ new_content = content
+ if not isinstance(content, bytes):
+ new_content = bytes(content, "utf-8")
if only_if_modified and os.path.exists(path):
old_content = load(path, binary=True)
@@ -326,10 +273,11 @@
self.header = []
self.type = None
+ self.filemode = None
+ self.mode = None
def __iter__(self):
- for h in self.hunks:
- yield h
+ return iter(self.hunks)
class PatchSet(object):
@@ -344,6 +292,7 @@
self.name = None
# patch set type - one of constants
self.type = None
+ self.filemode = None
# list of Patch objects
self.items = []
@@ -359,8 +308,7 @@
return len(self.items)
def __iter__(self):
- for i in self.items:
- yield i
+ return iter(self.items)
def parse(self, stream):
""" parse unified diff
@@ -394,7 +342,7 @@
return False
try:
- self._lineno, self._line = compat_next(super(wrapumerate, self))
+ self._lineno, self._line = super(wrapumerate, self).__next__()
except StopIteration:
self._exhausted = True
self._line = False
@@ -431,6 +379,7 @@
header = []
srcname = None
tgtname = None
+ rename = False
# start of main cycle
# each parsing block already has line available in fe.line
@@ -441,19 +390,26 @@
# -- line fetched at the start of this cycle
if hunkparsed:
hunkparsed = False
+ rename = False
if re_hunk_start.match(fe.line):
hunkhead = True
elif fe.line.startswith(b"--- "):
filenames = True
+ elif fe.line.startswith(b"rename from "):
+ filenames = True
else:
headscan = True
# -- ------------------------------------
# read out header
if headscan:
- while not fe.is_empty and not fe.line.startswith(b"--- "):
- header.append(fe.line)
- fe.next()
+ while not fe.is_empty and not fe.line.startswith(b"--- ") and not
fe.line.startswith(b"rename from "):
+ header.append(fe.line)
+ fe.next()
+ if not fe.is_empty and fe.line.startswith(b"rename from "):
+ rename = True
+ hunkskip = True
+ hunkbody = False
if fe.is_empty:
if p is None:
debug("no patch data found") # error is shown later
@@ -548,7 +504,7 @@
# switch to hunkhead state
hunkskip = False
hunkhead = True
- elif line.startswith(b"--- "):
+ elif line.startswith(b"--- ") or line.startswith(b"rename from "):
# switch to filenames state
hunkskip = False
filenames = True
@@ -591,6 +547,50 @@
# switch back to headscan state
filenames = False
headscan = True
+ elif rename:
+ if line.startswith(b"rename from "):
+ re_rename_from = br"^rename from (.+)"
+ match = re.match(re_rename_from, line)
+ if match:
+ srcname = match.group(1).strip()
+ else:
+ warning("skipping invalid rename from at line %d" % (lineno+1))
+ self.errors += 1
+ # XXX p.header += line
+ # switch back to headscan state
+ filenames = False
+ headscan = True
+ if not fe.is_empty:
+ fe.next()
+ line = fe.line
+ lineno = fe.lineno
+ re_rename_to = br"^rename to (.+)"
+ match = re.match(re_rename_to, line)
+ if match:
+ tgtname = match.group(1).strip()
+ else:
+ warning("skipping invalid rename from at line %d" % (lineno +
1))
+ self.errors += 1
+ # XXX p.header += line
+ # switch back to headscan state
+ filenames = False
+ headscan = True
+ if p: # for the first run p is None
+ self.items.append(p)
+ p = Patch()
+ p.source = srcname
+ srcname = None
+ p.target = tgtname
+ tgtname = None
+ p.header = header
+ header = []
+ # switch to hunkhead state
+ filenames = False
+ hunkhead = False
+ nexthunkno = 0
+ p.hunkends = lineends.copy()
+ hunkparsed = True
+ continue
elif not line.startswith(b"+++ "):
if srcname != None:
warning("skipping invalid patch with no target for %s" % srcname)
@@ -715,6 +715,9 @@
# ---- detect patch and patchset types ----
for idx, p in enumerate(self.items):
self.items[idx].type = self._detect_type(p)
+ if self.items[idx].type == GIT:
+ self.items[idx].filemode = self._detect_file_mode(p)
+ self.items[idx].mode = self._detect_patch_mode(p)
types = set([p.type for p in self.items])
if len(types) > 1:
@@ -761,9 +764,34 @@
if p.header[idx].startswith(b"diff --git"):
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}|new file mode
\\d*)', p.header[idx+1])):
- if DVCS:
+ git_indicators = []
+ for i in range(idx + 1, len(p.header)):
+ git_indicators.append(p.header[i])
+ for line in git_indicators:
+ if re.match(
+ b'(?:index \\w{4,40}\\.\\.\\w{4,40}(?: \\d{6})?|new file
mode \\d+|deleted file mode \\d+|old mode \\d+|new mode \\d+)',
+ line):
+ if DVCS:
+ return GIT
+
+ # Additional check: look for mode change patterns
+ # "old mode XXXXX" followed by "new mode XXXXX"
+ has_old_mode = False
+ has_new_mode = False
+
+ for line in git_indicators:
+ if re.match(b'old mode \\d+', line):
+ has_old_mode = True
+ elif re.match(b'new mode \\d+', line):
+ has_new_mode = True
+
+ # If we have both old and new mode, it's definitely Git
+ if has_old_mode and has_new_mode and DVCS:
+ return GIT
+
+ # Check for similarity index (Git renames/copies)
+ for line in git_indicators:
+ if re.match(b'similarity index \\d+%', line):
return GIT
# HG check
@@ -788,6 +816,54 @@
return PLAIN
+ def _detect_file_mode(self, p):
+ """ Detect the file mode listed in the patch header
+
+ INFO: Only working with Git-style patches
+ """
+ if len(p.header) > 1:
+ for idx in reversed(range(len(p.header))):
+ if p.header[idx].startswith(b"diff --git"):
+ break
+ if p.header[idx].startswith(b'diff --git a/'):
+ if idx + 1 < len(p.header):
+ # new file (e.g)
+ # diff --git a/quote.txt b/quote.txt
+ # new file mode 100755
+ match = re.match(b'new file mode (\\d+)', p.header[idx + 1])
+ if match:
+ return int(match.group(1), 8)
+ # changed mode (e.g)
+ # diff --git a/quote.txt b/quote.txt
+ # old mode 100755
+ # new mode 100644
+ if idx + 2 < len(p.header):
+ match = re.match(b'new mode (\\d+)', p.header[idx + 2])
+ if match:
+ return int(match.group(1), 8)
+ return None
+
+ def _apply_filemode(self, filepath, filemode):
+ if filemode is not None and stat.S_ISREG(filemode):
+ try:
+ only_file_permissions = filemode & 0o777
+ os.chmod(filepath, only_file_permissions)
+ except Exception as error:
+ warning(f"Could not set filemode {oct(filemode)} for {filepath}:
{str(error)}")
+
+ def _detect_patch_mode(self, p):
+ """Detect patch mode - add, delete, rename, etc.
+ """
+ if len(p.header) > 1:
+ for idx in reversed(range(len(p.header))):
+ if p.header[idx].startswith(b"diff --git"):
+ break
+ change_pattern = re.compile(rb"^diff --git a/([^ ]+) b/(.+)")
+ match = change_pattern.match(p.header[idx])
+ if match:
+ if match.group(1) != match.group(2) and not p.hunks and p.source !=
b'/dev/null' and p.target != b'/dev/null':
+ return 'rename'
+ return None
def _normalize_filenames(self):
""" sanitize filenames, normalizing paths, i.e.:
@@ -805,6 +881,7 @@
for i,p in enumerate(self.items):
if debugmode:
debug(" patch type = %s" % p.type)
+ debug(" filemode = %s" % p.filemode)
debug(" source = %s" % p.source)
debug(" target = %s" % p.target)
if p.type in (HG, GIT):
@@ -823,6 +900,9 @@
p.source = xnormpath(p.source)
p.target = xnormpath(p.target)
+ p.source = p.source.strip(b'"')
+ p.target = p.target.strip(b'"')
+
sep = b'/' # sep value can be hardcoded, but it looks nice this way
# references to parent are not allowed
@@ -902,7 +982,7 @@
#print(iratio, dratio, iwidth, dwidth, histwidth)
hist = "+"*int(iwidth) + "-"*int(dwidth)
# -- /calculating +- histogram --
- output += (format % (tostr(names[i]), str(insert[i] + delete[i]), hist))
+ output += (format % (names[i].decode('utf-8'), str(insert[i] +
delete[i]), hist))
output += (" %d files changed, %d insertions(+), %d deletions(-), %+d
bytes"
% (len(names), sum(insert), sum(delete), delta))
@@ -981,9 +1061,17 @@
hunks = [s.decode("utf-8") for s in item.hunks[0].text]
new_file = "".join(hunk[1:] for hunk in hunks)
save(target, new_file)
+ self._apply_filemode(target, item.filemode)
elif "dev/null" in target:
source = self.strip_path(source, root, strip)
safe_unlink(source)
+ elif item.mode == 'rename':
+ source = self.strip_path(source, root, strip)
+ target = self.strip_path(target, root, strip)
+ if exists(source):
+ os.makedirs(os.path.dirname(target), exist_ok=True)
+ shutil.move(source, target)
+ self._apply_filemode(target, item.filemode)
else:
items.append(item)
self.items = items
@@ -1112,6 +1200,7 @@
else:
shutil.move(filenamen, backupname)
if self.write_hunks(backupname if filenameo == filenamen else
filenameo, filenamen, p.hunks):
+ self._apply_filemode(filenamen, p.filemode)
info("successfully patched %d/%d:\t %s" % (i+1, total, filenamen))
safe_unlink(backupname)
if new == b'/dev/null':
@@ -1270,15 +1359,11 @@
def write_hunks(self, srcname, tgtname, hunks):
- src = open(srcname, "rb")
- tgt = open(tgtname, "wb")
-
- debug("processing target file %s" % tgtname)
-
- tgt.writelines(self.patch_stream(src, hunks))
+ with open(srcname, "rb") as src, open(tgtname, "wb") as tgt:
+ debug("processing target file %s" % tgtname)
+
+ tgt.writelines(self.patch_stream(src, hunks))
- tgt.close()
- src.close()
# [ ] TODO: add test for permission copy
shutil.copymode(srcname, tgtname)
return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/06nested/tests/app/EVENT_LOOP.py
new/python-patch-ng-1.19.0/tests/06nested/tests/app/EVENT_LOOP.py
--- old/python-patch-ng-1.18.1/tests/06nested/tests/app/EVENT_LOOP.py
2024-10-25 14:05:56.000000000 +0200
+++ new/python-patch-ng-1.19.0/tests/06nested/tests/app/EVENT_LOOP.py
2025-10-08 16:28:08.000000000 +0200
@@ -21,8 +21,8 @@
class EVENT_LOOP(unittest.TestCase):
def t_scheduled(self, interval, iterations, sleep_time=0):
- print 'Test interval=%s, iterations=%s, sleep=%s' % (interval,
- iterations, sleep_time)
+ print('Test interval=%s, iterations=%s, sleep=%s' % (interval,
+ iterations, sleep_time))
warmup_iterations = iterations
self.last_t = 0.
@@ -76,6 +76,6 @@
if __name__ == '__main__':
if pyglet.version != '1.2dev':
- print 'Wrong version of pyglet imported; please check your PYTHONPATH'
+ print('Wrong version of pyglet imported; please check your PYTHONPATH')
else:
unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/emptypatches/create755.patch
new/python-patch-ng-1.19.0/tests/emptypatches/create755.patch
--- old/python-patch-ng-1.18.1/tests/emptypatches/create755.patch
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/emptypatches/create755.patch
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,21 @@
+From 39fdfb57a112a3b00cc352b45d17aba4f0f58005 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 12:39:25 +0200
+Subject: [PATCH] Add quotes.txt
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quote.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100755 quote.txt
+
+diff --git a/quote.txt b/quote.txt
+new file mode 100755
+index
0000000000000000000000000000000000000000..cbfafe956ec35385f5b728daa390603ff71f1933
+--- /dev/null
++++ b/quote.txt
+@@ -0,0 +1 @@
++post malam segetem, serendum est.
+--
+2.51.0
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/emptypatches/update644.patch
new/python-patch-ng-1.19.0/tests/emptypatches/update644.patch
--- old/python-patch-ng-1.18.1/tests/emptypatches/update644.patch
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/emptypatches/update644.patch
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,16 @@
+From 9a6f61c8cabffc01811605577d6d276a07c8bb95 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Fri, 3 Oct 2025 08:57:43 +0200
+Subject: [PATCH] Change file permission
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quote.txt | 0
+ 1 file changed, 0 insertions(+), 0 deletions(-)
+ mode change 100755 => 100644 quote.txt
+
+diff --git a/quote.txt b/quote.txt
+old mode 100755
+new mode 100644
+--
+2.51.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/filepermission/create755.patch
new/python-patch-ng-1.19.0/tests/filepermission/create755.patch
--- old/python-patch-ng-1.18.1/tests/filepermission/create755.patch
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/filepermission/create755.patch
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,21 @@
+From 39fdfb57a112a3b00cc352b45d17aba4f0f58005 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 12:39:25 +0200
+Subject: [PATCH] Add quotes.txt
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quote.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100755 quote.txt
+
+diff --git a/quote.txt b/quote.txt
+new file mode 100755
+index
0000000000000000000000000000000000000000..cbfafe956ec35385f5b728daa390603ff71f1933
+--- /dev/null
++++ b/quote.txt
+@@ -0,0 +1 @@
++post malam segetem, serendum est.
+--
+2.51.0
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/filepermission/update644.patch
new/python-patch-ng-1.19.0/tests/filepermission/update644.patch
--- old/python-patch-ng-1.18.1/tests/filepermission/update644.patch
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/filepermission/update644.patch
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,23 @@
+From 5f6ac26ddfe6ad80f76a1ec982abe95c11c7e947 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 15:56:37 +0200
+Subject: [PATCH] Read only
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quote.txt | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+ mode change 100755 => 100644 quote.txt
+
+diff --git a/quote.txt b/quote.txt
+old mode 100755
+new mode 100644
+index
cbfafe956ec35385f5b728daa390603ff71f1933..155913b0aafa16e4b37278209e772e946cecb393
+--- a/quote.txt
++++ b/quote.txt
+@@ -1 +1 @@
+-post malam segetem, serendum est.
++praestat cautela quam medela.
+--
+2.51.0
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/filewithspace/0001-quote.diff
new/python-patch-ng-1.19.0/tests/filewithspace/0001-quote.diff
--- old/python-patch-ng-1.18.1/tests/filewithspace/0001-quote.diff
1970-01-01 01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/filewithspace/0001-quote.diff
2025-10-08 16:28:08.000000000 +0200
@@ -0,0 +1,6 @@
+diff '--color=auto' -ruN "b/Wrapper/FreeImage.NET/cs/Samples/Sample 01 -
Loading and saving/Program.cs" "a/Wrapper/FreeImage.NET/cs/Samples/Sample 01 -
Loading and saving/Program.cs"
+--- "b/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and
saving/Program.cs" 2025-10-08 15:56:02.302486070 +0200
++++ "a/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and
saving/Program.cs" 2025-10-08 15:21:46.283174211 +0200
+@@ -1 +1 @@
+-feriunt summos, fulmina montes.
++lux oculorum laetificat animam.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/movefile/0001-quote.patch
new/python-patch-ng-1.19.0/tests/movefile/0001-quote.patch
--- old/python-patch-ng-1.18.1/tests/movefile/0001-quote.patch 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/movefile/0001-quote.patch 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,61 @@
+From 90ffe300b588f40f1409deb414498af8bc681072 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Fri, 3 Oct 2025 11:28:55 +0200
+Subject: [PATCH 1/3] Add file
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quotes.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100644 quotes.txt
+
+diff --git a/quotes.txt b/quotes.txt
+new file mode 100644
+index
0000000000000000000000000000000000000000..6da41619625400f0b6c99beccc47c328f3967366
+--- /dev/null
++++ b/quotes.txt
+@@ -0,0 +1 @@
++in herbis, salus.
+--
+2.51.0
+
+
+From a055d1be31d11c149bfd9ca88d9554199d57d444 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Fri, 3 Oct 2025 11:29:27 +0200
+Subject: [PATCH 2/3] Move file
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quotes.txt => quote/quotes.txt | 0
+ 1 file changed, 0 insertions(+), 0 deletions(-)
+ rename quotes.txt => quote/quotes.txt (100%)
+
+diff --git a/quotes.txt b/quote/quotes.txt
+similarity index 100%
+rename from quotes.txt
+rename to quote/quotes.txt
+--
+2.51.0
+
+
+From c275530ce83dc6eeeae671c2082660f1b6c16c4f Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Fri, 3 Oct 2025 11:30:14 +0200
+Subject: [PATCH 3/3] Update quote
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quote/quotes.txt | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/quote/quotes.txt b/quote/quotes.txt
+index
6da41619625400f0b6c99beccc47c328f3967366..928532e38b2a8e607814da280bb9e02862d2b4ea
100644
+--- a/quote/quotes.txt
++++ b/quote/quotes.txt
+@@ -1 +1 @@
+-in herbis, salus.
++dum tempus habemus, operemur bonum.
+--
+2.51.0
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/patchformat/create.patch
new/python-patch-ng-1.19.0/tests/patchformat/create.patch
--- old/python-patch-ng-1.18.1/tests/patchformat/create.patch 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/patchformat/create.patch 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,20 @@
+From 8cacee878aff9a79603c87366d3eeefa68d80cf3 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 10:59:19 +0200
+Subject: [PATCH] Add quotes.txt
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quotes.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100644 quotes.txt
+
+diff --git a/quotes.txt b/quotes.txt
+new file mode 100644
+index
0000000000000000000000000000000000000000..edcfcc5846847fffa4986b8437f1d7a3bd8aa8cf
+--- /dev/null
++++ b/quotes.txt
+@@ -0,0 +1 @@
++habita fides ipsam plerumque obligat fidem.
+--
+2.51.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/patchformat/remove.patch
new/python-patch-ng-1.19.0/tests/patchformat/remove.patch
--- old/python-patch-ng-1.18.1/tests/patchformat/remove.patch 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/patchformat/remove.patch 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,20 @@
+From 22751ffae72c0b955b55e68ec0e25a80e434e890 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 10:59:47 +0200
+Subject: [PATCH] Remove quotes.txt
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quotes.txt | 1 -
+ 1 file changed, 1 deletion(-)
+ delete mode 100644 quotes.txt
+
+diff --git a/quotes.txt b/quotes.txt
+deleted file mode 100644
+index
22174d6b96bbdd674d38f8be446d22758f996aba..0000000000000000000000000000000000000000
+--- a/quotes.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-Si vis pacem cole justitiam.
+--
+2.51.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python-patch-ng-1.18.1/tests/patchformat/update.patch
new/python-patch-ng-1.19.0/tests/patchformat/update.patch
--- old/python-patch-ng-1.18.1/tests/patchformat/update.patch 1970-01-01
01:00:00.000000000 +0100
+++ new/python-patch-ng-1.19.0/tests/patchformat/update.patch 2025-10-08
16:28:08.000000000 +0200
@@ -0,0 +1,19 @@
+From 3360e00554544de3a68a227ac9eff5bb687da723 Mon Sep 17 00:00:00 2001
+From: John Doe <[email protected]>
+Date: Wed, 1 Oct 2025 10:59:37 +0200
+Subject: [PATCH] Update quote
+
+Signed-off-by: John Doe <[email protected]>
+---
+ quotes.txt | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/quotes.txt b/quotes.txt
+index
edcfcc5846847fffa4986b8437f1d7a3bd8aa8cf..22174d6b96bbdd674d38f8be446d22758f996aba
100644
+--- a/quotes.txt
++++ b/quotes.txt
+@@ -1 +1 @@
+-habita fides ipsam plerumque obligat fidem.
++Si vis pacem cole justitiam.
+--
+2.51.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-patch-ng-1.18.1/tests/run_tests.py
new/python-patch-ng-1.19.0/tests/run_tests.py
--- old/python-patch-ng-1.18.1/tests/run_tests.py 2024-10-25
14:05:56.000000000 +0200
+++ new/python-patch-ng-1.19.0/tests/run_tests.py 2025-10-08
16:28:08.000000000 +0200
@@ -36,15 +36,11 @@
import re
import shutil
import unittest
-import copy
import stat
+import platform
from os import listdir, chmod
from os.path import abspath, dirname, exists, join, isdir, isfile
from tempfile import mkdtemp
-try:
- getcwdu = os.getcwdu
-except AttributeError:
- getcwdu = os.getcwd # python 3, where getcwd always returns a unicode object
verbose = False
if "-v" in sys.argv or "--verbose" in sys.argv:
@@ -150,7 +146,7 @@
# 3.
# test utility as a whole
patch_tool = join(dirname(TESTS), "patch_ng.py")
- save_cwd = getcwdu()
+ save_cwd = os.getcwd()
os.chdir(tmpdir)
extra = "-f" if "10fuzzy" in testname else ""
if verbose:
@@ -204,7 +200,7 @@
class TestCheckPatched(unittest.TestCase):
def setUp(self):
- self.save_cwd = getcwdu()
+ self.save_cwd = os.getcwd()
os.chdir(TESTS)
def tearDown(self):
@@ -355,7 +351,7 @@
class TestPatchApply(unittest.TestCase):
def setUp(self):
- self.save_cwd = getcwdu()
+ self.save_cwd = os.getcwd()
self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
os.chdir(self.tmpdir)
@@ -457,6 +453,176 @@
self.assertTrue(os.stat(some_file).st_mode, stat.S_IRUSR |
stat.S_IRGRP | stat.S_IROTH)
+class TestHugePatchFile(unittest.TestCase):
+
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ os.chdir(self.tmpdir)
+ self.huge_patchfile = self._create_huge_patchfile(self.tmpdir)
+
+ def _create_huge_patchfile(self, tmpdir):
+ """ Create a patch file with ~30MB of data
+ """
+ hugefile = join(tmpdir, 'hugefile')
+ with open(hugefile, 'wb') as f:
+ for i in range(2500000):
+ f.write(b'Line %d\n' % i)
+ huge_patchfile = join(tmpdir, 'huge.patch')
+ with open(huge_patchfile, 'wb') as f:
+ f.write(b'--- a/hugefile\n')
+ f.write(b'+++ b/hugefile\n')
+ f.write(b'@@ -1,2500000 +1,2500000 @@\n')
+ for i in range(2500000):
+ f.write(b' Line %d\n' % i)
+ return huge_patchfile
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ def test_apply_huge_patch(self):
+ """ Test that a huge patch file can be applied without issues
+ """
+ pto = patch_ng.fromfile(self.huge_patchfile)
+ self.assertTrue(pto.apply(root=self.tmpdir))
+
+
+class TestPreserveFilePermissions(unittest.TestCase):
+
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ shutil.copytree(join(TESTS, 'filepermission'), join(self.tmpdir,
'filepermission'))
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ @unittest.skipIf(platform.system() == "Windows", "File permission modes
are not supported on Windows")
+ def test_handle_full_index_patch_format(self):
+ """Test that when file permission mode is listed in the patch,
+ the same should be applied to the target file after patching.
+ """
+
+ os.chdir(self.tmpdir)
+ pto = patch_ng.fromfile(join(self.tmpdir, 'filepermission',
'create755.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertEqual(pto.items[0].filemode, 0o100755)
+ self.assertTrue(pto.apply())
+ self.assertTrue(os.path.exists(join(self.tmpdir, 'quote.txt')))
+ self.assertEqual(os.stat(join(self.tmpdir, 'quote.txt')).st_mode,
0o755 | stat.S_IFREG)
+
+ pto = patch_ng.fromfile(join(self.tmpdir, 'filepermission',
'update644.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertEqual(pto.items[0].filemode, 0o100644)
+ self.assertTrue(pto.apply())
+ self.assertEqual(os.stat(join(self.tmpdir, 'quote.txt')).st_mode,
0o644 | stat.S_IFREG)
+
+
+class TestPatchEmptyFile(unittest.TestCase):
+
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ shutil.copytree(join(TESTS, 'emptypatches'), join(self.tmpdir,
'emptypatches'))
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ @unittest.skipIf(platform.system() == "Windows", "File permission modes
are not supported on Windows")
+ @unittest.expectedFailure # FIXME:
https://github.com/conan-io/python-patch-ng/issues/35
+ def test_apply_patch_only_file_mode(self):
+ """Test when a patch file is empty in terms of content, but has file
+ permission mode listed in the patch, the same should be applied to
+ the target file after patching.
+ """
+
+ os.chdir(self.tmpdir)
+ pto = patch_ng.fromfile(join(self.tmpdir, 'emptypatches',
'create755.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertEqual(pto.items[0].filemode, 0o100755)
+ self.assertTrue(pto.apply())
+ self.assertTrue(os.path.exists(join(self.tmpdir, 'quote.txt')))
+ self.assertEqual(os.stat(join(self.tmpdir, 'quote.txt')).st_mode,
0o755 | stat.S_IFREG)
+
+ pto = patch_ng.fromfile(join(self.tmpdir, 'emptypatches',
'update644.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertEqual(pto.items[0].filemode, 0o100644)
+ self.assertTrue(pto.apply())
+ self.assertEqual(os.stat(join(self.tmpdir, 'quote.txt')).st_mode,
0o644 | stat.S_IFREG)
+
+class TestMoveAndPatch(unittest.TestCase):
+
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ shutil.copytree(join(TESTS, 'movefile'), join(self.tmpdir, 'movefile'))
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ def test_add_move_and_update_file(self):
+ """When a patch file contains a file move (rename) and an update to
the file,
+ the patch should be applied correctly.
+
+ Reported by https://github.com/conan-io/python-patch-ng/issues/24
+ """
+
+ os.chdir(self.tmpdir)
+ pto = patch_ng.fromfile(join(self.tmpdir, 'movefile',
'0001-quote.patch'))
+ self.assertEqual(len(pto), 3)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertEqual(pto.items[1].type, patch_ng.GIT)
+ self.assertEqual(pto.items[2].type, patch_ng.GIT)
+ self.assertEqual(pto.items[1].mode, 'rename')
+ self.assertTrue(pto.apply())
+ self.assertFalse(os.path.exists(join(self.tmpdir, 'quotes.txt')))
+ self.assertTrue(os.path.exists(join(self.tmpdir, 'quote',
'quotes.txt')))
+ with open(join(self.tmpdir, 'quote', 'quotes.txt'), 'rb') as f:
+ content = f.read()
+ self.assertTrue(b'dum tempus habemus, operemur bonum' in content)
+
+class TestPatchFileWithSpaces(unittest.TestCase):
+
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ shutil.copytree(join(TESTS, 'filewithspace'), join(self.tmpdir,
'filewithspace'))
+ patch_folder = join(self.tmpdir, "a", "Wrapper", "FreeImage.NET",
"cs", "Samples", "Sample 01 - Loading and saving")
+ os.makedirs(patch_folder, exist_ok=True)
+ self.program_cs = join(patch_folder, "Program.cs")
+ with open(self.program_cs, 'w') as fd:
+ fd.write("feriunt summos, fulmina montes.")
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ def test_patch_with_white_space(self):
+ """When a patch file is generated using `diff -ruN b/ a/` command, and
+ contains white spaces in the file path, the patch should be applied
correctly.
+
+ Reported by https://github.com/conan-io/conan/issues/16727
+ """
+
+ os.chdir(self.tmpdir)
+ print("TMPDIR:", self.tmpdir)
+ pto = patch_ng.fromfile(join(self.tmpdir, 'filewithspace',
'0001-quote.diff'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.PLAIN)
+ self.assertTrue(pto.apply())
+ with open(self.program_cs, 'rb') as f:
+ content = f.read()
+ self.assertTrue(b'lux oculorum laetificat animam.' in content)
+
+
class TestHelpers(unittest.TestCase):
# unittest setting
longMessage = True
@@ -488,6 +654,47 @@
self.assertEqual(patch_ng.pathstrip(b'path/name.diff', 1),
b'name.diff')
self.assertEqual(patch_ng.pathstrip(b'path/name.diff', 0),
b'path/name.diff')
+
+class TestPatchFormat(unittest.TestCase):
+ def setUp(self):
+ self.save_cwd = os.getcwd()
+ self.tmpdir = mkdtemp(prefix=self.__class__.__name__)
+ shutil.copytree(join(TESTS, 'patchformat'), join(self.tmpdir,
'patchformat'))
+
+ def tearDown(self):
+ os.chdir(self.save_cwd)
+ remove_tree_force(self.tmpdir)
+
+ def test_handle_full_index_patch_format(self):
+ """Test that a full index patch format is handled correctly
+
+ When parsing a git patch with a full index line (40 hex chars),
+ the patch type should be detected as GIT and the patch should be
+ applied correctly (create, update, remove a file).
+ """
+
+ os.chdir(self.tmpdir)
+ pto = patch_ng.fromfile(join(self.tmpdir, 'patchformat',
'create.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertTrue(pto.apply())
+ self.assertTrue(os.path.exists(join(self.tmpdir, 'quotes.txt')))
+
+ pto = patch_ng.fromfile(join(self.tmpdir, 'patchformat',
'update.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertTrue(pto.apply())
+ with open(join(self.tmpdir, 'quotes.txt'), 'rb') as f:
+ content = f.read()
+ self.assertTrue(b'Si vis pacem cole justitiam.' in content)
+
+ pto = patch_ng.fromfile(join(self.tmpdir, 'patchformat',
'remove.patch'))
+ self.assertEqual(len(pto), 1)
+ self.assertEqual(pto.items[0].type, patch_ng.GIT)
+ self.assertTrue(pto.apply())
+ self.assertFalse(os.path.exists(join(self.tmpdir, 'quotes.txt')))
+
+
def remove_tree_force(folder):
for root, _, files in os.walk(folder):
for it in files: