From 6fb231828ea845e7f0b80e52280f3efb98a25545 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 13:55:56 -0400 Subject: [PATCH 001/155] Reference HEAD in Makefile (more portable than head) This fixes "fatal: ambiguous argument 'head'", which occurs on some systems, inclding GNU/Linux systems, with "git rev-parse head". --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 389337d08..f3a5d3749 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,9 @@ clean: release: clean # Check if latest tag is the current head we're releasing echo "Latest tag = $$(git tag | sort -nr | head -n1)" - echo "HEAD SHA = $$(git rev-parse head)" + echo "HEAD SHA = $$(git rev-parse HEAD)" echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse head)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + @test "$$(git rev-parse HEAD)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" make force_release force_release: clean From 335d03b174569b4bc840e2020886a5f65adc56b7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 14:54:04 -0400 Subject: [PATCH 002/155] Have Makefile use git tag to sort the tags --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f3a5d3749..592f5658d 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,10 @@ clean: release: clean # Check if latest tag is the current head we're releasing - echo "Latest tag = $$(git tag | sort -nr | head -n1)" + echo "Latest tag = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1)" echo "HEAD SHA = $$(git rev-parse HEAD)" - echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse HEAD)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + echo "Latest tag SHA = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" + @test "$$(git rev-parse HEAD)" = "$$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" make force_release force_release: clean From b1c61d933d36a8e7fb6fb4109d2b07bd06bfbf32 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 05:44:16 -0400 Subject: [PATCH 003/155] Make "git tag" sort our SemVer-ish tags correctly This sorts numerically for each of major, minor, and patch, rather than, e.g., rating 2.1.15 as a higher version than 2.1.2. It also rates things like X-beta and X-rc as lower versions than X, but X-patched (not SemVer, but present in this project) as higher versions than X. --- Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 592f5658d..4e1927a9c 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,14 @@ clean: release: clean # Check if latest tag is the current head we're releasing - echo "Latest tag = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1)" - echo "HEAD SHA = $$(git rev-parse HEAD)" - echo "Latest tag SHA = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse HEAD)" = "$$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" + @config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ + latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ + head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ + printf '%-14s = %s\n' 'Latest tag' "$$latest_tag" \ + 'HEAD SHA' "$$head_sha" \ + 'Latest tag SHA' "$$latest_tag_sha" && \ + test "$$head_sha" = "$$latest_tag_sha" + make force_release force_release: clean From cc202cc0fcbbb365e86a8dbc99bb5d6381619671 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:13:44 -0400 Subject: [PATCH 004/155] Improve when and how Makefile suggests virtual env The avoids showing the message when the build command was already run in a virtual environment. It also keeps the command failing, so the subsequent twine command is not attempted. (Just adding "|| echo ..." caused the command to succeed, because "echo ..." itself succeeds except in the rare case it cannot write to standard output.) --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4e1927a9c..1c2c03a88 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,13 @@ release: clean force_release: clean # IF we're in a virtual environment, add build tools test -z "$$VIRTUAL_ENV" || pip install -U build twine - python3 -m build --sdist --wheel || echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" + + # Build the sdist and wheel that will be uploaded to PyPI. + python3 -m build --sdist --wheel || \ + test -z "$$VIRTUAL_ENV" && \ + echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ + false + + # Upload to PyPI and push the tag. twine upload dist/* git push --tags origin main From b54c34647bf6281d7f5f757d9850484bdb25f800 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:33:10 -0400 Subject: [PATCH 005/155] Use "python" in the virtual env, "python3" outside This changes the build command to run with "python" when in a virtual environment, since all virtual environments support this even when "python" outside it is absent or refers to the wrong version. On Windows, virtual environments don't contain a python3 command, but a global python3 command may be present, so the errors are confusing. This fixes that by avoiding such errors altogether. --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1c2c03a88..cd5eba36b 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,13 @@ force_release: clean test -z "$$VIRTUAL_ENV" || pip install -U build twine # Build the sdist and wheel that will be uploaded to PyPI. - python3 -m build --sdist --wheel || \ - test -z "$$VIRTUAL_ENV" && \ + if test -n "$$VIRTUAL_ENV"; then \ + python -m build --sdist --wheel; \ + else \ + python3 -m build --sdist --wheel || \ echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ - false + false; \ + fi # Upload to PyPI and push the tag. twine upload dist/* From ae9405a339e7b01fb539e1837e4c3f450250cfb7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:41:03 -0400 Subject: [PATCH 006/155] LF line endings for scripts that may need them This fixes how init-tests-after-clone.sh appears in .gitattributes so it gets LF (Unix-style) line endings on all systems as intended, and adds Makefile to be treated the same way. --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 6d2618f2f..739b2be29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ test/fixtures/* eol=lf -init-tests-after-clone.sh +init-tests-after-clone.sh eol=lf +Makefile eol=lf From f5da163bed2ababec006c0806187070341cc390e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 07:45:26 -0400 Subject: [PATCH 007/155] Have "make release" check other release preconditions As documented in the release instructions in README.md. --- Makefile | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index cd5eba36b..52d080788 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,29 @@ clean: rm -rf build/ dist/ .eggs/ .tox/ release: clean - # Check if latest tag is the current head we're releasing - @config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ + # Check that VERSION and changes.rst exist and have no uncommitted changes + test -f VERSION + test -f doc/source/changes.rst + git status -s VERSION doc/source/changes.rst + @test -z "$$(git status -s VERSION doc/source/changes.rst)" + + # Check that ALL changes are commited (can comment out if absolutely necessary) + git status -s + @test -z "$$(git status -s)" + + # Check that latest tag matches version and is the current head we're releasing + @version_file="$$(cat VERSION)" && \ + changes_file="$$(awk '/^[0-9]/ {print $$0; exit}' doc/source/changes.rst)" && \ + config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ - printf '%-14s = %s\n' 'Latest tag' "$$latest_tag" \ + printf '%-14s = %s\n' 'VERSION file' "$$version_file" \ + 'changes.rst' "$$changes_file" \ + 'Latest tag' "$$latest_tag" \ 'HEAD SHA' "$$head_sha" \ 'Latest tag SHA' "$$latest_tag_sha" && \ + test "$$version_file" = "$$changes_file" && \ + test "$$latest_tag" = "$$version_file" && \ test "$$head_sha" = "$$latest_tag_sha" make force_release From 5cf7f9777bc3f5bb92c57116071fd2bed3ac0b70 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 09:05:36 -0400 Subject: [PATCH 008/155] Fix non-venv branch always failing cc202cc put an end to the problem where, when run outside a virtual environment, it would always "succeed", because all "|| echo ..." required to succeed was for the echo command reporting the error to succeed. Unfortunately, that commit created the oppposite problem, causing that case to always fail! This commit fixes it, so it fails when there is an error, and succeeds when there is no error. --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 52d080788..fdefb0439 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,7 @@ force_release: clean python -m build --sdist --wheel; \ else \ python3 -m build --sdist --wheel || \ - echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ - false; \ + { echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && false; }; \ fi # Upload to PyPI and push the tag. From 35f233949ec0cef92ce4d51129de18f358f8f525 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 14 Sep 2023 08:48:45 +0200 Subject: [PATCH 009/155] delete sublime-text project, nobody uses it and it's probably very outdated --- etc/sublime-text/git-python.sublime-project | 62 --------------------- 1 file changed, 62 deletions(-) delete mode 100644 etc/sublime-text/git-python.sublime-project diff --git a/etc/sublime-text/git-python.sublime-project b/etc/sublime-text/git-python.sublime-project deleted file mode 100644 index 3dab9f656..000000000 --- a/etc/sublime-text/git-python.sublime-project +++ /dev/null @@ -1,62 +0,0 @@ -{ - "folders": - [ - // GIT-PYTHON - ///////////// - { - "follow_symlinks": true, - "path": "../..", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "git/ext", - "dist", - ".tox", - "doc/build", - "*.egg-info" - ] - }, - // GITDB - //////// - { - "follow_symlinks": true, - "path": "../../git/ext/gitdb", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "gitdb/ext", - "dist", - "doc/build", - ".tox", - ] - }, - // // SMMAP - // //////// - { - "follow_symlinks": true, - "path": "../../git/ext/gitdb/gitdb/ext/smmap", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - ] - }, - ] -} From 6495d84142b60ba81a5b4268a0dfc0785c22d60a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 04:24:17 -0400 Subject: [PATCH 010/155] Extract checks from release target to script This extracts the check logic from the release target in Makefile to a new script, check-version.sh. The code is also modified, mainly to account for different ways output is displayed and errors are reported and treated in a Makefile versus a standalone shell script. (The .sh suffix is for consistency with the naming of init-tests-after-clone.sh and is *not* intended to suggest sourcing the script; this script should be executed, not sourced.) --- .gitattributes | 1 + Makefile | 26 +------------------------- check-version.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 25 deletions(-) create mode 100755 check-version.sh diff --git a/.gitattributes b/.gitattributes index 739b2be29..eb503040b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ test/fixtures/* eol=lf init-tests-after-clone.sh eol=lf +check-version.sh eol=lf Makefile eol=lf diff --git a/Makefile b/Makefile index fdefb0439..d11dd4de9 100644 --- a/Makefile +++ b/Makefile @@ -7,31 +7,7 @@ clean: rm -rf build/ dist/ .eggs/ .tox/ release: clean - # Check that VERSION and changes.rst exist and have no uncommitted changes - test -f VERSION - test -f doc/source/changes.rst - git status -s VERSION doc/source/changes.rst - @test -z "$$(git status -s VERSION doc/source/changes.rst)" - - # Check that ALL changes are commited (can comment out if absolutely necessary) - git status -s - @test -z "$$(git status -s)" - - # Check that latest tag matches version and is the current head we're releasing - @version_file="$$(cat VERSION)" && \ - changes_file="$$(awk '/^[0-9]/ {print $$0; exit}' doc/source/changes.rst)" && \ - config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ - latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ - head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ - printf '%-14s = %s\n' 'VERSION file' "$$version_file" \ - 'changes.rst' "$$changes_file" \ - 'Latest tag' "$$latest_tag" \ - 'HEAD SHA' "$$head_sha" \ - 'Latest tag SHA' "$$latest_tag_sha" && \ - test "$$version_file" = "$$changes_file" && \ - test "$$latest_tag" = "$$version_file" && \ - test "$$head_sha" = "$$latest_tag_sha" - + ./check-version.sh make force_release force_release: clean diff --git a/check-version.sh b/check-version.sh new file mode 100755 index 000000000..5d3157033 --- /dev/null +++ b/check-version.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# This script checks if we appear ready to build and publish a new release. +# See the release instructions in README.md for the steps to make this pass. + +set -eEfuo pipefail +trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR + +readonly version_path='VERSION' +readonly changes_path='doc/source/changes.rst' + +printf 'Checking current directory.\n' +test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. + +printf 'Checking that %s and %s exist and have no committed changes.\n' \ + "$version_path" "$changes_path" +test -f "$version_path" +test -f "$changes_path" +git status -s -- "$version_path" "$changes_path" +test -z "$(git status -s -- "$version_path" "$changes_path")" + +# This section can be commented out, if absolutely necessary. +printf 'Checking that ALL changes are committed.\n' +git status -s +test -z "$(git status -s)" + +printf 'Gathering current version, latest tag, and current HEAD commit info.\n' +version_version="$(cat "$version_path")" +changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" +config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" +latest_tag="$(git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1)" +head_sha="$(git rev-parse HEAD)" +latest_tag_sha="$(git rev-parse "$latest_tag")" + +# Display a table of all the current version, tag, and HEAD commit information. +printf '%-14s = %s\n' 'VERSION file' "$version_version" \ + 'changes.rst' "$changes_version" \ + 'Latest tag' "$latest_tag" \ + 'HEAD SHA' "$head_sha" \ + 'Latest tag SHA' "$latest_tag_sha" + +# Check that latest tag matches version and is the current HEAD we're releasing +test "$version_version" = "$changes_version" +test "$latest_tag" = "$version_version" +test "$head_sha" = "$latest_tag_sha" +printf 'OK, everything looks good.\n' From 4b1c56409e905b852e3c93de142e109b147eee5e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 05:38:24 -0400 Subject: [PATCH 011/155] Extract build from force_release target to script This moves the conditional build dependency installation logic and build logic from the force_release Makefile target to a shell script build-release.sh, which force_release calls. The code is changed to clean it up, and also to account for differences between how output is displayed and errors reported in Makefiles and shell scripts. (As in check-version.sh, the .sh suffix does not signify anything about how the script is to be used: like the other shell scripts in the project, this should be executed, no sourced.) --- .gitattributes | 3 ++- Makefile | 13 +------------ build-release.sh | 22 ++++++++++++++++++++++ check-version.sh | 3 ++- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100755 build-release.sh diff --git a/.gitattributes b/.gitattributes index eb503040b..a66dc90ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ test/fixtures/* eol=lf init-tests-after-clone.sh eol=lf -check-version.sh eol=lf Makefile eol=lf +check-version.sh eol=lf +build-release.sh eol=lf diff --git a/Makefile b/Makefile index d11dd4de9..38090244c 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,6 @@ release: clean make force_release force_release: clean - # IF we're in a virtual environment, add build tools - test -z "$$VIRTUAL_ENV" || pip install -U build twine - - # Build the sdist and wheel that will be uploaded to PyPI. - if test -n "$$VIRTUAL_ENV"; then \ - python -m build --sdist --wheel; \ - else \ - python3 -m build --sdist --wheel || \ - { echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && false; }; \ - fi - - # Upload to PyPI and push the tag. + ./build-release.sh twine upload dist/* git push --tags origin main diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 000000000..ebb45e062 --- /dev/null +++ b/build-release.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# +# This script builds a release. If run in a venv, it auto-installs its tools. +# You may want to run "make release" instead of running this script directly. + +set -eEu + +if test -n "${VIRTUAL_ENV:-}"; then + deps=(build twine) # Install twine along with build, as we need it later. + printf 'Virtual environment detected. Adding packages: %s\n' "${deps[*]}" + pip install -U "${deps[@]}" + printf 'Starting the build.\n' + python -m build --sdist --wheel +else + suggest_venv() { + venv_cmd='python -m venv env && source env/bin/activate' + printf "Use a virtual-env with '%s' instead.\n" "$venv_cmd" + } + trap suggest_venv ERR # This keeps the original exit (error) code. + printf 'Starting the build.\n' + python3 -m build --sdist --wheel # Outside a venv, use python3. +fi diff --git a/check-version.sh b/check-version.sh index 5d3157033..802492d93 100755 --- a/check-version.sh +++ b/check-version.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # -# This script checks if we appear ready to build and publish a new release. +# This script checks if we are in a consistent state to build a new release. # See the release instructions in README.md for the steps to make this pass. +# You may want to run "make release" instead of running this script directly. set -eEfuo pipefail trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR From 729372f6f87639f0c4d8211ee7d173100117a257 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 07:16:08 -0400 Subject: [PATCH 012/155] Prevent buggy interaction between MinGW and WSL This changes the hashbangs in Makefile helper scripts to be static. Often, "#!/usr/bin/env bash" is a better hashbang for bash scripts than "#!/bin/bash", because it automatically works on Unix-like systems that are not GNU/Linux and do not have bash in /bin, but on which it has been installed in another $PATH directory, such as /usr/local/bin. (It can also be helpful on macOS, where /bin/bash is usually an old version of bash, while a package manager such as brew may have been used to install a newer version elsewhere.) Windows systems with WSL installed often have a deprecated bash.exe in the System32 directory that runs commands and scripts inside an installed WSL system. (wsl.exe should be used instead.) Anytime that bash is used due to a "#!/usr/bin/env bash" hashbang, it is wrong, because that only happens if the caller is some Unix-style script running natively or otherwise outside WSL. Normally this is not a reason to prefer a "#!/bin/bash" hashbang, because normally any environment in which one can run a script in a way that determines its interpreter from its hashbang is an environment in which a native (or otherwise appropriate) bash precedes the System32 bash in a PATH search. However, MinGW make, a popular make implementation used on Windows, is an exception. The goal of this change is not to sacrifice support for some Unix-like systems to better support Windows, which wouldn't necessarily be justified. Rather, this is to avoid extremely confusing wrong behavior that in some cases would have bizarre effects that are very hard to detect. I discovered this problem because the VIRTUAL_ENV variable was not inheried by the bash interpreter (because it was, fortunately, not passed through to WSL). But if "python3 -m build" finds a global "build" package, things might get much further before any problem is noticed. --- build-release.sh | 2 +- check-version.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-release.sh b/build-release.sh index ebb45e062..cbf0e91a9 100755 --- a/build-release.sh +++ b/build-release.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # # This script builds a release. If run in a venv, it auto-installs its tools. # You may want to run "make release" instead of running this script directly. diff --git a/check-version.sh b/check-version.sh index 802492d93..e74ec2606 100755 --- a/check-version.sh +++ b/check-version.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # # This script checks if we are in a consistent state to build a new release. # See the release instructions in README.md for the steps to make this pass. From ba84db487a31c593fe0618a63f80709e405039c9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 08:11:42 -0400 Subject: [PATCH 013/155] Fix message wording that was opposite of intended This also makes a correct but confusing comment clearer. --- check-version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check-version.sh b/check-version.sh index e74ec2606..373403c0e 100755 --- a/check-version.sh +++ b/check-version.sh @@ -13,7 +13,7 @@ readonly changes_path='doc/source/changes.rst' printf 'Checking current directory.\n' test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. -printf 'Checking that %s and %s exist and have no committed changes.\n' \ +printf 'Checking that %s and %s exist and have no uncommitted changes.\n' \ "$version_path" "$changes_path" test -f "$version_path" test -f "$changes_path" @@ -40,7 +40,7 @@ printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 'HEAD SHA' "$head_sha" \ 'Latest tag SHA' "$latest_tag_sha" -# Check that latest tag matches version and is the current HEAD we're releasing +# Check that the latest tag and current version match the HEAD we're releasing. test "$version_version" = "$changes_version" test "$latest_tag" = "$version_version" test "$head_sha" = "$latest_tag_sha" From de40e6864d5cbf8cd604b6f718b876c4d8c5a323 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 08:57:34 -0400 Subject: [PATCH 014/155] Ignore some other virtual environment directories Like ".venv" and "venv", ".env" and "env" are common, plus "env" appears in the example command shown for making a virtual environment for the purpose of building a release, under some circumstances when "make release" or "make force_release" fail. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 72da84eee..0bd307639 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.py[co] *.swp *~ +.env/ +env/ .venv/ venv/ /*.egg-info From 693d041869497137085171cdabbaf43e33fb9c84 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:40:36 +0200 Subject: [PATCH 015/155] make `.gitattributes` file more generic That way shell scripts will be handled correctly by default, anywhere. --- .gitattributes | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index a66dc90ca..3f3d2f050 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ test/fixtures/* eol=lf -init-tests-after-clone.sh eol=lf -Makefile eol=lf -check-version.sh eol=lf -build-release.sh eol=lf +*.sh eol=lf +/Makefile eol=lf From 962f747d9c2a877e70886f2ebee975b9be4bb672 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:47:31 +0200 Subject: [PATCH 016/155] submodules don't contribute to the release; ignore their changes --- check-version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check-version.sh b/check-version.sh index 373403c0e..81112b326 100755 --- a/check-version.sh +++ b/check-version.sh @@ -22,8 +22,8 @@ test -z "$(git status -s -- "$version_path" "$changes_path")" # This section can be commented out, if absolutely necessary. printf 'Checking that ALL changes are committed.\n' -git status -s -test -z "$(git status -s)" +git status -s --ignore-submodules +test -z "$(git status -s --ignore-submodules)" printf 'Gathering current version, latest tag, and current HEAD commit info.\n' version_version="$(cat "$version_path")" From d18d90a2c04abeff4bcc8d642fcd33be1c1eb35b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:53:55 +0200 Subject: [PATCH 017/155] Use 'echo' where possible to avoid explicit newlines --- check-version.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/check-version.sh b/check-version.sh index 81112b326..d29d457c0 100755 --- a/check-version.sh +++ b/check-version.sh @@ -5,27 +5,26 @@ # You may want to run "make release" instead of running this script directly. set -eEfuo pipefail -trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR +trap 'echo "$0: Check failed. Stopping." >&2' ERR readonly version_path='VERSION' readonly changes_path='doc/source/changes.rst' -printf 'Checking current directory.\n' +echo 'Checking current directory.' test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. -printf 'Checking that %s and %s exist and have no uncommitted changes.\n' \ - "$version_path" "$changes_path" +echo "Checking that $version_path and $changes_path exist and have no uncommitted changes." test -f "$version_path" test -f "$changes_path" git status -s -- "$version_path" "$changes_path" test -z "$(git status -s -- "$version_path" "$changes_path")" # This section can be commented out, if absolutely necessary. -printf 'Checking that ALL changes are committed.\n' +echo 'Checking that ALL changes are committed.' git status -s --ignore-submodules test -z "$(git status -s --ignore-submodules)" -printf 'Gathering current version, latest tag, and current HEAD commit info.\n' +echo 'Gathering current version, latest tag, and current HEAD commit info.' version_version="$(cat "$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" @@ -44,4 +43,4 @@ printf '%-14s = %s\n' 'VERSION file' "$version_version" \ test "$version_version" = "$changes_version" test "$latest_tag" = "$version_version" test "$head_sha" = "$latest_tag_sha" -printf 'OK, everything looks good.\n' +echo 'OK, everything looks good.' From 5919f8d04bccfaf0c98ae032437635d1a2de656b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 08:06:51 +0200 Subject: [PATCH 018/155] Be explicit on how to interpret the data table --- check-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check-version.sh b/check-version.sh index d29d457c0..6d10d6785 100755 --- a/check-version.sh +++ b/check-version.sh @@ -24,7 +24,6 @@ echo 'Checking that ALL changes are committed.' git status -s --ignore-submodules test -z "$(git status -s --ignore-submodules)" -echo 'Gathering current version, latest tag, and current HEAD commit info.' version_version="$(cat "$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" @@ -33,6 +32,7 @@ head_sha="$(git rev-parse HEAD)" latest_tag_sha="$(git rev-parse "$latest_tag")" # Display a table of all the current version, tag, and HEAD commit information. +echo $'\nThe VERSION must be the same in all locations, and so must the HEAD and tag SHA' printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 'changes.rst' "$changes_version" \ 'Latest tag' "$latest_tag" \ From 1e0b3f91f6dffad6bfc262528c14bf459c6a63c8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 08:32:30 +0200 Subject: [PATCH 019/155] refinements to `build-reelase.sh` - use `echo` where feasible to avoid explicit newlines - use `function` syntax out of habit - deduplicate release invocation - make `venv` based invocation less verbose - make help-text in non-venv more prominent --- build-release.sh | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/build-release.sh b/build-release.sh index cbf0e91a9..5840e4472 100755 --- a/build-release.sh +++ b/build-release.sh @@ -5,18 +5,22 @@ set -eEu +function release_with() { + $1 -m build --sdist --wheel +} + if test -n "${VIRTUAL_ENV:-}"; then deps=(build twine) # Install twine along with build, as we need it later. - printf 'Virtual environment detected. Adding packages: %s\n' "${deps[*]}" - pip install -U "${deps[@]}" - printf 'Starting the build.\n' - python -m build --sdist --wheel + echo "Virtual environment detected. Adding packages: ${deps[*]}" + pip install --quiet --upgrade "${deps[@]}" + echo 'Starting the build.' + release_with python else - suggest_venv() { + function suggest_venv() { venv_cmd='python -m venv env && source env/bin/activate' - printf "Use a virtual-env with '%s' instead.\n" "$venv_cmd" + printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" } trap suggest_venv ERR # This keeps the original exit (error) code. - printf 'Starting the build.\n' - python3 -m build --sdist --wheel # Outside a venv, use python3. + echo 'Starting the build.' + release_with python3 # Outside a venv, use python3. fi From ae8c018f3ab62f7ada3a56af29d4f647809654c6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 08:07:23 -0400 Subject: [PATCH 020/155] Fix URLs that were redirecting to another license All the opensource.org BSD license URLs at the top of source code files in this project had originally pointed to a page on the 3-clause BSD license that this project used and continues to use. But over time the site was apparently reorganized and the link became a redirect to the page about the 2-clause BSD license. Because it is identified only as the "BSD license" in the comments in this project that contain the links, this unfortunately makes it so those top-of-file comments all wrongly claim that the project is 2-clause BSD licensed. This fixes the links by replacing them with the current URL of the opensource.org page on the 3-clause BSD license. The current URL contains "bsd-3-clause" in it, so this specific problem is unlikely to recur with that URL (and even if it did, the text "bsd-3-clause is information that may clue readers in to what is going on). --- git/__init__.py | 2 +- git/cmd.py | 2 +- git/compat.py | 2 +- git/config.py | 2 +- git/diff.py | 2 +- git/exc.py | 2 +- git/index/base.py | 2 +- git/objects/base.py | 2 +- git/objects/blob.py | 2 +- git/objects/commit.py | 2 +- git/objects/tag.py | 2 +- git/objects/tree.py | 2 +- git/objects/util.py | 2 +- git/remote.py | 2 +- git/repo/base.py | 2 +- git/types.py | 2 +- git/util.py | 2 +- test/__init__.py | 2 +- test/lib/__init__.py | 2 +- test/lib/helper.py | 2 +- test/performance/test_commit.py | 2 +- test/test_actor.py | 2 +- test/test_base.py | 2 +- test/test_blob.py | 2 +- test/test_clone.py | 2 +- test/test_commit.py | 2 +- test/test_config.py | 2 +- test/test_db.py | 2 +- test/test_diff.py | 5 ++--- test/test_docs.py | 2 +- test/test_exc.py | 2 +- test/test_git.py | 2 +- test/test_index.py | 4 ++-- test/test_installation.py | 2 +- test/test_refs.py | 2 +- test/test_remote.py | 2 +- test/test_repo.py | 2 +- test/test_stats.py | 2 +- test/test_submodule.py | 2 +- test/test_tree.py | 2 +- test/test_util.py | 2 +- 41 files changed, 43 insertions(+), 44 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index 6196a42d7..e2d123fa5 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa # @PydevCodeAnalysisIgnore from git.exc import * # @NoMove @IgnorePep8 diff --git a/git/cmd.py b/git/cmd.py index d6f8f946a..9921dd6c9 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from __future__ import annotations import re import contextlib diff --git a/git/compat.py b/git/compat.py index e7ef28c30..624f26116 100644 --- a/git/compat.py +++ b/git/compat.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """utilities to help provide compatibility with python 3""" # flake8: noqa diff --git a/git/config.py b/git/config.py index 1973111eb..880eb9301 100644 --- a/git/config.py +++ b/git/config.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """Module containing module parser implementation able to properly read and write configuration files""" diff --git a/git/diff.py b/git/diff.py index 1424ff3ad..3e3de7bc1 100644 --- a/git/diff.py +++ b/git/diff.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import re from git.cmd import handle_process_output diff --git a/git/exc.py b/git/exc.py index 775528bf6..0786a8e8a 100644 --- a/git/exc.py +++ b/git/exc.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """ Module containing all exceptions thrown throughout the git package, """ from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 diff --git a/git/index/base.py b/git/index/base.py index 193baf3ad..cf016df6a 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from contextlib import ExitStack import datetime diff --git a/git/objects/base.py b/git/objects/base.py index eb9a8ac3d..1d07fd0f6 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.exc import WorkTreeRepositoryUnsupported from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex diff --git a/git/objects/blob.py b/git/objects/blob.py index 1881f210c..96ce486f5 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from mimetypes import guess_type from . import base diff --git a/git/objects/commit.py b/git/objects/commit.py index 6db3ea0f3..88c485d09 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import datetime import re from subprocess import Popen, PIPE diff --git a/git/objects/tag.py b/git/objects/tag.py index 3956a89e7..56fd05d1a 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """ Module containing all object based types. """ from . import base from .util import get_object_type_by_name, parse_actor_and_date diff --git a/git/objects/tree.py b/git/objects/tree.py index a9b491e23..4f490af54 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.util import IterableList, join_path import git.diff as git_diff diff --git a/git/objects/util.py b/git/objects/util.py index 56938507e..992a53d9c 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """Module for general utility functions""" # flake8: noqa F401 diff --git a/git/remote.py b/git/remote.py index 95a2b8ac6..fc2b2ceba 100644 --- a/git/remote.py +++ b/git/remote.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # Module implementing a remote object allowing easy access to git remotes import logging diff --git a/git/repo/base.py b/git/repo/base.py index 113fca459..fda3cdc88 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from __future__ import annotations import logging import os diff --git a/git/types.py b/git/types.py index 9f8621721..21276b5f1 100644 --- a/git/types.py +++ b/git/types.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa import os diff --git a/git/util.py b/git/util.py index 636e79806..638807a36 100644 --- a/git/util.py +++ b/git/util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from abc import abstractmethod import os.path as osp diff --git a/test/__init__.py b/test/__init__.py index 757cbad1f..a3d514523 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ diff --git a/test/lib/__init__.py b/test/lib/__init__.py index a4e57b8e0..299317c0b 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa import inspect diff --git a/test/lib/helper.py b/test/lib/helper.py index c04c5cd90..64c70a8f4 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib from functools import wraps import gc diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py index 38b529af7..dbe2ad43e 100644 --- a/test/performance/test_commit.py +++ b/test/performance/test_commit.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO from time import time import sys diff --git a/test/test_actor.py b/test/test_actor.py index ce0c74fc9..f495ac084 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase from git import Actor diff --git a/test/test_base.py b/test/test_base.py index 30029367d..b77c8117d 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import sys import tempfile diff --git a/test/test_blob.py b/test/test_blob.py index b94dcec23..692522b52 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase from git import Blob diff --git a/test/test_clone.py b/test/test_clone.py index 304ab33cb..1b4a6c332 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from pathlib import Path import re diff --git a/test/test_commit.py b/test/test_commit.py index 4871902ec..d13db1410 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import copy from datetime import datetime from io import BytesIO diff --git a/test/test_config.py b/test/test_config.py index b159ebe2d..481e129c6 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import glob import io diff --git a/test/test_db.py b/test/test_db.py index 228c70e7c..ebf73b535 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.db import GitCmdObjectDB from git.exc import BadObject from test.lib import TestBase diff --git a/test/test_diff.py b/test/test_diff.py index 504337744..dacbdc3bc 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import ddt import shutil import tempfile @@ -414,7 +414,7 @@ def test_diff_interface(self): @with_rw_directory def test_rename_override(self, rw_dir): - """Test disabling of diff rename detection""" + """Test disabling of diff rename detection""" # create and commit file_a.txt repo = Repo.init(rw_dir) @@ -480,4 +480,3 @@ def test_rename_override(self, rw_dir): self.assertEqual(True, diff.renamed_file) self.assertEqual('file_a.txt', diff.rename_from) self.assertEqual('file_b.txt', diff.rename_to) - diff --git a/test/test_docs.py b/test/test_docs.py index 20027c191..505b50f77 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import sys diff --git a/test/test_exc.py b/test/test_exc.py index f998ff4d5..9e125d246 100644 --- a/test/test_exc.py +++ b/test/test_exc.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009, 2016 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import re diff --git a/test/test_git.py b/test/test_git.py index 4d57a2d86..2c392155a 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import shutil import subprocess diff --git a/test/test_index.py b/test/test_index.py index 3bebb382b..9b7ba52a6 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO import os @@ -953,4 +953,4 @@ def test_index_add_pathlike(self, rw_repo): file = git_dir / "file.txt" file.touch() - rw_repo.index.add(file) \ No newline at end of file + rw_repo.index.add(file) diff --git a/test/test_installation.py b/test/test_installation.py index d856ebc94..1c9d2359c 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -1,5 +1,5 @@ # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import ast import os diff --git a/test/test_refs.py b/test/test_refs.py index e7526c3b2..afd273df9 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from itertools import chain from pathlib import Path diff --git a/test/test_remote.py b/test/test_remote.py index 9636ca486..e0dcb4131 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import random import tempfile diff --git a/test/test_repo.py b/test/test_repo.py index 08ed13a00..abae5ad78 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import glob import io from io import BytesIO diff --git a/test/test_stats.py b/test/test_stats.py index 1f6896555..335ce483b 100644 --- a/test/test_stats.py +++ b/test/test_stats.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase, fixture from git import Stats diff --git a/test/test_submodule.py b/test/test_submodule.py index 8c98a671e..5a7f26207 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib import os import shutil diff --git a/test/test_tree.py b/test/test_tree.py index 22c9c7d78..e59705645 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO from unittest import skipIf diff --git a/test/test_util.py b/test/test_util.py index c17efce35..517edd65c 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import pickle From e1af18377fd69f9c1007f8abf6ccb95b3c5a6558 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 09:42:40 -0400 Subject: [PATCH 021/155] Assorted small fixes/improvements to root dir docs This contains misc. formatting fixes and minor proofreading in the top-level documentation files, plus making the text in the LICENSE section of README.md have links both to external information about the license and to the license file itself. Note that the changes to the license file are just removal of excess whitespace (the extra blank line at the end, and spaces appearing at the end of lines). --- AUTHORS | 1 + CONTRIBUTING.md | 2 +- LICENSE | 35 +++++++++++++++++------------------ README.md | 7 ++++--- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/AUTHORS b/AUTHORS index ba5636db8..3e99ff785 100644 --- a/AUTHORS +++ b/AUTHORS @@ -52,4 +52,5 @@ Contributors are: -Joseph Hale -Santos Gallegos -Wenhan Zhu + Portions derived from other open source works and are clearly marked. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56af0df2a..e108f1b80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ The following is a short step-by-step rundown of what one typically would do to contribute. -- [fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. +- [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. - For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`. - Please try to **write a test that fails unless the contribution is present.** - Try to avoid massive commits and prefer to take small steps, with one commit for each. diff --git a/LICENSE b/LICENSE index 5a9a6f8d3..ba8a219fe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,29 @@ Copyright (C) 2008, 2009 Michael Trier and contributors All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of the GitPython project nor the names of -its contributors may be used to endorse or promote products derived +* Neither the name of the GitPython project nor the names of +its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/README.md b/README.md index ca470a851..69d69c56f 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Please have a look at the [contributions file][contributing]. - [User Documentation](http://gitpython.readthedocs.org) - [Questions and Answers](http://stackexchange.com/filters/167317/gitpython) -- Please post on stackoverflow and use the `gitpython` tag +- Please post on Stack Overflow and use the `gitpython` tag - [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues) - Post reproducible bugs and feature requests as a new issue. Please be sure to provide the following information if posting bugs: @@ -267,6 +267,7 @@ gpg --edit-key 4C08421980C9 ### LICENSE -New BSD License. See the LICENSE file. +[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file](https://github.com/gitpython-developers/GitPython/blob/main/license). -[contributing]: https://github.com/gitpython-developers/GitPython/blob/master/CONTRIBUTING.md +[contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md +[license]: https://github.com/gitpython-developers/GitPython/blob/main/license From ad76c99d73ecee3b834beea81ad78665b849adf8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 10:01:38 -0400 Subject: [PATCH 022/155] Use venv instead of virtualenv in test_installation This eliminates the test dependency on virtualenv by using the standard library venv module instead in test_installation. --- test-requirements.txt | 1 - test/test_installation.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 62f409824..b00dd6f06 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,4 +6,3 @@ pre-commit pytest pytest-cov pytest-sugar -virtualenv diff --git a/test/test_installation.py b/test/test_installation.py index d856ebc94..6cd97246e 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -4,6 +4,8 @@ import ast import os import subprocess +import sys + from git.compat import is_win from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -12,7 +14,7 @@ class TestInstallation(TestBase): def setUp_venv(self, rw_dir): self.venv = rw_dir - subprocess.run(["virtualenv", self.venv], stdout=subprocess.PIPE) + subprocess.run([sys.executable, "-m", "venv", self.venv], stdout=subprocess.PIPE) bin_name = "Scripts" if is_win else "bin" self.python = os.path.join(self.venv, bin_name, "python") self.pip = os.path.join(self.venv, bin_name, "pip") From 3fbbfd7770c734d2997f16a3f8967ae8f3910dd1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 23:25:19 -0400 Subject: [PATCH 023/155] Omit py_modules in setup This removes the py_modules keyword argument in the call to setup, and further shortens/simplifies setup.py by removing the now-unused build_py_modules function. The packages keyword argument already covers this, because we have no loose modules that are included in the distribution, and the call to find_packages: - Omits everything in test/ because it is directed to do so in the call. - Omits the gitdb/ directory (currently existing as a git submodule, not to be confused with Python submodules), because the ext/ directory that contains it does not itself directly contain an __init__.py file, so it is not a traditional package, yet ext/ is contained and found inside the directory git/ that *is* a traditional package, so the ext/ directory is not a namespace package either. - Includes all other modules, recursively, because they are all in a recursive traditional package structure under git/ that find_packages recognizes. To verify that this includes the same files in the built wheel and sdist distributions, I have listed the contents of the wheel with "unzip -l" and the sdist .tar.gz file with "tar tf" both before and after this change, verifying they list all the same entries. --- setup.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/setup.py b/setup.py index bc53bf6c8..90df8d7ea 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ from setuptools import setup, find_packages from setuptools.command.build_py import build_py as _build_py from setuptools.command.sdist import sdist as _sdist -import fnmatch import os import sys @@ -62,24 +61,6 @@ def _stamp_version(filename: str) -> None: print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr) -def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: - # create list of py_modules from tree - res = set() - _prefix = os.path.basename(basedir) - for root, _, files in os.walk(basedir): - for f in files: - _f, _ext = os.path.splitext(f) - if _ext not in [".py"]: - continue - _f = os.path.join(root, _f) - _f = os.path.relpath(_f, basedir) - _f = "{}.{}".format(_prefix, _f.replace(os.sep, ".")) - if any(fnmatch.fnmatch(_f, x) for x in excludes): - continue - res.add(_f) - return list(res) - - setup( name="GitPython", cmdclass={"build_py": build_py, "sdist": sdist}, @@ -91,7 +72,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: url="https://github.com/gitpython-developers/GitPython", packages=find_packages(exclude=["test", "test.*"]), include_package_data=True, - py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={"git": "git"}, python_requires=">=3.7", install_requires=requirements, From 407151878361badba63bb809c75cc3a877778de8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 17:57:38 -0400 Subject: [PATCH 024/155] Don't track code coverage temporary files While running tests with coverage enabled, files of the form .coverage.MachineName.####.###### are created temporarily. When tests complete normally (whether or not there are failures), these files are automatically removed. However, when tests are cancelled with SIGINT (Ctrl+C), they are sometimes retained. This adds another pattern to .gitignore so that, in addition to not tracking the .coverage file that is retained after tests, these other temporary files are also not tracked. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bd307639..e8b16da9d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ venv/ /lib/GitPython.egg-info cover/ .coverage +.coverage.* /build /dist /doc/_build From cd052b20a71bec0d605151eeb6b7ac87fbeb3e4a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 03:26:38 -0400 Subject: [PATCH 025/155] Start setting up tox It is not completely working yet. --- .gitignore | 1 - requirements-dev.txt | 3 --- tox.ini | 24 ++++++++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 0bd307639..139bf8ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,3 @@ nbproject .pytest_cache/ monkeytype.sqlite3 output.txt -tox.ini diff --git a/requirements-dev.txt b/requirements-dev.txt index f6705341c..e3030c597 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,6 +7,3 @@ flake8-type-checking;python_version>="3.8" # checks for TYPE_CHECKING only pytest-icdiff # pytest-profiling - - -tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..0b5591139 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +requires = tox>=4 +env_list = py{37,38,39,310,311,312}, lint, mypy, black + +[testenv] +description = Run unit tests +package = wheel +extras = test +commands = pytest --color=yes {posargs} + +[testenv:lint] +description = Lint via pre-commit +basepython = py39 +commands = pre-commit run --all-files + +[testenv:mypy] +description = Typecheck with mypy +basepython = py39 +commands = mypy -p git + +[testenv:black] +description = Check style with black +basepython = py39 +commands = black --check --diff git From 2cc2db77574b8197cce215ab703c9d383ea645c1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 19:49:00 -0400 Subject: [PATCH 026/155] Pass through SSH_ env vars to tox envs This fixes a problem where the tests for fetching a nonexistent ref prompt like "Enter passphrase for key '/home/USERNAME/.ssh/id_rsa':" and block, if the repository on the machine where the tests are being run has the remote set up using an SSH URL. This passes through all environment variables whose names start with SSH_, even though it should be enough to pass SSH_AGENT_PID and SSH_AUTH_SOCK through, at least for this particular issue. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 0b5591139..9b918f560 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ env_list = py{37,38,39,310,311,312}, lint, mypy, black description = Run unit tests package = wheel extras = test +pass_env = SSH_* commands = pytest --color=yes {posargs} [testenv:lint] From 4bea7cf4cfdbb9d69f24a245bdc8a7e0638524a2 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 20:36:31 -0400 Subject: [PATCH 027/155] Don't have mypy failure fail the whole tox run Other environments would still be run even after mypy has failed, but to avoid having tox runs be unnecessarily inconsistent with the mypy step in the pythonpackage.yml CI workflow, and also because GitPython is not currently expected to pass mypy checks, this keeps mypy errors from causing the whole tox run to be reported as failed. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9b918f560..a81cd2b45 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = pre-commit run --all-files description = Typecheck with mypy basepython = py39 commands = mypy -p git +ignore_outcome = true [testenv:black] description = Check style with black From e6ec6c87b8ed66e30f7addbd109ab6ec5d74326c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 22:58:58 -0400 Subject: [PATCH 028/155] Add tox environment to build HTML documentation The main use of this, similar to the step at the end of pythonpackage.yml, is to find errors produced by building. However, actual documentation *is* built, and unlike other tox environments, running this one actually writes outside the .tox/ directory, creating the documentation in the usual target location. For that reason, this environment is omitted from the env_list, so that it does not run by default and unexpectedly overwrite documentation that may recently have been built before changes are made that could cause generated documentation to be different. --- tox.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index a81cd2b45..8d64b929b 100644 --- a/tox.ini +++ b/tox.ini @@ -11,16 +11,25 @@ commands = pytest --color=yes {posargs} [testenv:lint] description = Lint via pre-commit -basepython = py39 +base_python = py39 commands = pre-commit run --all-files [testenv:mypy] description = Typecheck with mypy -basepython = py39 +base_python = py39 commands = mypy -p git ignore_outcome = true [testenv:black] description = Check style with black -basepython = py39 +base_python = py39 commands = black --check --diff git + +# Run "tox -e html" for this. It is deliberately excluded from env_list, as +# unlike the other environments, this one writes outside the .tox/ directory. +[testenv:html] +description = Build HTML documentation +base_python = py39 +deps = -r doc/requirements.txt +allowlist_externals = make +commands = make -C doc html From a774182c05731797cfe3353ff9618c0cd80b6f35 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:02:52 -0400 Subject: [PATCH 029/155] Fix black exclusions to omit .gitignore dirs This replaces "exclude" with "extend-exclude" in the black configuration, so that it keeps its default exclusions, of which all directories listed in .gitignore are automatically a part. That makes it possible to run "black ." to format just the files that should be formatted (git/ files, test/ files, and setup.py), while automatically omitting .venv/, .tox/, build/, and so on. This commit does not change how black is run yet, it just fixes the way its exclusions are configured. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 42bb31eda..fa06458eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,4 @@ omit = ["*/git/ext/*"] [tool.black] line-length = 120 target-version = ['py37'] -exclude = "git/ext/gitdb" +extend-exclude = "git/ext/gitdb" From e39ecb7269fe266311a4bf766c626de5b95a9f9f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:06:58 -0400 Subject: [PATCH 030/155] Small manual formatting improvements This adds a trailing "," in a few multi-line function calls in test/, where putting one argument per line was intended and is clearer. This is so that when black is run over test/, it recognizes the form and avoids collapsing it. --- test/test_commit.py | 4 ++-- test/test_docs.py | 2 +- test/test_repo.py | 2 +- test/test_submodule.py | 2 +- test/test_util.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_commit.py b/test/test_commit.py index d13db1410..560497547 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -173,12 +173,12 @@ def check_entries(path, changes): ".github/workflows/Future.yml" : { 'insertions': 57, 'deletions': 0, - 'lines': 57 + 'lines': 57, }, ".github/workflows/test_pytest.yml" : { 'insertions': 0, 'deletions': 55, - 'lines': 55 + 'lines': 55, }, } assert path in expected diff --git a/test/test_docs.py b/test/test_docs.py index 505b50f77..4c23e9f81 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -481,7 +481,7 @@ def test_references_and_objects(self, rw_dir): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find SHA for submodule", - raises=ValueError + raises=ValueError, ) def test_submodules(self): # [1-test_submodules] diff --git a/test/test_repo.py b/test/test_repo.py index abae5ad78..1b46fba7c 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1115,7 +1115,7 @@ def test_repo_odbtype(self): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find submodule SHA", - raises=ValueError + raises=ValueError, ) def test_submodules(self): self.assertEqual(len(self.rorepo.submodules), 1) # non-recursive diff --git a/test/test_submodule.py b/test/test_submodule.py index 5a7f26207..f7195626f 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -469,7 +469,7 @@ def test_base_bare(self, rwrepo): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find submodule SHA", - raises=ValueError + raises=ValueError, ) @skipIf( HIDE_WINDOWS_KNOWN_ERRORS, diff --git a/test/test_util.py b/test/test_util.py index 517edd65c..42edc57cf 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -159,7 +159,7 @@ def test_lock_file(self): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin fails here for some reason, always", - raises=AssertionError + raises=AssertionError, ) def test_blocking_lock_file(self): my_file = tempfile.mktemp() From 288cf03e120ed6f7e62d6b0e5c974649e50e69de Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:09:08 -0400 Subject: [PATCH 031/155] Don't limit black to git/ This changes the documentation in README.md to recommend running "black ." and changes the command to that in tox.ini, so that more paths are covered (in practice, test/ and setup.py). --- README.md | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69d69c56f..dbec36024 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ mypy -p git For automatic code formatting, run: ```bash -black git +black . ``` Configuration for flake8 is in the `./.flake8` file. diff --git a/tox.ini b/tox.ini index 8d64b929b..82a41e22c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ ignore_outcome = true [testenv:black] description = Check style with black base_python = py39 -commands = black --check --diff git +commands = black --check --diff . # Run "tox -e html" for this. It is deliberately excluded from env_list, as # unlike the other environments, this one writes outside the .tox/ directory. From 15c736dc79922a1cead221f6fbda5378564e0b6d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 10:20:28 -0400 Subject: [PATCH 032/155] Reformat tests with black This actually runs black on the whole project, but the only changes are in test/ (as expected). --- test/performance/test_streams.py | 1 - test/test_commit.py | 17 ++++++++-------- test/test_diff.py | 28 +++++++++++++-------------- test/test_index.py | 2 +- test/test_quick_doc.py | 27 ++++++++++++-------------- test/test_repo.py | 28 ++++++++++++++++----------- test/test_submodule.py | 33 ++++++++++++++++++++++---------- 7 files changed, 75 insertions(+), 61 deletions(-) diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py index 5588212e0..25e081578 100644 --- a/test/performance/test_streams.py +++ b/test/performance/test_streams.py @@ -15,7 +15,6 @@ class TestObjDBPerformance(TestBigRepoR): - large_data_size_bytes = 1000 * 1000 * 10 # some MiB should do it moderate_data_size_bytes = 1000 * 1000 * 1 # just 1 MiB diff --git a/test/test_commit.py b/test/test_commit.py index 560497547..f6fb49d50 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -93,7 +93,6 @@ def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info= class TestCommit(TestCommitSerialization): def test_bake(self): - commit = self.rorepo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") # commits have no dict self.assertRaises(AttributeError, setattr, commit, "someattr", 1) @@ -170,15 +169,15 @@ def test_renames(self): def check_entries(path, changes): expected = { - ".github/workflows/Future.yml" : { - 'insertions': 57, - 'deletions': 0, - 'lines': 57, + ".github/workflows/Future.yml": { + "insertions": 57, + "deletions": 0, + "lines": 57, }, - ".github/workflows/test_pytest.yml" : { - 'insertions': 0, - 'deletions': 55, - 'lines': 55, + ".github/workflows/test_pytest.yml": { + "insertions": 0, + "deletions": 55, + "lines": 55, }, } assert path in expected diff --git a/test/test_diff.py b/test/test_diff.py index dacbdc3bc..9c3888f03 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -419,7 +419,7 @@ def test_rename_override(self, rw_dir): # create and commit file_a.txt repo = Repo.init(rw_dir) file_a = osp.join(rw_dir, "file_a.txt") - with open(file_a, "w", encoding='utf-8') as outfile: + with open(file_a, "w", encoding="utf-8") as outfile: outfile.write("hello world\n") repo.git.add(Git.polish_url(file_a)) repo.git.commit(message="Added file_a.txt") @@ -429,21 +429,21 @@ def test_rename_override(self, rw_dir): # create and commit file_b.txt with similarity index of 52 file_b = osp.join(rw_dir, "file_b.txt") - with open(file_b, "w", encoding='utf-8') as outfile: + with open(file_b, "w", encoding="utf-8") as outfile: outfile.write("hello world\nhello world") repo.git.add(Git.polish_url(file_b)) repo.git.commit(message="Removed file_a.txt. Added file_b.txt") - commit_a = repo.commit('HEAD') - commit_b = repo.commit('HEAD~1') + commit_a = repo.commit("HEAD") + commit_b = repo.commit("HEAD~1") # check default diff command with renamed files enabled diffs = commit_b.diff(commit_a) self.assertEqual(1, len(diffs)) diff = diffs[0] self.assertEqual(True, diff.renamed_file) - self.assertEqual('file_a.txt', diff.rename_from) - self.assertEqual('file_b.txt', diff.rename_to) + self.assertEqual("file_a.txt", diff.rename_from) + self.assertEqual("file_b.txt", diff.rename_to) # check diff with rename files disabled diffs = commit_b.diff(commit_a, no_renames=True) @@ -452,31 +452,31 @@ def test_rename_override(self, rw_dir): # check fileA.txt deleted diff = diffs[0] self.assertEqual(True, diff.deleted_file) - self.assertEqual('file_a.txt', diff.a_path) + self.assertEqual("file_a.txt", diff.a_path) # check fileB.txt added diff = diffs[1] self.assertEqual(True, diff.new_file) - self.assertEqual('file_b.txt', diff.a_path) + self.assertEqual("file_b.txt", diff.a_path) # check diff with high similarity index - diffs = commit_b.diff(commit_a, split_single_char_options=False, M='75%') + diffs = commit_b.diff(commit_a, split_single_char_options=False, M="75%") self.assertEqual(2, len(diffs)) # check fileA.txt deleted diff = diffs[0] self.assertEqual(True, diff.deleted_file) - self.assertEqual('file_a.txt', diff.a_path) + self.assertEqual("file_a.txt", diff.a_path) # check fileB.txt added diff = diffs[1] self.assertEqual(True, diff.new_file) - self.assertEqual('file_b.txt', diff.a_path) + self.assertEqual("file_b.txt", diff.a_path) # check diff with low similarity index - diffs = commit_b.diff(commit_a, split_single_char_options=False, M='40%') + diffs = commit_b.diff(commit_a, split_single_char_options=False, M="40%") self.assertEqual(1, len(diffs)) diff = diffs[0] self.assertEqual(True, diff.renamed_file) - self.assertEqual('file_a.txt', diff.rename_from) - self.assertEqual('file_b.txt', diff.rename_to) + self.assertEqual("file_a.txt", diff.rename_from) + self.assertEqual("file_b.txt", diff.rename_to) diff --git a/test/test_index.py b/test/test_index.py index 9b7ba52a6..fba9c78ec 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -946,7 +946,7 @@ def test_commit_msg_hook_fail(self, rw_repo): else: raise AssertionError("Should have caught a HookExecutionError") - @with_rw_repo('HEAD') + @with_rw_repo("HEAD") def test_index_add_pathlike(self, rw_repo): git_dir = Path(rw_repo.git_dir) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index eaee4e581..9dc7b8d2e 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -13,14 +13,13 @@ def tearDown(self): @with_rw_directory def test_init_repo_object(self, path_to_dir): - # [1-test_init_repo_object] # $ git init from git import Repo repo = Repo.init(path_to_dir) - # ![1-test_init_repo_object] + # ![1-test_init_repo_object] # [2-test_init_repo_object] repo = Repo(path_to_dir) @@ -28,9 +27,9 @@ def test_init_repo_object(self, path_to_dir): @with_rw_directory def test_cloned_repo_object(self, local_dir): - from git import Repo import git + # code to clone from url # [1-test_cloned_repo_object] # $ git clone @@ -44,9 +43,9 @@ def test_cloned_repo_object(self, local_dir): # [2-test_cloned_repo_object] # We must make a change to a file so that we can add the update to git - update_file = 'dir1/file2.txt' # we'll use local_dir/dir1/file2.txt - with open(f"{local_dir}/{update_file}", 'a') as f: - f.write('\nUpdate version 2') + update_file = "dir1/file2.txt" # we'll use local_dir/dir1/file2.txt + with open(f"{local_dir}/{update_file}", "a") as f: + f.write("\nUpdate version 2") # ![2-test_cloned_repo_object] # [3-test_cloned_repo_object] @@ -82,7 +81,7 @@ def test_cloned_repo_object(self, local_dir): # Untracked files - create new file # [7-test_cloned_repo_object] - f = open(f'{local_dir}/untracked.txt', 'w') # creates an empty file + f = open(f"{local_dir}/untracked.txt", "w") # creates an empty file f.close() # ![7-test_cloned_repo_object] @@ -95,8 +94,8 @@ def test_cloned_repo_object(self, local_dir): # [9-test_cloned_repo_object] # Let's modify one of our tracked files - with open(f'{local_dir}/Downloads/file3.txt', 'w') as f: - f.write('file3 version 2') # overwrite file 3 + with open(f"{local_dir}/Downloads/file3.txt", "w") as f: + f.write("file3 version 2") # overwrite file 3 # ![9-test_cloned_repo_object] # [10-test_cloned_repo_object] @@ -126,7 +125,7 @@ def test_cloned_repo_object(self, local_dir): # ![11.1-test_cloned_repo_object] # [11.2-test_cloned_repo_object] # lets add untracked.txt - repo.index.add(['untracked.txt']) + repo.index.add(["untracked.txt"]) diffs = repo.index.diff(repo.head.commit) for d in diffs: print(d.a_path) @@ -146,9 +145,7 @@ def test_cloned_repo_object(self, local_dir): # dir1/file2.txt # ![11.3-test_cloned_repo_object] - - - '''Trees and Blobs''' + """Trees and Blobs""" # Latest commit tree # [12-test_cloned_repo_object] @@ -195,7 +192,7 @@ def print_files_from_git(root, level=0): # Printing text files # [17-test_cloned_repo_object] - print_file = 'dir1/file2.txt' + print_file = "dir1/file2.txt" tree[print_file] # the head commit tree # Output @@ -221,4 +218,4 @@ def print_files_from_git(root, level=0): # Output # file 2 version 1 - # ![18.1-test_cloned_repo_object] \ No newline at end of file + # ![18.1-test_cloned_repo_object] diff --git a/test/test_repo.py b/test/test_repo.py index 1b46fba7c..6432b8c6f 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -251,7 +251,9 @@ def test_clone_from_with_path_contains_unicode(self): self.fail("Raised UnicodeEncodeError") @with_rw_directory - @skip("the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control") + @skip( + "the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control" + ) def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" try: @@ -391,7 +393,9 @@ def test_clone_from_unsafe_options_allowed(self, rw_repo): for i, unsafe_option in enumerate(unsafe_options): destination = tmp_dir / str(i) assert not destination.exists() - Repo.clone_from(rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True) + Repo.clone_from( + rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True + ) assert destination.exists() @with_rw_repo("HEAD") @@ -755,8 +759,8 @@ def test_blame_complex_revision(self, git): @mock.patch.object(Git, "_call_process") def test_blame_accepts_rev_opts(self, git): res = self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) - expected_args = ['blame', 'HEAD', '-M', '-C', '-C', '--', 'README.md'] - boilerplate_kwargs = {"p" : True, "stdout_as_string": False} + expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] + boilerplate_kwargs = {"p": True, "stdout_as_string": False} git.assert_called_once_with(*expected_args, **boilerplate_kwargs) @skipIf( @@ -1415,14 +1419,16 @@ def test_ignored_items_reported(self): gi = tmp_dir / "repo" / ".gitignore" - with open(gi, 'w') as file: - file.write('ignored_file.txt\n') - file.write('ignored_dir/\n') + with open(gi, "w") as file: + file.write("ignored_file.txt\n") + file.write("ignored_dir/\n") - assert temp_repo.ignored(['included_file.txt', 'included_dir/file.txt']) == [] - assert temp_repo.ignored(['ignored_file.txt']) == ['ignored_file.txt'] - assert temp_repo.ignored(['included_file.txt', 'ignored_file.txt']) == ['ignored_file.txt'] - assert temp_repo.ignored(['included_file.txt', 'ignored_file.txt', 'included_dir/file.txt', 'ignored_dir/file.txt']) == ['ignored_file.txt', 'ignored_dir/file.txt'] + assert temp_repo.ignored(["included_file.txt", "included_dir/file.txt"]) == [] + assert temp_repo.ignored(["ignored_file.txt"]) == ["ignored_file.txt"] + assert temp_repo.ignored(["included_file.txt", "ignored_file.txt"]) == ["ignored_file.txt"] + assert temp_repo.ignored( + ["included_file.txt", "ignored_file.txt", "included_dir/file.txt", "ignored_dir/file.txt"] + ) == ["ignored_file.txt", "ignored_dir/file.txt"] def test_ignored_raises_error_w_symlink(self): with tempfile.TemporaryDirectory() as tdir: diff --git a/test/test_submodule.py b/test/test_submodule.py index f7195626f..88717e220 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -39,11 +39,14 @@ def _patch_git_config(name, value): # This is recomputed each time the context is entered, for compatibility with # existing GIT_CONFIG_* environment variables, even if changed in this process. - patcher = mock.patch.dict(os.environ, { - "GIT_CONFIG_COUNT": str(pair_index + 1), - f"GIT_CONFIG_KEY_{pair_index}": name, - f"GIT_CONFIG_VALUE_{pair_index}": value, - }) + patcher = mock.patch.dict( + os.environ, + { + "GIT_CONFIG_COUNT": str(pair_index + 1), + f"GIT_CONFIG_KEY_{pair_index}": name, + f"GIT_CONFIG_VALUE_{pair_index}": value, + }, + ) with patcher: yield @@ -914,17 +917,17 @@ def test_ignore_non_submodule_file(self, rwdir): os.mkdir(smp) with open(osp.join(smp, "a"), "w", encoding="utf-8") as f: - f.write('test\n') + f.write("test\n") with open(osp.join(rwdir, ".gitmodules"), "w", encoding="utf-8") as f: - f.write("[submodule \"a\"]\n") + f.write('[submodule "a"]\n') f.write(" path = module\n") f.write(" url = https://github.com/chaconinc/DbConnector\n") parent.git.add(Git.polish_url(osp.join(smp, "a"))) parent.git.add(Git.polish_url(osp.join(rwdir, ".gitmodules"))) - parent.git.commit(message='test') + parent.git.commit(message="test") assert len(parent.submodules) == 0 @@ -1200,7 +1203,12 @@ def test_submodule_add_unsafe_options_allowed(self, rw_repo): # The options will be allowed, but the command will fail. with self.assertRaises(GitCommandError): Submodule.add( - rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True + rw_repo, + "new", + "new", + str(tmp_dir), + clone_multi_options=[unsafe_option], + allow_unsafe_options=True, ) assert not tmp_file.exists() @@ -1211,7 +1219,12 @@ def test_submodule_add_unsafe_options_allowed(self, rw_repo): for unsafe_option in unsafe_options: with self.assertRaises(GitCommandError): Submodule.add( - rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True + rw_repo, + "new", + "new", + str(tmp_dir), + clone_multi_options=[unsafe_option], + allow_unsafe_options=True, ) @with_rw_repo("HEAD") From bf7af69306ed8f14b33528165473ca3591a76246 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 21:29:33 -0400 Subject: [PATCH 033/155] Upgrade flake8 in pre-commit and fix new warnings Upgrading flake8 from 6.0.0 to 6.1.0 causes its pycodestyle dependency to be upgraded from 2.10.* to 2.11.*, which is desirable because: - Spurious "E231 missing whitespace after ':'" warnings on 3.12 due to the lack of full compatibility with Python 3.12 are gone. - New warnings appear, at least one of which, "E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`", does identify something we can improve. This upgrades flake8 in pre-commit and fixes the new warnings. --- .pre-commit-config.yaml | 2 +- git/objects/submodule/base.py | 2 +- git/refs/log.py | 2 +- git/util.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 581cb69b2..888e62d91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 0d20305c6..c7e7856f0 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1403,7 +1403,7 @@ def iter_items( # END handle critical error # Make sure we are looking at a submodule object - if type(sm) != git.objects.submodule.base.Submodule: + if type(sm) is not git.objects.submodule.base.Submodule: continue # fill in remaining info - saves time as it doesn't have to be parsed again diff --git a/git/refs/log.py b/git/refs/log.py index 1f86356a4..1c2a2c470 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -244,7 +244,7 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry": for i in range(index + 1): line = fp.readline() if not line: - raise IndexError(f"Index file ended at line {i+1}, before given index was reached") + raise IndexError(f"Index file ended at line {i + 1}, before given index was reached") # END abort on eof # END handle runup diff --git a/git/util.py b/git/util.py index 638807a36..48901ba0c 100644 --- a/git/util.py +++ b/git/util.py @@ -1136,7 +1136,7 @@ class IterableClassWatcher(type): def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: for base in bases: - if type(base) == IterableClassWatcher: + if type(base) is IterableClassWatcher: warnings.warn( f"GitPython Iterable subclassed by {name}. " "Iterable is deprecated due to naming clash since v3.1.18" From c1ec9cbd2a995585e53c90d61ae30102aaaff2a5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 22:25:40 -0400 Subject: [PATCH 034/155] Update flake8 additional dependencies, fix warning This bumps the versions of the flake8 plugins specified with pinned versions as additional dependencies of flake8 for pre-commit. Doing so gains a warning about a call to warnings.warn with no stacklevel argument. This appears to be the uncommon case where the implifit effect of stacklevel=1 is intended, so I have made that explicit, which clarifies this intent and dismisses the warning. --- .pre-commit-config.yaml | 4 ++-- git/repo/base.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 888e62d91..4c92656e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,8 @@ repos: - id: flake8 additional_dependencies: [ - flake8-bugbear==22.12.6, - flake8-comprehensions==3.10.1, + flake8-bugbear==23.9.16, + flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] exclude: ^doc|^git/ext/|^test/ diff --git a/git/repo/base.py b/git/repo/base.py index fda3cdc88..bc1b8876d 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -206,7 +206,8 @@ def __init__( if expand_vars and re.search(self.re_envvars, epath): warnings.warn( "The use of environment variables in paths is deprecated" - + "\nfor security reasons and may be removed in the future!!" + + "\nfor security reasons and may be removed in the future!!", + stacklevel=1, ) epath = expand_path(epath, expand_vars) if epath is not None: From 48441a94189494789f751e75f9e1919b9b700b13 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 23:40:44 -0400 Subject: [PATCH 035/155] Lint test/ (not just git/), fix warnings and a bug This expands flake8 linting to include the test suite, and fixes the resulting warnings. Four code changes are especially notable: - Unit tests from which documentation is automatically generated contain a few occurrences of "# @NoEffect". These suppressions are extended to "# noqa: B015 # @NoEffect" so the corresponding suppression is also applied for flake8. This is significant because it actually changes what appears in the code examples in the generated documentation. However, since the "@NoEffect" annotation was considered acceptable, this may be okay too. The resulting examples did not become excessively long. - Expressions like "[c for c in commits_for_file_generator]" appear in some unit tests from which documentation is automatically generated. The simpler form "list(commits_for_file_generator)" can replace them. This changes the examples in the documentation, but for the better, since that form is simpler (and also a better practice in general, thus a better thing to show readers). So I made those substitutions. - In test_repo.TestRepo.test_git_work_tree_env, the code intended to unpatch environment variables after the test was ineffective, instead causing os.environ to refer to an ordinary dict object that does not affect environment variables when written to. This uses unittest.mock.patch.dict instead, so the variables are unpatched and subsequent writes to environment variables in the test process are effective. - In test_submodule.TestSubmodule._do_base_tests, the expression statement "csm.module().head.ref.tracking_branch() is not None" appeared to be intended as an assertion, in spite of having been annoated @NoEffect. This is in light of the context and because, if the goal were only to exercise the function call, the "is not None" part would be superfluous. So I made it an assertion. --- .flake8 | 2 +- .pre-commit-config.yaml | 2 +- test/test_commit.py | 14 +++++++------- test/test_diff.py | 1 - test/test_docs.py | 8 ++++---- test/test_quick_doc.py | 14 ++++++-------- test/test_remote.py | 2 +- test/test_repo.py | 16 +++++++--------- test/test_submodule.py | 9 +++++---- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/.flake8 b/.flake8 index 08001ffac..ed5d036bf 100644 --- a/.flake8 +++ b/.flake8 @@ -26,7 +26,7 @@ ignore = E265,E266,E731,E704, D, RST, RST3 -exclude = .tox,.venv,build,dist,doc,git/ext/,test +exclude = .tox,.venv,build,dist,doc,git/ext/ rst-roles = # for flake8-RST-docstrings attr,class,func,meth,mod,obj,ref,term,var # used by sphinx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c92656e4..5a34b8af0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] - exclude: ^doc|^git/ext/|^test/ + exclude: ^doc|^git/ext/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/test/test_commit.py b/test/test_commit.py index f6fb49d50..527aea334 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -277,7 +277,7 @@ def __init__(self, *args, **kwargs): super(Child, self).__init__(*args, **kwargs) child_commits = list(Child.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS"))) - assert type(child_commits[0]) == Child + assert type(child_commits[0]) is Child def test_iter_items(self): # pretty not allowed @@ -525,12 +525,12 @@ def test_trailers(self): # check that trailer stays empty for multiple msg combinations msgs = [ - f"Subject\n", - f"Subject\n\nBody with some\nText\n", - f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", - f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", - f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", - f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n", + "Subject\n", + "Subject\n\nBody with some\nText\n", + "Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", + "Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", + "Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", + "Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n", ] for msg in msgs: diff --git a/test/test_diff.py b/test/test_diff.py index 9c3888f03..5aa4408bf 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -7,7 +7,6 @@ import ddt import shutil import tempfile -import unittest from git import ( Repo, GitCommandError, diff --git a/test/test_docs.py b/test/test_docs.py index 4c23e9f81..d6b88855f 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -263,9 +263,9 @@ def test_references_and_objects(self, rw_dir): # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree - hc != hct # @NoEffect - hc != repo.tags[0] # @NoEffect - hc == repo.head.reference.commit # @NoEffect + hc != hct # noqa: B015 # @NoEffect + hc != repo.tags[0] # noqa: B015 # @NoEffect + hc == repo.head.reference.commit # noqa: B015 # @NoEffect # ![8-test_references_and_objects] # [9-test_references_and_objects] @@ -369,7 +369,7 @@ def test_references_and_objects(self, rw_dir): # The index contains all blobs in a flat list assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == "blob"]) # Access blob objects - for (_path, _stage), entry in index.entries.items(): + for (_path, _stage), _entry in index.entries.items(): pass new_file_path = os.path.join(repo.working_tree_dir, "new-file-name") open(new_file_path, "w").close() diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 9dc7b8d2e..342a7f293 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -1,6 +1,3 @@ -import pytest - - from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -25,10 +22,11 @@ def test_init_repo_object(self, path_to_dir): repo = Repo(path_to_dir) # ![2-test_init_repo_object] + del repo # Avoids "assigned to but never used" warning. Doesn't go in the docs. + @with_rw_directory def test_cloned_repo_object(self, local_dir): from git import Repo - import git # code to clone from url # [1-test_cloned_repo_object] @@ -72,7 +70,7 @@ def test_cloned_repo_object(self, local_dir): # [6-test_cloned_repo_object] commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=update_file) - commits_for_file = [c for c in commits_for_file_generator] + commits_for_file = list(commits_for_file_generator) commits_for_file # Outputs: [, @@ -136,7 +134,7 @@ def test_cloned_repo_object(self, local_dir): # Compare commit to commit # [11.3-test_cloned_repo_object] - first_commit = [c for c in repo.iter_commits(all=True)][-1] + first_commit = list(repo.iter_commits(all=True))[-1] diffs = repo.head.commit.diff(first_commit) for d in diffs: print(d.a_path) @@ -154,7 +152,7 @@ def test_cloned_repo_object(self, local_dir): # Previous commit tree # [13-test_cloned_repo_object] - prev_commits = [c for c in repo.iter_commits(all=True, max_count=10)] # last 10 commits from all branches + prev_commits = list(repo.iter_commits(all=True, max_count=10)) # last 10 commits from all branches tree = prev_commits[0].tree # ![13-test_cloned_repo_object] @@ -210,7 +208,7 @@ def print_files_from_git(root, level=0): # print previous tree # [18.1-test_cloned_repo_object] - commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + commits_for_file = list(repo.iter_commits(all=True, paths=print_file)) tree = commits_for_file[-1].tree # gets the first commit tree blob = tree[print_file] diff --git a/test/test_remote.py b/test/test_remote.py index e0dcb4131..7144b2791 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -160,7 +160,7 @@ def _do_test_push_result(self, results, remote): # END error checking # END for each info - if any([info.flags & info.ERROR for info in results]): + if any(info.flags & info.ERROR for info in results): self.assertRaises(GitCommandError, results.raise_if_error) else: # No errors, so this should do nothing diff --git a/test/test_repo.py b/test/test_repo.py index 6432b8c6f..d3bc864cd 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -252,7 +252,8 @@ def test_clone_from_with_path_contains_unicode(self): @with_rw_directory @skip( - "the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control" + """The referenced repository was removed, and one needs to set up a new + password controlled repo under the org's control.""" ) def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" @@ -758,9 +759,9 @@ def test_blame_complex_revision(self, git): @mock.patch.object(Git, "_call_process") def test_blame_accepts_rev_opts(self, git): - res = self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] boilerplate_kwargs = {"p": True, "stdout_as_string": False} + self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) git.assert_called_once_with(*expected_args, **boilerplate_kwargs) @skipIf( @@ -971,7 +972,7 @@ def _assert_rev_parse(self, name): # history with number ni = 11 history = [obj.parents[0]] - for pn in range(ni): + for _ in range(ni): history.append(history[-1].parents[0]) # END get given amount of commits @@ -1329,6 +1330,7 @@ def test_git_work_tree_env(self, rw_dir): # move .git directory to a subdirectory # set GIT_DIR and GIT_WORK_TREE appropriately # check that repo.working_tree_dir == rw_dir + self.rorepo.clone(join_path_native(rw_dir, "master_repo")) repo_dir = join_path_native(rw_dir, "master_repo") @@ -1338,16 +1340,12 @@ def test_git_work_tree_env(self, rw_dir): os.mkdir(new_subdir) os.rename(old_git_dir, new_git_dir) - oldenv = os.environ.copy() - os.environ["GIT_DIR"] = new_git_dir - os.environ["GIT_WORK_TREE"] = repo_dir + to_patch = {"GIT_DIR": new_git_dir, "GIT_WORK_TREE": repo_dir} - try: + with mock.patch.dict(os.environ, to_patch): r = Repo() self.assertEqual(r.working_tree_dir, repo_dir) self.assertEqual(r.working_dir, repo_dir) - finally: - os.environ = oldenv @with_rw_directory def test_rebasing(self, rw_dir): diff --git a/test/test_submodule.py b/test/test_submodule.py index 88717e220..35ff0d7a8 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -111,7 +111,7 @@ def _do_base_tests(self, rwrepo): # force it to reread its information del smold._url - smold.url == sm.url # @NoEffect + smold.url == sm.url # noqa: B015 # @NoEffect # test config_reader/writer methods sm.config_reader() @@ -248,7 +248,7 @@ def _do_base_tests(self, rwrepo): assert csm.module_exists() # tracking branch once again - csm.module().head.ref.tracking_branch() is not None # @NoEffect + assert csm.module().head.ref.tracking_branch() is not None # this flushed in a sub-submodule assert len(list(rwrepo.iter_submodules())) == 2 @@ -480,8 +480,9 @@ def test_base_bare(self, rwrepo): File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute raise GitCommandNotFound(command, err) git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module""", - ) # noqa E501 + cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module + """, # noqa E501 + ) @with_rw_repo(k_subm_current, bare=False) def test_root_module(self, rwrepo): # Can query everything without problems From 98877c58cd402ea94d4235ab9d4074db76dca74d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 02:46:48 -0400 Subject: [PATCH 036/155] Refactor "finally" cleanup in tests, fix minor bug This refactors code in the test suite that uses try-finally, to simplify and clarify what it is doing. An exception to this being a refactoring is that a possible bug is fixed where a throwaway environment variable "FOO" was patched for a test and never unpatched. There are two patterns refactored here: - try-(try-except)-finally to try-except-finally. When the entire suite of the try-block in try-finally is itself a try-except, that has the same effect as try-except-finally. (Python always attempts to run the finally suite in either case, and does so after any applicable except suite is run.) - Replacing try-finally with an appropriate context manager. (These changes do not fix, nor introduce, any flake8 errors, but they were found by manual search for possible problems related to recent flake8 findings but outside of what flake8 can catch. To limit scope, this only refactors try-finally in the test suite.) --- test/lib/helper.py | 32 +++++++++++++++----------------- test/test_git.py | 17 ++++++----------- test/test_repo.py | 9 ++------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/test/lib/helper.py b/test/lib/helper.py index 64c70a8f4..e8464b7d4 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -94,17 +94,16 @@ def wrapper(self): os.mkdir(path) keep = False try: - try: - return func(self, path) - except Exception: - log.info( - "Test %s.%s failed, output is at %r\n", - type(self).__name__, - func.__name__, - path, - ) - keep = True - raise + return func(self, path) + except Exception: + log.info( + "Test %s.%s failed, output is at %r\n", + type(self).__name__, + func.__name__, + path, + ) + keep = True + raise finally: # Need to collect here to be sure all handles have been closed. It appears # a windows-only issue. In fact things should be deleted, as well as @@ -147,12 +146,11 @@ def repo_creator(self): prev_cwd = os.getcwd() os.chdir(rw_repo.working_dir) try: - try: - return func(self, rw_repo) - except: # noqa E722 - log.info("Keeping repo after failure: %s", repo_dir) - repo_dir = None - raise + return func(self, rw_repo) + except: # noqa E722 + log.info("Keeping repo after failure: %s", repo_dir) + repo_dir = None + raise finally: os.chdir(prev_cwd) rw_repo.git.clear_cache() diff --git a/test/test_git.py b/test/test_git.py index 2c392155a..31065d8fd 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -195,17 +195,12 @@ def test_version(self): # END verify number types def test_cmd_override(self): - prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE - exc = GitCommandNotFound - try: - # set it to something that doesn't exist, assure it raises - type(self.git).GIT_PYTHON_GIT_EXECUTABLE = osp.join( - "some", "path", "which", "doesn't", "exist", "gitbinary" - ) - self.assertRaises(exc, self.git.version) - finally: - type(self.git).GIT_PYTHON_GIT_EXECUTABLE = prev_cmd - # END undo adjustment + with mock.patch.object( + type(self.git), + "GIT_PYTHON_GIT_EXECUTABLE", + osp.join("some", "path", "which", "doesn't", "exist", "gitbinary"), + ): + self.assertRaises(GitCommandNotFound, self.git.version) def test_refresh(self): # test a bad git path refresh diff --git a/test/test_repo.py b/test/test_repo.py index d3bc864cd..15899ec50 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -847,18 +847,13 @@ def test_comparison_and_hash(self): @with_rw_directory def test_tilde_and_env_vars_in_repo_path(self, rw_dir): - ph = os.environ.get("HOME") - try: + with mock.patch.dict(os.environ, {"HOME": rw_dir}): os.environ["HOME"] = rw_dir Repo.init(osp.join("~", "test.git"), bare=True) + with mock.patch.dict(os.environ, {"FOO": rw_dir}): os.environ["FOO"] = rw_dir Repo.init(osp.join("$FOO", "test.git"), bare=True) - finally: - if ph: - os.environ["HOME"] = ph - del os.environ["FOO"] - # end assure HOME gets reset to what it was def test_git_cmd(self): # test CatFileContentStream, just to be very sure we have no fencepost errors From 46d3d0520d8877b1979e6385bada9d9e1e0731ec Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Thu, 21 Sep 2023 12:00:59 +0200 Subject: [PATCH 037/155] Add more checks for the validity of refnames This change adds checks based on the rules described in [0] in order to more robustly check a refname's validity. [0]: https://git-scm.com/docs/git-check-ref-format --- git/refs/symbolic.py | 50 ++++++++++++++++++++++++++++++++++++++++++-- test/test_refs.py | 36 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 5c293aa7b..819615103 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -161,6 +161,51 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> return hexsha # END recursive dereferencing + @staticmethod + def _check_ref_name_valid(ref_path: PathLike) -> None: + # Based on the rules described in https://git-scm.com/docs/git-check-ref-format/#_description + previous: Union[str, None] = None + one_before_previous: Union[str, None] = None + for c in str(ref_path): + if c in " ~^:?*[\\": + raise ValueError( + f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^)," + f" colons (:), question marks (?), asterisks (*), open brackets ([) or backslashes (\\)" + ) + elif c == ".": + if previous is None or previous == "/": + raise ValueError( + f"Invalid reference '{ref_path}': references cannot start with a period (.) or contain '/.'" + ) + elif previous == ".": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '..'") + elif c == "/": + if previous == "/": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '//'") + elif previous is None: + raise ValueError( + f"Invalid reference '{ref_path}': references cannot start with forward slashes '/'" + ) + elif c == "{" and previous == "@": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '@{{'") + elif ord(c) < 32 or ord(c) == 127: + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain ASCII control characters") + + one_before_previous = previous + previous = c + + if previous == ".": + raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a period (.)") + elif previous == "/": + raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)") + elif previous == "@" and one_before_previous is None: + raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'") + elif any([component.endswith(".lock") for component in str(ref_path).split("/")]): + raise ValueError( + f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with" + f" '.lock'" + ) + @classmethod def _get_ref_info_helper( cls, repo: "Repo", ref_path: Union[PathLike, None] @@ -168,8 +213,9 @@ def _get_ref_info_helper( """Return: (str(sha), str(target_ref_path)) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" - if ".." in str(ref_path): - raise ValueError(f"Invalid reference '{ref_path}'") + if ref_path: + cls._check_ref_name_valid(ref_path) + tokens: Union[None, List[str], Tuple[str, str]] = None repodir = _git_dir(repo, ref_path) try: diff --git a/test/test_refs.py b/test/test_refs.py index afd273df9..80166f651 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -631,3 +631,39 @@ def test_refs_outside_repo(self): ref_file.flush() ref_file_name = Path(ref_file.name).name self.assertRaises(BadName, self.rorepo.commit, f"../../{ref_file_name}") + + def test_validity_ref_names(self): + check_ref = SymbolicReference._check_ref_name_valid + # Based on the rules specified in https://git-scm.com/docs/git-check-ref-format/#_description + # Rule 1 + self.assertRaises(ValueError, check_ref, ".ref/begins/with/dot") + self.assertRaises(ValueError, check_ref, "ref/component/.begins/with/dot") + self.assertRaises(ValueError, check_ref, "ref/ends/with/a.lock") + self.assertRaises(ValueError, check_ref, "ref/component/ends.lock/with/period_lock") + # Rule 2 + check_ref("valid_one_level_refname") + # Rule 3 + self.assertRaises(ValueError, check_ref, "ref/contains/../double/period") + # Rule 4 + for c in " ~^:": + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character") + for code in range(0, 32): + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(code)}/ASCII/control_character") + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(127)}/ASCII/control_character") + # Rule 5 + for c in "*?[": + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character") + # Rule 6 + self.assertRaises(ValueError, check_ref, "/ref/begins/with/slash") + self.assertRaises(ValueError, check_ref, "ref/ends/with/slash/") + self.assertRaises(ValueError, check_ref, "ref/contains//double/slash/") + # Rule 7 + self.assertRaises(ValueError, check_ref, "ref/ends/with/dot.") + # Rule 8 + self.assertRaises(ValueError, check_ref, "ref/contains@{/at_brace") + # Rule 9 + self.assertRaises(ValueError, check_ref, "@") + # Rule 10 + self.assertRaises(ValueError, check_ref, "ref/contain\\s/backslash") + # Valid reference name should not raise + check_ref("valid/ref/name") From c5693204f772102f289c2e4269a399370a38d82b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 06:40:03 -0400 Subject: [PATCH 038/155] Make an old mock.patch.dict on os.environ clearer This clarifies that the object's contents are patched, rather than patching the attribute used to get the object in the first place. It is also in keeping with the style of patching os.environ elsewhere in the test suite. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 31065d8fd..481309538 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -245,7 +245,7 @@ def test_insert_after_kwarg_raises(self): def test_env_vars_passed_to_git(self): editor = "non_existent_editor" - with mock.patch.dict("os.environ", {"GIT_EDITOR": editor}): # @UndefinedVariable + with mock.patch.dict(os.environ, {"GIT_EDITOR": editor}): self.assertEqual(self.git.var("GIT_EDITOR"), editor) @with_rw_directory From 592ec8492326ccef7b4af022bffcc385eac8adb8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 06:57:27 -0400 Subject: [PATCH 039/155] Fix rollback bug in SymbolicReference.set_reference This fixes the initialization of the "ok" flag by setting it to False before the operation that might fail is attempted. (This is a fix for the *specific* problem reported in #1669 about how the pattern SymbolicReference.set_reference attempts to use with LockedFD is not correctly implemented.) --- git/refs/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 5c293aa7b..6361713de 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -370,7 +370,7 @@ def set_reference( lfd = LockedFD(fpath) fd = lfd.open(write=True, stream=True) - ok = True + ok = False try: fd.write(write_value.encode("utf-8") + b"\n") lfd.commit() From ff84b26445b147ee9e2c75d82903b0c6b09e2b7a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 03:00:04 -0400 Subject: [PATCH 040/155] Refactor try-finally cleanup in git/ This is, in part, to help avoid (or be able to notice) other bugs like the rollback bug that affected SymbolicReference. However, it also includes a simplification of try-(try-except)-finally to try-except-finally. --- git/config.py | 17 ++++++++--------- git/index/base.py | 8 +++----- git/refs/symbolic.py | 8 +++----- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/git/config.py b/git/config.py index 880eb9301..76b149179 100644 --- a/git/config.py +++ b/git/config.py @@ -406,15 +406,14 @@ def release(self) -> None: return try: - try: - self.write() - except IOError: - log.error("Exception during destruction of GitConfigParser", exc_info=True) - except ReferenceError: - # This happens in PY3 ... and usually means that some state cannot be written - # as the sections dict cannot be iterated - # Usually when shutting down the interpreter, don'y know how to fix this - pass + self.write() + except IOError: + log.error("Exception during destruction of GitConfigParser", exc_info=True) + except ReferenceError: + # This happens in PY3 ... and usually means that some state cannot be + # written as the sections dict cannot be iterated + # Usually when shutting down the interpreter, don't know how to fix this + pass finally: if self._lock is not None: self._lock._release_lock() diff --git a/git/index/base.py b/git/index/base.py index cf016df6a..0cdeb1ce5 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -224,13 +224,11 @@ def write( lfd = LockedFD(file_path or self._file_path) stream = lfd.open(write=True, stream=True) - ok = False try: self._serialize(stream, ignore_extension_data) - ok = True - finally: - if not ok: - lfd.rollback() + except BaseException: + lfd.rollback() + raise lfd.commit() diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 6361713de..734bf32d8 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -370,14 +370,12 @@ def set_reference( lfd = LockedFD(fpath) fd = lfd.open(write=True, stream=True) - ok = False try: fd.write(write_value.encode("utf-8") + b"\n") lfd.commit() - ok = True - finally: - if not ok: - lfd.rollback() + except BaseException: + lfd.rollback() + raise # Adjust the reflog if logmsg is not None: self.log_append(oldbinsha, logmsg) From e480985aa4d358d0cc167d4552910e85944b8966 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 07:05:40 -0400 Subject: [PATCH 041/155] Tweak rollback logic in log.to_file This modifies the exception handling in log.to_file so it catches BaseException rather than Exception and rolls back. Ordinarily we do not want to catch BaseException, since this means catching things like SystemExit, KeyboardInterupt, etc., but the other cases of rolling back with LockedFD do it that strongly (both before when try-finally was used with a flag, and now with try-except catching BaseException to roll back the temporary-file write and reraise). Having this behave subtly different does not appear intentional. (This is also closer to what will happen if LockedFD becomes a context manager and these pieces of code use it in a with-statement: even exceptions not inheriting from Exception will cause __exit__ to be called.) --- git/refs/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/git/refs/log.py b/git/refs/log.py index 1f86356a4..9acc0e360 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -262,8 +262,7 @@ def to_file(self, filepath: PathLike) -> None: try: self._serialize(fp) lfd.commit() - except Exception: - # on failure it rolls back automatically, but we make it clear + except BaseException: lfd.rollback() raise # END handle change From a4701a0f17308ec8d4b5871e6e2a95c4e2ca5b91 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 19:12:03 -0400 Subject: [PATCH 042/155] Remove `@NoEffect` annotations + Add missing asserts, where an expression statement was by itself that was intended as an assertion. This turned out to be the case in the places `@NoEffect` appeared in rendered documentation, making it so no per-file-ignores or other broadened suppressions were needed. + Fix misspellings (including one case affecting documentation). + Add a FIXME comment for investigating a free-standing expression statement with no obvious side effects that may have been meant as an assertion but would fail if turned into one. --- test/test_docs.py | 10 +++++----- test/test_refs.py | 4 ++-- test/test_submodule.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_docs.py b/test/test_docs.py index d6b88855f..79e1f1be4 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -167,7 +167,7 @@ def update(self, op_code, cur_count, max_count=None, message=""): open(new_file_path, "wb").close() # create new file in working tree cloned_repo.index.add([new_file_path]) # add it to the index # Commit the changes to deviate masters history - cloned_repo.index.commit("Added a new file in the past - for later merege") + cloned_repo.index.commit("Added a new file in the past - for later merge") # prepare a merge master = cloned_repo.heads.master # right-hand side is ahead of us, in the future @@ -198,7 +198,7 @@ def update(self, op_code, cur_count, max_count=None, message=""): # .gitmodules was written and added to the index, which is now being committed cloned_repo.index.commit("Added submodule") - assert sm.exists() and sm.module_exists() # this submodule is defintely available + assert sm.exists() and sm.module_exists() # this submodule is definitely available sm.remove(module=True, configuration=False) # remove the working tree assert sm.exists() and not sm.module_exists() # the submodule itself is still available @@ -263,9 +263,9 @@ def test_references_and_objects(self, rw_dir): # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree - hc != hct # noqa: B015 # @NoEffect - hc != repo.tags[0] # noqa: B015 # @NoEffect - hc == repo.head.reference.commit # noqa: B015 # @NoEffect + assert hc != hct + assert hc != repo.tags[0] + assert hc == repo.head.reference.commit # ![8-test_references_and_objects] # [9-test_references_and_objects] diff --git a/test/test_refs.py b/test/test_refs.py index afd273df9..7598deb08 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -386,7 +386,7 @@ def test_head_reset(self, rw_repo): head_tree = head.commit.tree self.assertRaises(ValueError, setattr, head, "commit", head_tree) assert head.commit == old_commit # and the ref did not change - # we allow heds to point to any object + # we allow heads to point to any object head.object = head_tree assert head.object == head_tree # cannot query tree as commit @@ -489,7 +489,7 @@ def test_head_reset(self, rw_repo): cur_head.reference.commit, ) # it works if the new ref points to the same reference - assert SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path # @NoEffect + assert SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path SymbolicReference.delete(rw_repo, symref) # would raise if the symref wouldn't have been deletedpbl symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) diff --git a/test/test_submodule.py b/test/test_submodule.py index 35ff0d7a8..4a9c9c582 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -111,7 +111,7 @@ def _do_base_tests(self, rwrepo): # force it to reread its information del smold._url - smold.url == sm.url # noqa: B015 # @NoEffect + smold.url == sm.url # noqa: B015 # FIXME: Should this be an assertion? # test config_reader/writer methods sm.config_reader() From 832b6eeb4a14e669099c486862c9f568215d5afb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:04:21 +0200 Subject: [PATCH 043/155] remove unnecessary list comprehension to fix CI --- git/refs/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 4d087e7a7..549160444 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -200,7 +200,7 @@ def _check_ref_name_valid(ref_path: PathLike) -> None: raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)") elif previous == "@" and one_before_previous is None: raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'") - elif any([component.endswith(".lock") for component in str(ref_path).split("/")]): + elif any(component.endswith(".lock") for component in str(ref_path).split("/")): raise ValueError( f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with" f" '.lock'" From 0bd2890ef42a7506b81a96c3c94b064917ed0d7b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:33:54 +0200 Subject: [PATCH 044/155] prepare next release --- VERSION | 2 +- doc/source/changes.rst | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b402c1a8b..1f1a39706 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.36 +3.1.37 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 06ec4b72c..a789b068d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,15 @@ Changelog ========= +3.1.37 +====== + +This release contains another security fix that further improves validation of symbolic references +and thus properly fixes this CVE: https://github.com/advisories/GHSA-cwvm-v4w8-q58c . + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/67?closed=1 + 3.1.36 ====== From b27a89f683cda85ebd78243c055e876282df89ee Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:37:09 +0200 Subject: [PATCH 045/155] fix makefile to compare commit hashes only Otherwise, with signed commits, the latest-tag-HEAD comparison would always fail. --- check-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check-version.sh b/check-version.sh index 6d10d6785..c50bf498b 100755 --- a/check-version.sh +++ b/check-version.sh @@ -29,7 +29,7 @@ changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" latest_tag="$(git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1)" head_sha="$(git rev-parse HEAD)" -latest_tag_sha="$(git rev-parse "$latest_tag")" +latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")" # Display a table of all the current version, tag, and HEAD commit information. echo $'\nThe VERSION must be the same in all locations, and so must the HEAD and tag SHA' From 4e701bdae829bb02abd7f9acf5e6508e242b6977 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 05:54:47 -0400 Subject: [PATCH 046/155] Add missing assert keywords This turns two tuple expression statements--each of an equality comparison and message--that were not being checked or otherwise used, and that were intended to be assertions, into assertions. --- test/test_submodule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 4a9c9c582..0aa80e5ce 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1031,8 +1031,8 @@ def test_branch_renames(self, rw_dir): # This doesn't fail as our own submodule binsha didn't change, and the reset is only triggered if # to latest revision is True. parent_repo.submodule_update(to_latest_revision=False) - sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head" - sm_mod.commit() == sm_fb.commit, "Head wasn't reset" + assert sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head" + assert sm_mod.commit() == sm_fb.commit, "Head wasn't reset" self.assertRaises(RepositoryDirtyError, parent_repo.submodule_update, to_latest_revision=True) parent_repo.submodule_update(to_latest_revision=True, force_reset=True) From 45773c27fa40fddf72b40970836b3649094cd994 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 7 Sep 2023 08:16:29 -0400 Subject: [PATCH 047/155] Instrument workflows to investigate skipped tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes jobs from both test workflows give more information relevant to examining which tests are skipped (and if any tests xfail, those too) in what environments: - Values of os.name and git.util.is_win. - The name of each test that runs, with its status. The latter doesn't increase the output length as much as might be expected, because due to the way the output is handled, the pytest-sugar pretty output format without -v looked like: test/test_actor.py ✓ 0% test/test_actor.py ✓✓ 0% ▏ test/test_actor.py ✓✓✓ 1% ▏ test/test_actor.py ✓✓✓✓ 1% ▏ When instead it was intended to fit on a single line. Still, the current output with -v has extra newlines, increasing length and worsening readability, so it should be improved on if possible. --- .github/workflows/cygwin-test.yml | 6 +++++- .github/workflows/pythonpackage.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 962791ae7..3f93f6830 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -58,7 +58,11 @@ jobs: run: | /usr/bin/python -m pip install ".[test]" + - name: Check 'is_win' + run: | + /usr/bin/python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")' + - name: Test with pytest run: | set +x - /usr/bin/python -m pytest + /usr/bin/python -m pytest -v diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a5467ef94..c57f31cdc 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -73,9 +73,13 @@ jobs: # so we have to ignore errors until that changes. continue-on-error: true + - name: Check 'is_win' + run: | + python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")' + - name: Test with pytest run: | - pytest + pytest -v continue-on-error: false - name: Documentation From 6fbe5118514618d34ea8912e99fbde3fd6d7a557 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 16:09:01 -0400 Subject: [PATCH 048/155] Show version and platform info in one place Instead of splitting it in into two places where at least one of the places is highly likely to be missed, this puts it together just before the first steps that makes nontrivial use of the installed packages. Grouping it together, it can't really be shown earlier, because one of the pieces of information is obtained using the git module (to examine that behavior of the code). This also presents the information more clearly. "set -x" makes this easy, so the commands are rewritten to take advantage of it. --- .github/workflows/cygwin-test.yml | 13 ++++++------- .github/workflows/pythonpackage.yml | 17 ++++++++--------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 3f93f6830..1563afc95 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -29,11 +29,6 @@ jobs: with: packages: python39 python39-pip python39-virtualenv git - - name: Show python and git versions - run: | - /usr/bin/python --version - /usr/bin/git version - - name: Tell git to trust this repo run: | /usr/bin/git config --global --add safe.directory "$(pwd)" @@ -58,9 +53,13 @@ jobs: run: | /usr/bin/python -m pip install ".[test]" - - name: Check 'is_win' + - name: Show version and platform information run: | - /usr/bin/python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")' + /usr/bin/git version + /usr/bin/python --version + /usr/bin/python -c 'import sys; print(sys.platform)' + /usr/bin/python -c 'import os; print(os.name)' + /usr/bin/python -c 'import git; print(git.compat.is_win)' - name: Test with pytest run: | diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c57f31cdc..696057ae2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -36,11 +36,6 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: ${{ matrix.experimental }} - - name: Show python and git versions - run: | - python --version - git version - - name: Prepare this repo for tests run: | TRAVIS=yes ./init-tests-after-clone.sh @@ -66,6 +61,14 @@ jobs: run: | pip install ".[test]" + - name: Show version and platform information + run: | + git version + python --version + python -c 'import sys; print(sys.platform)' + python -c 'import os; print(os.name)' + python -c 'import git; print(git.compat.is_win)' + - name: Check types with mypy run: | mypy -p git @@ -73,10 +76,6 @@ jobs: # so we have to ignore errors until that changes. continue-on-error: true - - name: Check 'is_win' - run: | - python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")' - - name: Test with pytest run: | pytest -v From bd3307ad428025e72fb66a9ca4e8231aa0917f9e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 16:29:43 -0400 Subject: [PATCH 049/155] Make "Update PyPA packages" step clearer --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 696057ae2..a311798e2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -50,12 +50,12 @@ jobs: - name: Update PyPA packages run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel + + # Python prior to 3.12 ships setuptools. Upgrade it if present. if pip freeze --all | grep --quiet '^setuptools=='; then - # Python prior to 3.12 ships setuptools. Upgrade it if present. python -m pip install --upgrade setuptools fi - python -m pip install --upgrade wheel - name: Install project and test dependencies run: | From 680d7957373c6bf193388907e3dbb770f3867ffe Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 17:13:01 -0400 Subject: [PATCH 050/155] Show all the failures Don't stop after the first 10. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa06458eb..0466ed4c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] python_files = 'test_*.py' testpaths = 'test' # space separated list of paths from root e.g test tests doc/testing -addopts = '--cov=git --cov-report=term --maxfail=10 --force-sugar --disable-warnings' +addopts = '--cov=git --cov-report=term --force-sugar --disable-warnings' filterwarnings = 'ignore::DeprecationWarning' # --cov coverage # --cov-report term # send report to terminal term-missing -> terminal with line numbers html xml From 75cf5402d0d6c6712c1b0f5bd114cc9fd8780edc Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 01:39:33 -0400 Subject: [PATCH 051/155] Keep sugar for local use, but use instafail on CI There are two benefits of the pytest-sugar plugin: 1. Pretty output. 2. Show details on each failure immediately instead of at the end. The first benefit is effectively local-only, because extra newlines are appearing when it runs on CI, both with and without -v. The second benefit applies both locally and on CI. So this adds the pytest-instafail plugin and uses it on CI to get the second benefit. It is not set up to run automatically, and pytest-sugar still is (though no longer forced), so local testing retains no benefit and we don't have a clash. The name "instafail" refers only to instantly *seeing* failures: it does not cause the pytest runner to stop earlier than otherwise. --- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- pyproject.toml | 2 +- test-requirements.txt | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 1563afc95..cae828099 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -64,4 +64,4 @@ jobs: - name: Test with pytest run: | set +x - /usr/bin/python -m pytest -v + /usr/bin/python -m pytest -p no:sugar -v --instafail diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a311798e2..9ac1088f7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -78,7 +78,7 @@ jobs: - name: Test with pytest run: | - pytest -v + pytest -v -p no:sugar --instafail continue-on-error: false - name: Documentation diff --git a/pyproject.toml b/pyproject.toml index 0466ed4c4..f4fc33fec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] python_files = 'test_*.py' testpaths = 'test' # space separated list of paths from root e.g test tests doc/testing -addopts = '--cov=git --cov-report=term --force-sugar --disable-warnings' +addopts = '--cov=git --cov-report=term --disable-warnings' filterwarnings = 'ignore::DeprecationWarning' # --cov coverage # --cov-report term # send report to terminal term-missing -> terminal with line numbers html xml diff --git a/test-requirements.txt b/test-requirements.txt index b00dd6f06..1c08c736f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,4 +5,5 @@ mypy pre-commit pytest pytest-cov +pytest-instafail pytest-sugar From eb56e7bdf15344739d3c2d671c1ca7dc185b8abe Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 02:01:25 -0400 Subject: [PATCH 052/155] Pass -v twice to see full skip reasons + Reorder pytest arguments so both workflows are consistent. --- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index cae828099..8d1145e76 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -64,4 +64,4 @@ jobs: - name: Test with pytest run: | set +x - /usr/bin/python -m pytest -p no:sugar -v --instafail + /usr/bin/python -m pytest -p no:sugar --instafail -vv diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 9ac1088f7..c0402e4bb 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -78,7 +78,7 @@ jobs: - name: Test with pytest run: | - pytest -v -p no:sugar --instafail + pytest -p no:sugar --instafail -vv continue-on-error: false - name: Documentation From 9c7ff1e4918128ff28ba02cb2771b440a392644c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 02:04:52 -0400 Subject: [PATCH 053/155] Force pytest color output on CI While pytest-sugar output gets mangled with extra newlines on CI, colorized output seems to work fine and improves readability. --- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 8d1145e76..337c0a809 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -64,4 +64,4 @@ jobs: - name: Test with pytest run: | set +x - /usr/bin/python -m pytest -p no:sugar --instafail -vv + /usr/bin/python -m pytest --color=yes -p no:sugar --instafail -vv diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c0402e4bb..392c894bc 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -78,7 +78,7 @@ jobs: - name: Test with pytest run: | - pytest -p no:sugar --instafail -vv + pytest --color=yes -p no:sugar --instafail -vv continue-on-error: false - name: Documentation From 0eb38bcedf3703c2e3aacae27ea4cbafce33e941 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 15 Sep 2023 06:33:10 -0400 Subject: [PATCH 054/155] Fix test_blocking_lock_file for cygwin This permits the longer delay in test_blocking_lock_file--which was already allowed for native Windows--on Cygwin, where it is also needed. That lets the xfail mark for Cygwin be removed. This also updates the comments to avoid implying that the need for the delay is AppVeyor-specific (it seems needed on CI and locally). --- test/test_util.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 42edc57cf..308ba311b 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -12,7 +12,6 @@ from unittest import mock, skipIf from datetime import datetime -import pytest import ddt from git.cmd import dashify @@ -156,11 +155,6 @@ def test_lock_file(self): lock_file._obtain_lock_or_raise() lock_file._release_lock() - @pytest.mark.xfail( - sys.platform == "cygwin", - reason="Cygwin fails here for some reason, always", - raises=AssertionError, - ) def test_blocking_lock_file(self): my_file = tempfile.mktemp() lock_file = BlockingLockFile(my_file) @@ -173,9 +167,8 @@ def test_blocking_lock_file(self): self.assertRaises(IOError, wait_lock._obtain_lock) elapsed = time.time() - start extra_time = 0.02 - if is_win: - # for Appveyor - extra_time *= 6 # NOTE: Indeterministic failures here... + if is_win or sys.platform == "cygwin": + extra_time *= 6 # NOTE: Indeterministic failures without this... self.assertLess(elapsed, wait_time + extra_time) def test_user_id(self): From 715dba473a202ef3631b6c4bd724b8ff4e6c6d0b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 02:51:58 -0400 Subject: [PATCH 055/155] Run cygpath tests on Cygwin, not native Windows They were not running on Cygwin, because git.util.is_win is False on Cygwin. They were running on native Windows, with a number of them always failing; these failures had sometimes been obscured by the --maxfail=10 that had formerly been used (from pyproject.toml). Many of them (not all the same ones) fail on Cygwin, and it might be valuable for cygpath to work on other platforms, especially native Windows. But I think it still makes sense to limit the tests to Cygwin at this time, because all the uses of cygpath in the project are in code that only runs after a check that the platform is Cygwin. Part of that check, as it is implemented, explicitly excludes native Windows (is_win must be false). --- test/test_util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 308ba311b..41d874678 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -9,7 +9,7 @@ import sys import tempfile import time -from unittest import mock, skipIf +from unittest import mock, skipUnless from datetime import datetime import ddt @@ -84,14 +84,14 @@ def setup(self): "array": [42], } - @skipIf(not is_win, "Paths specifically for Windows.") + @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.idata(_norm_cygpath_pairs + _unc_cygpath_pairs) def test_cygpath_ok(self, case): wpath, cpath = case cwpath = cygpath(wpath) self.assertEqual(cwpath, cpath, wpath) - @skipIf(not is_win, "Paths specifically for Windows.") + @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( (r"./bar", "bar"), (r".\bar", "bar"), @@ -104,7 +104,7 @@ def test_cygpath_norm_ok(self, case): cwpath = cygpath(wpath) self.assertEqual(cwpath, cpath or wpath, wpath) - @skipIf(not is_win, "Paths specifically for Windows.") + @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( r"C:", r"C:Relative", @@ -117,7 +117,7 @@ def test_cygpath_invalids(self, wpath): cwpath = cygpath(wpath) self.assertEqual(cwpath, wpath.replace("\\", "/"), wpath) - @skipIf(not is_win, "Paths specifically for Windows.") + @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.idata(_norm_cygpath_pairs) def test_decygpath(self, case): wpath, cpath = case From d6a2d2807de99715ce85887b8992dbcafcefcee9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 04:08:02 -0400 Subject: [PATCH 056/155] Mark some cygpath tests xfail Two of the groups of cygpath tests in test_util.py generate tests that fail on Cygwin. There is no easy way to still run, but xfail, just the specific tests that fail, because the groups of tests are generated with `@ddt` parameterization, but neither the unittest nor pytest xfail mechanisms interact with that. If `@pytest.mark.parametrized` were used, this could be done. But that does not work on methods of test classes that derive from unittest.TestCase, including those in this project that indirectly derive from it by deriving from TestBase. The TestBase base class cannot be removed without overhauling many tests, due to fixtures it provides such as rorepo. So this marks too many tests as xfail, but in doing so allows test runs to pass while still exercising and showing status on all the tests, allowing result changes to be observed easily. --- test/test_util.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_util.py b/test/test_util.py index 41d874678..f2135a272 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -13,6 +13,7 @@ from datetime import datetime import ddt +import pytest from git.cmd import dashify from git.compat import is_win @@ -84,6 +85,10 @@ def setup(self): "array": [42], } + @pytest.mark.xfail( + reason="Many return paths prefixed /proc/cygdrive instead.", + raises=AssertionError, + ) @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.idata(_norm_cygpath_pairs + _unc_cygpath_pairs) def test_cygpath_ok(self, case): @@ -91,6 +96,10 @@ def test_cygpath_ok(self, case): cwpath = cygpath(wpath) self.assertEqual(cwpath, cpath, wpath) + @pytest.mark.xfail( + reason=r'2nd example r".\bar" -> "bar" fails, returns "./bar"', + raises=AssertionError, + ) @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( (r"./bar", "bar"), From 881456bdceb61d51fa84ea286e6ca0e3587e8dc5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 06:54:56 -0400 Subject: [PATCH 057/155] Run test_commit_msg_hook_success on more systems This changes a default Windows skip of test_commit_msg_hook_success to an xfail, and makes it more specific, expecting failure only when either bash.exe is unavailable (definitely expected) or when bash.exe is the WSL bash wrapper in System32, which fails for some reason even though it's not at all clear it ought to. This showcases the failures rather than skipping, and also lets the test pass on Windows systems where bash.exe is something else, including the Git Bash bash.exe that native Windows CI would use. --- test/test_index.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/test_index.py b/test/test_index.py index fba9c78ec..f4858a586 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -7,10 +7,14 @@ from io import BytesIO import os +import os.path as osp +from pathlib import Path from stat import S_ISLNK, ST_MODE +import shutil import tempfile from unittest import skipIf -import shutil + +import pytest from git import ( IndexFile, @@ -23,6 +27,7 @@ GitCommandError, CheckoutError, ) +from git.cmd import Git from git.compat import is_win from git.exc import HookExecutionError, InvalidGitRepositoryError from git.index.fun import hook_path @@ -34,15 +39,22 @@ from git.util import HIDE_WINDOWS_KNOWN_ERRORS, hex_to_bin from gitdb.base import IStream -import os.path as osp -from git.cmd import Git +HOOKS_SHEBANG = "#!/usr/bin/env sh\n" -from pathlib import Path -HOOKS_SHEBANG = "#!/usr/bin/env sh\n" +def _found_in(cmd, directory): + """Check if a command is resolved in a directory (without following symlinks).""" + path = shutil.which(cmd) + return path and Path(path).parent == Path(directory) + is_win_without_bash = is_win and not shutil.which("bash.exe") +is_win_with_wsl_bash = is_win and _found_in( + cmd="bash.exe", + directory=Path(os.getenv("WINDIR")) / "System32", +) + def _make_hook(git_dir, name, content, make_exec=True): """A helper to create a hook""" @@ -910,7 +922,11 @@ def test_pre_commit_hook_fail(self, rw_repo): else: raise AssertionError("Should have caught a HookExecutionError") - @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703") + @pytest.mark.xfail( + is_win_without_bash or is_win_with_wsl_bash, + reason="Specifically seems to fail on WSL bash (in spite of #1399)", + raises=AssertionError, + ) @with_rw_repo("HEAD", bare=True) def test_commit_msg_hook_success(self, rw_repo): commit_message = "commit default head by Frèderic Çaufl€" From c6a586ab4817a9dcb8c8290a6a3e7071b1834f32 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 07:07:52 -0400 Subject: [PATCH 058/155] No longer skip test_index_mutation on Cygwin As it seems to be working now on Cygwin (maybe not native Windows). --- test/test_index.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/test/test_index.py b/test/test_index.py index f4858a586..06db3aedd 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -12,7 +12,6 @@ from stat import S_ISLNK, ST_MODE import shutil import tempfile -from unittest import skipIf import pytest @@ -27,16 +26,13 @@ GitCommandError, CheckoutError, ) -from git.cmd import Git from git.compat import is_win from git.exc import HookExecutionError, InvalidGitRepositoryError from git.index.fun import hook_path from git.index.typ import BaseIndexEntry, IndexEntry from git.objects import Blob -from test.lib import TestBase, fixture_path, fixture, with_rw_repo -from test.lib import with_rw_directory -from git.util import Actor, rmtree -from git.util import HIDE_WINDOWS_KNOWN_ERRORS, hex_to_bin +from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo +from git.util import Actor, hex_to_bin, rmtree from gitdb.base import IStream HOOKS_SHEBANG = "#!/usr/bin/env sh\n" @@ -434,14 +430,6 @@ def _count_existing(self, repo, files): # END num existing helper - @skipIf( - HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(), - """FIXME: File "C:\\projects\\gitpython\\git\\test\\test_index.py", line 642, in test_index_mutation - self.assertEqual(fd.read(), link_target) - AssertionError: '!\xff\xfe/\x00e\x00t\x00c\x00/\x00t\x00h\x00a\x00t\x00\x00\x00' - != '/etc/that' - """, - ) @with_rw_repo("0.1.6") def test_index_mutation(self, rw_repo): index = rw_repo.index From fc022304d2f4bc8770f75b0c5fc289dccab0ae5b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 24 Sep 2023 02:27:31 -0400 Subject: [PATCH 059/155] Report encoding error in test_add_unicode as error This makes the test explicitly error out, rather than skipping, if it appears the environment doesn't support encoding Unicode filenames. Platforms these days should be capable of that, and reporting it as an error lessens the risk of missing a bug in the test code (that method or a fixture) if one is ever introduced. Erroring out will also make it easier to see the details in the chained UnicodeDecodeError exception. This does not affect the behavior of GitPython itself. It only changes how a test reports an unusual condition that keeps the test\ from being usefully run. --- test/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_base.py b/test/test_base.py index b77c8117d..90e701c4b 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -7,7 +7,7 @@ import os import sys import tempfile -from unittest import SkipTest, skipIf +from unittest import skipIf from git import Repo from git.objects import Blob, Tree, Commit, TagObject @@ -126,7 +126,7 @@ def test_add_unicode(self, rw_repo): try: file_path.encode(sys.getfilesystemencoding()) except UnicodeEncodeError as e: - raise SkipTest("Environment doesn't support unicode filenames") from e + raise RuntimeError("Environment doesn't support unicode filenames") from e with open(file_path, "wb") as fp: fp.write(b"something") From 203da23e5fe2ae5a0ae13d0f9b2a276ae584ea7b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 24 Sep 2023 03:19:23 -0400 Subject: [PATCH 060/155] Add a few FIXMEs re: better use of xfail --- test/test_config.py | 1 + test/test_util.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_config.py b/test/test_config.py index 481e129c6..f805570d5 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -100,6 +100,7 @@ def test_includes_order(self): # values must be considered as soon as they get them assert r_config.get_value("diff", "tool") == "meld" try: + # FIXME: Split this assertion out somehow and mark it xfail (or fix it). assert r_config.get_value("sec", "var1") == "value1_main" except AssertionError as e: raise SkipTest("Known failure -- included values are not in effect right away") from e diff --git a/test/test_util.py b/test/test_util.py index f2135a272..2b1e518ed 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -85,6 +85,7 @@ def setup(self): "array": [42], } + # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them). @pytest.mark.xfail( reason="Many return paths prefixed /proc/cygdrive instead.", raises=AssertionError, @@ -103,7 +104,7 @@ def test_cygpath_ok(self, case): @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( (r"./bar", "bar"), - (r".\bar", "bar"), + (r".\bar", "bar"), # FIXME: Mark only this one xfail, somehow (or fix it). (r"../bar", "../bar"), (r"..\bar", "../bar"), (r"../bar/.\foo/../chu", "../bar/chu"), From cf5f1dca139b809a744badb9f9740dd6d0a70c56 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 03:21:05 -0400 Subject: [PATCH 061/155] Report <2.5.1 in test_linked_worktree_traversal as error Although GitPython does not require git >=2.5.1 in general, and this does *not* change that, this makes the unavailability of git 2.5.1 or later an error in test_linked_worktree_traversal, where it is needed to exercise that test, rather than skipping the test. A few systems, such as CentOS 7, may have downstream patched versions of git that remain safe to use yet are numbered <2.5.1 and do not have the necesary feature to run this test. But by now, users of those systems likely anticipate that other software would rely on the presence of features added in git 2.5.1, which was released over 7 years ago. As such, I think it is more useful to give an error for that test, so the test's inability to be run on the system is clear, than to automatically skip the test, which is likely to go unnoticed. --- test/test_fun.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_fun.py b/test/test_fun.py index d76e189ed..f39955aa0 100644 --- a/test/test_fun.py +++ b/test/test_fun.py @@ -2,7 +2,6 @@ from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR from os import stat import os.path as osp -from unittest import SkipTest from git import Git from git.index import IndexFile @@ -279,7 +278,7 @@ def test_linked_worktree_traversal(self, rw_dir): """Check that we can identify a linked worktree based on a .git file""" git = Git(rw_dir) if git.version_info[:3] < (2, 5, 1): - raise SkipTest("worktree feature unsupported") + raise RuntimeError("worktree feature unsupported (test needs git 2.5.1 or later)") rw_master = self.rorepo.clone(join_path_native(rw_dir, "master_repo")) branch = rw_master.create_head("aaaaaaaa") From 89232365fb0204c32d1f7c23b9eb39fe4401eb7f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 04:13:36 -0400 Subject: [PATCH 062/155] Change skipIf(not ...) to skipUnless(...) --- test/test_submodule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 0aa80e5ce..432c02686 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path import sys -from unittest import mock, skipIf +from unittest import mock, skipIf, skipUnless import pytest @@ -1039,7 +1039,7 @@ def test_branch_renames(self, rw_dir): assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset" assert sm_mod.head.ref.name == sm_pfb.name - @skipIf(not is_win, "Specifically for Windows.") + @skipUnless(is_win, "Specifically for Windows.") def test_to_relative_path_with_super_at_root_drive(self): class Repo(object): working_tree_dir = "D:\\" From b198bf1e7d9c9d9db675c6c4e94d2f136a0a7923 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 05:43:00 -0400 Subject: [PATCH 063/155] Express known test_depth failure with xfail Rather than skipping, so it becomes known if the situation changes. --- test/test_submodule.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 432c02686..54b1e796b 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1050,9 +1050,8 @@ class Repo(object): msg = '_to_relative_path should be "submodule_path" but was "%s"' % relative_path assert relative_path == "submodule_path", msg - @skipIf( - True, - "for some unknown reason the assertion fails, even though it in fact is working in more common setup", + @pytest.mark.xfail( + reason="for some unknown reason the assertion fails, even though it in fact is working in more common setup", ) @with_rw_directory def test_depth(self, rwdir): From cd175a598ed457833bc06adba776e2bbb1d9014b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 07:09:43 -0400 Subject: [PATCH 064/155] Remove no-effect `@skipIf` on test_untracked_files It looked like test_untracked_files was sometimes skipped, and specifically that it would be skipped on Cygwin. But the `@skipIf` on it had the condition: HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin() HIDE_WINDOWS_KNOWN_ERRORS can only ever be true if it is set to a truthy value directly (not an intended use as it's a "constant"), or on native Windows systems: no matter how the environment variable related to it is set, it's only checked if is_win, which is set by checking os.name, which is only "nt" on native Windows systems, not Cygwin. So whenever HIDE_WINDOWS_KNOWN_ERRORS is true Git.is_cygwin() will be false. Thus this condition is never true and the test was never being skipped anyway: it was running and passing on Cygwin. --- test/test_repo.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/test/test_repo.py b/test/test_repo.py index 15899ec50..fe3419e33 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -13,7 +13,7 @@ import pickle import sys import tempfile -from unittest import mock, skipIf, SkipTest, skip +from unittest import mock, SkipTest, skip import pytest @@ -42,7 +42,7 @@ ) from git.repo.fun import touch from test.lib import TestBase, with_rw_repo, fixture -from git.util import HIDE_WINDOWS_KNOWN_ERRORS, cygpath +from git.util import cygpath from test.lib import with_rw_directory from git.util import join_path_native, rmtree, rmfile, bin_to_hex @@ -764,16 +764,6 @@ def test_blame_accepts_rev_opts(self, git): self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) git.assert_called_once_with(*expected_args, **boilerplate_kwargs) - @skipIf( - HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(), - """FIXME: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute - raise GitCommandError(command, status, stderr_value, stdout_value) - GitCommandError: Cmd('git') failed due to: exit code(128) - cmdline: git add 1__��ava verb��ten 1_test _myfile 1_test_other_file - 1_��ava-----verb��ten - stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files' - """, - ) @with_rw_repo("HEAD", bare=False) def test_untracked_files(self, rwrepo): for run, repo_add in enumerate((rwrepo.index.add, rwrepo.git.add)): From f38cc000fe4d51f0f9d1bdedec86f6fcdb57f359 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 07:32:46 -0400 Subject: [PATCH 065/155] Make 2 more too-low git version skips into errors In the tests only, and not in any way affecting the feature set or requirements of GitPython itself. This is similar to, and with the same reasoning as, cf5f1dc. --- test/test_repo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_repo.py b/test/test_repo.py index fe3419e33..4257d8a47 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -13,7 +13,7 @@ import pickle import sys import tempfile -from unittest import mock, SkipTest, skip +from unittest import mock, skip import pytest @@ -1235,7 +1235,7 @@ def test_merge_base(self): def test_is_ancestor(self): git = self.rorepo.git if git.version_info[:3] < (1, 8, 0): - raise SkipTest("git merge-base --is-ancestor feature unsupported") + raise RuntimeError("git merge-base --is-ancestor feature unsupported (test needs git 1.8.0 or later)") repo = self.rorepo c1 = "f6aa8d1" @@ -1283,7 +1283,7 @@ def test_git_work_tree_dotgit(self, rw_dir): based on it.""" git = Git(rw_dir) if git.version_info[:3] < (2, 5, 1): - raise SkipTest("worktree feature unsupported") + raise RuntimeError("worktree feature unsupported (test needs git 2.5.1 or later)") rw_master = self.rorepo.clone(join_path_native(rw_dir, "master_repo")) branch = rw_master.create_head("aaaaaaaa") From 8fd56e78366470e0b07db48daf623b188e7245ea Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 08:03:58 -0400 Subject: [PATCH 066/155] Update test_root_module Windows skip reason The current cause of failure is different from what is documented in the skip reason. --- test/test_submodule.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 54b1e796b..4c087caa1 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -477,11 +477,11 @@ def test_base_bare(self, rwrepo): @skipIf( HIDE_WINDOWS_KNOWN_ERRORS, """ - File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute - raise GitCommandNotFound(command, err) - git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module - """, # noqa E501 + E PermissionError: + [WinError 32] The process cannot access the file because it is being used by another process: + 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\git\\ext\\gitdb' + -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\path\\prefix\\git\\ext\\gitdb' + """, ) @with_rw_repo(k_subm_current, bare=False) def test_root_module(self, rwrepo): From c1798f5ead60f74de422732d5229244d00325f24 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 08:06:45 -0400 Subject: [PATCH 067/155] Change test_root_module Windows skip to xfail And rewrite the reason to give more useful information. (The new reason also doesn't state the exception type, because that is now specified, and checked by pytest, by being passed as "raises".) --- test/test_submodule.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 4c087caa1..256eb7dbf 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -474,14 +474,13 @@ def test_base_bare(self, rwrepo): reason="Cygwin GitPython can't find submodule SHA", raises=ValueError, ) - @skipIf( + @pytest.mark.xfail( HIDE_WINDOWS_KNOWN_ERRORS, - """ - E PermissionError: - [WinError 32] The process cannot access the file because it is being used by another process: - 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\git\\ext\\gitdb' - -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\path\\prefix\\git\\ext\\gitdb' - """, + reason=( + '"The process cannot access the file because it is being used by another process"' + + " on first call to rm.update" + ), + raises=PermissionError, ) @with_rw_repo(k_subm_current, bare=False) def test_root_module(self, rwrepo): From ba567521a5a01b93420cab4021d5663af66231ae Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 08:16:05 -0400 Subject: [PATCH 068/155] Update test_git_submodules_and_add_sm_with_new_commit skip reason This is working on Cygwin, so that old reason no longer applies. (The test was not being skipped on Cygwin, and was passing.) It is not working on native Windows, due to a PermissionError from attempting to move a file that is still open (which Windows doesn't allow). That may have been the original native Windows skip reason, but the old AppVeyor CI link for it is broken or not public. This makes the reason clear, though maybe I should add more details. --- test/test_submodule.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 256eb7dbf..25b0daa01 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -750,13 +750,12 @@ def test_list_only_valid_submodules(self, rwdir): @skipIf( HIDE_WINDOWS_KNOWN_ERRORS, - """FIXME on cygwin: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute - raise GitCommandError(command, status, stderr_value, stdout_value) - GitCommandError: Cmd('git') failed due to: exit code(128) - cmdline: git add 1__Xava verbXXten 1_test _myfile 1_test_other_file 1_XXava-----verbXXten - stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files' - FIXME on appveyor: see https://ci.appveyor.com/project/Byron/gitpython/build/1.0.185 - """, + """ + E PermissionError: + [WinError 32] The process cannot access the file because it is being used by another process: + 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module' + -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module_moved' + """ # noqa: E501, ) @with_rw_directory @_patch_git_config("protocol.file.allow", "always") From 8704d1b81ba080cd4baa74968ac4cf84a84e4cbe Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 08:22:32 -0400 Subject: [PATCH 069/155] Change test_git_submodules_and_add_sm_with_new_commit Windows skip to xfail And improve details. The xfail is only for native Windows, not Cygwin (same as the old skip was, and still via checking HIDE_WINDOWS_KNOWN_ERRORS). This change is analogous to the change in c1798f5, but for test_git_submodules_and_add_sm_with_new_commit rather than test_root_module. --- test/test_submodule.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 25b0daa01..72d7255b0 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path import sys -from unittest import mock, skipIf, skipUnless +from unittest import mock, skipUnless import pytest @@ -748,14 +748,13 @@ def test_list_only_valid_submodules(self, rwdir): repo = git.Repo(repo_path) assert len(repo.submodules) == 0 - @skipIf( + @pytest.mark.xfail( HIDE_WINDOWS_KNOWN_ERRORS, - """ - E PermissionError: - [WinError 32] The process cannot access the file because it is being used by another process: - 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module' - -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module_moved' - """ # noqa: E501, + reason=( + '"The process cannot access the file because it is being used by another process"' + + " on first call to sm.move" + ), + raises=PermissionError, ) @with_rw_directory @_patch_git_config("protocol.file.allow", "always") From 1d6abdca134082bf0bce2e590a52d8c08bf04d9b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 08:34:54 -0400 Subject: [PATCH 070/155] Run the tests in test_tree on Windows This stops skipping them, as they are now working. --- test/test_tree.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/test_tree.py b/test/test_tree.py index e59705645..c5ac8d539 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -5,24 +5,14 @@ # the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO -from unittest import skipIf from git.objects import Tree, Blob from test.lib import TestBase -from git.util import HIDE_WINDOWS_KNOWN_ERRORS import os.path as osp class TestTree(TestBase): - @skipIf( - HIDE_WINDOWS_KNOWN_ERRORS, - """ - File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute - raise GitCommandNotFound(command, err) - git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git cat-file --batch-check""", - ) def test_serializable(self): # tree at the given commit contains a submodule as well roottree = self.rorepo.tree("6c1faef799095f3990e9970bc2cb10aa0221cf9c") @@ -51,14 +41,6 @@ def test_serializable(self): testtree._deserialize(stream) # END for each item in tree - @skipIf( - HIDE_WINDOWS_KNOWN_ERRORS, - """ - File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute - raise GitCommandNotFound(command, err) - git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git cat-file --batch-check""", - ) def test_traverse(self): root = self.rorepo.tree("0.1.6") num_recursive = 0 From 5609faa5a1c22654dfc007f7bf229e1b08087aa8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 09:01:31 -0400 Subject: [PATCH 071/155] Add missing raises keyword for test_depth xfail I had forgotten to do this earlier when converting from skip to xfail. Besides consistency with the other uses of xfail in the test suite, the benefit of passing "raises" is that pytest checks that the failure gave the expected exception and makes it a non-expected failure if it didn't. --- test/test_submodule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_submodule.py b/test/test_submodule.py index 72d7255b0..79ff2c5f2 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1049,6 +1049,7 @@ class Repo(object): @pytest.mark.xfail( reason="for some unknown reason the assertion fails, even though it in fact is working in more common setup", + raises=AssertionError, ) @with_rw_directory def test_depth(self, rwdir): From ed95e8e72f6aa697c1ec69595335168e717da013 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 09:07:53 -0400 Subject: [PATCH 072/155] Consolidate test_repo module import statements --- test/test_repo.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_repo.py b/test/test_repo.py index 4257d8a47..364b895fb 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -41,10 +41,8 @@ UnsafeProtocolError, ) from git.repo.fun import touch -from test.lib import TestBase, with_rw_repo, fixture -from git.util import cygpath -from test.lib import with_rw_directory -from git.util import join_path_native, rmtree, rmfile, bin_to_hex +from git.util import bin_to_hex, cygpath, join_path_native, rmfile, rmtree +from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo import os.path as osp From ceb4dd3a7e89ac281a6c0d4d815fc13c00cbdf9f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 10:33:23 -0400 Subject: [PATCH 073/155] Show more CI system information --- .github/workflows/cygwin-test.yml | 1 + .github/workflows/pythonpackage.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 337c0a809..d0be6bc39 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -55,6 +55,7 @@ jobs: - name: Show version and platform information run: | + /usr/bin/uname -a /usr/bin/git version /usr/bin/python --version /usr/bin/python -c 'import sys; print(sys.platform)' diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 392c894bc..0dc9d217a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -63,6 +63,7 @@ jobs: - name: Show version and platform information run: | + uname -a git version python --version python -c 'import sys; print(sys.platform)' From 3276aac711186a5dd0bd74ba1be8bb6f4ad3d03a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 10:49:50 -0400 Subject: [PATCH 074/155] Use Cygwin's bash and git for more CI steps --- .github/workflows/cygwin-test.yml | 34 +++++++++++++++++------------ .github/workflows/pythonpackage.yml | 1 + 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index d0be6bc39..2269df0da 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -14,11 +14,13 @@ jobs: TEMP: "/tmp" defaults: run: - shell: bash.exe --noprofile --norc -exo pipefail -o igncr "{0}" + shell: C:\cygwin\bin\bash.exe --noprofile --norc -exo pipefail -o igncr "{0}" steps: - name: Force LF line endings - run: git config --global core.autocrlf input + run: | + git config --global core.autocrlf input + shell: bash - uses: actions/checkout@v4 with: @@ -29,9 +31,12 @@ jobs: with: packages: python39 python39-pip python39-virtualenv git + - name: Limit $PATH to Cygwin + run: echo 'C:\cygwin\bin' >"$GITHUB_PATH" + - name: Tell git to trust this repo run: | - /usr/bin/git config --global --add safe.directory "$(pwd)" + git config --global --add safe.directory "$(pwd)" - name: Prepare this repo for tests run: | @@ -39,30 +44,31 @@ jobs: - name: Further prepare git configuration for tests run: | - /usr/bin/git config --global user.email "travis@ci.com" - /usr/bin/git config --global user.name "Travis Runner" + git config --global user.email "travis@ci.com" + git config --global user.name "Travis Runner" # If we rewrite the user's config by accident, we will mess it up # and cause subsequent tests to fail cat test/fixtures/.gitconfig >> ~/.gitconfig - name: Update PyPA packages run: | - /usr/bin/python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip setuptools wheel - name: Install project and test dependencies run: | - /usr/bin/python -m pip install ".[test]" + python -m pip install ".[test]" - name: Show version and platform information run: | - /usr/bin/uname -a - /usr/bin/git version - /usr/bin/python --version - /usr/bin/python -c 'import sys; print(sys.platform)' - /usr/bin/python -c 'import os; print(os.name)' - /usr/bin/python -c 'import git; print(git.compat.is_win)' + uname -a + command -v git python + git version + python --version + python -c 'import sys; print(sys.platform)' + python -c 'import os; print(os.name)' + python -c 'import git; print(git.compat.is_win)' - name: Test with pytest run: | set +x - /usr/bin/python -m pytest --color=yes -p no:sugar --instafail -vv + python -m pytest --color=yes -p no:sugar --instafail -vv diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0dc9d217a..78d3ddf86 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -64,6 +64,7 @@ jobs: - name: Show version and platform information run: | uname -a + command -v git python git version python --version python -c 'import sys; print(sys.platform)' From 5d4097654c6498d56defcc98dc1611fbfba9b75a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 12:02:35 -0400 Subject: [PATCH 075/155] Try to work in all LF on Cygwin CI + Style tweak and comment to clarify the "Limit $PATH" step. --- .github/workflows/cygwin-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 2269df0da..b8f3efbba 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -9,7 +9,6 @@ jobs: fail-fast: false env: CHERE_INVOKING: 1 - SHELLOPTS: igncr TMP: "/tmp" TEMP: "/tmp" defaults: @@ -19,7 +18,7 @@ jobs: steps: - name: Force LF line endings run: | - git config --global core.autocrlf input + git config --global core.autocrlf false # Affects the non-Cygwin git. shell: bash - uses: actions/checkout@v4 @@ -32,11 +31,13 @@ jobs: packages: python39 python39-pip python39-virtualenv git - name: Limit $PATH to Cygwin - run: echo 'C:\cygwin\bin' >"$GITHUB_PATH" + run: | + echo 'C:\cygwin\bin' > "$GITHUB_PATH" # Overwrite it with just this. - - name: Tell git to trust this repo + - name: Special configuration for Cygwin's git run: | git config --global --add safe.directory "$(pwd)" + git config --global core.autocrlf false - name: Prepare this repo for tests run: | @@ -70,5 +71,4 @@ jobs: - name: Test with pytest run: | - set +x python -m pytest --color=yes -p no:sugar --instafail -vv From dda428640113d6a2c30225ea33f1387e784b7289 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 25 Sep 2023 13:42:29 -0400 Subject: [PATCH 076/155] Consistent formatting style across all workflows --- .github/workflows/cygwin-test.yml | 3 +++ .github/workflows/lint.yml | 12 +++++++----- .github/workflows/pythonpackage.yml | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index b8f3efbba..3cca4dd5f 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -5,12 +5,15 @@ on: [push, pull_request, workflow_dispatch] jobs: build: runs-on: windows-latest + strategy: fail-fast: false + env: CHERE_INVOKING: 1 TMP: "/tmp" TEMP: "/tmp" + defaults: run: shell: C:\cygwin\bin\bash.exe --noprofile --norc -exo pipefail -o igncr "{0}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e79664a8..2204bb792 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,8 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - uses: pre-commit/action@v3.0.0 + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 78d3ddf86..e9ccdd566 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,8 +10,8 @@ permissions: jobs: build: - runs-on: ubuntu-latest + strategy: fail-fast: false matrix: @@ -20,6 +20,7 @@ jobs: - experimental: false - python-version: "3.12" experimental: true + defaults: run: shell: /bin/bash --noprofile --norc -exo pipefail {0} From 3007abc6d229bcfe6643963f648597b7e231ab3d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 26 Sep 2023 00:01:32 -0400 Subject: [PATCH 077/155] Remove the recently added "Limit $PATH" step I had put that step in the Cygwin workflow for purposes of experimentation, and it seemed to make clearer what is going on, but really it does the opposite: it's deceptive because Cygwin uses other logic to set its PATH. So this step is unnecessary and ineffective at doing what it appears to do. --- .github/workflows/cygwin-test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 3cca4dd5f..35e9fde72 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -33,10 +33,6 @@ jobs: with: packages: python39 python39-pip python39-virtualenv git - - name: Limit $PATH to Cygwin - run: | - echo 'C:\cygwin\bin' > "$GITHUB_PATH" # Overwrite it with just this. - - name: Special configuration for Cygwin's git run: | git config --global --add safe.directory "$(pwd)" From 4860f701b96dc07ac7c489c74c06cae069ae3cd1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 26 Sep 2023 00:14:29 -0400 Subject: [PATCH 078/155] Further reduce differences between test workflows This makes the two CI test workflows more similar in a couple of the remaining ways they differ unnecessarily. This could be extended, and otherwise improved upon, in the future. --- .github/workflows/cygwin-test.yml | 5 +++-- .github/workflows/pythonpackage.yml | 10 +++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 35e9fde72..e818803f1 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -42,7 +42,7 @@ jobs: run: | TRAVIS=yes ./init-tests-after-clone.sh - - name: Further prepare git configuration for tests + - name: Set git user identity and command aliases for the tests run: | git config --global user.email "travis@ci.com" git config --global user.name "Travis Runner" @@ -52,7 +52,8 @@ jobs: - name: Update PyPA packages run: | - python -m pip install --upgrade pip setuptools wheel + # Get the latest pip, wheel, and prior to Python 3.12, setuptools. + python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel - name: Install project and test dependencies run: | diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index e9ccdd566..1b049ba02 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -41,7 +41,7 @@ jobs: run: | TRAVIS=yes ./init-tests-after-clone.sh - - name: Prepare git configuration for tests + - name: Set git user identity and command aliases for the tests run: | git config --global user.email "travis@ci.com" git config --global user.name "Travis Runner" @@ -51,12 +51,8 @@ jobs: - name: Update PyPA packages run: | - python -m pip install --upgrade pip wheel - - # Python prior to 3.12 ships setuptools. Upgrade it if present. - if pip freeze --all | grep --quiet '^setuptools=='; then - python -m pip install --upgrade setuptools - fi + # Get the latest pip, wheel, and prior to Python 3.12, setuptools. + python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel - name: Install project and test dependencies run: | From 13b859745de3f5a610e9e6390d55da40c582e663 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 26 Sep 2023 23:41:02 -0400 Subject: [PATCH 079/155] Fix new link to license in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbec36024..c7c2992e3 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ gpg --edit-key 4C08421980C9 ### LICENSE -[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file](https://github.com/gitpython-developers/GitPython/blob/main/license). +[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file][license]. [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md -[license]: https://github.com/gitpython-developers/GitPython/blob/main/license +[license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE From 388c7d1d70c2dabe1fe5e5eb1d453edd8f40a49d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 00:05:08 -0400 Subject: [PATCH 080/155] Clearer YAML style for flake8 extra plugin list --- .pre-commit-config.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a34b8af0..67aefb342 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,9 @@ repos: hooks: - id: flake8 additional_dependencies: - [ - flake8-bugbear==23.9.16, - flake8-comprehensions==3.14.0, - flake8-typing-imports==1.14.0, - ] + - flake8-bugbear==23.9.16 + - flake8-comprehensions==3.14.0 + - flake8-typing-imports==1.14.0 exclude: ^doc|^git/ext/ - repo: https://github.com/pre-commit/pre-commit-hooks From 1a8f210a42c3f9c46ce3cfa9c25ea39b0a8ca6c6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 02:40:11 -0400 Subject: [PATCH 081/155] Drop flake8 suppressions that are no longer needed + Remove the comments that documented those old suppressions + Format the .flake8 file more readably --- .flake8 | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/.flake8 b/.flake8 index ed5d036bf..1cc049a69 100644 --- a/.flake8 +++ b/.flake8 @@ -1,38 +1,26 @@ [flake8] + show-source = True -count= True +count = True statistics = True -# E265 = comment blocks like @{ section, which it can't handle + # E266 = too many leading '#' for block comment # E731 = do not assign a lambda expression, use a def -# W293 = Blank line contains whitespace -# W504 = Line break after operator -# E704 = multiple statements in one line - used for @override # TC002 = move third party import to TYPE_CHECKING -# ANN = flake8-annotations # TC, TC2 = flake8-type-checking -# D = flake8-docstrings # select = C,E,F,W ANN, TC, TC2 # to enable code. Disabled if not listed, including builtin codes enable-extensions = TC, TC2 # only needed for extensions not enabled by default -ignore = E265,E266,E731,E704, - W293, W504, - ANN0 ANN1 ANN2, - TC002, - TC0, TC1, TC2 - # B, - A, - D, - RST, RST3 +ignore = E266, E731 -exclude = .tox,.venv,build,dist,doc,git/ext/ +exclude = .tox, .venv, build, dist, doc, git/ext/ rst-roles = # for flake8-RST-docstrings - attr,class,func,meth,mod,obj,ref,term,var # used by sphinx + attr, class, func, meth, mod, obj, ref, term, var # used by sphinx min-python-version = 3.7.0 # for `black` compatibility max-line-length = 120 -extend-ignore = E203,W503 +extend-ignore = E203, W503 From e07d91a6f67c1c4adb897096cb13d6cd1b98cf42 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 1 Oct 2023 15:31:00 -0400 Subject: [PATCH 082/155] Drop claim about Cygwin not having git-daemon On a current Cygwin system with git 2.39.0 (the latest version offered by the Cygwin package manager), git-daemon is present, with the Cygwin path /usr/libexec/git-core/git-daemon.exe. In addition, the cygwin-test.yml workflow does not take any special steps to allow git-daemon to work, but all tests pass in it even without skipping or xfailing tests that seem related to git-daemon. The git_daemon_launched function in test/lib/helper.py only invokes git-daemon directly (rather than through "git daemon") when is_win evaluates true, which only happens on native Windows systems, not Cygwin, which is treated the same as (other) Unix-like systems and still works. So maybe Cygwin git-daemon was never a special case. Whether or not it was, the message about git-daemon needing to be findable in a PATH search is also under an is_win check, and thus is never shown on Cygwin. So I've removed the Cygwin part of that message. (Because the path shown is a MinGW-style path, I have kept the wording about that being for MinGW-git, even though it is no longer needed to disambiguate the Cygwin case.) --- README.md | 3 +-- test/lib/helper.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c7c2992e3..65c1e7bae 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,7 @@ executed `git fetch --tags` followed by the `./init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures. On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` -exists in `Git\mingw64\libexec\git-core\`; CYGWIN has no daemon, but should get along fine -with MINGW's. +exists in `Git\mingw64\libexec\git-core\`. #### Install test dependencies diff --git a/test/lib/helper.py b/test/lib/helper.py index e8464b7d4..9656345de 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -177,12 +177,10 @@ def git_daemon_launched(base_path, ip, port): gd = None try: if is_win: - ## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\, - # but if invoked as 'git daemon', it detaches from parent `git` cmd, - # and then CANNOT DIE! - # So, invoke it as a single command. - ## Cygwin-git has no daemon. But it can use MINGW's. - # + # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\, + # but if invoked as 'git daemon', it detaches from parent `git` cmd, + # and then CANNOT DIE! + # So, invoke it as a single command. daemon_cmd = [ "git-daemon", "--enable=receive-pack", @@ -217,12 +215,11 @@ def git_daemon_launched(base_path, ip, port): ) if is_win: msg += textwrap.dedent( - r""" + R""" On Windows, the `git-daemon.exe` must be in PATH. - For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear. - CYGWIN has no daemon, but if one exists, it gets along fine (but has also paths problems).""" + For MINGW, look into \Git\mingw64\libexec\git-core\, but problems with paths might appear.""" ) log.warning(msg, ex, ip, port, base_path, base_path, exc_info=1) From 35e3875cb71913771885646f294cd25cd19c35f3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 1 Oct 2023 17:06:47 -0400 Subject: [PATCH 083/155] Allow base_daemon_path to be normalized for Cygwin Since the Cygwin git-daemon can be used. --- test/lib/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/helper.py b/test/lib/helper.py index 9656345de..1de904610 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -302,7 +302,7 @@ def remote_repo_creator(self): cw.set("url", remote_repo_url) with git_daemon_launched( - Git.polish_url(base_daemon_path, is_cygwin=False), # No daemon in Cygwin. + Git.polish_url(base_daemon_path), "127.0.0.1", GIT_DAEMON_PORT, ): From 5e71c270b2ea0adfc5c0a103fce33ab6acf275b1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 1 Oct 2023 22:43:36 -0400 Subject: [PATCH 084/155] Fix the name of the "executes git" test That test is not testing whether or not a shell is used (nor does it need to test that), but just whether the library actually runs git, passes an argument to it, and returns text from its stdout. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 481309538..a21a3c78e 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -73,7 +73,7 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): res = self.git.transform_kwargs(**{"s": True, "t": True}) self.assertEqual({"-s", "-t"}, set(res)) - def test_it_executes_git_to_shell_and_returns_result(self): + def test_it_executes_git_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") def test_it_executes_git_not_from_cwd(self): From 59440607406873a28788ca38526871749c5549f9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 00:17:05 -0400 Subject: [PATCH 085/155] Test whether a shell is used In the Popen calls in Git.execute, for all combinations of allowed values for Git.USE_SHELL and the shell= keyword argument. --- test/test_git.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index a21a3c78e..6fd2c8268 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -4,23 +4,24 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ +import contextlib import os +import os.path as osp import shutil import subprocess import sys from tempfile import TemporaryDirectory, TemporaryFile from unittest import mock, skipUnless -from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd -from test.lib import TestBase, fixture_path -from test.lib import with_rw_directory -from git.util import cwd, finalize_process - -import os.path as osp +import ddt +from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd from git.compat import is_win +from git.util import cwd, finalize_process +from test.lib import TestBase, fixture_path, with_rw_directory +@ddt.ddt class TestGit(TestBase): @classmethod def setUpClass(cls): @@ -73,6 +74,28 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): res = self.git.transform_kwargs(**{"s": True, "t": True}) self.assertEqual({"-s", "-t"}, set(res)) + @ddt.data( + (None, False, False), + (None, True, True), + (False, True, False), + (False, False, False), + (True, False, True), + (True, True, True), + ) + @mock.patch.object(cmd, "Popen", wraps=cmd.Popen) # Since it is gotten via a "from" import. + def test_it_uses_shell_or_not_as_specified(self, case, mock_popen): + """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`.""" + value_in_call, value_from_class, expected_popen_arg = case + # FIXME: Check what gets logged too! + with mock.patch.object(Git, "USE_SHELL", value_from_class): + with contextlib.suppress(GitCommandError): + self.git.execute( + "git", # No args, so it runs with or without a shell, on all OSes. + shell=value_in_call, + ) + mock_popen.assert_called_once() + self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg) + def test_it_executes_git_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") From aa5e2f6b24e36d2d38e84e7f2241b104318396c3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 01:45:17 -0400 Subject: [PATCH 086/155] Test if whether a shell is used is logged The log message shows "Popen(...)", not "execute(...)". So it should faithfully report what is about to be passed to Popen in cases where a user reading the log would otherwise be misled into thinking this is what has happened. Reporting the actual "shell=" argument passed to Popen is also more useful to log than the argument passed to Git.execute (at least if only one of them is to be logged). --- test/test_git.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 6fd2c8268..389e80552 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -5,8 +5,10 @@ # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib +import logging import os import os.path as osp +import re import shutil import subprocess import sys @@ -74,7 +76,8 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): res = self.git.transform_kwargs(**{"s": True, "t": True}) self.assertEqual({"-s", "-t"}, set(res)) - @ddt.data( + _shell_cases = ( + # value_in_call, value_from_class, expected_popen_arg (None, False, False), (None, True, True), (False, True, False), @@ -82,20 +85,42 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): (True, False, True), (True, True, True), ) + + @ddt.idata(_shell_cases) @mock.patch.object(cmd, "Popen", wraps=cmd.Popen) # Since it is gotten via a "from" import. def test_it_uses_shell_or_not_as_specified(self, case, mock_popen): """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`.""" value_in_call, value_from_class, expected_popen_arg = case - # FIXME: Check what gets logged too! + with mock.patch.object(Git, "USE_SHELL", value_from_class): with contextlib.suppress(GitCommandError): self.git.execute( "git", # No args, so it runs with or without a shell, on all OSes. shell=value_in_call, ) + mock_popen.assert_called_once() self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg) + @ddt.idata(full_case[:2] for full_case in _shell_cases) + @mock.patch.object(cmd, "Popen", wraps=cmd.Popen) # Since it is gotten via a "from" import. + def test_it_logs_if_it_uses_a_shell(self, case, mock_popen): + """``shell=`` in the log message agrees with what is passed to `Popen`.""" + value_in_call, value_from_class = case + + with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: + with mock.patch.object(Git, "USE_SHELL", value_from_class): + with contextlib.suppress(GitCommandError): + self.git.execute( + "git", # No args, so it runs with or without a shell, on all OSes. + shell=value_in_call, + ) + + popen_shell_arg = mock_popen.call_args.kwargs["shell"] + expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)") + match_attempts = [expected_message.fullmatch(message) for message in log_watcher.output] + self.assertTrue(any(match_attempts), repr(log_watcher.output)) + def test_it_executes_git_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") From 0f19fb0be1bd3ccd3ff8f35dba9e924c9d379e41 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 02:21:10 -0400 Subject: [PATCH 087/155] Fix tests so they don't try to run "g" Both new shell-related tests were causing the code under test to split "git" into ["g", "i", "t"] and thus causing the code under test to attempt to execute a "g" command. This passes the command as a one-element list of strings rather than as a string, which avoids this on all operating systems regardless of whether the code under test has a bug being tested for. This would not occur on Windows, which does not iterate commands of type str character-by-character even when the command is run without a shell. But it did happen on other systems. Most of the time, the benefit of using a command that actually runs "git" rather than "g" is the avoidance of confusion in the error message. But this is also important because it is possible for the user who runs the tests to have a "g" command, in which case it could be very inconvenient, or even unsafe, to run "g". This should be avoided even when the code under test has a bug that causes a shell to be used when it shouldn't or not used when it should, so having separate commands (list and str) per test case parameters would not be a sufficient solution: it would only guard against running "g" when a bug in the code under test were absent. --- test/test_git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 389e80552..7e8e7c805 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -95,7 +95,7 @@ def test_it_uses_shell_or_not_as_specified(self, case, mock_popen): with mock.patch.object(Git, "USE_SHELL", value_from_class): with contextlib.suppress(GitCommandError): self.git.execute( - "git", # No args, so it runs with or without a shell, on all OSes. + ["git"], # No args, so it runs with or without a shell, on all OSes. shell=value_in_call, ) @@ -112,7 +112,7 @@ def test_it_logs_if_it_uses_a_shell(self, case, mock_popen): with mock.patch.object(Git, "USE_SHELL", value_from_class): with contextlib.suppress(GitCommandError): self.git.execute( - "git", # No args, so it runs with or without a shell, on all OSes. + ["git"], # No args, so it runs with or without a shell, on all OSes. shell=value_in_call, ) From da3460c6cc3a7c6981dfd1d4675d167a7a5f2b0c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 02:56:37 -0400 Subject: [PATCH 088/155] Extract shared test logic to a helper This also helps mock Popen over a smaller scope, which may be beneficial (especially if it is mocked in the subprocess module, rather than the git.cmd module, in the future). --- test/test_git.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 7e8e7c805..343bf7a19 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -86,35 +86,33 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): (True, True, True), ) + def _do_shell_combo(self, value_in_call, value_from_class): + with mock.patch.object(Git, "USE_SHELL", value_from_class): + # git.cmd gets Popen via a "from" import, so patch it there. + with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen: + # Use a command with no arguments (besides the program name), so it runs + # with or without a shell, on all OSes, with the same effect. Since git + # errors out when run with no arguments, we swallow that error. + with contextlib.suppress(GitCommandError): + self.git.execute(["git"], shell=value_in_call) + + return mock_popen + @ddt.idata(_shell_cases) - @mock.patch.object(cmd, "Popen", wraps=cmd.Popen) # Since it is gotten via a "from" import. - def test_it_uses_shell_or_not_as_specified(self, case, mock_popen): + def test_it_uses_shell_or_not_as_specified(self, case): """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`.""" value_in_call, value_from_class, expected_popen_arg = case - - with mock.patch.object(Git, "USE_SHELL", value_from_class): - with contextlib.suppress(GitCommandError): - self.git.execute( - ["git"], # No args, so it runs with or without a shell, on all OSes. - shell=value_in_call, - ) - + mock_popen = self._do_shell_combo(value_in_call, value_from_class) mock_popen.assert_called_once() self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg) @ddt.idata(full_case[:2] for full_case in _shell_cases) - @mock.patch.object(cmd, "Popen", wraps=cmd.Popen) # Since it is gotten via a "from" import. - def test_it_logs_if_it_uses_a_shell(self, case, mock_popen): + def test_it_logs_if_it_uses_a_shell(self, case): """``shell=`` in the log message agrees with what is passed to `Popen`.""" value_in_call, value_from_class = case with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: - with mock.patch.object(Git, "USE_SHELL", value_from_class): - with contextlib.suppress(GitCommandError): - self.git.execute( - ["git"], # No args, so it runs with or without a shell, on all OSes. - shell=value_in_call, - ) + mock_popen = self._do_shell_combo(value_in_call, value_from_class) popen_shell_arg = mock_popen.call_args.kwargs["shell"] expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)") From 41294d578471f7f63c02bf59e8abc3459f9d6390 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 03:26:02 -0400 Subject: [PATCH 089/155] Use the mock backport on Python 3.7 Because mock.call.kwargs, i.e. the ability to examine m.call_args.kwargs where m is a Mock or MagicMock, was introduced in Python 3.8. Currently it is only in test/test_git.py that any use of mocks requires this, so I've put the conditional import logic to import mock (the top-level package) rather than unittest.mock only there. The mock library is added as a development (testing) dependency only when the Python version is lower than 3.8, so it is not installed when not needed. This fixes a problem in the new tests of whether a shell is used, and reported as used, in the Popen call in Git.execute. Those just-introduced tests need this, to be able to use mock_popen.call_args.kwargs on Python 3.7. --- test-requirements.txt | 3 ++- test/test_git.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1c08c736f..9414da09c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,7 @@ black coverage[toml] -ddt>=1.1.1, !=1.4.3 +ddt >= 1.1.1, != 1.4.3 +mock ; python_version < "3.8" mypy pre-commit pytest diff --git a/test/test_git.py b/test/test_git.py index 343bf7a19..583c74fa3 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -13,7 +13,12 @@ import subprocess import sys from tempfile import TemporaryDirectory, TemporaryFile -from unittest import mock, skipUnless +from unittest import skipUnless + +if sys.version_info >= (3, 8): + from unittest import mock +else: + import mock # To be able to examine call_args.kwargs on a mock. import ddt From baf3457ec8f92c64d2481b812107e6acc4059ddd Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 2 Oct 2023 04:11:18 -0400 Subject: [PATCH 090/155] Fix Git.execute shell use and reporting bugs This updates the shell variable itself, only when it is None, from self.USE_SHELL. (That attribute is usually Git.USE_SHELL rather than an instance attribute. But although people probably shouldn't set it on instances, people will expect that this is permitted.) Now: - USE_SHELL is used as a fallback only. When shell=False is passed, USE_SHELL is no longer consuled. Thus shell=False always keeps a shell from being used, even in the non-default case where the USE_SHELL attribue has been set to True. - The debug message printed to the log shows the actual value that is being passed to Popen, because the updated shell variable is used both to produce that message and in the Popen call. --- git/cmd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 9921dd6c9..53b8b9050 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -974,6 +974,8 @@ def execute( istream_ok = "None" if istream: istream_ok = "" + if shell is None: + shell = self.USE_SHELL log.debug( "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", redacted_command, @@ -992,7 +994,7 @@ def execute( stdin=istream or DEVNULL, stderr=PIPE, stdout=stdout_sink, - shell=shell is not None and shell or self.USE_SHELL, + shell=shell, close_fds=is_posix, # unsupported on windows universal_newlines=universal_newlines, creationflags=PROC_CREATIONFLAGS, From b79198a880982e6768fec4d0ef244338420efbdc Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 04:58:13 -0400 Subject: [PATCH 091/155] Document Git.execute parameters in definition order - Reorder the items in the git.cmd.Git.execute docstring that document its parameters, to be in the same order the parameters are actually defined in. - Use consistent spacing, by having a blank line between successive items that document parameters. Before, most of them were separated this way, but some of them were not. - Reorder the elements of execute_kwargs (which list all those parameters except the first parameter, command) to be listed in that order as well. These were mostly in order, but a couple were out of order. This is just about the order they appear in the definition, since sets in Python (unlike dicts) have no key order guarantees. --- git/cmd.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 53b8b9050..4d772e909 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -66,10 +66,10 @@ "with_extended_output", "with_exceptions", "as_process", - "stdout_as_string", "output_stream", - "with_stdout", + "stdout_as_string", "kill_after_timeout", + "with_stdout", "universal_newlines", "shell", "env", @@ -883,6 +883,27 @@ def execute( decoded into a string using the default encoding (usually utf-8). The latter can fail, if the output contains binary data. + :param kill_after_timeout: + To specify a timeout in seconds for the git command, after which the process + should be killed. This will have no effect if as_process is set to True. It is + set to None by default and will let the process run until the timeout is + explicitly specified. This feature is not supported on Windows. It's also worth + noting that kill_after_timeout uses SIGKILL, which can have negative side + effects on a repository. For example, stale locks in case of git gc could + render the repository incapable of accepting changes until the lock is manually + removed. + + :param with_stdout: + If True, default True, we open stdout on the created process + + :param universal_newlines: + if True, pipes will be opened as text, and lines are split at + all known line endings. + + :param shell: + Whether to invoke commands through a shell (see `Popen(..., shell=True)`). + It overrides :attr:`USE_SHELL` if it is not `None`. + :param env: A dictionary of environment variables to be passed to `subprocess.Popen`. @@ -891,29 +912,14 @@ def execute( one invocation of write() method. If the given number is not positive then the default value is used. + :param strip_newline_in_stdout: + Whether to strip the trailing ``\\n`` of the command stdout. + :param subprocess_kwargs: Keyword arguments to be passed to subprocess.Popen. Please note that some of the valid kwargs are already set by this method, the ones you specify may not be the same ones. - :param with_stdout: If True, default True, we open stdout on the created process - :param universal_newlines: - if True, pipes will be opened as text, and lines are split at - all known line endings. - :param shell: - Whether to invoke commands through a shell (see `Popen(..., shell=True)`). - It overrides :attr:`USE_SHELL` if it is not `None`. - :param kill_after_timeout: - To specify a timeout in seconds for the git command, after which the process - should be killed. This will have no effect if as_process is set to True. It is - set to None by default and will let the process run until the timeout is - explicitly specified. This feature is not supported on Windows. It's also worth - noting that kill_after_timeout uses SIGKILL, which can have negative side - effects on a repository. For example, stale locks in case of git gc could - render the repository incapable of accepting changes until the lock is manually - removed. - :param strip_newline_in_stdout: - Whether to strip the trailing ``\\n`` of the command stdout. :return: * str(output) if extended_output = False (Default) * tuple(int(status), str(stdout), str(stderr)) if extended_output = True From 13e1593fc6a3c218451e29dd6b4a58b3a44afee3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 05:30:22 -0400 Subject: [PATCH 092/155] Don't say Git.execute uses a shell, in its summary The top line of the Git.execute docstring said that it used a shell, which is not necessarily the case (and is not usually the case, since the default is not to use one). This removes that claim while keeping the top-line wording otherwise the same. It also rephrases the description of the command parameter, in a way that does not change its meaning but reflects the more common practice of passing a sequence of arguments (since portable calls that do not use a shell must do that). --- git/cmd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 4d772e909..e324db7e2 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -842,12 +842,12 @@ def execute( strip_newline_in_stdout: bool = True, **subprocess_kwargs: Any, ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: - """Handles executing the command on the shell and consumes and returns - the returned information (stdout) + """Handles executing the command and consumes and returns the returned + information (stdout) :param command: The command argument list to execute. - It should be a string, or a sequence of program arguments. The + It should be a sequence of program arguments, or a string. The program to execute is the first item in the args sequence or string. :param istream: From 74b68e9b621a729a3407b2020b0a48d7921fb1e9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 05:55:54 -0400 Subject: [PATCH 093/155] Copyedit Git.execute docstring These are some small clarity and consistency revisions to the docstring of git.cmd.Git.execute that didn't specifically fit in the narrow topics of either of the preceding two commits. --- git/cmd.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index e324db7e2..f20cd77af 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -851,7 +851,7 @@ def execute( program to execute is the first item in the args sequence or string. :param istream: - Standard input filehandle passed to subprocess.Popen. + Standard input filehandle passed to `subprocess.Popen`. :param with_extended_output: Whether to return a (status, stdout, stderr) tuple. @@ -862,8 +862,7 @@ def execute( :param as_process: Whether to return the created process instance directly from which streams can be read on demand. This will render with_extended_output and - with_exceptions ineffective - the caller will have - to deal with the details himself. + with_exceptions ineffective - the caller will have to deal with the details. It is important to note that the process will be placed into an AutoInterrupt wrapper that will interrupt the process once it goes out of scope. If you use the command in iterators, you should pass the whole process instance @@ -876,25 +875,25 @@ def execute( always be created with a pipe due to issues with subprocess. This merely is a workaround as data will be copied from the output pipe to the given output stream directly. - Judging from the implementation, you shouldn't use this flag ! + Judging from the implementation, you shouldn't use this parameter! :param stdout_as_string: - if False, the commands standard output will be bytes. Otherwise, it will be - decoded into a string using the default encoding (usually utf-8). + If False, the command's standard output will be bytes. Otherwise, it will be + decoded into a string using the default encoding (usually UTF-8). The latter can fail, if the output contains binary data. :param kill_after_timeout: - To specify a timeout in seconds for the git command, after which the process + Specifies a timeout in seconds for the git command, after which the process should be killed. This will have no effect if as_process is set to True. It is set to None by default and will let the process run until the timeout is explicitly specified. This feature is not supported on Windows. It's also worth noting that kill_after_timeout uses SIGKILL, which can have negative side - effects on a repository. For example, stale locks in case of git gc could + effects on a repository. For example, stale locks in case of ``git gc`` could render the repository incapable of accepting changes until the lock is manually removed. :param with_stdout: - If True, default True, we open stdout on the created process + If True, default True, we open stdout on the created process. :param universal_newlines: if True, pipes will be opened as text, and lines are split at @@ -916,19 +915,19 @@ def execute( Whether to strip the trailing ``\\n`` of the command stdout. :param subprocess_kwargs: - Keyword arguments to be passed to subprocess.Popen. Please note that - some of the valid kwargs are already set by this method, the ones you + Keyword arguments to be passed to `subprocess.Popen`. Please note that + some of the valid kwargs are already set by this method; the ones you specify may not be the same ones. :return: * str(output) if extended_output = False (Default) * tuple(int(status), str(stdout), str(stderr)) if extended_output = True - if output_stream is True, the stdout value will be your output stream: + If output_stream is True, the stdout value will be your output stream: * output_stream if extended_output = False * tuple(int(status), output_stream, str(stderr)) if extended_output = True - Note git is executed with LC_MESSAGES="C" to ensure consistent + Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent output regardless of system language. :raise GitCommandError: From 133271bb3871baff3ed772fbdea8bc359f115fd6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 06:34:05 -0400 Subject: [PATCH 094/155] Other copyediting in the git.cmd module (Not specific to git.cmd.Git.execute.) --- git/cmd.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index f20cd77af..a6d287986 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -105,7 +105,7 @@ def handle_process_output( ) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. - This function returns once the finalizer returns + This function returns once the finalizer returns. :return: result of finalizer :param process: subprocess.Popen instance @@ -294,9 +294,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: @classmethod def refresh(cls, path: Union[None, PathLike] = None) -> bool: - """This gets called by the refresh function (see the top level - __init__). - """ + """This gets called by the refresh function (see the top level __init__).""" # discern which path to refresh with if path is not None: new_git = os.path.expanduser(path) @@ -446,9 +444,9 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: if is_cygwin: url = cygpath(url) else: - """Remove any backslahes from urls to be written in config files. + """Remove any backslashes from urls to be written in config files. - Windows might create config-files containing paths with backslashed, + Windows might create config files containing paths with backslashes, but git stops liking them as it will escape the backslashes. Hence we undo the escaping just to be sure. """ @@ -464,8 +462,8 @@ def check_unsafe_protocols(cls, url: str) -> None: Check for unsafe protocols. Apart from the usual protocols (http, git, ssh), - Git allows "remote helpers" that have the form `::
`, - one of these helpers (`ext::`) can be used to invoke any arbitrary command. + Git allows "remote helpers" that have the form ``::
``, + one of these helpers (``ext::``) can be used to invoke any arbitrary command. See: @@ -517,7 +515,7 @@ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.status: Union[int, None] = None def _terminate(self) -> None: - """Terminate the underlying process""" + """Terminate the underlying process.""" if self.proc is None: return @@ -572,7 +570,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: """Wait for the process and return its status code. :param stderr: Previously read value of stderr, in case stderr is already closed. - :warn: may deadlock if output or error pipes are used and not handled separately. + :warn: May deadlock if output or error pipes are used and not handled separately. :raise GitCommandError: if the return status is not 0""" if stderr is None: stderr_b = b"" @@ -605,13 +603,12 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte # END auto interrupt class CatFileContentStream(object): - """Object representing a sized read-only stream returning the contents of an object. It behaves like a stream, but counts the data read and simulates an empty stream once our sized content region is empty. - If not all data is read to the end of the objects's lifetime, we read the - rest to assure the underlying stream continues to work""" + If not all data is read to the end of the object's lifetime, we read the + rest to assure the underlying stream continues to work.""" __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size") @@ -740,11 +737,11 @@ def __getattr__(self, name: str) -> Any: def set_persistent_git_options(self, **kwargs: Any) -> None: """Specify command line options to the git executable - for subsequent subcommand calls + for subsequent subcommand calls. :param kwargs: is a dict of keyword arguments. - these arguments are passed as in _call_process + These arguments are passed as in _call_process but will be passed to the git command rather than the subcommand. """ @@ -775,7 +772,7 @@ def version_info(self) -> Tuple[int, int, int, int]: """ :return: tuple(int, int, int, int) tuple with integers representing the major, minor and additional version numbers as parsed from git version. - This value is generated on demand and is cached""" + This value is generated on demand and is cached.""" return self._version_info @overload @@ -843,7 +840,7 @@ def execute( **subprocess_kwargs: Any, ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command and consumes and returns the returned - information (stdout) + information (stdout). :param command: The command argument list to execute. @@ -1213,7 +1210,7 @@ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: def __call__(self, **kwargs: Any) -> "Git": """Specify command line options to the git executable - for a subcommand call + for a subcommand call. :param kwargs: is a dict of keyword arguments. @@ -1251,7 +1248,7 @@ def _call_process( self, method: str, *args: Any, **kwargs: Any ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: """Run the given git command with the specified arguments and return - the result as a String + the result as a string. :param method: is the command. Contained "_" characters will be converted to dashes, @@ -1260,7 +1257,7 @@ def _call_process( :param args: is the list of arguments. If None is included, it will be pruned. This allows your commands to call git more conveniently as None - is realized as non-existent + is realized as non-existent. :param kwargs: It contains key-values for the following: @@ -1390,7 +1387,7 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]: return self.__get_object_header(cmd, ref) def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: - """As get_object_header, but returns object data as well + """As get_object_header, but returns object data as well. :return: (hexsha, type_string, size_as_int, data_string) :note: not threadsafe""" @@ -1400,10 +1397,10 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: return (hexsha, typename, size, data) def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]: - """As get_object_header, but returns the data as a stream + """As get_object_header, but returns the data as a stream. :return: (hexsha, type_string, size_as_int, stream) - :note: This method is not threadsafe, you need one independent Command instance per thread to be safe !""" + :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!""" cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True) hexsha, typename, size = self.__get_object_header(cmd, ref) cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() From fc755dae6866b9c9e0aa347297b693fec2c5b415 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 06:38:54 -0400 Subject: [PATCH 095/155] Avoid having a local function seem to be a method The kill_process local function defined in the Git.execute method is a local function and not a method, but it was easy to misread as a method for two reasons: - Its docstring described it as a method. - It was named with a leading underscore, as though it were a nonpublic method. But this name is a local variable, and local variables are always nonpublic (except when they are function parameters, in which case they are in a sense public). A leading underscore in a local variable name usually means the variable is unused in the function. This fixes the docstring and drops the leading underscore from the name. If this is ever extracted from the Git.execute method and placed at class or module scope, then the name can be changed back. --- git/cmd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index a6d287986..1d8c70f32 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -1012,8 +1012,8 @@ def execute( if as_process: return self.AutoInterrupt(proc, command) - def _kill_process(pid: int) -> None: - """Callback method to kill a process.""" + def kill_process(pid: int) -> None: + """Callback to kill a process.""" p = Popen( ["ps", "--ppid", str(pid)], stdout=PIPE, @@ -1046,7 +1046,7 @@ def _kill_process(pid: int) -> None: if kill_after_timeout is not None: kill_check = threading.Event() - watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,)) + watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,)) # Wait for the process to return status = 0 From 2d1efdca84e266a422f4298ee94ee9b8dae6c32e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 07:55:35 -0400 Subject: [PATCH 096/155] Test that git.cmd.execute_kwargs is correct --- test/test_git.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_git.py b/test/test_git.py index 583c74fa3..723bf624b 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -5,6 +5,7 @@ # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib +import inspect import logging import os import os.path as osp @@ -364,3 +365,11 @@ def counter_stderr(line): self.assertEqual(count[1], line_count) self.assertEqual(count[2], line_count) + + def test_execute_kwargs_set_agrees_with_method(self): + parameter_names = inspect.signature(cmd.Git.execute).parameters.keys() + self_param, command_param, *most_params, extra_kwargs_param = parameter_names + self.assertEqual(self_param, "self") + self.assertEqual(command_param, "command") + self.assertEqual(set(most_params), cmd.execute_kwargs) # Most important. + self.assertEqual(extra_kwargs_param, "subprocess_kwargs") From a8a43fe6f8d6f0a7f9067149859634d624406bb1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 08:50:17 -0400 Subject: [PATCH 097/155] Simplify shell test helper with with_exceptions=False Instead of swallowing GitCommandError exceptions in the helper used by test_it_uses_shell_or_not_as_specified and test_it_logs_if_it_uses_a_shell, this modifies the helper so it prevents Git.execute from raising the exception in the first place. --- test/test_git.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 723bf624b..8d269f38a 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -4,7 +4,6 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -import contextlib import inspect import logging import os @@ -97,10 +96,8 @@ def _do_shell_combo(self, value_in_call, value_from_class): # git.cmd gets Popen via a "from" import, so patch it there. with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen: # Use a command with no arguments (besides the program name), so it runs - # with or without a shell, on all OSes, with the same effect. Since git - # errors out when run with no arguments, we swallow that error. - with contextlib.suppress(GitCommandError): - self.git.execute(["git"], shell=value_in_call) + # with or without a shell, on all OSes, with the same effect. + self.git.execute(["git"], with_exceptions=False, shell=value_in_call) return mock_popen From 9fa1ceef2c47c9f58e10d8925cc166fdfd6b5183 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 09:45:45 -0400 Subject: [PATCH 098/155] Extract a _assert_logged_for_popen method This extracts the logic of searching log messages, and asserting that (at least) one matches a pattern for the report of a Popen call with a given argument, from test_it_logs_if_it_uses_a_shell into a new nonpublic test helper method _assert_logged_for_popen. The extracted version is modified to make it slightly more general, and slightly more robust. This is still not extremely robust: the notation used to log Popen calls is informal, so it wouldn't make sense to really parse it as code. But this no longer assumes that the representation of a value ends at a word boundary, nor that the value is free of regular expression metacharacters. --- test/test_git.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 8d269f38a..10b346e4d 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -40,6 +40,13 @@ def tearDown(self): gc.collect() + def _assert_logged_for_popen(self, log_watcher, name, value): + re_name = re.escape(name) + re_value = re.escape(str(value)) + re_line = re.compile(fr"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]") + match_attempts = [re_line.match(message) for message in log_watcher.output] + self.assertTrue(any(match_attempts), repr(log_watcher.output)) + @mock.patch.object(Git, "execute") def test_call_process_calls_execute(self, git): git.return_value = "" @@ -113,14 +120,9 @@ def test_it_uses_shell_or_not_as_specified(self, case): def test_it_logs_if_it_uses_a_shell(self, case): """``shell=`` in the log message agrees with what is passed to `Popen`.""" value_in_call, value_from_class = case - with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: mock_popen = self._do_shell_combo(value_in_call, value_from_class) - - popen_shell_arg = mock_popen.call_args.kwargs["shell"] - expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)") - match_attempts = [expected_message.fullmatch(message) for message in log_watcher.output] - self.assertTrue(any(match_attempts), repr(log_watcher.output)) + self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"]) def test_it_executes_git_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") From 790a790f49a2548c620532ee2b9705b09fb33855 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 09:59:28 -0400 Subject: [PATCH 099/155] Log stdin arg as such, and test that this is done This changes how the Popen call debug logging line shows the informal summary of what kind of thing is being passed as the stdin argument to Popen, showing it with stdin= rather than istream=. The new test, with "istream" in place of "stdin", passed before the code change in the git.cmd module, failed once "istream" was changed to "stdin" in the test, and then, as expected, passed again once "istream=" was changed to "stdin=" in the log.debug call in git.cmd.Git.execute. --- git/cmd.py | 2 +- test/test_git.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 1d8c70f32..e545ba160 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -979,7 +979,7 @@ def execute( if shell is None: shell = self.USE_SHELL log.debug( - "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", + "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, stdin=%s)", redacted_command, cwd, universal_newlines, diff --git a/test/test_git.py b/test/test_git.py index 10b346e4d..1ee7b3642 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -124,6 +124,16 @@ def test_it_logs_if_it_uses_a_shell(self, case): mock_popen = self._do_shell_combo(value_in_call, value_from_class) self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"]) + @ddt.data( + ("None", None), + ("", subprocess.PIPE), + ) + def test_it_logs_istream_summary_for_stdin(self, case): + expected_summary, istream_argument = case + with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: + self.git.execute(["git", "version"], istream=istream_argument) + self._assert_logged_for_popen(log_watcher, "stdin", expected_summary) + def test_it_executes_git_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") From c3fde7fb8dcd48d17ee24b78db7b0dd25d2348ab Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 10:16:10 -0400 Subject: [PATCH 100/155] Log args in the order they are passed to Popen This is still not including all or even most of the arguments, nor are all the logged arguments literal (nor should either of those things likely be changed). It is just to facilitate easier comparison of what is logged to the Popen call in the code. --- git/cmd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index e545ba160..c35e56703 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -979,12 +979,12 @@ def execute( if shell is None: shell = self.USE_SHELL log.debug( - "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, stdin=%s)", + "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)", redacted_command, cwd, - universal_newlines, - shell, istream_ok, + shell, + universal_newlines, ) try: with maybe_patch_caller_env: From ab958865d89b3146bb953f826f30da11dc59139a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 10:30:58 -0400 Subject: [PATCH 101/155] Eliminate istream_ok variable In Git.execute, the stdin argument to Popen is the only one where a compound expression (rather than a single term) is currently passed. So having that be the same in the log message makes it easier to understand what is going on, as well as to see how the information shown in the log corresponds to what Popen receives. --- git/cmd.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index c35e56703..7c448e3f2 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -973,16 +973,13 @@ def execute( # end handle stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") - istream_ok = "None" - if istream: - istream_ok = "" if shell is None: shell = self.USE_SHELL log.debug( "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)", redacted_command, cwd, - istream_ok, + "" if istream else "None", shell, universal_newlines, ) From c3e926fbfda3bf5a1723258e990dfcfa45f2ef86 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 12:16:07 -0400 Subject: [PATCH 102/155] Fix a small YAML formatting style inconsistency --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1b049ba02..5564a526b 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,9 +17,9 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - - experimental: false - - python-version: "3.12" - experimental: true + - experimental: false + - python-version: "3.12" + experimental: true defaults: run: From b54a28a50b3e81e04b5b9ef61297fd12c62d09b3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 12:17:58 -0400 Subject: [PATCH 103/155] No longer allow CI to select a prerelease for 3.12 Since 3.12.0 stable is out, and available via setup-python, per: https://github.com/actions/python-versions/blob/main/versions-manifest.json --- .github/workflows/pythonpackage.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5564a526b..e43317807 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -18,8 +18,6 @@ jobs: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - experimental: false - - python-version: "3.12" - experimental: true defaults: run: From 8c4df3cfdca1eebd2f07c7be24ab5e2805ec2708 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 03:12:48 -0400 Subject: [PATCH 104/155] Add pre-commit hook to run shellcheck This also reorders the hooks from pre-commit/pre-commit-hooks so that the overall order of all hooks from all repositories is: lint Python, lint non-Python, non-lint. --- .pre-commit-config.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67aefb342..c5973c9ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,9 +9,14 @@ repos: - flake8-typing-imports==1.14.0 exclude: ^doc|^git/ext/ + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.9.0.5 + hooks: + - id: shellcheck + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-merge-conflict - id: check-toml - id: check-yaml + - id: check-merge-conflict From f3be76f474636f9805756bc9f05b22fb4aa8809d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 04:49:28 -0400 Subject: [PATCH 105/155] Force color when running shellcheck in pre-commit Its output is colorized normally, but on CI it is not colorized without this (pre-commit's own output is, but not shellcheck's). --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5973c9ea..bacc90913 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,7 @@ repos: rev: v0.9.0.5 hooks: - id: shellcheck + args: [--color] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 From 7dd8added2b1695b1740f0d1d7d7b2858a49a88c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 04:17:04 -0400 Subject: [PATCH 106/155] Suppress SC2086 where word splitting is intended This suppresses ShellCheck SC2016, "Double quote to prevent globbing and word splitting," on the command in the version check script that expands $config_opts to build the "-c ..." arguments. It also moves the code repsonsible for getting the latest tag, which this is part of, into a function for that purpose, so it's clear that building config_opts is specifically for that, and so that the code is not made harder to read by adding the ShellCheck suppression comment. (The suppression applies only to the immediate next command.) --- check-version.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/check-version.sh b/check-version.sh index c50bf498b..46aa56173 100755 --- a/check-version.sh +++ b/check-version.sh @@ -10,6 +10,13 @@ trap 'echo "$0: Check failed. Stopping." >&2' ERR readonly version_path='VERSION' readonly changes_path='doc/source/changes.rst' +function get_latest_tag() { + local config_opts + config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" + # shellcheck disable=SC2086 # Deliberately word-splitting the arguments. + git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1 +} + echo 'Checking current directory.' test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. @@ -26,8 +33,7 @@ test -z "$(git status -s --ignore-submodules)" version_version="$(cat "$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" -config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" -latest_tag="$(git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1)" +latest_tag="$(get_latest_tag)" head_sha="$(git rev-parse HEAD)" latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")" From 21875b5a84c899a5b38c627e895a1bb58344b2a1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 04:31:18 -0400 Subject: [PATCH 107/155] Don't split and glob the interpreter name In the release building script, this changes $1 to "$1", because $1 without quotes means to perform word splitting and globbing (pathname expansion) on the parameter (unless otherwise disabled by the value of $IFS or "set -f", respectively) and use the result of doing so, which isn't the intent of the code. This function is only used from within the script, where it is not given values that would be changed by these additional expansions. So this is mainly about communicating intent. (If in the future it is intended that multiple arguments be usable, then they should be passed as separate arguments to release_with, which should be modified by replacing "$1" with "$@". I have not used "$@" at this time because it is not intuitively obvious that the arguments should be to the interpreter, rather than to the build module, so I don't think this should be supported unless or until a need for it determines that.) --- build-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-release.sh b/build-release.sh index 5840e4472..4eb760ddd 100755 --- a/build-release.sh +++ b/build-release.sh @@ -6,7 +6,7 @@ set -eEu function release_with() { - $1 -m build --sdist --wheel + "$1" -m build --sdist --wheel } if test -n "${VIRTUAL_ENV:-}"; then From 0920371905561d7a242a8be158b79d1a8408a7c4 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 04:40:08 -0400 Subject: [PATCH 108/155] Extract suggest_venv out of the else block I think this is easier to read, but this is admittedly subjective. This commit also makes the separate change of adjusting comment spacing for consistency within the script. (Two spaces before "#" is not widely regarded as better than one in shell scripting, so unlike Python where PEP-8 recommends that, it would be equally good to have changed all the other places in the shell scrips to have just one.) --- build-release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build-release.sh b/build-release.sh index 4eb760ddd..4fd4a2251 100755 --- a/build-release.sh +++ b/build-release.sh @@ -9,6 +9,11 @@ function release_with() { "$1" -m build --sdist --wheel } +function suggest_venv() { + local venv_cmd='python -m venv env && source env/bin/activate' + printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" +} + if test -n "${VIRTUAL_ENV:-}"; then deps=(build twine) # Install twine along with build, as we need it later. echo "Virtual environment detected. Adding packages: ${deps[*]}" @@ -16,11 +21,7 @@ if test -n "${VIRTUAL_ENV:-}"; then echo 'Starting the build.' release_with python else - function suggest_venv() { - venv_cmd='python -m venv env && source env/bin/activate' - printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" - } trap suggest_venv ERR # This keeps the original exit (error) code. echo 'Starting the build.' - release_with python3 # Outside a venv, use python3. + release_with python3 # Outside a venv, use python3. fi From e973f527f92a932e833167c57cb4d4eeb3103429 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 05:06:59 -0400 Subject: [PATCH 109/155] Use some handy bash-isms in version check script The script is nonportable to other shells already because of its use of trap (and other features, such as implicit assumptions made about echo, and the function keyword), which its hashbang already clearly conveys. This uses: - $( Date: Wed, 27 Sep 2023 05:23:27 -0400 Subject: [PATCH 110/155] Have init script treat master unambiguously as a branch Because users may have an old version of git without "git switch", init-tests-after-clone.sh should continue to use "git checkout" to attempt to switch to master. But without "--", this suffers from the problem that it's ambiguous if master is a branch (so checkout behaves like switch) or a path (so checkout behaves like restore). There are two cases where this ambiguity can be a problem. The most common is on a fork with no master branch but also, fortunately, no file or directory named "master". Then the problem is just the error message (printed just before the script proceeds to redo the checkout with -b): error: pathspec 'master' did not match any file(s) known to git The real cause of the error is the branch being absent, as happens when a fork copies only the main branch and the upstream remote is not also set up. Adding the "--" improves the error message: fatal: invalid reference: master However, it is possible, though unlikely, for a file or directory called "master" to exist. In that case, if there is also no master branch, git discards unstaged changes made to the file or (much worse!) everywhere in that directory, potentially losing work. This commit adds "--" to the right of "master" so git never regards it as a path. This is not needed with -b, which is always followed by a symbolic name, so I have not added it there. (Note that the command is still imperfect because, for example, in rare cases there could be a master *tag*--and no master branch--in which case, as before, HEAD would be detached there and the script would attempt to continue.) --- init-tests-after-clone.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 95ced98b7..52c0c06aa 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -10,7 +10,7 @@ if [[ -z "$TRAVIS" ]]; then fi git tag __testing_point__ -git checkout master || git checkout -b master +git checkout master -- || git checkout -b master git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard HEAD~1 From e604b469a1bdfb83f03d4c2ef1f79b45b8a5c3fa Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 05:44:02 -0400 Subject: [PATCH 111/155] Use 4-space indentation in all shell scripts This had been done everywhere except in init-tests-after-clone.sh. --- init-tests-after-clone.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 52c0c06aa..9d65570da 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -3,10 +3,10 @@ set -e if [[ -z "$TRAVIS" ]]; then - read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer - if [[ ! $answer =~ [yY] ]]; then - exit 2 - fi + read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer + if [[ ! $answer =~ [yY] ]]; then + exit 2 + fi fi git tag __testing_point__ From 19dfbd8ce4df9bde9b36eda12304ec4db71b084a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 07:33:56 -0400 Subject: [PATCH 112/155] Make the init script a portable POSIX shell script This translates init-tests-after-clone.sh from bash to POSIX sh, changing the hashbang and replacing all bash-specific features, so that it can be run on any POSIX-compatible ("Bourne style") shell, including but not limited to bash. This allows it to be used even on systems that don't have any version of bash installed anywhere. Only that script is modified. The other shell scripts make greater use of (and gain greater benefit from) bash features, and are also only used for building and releasing. In contrast, this script is meant to be used by most users who clone the repository. --- init-tests-after-clone.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 9d65570da..19ff0fd28 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -1,12 +1,16 @@ -#!/usr/bin/env bash +#!/bin/sh set -e -if [[ -z "$TRAVIS" ]]; then - read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer - if [[ ! $answer =~ [yY] ]]; then - exit 2 - fi +if test -z "$TRAVIS"; then + printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 + read -r answer + case "$answer" in + [yY]) + ;; + *) + exit 2 ;; + esac fi git tag __testing_point__ From 7110bf8e04f96ffb20518ebf48803a50d1e3bf22 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 14:56:20 -0400 Subject: [PATCH 113/155] Move extra tag-fetching step into init script This makes three related changes: - Removes "git fetch --tags" from the instructions in the readme, because the goal of this command can be achieved in the init-tests-after-clone.sh script, and because this fetch command, as written, is probably only achieving that goal in narrow cases. In clones and fetches, tags on branches are fetched by default, and the tags needed by the tests are on branches. So the situations where "git fetch --tags" was helping were (a) when the remote recently gained the tags, and (b) when the original remote was cloned in an unusual way, not fetching all tags. In both cases, the "--tags" option is not what makes that fetch get the needed tags. - Adds "git fetch --all --tags" to init-tests-after-clone.sh. The "--all" option causes it to fetch from all remotes, and this is more significant than "--tags", since the tags needed for testing are on fetched branches. This achieves what "git fetch --tags" was achieving, and it also has the benefit of getting tags from remotes that have been added but not fetched from, as happens with an upstream remote that was manually added with no further action. (It also gets branches from those remotes, but if master is on multiple remotes but at different commits then "git checkout master" may not be able to pick one. So do this *after* rather than before that.) - Skips this extra fetch, and also the submodule cloning/updating step, when running on CI. CI jobs will already have taken care of cloning the repo with submodules recursively, and fetching all available tags. In forks without tags, the necessary tags for the test are not fetched--but the upstream remote is not set up on CI, so they wouldn't be obtained anyway, not even by refetching with "--all". --- README.md | 7 +++---- init-tests-after-clone.sh | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 65c1e7bae..c17340f63 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ To clone the [the GitHub repository](https://github.com/gitpython-developers/Git ```bash git clone https://github.com/gitpython-developers/GitPython cd GitPython -git fetch --tags ./init-tests-after-clone.sh ``` @@ -114,9 +113,9 @@ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525). ### RUNNING TESTS -_Important_: Right after cloning this repository, please be sure to have -executed `git fetch --tags` followed by the `./init-tests-after-clone.sh` -script in the repository root. Otherwise you will encounter test failures. +_Important_: Right after cloning this repository, please be sure to have executed +the `./init-tests-after-clone.sh` script in the repository root. Otherwise +you will encounter test failures. On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` exists in `Git\mingw64\libexec\git-core\`. diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 19ff0fd28..efa30a2dc 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -19,4 +19,8 @@ git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard __testing_point__ + +test -z "$TRAVIS" || exit 0 # CI jobs will already have taken care of the rest. + +git fetch --all --tags git submodule update --init --recursive From c7cdaf48865f4483bfd34e1f7527ab3e1d205dad Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 18:27:44 -0400 Subject: [PATCH 114/155] Reduce code duplication in version check script This extracts duplicated code to functions in check-version.sh. One effect is unfortunately that the specific commands being run are less explicitly clear when reading the script. However, small future changes, if accidentally made to one but not the other in either pair of "git status" commands, would create a much more confusing situation. So I think this change is justified overall. --- check-version.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/check-version.sh b/check-version.sh index d4200bd20..b47482d7e 100755 --- a/check-version.sh +++ b/check-version.sh @@ -10,6 +10,11 @@ trap 'echo "$0: Check failed. Stopping." >&2' ERR readonly version_path='VERSION' readonly changes_path='doc/source/changes.rst' +function check_status() { + git status -s "$@" + test -z "$(git status -s "$@")" +} + function get_latest_tag() { local config_opts printf -v config_opts ' -c versionsort.suffix=-%s' alpha beta pre rc RC @@ -23,13 +28,11 @@ test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. echo "Checking that $version_path and $changes_path exist and have no uncommitted changes." test -f "$version_path" test -f "$changes_path" -git status -s -- "$version_path" "$changes_path" -test -z "$(git status -s -- "$version_path" "$changes_path")" +check_status -- "$version_path" "$changes_path" # This section can be commented out, if absolutely necessary. echo 'Checking that ALL changes are committed.' -git status -s --ignore-submodules -test -z "$(git status -s --ignore-submodules)" +check_status --ignore-submodules version_version="$(<"$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" From f6dbba2ae4ad9eb3c470b30acce280a4fb6d0445 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 18:37:26 -0400 Subject: [PATCH 115/155] A couple more script tweaks for clarity - Because the substitition string after the hyphen is empty, "${VIRTUAL_ENV:-}" and "${VIRTUAL_ENV-}" have the same effect. However, the latter, which this changes it to, expresses the correct idea that the special case being handled is when the variable is unset: in this case, we expand an empty field rather than triggering an error due to set -u. When the variable is set but empty, it already expands to the substitution value, and including that in the special case with ":" is thus misleading. - Continuing in the vein of d18d90a (and 1e0b3f9), this removes another explicit newline by adding another echo command to print the leading blank line before the table. --- build-release.sh | 2 +- check-version.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build-release.sh b/build-release.sh index 4fd4a2251..49c13b93a 100755 --- a/build-release.sh +++ b/build-release.sh @@ -14,7 +14,7 @@ function suggest_venv() { printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" } -if test -n "${VIRTUAL_ENV:-}"; then +if test -n "${VIRTUAL_ENV-}"; then deps=(build twine) # Install twine along with build, as we need it later. echo "Virtual environment detected. Adding packages: ${deps[*]}" pip install --quiet --upgrade "${deps[@]}" diff --git a/check-version.sh b/check-version.sh index b47482d7e..dac386e46 100755 --- a/check-version.sh +++ b/check-version.sh @@ -41,7 +41,8 @@ head_sha="$(git rev-parse HEAD)" latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")" # Display a table of all the current version, tag, and HEAD commit information. -echo $'\nThe VERSION must be the same in all locations, and so must the HEAD and tag SHA' +echo +echo 'The VERSION must be the same in all locations, and so must the HEAD and tag SHA' printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 'changes.rst' "$changes_version" \ 'Latest tag' "$latest_tag" \ From 5060c9dc42677c04af1b696e77228d17dac645a4 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 27 Sep 2023 19:05:25 -0400 Subject: [PATCH 116/155] Explain what each step in the init script achieves This adds comments to init-tests-after-clone.sh to explain what each of the steps is doing that is needed by some of the tests. It also refactors some recently added logic, in a way that lends itself to greater clarity when combined with these comments. --- init-tests-after-clone.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index efa30a2dc..a53acbbef 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -13,14 +13,26 @@ if test -z "$TRAVIS"; then esac fi +# Stop if we have run this. (You can delete __testing_point__ to let it rerun.) +# This also keeps track of where we are, so we can get back here. git tag __testing_point__ + +# The tests need a branch called master. git checkout master -- || git checkout -b master + +# The tests need a reflog history on the master branch. git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard HEAD~1 + +# Point the master branch where we started, so we test the correct code. git reset --hard __testing_point__ -test -z "$TRAVIS" || exit 0 # CI jobs will already have taken care of the rest. +# Do some setup that CI takes care of but that may not have been done locally. +if test -z "$TRAVIS"; then + # The tests needs some version tags. Try to get them even in forks. + git fetch --all --tags -git fetch --all --tags -git submodule update --init --recursive + # The tests need submodules, including a submodule with a submodule. + git submodule update --init --recursive +fi From d5479b206fd3a5815bad618d269ecb5e1577feb8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 01:40:33 -0400 Subject: [PATCH 117/155] Use set -u in init script This is already done in the other shell scripts. Although init-tests-after-clone.sh does not have as many places where a bug could slip through by an inadvertently nonexistent parameter, it does have $answer (and it may have more expansions in the future). --- init-tests-after-clone.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index a53acbbef..5ca767654 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -1,8 +1,8 @@ #!/bin/sh -set -e +set -eu -if test -z "$TRAVIS"; then +if test -z "${TRAVIS-}"; then printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 read -r answer case "$answer" in @@ -29,7 +29,7 @@ git reset --hard HEAD~1 git reset --hard __testing_point__ # Do some setup that CI takes care of but that may not have been done locally. -if test -z "$TRAVIS"; then +if test -z "${TRAVIS-}"; then # The tests needs some version tags. Try to get them even in forks. git fetch --all --tags From 52f9a68d04233c3be9653a4a8b56a58afb9d7093 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 03:01:43 -0400 Subject: [PATCH 118/155] Make the "all" Makefile target more robust This is a minor improvement to the robustness of the command for "make all", in the face of plausible future target names. - Use [[:alpha:]] instead of [a-z] because, in POSIX BRE and ERE, whether [a-z] includes capital letters depends on the current collation order. (The goal here is greater consistency across systems, and for this it would also be okay to use [[:lower:]].) - Pass -x to the command that filters out the all target itself, so that the entire name must be "all", because a future target may contain the substring "all" as part of a larger word. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 38090244c..fe9d04a18 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean release force_release all: - @grep -Ee '^[a-z].*:' Makefile | cut -d: -f1 | grep -vF all + @grep -E '^[[:alpha:]].*:' Makefile | cut -d: -f1 | grep -vxF all clean: rm -rf build/ dist/ .eggs/ .tox/ From b88d07e47667194e0668431e2a871926eb54d948 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 03:16:33 -0400 Subject: [PATCH 119/155] Use a single awk instead of two greps and a cut This seems simpler to me, but I admit it is subjective. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fe9d04a18..d4f9acf87 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean release force_release all: - @grep -E '^[[:alpha:]].*:' Makefile | cut -d: -f1 | grep -vxF all + @awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile clean: rm -rf build/ dist/ .eggs/ .tox/ From d36818cf59d398e50cc6568f2239d69cd904883d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 03:33:59 -0400 Subject: [PATCH 120/155] Add a black check to pre-commit - Add the psf/black-pre-commit-mirror pre-commit hook but have it just check and report violations rather than fixing them automatically (to avoid inconsistency with all the other hooks, and also so that the linting instructions and CI lint workflow can remain the same and automatically do the black check). - Remove the "black" environment from tox.ini, since it is now part of the linting done in the "lint" environment. (But README.md remains the same, as it documented actually auto-formatting with black, which is still done the same way.) - Add missing "exclude" keys for the shellcheck and black pre-commit hooks to explicitly avoid traversing into the submodules. --- .pre-commit-config.yaml | 8 ++++++++ tox.ini | 7 +------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bacc90913..d6b496a31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,11 @@ repos: + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.9.1 + hooks: + - id: black + args: [--check, --diff] + exclude: ^git/ext/ + - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: @@ -14,6 +21,7 @@ repos: hooks: - id: shellcheck args: [--color] + exclude: ^git/ext/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/tox.ini b/tox.ini index 82a41e22c..ed7896b59 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] requires = tox>=4 -env_list = py{37,38,39,310,311,312}, lint, mypy, black +env_list = py{37,38,39,310,311,312}, lint, mypy [testenv] description = Run unit tests @@ -20,11 +20,6 @@ base_python = py39 commands = mypy -p git ignore_outcome = true -[testenv:black] -description = Check style with black -base_python = py39 -commands = black --check --diff . - # Run "tox -e html" for this. It is deliberately excluded from env_list, as # unlike the other environments, this one writes outside the .tox/ directory. [testenv:html] From 4ba5ad107c4e91f62dacee9bfef277f2674fd90f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 04:27:07 -0400 Subject: [PATCH 121/155] Fix typo in comment --- init-tests-after-clone.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 5ca767654..5d1c16f0a 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -30,7 +30,7 @@ git reset --hard __testing_point__ # Do some setup that CI takes care of but that may not have been done locally. if test -z "${TRAVIS-}"; then - # The tests needs some version tags. Try to get them even in forks. + # The tests need some version tags. Try to get them even in forks. git fetch --all --tags # The tests need submodules, including a submodule with a submodule. From 5d8ddd9009ec38ecafddb55acfe7ad9919b1bb0d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 05:12:24 -0400 Subject: [PATCH 122/155] Use two hooks for black: to check, and format This splits the pre-commit hook for black into two hooks, both using the same repo and id but separately aliased: - black-check, which checks but does not modify files. This only runs when the manual stage is specified, and it is used by tox and on CI, with tox.ini and lint.yml modified accordingly. - black-format, which autoformats code. This provides the behavior most users will expect from a pre-commit hook for black. It runs automatically along with other hooks. But tox.ini and lint.yml, in addition to enabling the black-check hook, also explicitly skip the black-format hook. The effect is that in ordinary development the pre-commit hook for black does auto-formatting, but that pre-commit continues to be used effectively for running checks in which code should not be changed. --- .github/workflows/lint.yml | 4 ++++ .pre-commit-config.yaml | 8 ++++++++ README.md | 12 ++++++------ tox.ini | 4 +++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2204bb792..71e0a8853 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,3 +14,7 @@ jobs: python-version: "3.x" - uses: pre-commit/action@v3.0.0 + with: + extra_args: --hook-stage manual + env: + SKIP: black-format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6b496a31..5664fe980 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,16 @@ repos: rev: 23.9.1 hooks: - id: black + alias: black-check + name: black (check) args: [--check, --diff] exclude: ^git/ext/ + stages: [manual] + + - id: black + alias: black-format + name: black (format) + exclude: ^git/ext/ - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 diff --git a/README.md b/README.md index c17340f63..8cb6c88c2 100644 --- a/README.md +++ b/README.md @@ -142,22 +142,22 @@ To test, run: pytest ``` -To lint, run: +To lint, and apply automatic code formatting, run: ```bash pre-commit run --all-files ``` -To typecheck, run: +Code formatting can also be done by itself by running: -```bash -mypy -p git +``` +black . ``` -For automatic code formatting, run: +To typecheck, run: ```bash -black . +mypy -p git ``` Configuration for flake8 is in the `./.flake8` file. diff --git a/tox.ini b/tox.ini index ed7896b59..518577183 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,9 @@ commands = pytest --color=yes {posargs} [testenv:lint] description = Lint via pre-commit base_python = py39 -commands = pre-commit run --all-files +set_env = + SKIP = black-format +commands = pre-commit run --all-files --hook-stage manual [testenv:mypy] description = Typecheck with mypy From a872d9c9c90b2a99c0b1a29e10aaecbe2fa387ed Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 15:29:10 -0400 Subject: [PATCH 123/155] Pass --all-files explicitly so it is retained In the lint workflow, passing extra-args replaced --all-files, rather than keeping it but adding the extra arguments after it. (But --show-diff-on-failure and --color=always were still passed.) --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 71e0a8853..91dd919e0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,6 +15,6 @@ jobs: - uses: pre-commit/action@v3.0.0 with: - extra_args: --hook-stage manual + extra_args: --all-files --hook-stage manual env: SKIP: black-format From 9b9de1133b85fd308c8795a4f312d3cfbf40b75f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 15:34:28 -0400 Subject: [PATCH 124/155] Fix the formatting Now that the changes to lint.yml are fixed up, tested, and shown to be capable of revealing formatting errors, the formatting error I was using for testing (which is from 9fa1cee where I had introduced it inadvertently) can be fixed. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 1ee7b3642..cf82d9ac7 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -43,7 +43,7 @@ def tearDown(self): def _assert_logged_for_popen(self, log_watcher, name, value): re_name = re.escape(name) re_value = re.escape(str(value)) - re_line = re.compile(fr"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]") + re_line = re.compile(rf"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]") match_attempts = [re_line.match(message) for message in log_watcher.output] self.assertTrue(any(match_attempts), repr(log_watcher.output)) From 5d1506352cdff5f3207c5942d82cdc628cb03f3c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 05:29:50 -0400 Subject: [PATCH 125/155] Add "make lint" to lint without auto-formatting As on CI and with tox (but not having to build and create and use a tox environment). This may not be the best way to do it, since currently the project's makefiles are otherwise used only for things directly related to building and publishing. However, this seemed like a readily available way to run the moderately complex command that produces the same behavior as on CI or with tox, but outside a tox environment. It may be possible to improve on this in the future. --- Makefile | 5 ++++- README.md | 7 ++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d4f9acf87..839dc9f78 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ -.PHONY: all clean release force_release +.PHONY: all lint clean release force_release all: @awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile +lint: + SKIP=black-format pre-commit run --all-files --hook-stage manual + clean: rm -rf build/ dist/ .eggs/ .tox/ diff --git a/README.md b/README.md index 8cb6c88c2..5ae43dc46 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,8 @@ To lint, and apply automatic code formatting, run: pre-commit run --all-files ``` -Code formatting can also be done by itself by running: - -``` -black . -``` +- Linting without modifying code can be done with: `make lint` +- Auto-formatting without other lint checks can be done with: `black .` To typecheck, run: From 6de86a85e1d17e32cab8996aa66f10783e6beced Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 05:50:35 -0400 Subject: [PATCH 126/155] Update readme about most of the test/lint tools Including tox. --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5ae43dc46..aac69e0df 100644 --- a/README.md +++ b/README.md @@ -157,12 +157,26 @@ To typecheck, run: mypy -p git ``` -Configuration for flake8 is in the `./.flake8` file. +#### CI (and tox) -Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`. +The same linting, and running tests on all the different supported Python versions, will be performed: -The same linting and testing will also be performed against different supported python versions -upon submitting a pull request (or on each push if you have a fork with a "main" branch and actions enabled). +- Upon submitting a pull request. +- On each push, *if* you have a fork with a "main" branch and GitHub Actions enabled. +- Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed). + +#### Configuration files + +Specific tools: + +- Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`. +- Configuration for `flake8` is in the `./.flake8` file. + +Orchestration tools: + +- Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file. +- Configuration for `tox` is in `./tox.ini`. +- Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`. ### Contributions From f0949096f4a2dec466bce48d46a4bf2dd598d36f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 06:31:05 -0400 Subject: [PATCH 127/155] Add BUILDDIR var to doc/Makefile; have tox use it This adds BUILDDIR as a variable in the documentation generation makefile that, along SPHINXOPTS, SPHINXBUILD, and PAPER, defaults to the usual best value but can be set when invoking make. The main use for choosing a different build output directory is to test building without overwriting or otherwise interfering with any files from a build one may really want to use. tox.ini is thus modified to pass a path pointing inside its temporary directory as BUILDDIR, so the "html" tox environment now makes no changes outside the .tox directory. This is thus suitable for being run automatically, so the "html" environment is now in the envlist. --- doc/Makefile | 43 ++++++++++++++++++++++--------------------- tox.ini | 8 ++++---- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index ef2d60e5f..ddeadbd7e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,6 +2,7 @@ # # You can set these variables from the command line. +BUILDDIR = build SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = @@ -9,7 +10,7 @@ PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html web pickle htmlhelp latex changes linkcheck @@ -24,52 +25,52 @@ help: @echo " linkcheck to check all external links for integrity" clean: - -rm -rf build/* + -rm -rf $(BUILDDIR)/* html: - mkdir -p build/html build/doctrees - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html + mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo - @echo "Build finished. The HTML pages are in build/html." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." pickle: - mkdir -p build/pickle build/doctrees - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle + mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." web: pickle json: - mkdir -p build/json build/doctrees - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json + mkdir -p $(BUILDDIR)/json $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: - mkdir -p build/htmlhelp build/doctrees - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp + mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in build/htmlhelp." + ".hhp project file in $(BUILDDIR)/htmlhelp." latex: - mkdir -p build/latex build/doctrees - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex + mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo - @echo "Build finished; the LaTeX files are in build/latex." + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: - mkdir -p build/changes build/doctrees - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes + mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo - @echo "The overview file is in build/changes." + @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - mkdir -p build/linkcheck build/doctrees - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck + mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ - "or in build/linkcheck/output.txt." + "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/tox.ini b/tox.ini index 518577183..47a7a6209 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] requires = tox>=4 -env_list = py{37,38,39,310,311,312}, lint, mypy +env_list = py{37,38,39,310,311,312}, lint, mypy, html [testenv] description = Run unit tests @@ -22,11 +22,11 @@ base_python = py39 commands = mypy -p git ignore_outcome = true -# Run "tox -e html" for this. It is deliberately excluded from env_list, as -# unlike the other environments, this one writes outside the .tox/ directory. [testenv:html] description = Build HTML documentation base_python = py39 deps = -r doc/requirements.txt allowlist_externals = make -commands = make -C doc html +commands = + make BUILDDIR={env_tmp_dir}/doc/build -C doc clean + make BUILDDIR={env_tmp_dir}/doc/build -C doc html From fc969807086d4483c4c32b80d2c2b67a6c6813e7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 15:27:11 -0400 Subject: [PATCH 128/155] Have init script check for GitHub Actions As well as still checking for Travis, for backward compatibility and because experience shows that this is safe. The check can be much broader, and would be more accurate, with fewer false negatives. But a false positive can result in local data loss because the script does hard resets on CI without prompting for confirmation. So for now, this just checks $TRAVIS and $GITHUB_ACTIONS. Now that GHA is included, the CI workflows no longer need to set $TRAVIS when running the script, so that is removed. --- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- init-tests-after-clone.sh | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index e818803f1..cd913385f 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -40,7 +40,7 @@ jobs: - name: Prepare this repo for tests run: | - TRAVIS=yes ./init-tests-after-clone.sh + ./init-tests-after-clone.sh - name: Set git user identity and command aliases for the tests run: | diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index e43317807..2a82e0e03 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -37,7 +37,7 @@ jobs: - name: Prepare this repo for tests run: | - TRAVIS=yes ./init-tests-after-clone.sh + ./init-tests-after-clone.sh - name: Set git user identity and command aliases for the tests run: | diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 5d1c16f0a..4697c2ecc 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -2,7 +2,12 @@ set -eu -if test -z "${TRAVIS-}"; then +ci() { + # For now, check just these, as a false positive could lead to data loss. + test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}" +} + +if ! ci; then printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 read -r answer case "$answer" in @@ -29,7 +34,7 @@ git reset --hard HEAD~1 git reset --hard __testing_point__ # Do some setup that CI takes care of but that may not have been done locally. -if test -z "${TRAVIS-}"; then +if ! ci; then # The tests need some version tags. Try to get them even in forks. git fetch --all --tags From b98f15e46a7d5f343b1303b55bc4dae2d18bd621 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 28 Sep 2023 23:59:14 -0400 Subject: [PATCH 129/155] Get tags for tests from original repo as fallback This extends the init script so it tries harder to get version tags: - As before, locally, "git fetch --all --tags" is always run. (This also fetches other objects, and the developer experience would be undesirably inconsistent if that were only sometimes done.) - On CI, run it if version tags are absent. The criterion followed here and in subsequent steps is that if any existing tag starts with a digit, or with the letter v followed by a digit, we regard version tags to be present. This is to balance the benefit of getting the tags (to make the tests work) against the risk of creating a very confusing situation in clones of forks that publish packages or otherwise use their own separate versioning scheme (especially if those tags later ended up being pushed). - Both locally and on CI, after that, if version tags are absent, try to copy them from the original gitpython-developers/GitPython repo, without copying other tags or adding that repo as a remote. Copy only tags that start with a digit, since v+digit tags aren't currently used in this project (though forks may use them). This is a fallback option and it always displays a warning. If it fails, another message is issued for that. Unexpected failure to access the repo terminates the script with a failing exit status, but the absence of version tags in the fallback remote does not, so CI jobs can continue and reveal which tests fail as a result. On GitHub Actions CI specifically, the Actions syntax for creating a warning annotation on the workflow is used, but the warning is still also written to stderr (as otherwise). GHA workflow annotations don't support multi-line warnings, so the message is adjusted into a single line where needed. --- init-tests-after-clone.sh | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 4697c2ecc..5e31e3315 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -2,11 +2,25 @@ set -eu +fallback_repo_for_tags='https://github.com/gitpython-developers/GitPython.git' + ci() { # For now, check just these, as a false positive could lead to data loss. test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}" } +no_version_tags() { + test -z "$(git tag -l '[0-9]*' 'v[0-9]*')" +} + +warn() { + printf '%s\n' "$@" >&2 # Warn in step output. + + if test -n "${GITHUB_ACTIONS-}"; then + printf '::warning ::%s\n' "$*" >&2 # Annotate workflow. + fi +} + if ! ci; then printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 read -r answer @@ -33,11 +47,27 @@ git reset --hard HEAD~1 # Point the master branch where we started, so we test the correct code. git reset --hard __testing_point__ -# Do some setup that CI takes care of but that may not have been done locally. +# The tests need submodules. (On CI, they would already have been checked out.) if ! ci; then - # The tests need some version tags. Try to get them even in forks. + git submodule update --init --recursive +fi + +# The tests need some version tags. Try to get them even in forks. This fetch +# gets other objects too, so for a consistent experience, always do it locally. +if ! ci || no_version_tags; then git fetch --all --tags +fi - # The tests need submodules, including a submodule with a submodule. - git submodule update --init --recursive +# If we still have no version tags, try to get them from the original repo. +if no_version_tags; then + warn 'No local or remote version tags found. Trying fallback remote:' \ + "$fallback_repo_for_tags" + + # git fetch supports * but not [], and --no-tags means no *other* tags, so... + printf 'refs/tags/%d*:refs/tags/%d*\n' 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 | + xargs git fetch --no-tags "$fallback_repo_for_tags" + + if no_version_tags; then + warn 'No version tags found anywhere. Some tests will fail.' + fi fi From 7cca7d2245a504047188943623cc58c4c2e0c35f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 29 Sep 2023 10:28:17 -0400 Subject: [PATCH 130/155] Don't print the exact same warning twice In the step output, the warning that produces a workflow annotation is fully readable (and even made to stand out), so it doesn't need to be printed in the plain way as well, which can be reserved for when GitHub Actions is not detected. --- init-tests-after-clone.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 5e31e3315..49df49daa 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -14,10 +14,10 @@ no_version_tags() { } warn() { - printf '%s\n' "$@" >&2 # Warn in step output. - if test -n "${GITHUB_ACTIONS-}"; then printf '::warning ::%s\n' "$*" >&2 # Annotate workflow. + else + printf '%s\n' "$@" >&2 fi } From e4e009d03b890d456b4c1ff2411591d3a50dcdd0 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 29 Sep 2023 16:59:58 -0400 Subject: [PATCH 131/155] Reword comment to fix ambiguity --- init-tests-after-clone.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index 49df49daa..21d1f86d8 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -52,8 +52,8 @@ if ! ci; then git submodule update --init --recursive fi -# The tests need some version tags. Try to get them even in forks. This fetch -# gets other objects too, so for a consistent experience, always do it locally. +# The tests need some version tags. Try to get them even in forks. This fetches +# other objects too. So, locally, we always do it, for a consistent experience. if ! ci || no_version_tags; then git fetch --all --tags fi From e16e4c0099cd0197b5c80ce6ec9a6b4bca41845e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 29 Sep 2023 18:58:08 -0400 Subject: [PATCH 132/155] Format all YAML files in the same style --- .github/dependabot.yml | 2 +- .pre-commit-config.yaml | 68 ++++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 203f3c889..8c139c7be 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,4 +3,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "weekly" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5664fe980..be97d5f9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,39 +1,39 @@ repos: - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 - hooks: - - id: black - alias: black-check - name: black (check) - args: [--check, --diff] - exclude: ^git/ext/ - stages: [manual] +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.9.1 + hooks: + - id: black + alias: black-check + name: black (check) + args: [--check, --diff] + exclude: ^git/ext/ + stages: [manual] - - id: black - alias: black-format - name: black (format) - exclude: ^git/ext/ + - id: black + alias: black-format + name: black (format) + exclude: ^git/ext/ - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.9.16 - - flake8-comprehensions==3.14.0 - - flake8-typing-imports==1.14.0 - exclude: ^doc|^git/ext/ +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear==23.9.16 + - flake8-comprehensions==3.14.0 + - flake8-typing-imports==1.14.0 + exclude: ^doc|^git/ext/ - - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.9.0.5 - hooks: - - id: shellcheck - args: [--color] - exclude: ^git/ext/ +- repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.9.0.5 + hooks: + - id: shellcheck + args: [--color] + exclude: ^git/ext/ - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-toml - - id: check-yaml - - id: check-merge-conflict +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-toml + - id: check-yaml + - id: check-merge-conflict From 62c024e277820b2fd3764a0499a71f03d4aa432d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 29 Sep 2023 19:33:07 -0400 Subject: [PATCH 133/155] Let tox run lint, mypy, and html envs without 3.9 The tox environments that are not duplicated per Python version were set to run on Python 3.9, for consistency with Cygwin, where 3.9 is the latest available (through official Cygwin packages), so output can be compared between Cygwin and other platforms while troubleshooting problems. However, this prevented them from running altogether on systems where Python 3.9 was not installed. So I've added all the other Python versions the project supports as fallback versions for those tox environments to use, in one of the reasonable precedence orders, while keeping 3.9 as the first choice for the same reasons as before. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 47a7a6209..f9ac25b78 100644 --- a/tox.ini +++ b/tox.ini @@ -11,20 +11,20 @@ commands = pytest --color=yes {posargs} [testenv:lint] description = Lint via pre-commit -base_python = py39 +base_python = py{39,310,311,312,38,37} set_env = SKIP = black-format commands = pre-commit run --all-files --hook-stage manual [testenv:mypy] description = Typecheck with mypy -base_python = py39 +base_python = py{39,310,311,312,38,37} commands = mypy -p git ignore_outcome = true [testenv:html] description = Build HTML documentation -base_python = py39 +base_python = py{39,310,311,312,38,37} deps = -r doc/requirements.txt allowlist_externals = make commands = From 9e245d0561ca2f6249e9435a04761da2c64977f1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 1 Oct 2023 00:03:16 -0400 Subject: [PATCH 134/155] Update readme: CI jobs not just for "main" branch This changed a while ago but README.md still listed having a main branch as a condition for automatic CI linting and testing in a fork. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aac69e0df..021aab15f 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ mypy -p git The same linting, and running tests on all the different supported Python versions, will be performed: - Upon submitting a pull request. -- On each push, *if* you have a fork with a "main" branch and GitHub Actions enabled. +- On each push, *if* you have a fork with GitHub Actions enabled. - Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed). #### Configuration files From c2472e9b57afde98bb18ada55ae978721a27713d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 1 Oct 2023 00:11:37 -0400 Subject: [PATCH 135/155] Note that the init script can be run from Git Bash This is probably the *only* way anyone should run that script on Windows, but I don't know of specific bad things that happen if it is run in some other way, such as with WSL bash, aside from messing up line endings, which users are likely to notice anyway. This commit also clarifies the instructions by breaking up another paragraph that really represented two separate steps. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 021aab15f..69fb54c9f 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,17 @@ cd GitPython ./init-tests-after-clone.sh ``` +On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell. + If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`): ```bash gh repo clone GitPython ``` -Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html). Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): +Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html). + +Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): ```bash pip install -e ".[test]" From 04f3200723d67aaf5d46106c5c5602a0d5caae94 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 11:40:01 -0400 Subject: [PATCH 136/155] Ask git where its daemon is and use that This changes the test helpers on Windows to use "git --exec-path" (with whatever "git" GitPython is using) to find the directory that contains "git-daemon.exe", instead of finding it in a PATH search. --- README.md | 3 --- test/lib/helper.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 69fb54c9f..b9e61912a 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,6 @@ _Important_: Right after cloning this repository, please be sure to have execute the `./init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures. -On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` -exists in `Git\mingw64\libexec\git-core\`. - #### Install test dependencies Ensure testing libraries are installed. This is taken care of already if you installed with: diff --git a/test/lib/helper.py b/test/lib/helper.py index 1de904610..d415ba2e7 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -182,7 +182,7 @@ def git_daemon_launched(base_path, ip, port): # and then CANNOT DIE! # So, invoke it as a single command. daemon_cmd = [ - "git-daemon", + osp.join(Git()._call_process("--exec-path"), "git-daemon"), "--enable=receive-pack", "--listen=%s" % ip, "--port=%s" % port, From 28142630f6c3bfd83dcd654c102235c5c00075e5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 01:47:38 -0400 Subject: [PATCH 137/155] Add a missing PermissionError xfail on Windows One of the tests that was commented as being skipped as a result of SkipTest rasied in git.util.rmtree or one of the functions that calls it, test_git_submodule_compatibility, was not skipped in that way and was actually failing on Windows with PermissionError. It appears that the cause of failure changed over time, so that it once involved rmtree but no longer does. This removes the outdated comment and adds an xfail mark instead, specific to PermissionError and with a message identifying where in the test case's logic the PermissionError is currently triggered. --- test/test_submodule.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index 79ff2c5f2..0ebd8c51d 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -819,9 +819,11 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): assert commit_sm.binsha == sm_too.binsha assert sm_too.binsha != sm.binsha - # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`. - # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: " - # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501 + @pytest.mark.xfail( + HIDE_WINDOWS_KNOWN_ERRORS, + reason='"The process cannot access the file because it is being used by another process" on call to sm.move', + raises=PermissionError, + ) @with_rw_directory def test_git_submodule_compatibility(self, rwdir): parent = git.Repo.init(osp.join(rwdir, "parent")) From fba59aa32119e22b1b300fea8959c0abd3c9f863 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 04:39:56 -0400 Subject: [PATCH 138/155] Update "ACTUALLY skipped by" comments The remaining "ACTUALLY skipped by" comments in the test suite were for tests that are actually skipped by SkipTest exceptions raised from the code under test. But the information provided about where in the code they were skipped was out of date, and also not detailed enough because references to line numbers become stale when code is added or removed in the referenced module before the referenced code. This updates them and also provides more detailed information about the referenced code doing the skipping. The error messages are the same as before, and the paths are the same in relevant details, so this doesn't modify those parts of the comments. --- test/test_docs.py | 5 ++++- test/test_submodule.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_docs.py b/test/test_docs.py index 79e1f1be4..b38ac31b0 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -21,7 +21,10 @@ def tearDown(self): gc.collect() - # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`. + # ACTUALLY skipped by git.objects.submodule.base.Submodule.remove, at the last + # rmtree call (in "handle separate bare repository"), line 1082. + # + # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: " # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501 @with_rw_directory diff --git a/test/test_submodule.py b/test/test_submodule.py index 0ebd8c51d..1c105c816 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -457,7 +457,10 @@ def _do_base_tests(self, rwrepo): True, ) - # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`. + # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via + # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1018. + # + # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because" # "it is being used by another process: " # "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'") # noqa E501 From 5039df3560d321af1746bbecbeb1b2838daf7f91 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 06:31:27 -0400 Subject: [PATCH 139/155] Eliminate duplicate rmtree try-except logic In git.util.rmtree, exceptions are caught and conditionally (depending on the value of HIDE_WINDOWS_KNOWN_ERRORS) reraised wrapped in a unittest.SkipTest exception. Although this logic is part of git.util.rmtree itself, two of the calls to that rmtree function contain this same logic. This is not quite a refactoring: because SkipTest derives from Exception, and Exception rather than PermissionError is being caught including in the duplicated logic, duplicated logic where git.util.rmtree was called added another layer of indirection in the chained exceptions leading back to the original that was raised in an unsuccessful attempt to delete a file or directory in rmtree. However, that appeared unintended and may be considered a minor bug. The new behavior, differing only subtly, is preferable. --- git/objects/submodule/base.py | 20 ++------------------ test/test_docs.py | 4 ++-- test/test_submodule.py | 2 +- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index c7e7856f0..6fe946084 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -29,7 +29,6 @@ unbare_repo, IterableList, ) -from git.util import HIDE_WINDOWS_KNOWN_ERRORS import os.path as osp @@ -1060,28 +1059,13 @@ def remove( import gc gc.collect() - try: - rmtree(str(wtd)) - except Exception as ex: - if HIDE_WINDOWS_KNOWN_ERRORS: - from unittest import SkipTest - - raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex - raise + rmtree(str(wtd)) # END delete tree if possible # END handle force if not dry_run and osp.isdir(git_dir): self._clear_cache() - try: - rmtree(git_dir) - except Exception as ex: - if HIDE_WINDOWS_KNOWN_ERRORS: - from unittest import SkipTest - - raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex - else: - raise + rmtree(git_dir) # end handle separate bare repository # END handle module deletion diff --git a/test/test_docs.py b/test/test_docs.py index b38ac31b0..f17538aeb 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -21,8 +21,8 @@ def tearDown(self): gc.collect() - # ACTUALLY skipped by git.objects.submodule.base.Submodule.remove, at the last - # rmtree call (in "handle separate bare repository"), line 1082. + # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via + # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1068. # # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: " diff --git a/test/test_submodule.py b/test/test_submodule.py index 1c105c816..318a5afde 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -458,7 +458,7 @@ def _do_base_tests(self, rwrepo): ) # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via - # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1018. + # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1017. # # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because" From 683a3eeba838bb786bb1f334c963deb8e13eed0f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 13:18:09 -0400 Subject: [PATCH 140/155] Clean up git.objects.submodule.base imports This reorders them lexicographically within each group, makes spacing/formatting more consistent, and removes the old comment about needing a dict to set .name, which had originally been on what later became the BytesIO import but had become separate from it. (In Python 2, there was a cStringIO type, which could provide a speed advantage over StringIO, but its instances, not having instance dictionaries, didn't support the dynamic creation of new attributes. This was changed to StringIO in 00ce31a to allow .name to be added. It was changed to BytesIO in bc8c912 to work with bytes on both Python 2 and Python 3. The comment about needing a dict later ended up on the preceding line in 0210e39, at which point its meaning was unclear. Because Python 2 is no longer supported and Python 3 has no cStringIO type, the comment is no longer needed, and this commit removes it.) --- git/objects/submodule/base.py | 24 +++++++++--------------- test/test_docs.py | 2 +- test/test_submodule.py | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 6fe946084..13d897263 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1,43 +1,37 @@ -# need a dict to set bloody .name field from io import BytesIO import logging import os +import os.path as osp import stat import uuid import git from git.cmd import Git -from git.compat import ( - defenc, - is_win, -) -from git.config import SectionConstraint, GitConfigParser, cp +from git.compat import defenc, is_win +from git.config import GitConfigParser, SectionConstraint, cp from git.exc import ( + BadName, InvalidGitRepositoryError, NoSuchPathError, RepositoryDirtyError, - BadName, ) from git.objects.base import IndexObject, Object from git.objects.util import TraversableIterableObj - from git.util import ( - join_path_native, - to_native_path_linux, + IterableList, RemoteProgress, + join_path_native, rmtree, + to_native_path_linux, unbare_repo, - IterableList, ) -import os.path as osp - from .util import ( + SubmoduleConfigParser, + find_first_remote_branch, mkhead, sm_name, sm_section, - SubmoduleConfigParser, - find_first_remote_branch, ) diff --git a/test/test_docs.py b/test/test_docs.py index f17538aeb..d1ed46926 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -22,7 +22,7 @@ def tearDown(self): gc.collect() # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via - # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1068. + # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1062. # # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: " diff --git a/test/test_submodule.py b/test/test_submodule.py index 318a5afde..31a555ce2 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -458,7 +458,7 @@ def _do_base_tests(self, rwrepo): ) # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via - # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1017. + # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1011. # # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, # "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because" From 2fe7f3c4a6f9870bb332761740c883a2c2ff2487 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 22:46:43 -0400 Subject: [PATCH 141/155] Test current expected behavior of git.util.rmtree --- test/test_util.py | 70 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 2b1e518ed..552700c98 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,11 +5,13 @@ # the BSD License: https://opensource.org/license/bsd-3-clause/ import os +import pathlib import pickle +import stat import sys import tempfile import time -from unittest import mock, skipUnless +from unittest import SkipTest, mock, skipIf, skipUnless from datetime import datetime import ddt @@ -19,25 +21,26 @@ from git.compat import is_win from git.objects.util import ( altz_to_utctz_str, - utctz_to_altz, - verify_utctz, + from_timestamp, parse_date, tzoffset, - from_timestamp, + utctz_to_altz, + verify_utctz, ) from test.lib import ( TestBase, with_rw_repo, ) from git.util import ( - LockFile, - BlockingLockFile, - get_user_id, Actor, + BlockingLockFile, IterableList, + LockFile, cygpath, decygpath, + get_user_id, remove_password_if_present, + rmtree, ) @@ -85,6 +88,59 @@ def setup(self): "array": [42], } + def test_rmtree_deletes_nested_dir_with_files(self): + with tempfile.TemporaryDirectory() as parent: + td = pathlib.Path(parent, "testdir") + for d in td, td / "q", td / "s": + d.mkdir() + for f in td / "p", td / "q" / "w", td / "q" / "x", td / "r", td / "s" / "y", td / "s" / "z": + f.write_bytes(b"") + + try: + rmtree(td) + except SkipTest as ex: + self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") + + self.assertFalse(td.exists()) + + @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") + def test_rmtree_deletes_dir_with_readonly_files(self): + # Automatically works on Unix, but requires special handling on Windows. + with tempfile.TemporaryDirectory() as parent: + td = pathlib.Path(parent, "testdir") + for d in td, td / "sub": + d.mkdir() + for f in td / "x", td / "sub" / "y": + f.write_bytes(b"") + f.chmod(0) + + try: + rmtree(td) + except SkipTest as ex: + self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") + + self.assertFalse(td.exists()) + + @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") + @skipIf(sys.version_info < (3, 8), "In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") + def test_rmtree_can_wrap_exceptions(self): + with tempfile.TemporaryDirectory() as parent: + td = pathlib.Path(parent, "testdir") + td.mkdir() + (td / "x").write_bytes(b"") + (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. + td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. + + # Access the module through sys.modules so it is unambiguous which module's + # attribute we patch: the original git.util, not git.index.util even though + # git.index.util "replaces" git.util and is what "import git.util" gives us. + with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True): + # Disable common chmod functions so the callback can't fix the problem. + with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"): + # Now we can see how an intractable PermissionError is treated. + with self.assertRaises(SkipTest): + rmtree(td) + # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them). @pytest.mark.xfail( reason="Many return paths prefixed /proc/cygdrive instead.", From d42cd721112d748c35d0abd11ba8dfc71052e864 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 9 Oct 2023 01:45:57 -0400 Subject: [PATCH 142/155] Test situations git.util.rmtree shouldn't wrap One of the new test cases fails, showing the bug where git.util.rmtree wraps any exception in SkipTest when HIDE_WINDOWS_KNOWN_ERRORS is true, even though the message it uses (and its purpose) is specific to PermissionError. The other new cases pass, because wrapping exceptions in SkipTest rightly does not occur when HIDE_WINDOWS_KNOWN_ERRORS is false. --- test/test_util.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 552700c98..a852eb975 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -4,6 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ +import contextlib import os import pathlib import pickle @@ -121,16 +122,31 @@ def test_rmtree_deletes_dir_with_readonly_files(self): self.assertFalse(td.exists()) - @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") - @skipIf(sys.version_info < (3, 8), "In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") - def test_rmtree_can_wrap_exceptions(self): + @staticmethod + @contextlib.contextmanager + def _tmpdir_to_force_permission_error(): + if sys.platform == "cygwin": + raise SkipTest("Cygwin can't set the permissions that make the test meaningful.") + if sys.version_info < (3, 8): + raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") + with tempfile.TemporaryDirectory() as parent: td = pathlib.Path(parent, "testdir") td.mkdir() (td / "x").write_bytes(b"") (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. + yield td + @staticmethod + @contextlib.contextmanager + def _tmpdir_for_file_not_found(): + with tempfile.TemporaryDirectory() as parent: + yield pathlib.Path(parent, "testdir") # It is deliberately never created. + + def test_rmtree_can_wrap_exceptions(self): + """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" + with self._tmpdir_to_force_permission_error() as td: # Access the module through sys.modules so it is unambiguous which module's # attribute we patch: the original git.util, not git.index.util even though # git.index.util "replaces" git.util and is what "import git.util" gives us. @@ -141,6 +157,25 @@ def test_rmtree_can_wrap_exceptions(self): with self.assertRaises(SkipTest): rmtree(td) + @ddt.data( + (False, PermissionError, _tmpdir_to_force_permission_error), + (False, FileNotFoundError, _tmpdir_for_file_not_found), + (True, FileNotFoundError, _tmpdir_for_file_not_found), + ) + def test_rmtree_does_not_wrap_unless_called_for(self, case): + """Our rmtree doesn't wrap non-PermissionError, nor when HIDE_WINDOWS_KNOWN_ERRORS is false.""" + hide_windows_known_errors, exception_type, tmpdir_context_factory = case + + with tmpdir_context_factory() as td: + # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here. + with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors): + with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"): + with self.assertRaises(exception_type): + try: + rmtree(td) + except SkipTest as ex: + self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") + # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them). @pytest.mark.xfail( reason="Many return paths prefixed /proc/cygdrive instead.", From 2a32e25bbd1cb42878928ef57a57100a17366202 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 9 Oct 2023 02:30:22 -0400 Subject: [PATCH 143/155] Fix test bug that assumed staticmethod callability staticmethod objects are descriptors that cause the same-named attribute on a class or its instances to be callable (and the first argument, representing a class or instance, not to be passed). But the actual staticmethod objects themselves are only callable starting in Python 3.10. This reorgnizes the just-added test code so it no longer wrongly relies on being able to call such objects. --- test/test_util.py | 49 +++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index a852eb975..b108dc146 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -80,6 +80,30 @@ def __repr__(self): return "TestIterableMember(%r)" % self.name +@contextlib.contextmanager +def _tmpdir_to_force_permission_error(): + """Context manager to test permission errors in situations where we do not fix them.""" + if sys.platform == "cygwin": + raise SkipTest("Cygwin can't set the permissions that make the test meaningful.") + if sys.version_info < (3, 8): + raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") + + with tempfile.TemporaryDirectory() as parent: + td = pathlib.Path(parent, "testdir") + td.mkdir() + (td / "x").write_bytes(b"") + (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. + td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. + yield td + + +@contextlib.contextmanager +def _tmpdir_for_file_not_found(): + """Context manager to test errors deleting a directory that are not due to permissions.""" + with tempfile.TemporaryDirectory() as parent: + yield pathlib.Path(parent, "testdir") # It is deliberately never created. + + @ddt.ddt class TestUtils(TestBase): def setup(self): @@ -107,6 +131,7 @@ def test_rmtree_deletes_nested_dir_with_files(self): @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") def test_rmtree_deletes_dir_with_readonly_files(self): # Automatically works on Unix, but requires special handling on Windows. + # Not to be confused with _tmpdir_to_force_permission_error (which is used below). with tempfile.TemporaryDirectory() as parent: td = pathlib.Path(parent, "testdir") for d in td, td / "sub": @@ -122,31 +147,9 @@ def test_rmtree_deletes_dir_with_readonly_files(self): self.assertFalse(td.exists()) - @staticmethod - @contextlib.contextmanager - def _tmpdir_to_force_permission_error(): - if sys.platform == "cygwin": - raise SkipTest("Cygwin can't set the permissions that make the test meaningful.") - if sys.version_info < (3, 8): - raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") - - with tempfile.TemporaryDirectory() as parent: - td = pathlib.Path(parent, "testdir") - td.mkdir() - (td / "x").write_bytes(b"") - (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. - td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. - yield td - - @staticmethod - @contextlib.contextmanager - def _tmpdir_for_file_not_found(): - with tempfile.TemporaryDirectory() as parent: - yield pathlib.Path(parent, "testdir") # It is deliberately never created. - def test_rmtree_can_wrap_exceptions(self): """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" - with self._tmpdir_to_force_permission_error() as td: + with _tmpdir_to_force_permission_error() as td: # Access the module through sys.modules so it is unambiguous which module's # attribute we patch: the original git.util, not git.index.util even though # git.index.util "replaces" git.util and is what "import git.util" gives us. From b8e009e8d31e32a2c0e247e0a7dc41ccdd3556e7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 13:59:03 -0400 Subject: [PATCH 144/155] In rmtree, have onerror catch only PermissionError The onerror function is still called on, and tries to resolve, any exception. But now, when it re-calls the file deletion function passed as func, the only exception it catches to conditionally convert to SkipTest is PermissionError (or derived exceptions). The old behavior of catching Exception was overly broad, and inconsistent with the hard-coded prefix of "FIXME: fails with: PermissionError" used to build the SkipTest exception messages. This commit also changes the message to use an f-string (which was one of the styles in the equivalent but differently coded duplicate logic eliminated in 5039df3, and seems clearer in this case). That change is a pure refactoring, not affecting generated messages. --- git/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/util.py b/git/util.py index 48901ba0c..c0e40d7e6 100644 --- a/git/util.py +++ b/git/util.py @@ -188,11 +188,11 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None: try: func(path) # Will scream if still not possible to delete. - except Exception as ex: + except PermissionError as ex: if HIDE_WINDOWS_KNOWN_ERRORS: from unittest import SkipTest - raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex + raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex raise return shutil.rmtree(path, False, onerror) From ccbb2732efcfa265568f1a535a8b746ed07ed82a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 18:14:19 -0400 Subject: [PATCH 145/155] Fix onerror callback type hinting, improve style The onerror parameter of shutil.rmtree receives a 3-tuple like what sys.exc_info() gives, not a string. Since we are not using that parameter anyway, I've fixed it in the onerror function defined in git.util.rmtree by changing it to Any rather than hinting it narrowly. I've also renamed the parameters so the names are based on those that are documented in the shutil.rmtree documentation. The names are not part of the function's interface (this follows both from the documentation and the typeshed hints) but using those names may make it easier to understand the function. - func is renamed to function. - path remains path. - exc_info is renamed to _excinfo. This parameter is documented as excinfo, but I've prepended an underscore to signifity that it is unused. These changes are to a local function and non-breaking. Finally, although not all type checkers will flag this as an error automatically, the git.util.rmtree function, like the shutil.rmtree function it calls, is conceptually void, so it should not have any return statements with operands. Because the return statement appeared at the end, I've removed "return". --- git/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/util.py b/git/util.py index c0e40d7e6..6e1e95e49 100644 --- a/git/util.py +++ b/git/util.py @@ -182,12 +182,12 @@ def rmtree(path: PathLike) -> None: :note: we use shutil rmtree but adjust its behaviour to see whether files that couldn't be deleted are read-only. Windows will not remove them in that case""" - def onerror(func: Callable, path: PathLike, exc_info: str) -> None: + def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None: # Is the error an access error ? os.chmod(path, stat.S_IWUSR) try: - func(path) # Will scream if still not possible to delete. + function(path) # Will scream if still not possible to delete. except PermissionError as ex: if HIDE_WINDOWS_KNOWN_ERRORS: from unittest import SkipTest @@ -195,7 +195,7 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None: raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex raise - return shutil.rmtree(path, False, onerror) + shutil.rmtree(path, False, onerror) def rmfile(path: PathLike) -> None: From 0b88012471d8021fffe61beb9d058840c0235f5d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 20:16:38 -0400 Subject: [PATCH 146/155] Use onexc callback where supported The shutil.rmtree callback defined as a local function in git.util.rmtree was already capable of being used as both the old onerror parameter and the new onexc parameter--introduced in Python 3.12, which also deprecates onerror--because they differ only in the meaning of their third parameter (excinfo), which the callback defined in git.util.rmtree does not use. This modifies git.util.rmtree to pass it as onexc on 3.12 and later, while still passing it as onerror on 3.11 and earlier. Because the default value of ignore_errors is False, this makes the varying logic clearer by omitting that argument and using a keyword argument both when passing onexc (which is keyword-only) and when passing onerror (which is not keyword-only but can only be passed positionally if ignore_errors is passed explicitly). --- git/util.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/git/util.py b/git/util.py index 6e1e95e49..8a4a20253 100644 --- a/git/util.py +++ b/git/util.py @@ -5,24 +5,24 @@ # the BSD License: https://opensource.org/license/bsd-3-clause/ from abc import abstractmethod -import os.path as osp -from .compat import is_win import contextlib from functools import wraps import getpass import logging import os +import os.path as osp +import pathlib import platform -import subprocess import re import shutil import stat -from sys import maxsize +import subprocess +import sys import time from urllib.parse import urlsplit, urlunsplit import warnings -# from git.objects.util import Traversable +from .compat import is_win # typing --------------------------------------------------------- @@ -42,22 +42,17 @@ Tuple, TypeVar, Union, - cast, TYPE_CHECKING, + cast, overload, ) -import pathlib - if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo from git.config import GitConfigParser, SectionConstraint from git import Git - # from git.objects.base import IndexObject - - from .types import ( Literal, SupportsIndex, @@ -75,7 +70,6 @@ # --------------------------------------------------------------------- - from gitdb.util import ( # NOQA @IgnorePep8 make_sha, LockedFD, # @UnusedImport @@ -88,7 +82,6 @@ hex_to_bin, # @UnusedImport ) - # NOTE: Some of the unused imports might be used/imported by others. # Handle once test-cases are back up and running. # Most of these are unused here, but are for use by git-python modules so these @@ -182,7 +175,7 @@ def rmtree(path: PathLike) -> None: :note: we use shutil rmtree but adjust its behaviour to see whether files that couldn't be deleted are read-only. Windows will not remove them in that case""" - def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None: + def handler(function: Callable, path: PathLike, _excinfo: Any) -> None: # Is the error an access error ? os.chmod(path, stat.S_IWUSR) @@ -195,7 +188,10 @@ def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None: raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex raise - shutil.rmtree(path, False, onerror) + if sys.version_info >= (3, 12): + shutil.rmtree(path, onexc=handler) + else: + shutil.rmtree(path, onerror=handler) def rmfile(path: PathLike) -> None: @@ -995,7 +991,7 @@ def __init__( self, file_path: PathLike, check_interval_s: float = 0.3, - max_block_time_s: int = maxsize, + max_block_time_s: int = sys.maxsize, ) -> None: """Configure the instance From 7dd59043b44d0f2169304f90da74b1b2f7f2b02e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 8 Oct 2023 20:27:08 -0400 Subject: [PATCH 147/155] Revise and update rmtree docstrings and comments --- git/util.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/git/util.py b/git/util.py index 8a4a20253..d7f348d48 100644 --- a/git/util.py +++ b/git/util.py @@ -170,17 +170,18 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]: def rmtree(path: PathLike) -> None: - """Remove the given recursively. + """Remove the given directory tree recursively. - :note: we use shutil rmtree but adjust its behaviour to see whether files that - couldn't be deleted are read-only. Windows will not remove them in that case""" + :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that + couldn't be deleted are read-only. Windows will not remove them in that case.""" def handler(function: Callable, path: PathLike, _excinfo: Any) -> None: - # Is the error an access error ? + """Callback for :func:`shutil.rmtree`. Works either as ``onexc`` or ``onerror``.""" + # Is the error an access error? os.chmod(path, stat.S_IWUSR) try: - function(path) # Will scream if still not possible to delete. + function(path) except PermissionError as ex: if HIDE_WINDOWS_KNOWN_ERRORS: from unittest import SkipTest From 196cfbeefb5ab0844d37da0b1e010b6ee7cf9041 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 9 Oct 2023 03:27:58 -0400 Subject: [PATCH 148/155] Clean up test_util, reorganizing for readability - Slightly improve import sorting, grouping, and formatting. - Move the cygpath pairs parameters into the test class, so they can be immediately above the tests that use them. This was almost the case in the past, but stopped being the case when helpers for some new tests above those were introduced (and those helpers can't be moved inside the class without extra complexity). - Rename TestIterableMember to _Member, so it is no longer named as a test class. The unittest framework wouldn't consider it one, since it doesn't derive from unittest.TestCase, but the pytest runner, which we're actually using, does. More importanly (since it has no test methods anyway), this makes clear to humans that it is a helper class for tests, rather than a class of tests. - Improve the style of _Member, and have its __repr__ show the actual class of the instance, so if future tests ever use a derived class of it--or if its name ever changes again--the type name in the repr will be correct. - Remove the setup method (of TestUtils). It looks like this may at one time have been intended as a setUp method (note the case difference), but it is unused and there doesn't seem to be any attempt to use the instance attribute it was setting. - Use R"" instead of r"" for raw strings representing Windows paths, so that some editors (at least VS Code) refrain from highlighting their contents as regular expressions. - Other very minor reformatting and slight comment rewording. --- test/test_util.py | 101 +++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index b108dc146..7b37918d1 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,6 +5,7 @@ # the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib +from datetime import datetime import os import pathlib import pickle @@ -13,7 +14,6 @@ import tempfile import time from unittest import SkipTest, mock, skipIf, skipUnless -from datetime import datetime import ddt import pytest @@ -28,10 +28,6 @@ utctz_to_altz, verify_utctz, ) -from test.lib import ( - TestBase, - with_rw_repo, -) from git.util import ( Actor, BlockingLockFile, @@ -43,41 +39,19 @@ remove_password_if_present, rmtree, ) +from test.lib import TestBase, with_rw_repo -_norm_cygpath_pairs = ( - (r"foo\bar", "foo/bar"), - (r"foo/bar", "foo/bar"), - (r"C:\Users", "/cygdrive/c/Users"), - (r"C:\d/e", "/cygdrive/c/d/e"), - ("C:\\", "/cygdrive/c/"), - (r"\\server\C$\Users", "//server/C$/Users"), - (r"\\server\C$", "//server/C$"), - ("\\\\server\\c$\\", "//server/c$/"), - (r"\\server\BAR/", "//server/BAR/"), - (r"D:/Apps", "/cygdrive/d/Apps"), - (r"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"), - (r"D:\Apps/123", "/cygdrive/d/Apps/123"), -) - -_unc_cygpath_pairs = ( - (r"\\?\a:\com", "/cygdrive/a/com"), - (r"\\?\a:/com", "/cygdrive/a/com"), - (r"\\?\UNC\server\D$\Apps", "//server/D$/Apps"), -) - - -class TestIterableMember(object): - - """A member of an iterable list""" +class _Member: + """A member of an IterableList.""" - __slots__ = "name" + __slots__ = ("name",) def __init__(self, name): self.name = name def __repr__(self): - return "TestIterableMember(%r)" % self.name + return f"{type(self).__name__}({self.name!r})" @contextlib.contextmanager @@ -106,13 +80,6 @@ def _tmpdir_for_file_not_found(): @ddt.ddt class TestUtils(TestBase): - def setup(self): - self.testdict = { - "string": "42", - "int": 42, - "array": [42], - } - def test_rmtree_deletes_nested_dir_with_files(self): with tempfile.TemporaryDirectory() as parent: td = pathlib.Path(parent, "testdir") @@ -131,7 +98,7 @@ def test_rmtree_deletes_nested_dir_with_files(self): @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") def test_rmtree_deletes_dir_with_readonly_files(self): # Automatically works on Unix, but requires special handling on Windows. - # Not to be confused with _tmpdir_to_force_permission_error (which is used below). + # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below). with tempfile.TemporaryDirectory() as parent: td = pathlib.Path(parent, "testdir") for d in td, td / "sub": @@ -179,6 +146,27 @@ def test_rmtree_does_not_wrap_unless_called_for(self, case): except SkipTest as ex: self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") + _norm_cygpath_pairs = ( + (R"foo\bar", "foo/bar"), + (R"foo/bar", "foo/bar"), + (R"C:\Users", "/cygdrive/c/Users"), + (R"C:\d/e", "/cygdrive/c/d/e"), + ("C:\\", "/cygdrive/c/"), + (R"\\server\C$\Users", "//server/C$/Users"), + (R"\\server\C$", "//server/C$"), + ("\\\\server\\c$\\", "//server/c$/"), + (R"\\server\BAR/", "//server/BAR/"), + (R"D:/Apps", "/cygdrive/d/Apps"), + (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"), + (R"D:\Apps/123", "/cygdrive/d/Apps/123"), + ) + + _unc_cygpath_pairs = ( + (R"\\?\a:\com", "/cygdrive/a/com"), + (R"\\?\a:/com", "/cygdrive/a/com"), + (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"), + ) + # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them). @pytest.mark.xfail( reason="Many return paths prefixed /proc/cygdrive instead.", @@ -192,16 +180,16 @@ def test_cygpath_ok(self, case): self.assertEqual(cwpath, cpath, wpath) @pytest.mark.xfail( - reason=r'2nd example r".\bar" -> "bar" fails, returns "./bar"', + reason=R'2nd example r".\bar" -> "bar" fails, returns "./bar"', raises=AssertionError, ) @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( - (r"./bar", "bar"), - (r".\bar", "bar"), # FIXME: Mark only this one xfail, somehow (or fix it). - (r"../bar", "../bar"), - (r"..\bar", "../bar"), - (r"../bar/.\foo/../chu", "../bar/chu"), + (R"./bar", "bar"), + (R".\bar", "bar"), # FIXME: Mark only this one xfail, somehow (or fix it). + (R"../bar", "../bar"), + (R"..\bar", "../bar"), + (R"../bar/.\foo/../chu", "../bar/chu"), ) def test_cygpath_norm_ok(self, case): wpath, cpath = case @@ -210,12 +198,12 @@ def test_cygpath_norm_ok(self, case): @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.") @ddt.data( - r"C:", - r"C:Relative", - r"D:Apps\123", - r"D:Apps/123", - r"\\?\a:rel", - r"\\share\a:rel", + R"C:", + R"C:Relative", + R"D:Apps\123", + R"D:Apps/123", + R"\\?\a:rel", + R"\\share\a:rel", ) def test_cygpath_invalids(self, wpath): cwpath = cygpath(wpath) @@ -380,15 +368,18 @@ def test_actor_from_string(self): Actor("name last another", "some-very-long-email@example.com"), ) - @ddt.data(("name", ""), ("name", "prefix_")) + @ddt.data( + ("name", ""), + ("name", "prefix_"), + ) def test_iterable_list(self, case): name, prefix = case ilist = IterableList(name, prefix) name1 = "one" name2 = "two" - m1 = TestIterableMember(prefix + name1) - m2 = TestIterableMember(prefix + name2) + m1 = _Member(prefix + name1) + m2 = _Member(prefix + name2) ilist.extend((m1, m2)) From 100ab989fcba0b1d1bd89b5b4b41ea5014da3d82 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 15:31:25 -0400 Subject: [PATCH 149/155] Add initial test_env_vars_for_windows_tests The new test method just verifies the current behavior of the HIDE_WINDOWS_KNOWN_ERRORS and HIDE_WINDOWS_FREEZE_ERRORS environment variables. This is so there is a test to modify when changing that behavior. The purpose of these tests is *not* to claim that the behavior of either variable is something code that uses GitPython can (or has ever been able to) rely on. This also adds pytest-subtests as a dependency, so multiple failures from the subtests can be seen in the same test run. --- test-requirements.txt | 1 + test/test_util.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index 9414da09c..a69181be1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,4 +7,5 @@ pre-commit pytest pytest-cov pytest-instafail +pytest-subtests pytest-sugar diff --git a/test/test_util.py b/test/test_util.py index 7b37918d1..d65b25d49 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -4,12 +4,14 @@ # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ +import ast import contextlib from datetime import datetime import os import pathlib import pickle import stat +import subprocess import sys import tempfile import time @@ -502,3 +504,46 @@ def test_remove_password_from_command_line(self): assert cmd_4 == remove_password_if_present(cmd_4) assert cmd_5 == remove_password_if_present(cmd_5) + + @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS") + def test_env_vars_for_windows_tests(self, name): + def run_parse(value): + command = [ + sys.executable, + "-c", + f"from git.util import {name}; print(repr({name}))", + ] + output = subprocess.check_output( + command, + env=None if value is None else dict(os.environ, **{name: value}), + text=True, + ) + return ast.literal_eval(output) + + assert_true_iff_win = self.assertTrue if os.name == "nt" else self.assertFalse + + truthy_cases = [ + ("unset", None), + ("true-seeming", "1"), + ("true-seeming", "true"), + ("true-seeming", "True"), + ("true-seeming", "yes"), + ("true-seeming", "YES"), + ("false-seeming", "0"), + ("false-seeming", "false"), + ("false-seeming", "False"), + ("false-seeming", "no"), + ("false-seeming", "NO"), + ("whitespace", " "), + ] + falsy_cases = [ + ("empty", ""), + ] + + for msg, env_var_value in truthy_cases: + with self.subTest(msg, env_var_value=env_var_value): + assert_true_iff_win(run_parse(env_var_value)) + + for msg, env_var_value in falsy_cases: + with self.subTest(msg, env_var_value=env_var_value): + self.assertFalse(run_parse(env_var_value)) From 7604da185ce12b9ef540aff3255580db02c88268 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 16:06:49 -0400 Subject: [PATCH 150/155] Warn if HIDE_WINDOWS_*_ERRORS set in environment This warns if the HIDE_WINDOWS_KNOWN_ERRORS or HIDE_WINDOWS_FREEZE_ERRORS environment variables are set. These behave unexpectedly, including (and especially) in their effect on the same-named git.util module attributes, and neither their effects nor those of those attributes are documented in a way that would have supported code outside the project relying on their specific semantics. The new warning message characterizes their status as deprecated. - This is now the case for HIDE_WINDOWS_KNOWN_ERRORS, and almost so for the same-named attribute, whose existence (though not its meaning) can technically be relied on due to inclusion in `__all__` (which this does *not* change). - But the HIDE_WINDOWS_FREEZE_ERRORS attribute was never guaranteed even to exist, so technically neither it nor the same-named environment variable are not *even* deprecated. The attribute's presence has never been reflected in the public interface of any GitPython component in any way. However, these attributes are still used by the tests. Furthermore, in the case of HIDE_WINDOWS_KNOWN_ERRORS, setting it is the only way to disable the behavior of converting errors from some file deletion operations into SkipTest exceptions on Windows. Since that behavior has not yet changed, but is unlikely to be desired outside of testing, no *attributes* are deprecated at this time, and no effort to warn from accessing or using attributes is attempted. --- git/util.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/git/util.py b/git/util.py index d7f348d48..5ffe93e70 100644 --- a/git/util.py +++ b/git/util.py @@ -109,14 +109,28 @@ log = logging.getLogger(__name__) -# types############################################################ + +def _read_env_flag(name: str, default: bool) -> Union[bool, str]: + try: + value = os.environ[name] + except KeyError: + return default + + log.warning( + "The %s environment variable is deprecated. Its effect has never been documented and changes without warning.", + name, + ) + + # FIXME: This should always return bool, as that is how it is used. + # FIXME: This should treat some values besides "" as expressing falsehood. + return value #: We need an easy way to see if Appveyor TCs start failing, #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy, #: till then, we wish to hide them. -HIDE_WINDOWS_KNOWN_ERRORS = is_win and os.environ.get("HIDE_WINDOWS_KNOWN_ERRORS", True) -HIDE_WINDOWS_FREEZE_ERRORS = is_win and os.environ.get("HIDE_WINDOWS_FREEZE_ERRORS", True) +HIDE_WINDOWS_KNOWN_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_KNOWN_ERRORS", True) +HIDE_WINDOWS_FREEZE_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_FREEZE_ERRORS", True) # { Utility Methods From eb51277e73ca274e3948a3f009168d210dd587ca Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 17:01:46 -0400 Subject: [PATCH 151/155] Make HIDE_* attributes always bool For now, this doesn't change how the correspondng environment variables are interpreted, in terms of truth and falsehood. But it does *convert* them to bool, so that the values of the HIDE_WINDOWS_KNOWN_ERRORS and HIDE_WINDOWS_FREEZE_ERRORS attributes are always bools. It also updates the tests accordingly, to validate this behavior. --- git/util.py | 5 ++--- test/test_util.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/git/util.py b/git/util.py index 5ffe93e70..a6153f1f3 100644 --- a/git/util.py +++ b/git/util.py @@ -110,7 +110,7 @@ log = logging.getLogger(__name__) -def _read_env_flag(name: str, default: bool) -> Union[bool, str]: +def _read_env_flag(name: str, default: bool) -> bool: try: value = os.environ[name] except KeyError: @@ -121,9 +121,8 @@ def _read_env_flag(name: str, default: bool) -> Union[bool, str]: name, ) - # FIXME: This should always return bool, as that is how it is used. # FIXME: This should treat some values besides "" as expressing falsehood. - return value + return bool(value) #: We need an easy way to see if Appveyor TCs start failing, diff --git a/test/test_util.py b/test/test_util.py index d65b25d49..5912ea4a0 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -520,7 +520,7 @@ def run_parse(value): ) return ast.literal_eval(output) - assert_true_iff_win = self.assertTrue if os.name == "nt" else self.assertFalse + true_iff_win = os.name == "nt" # Same as is_win, but don't depend on that here. truthy_cases = [ ("unset", None), @@ -542,8 +542,8 @@ def run_parse(value): for msg, env_var_value in truthy_cases: with self.subTest(msg, env_var_value=env_var_value): - assert_true_iff_win(run_parse(env_var_value)) + self.assertIs(run_parse(env_var_value), true_iff_win) for msg, env_var_value in falsy_cases: with self.subTest(msg, env_var_value=env_var_value): - self.assertFalse(run_parse(env_var_value)) + self.assertIs(run_parse(env_var_value), False) From 333896b4447c56093fa4ae402a3d22491928ce29 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 17:29:04 -0400 Subject: [PATCH 152/155] Treat false-seeming HIDE_* env var values as false This changes how HIDE_WINDOWS_KNOWN_ERRORS and HIDE_WINDOWS_FREEZE_ERRORS environment variables, if present, are interpreted, so that values that strongly seem intuitivley to represent falsehood now do. Before, only the empty string was treated as false. Now: - "0", "false", "no", and their case variants, as well as the empty string, are treated as false. - The presence of leading and trailing whitespace in the value now longer changes the truth value it represents. For example, all-whitespace (e.g., a space) is treated as false. - Values other than the above false values, and the recognized true values "1", "true", "yes", and their variants, are still treated as true, but issue a warning about how they are unrecognied. --- git/util.py | 10 ++++++++-- test/test_util.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/git/util.py b/git/util.py index a6153f1f3..97f461a83 100644 --- a/git/util.py +++ b/git/util.py @@ -121,8 +121,14 @@ def _read_env_flag(name: str, default: bool) -> bool: name, ) - # FIXME: This should treat some values besides "" as expressing falsehood. - return bool(value) + adjusted_value = value.strip().lower() + + if adjusted_value in {"", "0", "false", "no"}: + return False + if adjusted_value in {"1", "true", "yes"}: + return True + log.warning("%s has unrecognized value %r, treating as %r.", name, value, default) + return default #: We need an easy way to see if Appveyor TCs start failing, diff --git a/test/test_util.py b/test/test_util.py index 5912ea4a0..647a02833 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -529,15 +529,15 @@ def run_parse(value): ("true-seeming", "True"), ("true-seeming", "yes"), ("true-seeming", "YES"), + ] + falsy_cases = [ + ("empty", ""), + ("whitespace", " "), ("false-seeming", "0"), ("false-seeming", "false"), ("false-seeming", "False"), ("false-seeming", "no"), ("false-seeming", "NO"), - ("whitespace", " "), - ] - falsy_cases = [ - ("empty", ""), ] for msg, env_var_value in truthy_cases: From c11b36660382c713709b36bbca1da8a1acb3a4ec Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 7 Oct 2023 17:52:27 -0400 Subject: [PATCH 153/155] Simplify HIDE_* env var test; add missing cases Now that the expected truth values are intuitive, it is no longer necessary to group them by result and include messages that acknowldge the unintuitive cases. This reorders them so that pairs (like "yes" and "no") appear together, removes the messages that are no longer necessary, and reduces test code duplication. This also adds cases to test leading/trailing whitespace in otherwise nonempty strings, so it is clearer what the test is asserting overall, and so a bug where lstrip or rstrip (or equivalent with a regex) were used instead strip would be caught. --- test/test_util.py | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 647a02833..a4c06b80e 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -520,30 +520,22 @@ def run_parse(value): ) return ast.literal_eval(output) - true_iff_win = os.name == "nt" # Same as is_win, but don't depend on that here. - - truthy_cases = [ - ("unset", None), - ("true-seeming", "1"), - ("true-seeming", "true"), - ("true-seeming", "True"), - ("true-seeming", "yes"), - ("true-seeming", "YES"), - ] - falsy_cases = [ - ("empty", ""), - ("whitespace", " "), - ("false-seeming", "0"), - ("false-seeming", "false"), - ("false-seeming", "False"), - ("false-seeming", "no"), - ("false-seeming", "NO"), - ] - - for msg, env_var_value in truthy_cases: - with self.subTest(msg, env_var_value=env_var_value): - self.assertIs(run_parse(env_var_value), true_iff_win) - - for msg, env_var_value in falsy_cases: - with self.subTest(msg, env_var_value=env_var_value): - self.assertIs(run_parse(env_var_value), False) + for env_var_value, expected_truth_value in ( + (None, os.name == "nt"), # True on Windows when the environment variable is unset. + ("", False), + (" ", False), + ("0", False), + ("1", os.name == "nt"), + ("false", False), + ("true", os.name == "nt"), + ("False", False), + ("True", os.name == "nt"), + ("no", False), + ("yes", os.name == "nt"), + ("NO", False), + ("YES", os.name == "nt"), + (" no ", False), + (" yes ", os.name == "nt"), + ): + with self.subTest(env_var_value=env_var_value): + self.assertIs(run_parse(env_var_value), expected_truth_value) From f0e15e8935580a4e4dc4ed6490c9e1b229493e19 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 9 Oct 2023 04:20:09 -0400 Subject: [PATCH 154/155] Further cleanup in test_util (on new tests) The main change here is to move the tests of how the HIDE_* environment variables are treated up near the rmtree tests, since they although the behaviors being tested are separate, they are conceptually related, and also not entirely independent in that they both involve the HIDE_WINDOWS_KNOWN_ERRORS attribute. Other changes are mostly to formatting. --- test/test_util.py | 94 +++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index a4c06b80e..b20657fb1 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -87,7 +87,14 @@ def test_rmtree_deletes_nested_dir_with_files(self): td = pathlib.Path(parent, "testdir") for d in td, td / "q", td / "s": d.mkdir() - for f in td / "p", td / "q" / "w", td / "q" / "x", td / "r", td / "s" / "y", td / "s" / "z": + for f in ( + td / "p", + td / "q" / "w", + td / "q" / "x", + td / "r", + td / "s" / "y", + td / "s" / "z", + ): f.write_bytes(b"") try: @@ -97,7 +104,10 @@ def test_rmtree_deletes_nested_dir_with_files(self): self.assertFalse(td.exists()) - @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.") + @skipIf( + sys.platform == "cygwin", + "Cygwin can't set the permissions that make the test meaningful.", + ) def test_rmtree_deletes_dir_with_readonly_files(self): # Automatically works on Unix, but requires special handling on Windows. # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below). @@ -117,7 +127,7 @@ def test_rmtree_deletes_dir_with_readonly_files(self): self.assertFalse(td.exists()) def test_rmtree_can_wrap_exceptions(self): - """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" + """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" with _tmpdir_to_force_permission_error() as td: # Access the module through sys.modules so it is unambiguous which module's # attribute we patch: the original git.util, not git.index.util even though @@ -135,12 +145,16 @@ def test_rmtree_can_wrap_exceptions(self): (True, FileNotFoundError, _tmpdir_for_file_not_found), ) def test_rmtree_does_not_wrap_unless_called_for(self, case): - """Our rmtree doesn't wrap non-PermissionError, nor when HIDE_WINDOWS_KNOWN_ERRORS is false.""" + """rmtree doesn't wrap non-PermissionError, nor if HIDE_WINDOWS_KNOWN_ERRORS is false.""" hide_windows_known_errors, exception_type, tmpdir_context_factory = case with tmpdir_context_factory() as td: # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here. - with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors): + with mock.patch.object( + sys.modules["git.util"], + "HIDE_WINDOWS_KNOWN_ERRORS", + hide_windows_known_errors, + ): with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"): with self.assertRaises(exception_type): try: @@ -148,6 +162,41 @@ def test_rmtree_does_not_wrap_unless_called_for(self, case): except SkipTest as ex: self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") + @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS") + def test_env_vars_for_windows_tests(self, name): + def run_parse(value): + command = [ + sys.executable, + "-c", + f"from git.util import {name}; print(repr({name}))", + ] + output = subprocess.check_output( + command, + env=None if value is None else dict(os.environ, **{name: value}), + text=True, + ) + return ast.literal_eval(output) + + for env_var_value, expected_truth_value in ( + (None, os.name == "nt"), # True on Windows when the environment variable is unset. + ("", False), + (" ", False), + ("0", False), + ("1", os.name == "nt"), + ("false", False), + ("true", os.name == "nt"), + ("False", False), + ("True", os.name == "nt"), + ("no", False), + ("yes", os.name == "nt"), + ("NO", False), + ("YES", os.name == "nt"), + (" no ", False), + (" yes ", os.name == "nt"), + ): + with self.subTest(env_var_value=env_var_value): + self.assertIs(run_parse(env_var_value), expected_truth_value) + _norm_cygpath_pairs = ( (R"foo\bar", "foo/bar"), (R"foo/bar", "foo/bar"), @@ -504,38 +553,3 @@ def test_remove_password_from_command_line(self): assert cmd_4 == remove_password_if_present(cmd_4) assert cmd_5 == remove_password_if_present(cmd_5) - - @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS") - def test_env_vars_for_windows_tests(self, name): - def run_parse(value): - command = [ - sys.executable, - "-c", - f"from git.util import {name}; print(repr({name}))", - ] - output = subprocess.check_output( - command, - env=None if value is None else dict(os.environ, **{name: value}), - text=True, - ) - return ast.literal_eval(output) - - for env_var_value, expected_truth_value in ( - (None, os.name == "nt"), # True on Windows when the environment variable is unset. - ("", False), - (" ", False), - ("0", False), - ("1", os.name == "nt"), - ("false", False), - ("true", os.name == "nt"), - ("False", False), - ("True", os.name == "nt"), - ("no", False), - ("yes", os.name == "nt"), - ("NO", False), - ("YES", os.name == "nt"), - (" no ", False), - (" yes ", os.name == "nt"), - ): - with self.subTest(env_var_value=env_var_value): - self.assertIs(run_parse(env_var_value), expected_truth_value) From a9b05ece674578d3417f8969ade17b06ab287ffe Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 9 Oct 2023 07:59:32 -0400 Subject: [PATCH 155/155] Clarify a test helper docstring The wording was ambiguous before, because fixing a PermissionError could mean addressing it successfully at runtime (which is the intended meaning in that docstring) but it could also mean fixing a bug or test case related to such an error (not intended). --- test/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index b20657fb1..f75231c98 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -58,7 +58,7 @@ def __repr__(self): @contextlib.contextmanager def _tmpdir_to_force_permission_error(): - """Context manager to test permission errors in situations where we do not fix them.""" + """Context manager to test permission errors in situations where they are not overcome.""" if sys.platform == "cygwin": raise SkipTest("Cygwin can't set the permissions that make the test meaningful.") if sys.version_info < (3, 8):