├── PyGitUp ├── __init__.py ├── tests │ ├── test_utils.py │ ├── test_fetch_interrupted.py │ ├── test_not_on_a_git_repo.py │ ├── test_no_remotes.py │ ├── test_version.py │ ├── test_git_not_in_path.py │ ├── test_fast_forwarded.py │ ├── test_issue_55.py │ ├── test_up_to_date.py │ ├── test_local_tracking.py │ ├── test_run_in_subdir.py │ ├── test_fetch_fail.py │ ├── test_diverged.py │ ├── test_tracking.py │ ├── test_detached.py │ ├── test_returning_to_branch.py │ ├── test_ahead_of_upstream.py │ ├── test_overwrite_untracked_error.py │ ├── test_fetchall.py │ ├── test_fetch_large_output.py │ ├── test_unstash_error.py │ ├── test_remote_branch_deleted.py │ ├── test_rebase_error.py │ ├── test_rebasing.py │ ├── test_push.py │ ├── test_separate_worktree.py │ ├── test_faster_fastforwarded.py │ ├── test_multiple_remotes.py │ ├── test_out_of_tree.py │ ├── test_log_hook.py │ ├── test_nofetch.py │ ├── test_rebase_arguments.py │ ├── test_submodules_dirty.py │ ├── test_stash_error.py │ ├── test_submodules.py │ └── __init__.py ├── utils.py ├── git_wrapper.py └── gitup.py ├── .pyup.yml ├── .coveragerc ├── .gitignore ├── .github └── workflows │ ├── publish-workflow.yml │ └── ci-workflow.yml ├── LICENCE ├── pyproject.toml ├── README.rst └── poetry.lock /PyGitUp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: every week 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | PyGitUp/tests/* 6 | __init__.py 7 | 8 | 9 | exclude_lines = 10 | pragma: no cover 11 | if __name__ == .__main__.: 12 | if self.testing: 13 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from PyGitUp import utils 2 | 3 | 4 | def test_find(): 5 | assert utils.find([1, 2, 3], lambda i: i == 3) == 3 6 | assert utils.find([1, 2, 3], lambda i: i == 4) is None 7 | 8 | 9 | def test_uniq(): 10 | assert utils.uniq([1, 1, 1, 2, 3]) == [1, 2, 3] 11 | assert utils.uniq([1]) == [1] 12 | assert utils.uniq([]) == [] 13 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_fetch_interrupted.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PyGitUp.gitup import GitUp 3 | 4 | def test_fetch_interrupted(monkeypatch): 5 | """ Run 'git up' and interrupt on fetch """ 6 | 7 | gitup = GitUp(testing=True) 8 | 9 | def mock_fetch(*args, **kwargs): 10 | raise KeyboardInterrupt 11 | 12 | monkeypatch.setattr(gitup, "fetch", mock_fetch) 13 | 14 | with pytest.raises(SystemExit) as error: 15 | gitup.run() 16 | 17 | assert error.value.code == 130 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | htmlcov/ 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # PyCharm 39 | .idea/ 40 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_not_on_a_git_repo.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | 7 | from PyGitUp.git_wrapper import GitError 8 | from PyGitUp.tests import basepath 9 | 10 | test_name = 'not-on-a-repo' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | os.makedirs(repo_path, 0o700) 16 | 17 | 18 | def test_not_a_git_repo(): 19 | """ Run 'git up' being not on a git repo """ 20 | os.chdir(repo_path) 21 | 22 | from PyGitUp.gitup import GitUp 23 | 24 | with pytest.raises(GitError): 25 | GitUp(testing=True) 26 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_no_remotes.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | 4 | import pytest 5 | 6 | from PyGitUp.git_wrapper import GitError 7 | from PyGitUp.tests import init_master 8 | 9 | test_name = 'no_remotes' 10 | 11 | 12 | def setup_function(): 13 | global master_path 14 | master_path, master = init_master(test_name) 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | 20 | def test_no_remotes(): 21 | """ Run 'git up' w/o remotes """ 22 | os.chdir(master_path) 23 | 24 | from PyGitUp.gitup import GitUp 25 | 26 | with pytest.raises(GitError): 27 | GitUp(testing=True) 28 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_version.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import socket 3 | 4 | import pytest 5 | 6 | from PyGitUp.tests import capture 7 | 8 | test_name = 'version' 9 | 10 | 11 | def test_version(): 12 | """ Run 'git up': Check version """ 13 | try: 14 | import pkg_resources as pkg 15 | except ImportError: 16 | pytest.skip('pip not installed') 17 | 18 | try: 19 | socket.gethostbyname('pypi.python.org') 20 | except OSError: 21 | pytest.skip('Can\'t connect to PYPI') 22 | 23 | from PyGitUp.gitup import GitUp 24 | with capture() as [stdout, _]: 25 | GitUp(sparse=True).version_info() 26 | stdout = stdout.getvalue() 27 | 28 | package = pkg.get_distribution('git-up') 29 | local_version_str = package.version 30 | 31 | assert local_version_str in stdout 32 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_git_not_in_path.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | 7 | from PyGitUp.git_wrapper import GitError 8 | from PyGitUp.tests import basepath 9 | 10 | test_name = 'git-not-in-path' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | os.makedirs(repo_path, 0o700) 16 | 17 | 18 | def test_not_a_git_repo(): 19 | """ Run 'git up' with git no being in PATH """ 20 | os.chdir(repo_path) 21 | environ = os.environ.copy() 22 | os.environ['PATH'] = '' 23 | 24 | try: 25 | with pytest.raises(GitError, match="The git executable could not be " 26 | "found"): 27 | from PyGitUp.gitup import GitUp 28 | GitUp(testing=True) 29 | 30 | finally: 31 | os.environ.update(environ) 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install poetry 23 | poetry install 24 | - name: Publish package 25 | env: 26 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} 27 | run: | 28 | poetry publish --build 29 | - name: Create Release 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref }} 35 | release_name: ${{ github.ref }} 36 | draft: false 37 | prerelease: false -------------------------------------------------------------------------------- /PyGitUp/tests/test_fast_forwarded.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'fast-forwarded' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | 15 | # Prepare master repo 16 | master.git.checkout(b=test_name) 17 | 18 | # Clone to test repo 19 | path = join(basepath, test_name) 20 | 21 | master.clone(path, b=test_name) 22 | repo = Repo(path, odbt=GitCmdObjectDB) 23 | 24 | assert repo.working_dir == path 25 | 26 | # Modify file in master 27 | update_file(master, test_name) 28 | 29 | 30 | def test_fast_forwarded(): 31 | """ Run 'git up' with result: fast-forwarding """ 32 | os.chdir(repo_path) 33 | 34 | from PyGitUp.gitup import GitUp 35 | gitup = GitUp(testing=True) 36 | gitup.run() 37 | 38 | assert len(gitup.states) == 1 39 | assert gitup.states[0] == 'fast-forwarding' 40 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_issue_55.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'issue-55' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | 15 | branch_name = 'feature/#11772-replace-api-url' 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=branch_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=branch_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | update_file(master, test_name) 30 | 31 | 32 | def test_issue_55(): 33 | """ Regression test for #55 """ 34 | os.chdir(repo_path) 35 | 36 | from PyGitUp.gitup import GitUp 37 | gitup = GitUp(testing=True) 38 | gitup.run() 39 | 40 | assert len(gitup.states) == 1 41 | assert gitup.states[0] == 'fast-forwarding' 42 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Markus Siemens 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_up_to_date.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'up-to-date' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | 15 | # Prepare master repo 16 | master.git.checkout(b=test_name) 17 | 18 | # Clone to test repo 19 | path = join(basepath, test_name) 20 | 21 | master.clone(path, b=test_name) 22 | repo = Repo(path, odbt=GitCmdObjectDB) 23 | 24 | assert repo.working_dir == path 25 | 26 | # Modify file in master 27 | update_file(master, test_name) 28 | 29 | # Update repo 30 | repo.remotes.origin.pull() 31 | 32 | 33 | def test_up_to_date(): 34 | """ Run 'git up' with result: up to date """ 35 | os.chdir(repo_path) 36 | 37 | from PyGitUp.gitup import GitUp 38 | gitup = GitUp(testing=True) 39 | gitup.run() 40 | 41 | assert len(gitup.states) == 1 42 | assert gitup.states[0] == 'up to date' 43 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_local_tracking.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from PyGitUp.tests import basepath, init_master, update_file 6 | 7 | test_name = 'local_tracking' 8 | repo_path = join(basepath, test_name + os.sep) 9 | 10 | 11 | def _read_file(path): 12 | with open(path) as f: 13 | return f.read() 14 | 15 | 16 | def setup_function(): 17 | global repo_path 18 | master_path, master = init_master(test_name) 19 | 20 | # Prepare master repo 21 | master.git.checkout(b=test_name) 22 | 23 | # Create branch with local tracking 24 | master.git.checkout(b=test_name + '_b', t=True) 25 | repo_path = master_path 26 | 27 | # Modify tracking branch 28 | master.git.checkout(test_name) 29 | update_file(master) 30 | 31 | 32 | def test_local_tracking(): 33 | """ Run 'git up' with a local tracking branch """ 34 | os.chdir(repo_path) 35 | 36 | from PyGitUp.gitup import GitUp 37 | gitup = GitUp(testing=True) 38 | gitup.run() 39 | 40 | assert len(gitup.states) == 1 41 | assert gitup.states[0] == 'fast-forwarding' 42 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_run_in_subdir.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'run-in-subdir' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | 15 | # Prepare master repo 16 | master.git.checkout(b=test_name) 17 | 18 | # Clone to test repo 19 | path = join(basepath, test_name) 20 | 21 | master.clone(path, b=test_name) 22 | repo = Repo(path, odbt=GitCmdObjectDB) 23 | 24 | assert repo.working_dir == path 25 | 26 | # Modify file in master 27 | update_file(master, test_name) 28 | 29 | 30 | def test_run_in_subdir(): 31 | """ Run 'git up' in a subdir of the repo """ 32 | subdir = join(repo_path, 'dir') 33 | os.mkdir(subdir) 34 | os.chdir(subdir) 35 | 36 | from PyGitUp.gitup import GitUp 37 | gitup = GitUp(testing=True) 38 | gitup.run() 39 | 40 | assert len(gitup.states) == 1 41 | assert gitup.states[0] == 'fast-forwarding' 42 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_fetch_fail.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import GitError 8 | from PyGitUp.tests import basepath, init_master, update_file 9 | 10 | test_name = 'test-fail' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Set remote 29 | repo.git.remote('set-url', 'origin', 'does-not-exist') 30 | 31 | # Modify file in master 32 | update_file(master, test_name) 33 | 34 | 35 | def test_fetch_fail(): 36 | """ Run 'git up' with a non-existent remote """ 37 | os.chdir(repo_path) 38 | 39 | from PyGitUp.gitup import GitUp 40 | gitup = GitUp(testing=True) 41 | 42 | with pytest.raises(GitError): 43 | gitup.run() 44 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_diverged.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'diverged' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | 15 | # Prepare master repo 16 | master.git.checkout(b=test_name) 17 | 18 | # Clone to test repo 19 | path = join(basepath, test_name) 20 | 21 | master.clone(path, b=test_name) 22 | repo = Repo(path, odbt=GitCmdObjectDB) 23 | 24 | assert repo.working_dir == path 25 | 26 | # Set git-up.rebase.auto to false 27 | repo.git.config('git-up.rebase.auto', 'false') 28 | 29 | # Modify file in master 30 | update_file(master, test_name) 31 | 32 | # Modify file in our repo 33 | update_file(repo, test_name) 34 | 35 | 36 | def test_diverged(): 37 | """ Run 'git up' with result: diverged """ 38 | os.chdir(repo_path) 39 | 40 | from PyGitUp.gitup import GitUp 41 | gitup = GitUp(testing=True) 42 | gitup.run() 43 | 44 | assert len(gitup.states) == 1 45 | assert gitup.states[0] == 'diverged' 46 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_tracking.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'tracking' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def _read_file(path): 13 | with open(path) as f: 14 | return f.read() 15 | 16 | 17 | def setup_function(): 18 | master_path, master = init_master(test_name) 19 | 20 | # Prepare master repo 21 | master.git.checkout(b=test_name) 22 | 23 | # Clone to test repo 24 | path = join(basepath, test_name) 25 | 26 | master.clone(path, b=test_name) 27 | repo = Repo(path, odbt=GitCmdObjectDB) 28 | 29 | # Rename test repo branch 30 | repo.git.branch(test_name + '_renamed', m=True) 31 | 32 | assert repo.working_dir == path 33 | 34 | # Modify file in master 35 | update_file(master, test_name) 36 | 37 | 38 | def test_tracking(): 39 | """ Run 'git up' with a local tracking branch """ 40 | os.chdir(repo_path) 41 | 42 | from PyGitUp.gitup import GitUp 43 | gitup = GitUp(testing=True) 44 | gitup.run() 45 | 46 | assert len(gitup.states) == 1 47 | assert gitup.states[0] == 'fast-forwarding' 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_detached.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import GitError 8 | from PyGitUp.tests import basepath, init_master, update_file 9 | 10 | test_name = 'detached' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | update_file(master, test_name) 30 | 31 | # Modify file in our repo 32 | update_file(repo, test_name) 33 | update_file(repo, test_name) 34 | 35 | # Check out parent commit 36 | repo.git.checkout('HEAD~') 37 | 38 | 39 | def test_detached(): 40 | """ Run 'git up' with detached head """ 41 | os.chdir(repo_path) 42 | 43 | from PyGitUp.gitup import GitUp 44 | gitup = GitUp(testing=True) 45 | 46 | with pytest.raises(GitError): 47 | gitup.run() 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_returning_to_branch.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'returning-to-branch' 9 | new_branch_name = test_name + '.2' 10 | repo_path = join(basepath, test_name + os.sep) 11 | 12 | 13 | def setup_function(): 14 | master_path, master = init_master(test_name) 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | # Clone to test repo 20 | path = join(basepath, test_name) 21 | 22 | master.clone(path, b=test_name) 23 | repo = Repo(path, odbt=GitCmdObjectDB) 24 | 25 | assert repo.working_dir == path 26 | 27 | # Create a new branch in repo 28 | repo.git.checkout(b=new_branch_name) 29 | 30 | # Modify file in master 31 | update_file(master, test_name) 32 | 33 | 34 | def test_returning_to_branch(): 35 | """ Run 'git up': return to branch """ 36 | os.chdir(repo_path) 37 | 38 | from PyGitUp.gitup import GitUp 39 | gitup = GitUp(testing=True) 40 | gitup.run() 41 | 42 | assert len(gitup.states) == 1 43 | assert gitup.states[0] == 'fast-forwarding' 44 | assert gitup.repo.head.ref.name == new_branch_name 45 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_ahead_of_upstream.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | 7 | from PyGitUp.tests import basepath, write_file, init_master 8 | 9 | test_name = 'ahead-of-upstream' 10 | testfile_name = 'file' 11 | 12 | repo_path = join(basepath, test_name + os.sep) 13 | 14 | 15 | def setup_function(): 16 | master_path, master = init_master(test_name) 17 | 18 | # Prepare master repo 19 | master.git.checkout(b=test_name) 20 | 21 | # Clone to test repo 22 | path = join(basepath, test_name) 23 | 24 | master.clone(path, b=test_name) 25 | repo = Repo(path, odbt=GitCmdObjectDB) 26 | 27 | assert repo.working_dir == path 28 | 29 | # Modify file in our repo 30 | repo_path_file = join(path, testfile_name) 31 | write_file(repo_path_file, 'line 1\nline 2\ncounter: 2') 32 | repo.index.add([repo_path_file]) 33 | repo.index.commit(test_name) 34 | 35 | 36 | def test_ahead_of_upstream(): 37 | """ Run 'git up' with result: ahead of upstream """ 38 | os.chdir(repo_path) 39 | 40 | from PyGitUp.gitup import GitUp 41 | gitup = GitUp(testing=True) 42 | gitup.run() 43 | 44 | assert len(gitup.states) == 1 45 | assert gitup.states[0] == 'ahead' 46 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_overwrite_untracked_error.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import RebaseError 8 | from PyGitUp.tests import basepath, init_master, update_file, write_file 9 | 10 | test_name = 'overwrite_untracked_error' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | update_file(master, test_name, filename='test1.txt') 30 | 31 | # Modify file in working directory 32 | write_file(join(path, 'test1.txt'), 'Hello world!') 33 | 34 | 35 | def test_fast_forwarded(): 36 | """ Fail correctly when a rebase would overwrite untracked files """ 37 | os.chdir(repo_path) 38 | 39 | from PyGitUp.gitup import GitUp 40 | gitup = GitUp(testing=True) 41 | 42 | with pytest.raises(RebaseError): 43 | gitup.run() 44 | -------------------------------------------------------------------------------- /PyGitUp/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some simple, generic useful methods. 3 | """ 4 | import subprocess 5 | import sys 6 | 7 | from subprocess import DEVNULL 8 | 9 | 10 | def find(seq, test): 11 | """ Return first item in sequence where test(item) == True """ 12 | for item in seq: 13 | if test(item): 14 | return item 15 | 16 | 17 | def uniq(seq): 18 | """ Return a copy of seq without duplicates. """ 19 | seen = set() 20 | return [x for x in seq if str(x) not in seen and not seen.add(str(x))] 21 | 22 | 23 | def execute(cmd, cwd=None): 24 | """ Execute a command and return it's output. """ 25 | try: 26 | lines = subprocess \ 27 | .check_output(cmd, cwd=cwd, stderr=DEVNULL) \ 28 | .splitlines() 29 | except subprocess.CalledProcessError: 30 | return None 31 | else: 32 | if lines: 33 | return decode(lines[0].strip()) 34 | else: 35 | return None 36 | 37 | 38 | def decode(s): 39 | """ 40 | Decode a string using the system encoding if needed (ie byte strings) 41 | """ 42 | if isinstance(s, bytes): 43 | return s.decode(sys.getdefaultencoding()) 44 | else: 45 | return s 46 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_fetchall.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, capture 7 | 8 | test_name = 'fetch-all' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master_path, master = init_master(test_name) 14 | master_path2, master2 = init_master(test_name + '2') 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | # Clone to test repo 20 | path = join(basepath, test_name) 21 | 22 | master.clone(path, b=test_name) 23 | repo = Repo(path, odbt=GitCmdObjectDB) 24 | 25 | assert repo.working_dir == path 26 | 27 | # Configure git up 28 | repo.git.config('git-up.fetch.all', 'true') 29 | 30 | # Add second master repo to remotes 31 | repo.git.remote('add', test_name, master_path2) 32 | 33 | 34 | def test_fetchall(): 35 | """ Run 'git up' with fetch.all """ 36 | os.chdir(repo_path) 37 | 38 | from PyGitUp.gitup import GitUp 39 | gitup = GitUp(testing=True) 40 | 41 | with capture() as [stdout, _]: 42 | gitup.run() 43 | 44 | stdout = stdout.getvalue() 45 | 46 | assert 'origin' in stdout 47 | assert test_name in stdout 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_fetch_large_output.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | from os import sep, chdir 3 | from os.path import join 4 | import io 5 | 6 | from git import * 7 | 8 | from PyGitUp.tests import basepath, init_master 9 | 10 | TEST_NAME = 'fetch-large-output' 11 | REPO_PATH = join(basepath, TEST_NAME + sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(TEST_NAME) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=TEST_NAME) 19 | 20 | # Clone to test repo 21 | path = join(basepath, TEST_NAME) 22 | 23 | master.clone(path, b=TEST_NAME) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Generate lots of branches 29 | total_branch_name_bytes = 0 30 | for i in range(0, 1500): 31 | branch_name = 'branch-name-%d' % i 32 | total_branch_name_bytes += len(branch_name) 33 | master.git.checkout(b=branch_name) 34 | 35 | 36 | def test_fetch_large_output(): 37 | """ Run 'git up' with a fetch that outputs lots of data """ 38 | # Arrange 39 | chdir(REPO_PATH) 40 | from PyGitUp.gitup import GitUp 41 | gitup = GitUp(testing=True) 42 | 43 | # Act 44 | gitup.run() 45 | 46 | # Assert 47 | assert len(gitup.states) == 1 48 | assert gitup.states[0] == 'up to date' 49 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_unstash_error.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import UnstashError 8 | from PyGitUp.tests import basepath, write_file, init_master, testfile_name 9 | 10 | test_name = 'unstash_error' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | master_path_file = join(master_path, testfile_name) 30 | write_file(master_path_file, 'contents') 31 | master.index.add([master_path_file]) 32 | master.index.commit(test_name) 33 | 34 | # Modify file in repo 35 | path_file = join(path, testfile_name) 36 | os.unlink(path_file) 37 | 38 | 39 | def test_unstash_error(): 40 | """ Run 'git up' with an unclean unstash """ 41 | os.chdir(repo_path) 42 | 43 | from PyGitUp.gitup import GitUp 44 | gitup = GitUp(testing=True) 45 | 46 | with pytest.raises(UnstashError): 47 | gitup.run() 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_remote_branch_deleted.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master 7 | 8 | test_name = 'remote-branch-deleted' 9 | new_branch_name = test_name + '.2' 10 | repo_path = join(basepath, test_name + os.sep) 11 | 12 | 13 | def setup_function(): 14 | master_path, master = init_master(test_name) 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | # Create new branch 20 | master.git.checkout(b=new_branch_name) 21 | 22 | # Clone to test repo 23 | path = join(basepath, test_name) 24 | 25 | master.clone(path, b=test_name) 26 | repo = Repo(path, odbt=GitCmdObjectDB) 27 | 28 | assert repo.working_dir == path 29 | 30 | # Checkout new branch in cloned repo 31 | repo.git.checkout(new_branch_name, 'origin/' + new_branch_name, b=True) 32 | 33 | # Remove branch from master again 34 | master.git.checkout(test_name) 35 | master.git.branch(new_branch_name, d=True) 36 | 37 | 38 | def test_remote_branch_deleted(): 39 | """ Run 'git up' with remotely deleted branch """ 40 | os.chdir(repo_path) 41 | 42 | from PyGitUp.gitup import GitUp 43 | gitup = GitUp(testing=True) 44 | gitup.run() 45 | 46 | assert len(gitup.states) == 2 47 | assert gitup.states[1] == 'remote branch doesn\'t exist' 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_rebase_error.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import RebaseError 8 | from PyGitUp.tests import basepath, write_file, init_master, update_file, testfile_name 9 | 10 | test_name = 'rebase_error' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | update_file(master, test_name) 30 | 31 | # Modify file in our repo 32 | contents = 'completely changed!' 33 | repo_file = join(path, testfile_name) 34 | 35 | write_file(repo_file, contents) 36 | repo.index.add([repo_file]) 37 | repo.index.commit(test_name) 38 | 39 | # Modify file in master 40 | update_file(master, test_name) 41 | 42 | 43 | def test_rebase_error(): 44 | """ Run 'git up' with a failing rebase """ 45 | os.chdir(repo_path) 46 | 47 | from PyGitUp.gitup import GitUp 48 | gitup = GitUp(testing=True) 49 | 50 | with pytest.raises(RebaseError): 51 | gitup.run() 52 | 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "git-up" 3 | version = "2.3.0" 4 | description = "A python implementation of 'git up'" 5 | authors = ["Markus Siemens "] 6 | license = "MIT" 7 | packages = [ 8 | { include = "PyGitUp" } 9 | ] 10 | 11 | readme = "README.rst" 12 | 13 | homepage = "https://github.com/msiemens/PyGitUp" 14 | keywords = ["database", "nosql"] 15 | 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Environment :: Console", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Topic :: Software Development :: Version Control", 29 | "Topic :: Utilities", 30 | ] 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.8" 34 | GitPython = "^3.0.0" 35 | colorama = "^0.4.0" 36 | termcolor = "^2.0.0" 37 | 38 | [tool.poetry.dev-dependencies] 39 | coveralls = "^3.0.0" 40 | pytest = "^8.3.3" 41 | pytest-cov = "^4.0.0" 42 | 43 | [build-system] 44 | requires = ["poetry-core>=1.0"] 45 | build-backend = "poetry.core.masonry.api" 46 | 47 | [tool.poetry.scripts] 48 | git-up = 'PyGitUp.gitup:run' 49 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_rebasing.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, write_file, init_master, update_file, \ 7 | testfile_name 8 | 9 | test_name = 'rebasing' 10 | repo_path = join(basepath, test_name + os.sep) 11 | 12 | 13 | def _read_file(path): 14 | with open(path) as f: 15 | return f.read() 16 | 17 | 18 | def setup_function(): 19 | master_path, master = init_master(test_name) 20 | 21 | # Prepare master repo 22 | master.git.checkout(b=test_name) 23 | 24 | # Clone to test repo 25 | path = join(basepath, test_name) 26 | 27 | master.clone(path, b=test_name) 28 | repo = Repo(path, odbt=GitCmdObjectDB) 29 | 30 | assert repo.working_dir == path 31 | 32 | # Modify file in master 33 | master_file = update_file(master, test_name) 34 | 35 | # Modify file in our repo 36 | contents = _read_file(master_file) 37 | contents = contents.replace('line 1', 'line x') 38 | repo_file = join(path, testfile_name) 39 | 40 | write_file(repo_file, contents) 41 | repo.index.add([repo_file]) 42 | repo.index.commit(test_name) 43 | 44 | 45 | def test_rebasing(): 46 | """ Run 'git up' with result: rebasing """ 47 | os.chdir(repo_path) 48 | 49 | from PyGitUp.gitup import GitUp 50 | gitup = GitUp(testing=True) 51 | gitup.run() 52 | 53 | assert len(gitup.states) == 1 54 | assert gitup.states[0] == 'rebasing' 55 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_push.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, write_file, init_master, update_file, \ 7 | testfile_name 8 | 9 | test_name = 'push' 10 | repo_path = join(basepath, test_name + os.sep) 11 | 12 | 13 | def _read_file(path): 14 | with open(path) as f: 15 | return f.read() 16 | 17 | 18 | def setup_function(): 19 | master_path, master = init_master(test_name) 20 | master.git.config('receive.denyCurrentBranch', 'ignore', add=True) 21 | 22 | # Prepare master repo 23 | master.git.checkout(b=test_name) 24 | 25 | # Clone to test repo 26 | path = join(basepath, test_name) 27 | 28 | master.clone(path, b=test_name) 29 | repo = Repo(path, odbt=GitCmdObjectDB) 30 | 31 | assert repo.working_dir == path 32 | 33 | # Modify file in master 34 | update_file(master, test_name) 35 | 36 | # Modify file in our repo 37 | repo_file = join(path, 'file2.txt') 38 | 39 | write_file(repo_file, 'test') 40 | repo.index.add([repo_file]) 41 | repo.index.commit(test_name) 42 | 43 | 44 | def test_rebasing(): 45 | """ Run 'git up' with pushing to origin """ 46 | os.chdir(repo_path) 47 | 48 | from PyGitUp.gitup import GitUp 49 | gitup = GitUp(testing=True) 50 | gitup.settings['push.auto'] = True 51 | gitup.run() 52 | 53 | assert len(gitup.states) == 1 54 | assert gitup.states[0] == 'rebasing' 55 | assert gitup.pushed 56 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_separate_worktree.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | from unittest import SkipTest 5 | 6 | from git import * 7 | from git.util import cygpath 8 | from PyGitUp.tests import basepath, init_master, update_file 9 | 10 | test_name = 'worktree' 11 | repo_path = join(basepath, test_name + os.sep) 12 | worktree_dir = f'{test_name}_worktree' 13 | worktree_path = join(basepath, worktree_dir) 14 | 15 | 16 | def setup_function(): 17 | if Git().version_info[:3] < (2, 5, 1): 18 | return 19 | 20 | master_path, master = init_master(test_name) 21 | 22 | # Prepare master repo 23 | master.git.checkout(b=test_name) 24 | 25 | # Clone to test repo 26 | path = join(basepath, test_name) 27 | 28 | master.clone(path, b=test_name) 29 | 30 | # Create work tree 31 | clone = Repo(path, odbt=GitCmdObjectDB) 32 | clone.git.worktree('add', '../' + worktree_dir) 33 | 34 | # repo = Repo(worktree_path, odbt=GitCmdObjectDB) 35 | # assert repo.working_dir == worktree_path 36 | 37 | # Modify file in master 38 | update_file(master, test_name) 39 | 40 | 41 | def test_separate_worktree(): 42 | """ Run 'git up' with separate work tree """ 43 | if Git().version_info[:3] < (2, 5, 1): 44 | raise SkipTest('Skip this test on Travis CI :(') 45 | 46 | os.chdir(worktree_path) 47 | 48 | from PyGitUp.gitup import GitUp 49 | gitup = GitUp(testing=True) 50 | gitup.run() 51 | 52 | assert len(gitup.states) == 1 53 | assert gitup.states[0] == 'fast-forwarding' 54 | -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request_target: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | allow-prereleases: true 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install poetry 29 | poetry install 30 | - name: Configure Git user/email 31 | run: | 32 | git config --global user.email user@example.com 33 | git config --global user.name "Example User" 34 | - name: Run test suite 35 | run: | 36 | poetry run python -m pytest -v --cov=PyGitUp 37 | shell: bash 38 | - name: Verify dist package format 39 | run: | 40 | poetry build 41 | poetry run pip install twine 42 | poetry run twine check dist/* 43 | if: matrix.os != 'windows-latest' 44 | - name: Upload coverage result 45 | if: github.repository_owner == 'msiemens' 46 | env: 47 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 48 | run: | 49 | poetry run coveralls 50 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_faster_fastforwarded.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'faster-forwarded' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | global master, repo 14 | master_path, master = init_master(test_name) 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | # Clone to test repo 20 | path = join(basepath, test_name) 21 | 22 | master.clone(path, b=test_name) 23 | repo = Repo(path, odbt=GitCmdObjectDB) 24 | repo.git.checkout('origin/' + test_name, b=test_name + '.2') 25 | 26 | assert repo.working_dir == path 27 | 28 | # Modify file in master 29 | update_file(master, test_name) 30 | 31 | 32 | def test_faster_forwarded(): 33 | """ Run 'git up' with result: (fast) fast-forwarding """ 34 | os.chdir(repo_path) 35 | 36 | assert master.branches[test_name].commit != repo.branches[test_name].commit 37 | assert master.branches[test_name].commit != repo.branches[test_name + '.2'].commit 38 | 39 | from PyGitUp.gitup import GitUp 40 | gitup = GitUp(testing=True) 41 | gitup.run() 42 | 43 | assert len(gitup.states) == 2 44 | assert gitup.states[0] == 'fast-forwarding' 45 | assert gitup.states[1] == 'fast-forwarding' 46 | assert master.branches[test_name].commit == repo.branches[test_name].commit 47 | assert master.branches[test_name].commit == repo.branches[test_name + '.2'].commit 48 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_multiple_remotes.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file 7 | 8 | test_name = 'multiple-remotes' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def setup_function(): 13 | master1_path, master1 = init_master(test_name + '.1') 14 | 15 | # Prepare master repo 16 | master1.git.checkout(b=test_name) 17 | 18 | # Clone to test repo 19 | path = join(basepath, test_name) 20 | 21 | master1.clone(path, b=test_name) 22 | repo = Repo(path, odbt=GitCmdObjectDB) 23 | 24 | assert repo.working_dir == path 25 | 26 | # Modify file in master 27 | update_file(master1, test_name) 28 | 29 | # Create second remote 30 | master2_path = join(basepath, 'master.' + test_name + '.2') 31 | master1.clone(master2_path, b=test_name) 32 | master2 = Repo(master2_path, odbt=GitCmdObjectDB) 33 | 34 | # Add second master as remote, too 35 | repo.git.checkout(b=test_name + '.2') 36 | repo.git.remote('add', 'upstream', master2_path) 37 | repo.git.fetch(all=True) 38 | repo.git.branch(set_upstream_to='upstream/' + test_name) 39 | 40 | update_file(master2, test_name) 41 | 42 | 43 | def test_fast_forwarded(): 44 | """ Run 'git up' with multiple remotes """ 45 | os.chdir(repo_path) 46 | 47 | from PyGitUp.gitup import GitUp 48 | gitup = GitUp(testing=True) 49 | gitup.run() 50 | 51 | assert len(gitup.states) == 2 52 | assert gitup.states[0] == 'fast-forwarding' 53 | assert gitup.states[1] == 'fast-forwarding' 54 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_out_of_tree.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, write_file, init_master 7 | 8 | test_name = 'out-of-tree' 9 | testfile_name = 'file' 10 | 11 | repo_path = join(basepath, test_name + os.sep) 12 | git_dir = join(repo_path, 'git-dir', '.git') 13 | work_tree = join(repo_path, 'work-tree') 14 | 15 | 16 | def setup_function(): 17 | master_path, master = init_master(test_name) 18 | 19 | os.makedirs(git_dir) 20 | os.makedirs(work_tree) 21 | 22 | # Prepare master repo 23 | master.git.checkout(b=test_name) 24 | 25 | # Create work tree 26 | with open(join(work_tree, '.git'), 'w') as f: 27 | f.write('gitdir: ' + git_dir) 28 | 29 | # Clone master 30 | os.environ['GIT_DIR'] = git_dir 31 | os.environ['GIT_WORK_TREE'] = work_tree 32 | 33 | repo = Repo.init(work_tree) 34 | repo.git.remote('add', 'origin', master_path) 35 | repo.git.fetch('origin') 36 | repo.git.checkout('origin/' + test_name, b=test_name) 37 | 38 | del os.environ['GIT_DIR'] 39 | del os.environ['GIT_WORK_TREE'] 40 | 41 | # Modify file in our repo 42 | repo_path_file = join(master_path, testfile_name) 43 | write_file(repo_path_file, 'line 1\nline 2\ncounter: 2') 44 | master.index.add([repo_path_file]) 45 | master.index.commit(test_name) 46 | 47 | 48 | def test_out_of_tree(): 49 | """ Run 'git up' with an out-of-tree source """ 50 | os.chdir(work_tree) 51 | 52 | from PyGitUp.gitup import GitUp 53 | gitup = GitUp(testing=True) 54 | gitup.run() 55 | 56 | assert gitup.states == ['fast-forwarding'] 57 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_log_hook.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | import platform 4 | from os.path import join 5 | 6 | from git import * 7 | from PyGitUp.tests import basepath, init_master, update_file 8 | 9 | test_name = 'log-hook' 10 | repo_path = join(basepath, test_name + os.sep) 11 | 12 | 13 | def setup_function(): 14 | master_path, master = init_master(test_name) 15 | 16 | # Prepare master repo 17 | master.git.checkout(b=test_name) 18 | 19 | # Clone to test repo 20 | path = join(basepath, test_name) 21 | 22 | master.clone(path, b=test_name) 23 | repo = Repo(path, odbt=GitCmdObjectDB) 24 | 25 | assert repo.working_dir == path 26 | 27 | # Set git-up.rebase.log-hook 28 | if platform.system() == 'Windows': 29 | repo.git.config( 30 | 'git-up.rebase.log-hook', 31 | 'IF [%1]==[] exit 1; ' # Note: this whole string is one line 32 | 'IF [%2]==[] exit 1; ' # and will be split by 'git up' to 33 | 'git log -n 1 $1 > nul; ' # multiple lines. 34 | 'git log -n 1 $2 > nul;' 35 | ) 36 | else: 37 | repo.git.config( 38 | 'git-up.rebase.log-hook', 39 | 'if [ -z "$1" -a -z "$2" ]; then exit 1; fi;' 40 | 'git log -n 1 "$1" &> /dev/null; ' 41 | 'git log -n 1 "$2" &> /dev/null;' 42 | ) 43 | 44 | # Modify file in master 45 | update_file(master, test_name) 46 | 47 | 48 | def test_log_hook(): 49 | """ Run 'git up' with log-hook""" 50 | os.chdir(repo_path) 51 | 52 | from PyGitUp.gitup import GitUp 53 | gitup = GitUp(testing=True) 54 | gitup.run() 55 | 56 | assert len(gitup.states) == 1 57 | assert gitup.states[0] == 'fast-forwarding' 58 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_nofetch.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os.path 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, capture, update_file, init_master 7 | 8 | test_name = 'no-fetch' 9 | new_branch_name = test_name + '.2' 10 | another_file_name = 'another_file.txt' 11 | 12 | origin_test_name = 'origin/' + test_name 13 | 14 | repo_path = join(basepath, test_name + os.sep) 15 | 16 | 17 | def setup_function(): 18 | master_path, master = init_master(test_name) 19 | 20 | # Prepare master repo 21 | master.git.checkout(b=test_name) 22 | 23 | # Clone to test repo 24 | path = join(basepath, test_name) 25 | master.clone(path, b=test_name) 26 | 27 | repo = Repo(path, odbt=GitCmdObjectDB) 28 | 29 | assert repo.working_dir == path 30 | 31 | # Create new local branch and set upstream 32 | repo.git.checkout(b=new_branch_name) 33 | repo.git.branch(u=origin_test_name) 34 | 35 | # Make non-conflicting change in new branch 36 | update_file(repo, new_branch_name, filename=another_file_name) 37 | 38 | # Modify file in master 39 | update_file(master, test_name) 40 | 41 | # Update first branch 42 | repo.git.checkout(test_name) 43 | repo.git.pull() 44 | 45 | 46 | def test_no_fetch(): 47 | """ Run 'git up' with '--no-fetch' argument """ 48 | os.chdir(repo_path) 49 | 50 | from PyGitUp.gitup import GitUp 51 | gitup = GitUp(testing=True) 52 | gitup.should_fetch = False 53 | 54 | with capture() as [stdout, _]: 55 | gitup.run() 56 | 57 | stdout = stdout.getvalue() 58 | 59 | assert 'Fetching' not in stdout 60 | 61 | assert 'rebasing' in stdout 62 | assert 'up to date' in stdout 63 | assert test_name in stdout 64 | assert new_branch_name in stdout 65 | 66 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_rebase_arguments.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import RebaseError 8 | from PyGitUp.tests import basepath, write_file, init_master, update_file, testfile_name 9 | 10 | test_name = 'rebase-arguments' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def _read_file(path): 15 | with open(path) as f: 16 | return f.read() 17 | 18 | 19 | def setup_function(): 20 | master_path, master = init_master(test_name) 21 | 22 | # Prepare master repo 23 | master.git.checkout(b=test_name) 24 | 25 | # Clone to test repo 26 | path = join(basepath, test_name) 27 | 28 | master.clone(path, b=test_name) 29 | repo = Repo(path, odbt=GitCmdObjectDB) 30 | 31 | assert repo.working_dir == path 32 | 33 | # Modify file in master 34 | master_file = update_file(master, test_name) 35 | 36 | # Modify file in our repo 37 | contents = _read_file(master_file) 38 | contents = contents.replace('line 1', 'line x') 39 | repo_file = join(path, testfile_name) 40 | 41 | write_file(repo_file, contents) 42 | repo.index.add([repo_file]) 43 | repo.index.commit(test_name) 44 | 45 | # Set git-up.rebase.arguments to '--abort', what results in an 46 | # invalid cmd and thus git returning an error, that we look for. 47 | repo.git.config('git-up.rebase.arguments', '--abort') 48 | 49 | 50 | def test_rebase_arguments(): 51 | """ Run 'git up' with rebasing.arguments """ 52 | os.chdir(repo_path) 53 | 54 | from PyGitUp.gitup import GitUp 55 | gitup = GitUp(testing=True) 56 | 57 | with pytest.raises(RebaseError): 58 | gitup.run() 59 | 60 | assert len(gitup.states) == 1 61 | assert gitup.states[0] == 'rebasing' 62 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_submodules_dirty.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file, write_file 7 | 8 | test_name = 'submodule-dirty' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def _read_file(path): 13 | with open(path) as f: 14 | return f.read() 15 | 16 | 17 | def setup_function(): 18 | master_path, master = init_master(test_name) 19 | 20 | # Prepare master repo 21 | master.git.checkout(b=test_name) 22 | 23 | # Crate test repo 24 | path = join(basepath, test_name) 25 | master.clone(path, b=test_name) 26 | repo = Repo(path, odbt=GitCmdObjectDB) 27 | # repo = Repo.init(path) 28 | # update_file(repo, 'Initial commit') 29 | 30 | os.chdir(path) 31 | assert repo.working_dir == path 32 | 33 | # Rename test repo branch 34 | repo.git.branch(test_name + '_renamed', m=True) 35 | 36 | # Add subrepo 37 | write_file(join(path, '.gitmodules'), '') 38 | repo.create_submodule('sub', 'sub', master_path) 39 | repo.git.add('.gitmodules', 'sub/') 40 | repo.git.commit(m='Added submodule') 41 | repo.git.submodule('init') 42 | 43 | # Modify file in master 44 | update_file(master, test_name) 45 | 46 | 47 | def test_submodules_dirty(): 48 | """ Run 'git up' with submodules in a dirty repo """ 49 | repo = Repo(repo_path) 50 | repo_head = repo.head.commit.hexsha 51 | submod_head = repo.submodules[0].hexsha 52 | 53 | # Change file in submodule 54 | write_file('sub/file', 'submodule changed') 55 | 56 | from PyGitUp.gitup import GitUp 57 | gitup = GitUp(testing=True) 58 | 59 | # PyGitUp uses the main repo 60 | assert repo_head == gitup.git.repo.head.commit.hexsha 61 | 62 | gitup.run() 63 | 64 | assert len(gitup.states) == 1 65 | assert gitup.states[0] == 'rebasing' 66 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_stash_error.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | import pytest 6 | from git import * 7 | from PyGitUp.git_wrapper import StashError 8 | from PyGitUp.tests import basepath, write_file, init_master, testfile_name 9 | 10 | test_name = 'stash_error' 11 | repo_path = join(basepath, test_name + os.sep) 12 | 13 | 14 | def setup_function(): 15 | master_path, master = init_master(test_name) 16 | 17 | # Prepare master repo 18 | master.git.checkout(b=test_name) 19 | 20 | # Clone to test repo 21 | path = join(basepath, test_name) 22 | 23 | master.clone(path, b=test_name) 24 | repo = Repo(path, odbt=GitCmdObjectDB) 25 | testfile_path = join(path, testfile_name) 26 | 27 | assert repo.working_dir == path 28 | 29 | # Modify file in master 30 | master_path_file = join(master_path, testfile_name) 31 | write_file(master_path_file, 'contents1') 32 | master.index.add([master_path_file]) 33 | master.index.commit(test_name) 34 | 35 | # Create unmerged paths in working dir 36 | branch_master = repo.active_branch 37 | branch_changed = repo.create_head(test_name + '.branch') 38 | branch_changed.set_commit('HEAD') 39 | branch_changed.checkout() 40 | write_file(testfile_path, 'contents1') 41 | repo.index.add([testfile_path]) 42 | repo.index.commit('Update in branch') 43 | 44 | branch_master.checkout() 45 | write_file(testfile_path, 'contents2') 46 | repo.index.add([testfile_path]) 47 | repo.index.commit('Update in origin') 48 | 49 | try: 50 | repo.git.merge(test_name + '.branch') 51 | except GitCommandError: 52 | pass 53 | 54 | 55 | def test_stash_error(): 56 | """ Run 'git up' with an error while stashing """ 57 | os.chdir(repo_path) 58 | 59 | from PyGitUp.gitup import GitUp 60 | gitup = GitUp(testing=True) 61 | 62 | with pytest.raises(StashError): 63 | gitup.run() 64 | -------------------------------------------------------------------------------- /PyGitUp/tests/test_submodules.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from os.path import join 4 | 5 | from git import * 6 | from PyGitUp.tests import basepath, init_master, update_file, write_file 7 | 8 | test_name = 'submodule' 9 | repo_path = join(basepath, test_name + os.sep) 10 | 11 | 12 | def _read_file(path): 13 | with open(path) as f: 14 | return f.read() 15 | 16 | 17 | def setup_function(): 18 | master_path, master = init_master(test_name) 19 | 20 | # Prepare master repo 21 | master.git.checkout(b=test_name) 22 | 23 | # Crate test repo 24 | path = join(basepath, test_name) 25 | master.clone(path, b=test_name) 26 | repo = Repo(path, odbt=GitCmdObjectDB) 27 | # repo = Repo.init(path) 28 | # update_file(repo, 'Initial commit') 29 | 30 | os.chdir(path) 31 | assert repo.working_dir == path 32 | 33 | # Rename test repo branch 34 | repo.git.branch(test_name + '_renamed', m=True) 35 | 36 | # Add subrepo 37 | write_file(join(path, '.gitmodules'), '') 38 | repo.create_submodule('sub', 'sub', master_path) 39 | repo.git.add('.gitmodules', 'sub/') 40 | repo.git.commit(m='Added submodule') 41 | repo.git.submodule('init') 42 | 43 | # Modify file in master 44 | update_file(master, test_name) 45 | 46 | 47 | def test_submodules(): 48 | """ Run 'git up' with submodules """ 49 | repo = Repo(repo_path) 50 | repo_head = repo.head.commit.hexsha 51 | submod_head = repo.submodules[0].hexsha 52 | 53 | os.chdir(join(repo_path, 'sub')) 54 | 55 | from PyGitUp.gitup import GitUp 56 | gitup = GitUp(testing=True) 57 | 58 | # PyGitUp uses the submodule instead of the toplevel git repo 59 | assert submod_head == gitup.git.repo.head.commit.hexsha 60 | 61 | gitup.run() 62 | 63 | repo = Repo(repo_path) 64 | 65 | assert len(gitup.states) == 1 66 | assert gitup.states[0] == 'fast-forwarding' 67 | 68 | # Repo itself is unchanged: 69 | assert repo.head.commit.hexsha, repo_head 70 | # Submodule is changed: 71 | assert gitup.git.repo.head.commit.hexsha != submod_head 72 | -------------------------------------------------------------------------------- /PyGitUp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | from tempfile import mkdtemp 3 | 4 | import contextlib 5 | import os 6 | from git import * 7 | 8 | basepath = mkdtemp(prefix='PyGitUp.') 9 | testfile_name = 'file.txt' 10 | 11 | 12 | @contextlib.contextmanager 13 | def capture(): 14 | import sys 15 | from io import StringIO 16 | oldout, olderr = sys.stdout, sys.stderr 17 | out = None 18 | try: 19 | out = [StringIO(), StringIO()] 20 | sys.stdout, sys.stderr = out 21 | yield out 22 | finally: 23 | sys.stdout, sys.stderr = oldout, olderr 24 | if out: 25 | out[0] = out[0].getvalue() 26 | out[1] = out[1].getvalue() 27 | 28 | 29 | def teardown_function(): 30 | """ 31 | Cleanup created files and directories 32 | """ 33 | import shutil 34 | import stat 35 | 36 | def onerror(func, path, _): 37 | if not os.access(path, os.W_OK): 38 | os.chmod(path, stat.S_IWUSR) 39 | func(path) 40 | else: 41 | raise 42 | 43 | os.chdir(join(basepath, '..')) 44 | shutil.rmtree(basepath, onerror=onerror) 45 | 46 | 47 | def write_file(path, contents): 48 | with open(path, 'w+') as f: 49 | f.write(contents) 50 | 51 | 52 | # noinspection PyDefaultArgument 53 | def update_file(repo, commit_message='', counter=[0], filename=testfile_name): 54 | """ 55 | Update 'testfile_name' using an increasing counter and commit the changes. 56 | """ 57 | counter[0] += 1 # See: http://stackoverflow.com/a/279592/997063 58 | 59 | path_file = join(repo.working_dir, filename) 60 | contents = f'line 1\nline 2\ncounter: {counter[0]}' 61 | write_file(path_file, contents) 62 | 63 | repo.index.add([path_file]) 64 | repo.index.commit(commit_message) 65 | 66 | return path_file 67 | 68 | 69 | def mkrepo(path): 70 | """ 71 | Make a repository in 'path', create the dir, if it doesn't exist. 72 | """ 73 | return Repo.init(path) 74 | 75 | 76 | def init_master(test_name): 77 | """ 78 | Initialize the master repo and create & commit a file. 79 | """ 80 | # Create repo 81 | path = join(basepath, 'master.' + test_name) 82 | repo = mkrepo(path) 83 | 84 | assert repo.working_dir == path 85 | 86 | # Add file 87 | update_file(repo, 'Initial commit') 88 | repo.git.checkout(b='initial') 89 | 90 | return path, repo 91 | -------------------------------------------------------------------------------- /PyGitUp/git_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | A wrapper extending GitPython's repo.git. 3 | 4 | This wrapper class provides support for stdout messages in Git Exceptions 5 | and (nearly) realtime stdout output. In addition, some methods of the 6 | original repo.git are shadowed by custom methods providing functionality 7 | needed for `git up`. 8 | """ 9 | 10 | 11 | __all__ = ['GitWrapper', 'GitError'] 12 | 13 | ############################################################################### 14 | # IMPORTS 15 | ############################################################################### 16 | 17 | # Python libs 18 | import sys 19 | import re 20 | import subprocess 21 | import platform 22 | from contextlib import contextmanager 23 | from io import BufferedReader 24 | from threading import Thread 25 | from typing import IO, Optional, List 26 | 27 | # 3rd party libs 28 | from termcolor import colored # Assume, colorama is already initialized 29 | from git import GitCommandError, CheckoutError as OrigCheckoutError, Git 30 | from git.cmd import Git as GitCmd 31 | 32 | # PyGitUp libs 33 | from PyGitUp.utils import find 34 | 35 | 36 | ############################################################################### 37 | # GitWrapper 38 | ############################################################################### 39 | 40 | class GitWrapper: 41 | """ 42 | A wrapper for repo.git providing better stdout handling + better exceptions. 43 | 44 | It is preferred to repo.git because it doesn't print to stdout 45 | in real time. In addition, this wrapper provides better error 46 | handling (it provides stdout messages inside the exception, too). 47 | """ 48 | 49 | def __init__(self, repo): 50 | if repo: 51 | #: :type: git.Repo 52 | self.repo = repo 53 | #: :type: git.Git 54 | self.git = self.repo.git 55 | else: 56 | #: :type: git.Git 57 | self.git = Git() 58 | 59 | def __del__(self): 60 | # Is the following true? 61 | 62 | # GitPython runs persistent git processes in the working directory. 63 | # Therefore, when we use 'git up' in something like a test environment, 64 | # this might cause troubles because of the open file handlers (like 65 | # trying to remove the directory right after the test has finished). 66 | # 'clear_cache' kills the processes... 67 | 68 | if platform.system() == 'Windows': # pragma: no cover 69 | pass 70 | # ... or rather "should kill", because but somehow it recently 71 | # started to not kill cat_file_header out of the blue (I even 72 | # tried running old code, but the once working code failed). 73 | # Thus, we kill it manually here. 74 | if self.git.cat_file_header is not None: 75 | subprocess.call(("TASKKILL /F /T /PID {} 2>nul 1>nul".format( 76 | str(self.git.cat_file_header.proc.pid) 77 | )), shell=True) 78 | if self.git.cat_file_all is not None: 79 | subprocess.call(("TASKKILL /F /T /PID {} 2>nul 1>nul".format( 80 | str(self.git.cat_file_all.proc.pid) 81 | )), shell=True) 82 | 83 | self.git.clear_cache() 84 | 85 | def _run(self, name, *args, **kwargs): 86 | 87 | """ Run a git command specified by name and args/kwargs. """ 88 | 89 | stdout = b'' 90 | cmd = getattr(self.git, name) 91 | 92 | # Ask cmd(...) to return a (status, stdout, stderr) tuple 93 | kwargs['with_extended_output'] = True 94 | 95 | # Execute command 96 | try: 97 | (_, stdout, _) = cmd(*args, **kwargs) 98 | except GitCommandError as error: 99 | # Add more meta-information to errors 100 | message = "'{}' returned exit status {}".format( 101 | ' '.join(str(c) for c in error.command), 102 | error.status 103 | ) 104 | 105 | raise GitError(message, stderr=error.stderr, stdout=stdout) 106 | 107 | return stdout.strip() 108 | 109 | def __getattr__(self, name): 110 | return lambda *args, **kwargs: self._run(name, *args, **kwargs) 111 | 112 | ########################################################################### 113 | # Overwrite some methods and add new ones 114 | ########################################################################### 115 | 116 | @contextmanager 117 | def stasher(self): 118 | """ 119 | A stashing contextmanager. 120 | """ 121 | # nonlocal for python2 122 | stashed = [False] 123 | clean = [False] 124 | 125 | def stash(): 126 | if clean[0] or not self.repo.is_dirty(submodules=False): 127 | clean[0] = True 128 | return 129 | if stashed[0]: 130 | return 131 | 132 | if self.change_count > 1: 133 | message = 'stashing {0} changes' 134 | else: 135 | message = 'stashing {0} change' 136 | print(colored( 137 | message.format(self.change_count), 138 | 'magenta' 139 | )) 140 | try: 141 | self._run('stash') 142 | except GitError as git_error: 143 | raise StashError(stderr=git_error.stderr, stdout=git_error.stdout) 144 | 145 | stashed[0] = True 146 | 147 | yield stash 148 | 149 | if stashed[0]: 150 | print(colored('unstashing', 'magenta')) 151 | try: 152 | self._run('stash', 'pop') 153 | except GitError as e: 154 | raise UnstashError(stderr=e.stderr, stdout=e.stdout) 155 | 156 | def checkout(self, branch_name): 157 | """ Checkout a branch by name. """ 158 | try: 159 | find( 160 | self.repo.branches, lambda b: b.name == branch_name 161 | ).checkout() 162 | except OrigCheckoutError as e: 163 | raise CheckoutError(branch_name, details=e) 164 | 165 | def rebase(self, target_branch): 166 | """ Rebase to target branch. """ 167 | current_branch = self.repo.active_branch 168 | 169 | arguments = ( 170 | ([self.config('git-up.rebase.arguments')] or []) + 171 | [target_branch.name] 172 | ) 173 | try: 174 | self._run('rebase', *arguments) 175 | except GitError as e: 176 | raise RebaseError(current_branch.name, target_branch.name, 177 | **e.__dict__) 178 | 179 | def fetch(self, *args, **kwargs): 180 | """ Fetch remote commits. """ 181 | 182 | # Execute command 183 | cmd = self.git.fetch(as_process=True, *args, **kwargs) 184 | 185 | return self.run_cmd(cmd) 186 | 187 | def push(self, *args, **kwargs): 188 | """ Push commits to remote """ 189 | # Execute command 190 | cmd = self.git.push(as_process=True, *args, **kwargs) 191 | 192 | return self.run_cmd(cmd) 193 | 194 | @staticmethod 195 | def stream_reader(input_stream: BufferedReader, output_stream: Optional[IO], result_list: List[str]) -> None: 196 | """ 197 | Helper method to read from a stream and write to another stream. 198 | 199 | We use a list to store results because they are mutable and allow 200 | for passing data back to the caller from the thread without additional 201 | machinery. 202 | """ 203 | captured_bytes = b"" 204 | while True: 205 | read_byte = input_stream.read(1) 206 | captured_bytes += read_byte 207 | if output_stream is not None: 208 | output_stream.write(read_byte.decode('utf-8')) 209 | output_stream.flush() 210 | if read_byte == b"": 211 | break 212 | result_list.append(captured_bytes) 213 | 214 | @staticmethod 215 | def run_cmd(cmd: GitCmd.AutoInterrupt) -> bytes: 216 | """ Run a command and return stdout. """ 217 | std_outs = [] 218 | std_errs = [] 219 | stdout_thread = Thread(target=GitWrapper.stream_reader, 220 | args=(cmd.stdout, sys.stdout, std_outs)) 221 | stderr_thread = Thread(target=GitWrapper.stream_reader, 222 | args=(cmd.stderr, None, std_errs)) 223 | 224 | # Wait for the process to quit 225 | try: 226 | stdout_thread.start() 227 | stderr_thread.start() 228 | cmd.wait() 229 | stdout_thread.join() 230 | stderr_thread.join() 231 | except GitCommandError as error: 232 | # Add more meta-information to errors 233 | message = "'{}' returned exit status {}".format( 234 | ' '.join(str(c) for c in error.command), 235 | error.status 236 | ) 237 | 238 | raise GitError(message, stderr=error.stderr, stdout=std_outs[0] if std_outs else None) 239 | 240 | return std_outs[0].strip() if std_outs else bytes() 241 | 242 | def config(self, key): 243 | """ Return `git config key` output or None. """ 244 | try: 245 | return self.git.config(key) 246 | except GitCommandError: 247 | return None 248 | 249 | @property 250 | def change_count(self): 251 | """ The number of changes in the working directory. """ 252 | status = self.git.status(porcelain=True, untracked_files='no').strip() 253 | if not status: 254 | return 0 255 | else: 256 | return len(status.split('\n')) 257 | 258 | @property 259 | def version(self): 260 | """ 261 | Return git's version as a list of numbers. 262 | 263 | The original repo.git.version_info has problems with tome types of 264 | git version strings. 265 | """ 266 | return re.search(r'\d+(\.\d+)+', self.git.version()).group(0) 267 | 268 | def is_version_min(self, required_version): 269 | """ Does git's version match the requirements? """ 270 | return self.version.split('.') >= required_version.split('.') 271 | 272 | 273 | ############################################################################### 274 | # GitError + subclasses 275 | ############################################################################### 276 | 277 | class GitError(Exception): 278 | """ 279 | Extension of the GitCommandError class. 280 | 281 | New: 282 | - stdout 283 | - details: a 'nested' exception with more details 284 | """ 285 | 286 | def __init__(self, message=None, stderr=None, stdout=None, details=None): 287 | # super(GitError, self).__init__((), None, stderr) 288 | self.details = details 289 | self.message = message 290 | 291 | self.stderr = stderr 292 | self.stdout = stdout 293 | 294 | def __str__(self): # pragma: no cover 295 | return self.message 296 | 297 | 298 | class StashError(GitError): 299 | """ 300 | Error while stashing 301 | """ 302 | 303 | def __init__(self, **kwargs): 304 | kwargs.pop('message', None) 305 | GitError.__init__(self, 'Stashing failed!', **kwargs) 306 | 307 | 308 | class UnstashError(GitError): 309 | """ 310 | Error while unstashing 311 | """ 312 | 313 | def __init__(self, **kwargs): 314 | kwargs.pop('message', None) 315 | GitError.__init__(self, 'Unstashing failed!', **kwargs) 316 | 317 | 318 | class CheckoutError(GitError): 319 | """ 320 | Error during checkout 321 | """ 322 | 323 | def __init__(self, branch_name, **kwargs): 324 | kwargs.pop('message', None) 325 | GitError.__init__(self, 'Failed to checkout ' + branch_name, 326 | **kwargs) 327 | 328 | 329 | class RebaseError(GitError): 330 | """ 331 | Error during rebase command 332 | """ 333 | 334 | def __init__(self, current_branch, target_branch, **kwargs): 335 | # Remove kwargs we won't pass to GitError 336 | kwargs.pop('message', None) 337 | kwargs.pop('command', None) 338 | kwargs.pop('status', None) 339 | 340 | message = "Failed to rebase {1} onto {0}".format( 341 | current_branch, target_branch 342 | ) 343 | GitError.__init__(self, message, **kwargs) 344 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyGitUp |Version| |Build Status| |Coverage Status| 2 | ================================================== 3 | 4 | |PyGitUp|_ is a Python port of 5 | `aanand/git-up `__. It not only 6 | fully covers the abilities of git-up and should be a drop-in replacement, 7 | but also extends it slightly. 8 | 9 | .. |PyGitUp| replace:: ``PyGitUp`` 10 | .. _PyGitUp: https://github.com/msiemens/PyGitUp 11 | 12 | Why use ``git up``? 13 | ------------------- 14 | 15 | git pull has two problems: 16 | 17 | * It merges upstream changes by default, when it's really more polite to `rebase 18 | over them `__, 19 | unless your collaborators enjoy a commit graph that looks like bedhead. 20 | 21 | * It only updates the branch you're currently on, which means git push will 22 | shout at you for being behind on branches you don't particularly care about 23 | right now. 24 | 25 | (https://github.com/aanand/git-up/) 26 | 27 | Demonstration 28 | ------------- 29 | 30 | .. image:: http://i.imgur.com/EC3pvYu.gif 31 | 32 | Why use the Python port? 33 | ------------------------ 34 | 35 | I wasn't able to use the original ``git-up``, because I didn't want to install 36 | a whole Ruby suite just for `git-up` and even with Ruby installed, there were 37 | some problems running on my Windows machine. So, my reasons for writing 38 | and using this port are: 39 | 40 | 1. Windows support. 41 | 2. Written in Python ;) 42 | 43 | How do I install it? 44 | -------------------- 45 | 46 | 1. Install ``git-up`` via `pip `__: ``$ pip install git-up`` 47 | 2. ``cd`` to your project's directory. 48 | 3. Run ``git up`` and enjoy! 49 | 50 | Homebrew users can also use ``brew``: ``brew install pygitup`` 51 | 52 | How to run it locally? 53 | ---------------------- 54 | 55 | Could also checkout the **.github/workflows/ci-workflow.yml** 56 | 57 | 1. clone repo and ``cd`` to repo directory. 58 | 2. Install ``poetry`` as guided by `poetry installation doc `__ 59 | 3. Run ``poetry install`` 60 | 4. Run program with ``poetry run git-up`` 61 | 5. Run all tests with ``poetry run pytest -v --cov=PyGitUp`` or ``poetry run pytest -v --cov=PyGitUp --cov-report html`` 62 | 6. Run one test with ``poetry run pytest -q PyGitUp/tests/test_version.py -v --cov=PyGitUp`` 63 | 64 | Note for Windows users: 65 | ~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | See `these instructions `__ 68 | for installing pip, if you haven't already installed it. And don't forget 69 | to either: 70 | 71 | - make your ``Python/Scripts`` and ``Python/Lib/site-packages`` writable for 72 | you, 73 | - run ``pip`` with admin privileges 74 | - or use ``pip install --user git-up`` and add ``%APPDATA%/Python/Scripts`` 75 | to ``%PATH%``. 76 | 77 | Otherwise pip will refuse to install ``git-up`` due to ``Access denied`` errors. 78 | 79 | Python version compatibility: 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | Python 3.7 and upwards are supported :) 83 | 84 | Options and Configuration 85 | ------------------------- 86 | 87 | Command Line Arguments 88 | ~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | - ``git up -h`` shows a help message. 91 | 92 | - ``git up --quiet`` suppresses all output except for error messages. 93 | 94 | - ``git up --no-fetch`` skips fetching the remote and rebases all local branches. 95 | 96 | - ``git up --version`` shows the current version and optionally checks for 97 | updates (see below). 98 | 99 | Configuration 100 | ~~~~~~~~~~~~~ 101 | 102 | To configure ``PyGitUp``, you can set options in your git config. Run 103 | ``git config [--global] git-up.[name] [value]`` to set one of these 104 | options: 105 | 106 | - ``git-up.fetch.prune [*true*|false]``: If set to ``true``, 107 | ``PyGitUp`` will append the ``--prune``\ option to ``git fetch`` and 108 | thus remove any remote tracking branches which no longer exist on 109 | the remote (see `git fetch 110 | --help `__). 111 | 112 | - ``git-up.fetch.all [true|*false*]``: If set to ``false``, ``PyGitUp`` 113 | will only fetch remotes for which there is at least one local 114 | tracking branch. Setting this option will make ``git up`` always fetch 115 | from all remotes, which is useful if e.g. you use a remote to push to 116 | your CI system but never check those branches out. 117 | 118 | - ``git-up.push.auto [true|*false*]``: Push the current branch after 119 | rebasing and fast-forwarding. 120 | 121 | - ``git-up.push.all [true|*false*]``: Push all branches when auto-pushing. 122 | 123 | - ``git-up.push.tags [true|*false*]``: Push tags when auto-pushing. 124 | 125 | - ``git-up.rebase.arguments [string]``: If set, ``PyGitUp`` will use 126 | this string as additional arguments when calling ``git rebase``. 127 | Example: ``--preserve-merges`` to recreate merge commits in the 128 | rebased branch. 129 | 130 | - ``git-up.rebase.auto [*true*|false]``: If set to ``false``, 131 | ``PyGitUp`` won't rebase your branches for you but notify you that 132 | they diverged. This can be useful if you have a lot of in-progress 133 | work that you don't want to deal with at once, but still want to 134 | update other branches. 135 | 136 | - ``git-up.rebase.log-hook [cmd]``: Runs ``cmd`` every time a branch 137 | is rebased or fast-forwarded, with the old head as ``$1`` and the new 138 | head as ``$2``. This can be used to view logs or diffs of incoming 139 | changes. Example: 140 | ``echo "changes on $1:"; git log --oneline --decorate $1..$2``. 141 | 142 | - ``git-up.rebase.show-hashes [true|*false*]``: If set to ``true``, 143 | ``PyGitUp`` will show the hashes of the current commit (or the point 144 | where the rebase starts) and the target commit like ``git pull`` does. 145 | 146 | New in v1.0.0: 147 | ~~~~~~~~~~~~~~ 148 | 149 | - ``git-up.updates.check [*true*|false]``: When running ``git up --version``, 150 | it shows the version number and checks for updates. If you feel 151 | uncomfortable with it, just set it to ``false`` to turn off the checks. 152 | 153 | Credits 154 | ------- 155 | 156 | The original ``git-up`` has been written by aanand: 157 | `aanand/git-up/ `__. 158 | 159 | 160 | Changelog 161 | --------- 162 | 163 | v2.3.0 (*2024-10-05*) 164 | ~~~~~~~~~~~~~~~~~~~~~ 165 | 166 | - Improve logging when updating large repositories. Thanks 167 | `@bdmartin `_ for `Pull Request #132 168 | `_. 169 | - Drop support for Python 3.7 170 | 171 | v2.2.0 (*2022-11-21*) 172 | ~~~~~~~~~~~~~~~~~~~~~ 173 | 174 | - Add support for Python 3.11. Thanks 175 | `@hugovk `_ for `Pull Request #118 176 | `_. 177 | 178 | v2.1.0 (*2021-10-02*) 179 | ~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | - Switch to Python's ``argparse`` for CLI argument parsing. Thanks 182 | `@ekohl `_ for `Pull Request #96 183 | `_. 184 | 185 | v2.0.3 (*2021-09-23*) 186 | ~~~~~~~~~~~~~~~~~~~~~ 187 | 188 | - Drop support for Python 3.6 (following GitPython) 189 | - Update PyGitUp's CLI argument parser `Click `_ 190 | to version 8.0. Thanks `@hugovk `_ 191 | for `Pull Request #109 `_. 192 | - Update other dependencies 193 | 194 | v2.0.2 (*2020-12-30*) 195 | ~~~~~~~~~~~~~~~~~~~~~ 196 | 197 | - Remove old Python 2 code. Thanks `@hugovk `_ 198 | for `Pull Request #104 `_. 199 | 200 | v2.0.1 (*2020-08-26*) 201 | ~~~~~~~~~~~~~~~~~~~~~ 202 | 203 | - Update dependencies 204 | 205 | v2.0.0 (*2020-08-15*) 206 | ~~~~~~~~~~~~~~~~~~~~~ 207 | 208 | - Drop Python 2 support in order to fix `Issue 102 `_ 209 | - Drop Ruby Bundler integration 210 | - Migrate tests to ``py.test`` 211 | 212 | v1.6.1 (*2018-12-12*) 213 | ~~~~~~~~~~~~~~~~~~~~~ 214 | 215 | - Upgrade to click>=7.0.0. Thanks `@no-preserve-root `_ 216 | for `Pull Request #87 `_. 217 | 218 | v1.6.0 (*2018-10-26*) 219 | ~~~~~~~~~~~~~~~~~~~~~ 220 | 221 | - Skip stashing changes when possible. Thanks `@Chronial `_ 222 | for `Pull Request #86 `_. 223 | - Added faster fast-forward on branches that are not checked out. Thanks `@Chronial `_ 224 | for `Pull Request #83 `_. 225 | 226 | v1.5.2 (*2018-09-28*) 227 | ~~~~~~~~~~~~~~~~~~~~~ 228 | 229 | - Fixed version requirement for Click dependency (`#82 `__). 230 | 231 | v1.5.1 (*2018-09-13*) 232 | ~~~~~~~~~~~~~~~~~~~~~ 233 | 234 | - Fixed crash on Cygwin with rebase log hook enabled (`#80 `__). 235 | 236 | v1.5.0 (*2018-04-26*) 237 | ~~~~~~~~~~~~~~~~~~~~~ 238 | 239 | - Added auto-push support. Thanks `@WoLpH `_ 240 | for `Pull Request #74 `_. 241 | 242 | v1.4.7 (*2018-04-07*) 243 | ~~~~~~~~~~~~~~~~~~~~~ 244 | 245 | - Added shorthand commandline arguments (``-V, -q, -h``, see `#73 `__). 246 | 247 | v1.4.6 (*2017-12-19*) 248 | ~~~~~~~~~~~~~~~~~~~~~ 249 | 250 | - 3rd party dependencies have been updated (see `#65 `__). 251 | 252 | v1.4.5 (*2017-01-02*) 253 | ~~~~~~~~~~~~~~~~~~~~~ 254 | 255 | - Fixed problems when working with branches containing hash signs in their name 256 | (`#55 `__). 257 | - No longer installs a now unneeded script on ``pip install``. Thanks `@ekohl `_ 258 | for `Pull Request #60 `_. 259 | 260 | v1.4.4 (*2016-11-30*) 261 | ~~~~~~~~~~~~~~~~~~~~~ 262 | 263 | - Fixed a bug when working with ``git worktree`` (`#58 `__). 264 | 265 | v1.4.3 (*2016-11-22*) 266 | ~~~~~~~~~~~~~~~~~~~~~ 267 | 268 | - Fixed a bug with GitPython <= 2.0.8 (`#56 `__, `#57 `__). 269 | 270 | v1.4.2 (*2016-09-29*) 271 | ~~~~~~~~~~~~~~~~~~~~~ 272 | 273 | - Switched the command line argument parsing library (`#53 `__). 274 | 275 | v1.4.1 (*2016-08-02*) 276 | ~~~~~~~~~~~~~~~~~~~~~ 277 | 278 | - Include tests in PyPI distribution (`#51 `__). 279 | 280 | v1.4.0 (*2016-02-29*) 281 | ~~~~~~~~~~~~~~~~~~~~~ 282 | 283 | - 3rd party dependencies have been updated. 284 | - Dependencies on 3rd party libraries have been loosened to better interact with other installed packages. 285 | Thanks `MaximilianR `_ for `Pull Request #45 `_. 286 | - Added an command line argument to turn of fetching (``--no-fetch``). Thanks `@buoto `_ 287 | for `Pull Request #46 `_. 288 | - Don't show a stacktrace anymore when stashing fails (`#35 `_). 289 | - Fixed a bug that caused problems with submodules if the submodule had unstashed changes/ Thanks 290 | `@Javex `_ for `Pull Request #27 `_. 291 | 292 | v1.3.1 (*2015-08-31*) 293 | ~~~~~~~~~~~~~~~~~~~~~ 294 | 295 | - Fixed a bug when showing the version on Python 3 `#34 `__. 296 | 297 | v1.3.0 (*2015-04-08*) 298 | ~~~~~~~~~~~~~~~~~~~~~ 299 | 300 | - Support for Python 3 has been added. Thanks `@r4ts0n `_ 301 | for `Pull Request #23 `_ 302 | and `@Byron `_ for quickly merging a Pull Request 303 | in `GitPython `_ 304 | and releasing a new version on which this release depends. 305 | 306 | v1.2.2 (*2015-02-23*) 307 | ~~~~~~~~~~~~~~~~~~~~~ 308 | 309 | - Now updates submodules when called from ``git submodule foreach`` (`#8 `__). 310 | 311 | v1.2.1 (*2014-12-16*) 312 | ~~~~~~~~~~~~~~~~~~~~~ 313 | 314 | - Fixed a problem with ``setuptools 8.x`` (`#19 `__). 315 | - 3rd party dependencies have been updated 316 | 317 | v1.2.0 (*2014-12-10*) 318 | ~~~~~~~~~~~~~~~~~~~~~ 319 | 320 | - Added an option to show hashes when fast-forwarding/rebasing like ``git pull`` 321 | does (``git-up.rebase.show-hashes``). 322 | - Fixed a bug when having branches with both local tracking branches and 323 | remote tracking branches (`#17 `__). 324 | 325 | v1.1.5 (*2014-11-19*) 326 | ~~~~~~~~~~~~~~~~~~~~~ 327 | 328 | - 3rd party dependencies have been updated to fix a problem with a 3rd party 329 | library (`#18 `__). 330 | 331 | v1.1.4 (*2014-04-18*) 332 | ~~~~~~~~~~~~~~~~~~~~~ 333 | 334 | - Fixed some typos in README and ``PyGitUp`` output. 335 | - 3rd party dependencies have been updated. 336 | 337 | v1.1.3 (*2014-03-23*) 338 | ~~~~~~~~~~~~~~~~~~~~~ 339 | 340 | - ``ahead of upstream`` messages are now cyan (see `aanand/git-up#60 `__). 341 | - Fixed problem when using % in the log hook (`#11 `__). 342 | 343 | v1.1.2 (*2013-10-08*) 344 | ~~~~~~~~~~~~~~~~~~~~~ 345 | 346 | - Fixed problems with the dependency declaration. 347 | 348 | v1.1.1 (*2013-10-07*) 349 | ~~~~~~~~~~~~~~~~~~~~~ 350 | 351 | - Fix for `#7 `__ 352 | (AttributeError: 'GitUp' object has no attribute 'git') introduced by 353 | v1.1.0. 354 | 355 | v1.1.0 (*2013-10-07*) 356 | ~~~~~~~~~~~~~~~~~~~~~ 357 | 358 | - Prior to v1.1.0, ``PyGitUp`` tried to guess the upstream branch for a local 359 | branch by looking for a branch on any remote with the same name. With v1.1.0, 360 | ``PyGitUp`` stops guessing and uses the upstream branch config instead. 361 | 362 | This by the way fixes issue `#6 `__ 363 | (``git up`` doesn't work with local only branches). 364 | 365 | **Note:** 366 | This change may break setups, where a local branch accidentally has 367 | the same name as a remote branch without any tracking information set. Prior 368 | to v1.1.0, ``git up`` would still fetch and rebase from the remote branch. 369 | If you run into troubles with such a setup, setting tracking information 370 | using ``git branch -u / `` should help. 371 | 372 | - 3rd party dependencies have been updated. 373 | 374 | - Allows to run ``git up --version`` from non-git dirs, too. 375 | 376 | v1.0.0 (*2013-09-05*) 377 | ~~~~~~~~~~~~~~~~~~~~~ 378 | 379 | Finally ``PyGitUp`` reaches 1.0.0. You can consider it stable now :) 380 | 381 | - Added a comprehensive test suite, now with a coverage of about 90%. 382 | - Lots of code cleanup. 383 | - Added option ``-h`` to display a help screen (``--help`` **won't** work, because 384 | ``git`` catches this option and handles it before ``PyGitUp`` can do). 385 | - Added option ``--version`` to show, what version of ``PyGitUp`` is running. 386 | Also checks for updates (can be disabled, see configuration). 387 | - Added option ``--quiet`` to be quiet and only display error messages. 388 | 389 | v0.2.3 (*2013-06-05*) 390 | ~~~~~~~~~~~~~~~~~~~~~ 391 | 392 | - Fixed issue `#4 `__ (ugly 393 | exception if remote branch has been deleted). 394 | 395 | v0.2.2 (*2013-05-04*) 396 | ~~~~~~~~~~~~~~~~~~~~~ 397 | 398 | - Fixed issue `#3 `__ (didn't 399 | return to previous branch). 400 | 401 | 402 | v0.2.1 (*2013-03-18*) 403 | ~~~~~~~~~~~~~~~~~~~~~ 404 | 405 | - Fixed problem: check-bundler.rb has not been installed when installing via 406 | PyPI (problems with setup.py). 407 | 408 | v0.2 (*2013-03-18*) 409 | ~~~~~~~~~~~~~~~~~~~ 410 | 411 | - Incorporated `aanand/git-up#41 `__: Support for ``bundle install --local`` and 413 | ``rbenv rehash``. 414 | - Fixed issue `#1 `__ (strange 415 | output buffering when having multiple remotes to fetch from). 416 | - Some under-the-hood improvements. 417 | 418 | v0.1 (*2013-03-14*) 419 | ~~~~~~~~~~~~~~~~~~~ 420 | 421 | - Initial Release 422 | 423 | .. |Build Status| image:: https://img.shields.io/github/actions/workflow/status/msiemens/PyGitUp/ci-workflow.yml?style=flat-square 424 | :target: https://dev.azure.com/msiemens/github/_build?definitionId=1 425 | 426 | .. |Coverage Status| image:: http://img.shields.io/coveralls/msiemens/PyGitUp/master.svg?style=flat-square 427 | :target: https://coveralls.io/r/msiemens/PyGitUp 428 | 429 | .. |Version| image:: http://img.shields.io/pypi/v/git-up.svg?style=flat-square 430 | :target: https://pypi.python.org/pypi/git-up 431 | -------------------------------------------------------------------------------- /PyGitUp/gitup.py: -------------------------------------------------------------------------------- 1 | from git import Git 2 | from git import GitCommandNotFound 3 | 4 | __all__ = ['GitUp'] 5 | 6 | ############################################################################### 7 | # IMPORTS and LIBRARIES SETUP 8 | ############################################################################### 9 | 10 | # Python libs 11 | import argparse 12 | import codecs 13 | import errno 14 | import sys 15 | import os 16 | import re 17 | import json 18 | import subprocess 19 | from io import StringIO 20 | from tempfile import NamedTemporaryFile 21 | from urllib.error import HTTPError, URLError 22 | from urllib.request import urlopen 23 | 24 | # 3rd party libs 25 | try: 26 | # noinspection PyUnresolvedReferences 27 | import pkg_resources as pkg 28 | except ImportError: # pragma: no cover 29 | NO_DISTRIBUTE = True 30 | else: # pragma: no cover 31 | NO_DISTRIBUTE = False 32 | 33 | import colorama 34 | from git import Repo, GitCmdObjectDB 35 | from termcolor import colored 36 | 37 | # PyGitUp libs 38 | from PyGitUp.utils import execute, uniq, find 39 | from PyGitUp.git_wrapper import GitWrapper, GitError 40 | 41 | ON_WINDOWS = sys.platform == 'win32' 42 | 43 | ############################################################################### 44 | # Setup of 3rd party libs 45 | ############################################################################### 46 | 47 | colorama.init(autoreset=True, convert=ON_WINDOWS) 48 | 49 | ############################################################################### 50 | # Setup constants 51 | ############################################################################### 52 | 53 | PYPI_URL = 'https://pypi.python.org/pypi/git-up/json' 54 | 55 | 56 | ############################################################################### 57 | # GitUp 58 | ############################################################################### 59 | 60 | def get_git_dir(): 61 | toplevel_dir = execute(['git', 'rev-parse', '--show-toplevel']) 62 | 63 | if toplevel_dir is not None \ 64 | and os.path.isfile(os.path.join(toplevel_dir, '.git')): 65 | # Not a normal git repo. Check if it's a submodule, then use 66 | # toplevel_dir. Otherwise it's a worktree, thus use common_dir. 67 | # NOTE: git worktree support only comes with git v2.5.0 or 68 | # later, on earlier versions toplevel_dir is the best we can do. 69 | 70 | cmd = ['git', 'rev-parse', '--is-inside-work-tree'] 71 | inside_worktree = execute(cmd, cwd=os.path.join(toplevel_dir, '..')) 72 | 73 | if inside_worktree == 'true' or Git().version_info[:3] < (2, 5, 0): 74 | return toplevel_dir 75 | else: 76 | return execute(['git', 'rev-parse', '--git-common-dir']) 77 | 78 | return toplevel_dir 79 | 80 | 81 | class GitUp: 82 | """ Conainter class for GitUp methods """ 83 | 84 | default_settings = { 85 | 'fetch.prune': True, 86 | 'fetch.all': False, 87 | 'rebase.show-hashes': False, 88 | 'rebase.arguments': None, 89 | 'rebase.auto': True, 90 | 'rebase.log-hook': None, 91 | 'updates.check': True, 92 | 'push.auto': False, 93 | 'push.tags': False, 94 | 'push.all': False, 95 | } 96 | 97 | def __init__(self, testing=False, sparse=False): 98 | # Sparse init: config only 99 | if sparse: 100 | self.git = GitWrapper(None) 101 | 102 | # Load configuration 103 | self.settings = self.default_settings.copy() 104 | self.load_config() 105 | return 106 | 107 | # Testing: redirect stderr to stdout 108 | self.testing = testing 109 | if self.testing: 110 | self.stderr = sys.stdout # Quiet testing 111 | else: # pragma: no cover 112 | self.stderr = sys.stderr 113 | 114 | self.states = [] 115 | self.should_fetch = True 116 | self.pushed = False 117 | 118 | # Check, if we're in a git repo 119 | try: 120 | repo_dir = get_git_dir() 121 | except (OSError, GitCommandNotFound) as e: 122 | if isinstance(e, GitCommandNotFound) or e.errno == errno.ENOENT: 123 | exc = GitError("The git executable could not be found") 124 | raise exc 125 | else: 126 | raise 127 | else: 128 | if repo_dir is None: 129 | exc = GitError("We don't seem to be in a git repository.") 130 | raise exc 131 | 132 | self.repo = Repo(repo_dir, odbt=GitCmdObjectDB) 133 | 134 | # Check for branch tracking information 135 | if not any(b.tracking_branch() for b in self.repo.branches): 136 | exc = GitError("Can\'t update your repo because it doesn\'t has " 137 | "any branches with tracking information.") 138 | self.print_error(exc) 139 | 140 | raise exc 141 | 142 | self.git = GitWrapper(self.repo) 143 | 144 | # target_map: map local branch names to remote tracking branches 145 | #: :type: dict[str, git.refs.remote.RemoteReference] 146 | self.target_map = dict() 147 | 148 | for branch in self.repo.branches: 149 | target = branch.tracking_branch() 150 | 151 | if target: 152 | if target.name.startswith('./'): 153 | # Tracking branch is in local repo 154 | target.is_local = True 155 | else: 156 | target.is_local = False 157 | 158 | self.target_map[branch.name] = target 159 | 160 | # branches: all local branches with tracking information 161 | #: :type: list[git.refs.head.Head] 162 | self.branches = [b for b in self.repo.branches if b.tracking_branch()] 163 | self.branches.sort(key=lambda br: br.name) 164 | 165 | # remotes: all remotes that are associated with local branches 166 | #: :type: list[git.refs.remote.RemoteReference] 167 | self.remotes = uniq( 168 | # name = '/' -> '' 169 | [r.name.split('/', 2)[0] 170 | for r in list(self.target_map.values())] 171 | ) 172 | 173 | # change_count: Number of unstaged changes 174 | self.change_count = len( 175 | self.git.status(porcelain=True, untracked_files='no').split('\n') 176 | ) 177 | 178 | # Load configuration 179 | self.settings = self.default_settings.copy() 180 | self.load_config() 181 | 182 | def run(self): 183 | """ Run all the git-up stuff. """ 184 | try: 185 | if self.should_fetch: 186 | self.fetch() 187 | 188 | self.rebase_all_branches() 189 | 190 | if self.settings['push.auto']: 191 | self.push() 192 | 193 | except GitError as error: 194 | self.print_error(error) 195 | 196 | # Used for test cases 197 | if self.testing: 198 | raise 199 | else: # pragma: no cover 200 | sys.exit(1) 201 | except KeyboardInterrupt: 202 | sys.exit(130) 203 | 204 | def rebase_all_branches(self): 205 | """ Rebase all branches, if possible. """ 206 | col_width = max(len(b.name) for b in self.branches) + 1 207 | if self.repo.head.is_detached: 208 | raise GitError("You're not currently on a branch. I'm exiting" 209 | " in case you're in the middle of something.") 210 | original_branch = self.repo.active_branch 211 | 212 | with self.git.stasher() as stasher: 213 | for branch in self.branches: 214 | target = self.target_map[branch.name] 215 | 216 | # Print branch name 217 | if branch.name == original_branch.name: 218 | attrs = ['bold'] 219 | else: 220 | attrs = [] 221 | print(colored(branch.name.ljust(col_width), attrs=attrs), 222 | end=' ') 223 | 224 | # Check, if target branch exists 225 | try: 226 | if target.name.startswith('./'): 227 | # Check, if local branch exists 228 | self.git.rev_parse(target.name[2:]) 229 | else: 230 | # Check, if remote branch exists 231 | _ = target.commit 232 | 233 | except (ValueError, GitError): 234 | # Remote branch doesn't exist! 235 | print(colored('error: remote branch doesn\'t exist', 'red')) 236 | self.states.append('remote branch doesn\'t exist') 237 | 238 | continue 239 | 240 | # Get tracking branch 241 | if target.is_local: 242 | target = find(self.repo.branches, 243 | lambda b: b.name == target.name[2:]) 244 | 245 | # Check status and act appropriately 246 | if target.commit.hexsha == branch.commit.hexsha: 247 | print(colored('up to date', 'green')) 248 | self.states.append('up to date') 249 | 250 | continue # Do not do anything 251 | 252 | base = self.git.merge_base(branch.name, target.name) 253 | 254 | if base == target.commit.hexsha: 255 | print(colored('ahead of upstream', 'cyan')) 256 | self.states.append('ahead') 257 | 258 | continue # Do not do anything 259 | 260 | fast_fastforward = False 261 | if base == branch.commit.hexsha: 262 | print(colored('fast-forwarding...', 'yellow'), end='') 263 | self.states.append('fast-forwarding') 264 | # Don't fast fast-forward the currently checked-out branch 265 | fast_fastforward = (branch.name != 266 | self.repo.active_branch.name) 267 | 268 | elif not self.settings['rebase.auto']: 269 | print(colored('diverged', 'red')) 270 | self.states.append('diverged') 271 | 272 | continue # Do not do anything 273 | else: 274 | print(colored('rebasing', 'yellow'), end='') 275 | self.states.append('rebasing') 276 | 277 | if self.settings['rebase.show-hashes']: 278 | print(' {}..{}'.format(base[0:7], 279 | target.commit.hexsha[0:7])) 280 | else: 281 | print() 282 | 283 | self.log(branch, target) 284 | if fast_fastforward: 285 | branch.commit = target.commit 286 | else: 287 | stasher() 288 | self.git.checkout(branch.name) 289 | self.git.rebase(target) 290 | 291 | if (self.repo.head.is_detached # Only on Travis CI, 292 | # we get a detached head after doing our rebase *confused*. 293 | # Running self.repo.active_branch would fail. 294 | or not self.repo.active_branch.name == original_branch.name): 295 | print(colored(f'returning to {original_branch.name}', 296 | 'magenta')) 297 | original_branch.checkout() 298 | 299 | def fetch(self): 300 | """ 301 | Fetch the recent refs from the remotes. 302 | 303 | Unless git-up.fetch.all is set to true, all remotes with 304 | locally existent branches will be fetched. 305 | """ 306 | fetch_kwargs = {'multiple': True} 307 | fetch_args = [] 308 | 309 | if self.is_prune(): 310 | fetch_kwargs['prune'] = True 311 | 312 | if self.settings['fetch.all']: 313 | fetch_kwargs['all'] = True 314 | else: 315 | if '.' in self.remotes: 316 | self.remotes.remove('.') 317 | 318 | if not self.remotes: 319 | # Only local target branches, 320 | # `git fetch --multiple` will fail 321 | return 322 | 323 | fetch_args.append(self.remotes) 324 | 325 | try: 326 | self.git.fetch(*fetch_args, **fetch_kwargs) 327 | except GitError as error: 328 | error.message = "`git fetch` failed" 329 | raise error 330 | 331 | def push(self): 332 | """ 333 | Push the changes back to the remote(s) after fetching 334 | """ 335 | print('pushing...') 336 | push_kwargs = {} 337 | push_args = [] 338 | 339 | if self.settings['push.tags']: 340 | push_kwargs['push'] = True 341 | 342 | if self.settings['push.all']: 343 | push_kwargs['all'] = True 344 | else: 345 | if '.' in self.remotes: 346 | self.remotes.remove('.') 347 | 348 | if not self.remotes: 349 | # Only local target branches, 350 | # `git push` will fail 351 | return 352 | 353 | push_args.append(self.remotes) 354 | 355 | try: 356 | self.git.push(*push_args, **push_kwargs) 357 | self.pushed = True 358 | except GitError as error: 359 | error.message = "`git push` failed" 360 | raise error 361 | 362 | def log(self, branch, remote): 363 | """ Call a log-command, if set by git-up.fetch.all. """ 364 | log_hook = self.settings['rebase.log-hook'] 365 | 366 | if log_hook: 367 | if ON_WINDOWS: # pragma: no cover 368 | # Running a string in CMD from Python is not that easy on 369 | # Windows. Running 'cmd /C log_hook' produces problems when 370 | # using multiple statements or things like 'echo'. Therefore, 371 | # we write the string to a bat file and execute it. 372 | 373 | # In addition, we replace occurrences of $1 with %1 and so forth 374 | # in case the user is used to Bash or sh. 375 | # If there are occurrences of %something, we'll replace it with 376 | # %%something. This is the case when running something like 377 | # 'git log --pretty=format:"%Cred%h..."'. 378 | # Also, we replace a semicolon with a newline, because if you 379 | # start with 'echo' on Windows, it will simply echo the 380 | # semicolon and the commands behind instead of echoing and then 381 | # running other commands 382 | 383 | # Prepare log_hook 384 | log_hook = re.sub(r'\$(\d+)', r'%\1', log_hook) 385 | log_hook = re.sub(r'%(?!\d)', '%%', log_hook) 386 | log_hook = re.sub(r'; ?', r'\n', log_hook) 387 | 388 | # Write log_hook to an temporary file and get it's path 389 | with NamedTemporaryFile( 390 | prefix='PyGitUp.', suffix='.bat', delete=False 391 | ) as bat_file: 392 | # Don't echo all commands 393 | bat_file.file.write(b'@echo off\n') 394 | # Run log_hook 395 | bat_file.file.write(log_hook.encode('utf-8')) 396 | 397 | # Run bat_file 398 | state = subprocess.call( 399 | [bat_file.name, branch.name, remote.name] 400 | ) 401 | 402 | # Clean up file 403 | os.remove(bat_file.name) 404 | else: # pragma: no cover 405 | # Run log_hook via 'shell -c' 406 | state = subprocess.call( 407 | [log_hook, 'git-up', branch.name, remote.name], 408 | shell=True 409 | ) 410 | 411 | if self.testing: 412 | assert state == 0, 'log_hook returned != 0' 413 | 414 | def version_info(self): 415 | """ Tell, what version we're running at and if it's up to date. """ 416 | 417 | # Retrive and show local version info 418 | package = pkg.get_distribution('git-up') 419 | local_version_str = package.version 420 | local_version = package.parsed_version 421 | 422 | print('GitUp version is: ' + colored('v' + local_version_str, 'green')) 423 | 424 | if not self.settings['updates.check']: 425 | return 426 | 427 | # Check for updates 428 | print('Checking for updates...', end='') 429 | 430 | try: 431 | # Get version information from the PyPI JSON API 432 | reader = codecs.getreader('utf-8') 433 | details = json.load(reader(urlopen(PYPI_URL))) 434 | online_version = details['info']['version'] 435 | except (HTTPError, URLError, ValueError): 436 | recent = True # To not disturb the user with HTTP/parsing errors 437 | else: 438 | recent = local_version >= pkg.parse_version(online_version) 439 | 440 | if not recent: 441 | # noinspection PyUnboundLocalVariable 442 | print( 443 | '\rRecent version is: ' 444 | + colored('v' + online_version, color='yellow', attrs=['bold']) 445 | ) 446 | print('Run \'pip install -U git-up\' to get the update.') 447 | else: 448 | # Clear the update line 449 | sys.stdout.write('\r' + ' ' * 80 + '\n') 450 | 451 | ########################################################################### 452 | # Helpers 453 | ########################################################################### 454 | 455 | def load_config(self): 456 | """ 457 | Load the configuration from git config. 458 | """ 459 | for key in self.settings: 460 | value = self.config(key) 461 | # Parse true/false 462 | if value == '' or value is None: 463 | continue # Not set by user, go on 464 | if value.lower() == 'true': 465 | value = True 466 | elif value.lower() == 'false': 467 | value = False 468 | elif value: 469 | pass # A user-defined string, store the value later 470 | 471 | self.settings[key] = value 472 | 473 | def config(self, key): 474 | """ Get a git-up-specific config value. """ 475 | return self.git.config(f'git-up.{key}') 476 | 477 | def is_prune(self): 478 | """ 479 | Return True, if `git fetch --prune` is allowed. 480 | 481 | Because of possible incompatibilities, this requires special 482 | treatment. 483 | """ 484 | required_version = "1.6.6" 485 | config_value = self.settings['fetch.prune'] 486 | 487 | if self.git.is_version_min(required_version): 488 | return config_value is not False 489 | else: # pragma: no cover 490 | if config_value == 'true': 491 | print(colored( 492 | "Warning: fetch.prune is set to 'true' but your git" 493 | "version doesn't seem to support it ({} < {})." 494 | "Defaulting to 'false'.".format(self.git.version, 495 | required_version), 496 | 'yellow' 497 | )) 498 | 499 | def print_error(self, error): 500 | """ 501 | Print more information about an error. 502 | 503 | :type error: GitError 504 | """ 505 | print(colored(error.message, 'red'), file=self.stderr) 506 | 507 | if error.stdout or error.stderr: 508 | print(file=self.stderr) 509 | print("Here's what git said:", file=self.stderr) 510 | print(file=self.stderr) 511 | 512 | if error.stdout: 513 | print(error.stdout, file=self.stderr) 514 | if error.stderr: 515 | print(error.stderr, file=self.stderr) 516 | 517 | if error.details: 518 | print(file=self.stderr) 519 | print("Here's what we know:", file=self.stderr) 520 | print(str(error.details), file=self.stderr) 521 | print(file=self.stderr) 522 | 523 | 524 | ############################################################################### 525 | 526 | 527 | EPILOG = ''' 528 | For configuration options, please see 529 | https://github.com/msiemens/PyGitUp#readme. 530 | 531 | \b 532 | Python port of https://github.com/aanand/git-up/ 533 | Project Author: Markus Siemens 534 | Project URL: https://github.com/msiemens/PyGitUp 535 | \b 536 | ''' 537 | 538 | 539 | def run(): # pragma: no cover 540 | """ 541 | A nicer `git pull`. 542 | """ 543 | 544 | parser = argparse.ArgumentParser(description="A nicer `git pull`.", epilog=EPILOG) 545 | parser.add_argument('-V', '--version', action='store_true', 546 | help='Show version (and if there is a newer version).') 547 | parser.add_argument('-q', '--quiet', action='store_true', 548 | help='Be quiet, only print error messages.') 549 | parser.add_argument('--no-fetch', '--no-f', dest='fetch', action='store_false', 550 | help='Don\'t try to fetch from origin.') 551 | parser.add_argument('-p', '--push', action='store_true', 552 | help='Push the changes after pulling successfully.') 553 | 554 | args = parser.parse_args() 555 | 556 | if args.version: 557 | if NO_DISTRIBUTE: 558 | print(colored('Please install \'git-up\' via pip in order to ' 559 | 'get version information.', 'yellow')) 560 | else: 561 | GitUp(sparse=True).version_info() 562 | return 563 | 564 | if args.quiet: 565 | sys.stdout = StringIO() 566 | 567 | try: 568 | gitup = GitUp() 569 | gitup.settings['push.auto'] = args.push 570 | gitup.should_fetch = args.fetch 571 | except GitError: 572 | sys.exit(1) # Error in constructor 573 | else: 574 | gitup.run() 575 | 576 | 577 | if __name__ == '__main__': # pragma: no cover 578 | run() 579 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2024.8.30" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 11 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 12 | ] 13 | 14 | [[package]] 15 | name = "charset-normalizer" 16 | version = "3.3.2" 17 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 18 | optional = false 19 | python-versions = ">=3.7.0" 20 | files = [ 21 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 22 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 23 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 24 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 25 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 26 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 27 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 28 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 29 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 30 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 31 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 32 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 33 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 34 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 35 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 36 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 37 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 38 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 39 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 40 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 41 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 42 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 43 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 44 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 45 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 46 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 47 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 48 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 49 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 50 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 51 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 52 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 53 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 54 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 55 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 56 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 57 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 58 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 59 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 60 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 61 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 62 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 63 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 64 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 65 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 66 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 67 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 68 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 69 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 70 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 71 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 72 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 73 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 74 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 75 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 76 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 77 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 78 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 79 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 80 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 81 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 82 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 83 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 84 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 85 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 86 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 87 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 88 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 89 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 90 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 91 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 92 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 93 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 94 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 95 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 96 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 97 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 98 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 99 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 100 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 101 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 102 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 103 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 104 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 105 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 106 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 107 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 108 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 109 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 110 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 111 | ] 112 | 113 | [[package]] 114 | name = "colorama" 115 | version = "0.4.6" 116 | description = "Cross-platform colored terminal text." 117 | optional = false 118 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 119 | files = [ 120 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 121 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 122 | ] 123 | 124 | [[package]] 125 | name = "coverage" 126 | version = "6.5.0" 127 | description = "Code coverage measurement for Python" 128 | optional = false 129 | python-versions = ">=3.7" 130 | files = [ 131 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 132 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 133 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 134 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 135 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 136 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 137 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 138 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 139 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 140 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 141 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 142 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 143 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 144 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 145 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 146 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 147 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 148 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 149 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 150 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 151 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 152 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 153 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 154 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 155 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 156 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 157 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 158 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 159 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 160 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 161 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 162 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 163 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 164 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 165 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 166 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 167 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 168 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 169 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 170 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 171 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 172 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 173 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 174 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 175 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 176 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 177 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 178 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 179 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 180 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 181 | ] 182 | 183 | [package.dependencies] 184 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 185 | 186 | [package.extras] 187 | toml = ["tomli"] 188 | 189 | [[package]] 190 | name = "coveralls" 191 | version = "3.3.1" 192 | description = "Show coverage stats online via coveralls.io" 193 | optional = false 194 | python-versions = ">= 3.5" 195 | files = [ 196 | {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, 197 | {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, 198 | ] 199 | 200 | [package.dependencies] 201 | coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0" 202 | docopt = ">=0.6.1" 203 | requests = ">=1.0.0" 204 | 205 | [package.extras] 206 | yaml = ["PyYAML (>=3.10)"] 207 | 208 | [[package]] 209 | name = "docopt" 210 | version = "0.6.2" 211 | description = "Pythonic argument parser, that will make you smile" 212 | optional = false 213 | python-versions = "*" 214 | files = [ 215 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 216 | ] 217 | 218 | [[package]] 219 | name = "exceptiongroup" 220 | version = "1.2.2" 221 | description = "Backport of PEP 654 (exception groups)" 222 | optional = false 223 | python-versions = ">=3.7" 224 | files = [ 225 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 226 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 227 | ] 228 | 229 | [package.extras] 230 | test = ["pytest (>=6)"] 231 | 232 | [[package]] 233 | name = "gitdb" 234 | version = "4.0.11" 235 | description = "Git Object Database" 236 | optional = false 237 | python-versions = ">=3.7" 238 | files = [ 239 | {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, 240 | {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, 241 | ] 242 | 243 | [package.dependencies] 244 | smmap = ">=3.0.1,<6" 245 | 246 | [[package]] 247 | name = "gitpython" 248 | version = "3.1.43" 249 | description = "GitPython is a Python library used to interact with Git repositories" 250 | optional = false 251 | python-versions = ">=3.7" 252 | files = [ 253 | {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, 254 | {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, 255 | ] 256 | 257 | [package.dependencies] 258 | gitdb = ">=4.0.1,<5" 259 | 260 | [package.extras] 261 | doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] 262 | test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] 263 | 264 | [[package]] 265 | name = "idna" 266 | version = "3.10" 267 | description = "Internationalized Domain Names in Applications (IDNA)" 268 | optional = false 269 | python-versions = ">=3.6" 270 | files = [ 271 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 272 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 273 | ] 274 | 275 | [package.extras] 276 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 277 | 278 | [[package]] 279 | name = "iniconfig" 280 | version = "2.0.0" 281 | description = "brain-dead simple config-ini parsing" 282 | optional = false 283 | python-versions = ">=3.7" 284 | files = [ 285 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 286 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 287 | ] 288 | 289 | [[package]] 290 | name = "packaging" 291 | version = "24.1" 292 | description = "Core utilities for Python packages" 293 | optional = false 294 | python-versions = ">=3.8" 295 | files = [ 296 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 297 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 298 | ] 299 | 300 | [[package]] 301 | name = "pluggy" 302 | version = "1.5.0" 303 | description = "plugin and hook calling mechanisms for python" 304 | optional = false 305 | python-versions = ">=3.8" 306 | files = [ 307 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 308 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 309 | ] 310 | 311 | [package.extras] 312 | dev = ["pre-commit", "tox"] 313 | testing = ["pytest", "pytest-benchmark"] 314 | 315 | [[package]] 316 | name = "pytest" 317 | version = "8.3.3" 318 | description = "pytest: simple powerful testing with Python" 319 | optional = false 320 | python-versions = ">=3.8" 321 | files = [ 322 | {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, 323 | {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, 324 | ] 325 | 326 | [package.dependencies] 327 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 328 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 329 | iniconfig = "*" 330 | packaging = "*" 331 | pluggy = ">=1.5,<2" 332 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 333 | 334 | [package.extras] 335 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 336 | 337 | [[package]] 338 | name = "pytest-cov" 339 | version = "4.1.0" 340 | description = "Pytest plugin for measuring coverage." 341 | optional = false 342 | python-versions = ">=3.7" 343 | files = [ 344 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 345 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 346 | ] 347 | 348 | [package.dependencies] 349 | coverage = {version = ">=5.2.1", extras = ["toml"]} 350 | pytest = ">=4.6" 351 | 352 | [package.extras] 353 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 354 | 355 | [[package]] 356 | name = "requests" 357 | version = "2.32.3" 358 | description = "Python HTTP for Humans." 359 | optional = false 360 | python-versions = ">=3.8" 361 | files = [ 362 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 363 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 364 | ] 365 | 366 | [package.dependencies] 367 | certifi = ">=2017.4.17" 368 | charset-normalizer = ">=2,<4" 369 | idna = ">=2.5,<4" 370 | urllib3 = ">=1.21.1,<3" 371 | 372 | [package.extras] 373 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 374 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 375 | 376 | [[package]] 377 | name = "smmap" 378 | version = "5.0.1" 379 | description = "A pure Python implementation of a sliding window memory map manager" 380 | optional = false 381 | python-versions = ">=3.7" 382 | files = [ 383 | {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, 384 | {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, 385 | ] 386 | 387 | [[package]] 388 | name = "termcolor" 389 | version = "2.4.0" 390 | description = "ANSI color formatting for output in terminal" 391 | optional = false 392 | python-versions = ">=3.8" 393 | files = [ 394 | {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, 395 | {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, 396 | ] 397 | 398 | [package.extras] 399 | tests = ["pytest", "pytest-cov"] 400 | 401 | [[package]] 402 | name = "tomli" 403 | version = "2.0.2" 404 | description = "A lil' TOML parser" 405 | optional = false 406 | python-versions = ">=3.8" 407 | files = [ 408 | {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, 409 | {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, 410 | ] 411 | 412 | [[package]] 413 | name = "urllib3" 414 | version = "2.2.3" 415 | description = "HTTP library with thread-safe connection pooling, file post, and more." 416 | optional = false 417 | python-versions = ">=3.8" 418 | files = [ 419 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 420 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 421 | ] 422 | 423 | [package.extras] 424 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 425 | h2 = ["h2 (>=4,<5)"] 426 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 427 | zstd = ["zstandard (>=0.18.0)"] 428 | 429 | [metadata] 430 | lock-version = "2.0" 431 | python-versions = "^3.8" 432 | content-hash = "26420f42ea3198c24b454ed9560e4d8334e549e97a8b3bf0dd65c60326ddff9c" 433 | --------------------------------------------------------------------------------