├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── alpine-test.yml │ ├── codeql.yml │ ├── cygwin-test.yml │ ├── lint.yml │ └── pythonpackage.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── CHANGES ├── CONTRIBUTING.md ├── FUNDING.json ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── VERSION ├── build-release.sh ├── check-version.sh ├── doc ├── .gitignore ├── Makefile ├── requirements.txt └── source │ ├── changes.rst │ ├── conf.py │ ├── index.rst │ ├── intro.rst │ ├── quickstart.rst │ ├── reference.rst │ ├── roadmap.rst │ └── tutorial.rst ├── fuzzing ├── LICENSE-APACHE ├── LICENSE-BSD ├── README.md ├── fuzz-targets │ ├── fuzz_blob.py │ ├── fuzz_config.py │ ├── fuzz_diff.py │ ├── fuzz_repo.py │ ├── fuzz_submodule.py │ └── utils.py ├── local-dev-helpers │ └── Dockerfile └── oss-fuzz-scripts │ ├── build.sh │ └── container-environment-bootstrap.sh ├── git ├── __init__.py ├── cmd.py ├── compat.py ├── config.py ├── db.py ├── diff.py ├── exc.py ├── index │ ├── __init__.py │ ├── base.py │ ├── fun.py │ ├── typ.py │ └── util.py ├── objects │ ├── __init__.py │ ├── base.py │ ├── blob.py │ ├── commit.py │ ├── fun.py │ ├── submodule │ │ ├── __init__.py │ │ ├── base.py │ │ ├── root.py │ │ └── util.py │ ├── tag.py │ ├── tree.py │ └── util.py ├── py.typed ├── refs │ ├── __init__.py │ ├── head.py │ ├── log.py │ ├── reference.py │ ├── remote.py │ ├── symbolic.py │ └── tag.py ├── remote.py ├── repo │ ├── __init__.py │ ├── base.py │ └── fun.py ├── types.py └── util.py ├── init-tests-after-clone.sh ├── pyproject.toml ├── release-verification-key.asc ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── test-requirements.txt ├── test ├── __init__.py ├── deprecation │ ├── __init__.py │ ├── lib.py │ ├── test_basic.py │ ├── test_cmd_git.py │ ├── test_compat.py │ ├── test_toplevel.py │ └── test_types.py ├── fixtures │ ├── .gitconfig │ ├── blame │ ├── blame_binary │ ├── blame_complex_revision │ ├── blame_incremental │ ├── blame_incremental_2.11.1_plus │ ├── cat_file.py │ ├── cat_file_blob │ ├── cat_file_blob_nl │ ├── cat_file_blob_size │ ├── commit_invalid_data │ ├── commit_with_gpgsig │ ├── diff_2 │ ├── diff_2f │ ├── diff_abbrev-40_full-index_M_raw_no-color │ ├── diff_change_in_type │ ├── diff_change_in_type_raw │ ├── diff_copied_mode │ ├── diff_copied_mode_raw │ ├── diff_f │ ├── diff_file_with_colon │ ├── diff_file_with_spaces │ ├── diff_i │ ├── diff_index_patch │ ├── diff_index_raw │ ├── diff_initial │ ├── diff_mode_only │ ├── diff_new_mode │ ├── diff_numstat │ ├── diff_p │ ├── diff_patch_binary │ ├── diff_patch_unsafe_paths │ ├── diff_raw_binary │ ├── diff_rename │ ├── diff_rename_raw │ ├── diff_tree_numstat_root │ ├── env_case.py │ ├── for_each_ref_with_path_component │ ├── git_config │ ├── git_config-inc.cfg │ ├── git_config_global │ ├── git_config_multiple │ ├── git_config_with_comments │ ├── git_config_with_empty_value │ ├── git_config_with_quotes │ ├── git_config_with_quotes_whitespace │ ├── git_file │ ├── index │ ├── index_merge │ ├── issue-301_stderr │ ├── ls_tree_a │ ├── ls_tree_b │ ├── ls_tree_commit │ ├── ls_tree_empty │ ├── polyglot │ ├── reflog_HEAD │ ├── reflog_invalid_date │ ├── reflog_invalid_email │ ├── reflog_invalid_newsha │ ├── reflog_invalid_oldsha │ ├── reflog_invalid_sep │ ├── reflog_master │ ├── rev_list │ ├── rev_list_bisect_all │ ├── rev_list_commit_diffs │ ├── rev_list_commit_idabbrev │ ├── rev_list_commit_stats │ ├── rev_list_count │ ├── rev_list_delta_a │ ├── rev_list_delta_b │ ├── rev_list_single │ ├── rev_parse │ ├── show_empty_commit │ ├── uncommon_branch_prefix_FETCH_HEAD │ └── uncommon_branch_prefix_stderr ├── lib │ ├── __init__.py │ └── helper.py ├── performance │ ├── __init__.py │ ├── lib.py │ ├── test_commit.py │ ├── test_odb.py │ └── test_streams.py ├── test_actor.py ├── test_base.py ├── test_blob.py ├── test_blob_filter.py ├── test_clone.py ├── test_commit.py ├── test_config.py ├── test_db.py ├── test_diff.py ├── test_docs.py ├── test_exc.py ├── test_fun.py ├── test_git.py ├── test_imports.py ├── test_index.py ├── test_installation.py ├── test_quick_doc.py ├── test_reflog.py ├── test_refs.py ├── test_remote.py ├── test_repo.py ├── test_stats.py ├── test_submodule.py ├── test_tree.py ├── test_util.py └── tstrunner.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | test/fixtures/* eol=lf 2 | *.sh eol=lf 3 | /Makefile eol=lf 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: byron 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "gitsubmodule" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/alpine-test.yml: -------------------------------------------------------------------------------- 1 | name: test-alpine 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | container: 13 | image: alpine:latest 14 | 15 | defaults: 16 | run: 17 | shell: sudo -u runner sh -exo pipefail {0} 18 | 19 | steps: 20 | - name: Prepare Alpine Linux 21 | run: | 22 | apk add sudo git git-daemon python3 py3-pip py3-virtualenv 23 | echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env 24 | addgroup -g 127 docker 25 | adduser -D -u 1001 runner # TODO: Check if this still works on GHA as intended. 26 | adduser runner docker 27 | shell: sh -exo pipefail {0} # Run this as root, not the "runner" user. 28 | 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Set workspace ownership 34 | run: | 35 | chown -R runner:docker -- "$GITHUB_WORKSPACE" 36 | shell: sh -exo pipefail {0} # Run this as root, not the "runner" user. 37 | 38 | - name: Prepare this repo for tests 39 | run: | 40 | ./init-tests-after-clone.sh 41 | 42 | - name: Set git user identity and command aliases for the tests 43 | run: | 44 | git config --global user.email "travis@ci.com" 45 | git config --global user.name "Travis Runner" 46 | # If we rewrite the user's config by accident, we will mess it up 47 | # and cause subsequent tests to fail 48 | cat test/fixtures/.gitconfig >> ~/.gitconfig 49 | 50 | - name: Set up virtualenv 51 | run: | 52 | python -m venv .venv 53 | 54 | - name: Update PyPA packages 55 | run: | 56 | # Get the latest pip, wheel, and prior to Python 3.12, setuptools. 57 | . .venv/bin/activate 58 | python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel 59 | 60 | - name: Install project and test dependencies 61 | run: | 62 | . .venv/bin/activate 63 | pip install '.[test]' 64 | 65 | - name: Show version and platform information 66 | run: | 67 | . .venv/bin/activate 68 | uname -a 69 | command -v git python 70 | git version 71 | python --version 72 | python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' 73 | 74 | - name: Test with pytest 75 | run: | 76 | . .venv/bin/activate 77 | pytest --color=yes -p no:sugar --instafail -vv 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | pull_request: 17 | schedule: 18 | - cron: '27 10 * * 3' 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze (${{ matrix.language }}) 23 | # Runner size impacts CodeQL analysis time. To learn more, please see: 24 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 25 | # - https://gh.io/supported-runners-and-hardware-resources 26 | # - https://gh.io/using-larger-runners (GitHub.com only) 27 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 28 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 29 | permissions: 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | include: 36 | - language: actions 37 | build-mode: none 38 | - language: python 39 | build-mode: none 40 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 41 | # Use `c-cpp` to analyze code written in C, C++ or both 42 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 43 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 44 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 45 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 46 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 47 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | 52 | # Add any setup steps before running the `github/codeql-action/init` action. 53 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 54 | # or others). This is typically only required for manual builds. 55 | # - name: Setup runtime (example) 56 | # uses: actions/setup-example@v1 57 | 58 | # Initializes the CodeQL tools for scanning. 59 | - name: Initialize CodeQL 60 | uses: github/codeql-action/init@v3 61 | with: 62 | languages: ${{ matrix.language }} 63 | build-mode: ${{ matrix.build-mode }} 64 | # If you wish to specify custom queries, you can do so here or in a config file. 65 | # By default, queries listed here will override any specified in a config file. 66 | # Prefix the list here with "+" to use these queries and those in the config file. 67 | 68 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 69 | # queries: security-extended,security-and-quality 70 | 71 | # If the analyze step fails for one of the languages you are analyzing with 72 | # "We were unable to automatically build your code", modify the matrix above 73 | # to set the build mode to "manual" for that language. Then modify this step 74 | # to build your code. 75 | # ℹ️ Command-line programs to run using the OS shell. 76 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 77 | - if: matrix.build-mode == 'manual' 78 | shell: bash 79 | run: | 80 | echo 'If you are using a "manual" build mode for one or more of the' \ 81 | 'languages you are analyzing, replace this with the commands to build' \ 82 | 'your code, for example:' 83 | echo ' make bootstrap' 84 | echo ' make release' 85 | exit 1 86 | 87 | - name: Perform CodeQL Analysis 88 | uses: github/codeql-action/analyze@v3 89 | with: 90 | category: "/language:${{matrix.language}}" 91 | -------------------------------------------------------------------------------- /.github/workflows/cygwin-test.yml: -------------------------------------------------------------------------------- 1 | name: test-cygwin 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: windows-latest 11 | 12 | strategy: 13 | matrix: 14 | selection: [fast, perf] 15 | include: 16 | - selection: fast 17 | additional-pytest-args: --ignore=test/performance 18 | - selection: perf 19 | additional-pytest-args: test/performance 20 | 21 | fail-fast: false 22 | 23 | env: 24 | CHERE_INVOKING: "1" 25 | CYGWIN_NOWINPATH: "1" 26 | 27 | defaults: 28 | run: 29 | shell: C:\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}" 30 | 31 | steps: 32 | - name: Force LF line endings 33 | run: | 34 | git config --global core.autocrlf false # Affects the non-Cygwin git. 35 | shell: bash # Use Git Bash instead of Cygwin Bash for this step. 36 | 37 | - uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | 41 | - name: Install Cygwin 42 | uses: cygwin/cygwin-install-action@v5 43 | with: 44 | packages: python39 python39-pip python39-virtualenv git wget 45 | add-to-path: false # No need to change $PATH outside the Cygwin environment. 46 | 47 | - name: Arrange for verbose output 48 | run: | 49 | # Arrange for verbose output but without shell environment setup details. 50 | echo 'set -x' >~/.bash_profile 51 | 52 | - name: Special configuration for Cygwin git 53 | run: | 54 | git config --global --add safe.directory "$(pwd)" 55 | git config --global --add safe.directory "$(pwd)/.git" 56 | git config --global core.autocrlf false 57 | 58 | - name: Prepare this repo for tests 59 | run: | 60 | ./init-tests-after-clone.sh 61 | 62 | - name: Set git user identity and command aliases for the tests 63 | run: | 64 | git config --global user.email "travis@ci.com" 65 | git config --global user.name "Travis Runner" 66 | # If we rewrite the user's config by accident, we will mess it up 67 | # and cause subsequent tests to fail 68 | cat test/fixtures/.gitconfig >> ~/.gitconfig 69 | 70 | - name: Set up virtualenv 71 | run: | 72 | python3.9 -m venv --without-pip .venv 73 | echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV" 74 | 75 | - name: Bootstrap pip in virtualenv 76 | run: | 77 | wget -qO- https://bootstrap.pypa.io/get-pip.py | python 78 | 79 | - name: Update PyPA packages 80 | run: | 81 | # Get the latest pip, wheel, and prior to Python 3.12, setuptools. 82 | python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel 83 | 84 | - name: Install project and test dependencies 85 | run: | 86 | pip install '.[test]' 87 | 88 | - name: Show version and platform information 89 | run: | 90 | uname -a 91 | command -v git python 92 | git version 93 | python --version 94 | python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' 95 | 96 | - name: Test with pytest (${{ matrix.additional-pytest-args }}) 97 | run: | 98 | pytest --color=yes -p no:sugar --instafail -vv ${{ matrix.additional-pytest-args }} 99 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.x" 18 | 19 | - uses: pre-commit/action@v3.0.1 20 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: [push, pull_request, workflow_dispatch] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os-type: [ubuntu, macos, windows] 16 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"] 17 | exclude: 18 | - os-type: macos 19 | python-version: "3.7" # Not available for the ARM-based macOS runners. 20 | - os-type: macos 21 | python-version: "3.13t" 22 | - os-type: windows 23 | python-version: "3.13" # FIXME: Fix and enable Python 3.13 on Windows (#1955). 24 | - os-type: windows 25 | python-version: "3.13t" 26 | include: 27 | - os-ver: latest 28 | - os-type: ubuntu 29 | python-version: "3.7" 30 | os-ver: "22.04" 31 | - experimental: false 32 | 33 | fail-fast: false 34 | 35 | runs-on: ${{ matrix.os-type }}-${{ matrix.os-ver }} 36 | 37 | defaults: 38 | run: 39 | shell: bash --noprofile --norc -exo pipefail {0} 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 45 | 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | allow-prereleases: ${{ matrix.experimental }} 51 | 52 | - name: Set up WSL (Windows) 53 | if: matrix.os-type == 'windows' 54 | uses: Vampire/setup-wsl@v5.0.1 55 | with: 56 | wsl-version: 1 57 | distribution: Alpine 58 | additional-packages: bash 59 | 60 | - name: Prepare this repo for tests 61 | run: | 62 | ./init-tests-after-clone.sh 63 | 64 | - name: Set git user identity and command aliases for the tests 65 | run: | 66 | git config --global user.email "travis@ci.com" 67 | git config --global user.name "Travis Runner" 68 | # If we rewrite the user's config by accident, we will mess it up 69 | # and cause subsequent tests to fail 70 | cat test/fixtures/.gitconfig >> ~/.gitconfig 71 | 72 | - name: Update PyPA packages 73 | run: | 74 | # Get the latest pip, wheel, and prior to Python 3.12, setuptools. 75 | python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel 76 | 77 | - name: Install project and test dependencies 78 | run: | 79 | pip install '.[test]' 80 | 81 | - name: Show version and platform information 82 | run: | 83 | uname -a 84 | command -v git python 85 | git version 86 | python --version 87 | python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' 88 | 89 | # For debugging hook tests on native Windows systems that may have WSL. 90 | - name: Show bash.exe candidates (Windows) 91 | if: matrix.os-type == 'windows' 92 | run: | 93 | set +e 94 | bash.exe -c 'printenv WSL_DISTRO_NAME; uname -a' 95 | python -c 'import subprocess; subprocess.run(["bash.exe", "-c", "printenv WSL_DISTRO_NAME; uname -a"])' 96 | continue-on-error: true 97 | 98 | - name: Check types with mypy 99 | run: | 100 | mypy --python-version="${PYTHON_VERSION%t}" # Version only, with no "t" for free-threaded. 101 | env: 102 | MYPY_FORCE_COLOR: "1" 103 | TERM: "xterm-256color" # For color: https://github.com/python/mypy/issues/13817 104 | PYTHON_VERSION: ${{ matrix.python-version }} 105 | # With new versions of mypy new issues might arise. This is a problem if there is 106 | # nobody able to fix them, so we have to ignore errors until that changes. 107 | continue-on-error: true 108 | 109 | - name: Test with pytest 110 | run: | 111 | pytest --color=yes -p no:sugar --instafail -vv 112 | continue-on-error: false 113 | 114 | - name: Documentation 115 | if: matrix.python-version != '3.7' 116 | run: | 117 | pip install '.[doc]' 118 | make -C doc html 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cached Python bytecode 2 | __pycache__/ 3 | *.py[co] 4 | 5 | # Other caches 6 | .cache/ 7 | .mypy_cache/ 8 | .pytest_cache/ 9 | 10 | # Transient editor files 11 | *.swp 12 | *~ 13 | \#*# 14 | .#*# 15 | 16 | # Editor configuration 17 | nbproject 18 | *.sublime-workspace 19 | /.vscode/ 20 | .idea/ 21 | 22 | # Virtual environments 23 | .env/ 24 | env/ 25 | .venv/ 26 | venv/ 27 | 28 | # Build output 29 | /*egg-info 30 | /lib/GitPython.egg-info 31 | /build 32 | /dist 33 | /doc/_build 34 | 35 | # Tox builds/environments 36 | /.tox 37 | 38 | # Code coverage output 39 | cover/ 40 | .coverage 41 | .coverage.* 42 | 43 | # Monkeytype output 44 | monkeytype.sqlite3 45 | monkeytype.sqlite3.* 46 | 47 | # Manual command output 48 | output.txt 49 | 50 | # Finder metadata 51 | .DS_Store 52 | 53 | # Files created by OSS-Fuzz when running locally 54 | fuzz_*.pkg.spec 55 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gitdb"] 2 | url = https://github.com/gitpython-developers/gitdb.git 3 | path = git/ext/gitdb 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/codespell-project/codespell 3 | rev: v2.4.1 4 | hooks: 5 | - id: codespell 6 | additional_dependencies: [tomli] 7 | exclude: ^test/fixtures/ 8 | 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.11.12 11 | hooks: 12 | - id: ruff-check 13 | args: ["--fix"] 14 | exclude: ^git/ext/ 15 | - id: ruff-format 16 | exclude: ^git/ext/ 17 | 18 | - repo: https://github.com/shellcheck-py/shellcheck-py 19 | rev: v0.10.0.1 20 | hooks: 21 | - id: shellcheck 22 | args: [--color] 23 | exclude: ^test/fixtures/polyglot$|^git/ext/ 24 | 25 | - repo: https://github.com/pre-commit/pre-commit-hooks 26 | rev: v5.0.0 27 | hooks: 28 | - id: end-of-file-fixer 29 | exclude: ^test/fixtures/|COPYING|LICENSE 30 | - id: check-symlinks 31 | - id: check-toml 32 | - id: check-yaml 33 | - id: check-merge-conflict 34 | 35 | - repo: https://github.com/abravalheri/validate-pyproject 36 | rev: v0.24.1 37 | hooks: 38 | - id: validate-pyproject 39 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need. 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "doc/" directory with Sphinx. 18 | sphinx: 19 | configuration: doc/source/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub. 26 | formats: all 27 | 28 | # Optional but recommended, declare the Python requirements required 29 | # to build your documentation. 30 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 31 | python: 32 | install: 33 | - method: pip 34 | path: . 35 | extra_requirements: 36 | - doc 37 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | GitPython was originally written by Michael Trier. 2 | GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich. 3 | 4 | Contributors are: 5 | 6 | -Michael Trier 7 | -Alan Briolat 8 | -Florian Apolloner 9 | -David Aguilar 10 | -Jelmer Vernooij 11 | -Steve Frécinaux 12 | -Kai Lautaportti 13 | -Paul Sowden 14 | -Sebastian Thiel 15 | -Jonathan Chu 16 | -Vincent Driessen 17 | -Phil Elson 18 | -Bernard `Guyzmo` Pratz 19 | -Timothy B. Hartman 20 | -Konstantin Popov 21 | -Peter Jones 22 | -Anson Mansfield 23 | -Ken Odegard 24 | -Alexis Horgix Chotard 25 | -Piotr Babij 26 | -Mikuláš Poul 27 | -Charles Bouchard-Légaré 28 | -Yaroslav Halchenko 29 | -Tim Swast 30 | -William Luc Ritchie 31 | -David Host 32 | -A. Jesse Jiryu Davis 33 | -Steven Whitman 34 | -Stefan Stancu 35 | -César Izurieta 36 | -Arthur Milchior 37 | -Anil Khatri 38 | -JJ Graham 39 | -Ben Thayer 40 | -Dries Kennes 41 | -Pratik Anurag 42 | -Harmon 43 | -Liam Beguin 44 | -Ram Rachum 45 | -Alba Mendez 46 | -Robert Westman 47 | -Hugo van Kemenade 48 | -Hiroki Tokunaga 49 | -Julien Mauroy 50 | -Patrick Gerard 51 | -Luke Twist 52 | -Joseph Hale 53 | -Santos Gallegos 54 | -Wenhan Zhu 55 | -Eliah Kagan 56 | -Ethan Lin 57 | -Jonas Scharpf 58 | -Gordon Marx 59 | 60 | Portions derived from other open source works and are clearly marked. 61 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Please see the online documentation for the latest changelog: 2 | https://github.com/gitpython-developers/GitPython/blob/main/doc/source/changes.rst 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | The following is a short step-by-step rundown of what one typically would do to contribute. 4 | 5 | - [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. 6 | - For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`. 7 | - Please try to **write a test that fails unless the contribution is present.** 8 | - Try to avoid massive commits and prefer to take small steps, with one commit for each. 9 | - Feel free to add yourself to AUTHORS file. 10 | - Create a pull request. 11 | 12 | ## Fuzzing Test Specific Documentation 13 | 14 | For details related to contributing to the fuzzing test suite and OSS-Fuzz integration, please 15 | refer to the dedicated [fuzzing README](./fuzzing/README.md). 16 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0xD0d4dCFc194ec24bCc777e635289e0b10E1a7b87" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2008, 2009 Michael Trier and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the GitPython project nor the names of 16 | its contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 25 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGES 3 | include CONTRIBUTING.md 4 | include LICENSE 5 | include README.md 6 | include VERSION 7 | include requirements.txt 8 | include test-requirements.txt 9 | include git/py.typed 10 | 11 | recursive-include doc * 12 | recursive-exclude test * 13 | 14 | global-exclude __pycache__ *.pyc 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean release force_release 2 | 3 | all: 4 | @awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile 5 | 6 | clean: 7 | rm -rf build/ dist/ .eggs/ .tox/ 8 | 9 | release: clean 10 | ./check-version.sh 11 | make force_release 12 | 13 | force_release: clean 14 | ./build-release.sh 15 | twine upload dist/* 16 | git push --tags origin main 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version of GitPython can receive security updates. If a vulnerability is discovered, a fix can be issued in a new release. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.x.x | :white_check_mark: | 10 | | < 3.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report private portions of a vulnerability to . Doing so helps to receive updates and collaborate on the matter, without disclosing it publicly right away. 15 | 16 | Vulnerabilities in GitPython's dependencies [gitdb](https://github.com/gitpython-developers/gitdb/blob/master/SECURITY.md) or [smmap](https://github.com/gitpython-developers/smmap/blob/master/SECURITY.md), which primarily exist to support GitPython, can be reported here as well, at that same link. The affected package (`GitPython`, `gitdb`, or `smmap`) can be included in the report, if known. 17 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 3.1.44 2 | -------------------------------------------------------------------------------- /build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This file is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | # 6 | # This script builds a release. If run in a venv, it auto-installs its tools. 7 | # You may want to run "make release" instead of running this script directly. 8 | 9 | set -eEu 10 | 11 | function release_with() { 12 | "$1" -m build --sdist --wheel 13 | } 14 | 15 | function suggest_venv() { 16 | local venv_cmd='python -m venv env && source env/bin/activate' 17 | printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" 18 | } 19 | 20 | if test -n "${VIRTUAL_ENV-}"; then 21 | deps=(build twine) # Install twine along with build, as we need it later. 22 | echo "Virtual environment detected. Adding packages: ${deps[*]}" 23 | pip install --quiet --upgrade "${deps[@]}" 24 | echo 'Starting the build.' 25 | release_with python 26 | else 27 | trap suggest_venv ERR # This keeps the original exit (error) code. 28 | echo 'Starting the build.' 29 | release_with python3 # Outside a venv, use python3. 30 | fi 31 | -------------------------------------------------------------------------------- /check-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This file is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | # 6 | # This script checks if we are in a consistent state to build a new release. 7 | # See the release instructions in README.md for the steps to make this pass. 8 | # You may want to run "make release" instead of running this script directly. 9 | 10 | set -eEfuo pipefail 11 | trap 'echo "$0: Check failed. Stopping." >&2' ERR 12 | 13 | readonly version_path='VERSION' 14 | readonly changes_path='doc/source/changes.rst' 15 | 16 | function check_status() { 17 | git status -s "$@" 18 | test -z "$(git status -s "$@")" 19 | } 20 | 21 | function get_latest_tag() { 22 | local config_opts 23 | printf -v config_opts ' -c versionsort.suffix=-%s' alpha beta pre rc RC 24 | # shellcheck disable=SC2086 # Deliberately word-splitting the arguments. 25 | git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1 26 | } 27 | 28 | echo 'Checking current directory.' 29 | test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. 30 | 31 | echo "Checking that $version_path and $changes_path exist and have no uncommitted changes." 32 | test -f "$version_path" 33 | test -f "$changes_path" 34 | check_status -- "$version_path" "$changes_path" 35 | 36 | # This section can be commented out, if absolutely necessary. 37 | echo 'Checking that ALL changes are committed.' 38 | check_status --ignore-submodules 39 | 40 | version_version="$(<"$version_path")" 41 | changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" 42 | latest_tag="$(get_latest_tag)" 43 | head_sha="$(git rev-parse HEAD)" 44 | latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")" 45 | 46 | # Display a table of all the current version, tag, and HEAD commit information. 47 | echo 48 | echo 'The VERSION must be the same in all locations, and so must the HEAD and tag SHA' 49 | printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 50 | 'changes.rst' "$changes_version" \ 51 | 'Latest tag' "$latest_tag" \ 52 | 'HEAD SHA' "$head_sha" \ 53 | 'Latest tag SHA' "$latest_tag_sha" 54 | 55 | # Check that the latest tag and current version match the HEAD we're releasing. 56 | test "$version_version" = "$changes_version" 57 | test "$latest_tag" = "$version_version" 58 | test "$head_sha" = "$latest_tag_sha" 59 | echo 'OK, everything looks good.' 60 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | BUILDDIR = build 6 | SPHINXOPTS = -W 7 | SPHINXBUILD = sphinx-build 8 | PAPER = 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 24 | @echo " changes to make an overview over all changed/added/deprecated items" 25 | @echo " linkcheck to check all external links for integrity" 26 | 27 | clean: 28 | -rm -rf $(BUILDDIR)/* 29 | 30 | html: 31 | mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees 32 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 33 | @echo 34 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 35 | 36 | pickle: 37 | mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees 38 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 39 | @echo 40 | @echo "Build finished; now you can process the pickle files." 41 | 42 | web: pickle 43 | 44 | json: 45 | mkdir -p $(BUILDDIR)/json $(BUILDDIR)/doctrees 46 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 47 | @echo 48 | @echo "Build finished; now you can process the JSON files." 49 | 50 | htmlhelp: 51 | mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees 52 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 53 | @echo 54 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 55 | ".hhp project file in $(BUILDDIR)/htmlhelp." 56 | 57 | latex: 58 | mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees 59 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 60 | @echo 61 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 62 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 63 | "run these through (pdf)latex." 64 | 65 | changes: 66 | mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees 67 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 68 | @echo 69 | @echo "The overview file is in $(BUILDDIR)/changes." 70 | 71 | linkcheck: 72 | mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees 73 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 74 | @echo 75 | @echo "Link check complete; look for any errors in the above output " \ 76 | "or in $(BUILDDIR)/linkcheck/output.txt." 77 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx >= 7.1.2, < 7.2 2 | sphinx_rtd_theme 3 | sphinx-autodoc-typehints 4 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. GitPython documentation master file, created by sphinx-quickstart on Sat Jan 24 11:51:01 2009. 2 | You can adapt this file completely to your liking, but it should at least 3 | contain the root `toctree` directive. 4 | 5 | GitPython Documentation 6 | ======================= 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | intro 12 | quickstart 13 | tutorial 14 | reference 15 | roadmap 16 | changes 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /doc/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. _intro_toplevel: 2 | 3 | ================== 4 | Overview / Install 5 | ================== 6 | 7 | GitPython is a python library used to interact with git repositories, high-level like git-porcelain, or low-level like git-plumbing. 8 | 9 | It provides abstractions of git objects for easy access of repository data, and additionally allows you to access the git repository more directly using either a pure python implementation, or the faster, but more resource intensive git command implementation. 10 | 11 | The object database implementation is optimized for handling large quantities of objects and large datasets, which is achieved by using low-level structures and data streaming. 12 | 13 | Requirements 14 | ============ 15 | 16 | * `Python`_ >= 3.7 17 | * `Git`_ 1.7.0 or newer 18 | It should also work with older versions, but it may be that some operations 19 | involving remotes will not work as expected. 20 | * `GitDB`_ - a pure python git database implementation 21 | * `typing_extensions`_ >= 3.7.3.4 (if python < 3.10) 22 | 23 | .. _Python: https://www.python.org 24 | .. _Git: https://git-scm.com/ 25 | .. _GitDB: https://pypi.python.org/pypi/gitdb 26 | .. _typing_extensions: https://pypi.org/project/typing-extensions/ 27 | 28 | Installing GitPython 29 | ==================== 30 | 31 | Installing GitPython is easily done using 32 | `pip`_. Assuming it is 33 | installed, just run the following from the command-line: 34 | 35 | .. sourcecode:: none 36 | 37 | # pip install GitPython 38 | 39 | This command will download the latest version of GitPython from the 40 | `Python Package Index `_ and install it 41 | to your system. More information about ``pip`` and pypi can be found 42 | here: 43 | 44 | * `install pip `_ 45 | * `pypi `_ 46 | 47 | .. _pip: https://pip.pypa.io/en/latest/installing.html 48 | 49 | Alternatively, you can install from the distribution using the ``setup.py`` 50 | script: 51 | 52 | .. sourcecode:: none 53 | 54 | # python setup.py install 55 | 56 | .. note:: In this case, you have to manually install `GitDB`_ as well. It would be recommended to use the :ref:`git source repository ` in that case. 57 | 58 | Limitations 59 | =========== 60 | 61 | Leakage of System Resources 62 | --------------------------- 63 | 64 | GitPython is not suited for long-running processes (like daemons) as it tends to 65 | leak system resources. It was written in a time where destructors (as implemented 66 | in the `__del__` method) still ran deterministically. 67 | 68 | In case you still want to use it in such a context, you will want to search the 69 | codebase for `__del__` implementations and call these yourself when you see fit. 70 | 71 | Another way assure proper cleanup of resources is to factor out GitPython into a 72 | separate process which can be dropped periodically. 73 | 74 | Getting Started 75 | =============== 76 | 77 | * :ref:`tutorial-label` - This tutorial provides a walk-through of some of 78 | the basic functionality and concepts used in GitPython. It, however, is not 79 | exhaustive so you are encouraged to spend some time in the 80 | :ref:`api_reference_toplevel`. 81 | 82 | API Reference 83 | ============= 84 | 85 | An organized section of the GitPython API is at :ref:`api_reference_toplevel`. 86 | 87 | .. _source-code-label: 88 | 89 | Source Code 90 | =========== 91 | 92 | GitPython's git repo is available on GitHub, which can be browsed at: 93 | 94 | * https://github.com/gitpython-developers/GitPython 95 | 96 | and cloned using:: 97 | 98 | $ git clone https://github.com/gitpython-developers/GitPython git-python 99 | 100 | Initialize all submodules to obtain the required dependencies with:: 101 | 102 | $ cd git-python 103 | $ git submodule update --init --recursive 104 | 105 | Finally verify the installation by running unit tests:: 106 | 107 | $ python -m unittest 108 | 109 | Questions and Answers 110 | ===================== 111 | Please use stackoverflow for questions, and don't forget to tag it with `gitpython` to assure the right people see the question in a timely manner. 112 | 113 | http://stackoverflow.com/questions/tagged/gitpython 114 | 115 | Issue Tracker 116 | ============= 117 | The issue tracker is hosted by GitHub: 118 | 119 | https://github.com/gitpython-developers/GitPython/issues 120 | 121 | License Information 122 | =================== 123 | GitPython is licensed under the New BSD License. See the LICENSE file for 124 | more information. 125 | -------------------------------------------------------------------------------- /doc/source/reference.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference_toplevel: 2 | 3 | API Reference 4 | ============= 5 | 6 | Top-Level 7 | --------- 8 | 9 | .. py:data:: git.__version__ 10 | 11 | Current GitPython version. 12 | 13 | .. automodule:: git 14 | :members: refresh 15 | 16 | Objects.Base 17 | ------------ 18 | 19 | .. automodule:: git.objects.base 20 | :members: 21 | :undoc-members: 22 | :special-members: 23 | 24 | Objects.Blob 25 | ------------ 26 | 27 | .. automodule:: git.objects.blob 28 | :members: 29 | :undoc-members: 30 | :special-members: 31 | 32 | Objects.Commit 33 | -------------- 34 | 35 | .. automodule:: git.objects.commit 36 | :members: 37 | :undoc-members: 38 | :special-members: 39 | 40 | Objects.Tag 41 | ----------- 42 | 43 | .. automodule:: git.objects.tag 44 | :members: 45 | :undoc-members: 46 | :special-members: 47 | 48 | Objects.Tree 49 | ------------ 50 | 51 | .. automodule:: git.objects.tree 52 | :members: 53 | :undoc-members: 54 | :special-members: 55 | 56 | Objects.Functions 57 | ----------------- 58 | 59 | .. automodule:: git.objects.fun 60 | :members: 61 | :undoc-members: 62 | :special-members: 63 | 64 | Objects.Submodule.base 65 | ---------------------- 66 | 67 | .. automodule:: git.objects.submodule.base 68 | :members: 69 | :undoc-members: 70 | :special-members: 71 | 72 | Objects.Submodule.root 73 | ---------------------- 74 | 75 | .. automodule:: git.objects.submodule.root 76 | :members: 77 | :undoc-members: 78 | :special-members: 79 | 80 | Objects.Submodule.util 81 | ---------------------- 82 | 83 | .. automodule:: git.objects.submodule.util 84 | :members: 85 | :undoc-members: 86 | :special-members: 87 | 88 | Objects.Util 89 | ------------- 90 | 91 | .. automodule:: git.objects.util 92 | :members: 93 | :undoc-members: 94 | :special-members: 95 | 96 | Index.Base 97 | ---------- 98 | 99 | .. automodule:: git.index.base 100 | :members: 101 | :undoc-members: 102 | :special-members: 103 | 104 | Index.Functions 105 | --------------- 106 | 107 | .. automodule:: git.index.fun 108 | :members: 109 | :undoc-members: 110 | :special-members: 111 | 112 | Index.Types 113 | ----------- 114 | 115 | .. automodule:: git.index.typ 116 | :members: 117 | :undoc-members: 118 | :special-members: 119 | 120 | Index.Util 121 | ------------- 122 | 123 | .. automodule:: git.index.util 124 | :members: 125 | :undoc-members: 126 | :special-members: 127 | 128 | GitCmd 129 | ------ 130 | 131 | .. automodule:: git.cmd 132 | :members: 133 | :undoc-members: 134 | :special-members: 135 | 136 | Config 137 | ------ 138 | 139 | .. automodule:: git.config 140 | :members: 141 | :undoc-members: 142 | :special-members: 143 | 144 | Diff 145 | ---- 146 | 147 | .. automodule:: git.diff 148 | :members: 149 | :undoc-members: 150 | :special-members: 151 | 152 | Exceptions 153 | ---------- 154 | 155 | .. automodule:: git.exc 156 | :members: 157 | :undoc-members: 158 | :special-members: 159 | 160 | 161 | Refs.symbolic 162 | ------------- 163 | 164 | .. automodule:: git.refs.symbolic 165 | :members: 166 | :undoc-members: 167 | :special-members: 168 | 169 | Refs.reference 170 | -------------- 171 | 172 | .. automodule:: git.refs.reference 173 | :members: 174 | :undoc-members: 175 | :special-members: 176 | 177 | Refs.head 178 | --------- 179 | 180 | .. automodule:: git.refs.head 181 | :members: 182 | :undoc-members: 183 | :special-members: 184 | 185 | Refs.tag 186 | ------------ 187 | 188 | .. automodule:: git.refs.tag 189 | :members: 190 | :undoc-members: 191 | :special-members: 192 | 193 | Refs.remote 194 | ------------ 195 | 196 | .. automodule:: git.refs.remote 197 | :members: 198 | :undoc-members: 199 | :special-members: 200 | 201 | Refs.log 202 | ------------ 203 | 204 | .. automodule:: git.refs.log 205 | :members: 206 | :undoc-members: 207 | :special-members: 208 | 209 | Remote 210 | ------ 211 | 212 | .. automodule:: git.remote 213 | :members: 214 | :undoc-members: 215 | :special-members: 216 | 217 | Repo.Base 218 | --------- 219 | 220 | .. automodule:: git.repo.base 221 | :members: 222 | :undoc-members: 223 | :special-members: 224 | 225 | Repo.Functions 226 | -------------- 227 | 228 | .. automodule:: git.repo.fun 229 | :members: 230 | :undoc-members: 231 | :special-members: 232 | 233 | Compat 234 | ------ 235 | 236 | .. automodule:: git.compat 237 | :members: 238 | :undoc-members: 239 | :special-members: 240 | 241 | DB 242 | -- 243 | 244 | .. automodule:: git.db 245 | :members: 246 | :undoc-members: 247 | :special-members: 248 | 249 | Types 250 | ----- 251 | 252 | .. automodule:: git.types 253 | :members: 254 | :undoc-members: 255 | :special-members: 256 | 257 | Util 258 | ---- 259 | 260 | .. automodule:: git.util 261 | :members: 262 | :undoc-members: 263 | :special-members: 264 | -------------------------------------------------------------------------------- /doc/source/roadmap.rst: -------------------------------------------------------------------------------- 1 | 2 | ####### 3 | Roadmap 4 | ####### 5 | The full list of milestones including associated tasks can be found on GitHub: 6 | https://github.com/gitpython-developers/GitPython/issues 7 | 8 | Select the respective milestone to filter the list of issues accordingly. 9 | -------------------------------------------------------------------------------- /fuzzing/LICENSE-BSD: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/fuzz_blob.py: -------------------------------------------------------------------------------- 1 | import atheris 2 | import sys 3 | import os 4 | import tempfile 5 | 6 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 7 | path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) 8 | os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary 9 | 10 | with atheris.instrument_imports(): 11 | import git 12 | 13 | 14 | def TestOneInput(data): 15 | fdp = atheris.FuzzedDataProvider(data) 16 | 17 | with tempfile.TemporaryDirectory() as temp_dir: 18 | repo = git.Repo.init(path=temp_dir) 19 | binsha = fdp.ConsumeBytes(20) 20 | mode = fdp.ConsumeInt(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())) 21 | path = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes()) 22 | 23 | try: 24 | blob = git.Blob(repo, binsha, mode, path) 25 | except AssertionError as e: 26 | if "Require 20 byte binary sha, got" in str(e): 27 | return -1 28 | else: 29 | raise e 30 | 31 | _ = blob.mime_type 32 | 33 | 34 | def main(): 35 | atheris.Setup(sys.argv, TestOneInput) 36 | atheris.Fuzz() 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/fuzz_config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | ############################################################################### 16 | # Note: This file has been modified by contributors to GitPython. 17 | # The original state of this file may be referenced here: 18 | # https://github.com/google/oss-fuzz/commit/f26f254558fc48f3c9bc130b10507386b94522da 19 | ############################################################################### 20 | import atheris 21 | import sys 22 | import io 23 | import os 24 | from configparser import MissingSectionHeaderError, ParsingError 25 | 26 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 27 | path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) 28 | os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary 29 | 30 | with atheris.instrument_imports(): 31 | import git 32 | 33 | 34 | def TestOneInput(data): 35 | sio = io.BytesIO(data) 36 | sio.name = "/tmp/fuzzconfig.config" 37 | git_config = git.GitConfigParser(sio) 38 | try: 39 | git_config.read() 40 | except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError): 41 | return -1 # Reject inputs raising expected exceptions 42 | except ValueError as e: 43 | if "embedded null byte" in str(e): 44 | # The `os.path.expanduser` function, which does not accept strings 45 | # containing null bytes might raise this. 46 | return -1 47 | else: 48 | raise e # Raise unanticipated exceptions as they might be bugs 49 | 50 | 51 | def main(): 52 | atheris.Setup(sys.argv, TestOneInput) 53 | atheris.Fuzz() 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/fuzz_diff.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import io 4 | import tempfile 5 | from binascii import Error as BinasciiError 6 | 7 | import atheris 8 | 9 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 10 | path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) 11 | os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary 12 | 13 | with atheris.instrument_imports(): 14 | from git import Repo, Diff 15 | 16 | 17 | class BytesProcessAdapter: 18 | """Allows bytes to be used as process objects returned by subprocess.Popen.""" 19 | 20 | @atheris.instrument_func 21 | def __init__(self, input_string): 22 | self.stdout = io.BytesIO(input_string) 23 | self.stderr = io.BytesIO() 24 | 25 | @atheris.instrument_func 26 | def wait(self): 27 | return 0 28 | 29 | poll = wait 30 | 31 | 32 | @atheris.instrument_func 33 | def TestOneInput(data): 34 | fdp = atheris.FuzzedDataProvider(data) 35 | 36 | with tempfile.TemporaryDirectory() as temp_dir: 37 | repo = Repo.init(path=temp_dir) 38 | try: 39 | diff = Diff( 40 | repo, 41 | a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 42 | b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 43 | a_blob_id=fdp.ConsumeBytes(20), 44 | b_blob_id=fdp.ConsumeBytes(20), 45 | a_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 46 | b_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 47 | new_file=fdp.ConsumeBool(), 48 | deleted_file=fdp.ConsumeBool(), 49 | copied_file=fdp.ConsumeBool(), 50 | raw_rename_from=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 51 | raw_rename_to=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 52 | diff=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), 53 | change_type=fdp.PickValueInList(["A", "D", "C", "M", "R", "T", "U"]), 54 | score=fdp.ConsumeIntInRange(0, fdp.remaining_bytes()), 55 | ) 56 | except BinasciiError: 57 | return -1 58 | except AssertionError as e: 59 | if "Require 20 byte binary sha, got" in str(e): 60 | return -1 61 | else: 62 | raise e 63 | 64 | _ = diff.__str__() 65 | _ = diff.a_path 66 | _ = diff.b_path 67 | _ = diff.rename_from 68 | _ = diff.rename_to 69 | _ = diff.renamed_file 70 | 71 | diff_index = diff._index_from_patch_format( 72 | repo, proc=BytesProcessAdapter(fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes()))) 73 | ) 74 | 75 | diff._handle_diff_line( 76 | lines_bytes=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), repo=repo, index=diff_index 77 | ) 78 | 79 | 80 | def main(): 81 | atheris.Setup(sys.argv, TestOneInput) 82 | atheris.Fuzz() 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/fuzz_repo.py: -------------------------------------------------------------------------------- 1 | import atheris 2 | import io 3 | import sys 4 | import os 5 | import tempfile 6 | 7 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 8 | path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git")) 9 | os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary 10 | 11 | with atheris.instrument_imports(): 12 | import git 13 | 14 | 15 | def TestOneInput(data): 16 | fdp = atheris.FuzzedDataProvider(data) 17 | 18 | with tempfile.TemporaryDirectory() as temp_dir: 19 | repo = git.Repo.init(path=temp_dir) 20 | 21 | # Generate a minimal set of files based on fuzz data to minimize I/O operations. 22 | file_paths = [os.path.join(temp_dir, f"File{i}") for i in range(min(3, fdp.ConsumeIntInRange(1, 3)))] 23 | for file_path in file_paths: 24 | with open(file_path, "wb") as f: 25 | # The chosen upperbound for count of bytes we consume by writing to these 26 | # files is somewhat arbitrary and may be worth experimenting with if the 27 | # fuzzer coverage plateaus. 28 | f.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512))) 29 | 30 | repo.index.add(file_paths) 31 | repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 80))) 32 | 33 | fuzz_tree = git.Tree(repo, git.Tree.NULL_BIN_SHA, 0, "") 34 | 35 | try: 36 | fuzz_tree._deserialize(io.BytesIO(data)) 37 | except IndexError: 38 | return -1 39 | 40 | 41 | def main(): 42 | atheris.Setup(sys.argv, TestOneInput) 43 | atheris.Fuzz() 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/fuzz_submodule.py: -------------------------------------------------------------------------------- 1 | import atheris 2 | import sys 3 | import os 4 | import tempfile 5 | from configparser import ParsingError 6 | from utils import ( 7 | setup_git_environment, 8 | handle_exception, 9 | get_max_filename_length, 10 | ) 11 | 12 | # Setup the Git environment 13 | setup_git_environment() 14 | from git import Repo, GitCommandError, InvalidGitRepositoryError 15 | 16 | 17 | def sanitize_input(input_str, max_length=255): 18 | """Sanitize and truncate inputs to avoid invalid Git operations.""" 19 | sanitized = "".join(ch for ch in input_str if ch.isalnum() or ch in ("-", "_", ".")) 20 | return sanitized[:max_length] 21 | 22 | 23 | def TestOneInput(data): 24 | fdp = atheris.FuzzedDataProvider(data) 25 | 26 | with tempfile.TemporaryDirectory() as repo_temp_dir: 27 | repo = Repo.init(path=repo_temp_dir) 28 | repo.index.commit("Initial commit") 29 | 30 | try: 31 | with tempfile.TemporaryDirectory() as submodule_temp_dir: 32 | sub_repo = Repo.init(submodule_temp_dir, bare=fdp.ConsumeBool()) 33 | commit_message = sanitize_input(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))) 34 | sub_repo.index.commit(commit_message) 35 | 36 | submodule_name = sanitize_input( 37 | fdp.ConsumeUnicodeNoSurrogates( 38 | fdp.ConsumeIntInRange(1, get_max_filename_length(repo.working_tree_dir)) 39 | ) 40 | ) 41 | 42 | submodule_path = os.path.relpath( 43 | os.path.join(repo.working_tree_dir, submodule_name), 44 | start=repo.working_tree_dir, 45 | ) 46 | 47 | # Ensure submodule_path is valid 48 | if not submodule_name or submodule_name.startswith("/") or ".." in submodule_name: 49 | return -1 # Reject invalid input so they are not added to the corpus 50 | 51 | submodule = repo.create_submodule(submodule_name, submodule_path, url=sub_repo.git_dir) 52 | repo.index.commit("Added submodule") 53 | 54 | with submodule.config_writer() as writer: 55 | key_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes())) 56 | value_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes())) 57 | 58 | writer.set_value( 59 | sanitize_input(fdp.ConsumeUnicodeNoSurrogates(key_length)), 60 | sanitize_input(fdp.ConsumeUnicodeNoSurrogates(value_length)), 61 | ) 62 | writer.release() 63 | 64 | submodule.update( 65 | init=fdp.ConsumeBool(), 66 | dry_run=fdp.ConsumeBool(), 67 | force=fdp.ConsumeBool(), 68 | ) 69 | 70 | submodule_repo = submodule.module() 71 | 72 | new_file_name = sanitize_input( 73 | fdp.ConsumeUnicodeNoSurrogates( 74 | fdp.ConsumeIntInRange(1, get_max_filename_length(submodule_repo.working_tree_dir)) 75 | ) 76 | ) 77 | new_file_path = os.path.join(submodule_repo.working_tree_dir, new_file_name) 78 | with open(new_file_path, "wb") as new_file: 79 | new_file.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512))) 80 | 81 | submodule_repo.index.add([new_file_path]) 82 | submodule_repo.index.commit("Added new file to submodule") 83 | 84 | repo.submodule_update(recursive=fdp.ConsumeBool()) 85 | submodule_repo.head.reset( 86 | commit="HEAD~1", 87 | working_tree=fdp.ConsumeBool(), 88 | head=fdp.ConsumeBool(), 89 | ) 90 | 91 | module_option_value, configuration_option_value = fdp.PickValueInList( 92 | [(True, False), (False, True), (True, True)] 93 | ) 94 | submodule.remove( 95 | module=module_option_value, 96 | configuration=configuration_option_value, 97 | dry_run=fdp.ConsumeBool(), 98 | force=fdp.ConsumeBool(), 99 | ) 100 | repo.index.commit(f"Removed submodule {submodule_name}") 101 | 102 | except ( 103 | ParsingError, 104 | GitCommandError, 105 | InvalidGitRepositoryError, 106 | FileNotFoundError, 107 | FileExistsError, 108 | IsADirectoryError, 109 | NotADirectoryError, 110 | BrokenPipeError, 111 | PermissionError, 112 | ): 113 | return -1 114 | except Exception as e: 115 | return handle_exception(e) 116 | 117 | 118 | def main(): 119 | atheris.instrument_all() 120 | atheris.Setup(sys.argv, TestOneInput) 121 | atheris.Fuzz() 122 | 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /fuzzing/fuzz-targets/utils.py: -------------------------------------------------------------------------------- 1 | import atheris # pragma: no cover 2 | import os # pragma: no cover 3 | import re # pragma: no cover 4 | import traceback # pragma: no cover 5 | import sys # pragma: no cover 6 | from typing import Set, Tuple, List # pragma: no cover 7 | 8 | 9 | @atheris.instrument_func 10 | def is_expected_exception_message(exception: Exception, error_message_list: List[str]) -> bool: # pragma: no cover 11 | """ 12 | Checks if the message of a given exception matches any of the expected error messages, case-insensitively. 13 | 14 | Args: 15 | exception (Exception): The exception object raised during execution. 16 | error_message_list (List[str]): A list of error message substrings to check against the exception's message. 17 | 18 | Returns: 19 | bool: True if the exception's message contains any of the substrings from the error_message_list, 20 | case-insensitively, otherwise False. 21 | """ 22 | exception_message = str(exception).lower() 23 | for error in error_message_list: 24 | if error.lower() in exception_message: 25 | return True 26 | return False 27 | 28 | 29 | @atheris.instrument_func 30 | def get_max_filename_length(path: str) -> int: # pragma: no cover 31 | """ 32 | Get the maximum filename length for the filesystem containing the given path. 33 | 34 | Args: 35 | path (str): The path to check the filesystem for. 36 | 37 | Returns: 38 | int: The maximum filename length. 39 | """ 40 | return os.pathconf(path, "PC_NAME_MAX") 41 | 42 | 43 | @atheris.instrument_func 44 | def read_lines_from_file(file_path: str) -> list: 45 | """Read lines from a file and return them as a list.""" 46 | try: 47 | with open(file_path, "r") as f: 48 | return [line.strip() for line in f if line.strip()] 49 | except FileNotFoundError: 50 | print(f"File not found: {file_path}") 51 | return [] 52 | except IOError as e: 53 | print(f"Error reading file {file_path}: {e}") 54 | return [] 55 | 56 | 57 | @atheris.instrument_func 58 | def load_exception_list(file_path: str = "explicit-exceptions-list.txt") -> Set[Tuple[str, str]]: 59 | """Load and parse the exception list from a default or specified file.""" 60 | try: 61 | bundle_dir = os.path.dirname(os.path.abspath(__file__)) 62 | full_path = os.path.join(bundle_dir, file_path) 63 | lines = read_lines_from_file(full_path) 64 | exception_list: Set[Tuple[str, str]] = set() 65 | for line in lines: 66 | match = re.match(r"(.+):(\d+):", line) 67 | if match: 68 | file_path: str = match.group(1).strip() 69 | line_number: str = str(match.group(2).strip()) 70 | exception_list.add((file_path, line_number)) 71 | return exception_list 72 | except Exception as e: 73 | print(f"Error loading exception list: {e}") 74 | return set() 75 | 76 | 77 | @atheris.instrument_func 78 | def match_exception_with_traceback(exception_list: Set[Tuple[str, str]], exc_traceback) -> bool: 79 | """Match exception traceback with the entries in the exception list.""" 80 | for filename, lineno, _, _ in traceback.extract_tb(exc_traceback): 81 | for file_pattern, line_pattern in exception_list: 82 | # Ensure filename and line_number are strings for regex matching 83 | if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)): 84 | return True 85 | return False 86 | 87 | 88 | @atheris.instrument_func 89 | def check_exception_against_list(exc_traceback, exception_file: str = "explicit-exceptions-list.txt") -> bool: 90 | """Check if the exception traceback matches any entry in the exception list.""" 91 | exception_list = load_exception_list(exception_file) 92 | return match_exception_with_traceback(exception_list, exc_traceback) 93 | 94 | 95 | @atheris.instrument_func 96 | def handle_exception(e: Exception) -> int: 97 | """Encapsulate exception handling logic for reusability.""" 98 | exc_traceback = e.__traceback__ 99 | if check_exception_against_list(exc_traceback): 100 | return -1 101 | else: 102 | raise e 103 | 104 | 105 | @atheris.instrument_func 106 | def setup_git_environment() -> None: 107 | """Set up the environment variables for Git.""" 108 | bundle_dir = os.path.dirname(os.path.abspath(__file__)) 109 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): # pragma: no cover 110 | bundled_git_binary_path = os.path.join(bundle_dir, "git") 111 | os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path 112 | 113 | if not sys.warnoptions: # pragma: no cover 114 | # The warnings filter below can be overridden by passing the -W option 115 | # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable. 116 | import warnings 117 | import logging 118 | 119 | # Fuzzing data causes some modules to generate a large number of warnings 120 | # which are not usually interesting and make the test output hard to read, so we ignore them. 121 | warnings.simplefilter("ignore") 122 | logging.getLogger().setLevel(logging.ERROR) 123 | -------------------------------------------------------------------------------- /fuzzing/local-dev-helpers/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Use the same Python version as OSS-Fuzz to accidental incompatibilities in test code 4 | FROM python:3.8-bookworm 5 | 6 | LABEL project="GitPython Fuzzing Local Dev Helper" 7 | 8 | WORKDIR /src 9 | 10 | COPY . . 11 | 12 | # Update package managers, install necessary packages, and cleanup unnecessary files in a single RUN to keep the image smaller. 13 | RUN apt-get update && \ 14 | apt-get install -y git clang && \ 15 | python -m pip install --upgrade pip && \ 16 | python -m pip install atheris && \ 17 | python -m pip install -e . && \ 18 | apt-get clean && \ 19 | apt-get autoremove -y && \ 20 | rm -rf /var/lib/apt/lists/* 21 | 22 | CMD ["bash"] 23 | -------------------------------------------------------------------------------- /fuzzing/oss-fuzz-scripts/build.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | # 3 | # This file is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | set -euo pipefail 7 | 8 | python3 -m pip install . 9 | 10 | find "$SRC" -maxdepth 1 \ 11 | \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name '*.dict' \) \ 12 | -exec printf '[%s] Copying: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" {} \; \ 13 | -exec chmod a-x {} \; \ 14 | -exec cp {} "$OUT" \; 15 | 16 | # Build fuzzers in $OUT. 17 | find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do 18 | compile_python_fuzzer "$fuzz_harness" --add-binary="$(command -v git):." --add-data="$SRC/explicit-exceptions-list.txt:." 19 | done 20 | -------------------------------------------------------------------------------- /fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This file is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | set -euo pipefail 7 | 8 | ################# 9 | # Prerequisites # 10 | ################# 11 | 12 | for cmd in python3 git wget zip; do 13 | command -v "$cmd" >/dev/null 2>&1 || { 14 | printf '[%s] Required command %s not found, exiting.\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$cmd" >&2 15 | exit 1 16 | } 17 | done 18 | 19 | ############# 20 | # Functions # 21 | ############# 22 | 23 | download_and_concatenate_common_dictionaries() { 24 | # Assign the first argument as the target file where all contents will be concatenated 25 | local target_file="$1" 26 | 27 | # Shift the arguments so the first argument (target_file path) is removed 28 | # and only URLs are left for the loop below. 29 | shift 30 | 31 | for url in "$@"; do 32 | wget -qO- "$url" >>"$target_file" 33 | # Ensure there's a newline between each file's content 34 | echo >>"$target_file" 35 | done 36 | } 37 | 38 | create_seed_corpora_zips() { 39 | local seed_corpora_dir="$1" 40 | local output_zip 41 | for dir in "$seed_corpora_dir"/*; do 42 | if [ -d "$dir" ] && [ -n "$dir" ]; then 43 | output_zip="$SRC/$(basename "$dir")_seed_corpus.zip" 44 | printf '[%s] Zipping the contents of %s into %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$dir" "$output_zip" 45 | zip -jur "$output_zip" "$dir"/* 46 | fi 47 | done 48 | } 49 | 50 | prepare_dictionaries_for_fuzz_targets() { 51 | local dictionaries_dir="$1" 52 | local fuzz_targets_dir="$2" 53 | local common_base_dictionary_filename="$WORK/__base.dict" 54 | 55 | printf '[%s] Copying .dict files from %s to %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$dictionaries_dir" "$SRC/" 56 | cp -v "$dictionaries_dir"/*.dict "$SRC/" 57 | 58 | download_and_concatenate_common_dictionaries "$common_base_dictionary_filename" \ 59 | "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \ 60 | "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict" 61 | 62 | find "$fuzz_targets_dir" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do 63 | if [[ -r "$common_base_dictionary_filename" ]]; then 64 | # Strip the `.py` extension from the filename and replace it with `.dict`. 65 | fuzz_harness_dictionary_filename="$(basename "$fuzz_harness" .py).dict" 66 | local output_file="$SRC/$fuzz_harness_dictionary_filename" 67 | 68 | printf '[%s] Appending %s to %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$common_base_dictionary_filename" "$output_file" 69 | if [[ -s "$output_file" ]]; then 70 | # If a dictionary file for this fuzzer already exists and is not empty, 71 | # we append a new line to the end of it before appending any new entries. 72 | # 73 | # LibFuzzer will happily ignore multiple empty lines in a dictionary but fail with an error 74 | # if any single line has incorrect syntax (e.g., if we accidentally add two entries to the same line.) 75 | # See docs for valid syntax: https://llvm.org/docs/LibFuzzer.html#id32 76 | echo >>"$output_file" 77 | fi 78 | cat "$common_base_dictionary_filename" >>"$output_file" 79 | fi 80 | done 81 | } 82 | 83 | ######################## 84 | # Main execution logic # 85 | ######################## 86 | # Seed corpora and dictionaries are hosted in a separate repository to avoid additional bloat in this repo. 87 | # We clone into the $WORK directory because OSS-Fuzz cleans it up after building the image, keeping the image small. 88 | git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git "$WORK/qa-assets" 89 | 90 | create_seed_corpora_zips "$WORK/qa-assets/gitpython/corpora" 91 | 92 | prepare_dictionaries_for_fuzz_targets "$WORK/qa-assets/gitpython/dictionaries" "$SRC/gitpython/fuzzing" 93 | 94 | pushd "$SRC/gitpython/" 95 | # Search for 'raise' and 'assert' statements in Python files within GitPython's source code and submodules, saving the 96 | # matched file path, line number, and line content to a file named 'explicit-exceptions-list.txt'. 97 | # This file can then be used by fuzz harnesses to check exception tracebacks and filter out explicitly raised or otherwise 98 | # anticipated exceptions to reduce false positive test failures. 99 | 100 | git grep -n --recurse-submodules -e '\braise\b' -e '\bassert\b' -- '*.py' -- ':!setup.py' -- ':!test/**' -- ':!fuzzing/**' > "$SRC/explicit-exceptions-list.txt" 101 | 102 | popd 103 | 104 | 105 | # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below. 106 | python3 -m pip install --upgrade pip 107 | # Upgrade to the latest versions known to work at the time the below changes were introduced: 108 | python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0' 109 | -------------------------------------------------------------------------------- /git/compat.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | """Utilities to help provide compatibility with Python 3. 7 | 8 | This module exists for historical reasons. Code outside GitPython may make use of public 9 | members of this module, but is unlikely to benefit from doing so. GitPython continues to 10 | use some of these utilities, in some cases for compatibility across different platforms. 11 | """ 12 | 13 | import locale 14 | import os 15 | import sys 16 | import warnings 17 | 18 | from gitdb.utils.encoding import force_bytes, force_text # noqa: F401 19 | 20 | # typing -------------------------------------------------------------------- 21 | 22 | from typing import ( 23 | Any, # noqa: F401 24 | AnyStr, 25 | Dict, # noqa: F401 26 | IO, # noqa: F401 27 | List, 28 | Optional, 29 | TYPE_CHECKING, 30 | Tuple, # noqa: F401 31 | Type, # noqa: F401 32 | Union, 33 | overload, 34 | ) 35 | 36 | # --------------------------------------------------------------------------- 37 | 38 | 39 | _deprecated_platform_aliases = { 40 | "is_win": os.name == "nt", 41 | "is_posix": os.name == "posix", 42 | "is_darwin": sys.platform == "darwin", 43 | } 44 | 45 | 46 | def _getattr(name: str) -> Any: 47 | try: 48 | value = _deprecated_platform_aliases[name] 49 | except KeyError: 50 | raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None 51 | 52 | warnings.warn( 53 | f"{__name__}.{name} and other is_ aliases are deprecated. " 54 | "Write the desired os.name or sys.platform check explicitly instead.", 55 | DeprecationWarning, 56 | stacklevel=2, 57 | ) 58 | return value 59 | 60 | 61 | if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes. 62 | __getattr__ = _getattr 63 | 64 | 65 | def __dir__() -> List[str]: 66 | return [*globals(), *_deprecated_platform_aliases] 67 | 68 | 69 | is_win: bool 70 | """Deprecated alias for ``os.name == "nt"`` to check for native Windows. 71 | 72 | This is deprecated because it is clearer to write out :attr:`os.name` or 73 | :attr:`sys.platform` checks explicitly, especially in cases where it matters which is 74 | used. 75 | 76 | :note: 77 | ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To detect 78 | Cygwin, use ``sys.platform == "cygwin"``. 79 | """ 80 | 81 | is_posix: bool 82 | """Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems. 83 | 84 | This is deprecated because it clearer to write out :attr:`os.name` or 85 | :attr:`sys.platform` checks explicitly, especially in cases where it matters which is 86 | used. 87 | 88 | :note: 89 | For POSIX systems, more detailed information is available in :attr:`sys.platform`, 90 | while :attr:`os.name` is always ``"posix"`` on such systems, including macOS 91 | (Darwin). 92 | """ 93 | 94 | is_darwin: bool 95 | """Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin). 96 | 97 | This is deprecated because it clearer to write out :attr:`os.name` or 98 | :attr:`sys.platform` checks explicitly. 99 | 100 | :note: 101 | For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while 102 | ``sys.platform == "darwin"``. 103 | """ 104 | 105 | defenc = sys.getfilesystemencoding() 106 | """The encoding used to convert between Unicode and bytes filenames.""" 107 | 108 | 109 | @overload 110 | def safe_decode(s: None) -> None: ... 111 | 112 | 113 | @overload 114 | def safe_decode(s: AnyStr) -> str: ... 115 | 116 | 117 | def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: 118 | """Safely decode a binary string to Unicode.""" 119 | if isinstance(s, str): 120 | return s 121 | elif isinstance(s, bytes): 122 | return s.decode(defenc, "surrogateescape") 123 | elif s is None: 124 | return None 125 | else: 126 | raise TypeError("Expected bytes or text, but got %r" % (s,)) 127 | 128 | 129 | @overload 130 | def safe_encode(s: None) -> None: ... 131 | 132 | 133 | @overload 134 | def safe_encode(s: AnyStr) -> bytes: ... 135 | 136 | 137 | def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: 138 | """Safely encode a binary string to Unicode.""" 139 | if isinstance(s, str): 140 | return s.encode(defenc) 141 | elif isinstance(s, bytes): 142 | return s 143 | elif s is None: 144 | return None 145 | else: 146 | raise TypeError("Expected bytes or text, but got %r" % (s,)) 147 | 148 | 149 | @overload 150 | def win_encode(s: None) -> None: ... 151 | 152 | 153 | @overload 154 | def win_encode(s: AnyStr) -> bytes: ... 155 | 156 | 157 | def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: 158 | """Encode Unicode strings for process arguments on Windows.""" 159 | if isinstance(s, str): 160 | return s.encode(locale.getpreferredencoding(False)) 161 | elif isinstance(s, bytes): 162 | return s 163 | elif s is not None: 164 | raise TypeError("Expected bytes or text, but got %r" % (s,)) 165 | return None 166 | -------------------------------------------------------------------------------- /git/db.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Module with our own gitdb implementation - it uses the git command.""" 5 | 6 | __all__ = ["GitCmdObjectDB", "GitDB"] 7 | 8 | from gitdb.base import OInfo, OStream 9 | from gitdb.db import GitDB, LooseObjectDB 10 | from gitdb.exc import BadObject 11 | 12 | from git.util import bin_to_hex, hex_to_bin 13 | from git.exc import GitCommandError 14 | 15 | # typing------------------------------------------------- 16 | 17 | from typing import TYPE_CHECKING 18 | 19 | from git.types import PathLike 20 | 21 | if TYPE_CHECKING: 22 | from git.cmd import Git 23 | 24 | # -------------------------------------------------------- 25 | 26 | 27 | class GitCmdObjectDB(LooseObjectDB): 28 | """A database representing the default git object store, which includes loose 29 | objects, pack files and an alternates file. 30 | 31 | It will create objects only in the loose object database. 32 | """ 33 | 34 | def __init__(self, root_path: PathLike, git: "Git") -> None: 35 | """Initialize this instance with the root and a git command.""" 36 | super().__init__(root_path) 37 | self._git = git 38 | 39 | def info(self, binsha: bytes) -> OInfo: 40 | """Get a git object header (using git itself).""" 41 | hexsha, typename, size = self._git.get_object_header(bin_to_hex(binsha)) 42 | return OInfo(hex_to_bin(hexsha), typename, size) 43 | 44 | def stream(self, binsha: bytes) -> OStream: 45 | """Get git object data as a stream supporting ``read()`` (using git itself).""" 46 | hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha)) 47 | return OStream(hex_to_bin(hexsha), typename, size, stream) 48 | 49 | # { Interface 50 | 51 | def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes: 52 | """ 53 | :return: 54 | Full binary 20 byte sha from the given partial hexsha 55 | 56 | :raise gitdb.exc.AmbiguousObjectName: 57 | 58 | :raise gitdb.exc.BadObject: 59 | 60 | :note: 61 | Currently we only raise :exc:`~gitdb.exc.BadObject` as git does not 62 | communicate ambiguous objects separately. 63 | """ 64 | try: 65 | hexsha, _typename, _size = self._git.get_object_header(partial_hexsha) 66 | return hex_to_bin(hexsha) 67 | except (GitCommandError, ValueError) as e: 68 | raise BadObject(partial_hexsha) from e 69 | # END handle exceptions 70 | 71 | # } END interface 72 | -------------------------------------------------------------------------------- /git/index/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Initialize the index package.""" 5 | 6 | __all__ = [ 7 | "BaseIndexEntry", 8 | "BlobFilter", 9 | "CheckoutError", 10 | "IndexEntry", 11 | "IndexFile", 12 | "StageType", 13 | ] 14 | 15 | from .base import CheckoutError, IndexFile 16 | from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType 17 | -------------------------------------------------------------------------------- /git/index/util.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Index utilities.""" 5 | 6 | __all__ = ["TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir"] 7 | 8 | import contextlib 9 | from functools import wraps 10 | import os 11 | import os.path as osp 12 | import struct 13 | import tempfile 14 | from types import TracebackType 15 | 16 | # typing ---------------------------------------------------------------------- 17 | 18 | from typing import Any, Callable, TYPE_CHECKING, Optional, Type 19 | 20 | from git.types import Literal, PathLike, _T 21 | 22 | if TYPE_CHECKING: 23 | from git.index import IndexFile 24 | 25 | # --------------------------------------------------------------------------------- 26 | 27 | # { Aliases 28 | pack = struct.pack 29 | unpack = struct.unpack 30 | # } END aliases 31 | 32 | 33 | class TemporaryFileSwap: 34 | """Utility class moving a file to a temporary location within the same directory and 35 | moving it back on to where on object deletion.""" 36 | 37 | __slots__ = ("file_path", "tmp_file_path") 38 | 39 | def __init__(self, file_path: PathLike) -> None: 40 | self.file_path = file_path 41 | dirname, basename = osp.split(file_path) 42 | fd, self.tmp_file_path = tempfile.mkstemp(prefix=basename, dir=dirname) 43 | os.close(fd) 44 | with contextlib.suppress(OSError): # It may be that the source does not exist. 45 | os.replace(self.file_path, self.tmp_file_path) 46 | 47 | def __enter__(self) -> "TemporaryFileSwap": 48 | return self 49 | 50 | def __exit__( 51 | self, 52 | exc_type: Optional[Type[BaseException]], 53 | exc_val: Optional[BaseException], 54 | exc_tb: Optional[TracebackType], 55 | ) -> Literal[False]: 56 | if osp.isfile(self.tmp_file_path): 57 | os.replace(self.tmp_file_path, self.file_path) 58 | return False 59 | 60 | 61 | # { Decorators 62 | 63 | 64 | def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]: 65 | """Decorator for functions that alter the index using the git command. 66 | 67 | When a git command alters the index, this invalidates our possibly existing entries 68 | dictionary, which is why it must be deleted to allow it to be lazily reread later. 69 | """ 70 | 71 | @wraps(func) 72 | def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: 73 | rval = func(self, *args, **kwargs) 74 | self._delete_entries_cache() 75 | return rval 76 | 77 | # END wrapper method 78 | 79 | return post_clear_cache_if_not_raised 80 | 81 | 82 | def default_index(func: Callable[..., _T]) -> Callable[..., _T]: 83 | """Decorator ensuring the wrapped method may only run if we are the default 84 | repository index. 85 | 86 | This is as we rely on git commands that operate on that index only. 87 | """ 88 | 89 | @wraps(func) 90 | def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: 91 | if self._file_path != self._index_path(): 92 | raise AssertionError( 93 | "Cannot call %r on indices that do not represent the default git index" % func.__name__ 94 | ) 95 | return func(self, *args, **kwargs) 96 | 97 | # END wrapper method 98 | 99 | return check_default_index 100 | 101 | 102 | def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]: 103 | """Decorator which changes the current working dir to the one of the git 104 | repository in order to ensure relative paths are handled correctly.""" 105 | 106 | @wraps(func) 107 | def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: 108 | cur_wd = os.getcwd() 109 | os.chdir(str(self.repo.working_tree_dir)) 110 | try: 111 | return func(self, *args, **kwargs) 112 | finally: 113 | os.chdir(cur_wd) 114 | # END handle working dir 115 | 116 | # END wrapper 117 | 118 | return set_git_working_dir 119 | 120 | 121 | # } END decorators 122 | -------------------------------------------------------------------------------- /git/objects/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Import all submodules' main classes into the package space.""" 5 | 6 | __all__ = [ 7 | "IndexObject", 8 | "Object", 9 | "Blob", 10 | "Commit", 11 | "Submodule", 12 | "UpdateProgress", 13 | "RootModule", 14 | "RootUpdateProgress", 15 | "TagObject", 16 | "Tree", 17 | "TreeModifier", 18 | ] 19 | 20 | from .base import IndexObject, Object 21 | from .blob import Blob 22 | from .commit import Commit 23 | from .submodule import RootModule, RootUpdateProgress, Submodule, UpdateProgress 24 | from .tag import TagObject 25 | from .tree import Tree, TreeModifier 26 | -------------------------------------------------------------------------------- /git/objects/blob.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | __all__ = ["Blob"] 7 | 8 | from mimetypes import guess_type 9 | import sys 10 | 11 | if sys.version_info >= (3, 8): 12 | from typing import Literal 13 | else: 14 | from typing_extensions import Literal 15 | 16 | from . import base 17 | 18 | 19 | class Blob(base.IndexObject): 20 | """A Blob encapsulates a git blob object. 21 | 22 | See :manpage:`gitglossary(7)` on "blob": 23 | https://git-scm.com/docs/gitglossary#def_blob_object 24 | """ 25 | 26 | DEFAULT_MIME_TYPE = "text/plain" 27 | type: Literal["blob"] = "blob" 28 | 29 | # Valid blob modes 30 | executable_mode = 0o100755 31 | file_mode = 0o100644 32 | link_mode = 0o120000 33 | 34 | __slots__ = () 35 | 36 | @property 37 | def mime_type(self) -> str: 38 | """ 39 | :return: 40 | String describing the mime type of this file (based on the filename) 41 | 42 | :note: 43 | Defaults to ``text/plain`` in case the actual file type is unknown. 44 | """ 45 | guesses = None 46 | if self.path: 47 | guesses = guess_type(str(self.path)) 48 | return guesses and guesses[0] or self.DEFAULT_MIME_TYPE 49 | -------------------------------------------------------------------------------- /git/objects/submodule/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | __all__ = ["Submodule", "UpdateProgress", "RootModule", "RootUpdateProgress"] 5 | 6 | from .base import Submodule, UpdateProgress 7 | from .root import RootModule, RootUpdateProgress 8 | -------------------------------------------------------------------------------- /git/objects/submodule/util.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | __all__ = [ 5 | "sm_section", 6 | "sm_name", 7 | "mkhead", 8 | "find_first_remote_branch", 9 | "SubmoduleConfigParser", 10 | ] 11 | 12 | from io import BytesIO 13 | import weakref 14 | 15 | import git 16 | from git.config import GitConfigParser 17 | from git.exc import InvalidGitRepositoryError 18 | 19 | # typing ----------------------------------------------------------------------- 20 | 21 | from typing import Any, Sequence, TYPE_CHECKING, Union 22 | 23 | from git.types import PathLike 24 | 25 | if TYPE_CHECKING: 26 | from weakref import ReferenceType 27 | 28 | from git.refs import Head, RemoteReference 29 | from git.remote import Remote 30 | from git.repo import Repo 31 | 32 | from .base import Submodule 33 | 34 | # { Utilities 35 | 36 | 37 | def sm_section(name: str) -> str: 38 | """:return: Section title used in ``.gitmodules`` configuration file""" 39 | return f'submodule "{name}"' 40 | 41 | 42 | def sm_name(section: str) -> str: 43 | """:return: Name of the submodule as parsed from the section name""" 44 | section = section.strip() 45 | return section[11:-1] 46 | 47 | 48 | def mkhead(repo: "Repo", path: PathLike) -> "Head": 49 | """:return: New branch/head instance""" 50 | return git.Head(repo, git.Head.to_full_path(path)) 51 | 52 | 53 | def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference": 54 | """Find the remote branch matching the name of the given branch or raise 55 | :exc:`~git.exc.InvalidGitRepositoryError`.""" 56 | for remote in remotes: 57 | try: 58 | return remote.refs[branch_name] 59 | except IndexError: 60 | continue 61 | # END exception handling 62 | # END for remote 63 | raise InvalidGitRepositoryError("Didn't find remote branch '%r' in any of the given remotes" % branch_name) 64 | 65 | 66 | # } END utilities 67 | 68 | # { Classes 69 | 70 | 71 | class SubmoduleConfigParser(GitConfigParser): 72 | """Catches calls to :meth:`~git.config.GitConfigParser.write`, and updates the 73 | ``.gitmodules`` blob in the index with the new data, if we have written into a 74 | stream. 75 | 76 | Otherwise it would add the local file to the index to make it correspond with the 77 | working tree. Additionally, the cache must be cleared. 78 | 79 | Please note that no mutating method will work in bare mode. 80 | """ 81 | 82 | def __init__(self, *args: Any, **kwargs: Any) -> None: 83 | self._smref: Union["ReferenceType[Submodule]", None] = None 84 | self._index = None 85 | self._auto_write = True 86 | super().__init__(*args, **kwargs) 87 | 88 | # { Interface 89 | def set_submodule(self, submodule: "Submodule") -> None: 90 | """Set this instance's submodule. It must be called before the first write 91 | operation begins.""" 92 | self._smref = weakref.ref(submodule) 93 | 94 | def flush_to_index(self) -> None: 95 | """Flush changes in our configuration file to the index.""" 96 | assert self._smref is not None 97 | # Should always have a file here. 98 | assert not isinstance(self._file_or_files, BytesIO) 99 | 100 | sm = self._smref() 101 | if sm is not None: 102 | index = self._index 103 | if index is None: 104 | index = sm.repo.index 105 | # END handle index 106 | index.add([sm.k_modules_file], write=self._auto_write) 107 | sm._clear_cache() 108 | # END handle weakref 109 | 110 | # } END interface 111 | 112 | # { Overridden Methods 113 | def write(self) -> None: # type: ignore[override] 114 | rval: None = super().write() 115 | self.flush_to_index() 116 | return rval 117 | 118 | # END overridden methods 119 | 120 | 121 | # } END classes 122 | -------------------------------------------------------------------------------- /git/objects/tag.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | """Provides an :class:`~git.objects.base.Object`-based type for annotated tags. 7 | 8 | This defines the :class:`TagObject` class, which represents annotated tags. 9 | For lightweight tags, see the :mod:`git.refs.tag` module. 10 | """ 11 | 12 | __all__ = ["TagObject"] 13 | 14 | import sys 15 | 16 | from git.compat import defenc 17 | from git.util import Actor, hex_to_bin 18 | 19 | from . import base 20 | from .util import get_object_type_by_name, parse_actor_and_date 21 | 22 | # typing ---------------------------------------------- 23 | 24 | from typing import List, TYPE_CHECKING, Union 25 | 26 | if sys.version_info >= (3, 8): 27 | from typing import Literal 28 | else: 29 | from typing_extensions import Literal 30 | 31 | if TYPE_CHECKING: 32 | from git.repo import Repo 33 | 34 | from .blob import Blob 35 | from .commit import Commit 36 | from .tree import Tree 37 | 38 | # --------------------------------------------------- 39 | 40 | 41 | class TagObject(base.Object): 42 | """Annotated (i.e. non-lightweight) tag carrying additional information about an 43 | object we are pointing to. 44 | 45 | See :manpage:`gitglossary(7)` on "tag object": 46 | https://git-scm.com/docs/gitglossary#def_tag_object 47 | """ 48 | 49 | type: Literal["tag"] = "tag" 50 | 51 | __slots__ = ( 52 | "object", 53 | "tag", 54 | "tagger", 55 | "tagged_date", 56 | "tagger_tz_offset", 57 | "message", 58 | ) 59 | 60 | def __init__( 61 | self, 62 | repo: "Repo", 63 | binsha: bytes, 64 | object: Union[None, base.Object] = None, 65 | tag: Union[None, str] = None, 66 | tagger: Union[None, Actor] = None, 67 | tagged_date: Union[int, None] = None, 68 | tagger_tz_offset: Union[int, None] = None, 69 | message: Union[str, None] = None, 70 | ) -> None: # @ReservedAssignment 71 | """Initialize a tag object with additional data. 72 | 73 | :param repo: 74 | Repository this object is located in. 75 | 76 | :param binsha: 77 | 20 byte SHA1. 78 | 79 | :param object: 80 | :class:`~git.objects.base.Object` instance of object we are pointing to. 81 | 82 | :param tag: 83 | Name of this tag. 84 | 85 | :param tagger: 86 | :class:`~git.util.Actor` identifying the tagger. 87 | 88 | :param tagged_date: int_seconds_since_epoch 89 | The DateTime of the tag creation. 90 | Use :func:`time.gmtime` to convert it into a different format. 91 | 92 | :param tagger_tz_offset: int_seconds_west_of_utc 93 | The timezone that the `tagged_date` is in, in a format similar to 94 | :attr:`time.altzone`. 95 | """ 96 | super().__init__(repo, binsha) 97 | if object is not None: 98 | self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object 99 | if tag is not None: 100 | self.tag = tag 101 | if tagger is not None: 102 | self.tagger = tagger 103 | if tagged_date is not None: 104 | self.tagged_date = tagged_date 105 | if tagger_tz_offset is not None: 106 | self.tagger_tz_offset = tagger_tz_offset 107 | if message is not None: 108 | self.message = message 109 | 110 | def _set_cache_(self, attr: str) -> None: 111 | """Cache all our attributes at once.""" 112 | if attr in TagObject.__slots__: 113 | ostream = self.repo.odb.stream(self.binsha) 114 | lines: List[str] = ostream.read().decode(defenc, "replace").splitlines() 115 | 116 | _obj, hexsha = lines[0].split(" ") 117 | _type_token, type_name = lines[1].split(" ") 118 | object_type = get_object_type_by_name(type_name.encode("ascii")) 119 | self.object = object_type(self.repo, hex_to_bin(hexsha)) 120 | 121 | self.tag = lines[2][4:] # tag 122 | 123 | if len(lines) > 3: 124 | tagger_info = lines[3] # tagger 125 | ( 126 | self.tagger, 127 | self.tagged_date, 128 | self.tagger_tz_offset, 129 | ) = parse_actor_and_date(tagger_info) 130 | 131 | # Line 4 empty - it could mark the beginning of the next header. 132 | # In case there really is no message, it would not exist. 133 | # Otherwise a newline separates header from message. 134 | if len(lines) > 5: 135 | self.message = "\n".join(lines[5:]) 136 | else: 137 | self.message = "" 138 | # END check our attributes 139 | else: 140 | super()._set_cache_(attr) 141 | -------------------------------------------------------------------------------- /git/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/git/py.typed -------------------------------------------------------------------------------- /git/refs/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | __all__ = [ 5 | "HEAD", 6 | "Head", 7 | "RefLog", 8 | "RefLogEntry", 9 | "Reference", 10 | "RemoteReference", 11 | "SymbolicReference", 12 | "Tag", 13 | "TagReference", 14 | ] 15 | 16 | from .head import HEAD, Head 17 | from .log import RefLog, RefLogEntry 18 | from .reference import Reference 19 | from .remote import RemoteReference 20 | from .symbolic import SymbolicReference 21 | from .tag import Tag, TagReference 22 | -------------------------------------------------------------------------------- /git/refs/remote.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Module implementing a remote object allowing easy access to git remotes.""" 5 | 6 | __all__ = ["RemoteReference"] 7 | 8 | import os 9 | 10 | from git.util import join_path 11 | 12 | from .head import Head 13 | 14 | # typing ------------------------------------------------------------------ 15 | 16 | from typing import Any, Iterator, NoReturn, TYPE_CHECKING, Union 17 | 18 | from git.types import PathLike 19 | 20 | if TYPE_CHECKING: 21 | from git.remote import Remote 22 | from git.repo import Repo 23 | 24 | # ------------------------------------------------------------------------------ 25 | 26 | 27 | class RemoteReference(Head): 28 | """A reference pointing to a remote head.""" 29 | 30 | _common_path_default = Head._remote_common_path_default 31 | 32 | @classmethod 33 | def iter_items( 34 | cls, 35 | repo: "Repo", 36 | common_path: Union[PathLike, None] = None, 37 | remote: Union["Remote", None] = None, 38 | *args: Any, 39 | **kwargs: Any, 40 | ) -> Iterator["RemoteReference"]: 41 | """Iterate remote references, and if given, constrain them to the given remote.""" 42 | common_path = common_path or cls._common_path_default 43 | if remote is not None: 44 | common_path = join_path(common_path, str(remote)) 45 | # END handle remote constraint 46 | # super is Reference 47 | return super().iter_items(repo, common_path) 48 | 49 | # The Head implementation of delete also accepts strs, but this implementation does 50 | # not. mypy doesn't have a way of representing tightening the types of arguments in 51 | # subclasses and recommends Any or "type: ignore". 52 | # (See: https://github.com/python/typing/issues/241) 53 | @classmethod 54 | def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: # type: ignore[override] 55 | """Delete the given remote references. 56 | 57 | :note: 58 | `kwargs` are given for comparability with the base class method as we 59 | should not narrow the signature. 60 | """ 61 | repo.git.branch("-d", "-r", *refs) 62 | # The official deletion method will ignore remote symbolic refs - these are 63 | # generally ignored in the refs/ folder. We don't though and delete remainders 64 | # manually. 65 | for ref in refs: 66 | try: 67 | os.remove(os.path.join(repo.common_dir, ref.path)) 68 | except OSError: 69 | pass 70 | try: 71 | os.remove(os.path.join(repo.git_dir, ref.path)) 72 | except OSError: 73 | pass 74 | # END for each ref 75 | 76 | @classmethod 77 | def create(cls, *args: Any, **kwargs: Any) -> NoReturn: 78 | """Raise :exc:`TypeError`. Defined so the ``create`` method is disabled.""" 79 | raise TypeError("Cannot explicitly create remote references") 80 | -------------------------------------------------------------------------------- /git/refs/tag.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Provides a :class:`~git.refs.reference.Reference`-based type for lightweight tags. 5 | 6 | This defines the :class:`TagReference` class (and its alias :class:`Tag`), which 7 | represents lightweight tags. For annotated tags (which are git objects), see the 8 | :mod:`git.objects.tag` module. 9 | """ 10 | 11 | __all__ = ["TagReference", "Tag"] 12 | 13 | from .reference import Reference 14 | 15 | # typing ------------------------------------------------------------------ 16 | 17 | from typing import Any, TYPE_CHECKING, Type, Union 18 | 19 | from git.types import AnyGitObject, PathLike 20 | 21 | if TYPE_CHECKING: 22 | from git.objects import Commit, TagObject 23 | from git.refs import SymbolicReference 24 | from git.repo import Repo 25 | 26 | # ------------------------------------------------------------------------------ 27 | 28 | 29 | class TagReference(Reference): 30 | """A lightweight tag reference which either points to a commit, a tag object or any 31 | other object. In the latter case additional information, like the signature or the 32 | tag-creator, is available. 33 | 34 | This tag object will always point to a commit object, but may carry additional 35 | information in a tag object:: 36 | 37 | tagref = TagReference.list_items(repo)[0] 38 | print(tagref.commit.message) 39 | if tagref.tag is not None: 40 | print(tagref.tag.message) 41 | """ 42 | 43 | __slots__ = () 44 | 45 | _common_default = "tags" 46 | _common_path_default = Reference._common_path_default + "/" + _common_default 47 | 48 | @property 49 | def commit(self) -> "Commit": # type: ignore[override] # LazyMixin has unrelated commit method 50 | """:return: Commit object the tag ref points to 51 | 52 | :raise ValueError: 53 | If the tag points to a tree or blob. 54 | """ 55 | obj = self.object 56 | while obj.type != "commit": 57 | if obj.type == "tag": 58 | # It is a tag object which carries the commit as an object - we can point to anything. 59 | obj = obj.object 60 | else: 61 | raise ValueError( 62 | ( 63 | "Cannot resolve commit as tag %s points to a %s object - " 64 | + "use the `.object` property instead to access it" 65 | ) 66 | % (self, obj.type) 67 | ) 68 | return obj 69 | 70 | @property 71 | def tag(self) -> Union["TagObject", None]: 72 | """ 73 | :return: 74 | Tag object this tag ref points to, or ``None`` in case we are a lightweight 75 | tag 76 | """ 77 | obj = self.object 78 | if obj.type == "tag": 79 | return obj 80 | return None 81 | 82 | # Make object read-only. It should be reasonably hard to adjust an existing tag. 83 | @property 84 | def object(self) -> AnyGitObject: # type: ignore[override] 85 | return Reference._get_object(self) 86 | 87 | @classmethod 88 | def create( 89 | cls: Type["TagReference"], 90 | repo: "Repo", 91 | path: PathLike, 92 | reference: Union[str, "SymbolicReference"] = "HEAD", 93 | logmsg: Union[str, None] = None, 94 | force: bool = False, 95 | **kwargs: Any, 96 | ) -> "TagReference": 97 | """Create a new tag reference. 98 | 99 | :param repo: 100 | The :class:`~git.repo.base.Repo` to create the tag in. 101 | 102 | :param path: 103 | The name of the tag, e.g. ``1.0`` or ``releases/1.0``. 104 | The prefix ``refs/tags`` is implied. 105 | 106 | :param reference: 107 | A reference to the :class:`~git.objects.base.Object` you want to tag. 108 | The referenced object can be a commit, tree, or blob. 109 | 110 | :param logmsg: 111 | If not ``None``, the message will be used in your tag object. This will also 112 | create an additional tag object that allows to obtain that information, 113 | e.g.:: 114 | 115 | tagref.tag.message 116 | 117 | :param message: 118 | Synonym for the `logmsg` parameter. Included for backwards compatibility. 119 | `logmsg` takes precedence if both are passed. 120 | 121 | :param force: 122 | If ``True``, force creation of a tag even though that tag already exists. 123 | 124 | :param kwargs: 125 | Additional keyword arguments to be passed to :manpage:`git-tag(1)`. 126 | 127 | :return: 128 | A new :class:`TagReference`. 129 | """ 130 | if "ref" in kwargs and kwargs["ref"]: 131 | reference = kwargs["ref"] 132 | 133 | if "message" in kwargs and kwargs["message"]: 134 | kwargs["m"] = kwargs["message"] 135 | del kwargs["message"] 136 | 137 | if logmsg: 138 | kwargs["m"] = logmsg 139 | 140 | if force: 141 | kwargs["f"] = True 142 | 143 | args = (path, reference) 144 | 145 | repo.git.tag(*args, **kwargs) 146 | return TagReference(repo, "%s/%s" % (cls._common_path_default, path)) 147 | 148 | @classmethod 149 | def delete(cls, repo: "Repo", *tags: "TagReference") -> None: # type: ignore[override] 150 | """Delete the given existing tag or tags.""" 151 | repo.git.tag("-d", *tags) 152 | 153 | 154 | # Provide an alias. 155 | Tag = TagReference 156 | -------------------------------------------------------------------------------- /git/repo/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Initialize the repo package.""" 5 | 6 | __all__ = ["Repo"] 7 | 8 | from .base import Repo 9 | -------------------------------------------------------------------------------- /init-tests-after-clone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This file is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | set -eu 7 | 8 | fallback_repo_for_tags='https://github.com/gitpython-developers/GitPython.git' 9 | 10 | ci() { 11 | # For now, check just these, as a false positive could lead to data loss. 12 | test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}" 13 | } 14 | 15 | no_version_tags() { 16 | test -z "$(git tag -l '[0-9]*' 'v[0-9]*')" 17 | } 18 | 19 | warn() { 20 | if test -n "${GITHUB_ACTIONS-}"; then 21 | printf '::warning ::%s\n' "$*" >&2 # Annotate workflow. 22 | else 23 | printf '%s\n' "$@" >&2 24 | fi 25 | } 26 | 27 | if ! ci; then 28 | printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 29 | read -r answer 30 | case "$answer" in 31 | [yY]) 32 | ;; 33 | *) 34 | exit 2 ;; 35 | esac 36 | fi 37 | 38 | # Stop if we have run this. (You can delete __testing_point__ to let it rerun.) 39 | # This also keeps track of where we are, so we can get back here. 40 | git tag __testing_point__ 41 | 42 | # The tests need a branch called master. 43 | git checkout master -- || git checkout -b master 44 | 45 | # The tests need a reflog history on the master branch. 46 | git reset --hard HEAD~1 47 | git reset --hard HEAD~1 48 | git reset --hard HEAD~1 49 | 50 | # Point the master branch where we started, so we test the correct code. 51 | git reset --hard __testing_point__ 52 | 53 | # The tests need submodules, including a submodule with a submodule. 54 | git submodule update --init --recursive 55 | 56 | # The tests need some version tags. Try to get them even in forks. This fetches 57 | # other objects too. So, locally, we always do it, for a consistent experience. 58 | if ! ci || no_version_tags; then 59 | git fetch --all --tags 60 | fi 61 | 62 | # If we still have no version tags, try to get them from the original repo. 63 | if no_version_tags; then 64 | warn 'No local or remote version tags found. Trying fallback remote:' \ 65 | "$fallback_repo_for_tags" 66 | 67 | # git fetch supports * but not [], and --no-tags means no *other* tags, so... 68 | 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 | 69 | xargs git fetch --no-tags "$fallback_repo_for_tags" 70 | 71 | if no_version_tags; then 72 | warn 'No version tags found anywhere. Some tests will fail.' 73 | fi 74 | fi 75 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | addopts = "--cov=git --cov-report=term -ra" 7 | filterwarnings = "ignore::DeprecationWarning" 8 | python_files = "test_*.py" 9 | tmp_path_retention_policy = "failed" 10 | testpaths = "test" # Space separated list of paths from root e.g test tests doc/testing. 11 | # --cov coverage 12 | # --cov-report term # send report to terminal term-missing -> terminal with line numbers html xml 13 | # --cov-report term-missing # to terminal with line numbers 14 | # --cov-report html:path # html file at path 15 | # --maxfail # number of errors before giving up 16 | # -rfE # default test summary: list fail and error 17 | # -ra # test summary: list all non-passing (fail, error, skip, xfail, xpass) 18 | # --ignore-glob=**/gitdb/* # ignore glob paths 19 | # filterwarnings ignore::WarningType # ignores those warnings 20 | 21 | [tool.mypy] 22 | python_version = "3.8" 23 | files = ["git/", "test/deprecation/"] 24 | disallow_untyped_defs = true 25 | no_implicit_optional = true 26 | warn_redundant_casts = true 27 | warn_unused_ignores = true # Useful in general, but especially in test/deprecation. 28 | warn_unreachable = true 29 | implicit_reexport = true 30 | # strict = true 31 | # TODO: Remove when 'gitdb' is fully annotated. 32 | exclude = ["^git/ext/gitdb"] 33 | [[tool.mypy.overrides]] 34 | module = "gitdb.*" 35 | ignore_missing_imports = true 36 | 37 | [tool.coverage.run] 38 | source = ["git"] 39 | 40 | [tool.coverage.report] 41 | include = ["*/git/*"] 42 | omit = ["*/git/ext/*"] 43 | 44 | [tool.ruff] 45 | target-version = "py37" 46 | line-length = 120 47 | # Exclude a variety of commonly ignored directories. 48 | exclude = [ 49 | "git/ext/", 50 | "build", 51 | "dist", 52 | ] 53 | # Enable Pyflakes `E` and `F` codes by default. 54 | lint.select = [ 55 | "E", 56 | "W", # See: https://pypi.org/project/pycodestyle 57 | "F", # See: https://pypi.org/project/pyflakes 58 | # "I", # See: https://pypi.org/project/isort/ 59 | # "S", # See: https://pypi.org/project/flake8-bandit 60 | # "UP", # See: https://docs.astral.sh/ruff/rules/#pyupgrade-up 61 | ] 62 | lint.extend-select = [ 63 | # "A", # See: https://pypi.org/project/flake8-builtins 64 | "B", # See: https://pypi.org/project/flake8-bugbear 65 | "C4", # See: https://pypi.org/project/flake8-comprehensions 66 | "TCH004", # See: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/ 67 | ] 68 | lint.ignore = [ 69 | "E203", # Whitespace before ':' 70 | "E731", # Do not assign a `lambda` expression, use a `def` 71 | ] 72 | lint.ignore-init-module-imports = true 73 | lint.unfixable = [ 74 | "F401", # Module imported but unused 75 | ] 76 | 77 | [tool.ruff.lint.per-file-ignores] 78 | "test/**" = [ 79 | "B018", # useless-expression 80 | ] 81 | "fuzzing/fuzz-targets/**" = [ 82 | "E402", # environment setup must happen before the `git` module is imported, thus cannot happen at top of file 83 | ] 84 | 85 | 86 | [tool.codespell] 87 | ignore-words-list="afile,assertIn,doesnt,gud,uptodate" 88 | #count = true 89 | quiet-level = 3 90 | -------------------------------------------------------------------------------- /release-verification-key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBF8RDFQBEACvEIpL8Yql7PMEHyJBJMVpGG1n/ZOiPbPMrptERB2kwe6z5Kvc 4 | hPAQZwL/2z5Mdsr0K+CnW34SxFGS/OMNgq7dtH2C8XmFDy0qnNwBD5wSH8gmVCOs 5 | TW+6lvbdn1O/Wj96EkmP3QXlerD878SOElfpu9CHmDZeF+v5CUDRPGCri5ztamuv 6 | D5kjt05K+69pXgshQpCB84yWgiLSLfXocnB+k12xaCz+OLQjBcH9G2xnOAY+n/jn 7 | 84EYuoWnNdMo2lTj+PkgQ3jk57cDKM1hO9VWEKppzvSBr+hFHnoP773DXm2lMGQ2 8 | bdQHQujWNtj4x7ov9dp04O0IW08Fcm9M/QIoTG8w8oh8mpw+n8Rtx5snr/Ctuti/ 9 | L+wUMrgFLYS03v36zNKOt/7IZEVpU9WUDgdyd01NVwM56vd8tJNpwxka6SAocAa3 10 | U4Fg64zf0BXvfYZZqHGckwVYzUzB6zSPLki2I+/j4a62h4+Yen/Yxnv6g2hhG77X 11 | Tly34RHrUjrGcYW9fTcJygZ5h/y2dOl5qBwIRVXSqFg05NB4jFM2sHHxaJik8I+g 12 | A2Kfhq4/UWDJ5oHmtVTXYkm8JtUNa7lJ9qD+TdKyFzC0ExZEOKsR6yl5a3XlQk+Y 13 | Sh1BnN2Jl6lugxcageOlp0AFm/QMi9fFeH77ZhgStR/aC5w8/he/IBCTsQARAQAB 14 | tDRTZWJhc3RpYW4gVGhpZWwgKFl1YmlLZXkgVVNCLUMpIDxieXJvbmltb0BnbWFp 15 | bC5jb20+iQJOBBMBCAA4FiEEJ8UOf1kJR9cnOnQehRlMCEIZgMkFAl8RDMwCGwMF 16 | CwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQhRlMCEIZgMn1ig/8D+YobFzW+Twj 17 | DS3BUogITUGz1h6gks06Rv6IX6snZkgQNo2BsUZVJOZYRNNJJYVckJVP5DdgMVol 18 | ZFeP6dNBMzfocKeHVTPn2/6brpRSajWKC4QtBwqY6V/R2DfyztFsseqJSgfAs8l3 19 | pT0KfvSajuiarlLuB/Fx1YJ1gnuj0J7EPM+c7WMvyjKqPO5ysQL7fK0nDZLglOGb 20 | Vie9W8e7Mi0AVCQSzu/Hw8imBApOU47Sk2ceRvvOTwJlmVOfDfN1eaAiAq08PJlG 21 | OnVTsXC+D1kypBGFQZt6M3xHn4kgHzQaHtShdCH4WJBrL55t0URw6Di8aS3NwLFB 22 | YH+7owdAdS/jfqP8vtRa5bvcyviK+MP0slnvC4v46ketm8AVq5XhYsSDLBxOcrOm 23 | YO29CyJ5TVSvOayO6DEiorvmCuri9275jwHHePBB14mG54MRsh0yjvZraXzBJ2S4 24 | keZ3vW6rLI5SLIH9LtRlBAOltzIbZysAEq4J6YcAKR6KpM59xN8N902MYIyVH8zu 25 | eac13oJZ6iKlnF6Yl2oYzosQ40MsLmdonyanMcZ5emSgjihDMXZzh3EE8hb3+8aW 26 | R8Te/byYL6nlHfGugUpWK8b9HQTjdZKlVh6qS1wRBoeDGhVK9LkzluDGFoXMsSPs 27 | KW4WBExQ4MK3cgsDLcor2n3HsR3vdj20PlNlYmFzdGlhbiBUaGllbCAoSW4gUnVz 28 | dCBJIHRydXN0KSA8c2ViYXN0aWFuLnRoaWVsQGljbG91ZC5jb20+iQJOBBMBCAA4 29 | FiEEJ8UOf1kJR9cnOnQehRlMCEIZgMkFAl8RDFQCGwMFCwkIBwIGFQoJCAsCBBYC 30 | AwECHgECF4AACgkQhRlMCEIZgMnidA/8C1dg1PuOw4GRUPjn+9lAn6I0zI7o5lqV 31 | J7pi41iu/zypY6abHgr0yvUceeLEdgxCq7Wx+2CetHC34rDyKZDeNq/zP6b4fS31 32 | j+pvEDMa1Ss9FFgYZuLuMOELu3tskdUrUGzMTSNxBlxCMxrRsuSRIAoPEntrKizh 33 | EpNWIh85Ok1kGe7mCXbyO/z3Iqyy3HB7qfdErsTRdcWMJFdwYCZzIN8edfV7m8rX 34 | iFzUCn9apIIh0MSSB8GLmE1V7xPdYCvMEvZz8DVhMaObRhN+pMK7Wuv38w9IAK6b 35 | y7Au+1JrYOW07CYf5cOUaJsBIcunyPuTlyt+5zmW7Oh7eTtiX9xyf+dXeN2lLeww 36 | nltdBfBGAxNxAdmWfukKN4h+JvpIlCJqF9CzBJ2YCyqKgK8lWOGY2msEAZdOweD5 37 | mi7c0p1RKJb2brZlg3fMPh0fqorBPezOPKi7U3E+4JMCbgpJiu6H8fS9GMbWspge 38 | 8gXuW4mH3pQPY5b0WFMF5jPYCd0Bk5cHl/E1NrAQwOVDNsu/U3Xkt9nm963NOcbs 39 | WOI094Lt5PQJU8gnI3nC7aZuM12yKBqg0W+/FQCr6CfI3Bm+xq6hNuKdrUTZ+4wo 40 | IWLOMg/XYYLgmozKaE1UTXKMBOJLg1i5rxmCvwaUz7sQ6gPloNLkQpOqmHpkwoYc 41 | b95K6YaSmWu5Ag0EXxEMVAEQAKPc3X8q3rTlLJJ3aWHT2oH/IMrkp+oAHiEEytpX 42 | lrRxnQl5YTYTEmGRBni4Z8jif8ntohqFuzEsMKx2wyws6BlKsVCyTEGYXW6wgOB2 43 | /oqQ9jF64xhmpbiG6ep3psZ+nsxV9Utb+eqJH9f9Nz2I3TunKXeP2BN2I/3fC2iD 44 | X0ft9AJfl1hMTM9TgaCFaNm4Z1pdKRbcjE/EECzyBKpj/0HonB6k5W9/TUXUYEXH 45 | iDq1trV1zbHq6ZnRmBmLaT4eBaceEMPgvdgdjx8WAYhJUOrlRiul5SvlfBsT+4XS 46 | a6ZqAqD/206qweFDEPfU6sC0go/tVI/zgvT2CX16iNXH+8WJ+Z7xRXhDxbrhmNYM 47 | vJRyQ+ZVAH1DQuXfblTqfOSyi8tbhZqro8v76himQ8hcPzKv4YVTW2poBe/MSFwf 48 | 0Xm91cs5q8mh/UlG9Gz3/SxEUQWGoOkvkD1X87tc+ScM8K62CsPXx2ZqYLK36Upq 49 | aNdNX+Uni8pIEknneNkag7b/XaaGl6nfvTWh2DCdXAWJJ9S4FMFIlRgyUq+cL/pu 50 | gjiPUNdWAJYN76Nmpg5iMC4s7nZ8juSmzXbnphute/SViVEBHB3PU/jdzoCCR/XJ 51 | T8bxiVqfz79vukIyfZySr2qq6OA8sS6rJPiNgN4ki0dH8OGWrss58gqcydJav2Ac 52 | 1Vu1ABEBAAGJAjYEGAEIACAWIQQnxQ5/WQlH1yc6dB6FGUwIQhmAyQUCXxEMVAIb 53 | DAAKCRCFGUwIQhmAybgxEACLZucejgXVsAzpOoSlKNi+71cg5hhR0EaPqlgJeYp8 54 | SeBP9otn7z2qfZTOdYBVhsbZJnoH+M/qMlgfSjZMc+SsyKNsrqcZH8VNFOEGcGG1 55 | kanK3V81/eBC2I4jdZ/BfFUaJuARiiH/9kn/UX1LYh/aYmFu1EF/CZrkB6JBKsqg 56 | JHZL345zAvzJUxZ+9rM2FMSkhrDNNmWnGutfAa1e8oJyvVWTJEkJhz+60iIU83Wb 57 | 99tp0F372+CyMg8EYh2KT9eIwLZOuhUXVDkjKO5WIQ0vN+feMMclx9BBre5il4GW 58 | 552WBMgNEhYGwYdMTnPB6r3H+k6KeJxv5MGJtmMe+iblKyGantOXtVMjog3vmXDp 59 | 5TaG5FUy5IQJWPynRsMxSML6qyZwKr+OtRGvgz/QTZMZhzj0OKrWhpPSQZEPSlIX 60 | 9ZqM2vu9/jdT5jzrqkNShs4jXBImoRFIMu0IT9RrrFx3W1iUlcDilsuWZPH8vGX5 61 | 704Q1Wqt7WQ1L6Fqy2UJjVamelPedIK42kEWdir/jSW4JWvffN6UA7E0LtcGFs67 62 | DJx55D+5IvTLv0V8C+/pfEGb8T2A6AoftED97eQvWAJQZyFikeYr+HaHFFuwc9wG 63 | jUNSbfkPObp54cTsdQlw3GVaDmKm5a3a5YZ7EGskjO2IrIm3jDNidzA1Y7mINDy5 64 | Y7kBDQRfEQ3IAQgA2EXKY6Oke+xrkLWw2/nL6aeAp3qo/Gn8MRy8XXRkgT91aHP6 65 | q8KHF2JoiGrb7xWzm3iRHbcMJbS+NnGWrH+cGHzDynReoPyO0SGVCDBSLKIFJdnk 66 | l08tHRkp8iMOdDomF+e8Uq5SSTJq9is3b4/6BO5ycBwETYJAs6bEtkOcSY9i0EQI 67 | T53LxfhVLbsTQbdGhNpN+Ao9Q3Z3TXXNZX96e0JgJMv6FJGL2v8UGF1oiSz9Rhpv 68 | 198/n5TGcEd+hZ6KNBP7lGmHxivlDZpzO+FoKTeePdVLHB6d4zRUmEipE2+QVBo3 69 | XGZmVgDEs31TwaO4vDecz2tUQAY9TUEX+REpmQARAQABiQI2BBgBCAAgFiEEJ8UO 70 | f1kJR9cnOnQehRlMCEIZgMkFAl8RDcgCGyAACgkQhRlMCEIZgMlGqw/+Mm7ty3eH 71 | mS/HurpKCF0B7ds1jnUOfQzf3k9KRUDrIdpSpg6DTIJ5CAkk2NiN5qW6UfISvtPO 72 | qzxne1llBrbrfMLqXYH/9Pmuk/ObvLVQu2ha5vQhEsy5XWohH6PzqtP/tMuP2oiq 73 | M2qPH0U3cNsM8rYLMpEl07n9+q5yggaOUnoyRJH6y5xZISGi34+X+WMOmo1ZFP2r 74 | suvTl/K84ov7TPQdENSFTPjLuo6oTbr9VX/NjXXiYPbmyBiV2fUaHRB98wzhL7SG 75 | bqwmWXLcQQjlD4RN2E8H4JajuWFnlTHhnd8Sc6iYYg4ckRzaMlpxEs69YPkiZfN+ 76 | jSEe7S33ELwP6Hu4xwFs8I88t7YoVHnIR/S4pS1MxCkDzwSrEq/b3jynFVlhbYKZ 77 | ZwbPXb1kh0T5frErOScNyUvqvQn/Pg8pgLDOLz5bXO87pzhWe9rk8hiCVeMx5doF 78 | dLWvorwxvHL7MdsVjR0Z/RG+VslQI2leJDzroB+f6Fr+SPxAq5pvD/JtVMzJq7+G 79 | OTIk4hqDZbEVQCSgiRjNLw8nMgrpkPDk5pRTuPpMR48OhP35azMq9GvzNpTXxKQs 80 | /e8u4XkwjKviGmUrgiOAyBlUMWsF9IBRKm5B/STohCT4ZeU4VJdlzB7JHwrr7CJd 81 | fqxMjx0bDHkiDsZTgmEDJnz6+jK0DmvsFmU= 82 | =wC+d 83 | -----END PGP PUBLIC KEY BLOCK----- 84 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -r test-requirements.txt 3 | 4 | # For additional local testing/linting - to be added elsewhere eventually. 5 | ruff 6 | shellcheck 7 | pytest-icdiff 8 | # pytest-profiling 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gitdb>=4.0.1,<5 2 | typing-extensions>=3.10.0.2;python_version<"3.10" 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from pathlib import Path 5 | import sys 6 | from typing import Sequence 7 | 8 | from setuptools import setup, find_packages 9 | from setuptools.command.build_py import build_py as _build_py 10 | from setuptools.command.sdist import sdist as _sdist 11 | 12 | 13 | def _read_content(path: str) -> str: 14 | return (Path(__file__).parent / path).read_text(encoding="utf-8") 15 | 16 | 17 | version = _read_content("VERSION").strip() 18 | requirements = _read_content("requirements.txt").splitlines() 19 | test_requirements = _read_content("test-requirements.txt").splitlines() 20 | doc_requirements = _read_content("doc/requirements.txt").splitlines() 21 | long_description = _read_content("README.md") 22 | 23 | 24 | class build_py(_build_py): 25 | def run(self) -> None: 26 | init = os.path.join(self.build_lib, "git", "__init__.py") 27 | if os.path.exists(init): 28 | os.unlink(init) 29 | _build_py.run(self) 30 | _stamp_version(init) 31 | self.byte_compile([init]) 32 | 33 | 34 | class sdist(_sdist): 35 | def make_release_tree(self, base_dir: str, files: Sequence) -> None: 36 | _sdist.make_release_tree(self, base_dir, files) 37 | orig = os.path.join("git", "__init__.py") 38 | assert os.path.exists(orig), orig 39 | dest = os.path.join(base_dir, orig) 40 | if hasattr(os, "link") and os.path.exists(dest): 41 | os.unlink(dest) 42 | self.copy_file(orig, dest) 43 | _stamp_version(dest) 44 | 45 | 46 | def _stamp_version(filename: str) -> None: 47 | found, out = False, [] 48 | try: 49 | with open(filename) as f: 50 | for line in f: 51 | if "__version__ =" in line: 52 | line = line.replace('"git"', "'%s'" % version) 53 | found = True 54 | out.append(line) 55 | except OSError: 56 | print("Couldn't find file %s to stamp version" % filename, file=sys.stderr) 57 | 58 | if found: 59 | with open(filename, "w") as f: 60 | f.writelines(out) 61 | else: 62 | print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr) 63 | 64 | 65 | setup( 66 | name="GitPython", 67 | cmdclass={"build_py": build_py, "sdist": sdist}, 68 | version=version, 69 | description="GitPython is a Python library used to interact with Git repositories", 70 | author="Sebastian Thiel, Michael Trier", 71 | author_email="byronimo@gmail.com, mtrier@gmail.com", 72 | license="BSD-3-Clause", 73 | url="https://github.com/gitpython-developers/GitPython", 74 | packages=find_packages(exclude=["test", "test.*"]), 75 | include_package_data=True, 76 | package_dir={"git": "git"}, 77 | python_requires=">=3.7", 78 | install_requires=requirements, 79 | extras_require={ 80 | "test": test_requirements, 81 | "doc": doc_requirements, 82 | }, 83 | zip_safe=False, 84 | long_description=long_description, 85 | long_description_content_type="text/markdown", 86 | classifiers=[ 87 | # Picked from 88 | # http://pypi.python.org/pypi?:action=list_classifiers 89 | # "Development Status :: 1 - Planning", 90 | # "Development Status :: 2 - Pre-Alpha", 91 | # "Development Status :: 3 - Alpha", 92 | # "Development Status :: 4 - Beta", 93 | "Development Status :: 5 - Production/Stable", 94 | # "Development Status :: 6 - Mature", 95 | # "Development Status :: 7 - Inactive", 96 | "Environment :: Console", 97 | "Intended Audience :: Developers", 98 | "License :: OSI Approved :: BSD License", 99 | "Operating System :: OS Independent", 100 | "Operating System :: POSIX", 101 | "Operating System :: Microsoft :: Windows", 102 | "Operating System :: MacOS :: MacOS X", 103 | "Typing :: Typed", 104 | "Programming Language :: Python", 105 | "Programming Language :: Python :: 3", 106 | "Programming Language :: Python :: 3.7", 107 | "Programming Language :: Python :: 3.8", 108 | "Programming Language :: Python :: 3.9", 109 | "Programming Language :: Python :: 3.10", 110 | "Programming Language :: Python :: 3.11", 111 | "Programming Language :: Python :: 3.12", 112 | ], 113 | ) 114 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage[toml] 2 | ddt >= 1.1.1, != 1.4.3 3 | mock ; python_version < "3.8" 4 | mypy 5 | pre-commit 6 | pytest >= 7.3.1 7 | pytest-cov 8 | pytest-instafail 9 | pytest-mock 10 | pytest-sugar 11 | typing-extensions ; python_version < "3.11" 12 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | -------------------------------------------------------------------------------- /test/deprecation/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Tests of deprecation warnings and possible related attribute bugs. 5 | 6 | Most deprecation warnings are "basic" in the sense that there is no special complexity 7 | to consider, in introducing them. However, to issue deprecation warnings on mere 8 | attribute access can involve adding new dynamic behavior. This can lead to subtle bugs 9 | or less useful dynamic metadata. It can also weaken static typing, as happens if a type 10 | checker sees a method like ``__getattr__`` in a module or class whose attributes it did 11 | not already judge to be dynamic. This test.deprecation submodule covers all three cases: 12 | the basic cases, subtle dynamic behavior, and subtle static type checking issues. 13 | 14 | Static type checking is "tested" by a combination of code that should not be treated as 15 | a type error but would be in the presence of particular bugs, and code that *should* be 16 | treated as a type error and is accordingly marked ``# type: ignore[REASON]`` (for 17 | specific ``REASON``. The latter will only produce mypy errors when the expectation is 18 | not met if it is configured with ``warn_unused_ignores = true``. 19 | """ 20 | -------------------------------------------------------------------------------- /test/deprecation/lib.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Support library for deprecation tests.""" 5 | 6 | __all__ = ["assert_no_deprecation_warning", "suppress_deprecation_warning"] 7 | 8 | import contextlib 9 | import warnings 10 | 11 | from typing import Generator 12 | 13 | 14 | @contextlib.contextmanager 15 | def assert_no_deprecation_warning() -> Generator[None, None, None]: 16 | """Context manager to assert that code does not issue any deprecation warnings.""" 17 | with warnings.catch_warnings(): 18 | warnings.simplefilter("error", DeprecationWarning) 19 | warnings.simplefilter("error", PendingDeprecationWarning) 20 | yield 21 | 22 | 23 | @contextlib.contextmanager 24 | def suppress_deprecation_warning() -> Generator[None, None, None]: 25 | with warnings.catch_warnings(): 26 | warnings.simplefilter("ignore", DeprecationWarning) 27 | yield 28 | -------------------------------------------------------------------------------- /test/deprecation/test_basic.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Tests of assorted deprecation warnings when there are no extra subtleties to check. 5 | 6 | This tests deprecation warnings where all that needs be verified is that a deprecated 7 | property, function, or class issues a DeprecationWarning when used and, if applicable, 8 | that recommended alternatives do not issue the warning. 9 | 10 | This is in contrast to other modules within test.deprecation, which test warnings where 11 | there is a risk of breaking other runtime behavior, or of breaking static type checking 12 | or making it less useful, by introducing the warning or in plausible future changes to 13 | how the warning is implemented. That happens when it is necessary to customize attribute 14 | access on a module or class, in a way it was not customized before, to issue a warning. 15 | It is inapplicable to the deprecations whose warnings are tested in this module. 16 | """ 17 | 18 | import pytest 19 | 20 | from git.diff import NULL_TREE 21 | from git.objects.util import Traversable 22 | from git.repo import Repo 23 | from git.util import Iterable as _Iterable, IterableObj 24 | 25 | from .lib import assert_no_deprecation_warning 26 | 27 | # typing ----------------------------------------------------------------- 28 | 29 | from typing import Generator, TYPE_CHECKING 30 | 31 | if TYPE_CHECKING: 32 | from pathlib import Path 33 | 34 | from git.diff import Diff, DiffIndex 35 | from git.objects.commit import Commit 36 | 37 | # ------------------------------------------------------------------------ 38 | 39 | 40 | @pytest.fixture 41 | def commit(tmp_path: "Path") -> Generator["Commit", None, None]: 42 | """Fixture to supply a one-commit repo's commit, enough for deprecation tests.""" 43 | (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8") 44 | repo = Repo.init(tmp_path) 45 | repo.index.add(["a.txt"]) 46 | yield repo.index.commit("Initial commit") 47 | repo.close() 48 | 49 | 50 | @pytest.fixture 51 | def diff(commit: "Commit") -> Generator["Diff", None, None]: 52 | """Fixture to supply a single-file diff.""" 53 | (diff,) = commit.diff(NULL_TREE) # Exactly one file in the diff. 54 | yield diff 55 | 56 | 57 | @pytest.fixture 58 | def diffs(commit: "Commit") -> Generator["DiffIndex", None, None]: 59 | """Fixture to supply a DiffIndex.""" 60 | yield commit.diff(NULL_TREE) 61 | 62 | 63 | def test_diff_renamed_warns(diff: "Diff") -> None: 64 | """The deprecated Diff.renamed property issues a deprecation warning.""" 65 | with pytest.deprecated_call(): 66 | diff.renamed 67 | 68 | 69 | def test_diff_renamed_file_does_not_warn(diff: "Diff") -> None: 70 | """The preferred Diff.renamed_file property issues no deprecation warning.""" 71 | with assert_no_deprecation_warning(): 72 | diff.renamed_file 73 | 74 | 75 | def test_commit_trailers_warns(commit: "Commit") -> None: 76 | """The deprecated Commit.trailers property issues a deprecation warning.""" 77 | with pytest.deprecated_call(): 78 | commit.trailers 79 | 80 | 81 | def test_commit_trailers_list_does_not_warn(commit: "Commit") -> None: 82 | """The nondeprecated Commit.trailers_list property issues no deprecation warning.""" 83 | with assert_no_deprecation_warning(): 84 | commit.trailers_list 85 | 86 | 87 | def test_commit_trailers_dict_does_not_warn(commit: "Commit") -> None: 88 | """The nondeprecated Commit.trailers_dict property issues no deprecation warning.""" 89 | with assert_no_deprecation_warning(): 90 | commit.trailers_dict 91 | 92 | 93 | def test_traverse_list_traverse_in_base_class_warns(commit: "Commit") -> None: 94 | """Traversable.list_traverse's base implementation issues a deprecation warning.""" 95 | with pytest.deprecated_call(): 96 | Traversable.list_traverse(commit) 97 | 98 | 99 | def test_traversable_list_traverse_override_does_not_warn(commit: "Commit") -> None: 100 | """Calling list_traverse on concrete subclasses is not deprecated, does not warn.""" 101 | with assert_no_deprecation_warning(): 102 | commit.list_traverse() 103 | 104 | 105 | def test_traverse_traverse_in_base_class_warns(commit: "Commit") -> None: 106 | """Traversable.traverse's base implementation issues a deprecation warning.""" 107 | with pytest.deprecated_call(): 108 | Traversable.traverse(commit) 109 | 110 | 111 | def test_traverse_traverse_override_does_not_warn(commit: "Commit") -> None: 112 | """Calling traverse on concrete subclasses is not deprecated, does not warn.""" 113 | with assert_no_deprecation_warning(): 114 | commit.traverse() 115 | 116 | 117 | def test_iterable_inheriting_warns() -> None: 118 | """Subclassing the deprecated git.util.Iterable issues a deprecation warning.""" 119 | with pytest.deprecated_call(): 120 | 121 | class Derived(_Iterable): 122 | pass 123 | 124 | 125 | def test_iterable_obj_inheriting_does_not_warn() -> None: 126 | """Subclassing git.util.IterableObj is not deprecated, does not warn.""" 127 | with assert_no_deprecation_warning(): 128 | 129 | class Derived(IterableObj): 130 | pass 131 | 132 | 133 | def test_diff_iter_change_type(diffs: "DiffIndex") -> None: 134 | """The internal DiffIndex.iter_change_type function issues no deprecation warning.""" 135 | with assert_no_deprecation_warning(): 136 | for change_type in diffs.change_type: 137 | [*diffs.iter_change_type(change_type=change_type)] 138 | -------------------------------------------------------------------------------- /test/deprecation/test_compat.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Tests for dynamic and static characteristics of git.compat module attributes. 5 | 6 | These tests verify that the is_ attributes are available, and are even listed 7 | in the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized) 8 | attribute access is still an error both at runtime and with mypy. This is similar to 9 | some of the tests in test_toplevel, but the situation being tested here is simpler 10 | because it does not involve unintuitive module aliasing or import behavior. So this only 11 | tests attribute access, not "from" imports (whose behavior can be intuitively inferred). 12 | """ 13 | 14 | import os 15 | import sys 16 | 17 | if sys.version_info >= (3, 11): 18 | from typing import assert_type 19 | else: 20 | from typing_extensions import assert_type 21 | 22 | import pytest 23 | 24 | import git.compat 25 | 26 | _MESSAGE_LEADER = "{} and other is_ aliases are deprecated." 27 | """Form taken by the beginning of the warnings issued for is_ access.""" 28 | 29 | 30 | def test_cannot_access_undefined() -> None: 31 | """Accessing a bogus attribute in git.compat remains a dynamic and static error.""" 32 | with pytest.raises(AttributeError): 33 | git.compat.foo # type: ignore[attr-defined] 34 | 35 | 36 | def test_is_platform() -> None: 37 | """The is_ attributes work, warn, and mypy accepts code accessing them.""" 38 | fully_qualified_names = [ 39 | "git.compat.is_win", 40 | "git.compat.is_posix", 41 | "git.compat.is_darwin", 42 | ] 43 | 44 | with pytest.deprecated_call() as ctx: 45 | is_win = git.compat.is_win 46 | is_posix = git.compat.is_posix 47 | is_darwin = git.compat.is_darwin 48 | 49 | assert_type(is_win, bool) 50 | assert_type(is_posix, bool) 51 | assert_type(is_darwin, bool) 52 | 53 | messages = [str(entry.message) for entry in ctx] 54 | assert len(messages) == 3 55 | 56 | for fullname, message in zip(fully_qualified_names, messages): 57 | assert message.startswith(_MESSAGE_LEADER.format(fullname)) 58 | 59 | # These assertions exactly reproduce the expressions in the code under test, so they 60 | # are not good for testing that the values are correct. Instead, their purpose is to 61 | # ensure that any dynamic machinery put in place in git.compat to cause warnings to 62 | # be issued does not get in the way of the intended values being accessed. 63 | assert is_win == (os.name == "nt") 64 | assert is_posix == (os.name == "posix") 65 | assert is_darwin == (sys.platform == "darwin") 66 | 67 | 68 | def test_dir() -> None: 69 | """dir() on git.compat includes all public attributes, even if deprecated. 70 | 71 | As dir() usually does, it also has nonpublic attributes, which should also not be 72 | removed by a custom __dir__ function, but those are less important to test. 73 | """ 74 | expected_subset = { 75 | "is_win", 76 | "is_posix", 77 | "is_darwin", 78 | "defenc", 79 | "safe_decode", 80 | "safe_encode", 81 | "win_encode", 82 | } 83 | actual = set(dir(git.compat)) 84 | assert expected_subset <= actual 85 | -------------------------------------------------------------------------------- /test/deprecation/test_types.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Tests for dynamic and static characteristics of git.types module attributes.""" 5 | 6 | import sys 7 | 8 | if sys.version_info >= (3, 8): 9 | from typing import Literal 10 | else: 11 | from typing_extensions import Literal 12 | 13 | import pytest 14 | 15 | import git.types 16 | 17 | 18 | def test_cannot_access_undefined() -> None: 19 | """Accessing a bogus attribute in git.types remains a dynamic and static error.""" 20 | with pytest.raises(AttributeError): 21 | git.types.foo # type: ignore[attr-defined] 22 | 23 | 24 | def test_can_access_lit_commit_ish_but_it_is_not_usable() -> None: 25 | """Lit_commit_ish_can be accessed, but warns and is an invalid type annotation.""" 26 | # It would be fine to test attribute access rather than a "from" import. But a 27 | # "from" import is more likely to appear in actual usage, so it is used here. 28 | with pytest.deprecated_call() as ctx: 29 | from git.types import Lit_commit_ish 30 | 31 | # As noted in test_toplevel.test_util_alias_import, there may be multiple warnings, 32 | # but all with the same message. 33 | (message,) = {str(entry.message) for entry in ctx} 34 | assert "Lit_commit_ish is deprecated." in message 35 | assert 'Literal["commit", "tag", "blob", "tree"]' in message, "Has old definition." 36 | assert 'Literal["commit", "tag"]' in message, "Has new definition." 37 | assert "GitObjectTypeString" in message, "Has new type name for old definition." 38 | 39 | _: Lit_commit_ish = "commit" # type: ignore[valid-type] 40 | 41 | # It should be as documented (even though deliberately unusable in static checks). 42 | assert Lit_commit_ish == Literal["commit", "tag"] 43 | 44 | 45 | def test_dir() -> None: 46 | """dir() on git.types includes public names, even ``Lit_commit_ish``. 47 | 48 | It also contains private names that we don't test. See test_compat.test_dir. 49 | """ 50 | expected_subset = { 51 | "PathLike", 52 | "TBD", 53 | "AnyGitObject", 54 | "Tree_ish", 55 | "Commit_ish", 56 | "GitObjectTypeString", 57 | "Lit_commit_ish", 58 | "Lit_config_levels", 59 | "ConfigLevels_Tup", 60 | "CallableProgress", 61 | "assert_never", 62 | "Files_TD", 63 | "Total_TD", 64 | "HSH_TD", 65 | "Has_Repo", 66 | "Has_id_attribute", 67 | } 68 | actual = set(dir(git.types)) 69 | assert expected_subset <= actual 70 | -------------------------------------------------------------------------------- /test/fixtures/.gitconfig: -------------------------------------------------------------------------------- 1 | [alias] 2 | rbi = "!g() { git rebase -i origin/${1:-master} ; } ; g" 3 | expush = "!f() { git branch -f tmp ; { git rbi $1 && git push ; } ; git reset --hard tmp ; git rebase origin/${1:-master}; } ; f" -------------------------------------------------------------------------------- /test/fixtures/blame: -------------------------------------------------------------------------------- 1 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 2 | author Tom Preston-Werner 3 | author-mail 4 | author-time 1191997100 5 | author-tz -0700 6 | committer Tom Preston-Werner 7 | committer-mail 8 | committer-time 1191997100 9 | committer-tz -0700 10 | filename lib/grit.rb 11 | summary initial grit setup 12 | boundary 13 | $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed 14 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 15 | 16 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 3 3 17 | # core 18 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 4 4 19 | 20 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 5 5 21 | # stdlib 22 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 6 6 23 | 24 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 7 7 25 | # internal requires 26 | 3b1930208a82457747d76729ae088e90edca4673 8 8 1 27 | author Tom Preston-Werner 28 | author-mail 29 | author-time 1192267241 30 | author-tz -0700 31 | committer Tom Preston-Werner 32 | committer-mail 33 | committer-time 1192267241 34 | committer-tz -0700 35 | filename lib/grit.rb 36 | summary big refactor to do lazy loading 37 | require 'grit/lazy' 38 | 4c8124ffcf4039d292442eeccabdeca5af5c5017 8 9 1 39 | author Tom Preston-Werner 40 | author-mail 41 | author-time 1191999972 42 | author-tz -0700 43 | committer Tom Preston-Werner 44 | committer-mail 45 | committer-time 1191999972 46 | committer-tz -0700 47 | filename lib/grit.rb 48 | summary implement Grit#heads 49 | require 'grit/errors' 50 | d01a4cfad6ea50285c4710243e3cbe019d381eba 9 10 1 51 | author Tom Preston-Werner 52 | author-mail 53 | author-time 1192032303 54 | author-tz -0700 55 | committer Tom Preston-Werner 56 | committer-mail 57 | committer-time 1192032303 58 | committer-tz -0700 59 | filename lib/grit.rb 60 | summary convert to Grit module, refactor to be more OO 61 | require 'grit/git' 62 | 4c8124ffcf4039d292442eeccabdeca5af5c5017 9 11 1 63 | require 'grit/head' 64 | a47fd41f3aa4610ea527dcc1669dfdb9c15c5425 10 12 1 65 | author Tom Preston-Werner 66 | author-mail 67 | author-time 1192002639 68 | author-tz -0700 69 | committer Tom Preston-Werner 70 | committer-mail 71 | committer-time 1192002639 72 | committer-tz -0700 73 | filename lib/grit.rb 74 | summary add more comments throughout 75 | require 'grit/commit' 76 | b17b974691f0a26f26908495d24d9c4c718920f8 13 13 1 77 | author Tom Preston-Werner 78 | author-mail 79 | author-time 1192271832 80 | author-tz -0700 81 | committer Tom Preston-Werner 82 | committer-mail 83 | committer-time 1192271832 84 | committer-tz -0700 85 | filename lib/grit.rb 86 | summary started implementing Tree 87 | require 'grit/tree' 88 | 74fd66519e983a0f29e16a342a6059dbffe36020 14 14 1 89 | author Tom Preston-Werner 90 | author-mail 91 | author-time 1192317005 92 | author-tz -0700 93 | committer Tom Preston-Werner 94 | committer-mail 95 | committer-time 1192317005 96 | committer-tz -0700 97 | filename lib/grit.rb 98 | summary add Blob 99 | require 'grit/blob' 100 | d01a4cfad6ea50285c4710243e3cbe019d381eba 12 15 1 101 | require 'grit/repo' 102 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 9 16 1 103 | 104 | d01a4cfad6ea50285c4710243e3cbe019d381eba 14 17 1 105 | module Grit 106 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 18 18 6 107 | author Tom Preston-Werner 108 | author-mail 109 | author-time 1192860483 110 | author-tz -0700 111 | committer Tom Preston-Werner 112 | committer-mail 113 | committer-time 1192860483 114 | committer-tz -0700 115 | filename lib/grit.rb 116 | summary implement Repo.init_bare 117 | class << self 118 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 19 19 119 | attr_accessor :debug 120 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 20 20 121 | end 122 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 21 21 123 | 124 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 22 22 125 | self.debug = false 126 | b6e1b765e0c15586a2c5b9832854f95defd71e1f 23 23 127 | 128 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 11 24 2 129 | VERSION = '1.0.0' 130 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 12 25 131 | end -------------------------------------------------------------------------------- /test/fixtures/blame_binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/test/fixtures/blame_binary -------------------------------------------------------------------------------- /test/fixtures/blame_incremental: -------------------------------------------------------------------------------- 1 | 82b8902e033430000481eb355733cd7065342037 2 2 1 2 | author Sebastian Thiel 3 | author-mail 4 | author-time 1270634931 5 | author-tz +0200 6 | committer Sebastian Thiel 7 | committer-mail 8 | committer-time 1270634931 9 | committer-tz +0200 10 | summary Used this release for a first beta of the 0.2 branch of development 11 | previous 501bf602abea7d21c3dbb409b435976e92033145 AUTHORS 12 | filename AUTHORS 13 | 82b8902e033430000481eb355733cd7065342037 14 14 1 14 | filename AUTHORS 15 | c76852d0bff115720af3f27acdb084c59361e5f6 1 1 1 16 | author Michael Trier 17 | author-mail 18 | author-time 1232829627 19 | author-tz -0500 20 | committer Michael Trier 21 | committer-mail 22 | committer-time 1232829627 23 | committer-tz -0500 24 | summary Lots of spring cleaning and added in Sphinx documentation. 25 | previous bcd57e349c08bd7f076f8d6d2f39b702015358c1 AUTHORS 26 | filename AUTHORS 27 | c76852d0bff115720af3f27acdb084c59361e5f6 2 3 11 28 | filename AUTHORS 29 | c76852d0bff115720af3f27acdb084c59361e5f6 13 15 2 30 | filename AUTHORS 31 | -------------------------------------------------------------------------------- /test/fixtures/blame_incremental_2.11.1_plus: -------------------------------------------------------------------------------- 1 | 82b8902e033430000481eb355733cd7065342037 2 2 1 2 | author Sebastian Thiel 3 | author-mail 4 | author-time 1270634931 5 | author-tz +0200 6 | committer Sebastian Thiel 7 | committer-mail 8 | committer-time 1270634931 9 | committer-tz +0200 10 | summary Used this release for a first beta of the 0.2 branch of development 11 | previous 501bf602abea7d21c3dbb409b435976e92033145 AUTHORS 12 | filename AUTHORS 13 | 82b8902e033430000481eb355733cd7065342037 14 14 1 14 | previous 501bf602abea7d21c3dbb409b435976e92033145 AUTHORS 15 | filename AUTHORS 16 | c76852d0bff115720af3f27acdb084c59361e5f6 1 1 1 17 | author Michael Trier 18 | author-mail 19 | author-time 1232829627 20 | author-tz -0500 21 | committer Michael Trier 22 | committer-mail 23 | committer-time 1232829627 24 | committer-tz -0500 25 | summary Lots of spring cleaning and added in Sphinx documentation. 26 | previous bcd57e349c08bd7f076f8d6d2f39b702015358c1 AUTHORS 27 | filename AUTHORS 28 | c76852d0bff115720af3f27acdb084c59361e5f6 2 3 11 29 | previous bcd57e349c08bd7f076f8d6d2f39b702015358c1 AUTHORS 30 | filename AUTHORS 31 | c76852d0bff115720af3f27acdb084c59361e5f6 13 15 2 32 | previous bcd57e349c08bd7f076f8d6d2f39b702015358c1 AUTHORS 33 | filename AUTHORS 34 | -------------------------------------------------------------------------------- /test/fixtures/cat_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | with open(sys.argv[1]) as fd: 4 | for line in fd.readlines(): 5 | sys.stdout.write(line) 6 | sys.stderr.write(line) 7 | -------------------------------------------------------------------------------- /test/fixtures/cat_file_blob: -------------------------------------------------------------------------------- 1 | Hello world -------------------------------------------------------------------------------- /test/fixtures/cat_file_blob_nl: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /test/fixtures/cat_file_blob_size: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /test/fixtures/commit_invalid_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/test/fixtures/commit_invalid_data -------------------------------------------------------------------------------- /test/fixtures/commit_with_gpgsig: -------------------------------------------------------------------------------- 1 | tree cefbccb4843d821183ae195e70a17c9938318945 2 | parent 904435cf76a9bdd5eb41b1c4e049d5a64f3a8400 3 | author Jon Mason 1367013117 -0700 4 | committer Jon Mason 1368640702 -0700 5 | gpgsig -----BEGIN PGP SIGNATURE----- 6 | Version: GnuPG v1.4.11 (GNU/Linux) 7 | 8 | iQIcBAABAgAGBQJRk8zMAAoJEG5mS6x6i9IjsTEP/0v2Wx/i7dqyKban6XMIhVdj 9 | uI0DycfXqnCCZmejidzeao+P+cuK/ZAA/b9fU4MtwkDm2USvnIOrB00W0isxsrED 10 | sdv6uJNa2ybGjxBolLrfQcWutxGXLZ1FGRhEvkPTLMHHvVriKoNFXcS7ewxP9MBf 11 | NH97K2wauqA+J4BDLDHQJgADCOmLrGTAU+G1eAXHIschDqa6PZMH5nInetYZONDh 12 | 3SkOOv8VKFIF7gu8X7HC+7+Y8k8U0TW0cjlQ2icinwCc+KFoG6GwXS7u/VqIo1Yp 13 | Tack6sxIdK7NXJhV5gAeAOMJBGhO0fHl8UUr96vGEKwtxyZhWf8cuIPOWLk06jA0 14 | g9DpLqmy/pvyRfiPci+24YdYRBua/vta+yo/Lp85N7Hu/cpIh+q5WSLvUlv09Dmo 15 | TTTG8Hf6s3lEej7W8z2xcNZoB6GwXd8buSDU8cu0I6mEO9sNtAuUOHp2dBvTA6cX 16 | PuQW8jg3zofnx7CyNcd3KF3nh2z8mBcDLgh0Q84srZJCPRuxRcp9ylggvAG7iaNd 17 | XMNvSK8IZtWLkx7k3A3QYt1cN4y1zdSHLR2S+BVCEJea1mvUE+jK5wiB9S4XNtKm 18 | BX/otlTa8pNE3fWYBxURvfHnMY4i3HQT7Bc1QjImAhMnyo2vJk4ORBJIZ1FTNIhJ 19 | JzJMZDRLQLFvnzqZuCjE 20 | =przd 21 | -----END PGP SIGNATURE----- 22 | 23 | NTB: Multiple NTB client fix 24 | 25 | Fix issue with adding multiple ntb client devices to the ntb virtual 26 | bus. Previously, multiple devices would be added with the same name, 27 | resulting in crashes. To get around this issue, add a unique number to 28 | the device when it is added. 29 | 30 | Signed-off-by: Jon Mason 31 | -------------------------------------------------------------------------------- /test/fixtures/diff_2: -------------------------------------------------------------------------------- 1 | diff --git a/lib/grit/commit.rb b/lib/grit/commit.rb 2 | index a093bb1db8e884cccf396b297259181d1caebed4..80fd3d527f269ecbd570b65b8e21fd85baedb6e9 100644 3 | --- a/lib/grit/com mit.rb 4 | +++ b/lib/grit/com mit.rb 5 | @@ -156,12 +156,8 @@ module Grit 6 | 7 | def diffs 8 | if parents.empty? 9 | - diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id) 10 | - if diff =~ /diff --git a/ 11 | - diff = diff.sub(/.+?(diff --git a)/m, '\1') 12 | - else 13 | - diff = '' 14 | - end 15 | + diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id) 16 | + diff = diff.sub(/.+?(diff --git a)/m, '\1') 17 | Diff.list_from_string(@repo, diff) 18 | else 19 | self.class.diff(@repo, parents.first.id, @id) 20 | diff --git a/test/fixtures/show_empty_commit b/test/fixtures/show_empty_commit 21 | deleted file mode 100644 22 | index ea25e32a409fdf74c1b9268820108d1c16dcc553..0000000000000000000000000000000000000000 23 | --- a/test/fixtures/show_empty_commit 24 | +++ /dev/null 25 | @@ -1,6 +0,0 @@ 26 | -commit 1e3824339762bd48316fe87bfafc853732d43264 27 | -tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 28 | -author Tom Preston-Werner 1157392833 +0000 29 | -committer Tom Preston-Werner 1157392833 +0000 30 | - 31 | - initial directory structure 32 | diff --git a/test/test_commit.rb b/test/test_commit.rb 33 | index fdeb9000089b052f0b31a845e0173e9b089e06a0..bdbc450e08084d7d611e985cfa12fb424cab29b2 100644 34 | --- a/test/test_commit.rb 35 | +++ b/test/test_commit.rb 36 | @@ -98,18 +98,6 @@ class TestCommit < Test::Unit::TestCase 37 | assert_equal true, diffs[5].new_file 38 | end 39 | 40 | - def test_diffs_on_initial_import_with_empty_commit 41 | - Git.any_instance.expects(:show).with( 42 | - {:full_index => true, :pretty => 'raw'}, 43 | - '634396b2f541a9f2d58b00be1a07f0c358b999b3' 44 | - ).returns(fixture('show_empty_commit')) 45 | - 46 | - @c = Commit.create(@r, :id => '634396b2f541a9f2d58b00be1a07f0c358b999b3') 47 | - diffs = @c.diffs 48 | - 49 | - assert_equal [], diffs 50 | - end 51 | - 52 | # to_s 53 | 54 | def test_to_s 55 | -------------------------------------------------------------------------------- /test/fixtures/diff_2f: -------------------------------------------------------------------------------- 1 | diff --git a/lib/grit/commit.rb b/lib/grit/commit.rb 2 | index a093bb1db8e884cccf396b297259181d1caebed4..80fd3d527f269ecbd570b65b8e21fd85baedb6e9 100644 3 | --- a/lib/grit/commit.rb 4 | +++ b/lib/grit/commit.rb 5 | @@ -156,12 +156,8 @@ module Grit 6 | 7 | def diffs 8 | if parents.empty? 9 | - diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id) 10 | - if diff =~ /diff --git a/ 11 | - diff = diff.sub(/.+?(diff --git a)/m, '\1') 12 | - else 13 | - diff = '' 14 | - end 15 | + diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id) 16 | + diff = diff.sub(/.+?(diff --git a)/m, '\1') 17 | Diff.list_from_string(@repo, diff) 18 | else 19 | self.class.diff(@repo, parents.first.id, @id) 20 | -------------------------------------------------------------------------------- /test/fixtures/diff_abbrev-40_full-index_M_raw_no-color: -------------------------------------------------------------------------------- 1 | :100644 100644 739bc65220ad90e9ebfa2d6af1723b97555569a4 0000000000000000000000000000000000000000 M README.md 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_change_in_type: -------------------------------------------------------------------------------- 1 | diff --git a/this b/this 2 | deleted file mode 100644 3 | index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 4 | diff --git a/this b/this 5 | new file mode 120000 6 | index 0000000000000000000000000000000000000000..42061c01a1c70097d1e4579f29a5adf40abdec95 7 | --- /dev/null 8 | +++ b/this 9 | @@ -0,0 +1 @@ 10 | +that 11 | -------------------------------------------------------------------------------- /test/fixtures/diff_change_in_type_raw: -------------------------------------------------------------------------------- 1 | :100644 120000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 42061c01a1c70097d1e4579f29a5adf40abdec95 T this 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_copied_mode: -------------------------------------------------------------------------------- 1 | diff --git a/test1.txt b/test2.txt 2 | similarity index 100% 3 | copy from test1.txt 4 | copy to test2.txt 5 | -------------------------------------------------------------------------------- /test/fixtures/diff_copied_mode_raw: -------------------------------------------------------------------------------- 1 | :100644 100644 cfe9deac6e10683917e80f877566b58644aa21df cfe9deac6e10683917e80f877566b58644aa21df C100 test1.txt test2.txt 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_f: -------------------------------------------------------------------------------- 1 | diff --git a/lib/grit/diff.rb b/lib/grit/diff.rb 2 | index 537955bb86a8ceaa19aea89e75ccbea5ce6f2698..00b0b4a67eca9242db5f8991e99625acd55f040c 100644 3 | --- a/lib/grit/diff.rb 4 | +++ b/lib/grit/diff.rb 5 | @@ -27,6 +27,10 @@ module Grit 6 | while !lines.empty? 7 | m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(\S+) b/(\S+)$}) 8 | 9 | + if lines.first =~ /^old mode/ 10 | + 2.times { lines.shift } 11 | + end 12 | + 13 | new_file = false 14 | deleted_file = false 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/diff_file_with_colon: -------------------------------------------------------------------------------- 1 | :100644 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 366226fb970ac0caa9d3f55967ab01334a548f60 Mfile with : colon.txt:160000 160000 e45fd0792ee9a987a4df26e3139f5c3b107f0092 03ab3a1d40c04d6a944299c21db61cf9ce30f6bb Mgit/ext/gitdb:100644 100644 c6c9b67a08348b5a2792a8caab5d1d25abffec9f 431173b0dd488a468992f53e852e9dc0d65ced13 Mtest/test_diff.py -------------------------------------------------------------------------------- /test/fixtures/diff_file_with_spaces: -------------------------------------------------------------------------------- 1 | diff --git a/file with spaces b/file with spaces 2 | new file mode 100644 3 | index 0000000000000000000000000000000000000000..75c620d7b0d3b0100415421a97f553c979d75174 4 | --- /dev/null 5 | +++ b/file with spaces 6 | @@ -0,0 +1 @@ 7 | +ohai 8 | -------------------------------------------------------------------------------- /test/fixtures/diff_index_patch: -------------------------------------------------------------------------------- 1 | diff --git a/etc/sublime-text/git-python.sublime-project b/etc/sublime-text/git-python.sublime-project 2 | index 3dab9f6562ecb0408d9ece8dd63cc4461d280113..9c99a2cff7dc918fbbb61cd57d5d98750a1ef6c5 100644 3 | --- a/etc/sublime-text/git-python.sublime-project 4 | +++ b/etc/sublime-text/git-python.sublime-project 5 | @@ -23,7 +23,7 @@ 6 | ] 7 | }, 8 | // GITDB 9 | - //////// 10 | + // //////// 11 | { 12 | "follow_symlinks": true, 13 | "path": "../../git/ext/gitdb", 14 | @@ -42,8 +42,8 @@ 15 | ".tox", 16 | ] 17 | }, 18 | - // // SMMAP 19 | - // //////// 20 | + // // // SMMAP 21 | + // // //////// 22 | { 23 | "follow_symlinks": true, 24 | "path": "../../git/ext/gitdb/gitdb/ext/smmap", 25 | diff --git a/git/diff.py b/git/diff.py 26 | index 24e47bad9d79534d3cf474fec4f79e6fef122bb1..c1ad532e0217e293906bcfef43c523d6a8e21568 100644 27 | --- a/git/diff.py 28 | +++ b/git/diff.py 29 | @@ -302,13 +302,21 @@ class Diff(object): 30 | diff_header = cls.re_header.match 31 | for diff in ('\n' + text).split('\ndiff --git')[1:]: 32 | header = diff_header(diff) 33 | - assert header is not None, "Failed to parse diff header from " % diff 34 | + assert header is not None, "Failed to parse diff header from '%s'" % diff 35 | 36 | a_path, b_path, similarity_index, rename_from, rename_to, \ 37 | old_mode, new_mode, new_file_mode, deleted_file_mode, \ 38 | a_blob_id, b_blob_id, b_mode = header.groups() 39 | new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) 40 | 41 | + # if a_path.startswith('a/'): 42 | + # a_path = a_path[2:] 43 | + # if b_path.startswith('b/'): 44 | + # b_path = b_path[2:] 45 | + 46 | + for item in (a_path, b_path, a_blob_id, b_blob_id, old_mode, deleted_file_mode, new_mode, new_file_mode, b_mode, new_file, deleted_file, rename_from, rename_to, diff[header.end():]): 47 | + print( "####") 48 | + print(item) 49 | index.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id, 50 | old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, 51 | new_file, deleted_file, rename_from, rename_to, diff[header.end():])) 52 | diff --git a/git/ext/gitdb b/git/ext/gitdb 53 | index f2233fbf40f3f69309ce5cc714e99fcbdcd33ec3..a88a777df3909a61be97f1a7b1194dad6de25702 160000 54 | --- a/git/ext/gitdb 55 | +++ b/git/ext/gitdb 56 | @@ -1 +1 @@ 57 | -Subproject commit f2233fbf40f3f69309ce5cc714e99fcbdcd33ec3 58 | +Subproject commit a88a777df3909a61be97f1a7b1194dad6de25702-dirty 59 | diff --git a/test/fixtures/diff_patch_binary b/test/fixtures/diff_patch_binary 60 | new file mode 100644 61 | index 0000000000000000000000000000000000000000..c92ccd6ebc92a871d38ad7cb8a48bcdb1a5dbc33 62 | --- /dev/null 63 | +++ b/test/fixtures/diff_patch_binary 64 | @@ -0,0 +1,3 @@ 65 | +diff --git a/rps b/rps 66 | +index f4567df37451b230b1381b1bc9c2bcad76e08a3c..736bd596a36924d30b480942e9475ce0d734fa0d 100755 67 | +Binary files a/rps and b/rps differ 68 | diff --git a/test/fixtures/diff_raw_binary b/test/fixtures/diff_raw_binary 69 | new file mode 100644 70 | index 0000000000000000000000000000000000000000..d4673fa41ee8413384167fc7b9f25e4daf18a53a 71 | --- /dev/null 72 | +++ b/test/fixtures/diff_raw_binary 73 | @@ -0,0 +1 @@ 74 | +:100755 100755 f4567df37451b230b1381b1bc9c2bcad76e08a3c 736bd596a36924d30b480942e9475ce0d734fa0d M rps 75 | diff --git a/test/test_diff.py b/test/test_diff.py 76 | index ce0f64f2261bd8de063233108caac1f26742c1fd..4de26f8884fd048ac7f10007f2bf7c7fa3fa60f4 100644 77 | --- a/test/test_diff.py 78 | +++ b/test/test_diff.py 79 | @@ -65,6 +65,21 @@ class TestDiff(TestBase): 80 | assert diff.rename_to == 'that' 81 | assert len(list(diffs.iter_change_type('R'))) == 1 82 | 83 | + def test_binary_diff(self): 84 | + for method, file_name in ((Diff._index_from_patch_format, 'diff_patch_binary'), 85 | + (Diff._index_from_raw_format, 'diff_raw_binary')): 86 | + res = method(None, StringProcessAdapter(fixture(file_name)).stdout) 87 | + assert len(res) == 1 88 | + assert len(list(res.iter_change_type('M'))) == 1 89 | + if res[0].diff: 90 | + assert res[0].diff == "Binary files a/rps and b/rps differ\n", "in patch mode, we get a diff text" 91 | + assert isinstance(str(res[0]), str), "This call should just work" 92 | + # end for each method to test 93 | + 94 | + def test_diff_index(self): 95 | + res = self.rorepo.index.diff('17f5d13a7a741dcbb2a30e147bdafe929cff4697', create_patch=True) 96 | + assert len(res) == 3 97 | + 98 | def test_diff_patch_format(self): 99 | # test all of the 'old' format diffs for completeness - it should at least 100 | # be able to deal with it 101 | -------------------------------------------------------------------------------- /test/fixtures/diff_index_raw: -------------------------------------------------------------------------------- 1 | :100644 000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 D 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_initial: -------------------------------------------------------------------------------- 1 | @@ -0,0 +1,7 @@ 2 | +======= 3 | +CHANGES 4 | +======= 5 | + 6 | +0.1.0 7 | +===== 8 | +initial release 9 | -------------------------------------------------------------------------------- /test/fixtures/diff_new_mode: -------------------------------------------------------------------------------- 1 | diff --git a/conf/global_settings.py b/conf/global_settings.py 2 | old mode 100644 3 | new mode 100755 4 | index 9ec1bac000000000000000000000000000000000..1c4f83b000000000000000000000000000000000 5 | --- a/conf/global_settings.py 6 | +++ b/conf/global_settings.py 7 | @@ -58,6 +58,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 8 | ) 9 | 10 | MIDDLEWARE_CLASSES = ( 11 | + "django.middleware.cache.CacheMiddleware", 12 | "django.middleware.common.CommonMiddleware", 13 | "django.contrib.sessions.middleware.SessionMiddleware", 14 | "django.contrib.auth.middleware.AuthenticationMiddleware", 15 | -------------------------------------------------------------------------------- /test/fixtures/diff_numstat: -------------------------------------------------------------------------------- 1 | M 29 18 a.txt 2 | M 0 5 b.txt 3 | A 7 0 c.txt -------------------------------------------------------------------------------- /test/fixtures/diff_patch_binary: -------------------------------------------------------------------------------- 1 | diff --git a/rps b/rps 2 | index f4567df37451b230b1381b1bc9c2bcad76e08a3c..736bd596a36924d30b480942e9475ce0d734fa0d 100755 3 | Binary files a/rps and b/rps differ 4 | -------------------------------------------------------------------------------- /test/fixtures/diff_patch_unsafe_paths: -------------------------------------------------------------------------------- 1 | diff --git a/path/ starting with a space b/path/ starting with a space 2 | new file mode 100644 3 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 4 | --- /dev/null 5 | +++ b/path/ starting with a space 6 | @@ -0,0 +1 @@ 7 | +dummy content 8 | diff --git "a/path/\"with-quotes\"" "b/path/\"with-quotes\"" 9 | new file mode 100644 10 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 11 | --- /dev/null 12 | +++ "b/path/\"with-quotes\"" 13 | @@ -0,0 +1 @@ 14 | +dummy content 15 | diff --git a/path/'with-single-quotes' b/path/'with-single-quotes' 16 | new file mode 100644 17 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 18 | --- /dev/null 19 | +++ b/path/'with-single-quotes' 20 | @@ -0,0 +1 @@ 21 | +dummy content 22 | diff --git a/path/ending in a space b/path/ending in a space 23 | new file mode 100644 24 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 25 | --- /dev/null 26 | +++ b/path/ending in a space 27 | @@ -0,0 +1 @@ 28 | +dummy content 29 | diff --git "a/path/with\ttab" "b/path/with\ttab" 30 | new file mode 100644 31 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 32 | --- /dev/null 33 | +++ "b/path/with\ttab" 34 | @@ -0,0 +1 @@ 35 | +dummy content 36 | diff --git "a/path/with\nnewline" "b/path/with\nnewline" 37 | new file mode 100644 38 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 39 | --- /dev/null 40 | +++ "b/path/with\nnewline" 41 | @@ -0,0 +1 @@ 42 | +dummy content 43 | diff --git a/path/with spaces b/path/with spaces 44 | new file mode 100644 45 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 46 | --- /dev/null 47 | +++ b/path/with spaces 48 | @@ -0,0 +1 @@ 49 | +dummy content 50 | diff --git a/path/with-question-mark? b/path/with-question-mark? 51 | new file mode 100644 52 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 53 | --- /dev/null 54 | +++ b/path/with-question-mark? 55 | @@ -0,0 +1 @@ 56 | +dummy content 57 | diff --git "a/path/¯\\_(ツ)_|¯" "b/path/¯\\_(ツ)_|¯" 58 | new file mode 100644 59 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 60 | --- /dev/null 61 | +++ "b/path/¯\\_(ツ)_|¯" 62 | @@ -0,0 +1 @@ 63 | +dummy content 64 | diff --git "a/path/\360\237\222\251.txt" "b/path/\360\237\222\251.txt" 65 | new file mode 100644 66 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 67 | --- /dev/null 68 | +++ "b/path/\360\237\222\251.txt" 69 | @@ -0,0 +1 @@ 70 | +dummy content 71 | diff --git "a/path/\200-invalid-unicode-path.txt" "b/path/\200-invalid-unicode-path.txt" 72 | new file mode 100644 73 | index 0000000000000000000000000000000000000000..eaf5f7510320b6a327fb308379de2f94d8859a54 74 | --- /dev/null 75 | +++ "b/path/\200-invalid-unicode-path.txt" 76 | @@ -0,0 +1 @@ 77 | +dummy content 78 | diff --git a/a/with spaces b/b/with some spaces 79 | similarity index 100% 80 | rename from a/with spaces 81 | rename to b/with some spaces 82 | diff --git a/a/ending in a space b/b/ending with space 83 | similarity index 100% 84 | rename from a/ending in a space 85 | rename to b/ending with space 86 | diff --git "a/a/\"with-quotes\"" "b/b/\"with even more quotes\"" 87 | similarity index 100% 88 | rename from "a/\"with-quotes\"" 89 | rename to "b/\"with even more quotes\"" 90 | -------------------------------------------------------------------------------- /test/fixtures/diff_raw_binary: -------------------------------------------------------------------------------- 1 | :100755 100755 f4567df37451b230b1381b1bc9c2bcad76e08a3c 736bd596a36924d30b480942e9475ce0d734fa0d M rps 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_rename: -------------------------------------------------------------------------------- 1 | commit 2524c44334a8ba6b2ab8f3f0a478f04c5b073cc8 2 | tree e126e7b4203dadf083f5eb8e2f34c255b51d8bee 3 | parent d789e23b9ea8d90221d13c46f7c228d729385f92 4 | author Michael Trier 1229389391 -0500 5 | committer Michael Trier 1229389391 -0500 6 | 7 | Renamed AUTHORS to CONTRIBUTORS because it's cooler. 8 | 9 | diff --git a/AUTHORS b/CONTRIBUTORS 10 | similarity index 100% 11 | rename from Jérôme 12 | rename to müller 13 | -------------------------------------------------------------------------------- /test/fixtures/diff_rename_raw: -------------------------------------------------------------------------------- 1 | :100644 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 R100 this that 2 | -------------------------------------------------------------------------------- /test/fixtures/diff_tree_numstat_root: -------------------------------------------------------------------------------- 1 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 | 18 29 a.txt 3 | 5 0 b.txt 4 | -------------------------------------------------------------------------------- /test/fixtures/env_case.py: -------------------------------------------------------------------------------- 1 | """Steps 3 and 4 for test_it_avoids_upcasing_unrelated_environment_variable_names.""" 2 | 3 | import subprocess 4 | import sys 5 | 6 | # Step 3a: Import the module, in case that upcases the environment variable name. 7 | import git 8 | 9 | 10 | _, working_dir, env_var_name = sys.argv 11 | 12 | # Step 3b: Use Git.execute explicitly, in case that upcases the environment variable. 13 | # (Importing git should be enough, but this ensures Git.execute is called.) 14 | repo = git.Repo(working_dir) # Hold the reference. 15 | git.Git(repo.working_dir).execute(["git", "version"]) 16 | 17 | # Step 4: Create the non-Python grandchild that accesses the variable case-sensitively. 18 | print(subprocess.check_output(["set", env_var_name], shell=True, text=True)) 19 | -------------------------------------------------------------------------------- /test/fixtures/for_each_ref_with_path_component: -------------------------------------------------------------------------------- 1 | refs/heads/refactoring/feature1e7cbfc1a8e545120591bb186f91f3b1b8c8bbbb2commit180 2 | -------------------------------------------------------------------------------- /test/fixtures/git_config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | [remote "origin"] 7 | fetch = +refs/heads/*:refs/remotes/origin/* 8 | url = git://gitorious.org/~byron/git-python/byrons-clone.git 9 | pushurl = git@gitorious.org:~byron/git-python/byrons-clone.git 10 | # a tab indented section header 11 | [branch "master"] 12 | remote = origin 13 | merge = refs/heads/master 14 | # an space indented section header 15 | [remote "mainline"] 16 | # space indented comment 17 | url = git://gitorious.org/git-python/mainline.git 18 | fetch = +refs/heads/*:refs/remotes/mainline/* 19 | 20 | [remote "MartinMarcher"] 21 | # tab indented comment 22 | url = git://gitorious.org/~martin.marcher/git-python/serverhorror.git 23 | fetch = +refs/heads/*:refs/remotes/MartinMarcher/* 24 | # can handle comments - the section name is supposed to be stripped 25 | # causes stock git-config puke 26 | [ gui ] 27 | geometry = 1316x820+219+243 207 192 28 | [branch "mainline_performance"] 29 | remote = mainline 30 | merge = refs/heads/master 31 | # section with value defined before include to be overridden 32 | [sec] 33 | var0 = value0_main 34 | [include] 35 | path = doesntexist.cfg 36 | # field should be 'path' so abspath should be ignored 37 | abspath = /usr/bin/foodoesntexist.bar 38 | path = /usr/bin/foodoesntexist.bar 39 | # should be relative to the path of this config file 40 | path = ./git_config-inc.cfg 41 | # and defined after include. According to the documentation 42 | # and behavior of git config, this should be the value since 43 | # inclusions should be processed immediately 44 | [sec] 45 | var1 = value1_main 46 | 47 | -------------------------------------------------------------------------------- /test/fixtures/git_config-inc.cfg: -------------------------------------------------------------------------------- 1 | [sec] 2 | var0 = value0_included 3 | var1 = value1_included 4 | [diff] 5 | tool = diff_included 6 | -------------------------------------------------------------------------------- /test/fixtures/git_config_global: -------------------------------------------------------------------------------- 1 | # just a comment 2 | [alias] 3 | st = status 4 | ci = commit 5 | co = checkout 6 | br = branch 7 | [color] 8 | branch = auto 9 | diff = auto 10 | interactive = auto 11 | status = auto 12 | [user] 13 | name = Sebastian Thiel 14 | email = byronimo@gmail.com 15 | [core] 16 | editor = vim 17 | autocrlf = false 18 | packedGitLimit = 1g 19 | packedGitWindowSize = 512m 20 | [pack] 21 | windowMemory = 512m 22 | [merge] 23 | tool = meld 24 | [diff] 25 | tool = meld 26 | -------------------------------------------------------------------------------- /test/fixtures/git_config_multiple: -------------------------------------------------------------------------------- 1 | [section0] 2 | option0 = value0 3 | 4 | [section1] 5 | option1 = value1a 6 | option1 = value1b 7 | other_option1 = other_value1 8 | -------------------------------------------------------------------------------- /test/fixtures/git_config_with_comments: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Cody Veal 3 | email = cveal05@gmail.com 4 | 5 | [github] 6 | user = cjhveal 7 | 8 | [advice] 9 | statusHints = false 10 | 11 | [alias] 12 | # add 13 | a = add 14 | aa = add --all 15 | ap = add --patch 16 | 17 | aliases = !git config --list | grep 'alias\\.' | sed 's/alias\\.\\([^=]*\\)=\\(.*\\)/\\1\\\t => \\2/' | sort 18 | 19 | # branch 20 | br = branch 21 | branches = branch -av 22 | cp = cherry-pick 23 | diverges = !bash -c 'diff -u <(git rev-list --first-parent "${1}") <(git rev-list --first-parent "${2:-HEAD}"g | sed -ne \"s/^ //p\" | head -1' - 24 | track = checkout -t 25 | nb = checkout -b 26 | 27 | # commit 28 | amend = commit --amend -C HEAD 29 | c = commit 30 | ca = commit --amend 31 | cm = commit --message 32 | msg = commit --allow-empty -m 33 | 34 | co = checkout 35 | 36 | # diff 37 | d = diff --color-words # diff by word 38 | ds = diff --staged --color-words 39 | dd = diff --color-words=. # diff by char 40 | dds = diff --staged --color-words=. 41 | dl = diff # diff by line 42 | dls = diff --staged 43 | 44 | h = help 45 | 46 | # log 47 | authors = "!git log --pretty=format:%aN | sort | uniq -c | sort -rn" 48 | lc = log ORIG_HEAD.. --stat --no-merges 49 | lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative 50 | lol = log --graph --decorate --pretty=oneline --abbrev-commit 51 | lola = log --graph --decorate --pretty=oneline --abbrev-commit --all 52 | 53 | # merge 54 | m = merge 55 | mm = merge --no-ff 56 | ours = "!f() { git checkout --ours $@ && git add $@; }; f" 57 | theirs = "!f() { git checkout --theirs $@ && git add $@; }; f" 58 | 59 | # push/pull 60 | l = pull 61 | p = push 62 | sync = !git pull && git push 63 | 64 | # remotes 65 | prune-remotes = "!for remote in `git remote`; do git remote prune $remote; done" 66 | r = remote 67 | 68 | # rebase 69 | rb = rebase 70 | rba = rebase --abort 71 | rbc = rebase --continue 72 | rbs = rebase --skip 73 | 74 | # reset 75 | rh = reset --hard 76 | rhh = reset HEAD --hard 77 | uncommit = reset --soft HEAD^ 78 | unstage = reset HEAD -- 79 | unpush = push -f origin HEAD^:master 80 | 81 | # stash 82 | ss = stash 83 | sl = stash list 84 | sp = stash pop 85 | sd = stash drop 86 | snapshot = !git stash save "snapshot: $(date)" && git stash apply "stash@{0}" 87 | 88 | # status 89 | s = status --short --branch 90 | st = status 91 | 92 | # submodule 93 | sm = submodule 94 | sma = submodule add 95 | smu = submodule update --init 96 | pup = !git pull && git submodule init && git submodule update 97 | 98 | # file level ignoring 99 | assume = update-index --assume-unchanged 100 | unassume = update-index --no-assume-unchanged 101 | assumed = "!git ls-files -v | grep ^h | cut -c 3-" 102 | 103 | 104 | [apply] 105 | whitespace = fix 106 | 107 | [color] 108 | ui = auto 109 | 110 | [color "branch"] 111 | current = yellow reverse 112 | local = yellow 113 | remote = green 114 | 115 | [color "diff"] 116 | meta = yellow 117 | frag = magenta 118 | old = red bold 119 | new = green bold 120 | whitespace = red reverse 121 | 122 | [color "status"] 123 | added = green 124 | changed = yellow 125 | untracked = cyan 126 | 127 | [core] 128 | editor = /usr/bin/vim 129 | excludesfile = ~/.gitignore_global 130 | attributesfile = ~/.gitattributes 131 | 132 | [diff] 133 | renames = copies 134 | mnemonicprefix = true 135 | 136 | [diff "zip"] 137 | textconv = unzip -c -a 138 | 139 | [merge] 140 | log = true 141 | 142 | [merge "railsschema"] 143 | name = newer Rails schema version 144 | driver = "ruby -e '\n\ 145 | system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n\ 146 | b = File.read(%(%A))\n\ 147 | b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n>+ .*/) do\n\ 148 | %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n\ 149 | end\n\ 150 | File.open(%(%A), %(w)) {|f| f.write(b)}\n\ 151 | exit 1 if b.include?(%(<)*%L)'" 152 | 153 | [merge "gemfilelock"] 154 | name = relocks the gemfile.lock 155 | driver = bundle lock 156 | 157 | [pager] 158 | color = true 159 | 160 | [push] 161 | default = upstream 162 | 163 | [rerere] 164 | enabled = true 165 | 166 | [url "git@github.com:"] 167 | insteadOf = "gh:" 168 | pushInsteadOf = "github:" 169 | pushInsteadOf = "git://github.com/" 170 | 171 | [url "git://github.com/"] 172 | insteadOf = "github:" 173 | 174 | [url "git@gist.github.com:"] 175 | insteadOf = "gst:" 176 | pushInsteadOf = "gist:" 177 | pushInsteadOf = "git://gist.github.com/" 178 | 179 | [url "git://gist.github.com/"] 180 | insteadOf = "gist:" 181 | 182 | [url "git@heroku.com:"] 183 | insteadOf = "heroku:" 184 | -------------------------------------------------------------------------------- /test/fixtures/git_config_with_empty_value: -------------------------------------------------------------------------------- 1 | [color] 2 | ui 3 | [core] 4 | filemode = true -------------------------------------------------------------------------------- /test/fixtures/git_config_with_quotes: -------------------------------------------------------------------------------- 1 | [user] 2 | name = "Cody Veal" 3 | email = "cveal05@gmail.com" 4 | -------------------------------------------------------------------------------- /test/fixtures/git_config_with_quotes_whitespace: -------------------------------------------------------------------------------- 1 | [core] 2 | commentString = "# " 3 | -------------------------------------------------------------------------------- /test/fixtures/git_file: -------------------------------------------------------------------------------- 1 | gitdir: ./.real 2 | -------------------------------------------------------------------------------- /test/fixtures/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/test/fixtures/index -------------------------------------------------------------------------------- /test/fixtures/index_merge: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/test/fixtures/index_merge -------------------------------------------------------------------------------- /test/fixtures/ls_tree_a: -------------------------------------------------------------------------------- 1 | 100644 blob 81d2c27608b352814cbe979a6acd678d30219678 History.txt 2 | 100644 blob 641972d82c6d1b51122274ae8f6a0ecdfb56ee22 Manifest.txt 3 | 100644 blob 8b1e02c0fb554eed2ce2ef737a68bb369d7527df README.txt 4 | 100644 blob 735d7338b7cb208563aa282f0376c5c4049453a7 Rakefile 5 | 040000 tree c3d07b0083f01a6e1ac969a0f32b8d06f20c62e5 bin 6 | 040000 tree aa06ba24b4e3f463b3c4a85469d0fb9e5b421cf8 lib 7 | 040000 tree 650fa3f0c17f1edb4ae53d8dcca4ac59d86e6c44 test 8 | -------------------------------------------------------------------------------- /test/fixtures/ls_tree_b: -------------------------------------------------------------------------------- 1 | 100644 blob aa94e396335d2957ca92606f909e53e7beaf3fbb grit.rb 2 | 040000 tree 34868e6e7384cb5ee51c543a8187fdff2675b5a7 grit 3 | -------------------------------------------------------------------------------- /test/fixtures/ls_tree_commit: -------------------------------------------------------------------------------- 1 | 040000 tree 2afb47bcedf21663580d5e6d2f406f08f3f65f19 foo 2 | 160000 commit d35b34c6e931b9da8f6941007a92c9c9a9b0141a bar 3 | 040000 tree f623ee576a09ca491c4a27e48c0dfe04be5f4a2e baz 4 | -------------------------------------------------------------------------------- /test/fixtures/ls_tree_empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpython-developers/GitPython/b6a188ba8b3d856a449b772783c4005e75c54c6f/test/fixtures/ls_tree_empty -------------------------------------------------------------------------------- /test/fixtures/polyglot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # Valid script in both Bash and Python, but with different behavior. 3 | """:" 4 | echo 'Ran intended hook.' >output.txt 5 | exit 6 | " """ 7 | from pathlib import Path 8 | Path('payload.txt').write_text('Ran impostor hook!', encoding='utf-8') 9 | -------------------------------------------------------------------------------- /test/fixtures/reflog_invalid_date: -------------------------------------------------------------------------------- 1 | 501bf602abea7d21c3dbb409b435976e92033145 82b8902e033430000481eb355733cd7065342037 Sebastian Thiel 1270634931 +0200 commit: Used this release for a first beta of the 0.2 branch of development 2 | 82b8902e033430000481eb355733cd7065342037 69361d96a59381fde0ac34d19df2d4aff05fb9a9 Sebastian Thiel 1271229940 commit: conf.py: Adjusted version to match with the actual version 3 | -------------------------------------------------------------------------------- /test/fixtures/reflog_invalid_email: -------------------------------------------------------------------------------- 1 | 501bf602abea7d21c3dbb409b435976e92033145 82b8902e033430000481eb355733cd7065342037 Sebastian Thiel 1270634931 +0200 commit: Used this release for a first beta of the 0.2 branch of development 2 | 82b8902e033430000481eb355733cd7065342037 69361d96a59381fde0ac34d19df2d4aff05fb9a9 Sebastian Thiel 1270634931 +0200 commit: Used this release for a first beta of the 0.2 branch of development 2 | 82b8902e033430000481eb355733cd7065342037 69361d96a59381fde0ac34d19df2d Sebastian Thiel 1271229940 +0200 commit: conf.py: Adjusted version to match with the actual version 3 | -------------------------------------------------------------------------------- /test/fixtures/reflog_invalid_oldsha: -------------------------------------------------------------------------------- 1 | 501bf602abea7d21c3dbb409b435976e92033145 82b8902e033430000481eb355733cd7065342037 Sebastian Thiel 1270634931 +0200 commit: Used this release for a first beta of the 0.2 branch of development 2 | 82b8902e033430000481eb3 69361d96a59381fde0ac34d19df2d4aff05fb9a9 Sebastian Thiel 1271229940 +0200 commit: conf.py: Adjusted version to match with the actual version 3 | -------------------------------------------------------------------------------- /test/fixtures/reflog_invalid_sep: -------------------------------------------------------------------------------- 1 | 501bf602abea7d21c3dbb409b435976e92033145 82b8902e033430000481eb355733cd7065342037 Sebastian Thiel 1270634931 +0200 commit: Used this release for a first beta of the 0.2 branch of development 2 | 82b8902e033430000481eb355733cd7065342037 69361d96a59381fde0ac34d19df2d4aff05fb9a9 Sebastian Thiel 1271229940 +0200 commit: conf.py: Adjusted version to match with the actual version 3 | -------------------------------------------------------------------------------- /test/fixtures/rev_list: -------------------------------------------------------------------------------- 1 | 4c8124ffcf4039d292442eeccabdeca5af5c5017 2 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 3 | ab25fd8483882c3bda8a458ad2965d2248654335 4 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_bisect_all: -------------------------------------------------------------------------------- 1 | commit cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b (dist=2) 2 | tree 01fb5ddba393df486d850c37f40c9a87f4a28a14 3 | parent bfdc8e26d36833b3a7106c306fdbe6d38dec817e 4 | author Florian Apolloner 1218480521 +0200 5 | committer Florian Apolloner 1218480521 +0200 6 | 7 | use shell=True in windows (git.exe needs to be on %PATH%) 8 | One bug remaining: git on windows is returning status 0 for `git this-does-not-exist`, so no GitCommandError is raised. 9 | 10 | commit 33ebe7acec14b25c5f84f35a664803fcab2f7781 (dist=1) 11 | tree 960b40fe368a9882221bcdd8635b9080dec01ec6 12 | author Michael Trier 1210193388 -0400 13 | committer Michael Trier 1210193388 -0400 14 | 15 | initial project 16 | 17 | commit a6604a00a652e754cb8b6b0b9f194f839fc38d7c (dist=1) 18 | tree 547e8af2f10ffa77c4ed4d0a8381e64141f986b4 19 | parent cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b 20 | author Florian Apolloner 1219330141 +0200 21 | committer Florian Apolloner 1219330141 +0200 22 | 23 | fixed unneeded list unpacking 24 | 25 | commit 8df638c22c75ddc9a43ecdde90c0c9939f5009e7 (dist=0) 26 | tree 43a63b045e538a38161c8da5e154ff1c9436ea4e 27 | parent a6604a00a652e754cb8b6b0b9f194f839fc38d7c 28 | parent 127e511ea2e22f3bd9a0279e747e9cfa9509986d 29 | author Florian Apolloner 1219330182 +0200 30 | committer Florian Apolloner 1219330182 +0200 31 | 32 | Merge branch 'master' of git@gitorious.org:git-python/mainline 33 | 34 | commit c231551328faa864848bde6ff8127f59c9566e90 (dist=-1) 35 | tree 991ed402b4f6562209ea56550a3c5050d1aa0118 36 | parent 8df638c22c75ddc9a43ecdde90c0c9939f5009e7 37 | author David Aguilar 1220418344 -0700 38 | committer David Aguilar 1220418344 -0700 39 | 40 | commit: handle --bisect-all output in Commit.list_from_string 41 | 42 | Rui Abreu Ferrerira pointed out that "git rev-list --bisect-all" 43 | returns a slightly different format which we can easily accommodate 44 | by changing the way we parse rev-list output. 45 | 46 | http://groups.google.com/group/git-python/browse_thread/thread/aed1d5c4b31d5027 47 | 48 | This resolves the issue mentioned in that thread. 49 | 50 | Signed-off-by: David Aguilar 51 | 52 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_commit_diffs: -------------------------------------------------------------------------------- 1 | commit 91169e1f5fa4de2eaea3f176461f5dc784796769 2 | tree 802ed53edbf6f02ad664af3f7e5900f514024b2f 3 | parent 038af8c329ef7c1bae4568b98bd5c58510465493 4 | author Tom Preston-Werner 1193200199 -0700 5 | committer Tom Preston-Werner 1193200199 -0700 6 | 7 | fix some initialization warnings 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_commit_idabbrev: -------------------------------------------------------------------------------- 1 | commit 80f136f500dfdb8c3e8abf4ae716f875f0a1b57f 2 | tree 3fffd0fce0655433c945e6bdc5e9f338b087b211 3 | parent 44f82e5ac93ba322161019dce44b78c5bd1fdce2 4 | author tom 1195608462 -0800 5 | committer tom 1195608462 -0800 6 | 7 | fix tests on other machines 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_commit_stats: -------------------------------------------------------------------------------- 1 | commit 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 | tree b35b4bf642d667fdd613eebcfe4e17efd420fb8a 3 | author Tom Preston-Werner 1191997100 -0700 4 | committer Tom Preston-Werner 1191997100 -0700 5 | 6 | initial grit setup 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_delta_a: -------------------------------------------------------------------------------- 1 | e34590b7a2d186b3bb9a1170d02d52b36c791c78 2 | 8977833d74f8681aa0d9a5e84b0dd3d81519774d 3 | 6f5561530cb3a94e4c86454e84732197325be172 4 | ee419e04a961543444be6db66aef52e6e37936d6 5 | d845de9d438e1a249a0c2fcb778e8ea3b7e06cef 6 | 0bba4a6c10060405a94d52533af2f9bdacd4f29c 7 | 77711c0722964ead965e0ba2ee9ed4a03cb3d292 8 | 501d23cac6dd911511f15d091ee031a15b90ebde 9 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_delta_b: -------------------------------------------------------------------------------- 1 | 4c8124ffcf4039d292442eeccabdeca5af5c5017 2 | 634396b2f541a9f2d58b00be1a07f0c358b999b3 3 | ab25fd8483882c3bda8a458ad2965d2248654335 4 | e34590b7a2d186b3bb9a1170d02d52b36c791c78 5 | 8977833d74f8681aa0d9a5e84b0dd3d81519774d 6 | 6f5561530cb3a94e4c86454e84732197325be172 7 | ee419e04a961543444be6db66aef52e6e37936d6 8 | d845de9d438e1a249a0c2fcb778e8ea3b7e06cef 9 | 0bba4a6c10060405a94d52533af2f9bdacd4f29c 10 | 77711c0722964ead965e0ba2ee9ed4a03cb3d292 11 | 501d23cac6dd911511f15d091ee031a15b90ebde 12 | -------------------------------------------------------------------------------- /test/fixtures/rev_list_single: -------------------------------------------------------------------------------- 1 | commit 4c8124ffcf4039d292442eeccabdeca5af5c5017 2 | tree 672eca9b7f9e09c22dcb128c283e8c3c8d7697a4 3 | parent 634396b2f541a9f2d58b00be1a07f0c358b999b3 4 | author Tom Preston-Werner 1191999972 -0700 5 | committer Tom Preston-Werner 1191999972 -0700 6 | 7 | implement Grit#heads 8 | -------------------------------------------------------------------------------- /test/fixtures/rev_parse: -------------------------------------------------------------------------------- 1 | 80f136f 2 | -------------------------------------------------------------------------------- /test/fixtures/show_empty_commit: -------------------------------------------------------------------------------- 1 | commit 1e3824339762bd48316fe87bfafc853732d43264 2 | tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 3 | author Tom Preston-Werner 1157392833 +0000 4 | committer Tom Preston-Werner 1157392833 +0000 5 | 6 | initial directory structure 7 | -------------------------------------------------------------------------------- /test/fixtures/uncommon_branch_prefix_FETCH_HEAD: -------------------------------------------------------------------------------- 1 | c2e3c20affa3e2b61a05fdc9ee3061dd416d915e 'refs/pull/1/head' of http://github.com/loic-bot/testrepo 2 | fd8695d980e2c6df62b7785f93fd6292d1e283fb 'refs/pull/1/merge' of http://github.com/loic-bot/testrepo 3 | bb46faf089720d1a3f9e4dc3b11ed5ff77d7e764 'refs/pull/2/head' of http://github.com/loic-bot/testrepo 4 | 5faa366d58454eceea811e0e34c502bdd7b37e4b 'refs/pull/2/merge' of http://github.com/loic-bot/testrepo 5 | b3ad3c4f1864b50d4d3e09320947a1a3c34c9ea2 'refs/pull/3/head' of http://github.com/loic-bot/testrepo 6 | 71fe57e511776042b009ed4bb281b62b0522b434 'refs/pull/3/merge' of http://github.com/loic-bot/testrepo 7 | -------------------------------------------------------------------------------- /test/fixtures/uncommon_branch_prefix_stderr: -------------------------------------------------------------------------------- 1 | = [up to date] refs/pull/1/head -> pull/1/head 2 | = [up to date] refs/pull/1/merge -> pull/1/merge 3 | = [up to date] refs/pull/2/head -> pull/2/head 4 | = [up to date] refs/pull/2/merge -> pull/2/merge 5 | = [up to date] refs/pull/3/head -> pull/3/head 6 | = [up to date] refs/pull/3/merge -> pull/3/merge 7 | -------------------------------------------------------------------------------- /test/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | import inspect 7 | 8 | from .helper import * # noqa: F401 F403 9 | 10 | __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))] 11 | -------------------------------------------------------------------------------- /test/performance/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | -------------------------------------------------------------------------------- /test/performance/lib.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Support library for performance tests.""" 5 | 6 | import logging 7 | import os 8 | import os.path as osp 9 | import tempfile 10 | 11 | from git import Repo 12 | from git.db import GitCmdObjectDB, GitDB 13 | from git.util import rmtree 14 | 15 | from test.lib import TestBase 16 | 17 | # { Invariants 18 | 19 | k_env_git_repo = "GIT_PYTHON_TEST_GIT_REPO_BASE" 20 | 21 | # } END invariants 22 | 23 | 24 | # { Base Classes 25 | 26 | 27 | class TestBigRepoR(TestBase): 28 | """TestCase providing access to readonly 'big' repositories using the following 29 | member variables: 30 | 31 | * gitrorepo: 32 | Read-Only git repository - actually (by default) the repo of GitPython itself. 33 | 34 | * puregitrorepo: 35 | Like gitrorepo, but uses a pure Python implementation for its object database. 36 | """ 37 | 38 | def setUp(self): 39 | super().setUp() 40 | 41 | repo_path = os.environ.get(k_env_git_repo) 42 | if repo_path is None: 43 | logging.info( 44 | "You can set the %s environment variable to a .git repository of your" 45 | " choice - defaulting to the GitPython repository", 46 | k_env_git_repo, 47 | ) 48 | repo_path = osp.dirname(__file__) 49 | # END set some repo path 50 | self.gitrorepo = Repo(repo_path, odbt=GitCmdObjectDB, search_parent_directories=True) 51 | self.puregitrorepo = Repo(repo_path, odbt=GitDB, search_parent_directories=True) 52 | 53 | def tearDown(self): 54 | self.gitrorepo.git.clear_cache() 55 | self.gitrorepo = None 56 | self.puregitrorepo.git.clear_cache() 57 | self.puregitrorepo = None 58 | 59 | 60 | class TestBigRepoRW(TestBigRepoR): 61 | """Like :class:`TestBigRepoR`, but provides a big repository that we can write to. 62 | 63 | Provides ``self.gitrwrepo`` and ``self.puregitrwrepo``. 64 | """ 65 | 66 | def setUp(self): 67 | self.gitrwrepo = None 68 | super().setUp() 69 | dirname = tempfile.mkdtemp() 70 | self.gitrwrepo = self.gitrorepo.clone(dirname, shared=True, bare=True, odbt=GitCmdObjectDB) 71 | self.puregitrwrepo = Repo(dirname, odbt=GitDB) 72 | 73 | def tearDown(self): 74 | super().tearDown() 75 | if self.gitrwrepo is not None: 76 | rmtree(self.gitrwrepo.working_dir) 77 | self.gitrwrepo.git.clear_cache() 78 | self.gitrwrepo = None 79 | self.puregitrwrepo.git.clear_cache() 80 | self.puregitrwrepo = None 81 | 82 | 83 | # } END base classes 84 | -------------------------------------------------------------------------------- /test/performance/test_commit.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | """Performance tests for commits (iteration, traversal, and serialization).""" 7 | 8 | import gc 9 | from io import BytesIO 10 | from time import time 11 | import sys 12 | 13 | from gitdb import IStream 14 | 15 | from git import Commit 16 | 17 | from test.performance.lib import TestBigRepoRW 18 | from test.test_commit import TestCommitSerialization 19 | 20 | 21 | class TestPerformance(TestBigRepoRW, TestCommitSerialization): 22 | def tearDown(self): 23 | gc.collect() 24 | 25 | # ref with about 100 commits in its history. 26 | ref_100 = "0.1.6" 27 | 28 | def _query_commit_info(self, c): 29 | c.author 30 | c.authored_date 31 | c.author_tz_offset 32 | c.committer 33 | c.committed_date 34 | c.committer_tz_offset 35 | c.message 36 | c.parents 37 | 38 | def test_iteration(self): 39 | no = 0 40 | nc = 0 41 | 42 | # Find the first commit containing the given path. Always do a full iteration 43 | # (restricted to the path in question). This should return quite a lot of 44 | # commits. We just take one and hence abort the operation. 45 | 46 | st = time() 47 | for c in self.rorepo.iter_commits(self.ref_100): 48 | nc += 1 49 | self._query_commit_info(c) 50 | for obj in c.tree.traverse(): 51 | obj.size 52 | no += 1 53 | # END for each object 54 | # END for each commit 55 | elapsed_time = time() - st 56 | print( 57 | "Traversed %i Trees and a total of %i uncached objects in %s [s] ( %f objs/s )" 58 | % (nc, no, elapsed_time, no / elapsed_time), 59 | file=sys.stderr, 60 | ) 61 | 62 | def test_commit_traversal(self): 63 | # Bound to cat-file parsing performance. 64 | nc = 0 65 | st = time() 66 | for c in self.gitrorepo.commit().traverse(branch_first=False): 67 | nc += 1 68 | self._query_commit_info(c) 69 | # END for each traversed commit 70 | elapsed_time = time() - st 71 | print( 72 | "Traversed %i Commits in %s [s] ( %f commits/s )" % (nc, elapsed_time, nc / elapsed_time), 73 | file=sys.stderr, 74 | ) 75 | 76 | def test_commit_iteration(self): 77 | # Bound to stream parsing performance. 78 | nc = 0 79 | st = time() 80 | for c in Commit.iter_items(self.gitrorepo, self.gitrorepo.head): 81 | nc += 1 82 | self._query_commit_info(c) 83 | # END for each traversed commit 84 | elapsed_time = time() - st 85 | print( 86 | "Iterated %i Commits in %s [s] ( %f commits/s )" % (nc, elapsed_time, nc / elapsed_time), 87 | file=sys.stderr, 88 | ) 89 | 90 | def test_commit_serialization(self): 91 | self.assert_commit_serialization(self.gitrwrepo, "58c78e6", True) 92 | 93 | rwrepo = self.gitrwrepo 94 | make_object = rwrepo.odb.store 95 | # Direct serialization - deserialization can be tested afterwards. 96 | # Serialization is probably limited on IO. 97 | hc = rwrepo.commit(rwrepo.head) 98 | 99 | nc = 5000 100 | st = time() 101 | for i in range(nc): 102 | cm = Commit( 103 | rwrepo, 104 | Commit.NULL_BIN_SHA, 105 | hc.tree, 106 | hc.author, 107 | hc.authored_date, 108 | hc.author_tz_offset, 109 | hc.committer, 110 | hc.committed_date, 111 | hc.committer_tz_offset, 112 | str(i), 113 | parents=hc.parents, 114 | encoding=hc.encoding, 115 | ) 116 | 117 | stream = BytesIO() 118 | cm._serialize(stream) 119 | slen = stream.tell() 120 | stream.seek(0) 121 | 122 | cm.binsha = make_object(IStream(Commit.type, slen, stream)).binsha 123 | # END commit creation 124 | elapsed = time() - st 125 | 126 | print( 127 | "Serialized %i commits to loose objects in %f s ( %f commits / s )" % (nc, elapsed, nc / elapsed), 128 | file=sys.stderr, 129 | ) 130 | -------------------------------------------------------------------------------- /test/performance/test_odb.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Performance tests for object store.""" 5 | 6 | import sys 7 | from time import time 8 | 9 | from test.performance.lib import TestBigRepoR 10 | 11 | 12 | class TestObjDBPerformance(TestBigRepoR): 13 | def test_random_access(self): 14 | results = [["Iterate Commits"], ["Iterate Blobs"], ["Retrieve Blob Data"]] 15 | for repo in (self.gitrorepo, self.puregitrorepo): 16 | # GET COMMITS 17 | st = time() 18 | root_commit = repo.commit(repo.head) 19 | commits = list(root_commit.traverse()) 20 | nc = len(commits) 21 | elapsed = time() - st 22 | 23 | print( 24 | "%s: Retrieved %i commits from ObjectStore in %g s ( %f commits / s )" 25 | % (type(repo.odb), nc, elapsed, nc / elapsed), 26 | file=sys.stderr, 27 | ) 28 | results[0].append(elapsed) 29 | 30 | # GET TREES 31 | # Walk all trees of all commits. 32 | st = time() 33 | blobs_per_commit = [] 34 | nt = 0 35 | for commit in commits: 36 | tree = commit.tree 37 | blobs = [] 38 | for item in tree.traverse(): 39 | nt += 1 40 | if item.type == "blob": 41 | blobs.append(item) 42 | # Direct access for speed. 43 | # END while trees are there for walking 44 | blobs_per_commit.append(blobs) 45 | # END for each commit 46 | elapsed = time() - st 47 | 48 | print( 49 | "%s: Retrieved %i objects from %i commits in %g s ( %f objects / s )" 50 | % (type(repo.odb), nt, len(commits), elapsed, nt / elapsed), 51 | file=sys.stderr, 52 | ) 53 | results[1].append(elapsed) 54 | 55 | # GET BLOBS 56 | st = time() 57 | nb = 0 58 | too_many = 15000 59 | data_bytes = 0 60 | for blob_list in blobs_per_commit: 61 | for blob in blob_list: 62 | data_bytes += len(blob.data_stream.read()) 63 | # END for each blobsha 64 | nb += len(blob_list) 65 | if nb > too_many: 66 | break 67 | # END for each bloblist 68 | elapsed = time() - st 69 | 70 | msg = "%s: Retrieved %i blob (%i KiB) and their data in %g s ( %f blobs / s, %f KiB / s )" % ( 71 | type(repo.odb), 72 | nb, 73 | data_bytes / 1000, 74 | elapsed, 75 | nb / elapsed, 76 | (data_bytes / 1000) / elapsed, 77 | ) 78 | print(msg, file=sys.stderr) 79 | results[2].append(elapsed) 80 | # END for each repo type 81 | 82 | # Final results. 83 | for test_name, a, b in results: 84 | print( 85 | "%s: %f s vs %f s, pure is %f times slower" % (test_name, a, b, b / a), 86 | file=sys.stderr, 87 | ) 88 | # END for each result 89 | -------------------------------------------------------------------------------- /test/test_actor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | from git import Actor 7 | 8 | from test.lib import TestBase 9 | 10 | 11 | class TestActor(TestBase): 12 | def test_from_string_should_separate_name_and_email(self): 13 | a = Actor._from_string("Michael Trier ") 14 | self.assertEqual("Michael Trier", a.name) 15 | self.assertEqual("mtrier@example.com", a.email) 16 | 17 | # Base type capabilities 18 | assert a == a 19 | assert not (a != a) 20 | m = set() 21 | m.add(a) 22 | m.add(a) 23 | assert len(m) == 1 24 | 25 | def test_from_string_should_handle_just_name(self): 26 | a = Actor._from_string("Michael Trier") 27 | self.assertEqual("Michael Trier", a.name) 28 | self.assertEqual(None, a.email) 29 | 30 | def test_should_display_representation(self): 31 | a = Actor._from_string("Michael Trier ") 32 | self.assertEqual('">', repr(a)) 33 | 34 | def test_str_should_alias_name(self): 35 | a = Actor._from_string("Michael Trier ") 36 | self.assertEqual(a.name, str(a)) 37 | -------------------------------------------------------------------------------- /test/test_blob.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | from git import Blob 7 | 8 | from test.lib import TestBase 9 | 10 | 11 | class TestBlob(TestBase): 12 | def test_mime_type_should_return_mime_type_for_known_types(self): 13 | blob = Blob(self.rorepo, **{"binsha": Blob.NULL_BIN_SHA, "path": "foo.png"}) 14 | self.assertEqual("image/png", blob.mime_type) 15 | 16 | def test_mime_type_should_return_text_plain_for_unknown_types(self): 17 | blob = Blob(self.rorepo, **{"binsha": Blob.NULL_BIN_SHA, "path": "something"}) 18 | self.assertEqual("text/plain", blob.mime_type) 19 | 20 | def test_nodict(self): 21 | self.assertRaises(AttributeError, setattr, self.rorepo.tree()["AUTHORS"], "someattr", 2) 22 | -------------------------------------------------------------------------------- /test/test_blob_filter.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | """Tests for the blob filter.""" 5 | 6 | from pathlib import Path 7 | from typing import Sequence, Tuple 8 | from unittest.mock import MagicMock 9 | 10 | import pytest 11 | 12 | from git.index.typ import BlobFilter, StageType 13 | from git.objects import Blob 14 | from git.types import PathLike 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "paths, path, expected_result", 19 | [ 20 | ((Path("foo"),), Path("foo"), True), 21 | ((Path("foo"),), Path("foo/bar"), True), 22 | ((Path("foo/bar"),), Path("foo"), False), 23 | ((Path("foo"), Path("bar")), Path("foo"), True), 24 | ], 25 | ) 26 | def test_blob_filter(paths: Sequence[PathLike], path: PathLike, expected_result: bool) -> None: 27 | """Test the blob filter.""" 28 | blob_filter = BlobFilter(paths) 29 | 30 | binsha = MagicMock(__len__=lambda self: 20) 31 | stage_type: StageType = 0 32 | blob: Blob = Blob(repo=MagicMock(), binsha=binsha, path=path) 33 | stage_blob: Tuple[StageType, Blob] = (stage_type, blob) 34 | 35 | result = blob_filter(stage_blob) 36 | 37 | assert result == expected_result 38 | -------------------------------------------------------------------------------- /test/test_clone.py: -------------------------------------------------------------------------------- 1 | # This module is part of GitPython and is released under the 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 3 | 4 | from pathlib import Path 5 | import re 6 | 7 | import git 8 | 9 | from test.lib import TestBase, with_rw_directory 10 | 11 | 12 | class TestClone(TestBase): 13 | @with_rw_directory 14 | def test_checkout_in_non_empty_dir(self, rw_dir): 15 | non_empty_dir = Path(rw_dir) 16 | garbage_file = non_empty_dir / "not-empty" 17 | garbage_file.write_text("Garbage!") 18 | 19 | # Verify that cloning into the non-empty dir fails while complaining about the 20 | # target directory not being empty/non-existent. 21 | try: 22 | self.rorepo.clone(non_empty_dir) 23 | except git.GitCommandError as exc: 24 | self.assertTrue(exc.stderr, "GitCommandError's 'stderr' is unexpectedly empty") 25 | expr = re.compile(r"(?is).*\bfatal:\s+destination\s+path\b.*\bexists\b.*\bnot\b.*\bempty\s+directory\b") 26 | self.assertTrue( 27 | expr.search(exc.stderr), 28 | '"%s" does not match "%s"' % (expr.pattern, exc.stderr), 29 | ) 30 | else: 31 | self.fail("GitCommandError not raised") 32 | -------------------------------------------------------------------------------- /test/test_db.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | import os.path as osp 7 | 8 | from git.db import GitCmdObjectDB 9 | from git.exc import BadObject 10 | from git.util import bin_to_hex 11 | 12 | from test.lib import TestBase 13 | 14 | 15 | class TestDB(TestBase): 16 | def test_base(self): 17 | gdb = GitCmdObjectDB(osp.join(self.rorepo.git_dir, "objects"), self.rorepo.git) 18 | 19 | # Partial to complete - works with everything. 20 | hexsha = bin_to_hex(gdb.partial_to_complete_sha_hex("0.1.6")) 21 | assert len(hexsha) == 40 22 | 23 | assert bin_to_hex(gdb.partial_to_complete_sha_hex(hexsha[:20])) == hexsha 24 | 25 | # Fails with BadObject. 26 | for invalid_rev in ("0000", "bad/ref", "super bad"): 27 | self.assertRaises(BadObject, gdb.partial_to_complete_sha_hex, invalid_rev) 28 | -------------------------------------------------------------------------------- /test/test_exc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2008, 2009, 2016 Michael Trier (mtrier@gmail.com) and contributors 2 | # 3 | # This module is part of GitPython and is released under the 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 5 | 6 | from itertools import product 7 | import re 8 | 9 | import ddt 10 | 11 | from git.exc import ( 12 | InvalidGitRepositoryError, 13 | WorkTreeRepositoryUnsupported, 14 | NoSuchPathError, 15 | CommandError, 16 | GitCommandNotFound, 17 | GitCommandError, 18 | CheckoutError, 19 | CacheError, 20 | UnmergedEntriesError, 21 | HookExecutionError, 22 | RepositoryDirtyError, 23 | ) 24 | from git.util import remove_password_if_present 25 | 26 | from test.lib import TestBase 27 | 28 | 29 | _cmd_argvs = ( 30 | ("cmd",), 31 | ("θνιψοδε",), 32 | ("θνιψοδε", "normal", "argvs"), 33 | ("cmd", "ελληνικα", "args"), 34 | ("θνιψοδε", "κι", "αλλα", "strange", "args"), 35 | ("θνιψοδε", "κι", "αλλα", "non-unicode", "args"), 36 | ( 37 | "git", 38 | "clone", 39 | "-v", 40 | "https://fakeuser:fakepassword1234@fakerepo.example.com/testrepo", 41 | ), 42 | ) 43 | _causes_n_substrings = ( 44 | (None, None), 45 | (7, "exit code(7)"), 46 | ("Some string", "'Some string'"), 47 | ("παλιο string", "'παλιο string'"), 48 | (Exception("An exc."), "Exception('An exc.')"), 49 | (Exception("Κακια exc."), "Exception('Κακια exc.')"), 50 | (object(), "=4 3 | env_list = py{37,38,39,310,311,312}, ruff, format, mypy, html, misc 4 | 5 | [testenv] 6 | description = Run unit tests 7 | package = wheel 8 | extras = test 9 | pass_env = SSH_* 10 | commands = pytest --color=yes {posargs} 11 | 12 | [testenv:ruff] 13 | description = Lint with Ruff 14 | base_python = py{39,310,311,312,38,37} 15 | deps = ruff 16 | set_env = 17 | CLICOLOR_FORCE = 1 # Set NO_COLOR to override this. 18 | commands = ruff check . 19 | 20 | [testenv:format] 21 | description = Check formatting with Ruff 22 | base_python = py{39,310,311,312,38,37} 23 | deps = ruff 24 | set_env = 25 | CLICOLOR_FORCE = 1 # Set NO_COLOR to override this. 26 | commands = ruff format --check . 27 | 28 | [testenv:mypy] 29 | description = Typecheck with mypy 30 | base_python = py{39,310,311,312,38,37} 31 | set_env = 32 | MYPY_FORCE_COLOR = 1 33 | commands = mypy 34 | ignore_outcome = true 35 | 36 | [testenv:html] 37 | description = Build HTML documentation 38 | base_python = py{39,310,311,312,38,37} 39 | extras = doc 40 | allowlist_externals = make 41 | commands = 42 | make BUILDDIR={env_tmp_dir}/doc/build -C doc clean 43 | make BUILDDIR={env_tmp_dir}/doc/build -C doc html 44 | 45 | [testenv:misc] 46 | description = Run other checks via pre-commit 47 | base_python = py{39,310,311,312,38,37} 48 | set_env = 49 | SKIP = ruff-format,ruff 50 | commands = pre-commit run --all-files 51 | --------------------------------------------------------------------------------