diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml index 14506a30a..189826c0f 100644 --- a/.github/workflows/alpine-test.yml +++ b/.github/workflows/alpine-test.yml @@ -34,7 +34,6 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - name: Set workspace ownership @@ -42,10 +41,6 @@ jobs: chown -R runner:docker -- "$GITHUB_WORKSPACE" shell: sh -exo pipefail {0} # Run this as root, not the "runner" user. - - name: Prepare this repo for tests - run: | - ./init-tests-after-clone.sh - - name: Set git user identity and command aliases for the tests run: | git config --global user.email "travis@ci.com" @@ -95,6 +90,9 @@ jobs: python --version python -c 'import os, sys; print(f"{sys.platform=}, {os.name=}")' + - name: Remove source Git metadata to verify fixture isolation + run: .venv/bin/python -c "from pathlib import Path; from git.util import rmtree; rmtree('.git'); assert not Path('.git').exists()" + - name: Test with pytest run: | . .venv/bin/activate diff --git a/.github/workflows/backend-benchmark.yml b/.github/workflows/backend-benchmark.yml index 49f0155a0..140aa27cb 100644 --- a/.github/workflows/backend-benchmark.yml +++ b/.github/workflows/backend-benchmark.yml @@ -16,7 +16,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: @@ -50,7 +49,8 @@ jobs: .bench-gix/bin/python -m pip install '.[gix]' -r test/performance/benchmark-requirements.txt - name: Prepare the shared read-only benchmark fixture run: | - .bench-cli/bin/python -c 'from pathlib import Path; from gix import testtools; print("BENCH_REPO=" + str((testtools.scripted_fixture_read_only_with_args("benchmark.sh", [str(Path.cwd())]) / "repo").resolve()))' >> "$GITHUB_ENV" + rm -rf .git + .bench-cli/bin/python -c 'from gix import testtools; print("BENCH_REPO=" + str((testtools.scripted_fixture_read_only_with_args("benchmark.sh", []) / "repo").resolve()))' >> "$GITHUB_ENV" mkdir benchmark-results - name: Measure GixPython operations, opening and discovery first env: diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index ba3a09246..c55b73dd8 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -30,7 +30,6 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - name: Install Cygwin @@ -61,10 +60,6 @@ jobs: git config --global --add safe.directory "$(pwd)/.git" git config --global core.autocrlf false - - name: Prepare this repo for tests - run: | - ./init-tests-after-clone.sh - - name: Set git user identity and command aliases for the tests run: | git config --global user.email "travis@ci.com" diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 143a89018..b58704fe9 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -30,7 +30,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: @@ -45,8 +44,9 @@ jobs: run: flake8 "$PROJECT/$PROJECT" --count --select=E9,F63,F7,F82 --show-source --statistics - name: Test run: | + rm -rf .git if test "$PROJECT" = gitdb; then - GITDB_TEST_GIT_REPO_BASE="$(git rev-parse --git-common-dir)" pytest -o addopts= -v gitdb/gitdb/test + pytest -o addopts= -v gitdb/gitdb/test else pytest -o addopts= -v smmap/smmap/test fi diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1ca50a599..e8ef05cd4 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -61,7 +61,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - name: Set up Python ${{ matrix.python-version }} @@ -115,10 +114,6 @@ jobs: brew install git echo "$(brew --prefix git)/bin" >> "$GITHUB_PATH" - - name: Prepare this repo for tests - run: | - ./init-tests-after-clone.sh - - name: Set git user identity and command aliases for the tests run: | git config --global user.email "travis@ci.com" @@ -219,6 +214,9 @@ jobs: run: | basedpyright --warnings + - name: Remove source Git metadata to verify fixture isolation + run: python -c "from pathlib import Path; from git.util import rmtree; rmtree('.git'); assert not Path('.git').exists()" + - name: Test with pytest run: | if [[ "$RUNNER_OS" == "Windows" && -z "${GIT_PYTHON_GIT_EXECUTABLE+x}" ]]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b14b1d33..d5848e1bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,8 @@ The following is a short step-by-step rundown of what one typically would do to contribute. - [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. -- For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`. +- Install the `test` extra and select a backend before running `pytest`. Test + repositories are generated automatically; no source Git history is required. - Please try to **write a test that fails unless the contribution is present.** - Try to avoid massive commits and prefer to take small steps, with one commit for each. - Feel free to add yourself to AUTHORS file. @@ -64,8 +65,10 @@ Put initial repository and filesystem states in `tests/fixtures/*.sh` and use `test.lib.fixtures` to run them through `gix.testtools`. Read-only tests share cached output; mutating tests use writable copies. Use `execute=True` only when the fixture cannot be copied, and explain the reason at its call site. Keep -operations under test in Python. Immutable parser byte samples remain in -`test/fixtures`; generated caches and archives are ignored. +operations under test in Python. Generate histories from scratch and refer to +named fixture revisions; do not clone the source checkout, fetch project history, +or depend on its refs, object IDs, index, or working-tree state. Immutable parser +byte samples remain in `test/fixtures`; generated caches and archives are ignored. Use `test.cleanup.cleanup_directory` for cleanup outside the shared fixture owners. Close repository handles before removing their files. Cleanup is diff --git a/README.md b/README.md index 3974060f7..3776c2f43 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,8 @@ To clone the [the GitHub repository](https://github.com/gitpython-developers/Git ```sh git clone https://github.com/gitpython-developers/GitPython cd GitPython -./init-tests-after-clone.sh ``` -On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell. - If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`): ```sh @@ -175,9 +172,8 @@ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525). ### RUNNING TESTS -_Important_: Right after cloning this repository, please be sure to have executed -the `./init-tests-after-clone.sh` script in the repository root. Otherwise -you will encounter test failures. +Test repositories are generated from scratch by fixture scripts. Tests work from +a shallow clone or a source copy without `.git`; no checkout preparation is needed. #### Install test dependencies diff --git a/doc/gix-backend.md b/doc/gix-backend.md index c2959c5c3..8a5b2ec71 100644 --- a/doc/gix-backend.md +++ b/doc/gix-backend.md @@ -214,6 +214,10 @@ installation/import smoke coverage, including the Cygwin Python 3.9 job. Running tests also requires Bash and Git 2.52+; `gix` here is GixPython's module. Initial repository and filesystem states live in `tests/fixtures/*.sh`. +Histories are generated from scratch, including the shared sample repository and +its nested dependency repositories. Tests use named revisions such as `root`, +`release`, and `merge`, and require no source checkout history or Git metadata. +CI removes the source `.git` directory before running the full suite. `test/lib/fixtures.py` calls `gix.testtools` with an explicit lifetime: - `read_only_fixture()` and the default `fixture_repo()` reuse cached output. diff --git a/gitdb/README.rst b/gitdb/README.rst index 45344a078..e1ecb1d22 100644 --- a/gitdb/README.rst +++ b/gitdb/README.rst @@ -42,6 +42,9 @@ The source is available in the GitPython repository on GitHub: https://github.com/gitpython-developers/GitPython/tree/main/gitdb +Tests require Git and Bash. Each test class gets a temporary repository generated +from scratch with packed and loose objects; no source checkout history is needed. + Run the tests with:: pytest diff --git a/gitdb/gitdb/test/fixtures/repository.sh b/gitdb/gitdb/test/fixtures/repository.sh new file mode 100644 index 000000000..b7af78229 --- /dev/null +++ b/gitdb/gitdb/test/fixtures/repository.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -eu +git init -q --object-format=sha1 -b main repo +cd repo +for n in $(seq 1 30); do + printf 'revision %s\n' "$n" >> file + git add file + git commit -qm "Revision $n" +done +git gc --prune=now +# Keep both packed and loose objects for the compound database and tutorial tests. +printf 'loose object\n' > loose +git add loose +git commit -qm 'Add loose objects' diff --git a/gitdb/gitdb/test/lib.py b/gitdb/gitdb/test/lib.py index 8e602342c..190487706 100644 --- a/gitdb/gitdb/test/lib.py +++ b/gitdb/gitdb/test/lib.py @@ -17,7 +17,7 @@ import shutil import os import gc -import logging +import subprocess from functools import wraps @@ -30,13 +30,9 @@ class TestBase(unittest.TestCase): * gitrepopath - * read-only base path of the git source repository, i.e. .../git/.git + * base path of a generated repository containing packed and loose objects """ - #{ Invvariants - k_env_git_repo = "GITDB_TEST_GIT_REPO_BASE" - #} END invariants - @classmethod def setUpClass(cls): try: @@ -44,14 +40,18 @@ def setUpClass(cls): except AttributeError: pass - cls.gitrepopath = os.environ.get(cls.k_env_git_repo) - if not cls.gitrepopath: - logging.info( - "You can set the %s environment variable to a .git repository of your choice - defaulting to the gitdb repository", cls.k_env_git_repo) - ospd = os.path.dirname - cls.gitrepopath = os.path.join(ospd(ospd(ospd(__file__))), '.git') - # end assure gitrepo is set - assert cls.gitrepopath.endswith('.git') + temporary = tempfile.TemporaryDirectory(prefix="gitdb-fixture-") + cls.addClassCleanup(temporary.cleanup) + env = {key: value for key, value in os.environ.items() if not key.startswith('GIT_')} + env.update( + GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull, + GIT_AUTHOR_NAME='GitDB Tests', GIT_AUTHOR_EMAIL='tests@example.invalid', + GIT_COMMITTER_NAME='GitDB Tests', GIT_COMMITTER_EMAIL='tests@example.invalid', + GIT_AUTHOR_DATE='2001-01-01T00:00:00 +0000', GIT_COMMITTER_DATE='2001-01-01T00:00:00 +0000', + ) + # Keep the standalone suite usable on Python 3.8 without GixPython. + subprocess.run(['bash', fixture_path('repository.sh')], cwd=temporary.name, env=env, check=True) + cls.gitrepopath = os.path.join(temporary.name, 'repo', '.git') #} END bases diff --git a/gitdb/gitdb/test/performance/test_pack.py b/gitdb/gitdb/test/performance/test_pack.py index fc3d3349f..8e8796361 100644 --- a/gitdb/gitdb/test/performance/test_pack.py +++ b/gitdb/gitdb/test/performance/test_pack.py @@ -51,7 +51,7 @@ def test_pack_random_access(self): # END for each random mode # query info and streams only - max_items = 10000 # can wait longer when testing memory + max_items = min(10000, ns) # can wait longer when testing memory for pdb_fun in (pdb.info, pdb.stream): st = time() for sha in sha_list[:max_items]: @@ -62,7 +62,7 @@ def test_pack_random_access(self): # END for each function # retrieve stream and read all - max_items = 5000 + max_items = min(5000, ns) pdb_stream = pdb.stream total_size = 0 st = time() diff --git a/gitdb/gitdb/test/performance/test_pack_streaming.py b/gitdb/gitdb/test/performance/test_pack_streaming.py index 80c798bbc..c4e89b5ae 100644 --- a/gitdb/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/gitdb/test/performance/test_pack_streaming.py @@ -49,10 +49,10 @@ def test_pack_writing(self): # END gather objects for pack-writing elapsed = time() - st print("PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % - (ni, elapsed, ni / (elapsed or 1)), file=sys.stderr) + (count, elapsed, count / (elapsed or 1)), file=sys.stderr) st = time() - PackEntity.write_pack((pdb.stream(sha) for sha in pdb.sha_iter()), ostream.write, object_count=ni) + PackEntity.write_pack((pdb.stream(sha) for sha in pdb.sha_iter()), ostream.write, object_count=pdb.size()) elapsed = time() - st total_kb = ostream.bytes_written() / 1000 print(sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % @@ -78,4 +78,4 @@ def test_stream_reading(self): elapsed = time() - st total_kib = total_size / 1000 print(sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % - (ni, total_kib, total_kib / (elapsed or 1), elapsed, ni / (elapsed or 1)), sys.stderr) + (count, total_kib, total_kib / (elapsed or 1), elapsed, count / (elapsed or 1)), sys.stderr) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh deleted file mode 100755 index db3540857..000000000 --- a/init-tests-after-clone.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/sh -# -# This file is part of GitPython and is released under the -# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ - -set -eu - -fallback_repo_for_tags='https://github.com/gitpython-developers/GitPython.git' - -ci() { - # For now, check just these, as a false positive could lead to data loss. - test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}" -} - -no_version_tags() { - test -z "$(git tag -l '[0-9]*' 'v[0-9]*')" -} - -warn() { - if test -n "${GITHUB_ACTIONS-}"; then - printf '::warning ::%s\n' "$*" >&2 # Annotate workflow. - else - printf '%s\n' "$@" >&2 - fi -} - -if ! ci; then - printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2 - read -r answer - case "$answer" in - [yY]) - ;; - *) - exit 2 ;; - esac -fi - -# Stop if we have run this. (You can delete __testing_point__ to let it rerun.) -# This also keeps track of where we are, so we can get back here. -git tag __testing_point__ - -# The tests need a branch called master. -# -# If master is locally absent but more than one remote has it, checkout fails -# by default even if all remotes agree, and we fall back to creating it at -# HEAD. The reflog we populate below then traces HEAD's history rather than -# a remote master's, but master is reset to __testing_point__ either way. -git checkout master -- || git checkout -b master - -# The tests need a reflog history on the master branch. -git reset --hard HEAD~1 -git reset --hard HEAD~1 -git reset --hard HEAD~1 - -# Point the master branch where we started, so we test the correct code. -git reset --hard __testing_point__ - -# The tests need some version tags. Try to get them even in forks. This fetches -# other objects too. So, locally, we always do it, for a consistent experience. -if ! ci || no_version_tags; then - git fetch --all --tags -fi - -# If we still have no version tags, try to get them from the original repo. -if no_version_tags; then - warn 'No local or remote version tags found. Trying fallback remote:' \ - "$fallback_repo_for_tags" - - # git fetch supports * but not [], and --no-tags means no *other* tags, so... - printf 'refs/tags/%d*:refs/tags/%d*\n' 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 | - xargs git fetch --no-tags "$fallback_repo_for_tags" - - if no_version_tags; then - warn 'No version tags found anywhere. Some tests will fail.' - fi -fi diff --git a/test/conftest.py b/test/conftest.py index a7ee2bd74..d0c355229 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,11 +5,8 @@ from pathlib import Path import pytest -from gix import testtools from git import _backend -from test.lib import GIT_REPO, TestBase -from test.lib.fixtures import read_only_fixture def pytest_addoption(parser): @@ -99,29 +96,3 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): with open(path, "w", encoding="utf-8") as stream: json.dump(records, stream, indent=2) stream.write("\n") - - -@pytest.fixture(scope="session") -def dependency_repo_factory(): - """Prepare each immutable historical clone source only when a test needs it.""" - source = Path(GIT_REPO).resolve() - revision = testtools.git(source, "rev-parse HEAD").strip() - paths = {} - - def get(name): - if name not in paths: - paths[name] = str(read_only_fixture("dependencies.sh", source, revision, name) / "repo") - return paths[name] - - return get - - -@pytest.fixture(scope="class", autouse=True) -def historical_dependency_sources(request): - if request.cls is not None and issubclass(request.cls, TestBase): - factory = request.getfixturevalue("dependency_repo_factory") - request.cls._dependency_repo_factory = staticmethod(factory) - yield - del request.cls._dependency_repo_factory - else: - yield diff --git a/test/fixtures/diff_initial b/test/fixtures/diff_initial deleted file mode 100644 index 648d70437..000000000 --- a/test/fixtures/diff_initial +++ /dev/null @@ -1,8 +0,0 @@ -@@ -0,0 +1,7 @@ -+======= -+CHANGES -+======= -+ -+0.1.0 -+===== -+initial release diff --git a/test/lib/helper.py b/test/lib/helper.py index 5554f407e..dacb3dfe6 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -16,7 +16,6 @@ "TestCase", "SkipTest", "skipIf", - "GIT_REPO", "GIT_DAEMON_PORT", "xfail_if_raises", "symlinks_supported", @@ -31,7 +30,6 @@ import logging import os import os.path as osp -from pathlib import Path from stat import S_ISLNK, ST_MODE import socket import textwrap @@ -41,7 +39,6 @@ from typing import Union, Type, Tuple import pytest -from gix import testtools from git.util import cwd from test.cleanup import TemporaryDirectory @@ -53,7 +50,6 @@ ospd = osp.dirname -GIT_REPO = os.environ.get("GIT_PYTHON_TEST_GIT_REPO_BASE", ospd(ospd(ospd(__file__)))) GIT_DAEMON_PORT = os.environ.get("GIT_PYTHON_TEST_GIT_DAEMON_PORT", "19418") _logger = logging.getLogger(__name__) @@ -373,22 +369,21 @@ class TestBase(TestCase): self.rorepo # 'ro' stands for read-only - The rorepo is a scripted clone of your project's git repo. If you refer to specific - shas for your objects, be sure you choose some that are part of the immutable - portion of the project history (so that tests don't fail for others). + The rorepo is generated by sample-repositories.sh. Its named revisions describe + the test scenarios and do not depend on the project's checkout or history. """ def _small_repo_url(self): """:return: A path to a small, clonable repository""" from git.cmd import Git - return Git.polish_url(self._dependency_repo_factory("smmap")) + return Git.polish_url(str(read_only_fixture("sample-repositories.sh") / "smmap")) def _gitdb_repo_url(self): - """:return: A local gitdb repository reconstructed from merged history""" + """:return: A generated repository with a nested submodule""" from git.cmd import Git - return Git.polish_url(self._dependency_repo_factory("gitdb")) + return Git.polish_url(str(read_only_fixture("sample-repositories.sh") / "gitdb")) @classmethod def setUpClass(cls): @@ -399,9 +394,7 @@ def setUpClass(cls): from git import Repo gc.collect() - source = Path(GIT_REPO).resolve() - revision = testtools.git(source, "rev-parse HEAD").strip() - cls.rorepo = Repo(read_only_fixture("history.sh", source, revision, revision, "baseline") / "repo") + cls.rorepo = Repo(read_only_fixture("sample-repositories.sh") / "repo") @classmethod def tearDownClass(cls): diff --git a/test/performance/README.md b/test/performance/README.md index 2fb13fd9c..24878db2b 100644 --- a/test/performance/README.md +++ b/test/performance/README.md @@ -23,13 +23,13 @@ uv pip install --python .tox/gix/bin/python -r test/performance/benchmark-requir uv pip install --python .venv/bin/python -r test/performance/benchmark-requirements.txt ``` -Prepare the same fixed repository used by CI (GitPython 3.1.45) through -`gix.testtools`, outside the measured operations. Its Bash script lives in -`tests/fixtures/benchmark.sh`; no remote access is needed if that revision -already exists locally: +Prepare the same generated repository used by CI through `gix.testtools`, +outside the measured operations. `tests/fixtures/benchmark.sh` creates 30 +commits, an annotated tag, a nested directory, and a submodule from scratch. +It requires neither network access nor source Git metadata: ```sh -BENCH_REPO=$(.venv/bin/python -c 'from pathlib import Path; from gix import testtools; print((testtools.scripted_fixture_read_only_with_args("benchmark.sh", [str(Path.cwd())]) / "repo").resolve())') +BENCH_REPO=$(.venv/bin/python -c 'from gix import testtools; print((testtools.scripted_fixture_read_only_with_args("benchmark.sh", []) / "repo").resolve())') ``` Run from the source root, sequentially to avoid competing for CPU and disk: @@ -48,7 +48,7 @@ The default uses six worker processes, three measured values per process and one warmup, with calibrated loop counts and a minimum value duration of 100 ms. Pass standard `pyperf` options such as `--rigorous` for more samples or `--debug-single-value` for a smoke check. Raw JSON retains sample distributions, -environment details, fixture/source revisions, result digests, and one untimed +environment details, fixture revisions, source content digests, result digests, and one untimed invocation's native/fallback decisions. The comparison rejects differing results, revisions, Python/Git versions and storage formats. The harness also rejects the wrong backend rather than silently comparing CLI against itself. @@ -64,7 +64,7 @@ mutates that worktree, and ignores ambient Git environment/config overrides. The journey also enumerates submodules, resolves tree/index revisions with their paths and modes, and reads raw commit/tree bytes through the object database. The last operation covers serialization readback without rewriting -objects; the pinned fixture's signed commit remains intact. These three +objects. The generated fixture includes a submodule for the inventory check. These three measurements have zero-CLI ceilings and join the existing CI job automatically. Add a function returning JSON-compatible results to `MEASUREMENTS` in @@ -98,4 +98,8 @@ Timing is observational: shared CI runners and local activity introduce noise. The CI job publishes all means, standard deviations and ratios, retains raw artifacts, and uses `pyperf compare_to` for significance reporting. It fails on execution/parity errors, without a noisy speed threshold. Compare like-for-like -fixture and source revisions when tracking performance across backend changes. +fixture revisions and source content digests when tracking performance across backend changes. +The source digest covers `VERSION`, the Python package files, and the benchmark +harness sources in `test/performance`, including local edits. +Measurements from the old GitPython-history fixture are not comparable +with this generated workload. diff --git a/test/performance/bench_repository.py b/test/performance/bench_repository.py index 5d90191e6..cc52b4612 100644 --- a/test/performance/bench_repository.py +++ b/test/performance/bench_repository.py @@ -124,6 +124,14 @@ def worker_arguments(cmd, args): cmd.extend(["--repo", str(args.repo), "--expect-backend", args.expect_backend]) +def source_digest(root): + digest = sha256() + sources = [root / "VERSION", *(root / "git").rglob("*.py"), *(root / "test" / "performance").glob("*.py")] + for path in sorted(sources): + digest.update(path.relative_to(root).as_posix().encode() + b"\0" + path.read_bytes() + b"\0") + return digest.hexdigest() + + def main(): runner = pyperf.Runner( processes=6, @@ -156,7 +164,7 @@ def git(*arguments, cwd=args.repo): backend=_backend.name, gixpython=version("GixPython"), git_version=git("version"), - gitpython_revision=git("rev-parse", "HEAD", cwd=Path.cwd()), + gitpython_source=source_digest(Path(__file__).resolve().parents[2]), fixture_revision=git("rev-parse", "HEAD"), fixture_tree=git("rev-parse", "HEAD^{tree}"), fixture_status=git("status", "--porcelain") or "clean", diff --git a/test/performance/cli-budget.json b/test/performance/cli-budget.json index 987a249bb..0d0808157 100644 --- a/test/performance/cli-budget.json +++ b/test/performance/cli-budget.json @@ -1,5 +1,5 @@ { - "fixture_revision": "6ba2c0a2f9ee7feffd7e079621c4845820180c9a", + "fixture_revision": "bd49b71d9f760b7e1a0753c42a6c0ad0a6c48175", "max_gix_cli_processes": { "journey": 1, "inventory": 0, diff --git a/test/performance/compare_backends.py b/test/performance/compare_backends.py index 2c247ee43..cfc8b1fc0 100644 --- a/test/performance/compare_backends.py +++ b/test/performance/compare_backends.py @@ -19,7 +19,7 @@ def compare(cli_path, gix_path, cli_budget=None): if (lm["backend"], rm["backend"]) != ("cli", "gix"): raise ValueError("Expected CLI results followed by GixPython results") for key in ( - "gitpython_revision", + "gitpython_source", "fixture_revision", "fixture_tree", "fixture_status", diff --git a/test/performance/test_benchmark_comparison.py b/test/performance/test_benchmark_comparison.py index 4848a5344..d8a867fdc 100644 --- a/test/performance/test_benchmark_comparison.py +++ b/test/performance/test_benchmark_comparison.py @@ -7,7 +7,9 @@ pytest.importorskip("pyperf") -@pytest.mark.parametrize("changed", [None, "result_digest", "fixture_revision", "backend", "cli_processes"]) +@pytest.mark.parametrize( + "changed", [None, "result_digest", "fixture_revision", "gitpython_source", "backend", "cli_processes"] +) def test_comparison_requires_parity(capsys, changed): from test.performance.compare_backends import compare @@ -20,3 +22,12 @@ def test_comparison_requires_parity(capsys, changed): compare(*paths, cli_budget=fixture / "budget.json") assert "journey | 150.00" in capsys.readouterr().out compare(*paths, cli_budget=fixture / "relaxed-budget.json") + + +def test_source_digest_detects_code_changes_without_git_metadata(): + from test.performance.bench_repository import source_digest + + fixture = read_only_fixture("benchmark-source.sh") + assert source_digest(fixture / "source") != source_digest(fixture / "copy") + assert source_digest(fixture / "source") != source_digest(fixture / "harness") + assert source_digest(fixture / "source") == source_digest(fixture / "moved") diff --git a/test/run-local.py b/test/run-local.py index c7824bd53..b455ebb2f 100644 --- a/test/run-local.py +++ b/test/run-local.py @@ -37,7 +37,6 @@ def main(): raise RuntimeError("Git executable not found in PATH") # Windows can share minimum-version checks for an absolute path. env["GIT_PYTHON_GIT_EXECUTABLE"] = os.path.abspath(executable) - env["GIT_PYTHON_TEST_GIT_REPO_BASE"] = str(root) # Separate CLI and gix runs can use their own localhost daemon. with socket.socket() as listener: listener.bind(("127.0.0.1", 0)) diff --git a/test/test_base.py b/test/test_base.py index 214cb2c7e..9adfc4eab 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -28,10 +28,10 @@ def tearDown(self): gc.collect() type_tuples = ( - ("blob", "8741fc1d09d61f02ffd8cded15ff603eff1ec070", "blob.py"), - ("tree", "3a6a5e3eeed3723c09f1ef0399f81ed6b8d82e79", "directory"), - ("commit", "4251bd59fb8e11e40c40548cba38180a9536118c", None), - ("tag", "e56a60e8e9cd333cfba0140a77cd12b0d9398f10", None), + ("blob", "HEAD:git/objects/blob.py", "blob.py"), + ("tree", "HEAD:git", "directory"), + ("commit", "dated", None), + ("tag", "annotated", None), ) def test_base_object(self): @@ -42,7 +42,8 @@ def test_base_object(self): s = set() num_objs = 0 num_index_objs = 0 - for obj_type, (typename, hexsha, path) in zip(types, self.type_tuples): + for obj_type, (typename, revision, path) in zip(types, self.type_tuples): + hexsha = self.rorepo.git.rev_parse(revision) binsha = hex_to_bin(hexsha) item = None if path is None: @@ -116,7 +117,7 @@ def test_with_bare_rw_repo(self, bare_rw_repo: Repo): assert osp.isdir(bare_rw_repo.working_dir) assert bare_rw_repo.working_tree_dir is None - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_with_rw_repo(self, rw_repo: Repo): assert not rw_repo.config_reader("repository").getboolean("core", "bare") assert osp.isdir(rw_repo.working_tree_dir) @@ -125,7 +126,7 @@ def test_with_rw_repo(self, rw_repo: Repo): @pytest.mark.net @skipIf(sys.platform == "win32", "FIXME: Freezes! sometimes...") - @with_rw_and_rw_remote_repo("0.1.6") + @with_rw_and_rw_remote_repo("release") def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo): assert not rw_repo.config_reader("repository").getboolean("core", "bare") assert rw_remote_repo.config_reader("repository").getboolean("core", "bare") diff --git a/test/test_commit.py b/test/test_commit.py index 09570f15c..8c09828e5 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -5,6 +5,7 @@ import copy from datetime import datetime +from hashlib import sha256 from io import BytesIO from pathlib import Path import os.path as osp @@ -40,9 +41,9 @@ def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info= st = time.time() for cm in [ rwrepo.commit(commit_id), - rwrepo.commit("4c39f9da792792d4e73fc3a5effde66576ae128c"), - rwrepo.commit("0.1.6"), - next(rwrepo.commit("0.1.6").iter_parents(max_count=1)), + rwrepo.commit("root"), + rwrepo.commit("merge"), + rwrepo.commit("manifest-update"), ]: nds += 1 @@ -101,24 +102,24 @@ def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info= class TestCommit(TestCommitSerialization): def test_bake(self): - commit = self.rorepo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") + commit = self.rorepo.commit("manifest-update") # Commits have no dict. self.assertRaises(AttributeError, setattr, commit, "someattr", 1) commit.author # bake - self.assertEqual("Sebastian Thiel", commit.author.name) - self.assertEqual("byronimo@gmail.com", commit.author.email) + self.assertEqual("Fixture Author", commit.author.name) + self.assertEqual("author@example.com", commit.author.email) self.assertEqual(commit.author, commit.committer) assert isinstance(commit.authored_date, int) and isinstance(commit.committed_date, int) assert isinstance(commit.author_tz_offset, int) and isinstance(commit.committer_tz_offset, int) self.assertEqual( commit.message, - "Added missing information to docstrings of commit and stats module\n", + "Update manifest\n", ) @with_rw_repo("HEAD") def test_replace_no_changes(self, rw_repo): - old_commit = rw_repo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") + old_commit = rw_repo.commit("manifest-update") new_commit = old_commit.replace() for attr in old_commit.__slots__: @@ -126,21 +127,22 @@ def test_replace_no_changes(self, rw_repo): @with_rw_repo("HEAD") def test_replace_new_sha(self, rw_repo): - commit = rw_repo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") + commit = rw_repo.commit("manifest-update") new_commit = commit.replace(message="Added replace method") - assert new_commit.hexsha == "fc84cbecac1bd4ba4deaac07c1044889edd536e6" + assert new_commit.hexsha != commit.hexsha + assert rw_repo.commit(new_commit.hexsha).message == "Added replace method" assert new_commit.message == "Added replace method" @with_rw_repo("HEAD") def test_replace_invalid_attribute(self, rw_repo): - commit = rw_repo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") + commit = rw_repo.commit("manifest-update") with self.assertRaises(ValueError): commit.replace(badattr="This will never work") def test_stats(self): - commit = self.rorepo.commit("33ebe7acec14b25c5f84f35a664803fcab2f7781") + commit = self.rorepo.commit("root") stats = commit.stats def check_entries(d, has_change_type=False): @@ -163,14 +165,14 @@ def check_entries(d, has_change_type=False): # END for each stated file # Check that data is parsed properly. - michael = Actor._from_string("Michael Trier ") - self.assertEqual(commit.author, michael) - self.assertEqual(commit.committer, michael) - self.assertEqual(commit.authored_date, 1210193388) - self.assertEqual(commit.committed_date, 1210193388) - self.assertEqual(commit.author_tz_offset, 14400, commit.author_tz_offset) - self.assertEqual(commit.committer_tz_offset, 14400, commit.committer_tz_offset) - self.assertEqual(commit.message, "initial project\n") + author = Actor("Fixture Author", "author@example.com") + self.assertEqual(commit.author, author) + self.assertEqual(commit.committer, author) + self.assertEqual(commit.authored_date, 978307200) + self.assertEqual(commit.committed_date, 978307200) + self.assertEqual(commit.author_tz_offset, 0) + self.assertEqual(commit.committer_tz_offset, 0) + self.assertEqual(commit.message, "Initial commit\n") def test_stats_preserve_filename_spaces(self): paths = ("name", " name", "name ", " name ", " ") @@ -273,7 +275,7 @@ def test_stats_preserve_quoted_filename_bytes(self): } def test_renames(self): - commit = self.rorepo.commit("185d847ec7647fd2642a82d9205fb3d07ea71715") + commit = self.rorepo.commit("renamed") files = commit.stats.files # When a file is renamed, the output of git diff is like "dir/{old => new}" @@ -283,15 +285,15 @@ def test_renames(self): def check_entries(path, changes): expected = { - ".github/workflows/Future.yml": { - "insertions": 57, + "renamed.txt": { + "insertions": 1, "deletions": 0, - "lines": 57, + "lines": 1, }, - ".github/workflows/test_pytest.yml": { + "side.txt": { "insertions": 0, - "deletions": 55, - "lines": 55, + "deletions": 1, + "lines": 1, }, } assert path in expected @@ -312,8 +314,8 @@ def test_unicode_actor(self): assert isinstance(special.name, str) def test_traversal(self): - start = self.rorepo.commit("a4d06724202afccd2b5c54f81bcf2bf26dea7fff") - first = self.rorepo.commit("33ebe7acec14b25c5f84f35a664803fcab2f7781") + start = self.rorepo.commit("merge") + first = self.rorepo.commit("root") p0 = start.parents[0] p1 = start.parents[1] p00 = p0.parents[0] @@ -333,9 +335,7 @@ def test_traversal(self): # At some point, both iterations should stop. self.assertEqual(list(bfirst)[-1], first) - stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse( - ignore_self=0, as_edge=True - ) + stoptraverse = self.rorepo.commit("merge").traverse(ignore_self=0, as_edge=True) stoptraverse_list = list(stoptraverse) for itemtup in stoptraverse_list: self.assertIsInstance(itemtup, (tuple)) and self.assertEqual(len(itemtup), 2) # as_edge=True -> tuple @@ -346,7 +346,7 @@ def test_traversal(self): else: self.assertIsNone(src) # ignore_self=0 -> first is (None, Commit) - stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(as_edge=True) + stoptraverse = self.rorepo.commit("merge").traverse(as_edge=True) self.assertEqual(len(next(stoptraverse)), 2) # Ignore self @@ -374,7 +374,7 @@ def test_iteration(self): self.assertEqual(all_commits, list(self.rorepo.iter_commits())) # This includes merge commits. - mcomit = self.rorepo.commit("d884adc80c80300b4cc05321494713904ef1df2d") + mcomit = self.rorepo.commit("merge") assert mcomit in all_commits # We can limit the result to paths. @@ -420,20 +420,14 @@ def test_rev_list_bisect_all(self): in the commit header. This test ensures that we properly parse it. """ revs = self.rorepo.git.rev_list( - "933d23bf95a5bd1624fbcdf328d904e1fa173474", + "release", first_parent=True, bisect_all=True, ) commits = Commit._iter_from_process_or_stream(self.rorepo, StringProcessAdapter(revs.encode("ascii"))) - expected_ids = ( - "7156cece3c49544abb6bf7a0c218eb36646fad6d", - "1f66cfbbce58b4b552b041707a12d437cc5f400a", - "33ebe7acec14b25c5f84f35a664803fcab2f7781", - "933d23bf95a5bd1624fbcdf328d904e1fa173474", - ) - for sha1, commit in zip(expected_ids, commits): - self.assertEqual(sha1, commit.hexsha) + assert " (" in revs + self.assertEqual([line.split()[0] for line in revs.splitlines()], [commit.hexsha for commit in commits]) def test_iter_from_invalid_process_or_stream(self): for source, error in ((Mock(wait=Mock(), stdout=None), ValueError), (object(), TypeError)): @@ -445,14 +439,15 @@ def test_ambiguous_arg_iteration(self): assert list(repo.iter_commits(repo.head.ref)) == [repo.head.commit] def test_count(self): - self.assertEqual(self.rorepo.tag("refs/tags/0.1.5").commit.count(), 143) + self.assertEqual(self.rorepo.tag("refs/tags/root").commit.count(), 1) + self.assertEqual(self.rorepo.tag("refs/tags/base").commit.count(), 7) def test_list(self): # This doesn't work anymore, as we will either attempt getattr with bytes, or # compare 20 byte string with actual 20 byte bytes. This usage makes no sense # anyway. assert isinstance( - Commit.list_items(self.rorepo, "0.1.5", max_count=5)["5117c9c8a4d3af19a9958677e45cda9269de1541"], + Commit.list_items(self.rorepo, "base", max_count=5)[self.rorepo.commit("base").hexsha], Commit, ) @@ -473,7 +468,7 @@ def test_equality(self): def test_iter_parents(self): # Should return all but ourselves, even if skip is defined. - c = self.rorepo.commit("0.1.5") + c = self.rorepo.commit("base") for skip in (0, 1): piter = c.iter_parents(skip=skip) first_parent = next(piter) @@ -488,7 +483,7 @@ def test_name_rev(self): @with_rw_repo("HEAD", bare=True) def test_serialization(self, rwrepo): # Create all commits of our repo. - self.assert_commit_serialization(rwrepo, "0.1.6") + self.assert_commit_serialization(rwrepo, "release") @with_rw_repo("HEAD") def test_serialization_unicode_support(self, rw_repo): @@ -496,7 +491,7 @@ def test_serialization_unicode_support(self, rw_repo): # Create a commit with Unicode in the message, and the author's name. # Verify its serialization and deserialization. - cmt = rw_repo.commit("0.1.6") + cmt = rw_repo.commit("release") assert isinstance(cmt.message, str) # It automatically decodes it as such. assert isinstance(cmt.author.name, str) # Same here. @@ -526,7 +521,7 @@ def test_invalid_commit(self): "commit.sh", "raw", fixture_path("commit_invalid_data"), - Path(fixture_path("commit_invalid_data")).read_bytes().hex(), + sha256(Path(fixture_path("commit_invalid_data")).read_bytes()).hexdigest(), ) with Repo(fixture / "repo") as repo: cmt = Commit(repo, bytes.fromhex((fixture / "object-id").read_text().strip())) @@ -741,7 +736,7 @@ def test_gpgsig(self): "commit.sh", "raw", fixture_path("commit_with_gpgsig"), - Path(fixture_path("commit_with_gpgsig")).read_bytes().hex(), + sha256(Path(fixture_path("commit_with_gpgsig")).read_bytes()).hexdigest(), ) repo = Repo(fixture / "repo") self.addCleanup(repo.close) @@ -799,7 +794,7 @@ def stream(self, *args): assert getattr(c, field) is not None def test_datetimes(self): - commit = self.rorepo.commit("4251bd5") + commit = self.rorepo.commit("dated") self.assertEqual(commit.authored_date, 1255018625) self.assertEqual(commit.committed_date, 1255026171) self.assertEqual( @@ -878,7 +873,7 @@ def test_trailers(self): assert commit.trailers_dict == {KEY_2: [VALUE_2]} def test_commit_co_authors(self): - commit = copy.copy(self.rorepo.commit("4251bd5")) + commit = copy.copy(self.rorepo.commit("dated")) commit.message = """Commit message Co-authored-by: Test User 1 <602352+test@users.noreply.github.com> @@ -893,7 +888,7 @@ def test_commit_co_authors(self): def test_commit_co_authors_bounds_malformed_trailer(self): """A malformed trailer line must not make co_authors run in quadratic time.""" - commit = copy.copy(self.rorepo.commit("4251bd5")) + commit = copy.copy(self.rorepo.commit("dated")) # An unterminated trailer repeating " <" without a closing ">". The old # `(.*) <(.*?)>` regex backtracked over every " <" (O(n^2)); the crafted line # is fully attacker-controlled through the commit message. diff --git a/test/test_db.py b/test/test_db.py index 46580d84b..18bda8bcc 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -31,7 +31,7 @@ def test_base(self): gdb = GitCmdObjectDB(osp.join(self.rorepo.git_dir, "objects"), self.rorepo.git) # Partial to complete - works with everything. - hexsha = bin_to_hex(gdb.partial_to_complete_sha_hex("0.1.6")) + hexsha = bin_to_hex(gdb.partial_to_complete_sha_hex("release")) assert len(hexsha) == 40 assert bin_to_hex(gdb.partial_to_complete_sha_hex(hexsha[:20])) == hexsha diff --git a/test/test_diff.py b/test/test_diff.py index 5f3e44b80..abe41ac72 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -337,7 +337,7 @@ def test_empty_diff(self): self.assertEqual(res, []) def test_diff_initial_commit(self): - initial_commit = self.rorepo.commit("33ebe7acec14b25c5f84f35a664803fcab2f7781") + initial_commit = self.rorepo.commit("root") # Without creating a patch... diff_index = initial_commit.diff(NULL_TREE) @@ -350,7 +350,7 @@ def test_diff_initial_commit(self): self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path)) self.assertEqual(diff_index[0].b_path, "CHANGES", repr(diff_index[0].b_path)) self.assertIsNotNone(diff_index[0].new_file) - self.assertEqual(diff_index[0].diff, fixture("diff_initial")) + self.assertEqual(diff_index[0].diff, b"@@ -0,0 +1 @@\n+Initial changes\n") def test_diff_unsafe_paths(self): output = StringProcessAdapter(fixture("diff_patch_unsafe_paths")) @@ -471,7 +471,7 @@ def test_diff_rejects_no_index(self): def test_diff_interface(self): """Test a few variations of the main diff routine.""" assertion_map = {} - for i, commit in enumerate(self.rorepo.iter_commits("0.1.6", max_count=2)): + for i, commit in enumerate(self.rorepo.iter_commits("release", max_count=2)): diff_item = commit if i % 2 == 0: diff_item = commit.tree diff --git a/test/test_docs.py b/test/test_docs.py index ecbf525d5..a7abd9f4a 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -99,13 +99,15 @@ def test_init_repo_object(self, rw_dir, rw_repo): # [8-test_init_repo_object] active_branch = repo.active_branch self.assertEqual(repo.head.ref, active_branch) # HEAD is a sym-ref pointing to the active branch. - self.assertEqual(repo.tags["0.3.5"], repo.tag("refs/tags/0.3.5")) # You can access tags in various ways too. + self.assertEqual( + repo.tags["release"], repo.tag("refs/tags/release") + ) # You can access tags in various ways too. self.assertEqual(repo.refs[active_branch.name], repo.heads[active_branch.name]) # .refs provides all refs... if "TRAVIS" not in os.environ: remote_branch = next(ref for ref in repo.remotes.origin.refs if ref.remote_head != "HEAD") self.assertEqual(repo.refs[remote_branch.name], remote_branch) # ...remotes... - self.assertEqual(repo.refs["0.3.5"], repo.tags["0.3.5"]) # ... and tags. + self.assertEqual(repo.refs["release"], repo.tags["release"]) # ... and tags. # ![8-test_init_repo_object] # Create a new head/branch. @@ -316,17 +318,17 @@ def test_references_and_objects(self, rw_dir): # [12-test_references_and_objects] repo.commit("master") - repo.commit("v0.8.1") + repo.commit("my_tag") repo.commit("HEAD~10") # ![12-test_references_and_objects] # [13-test_references_and_objects] - fifty_first_commits = list(repo.iter_commits("master", max_count=50)) - assert len(fifty_first_commits) == 50 - # This will return commits 21-30 from the commit list as traversed backwards master. - ten_commits_past_twenty = list(repo.iter_commits("master", max_count=10, skip=20)) - assert len(ten_commits_past_twenty) == 10 - assert fifty_first_commits[20:30] == ten_commits_past_twenty + first_twenty_commits = list(repo.iter_commits("master", max_count=20)) + assert len(first_twenty_commits) == 20 + # This will return commits 11-20 from the commit list as traversed backwards master. + ten_commits_past_ten = list(repo.iter_commits("master", max_count=10, skip=10)) + assert len(ten_commits_past_ten) == 10 + assert first_twenty_commits[10:20] == ten_commits_past_ten # ![13-test_references_and_objects] # [14-test_references_and_objects] @@ -384,7 +386,7 @@ def test_references_and_objects(self, rw_dir): assert repo.tree() == repo.head.commit.tree past = repo.commit("HEAD~5") assert repo.tree(past) == repo.tree(past.hexsha) - self.assertEqual(repo.tree("v0.8.1").type, "tree") # Yes, you can provide any refspec - works everywhere. + self.assertEqual(repo.tree("my_tag").type, "tree") # Yes, you can provide any refspec - works everywhere. # ![21-test_references_and_objects] # [22-test_references_and_objects] @@ -506,7 +508,7 @@ def test_references_and_objects(self, rw_dir): repo.git.clear_cache() - @with_rw_repo("c15a6e1923a14bc760851913858a3942a4193cdb") + @with_rw_repo("submodule-current") @patch_git_config("protocol.file.allow", "always") def test_submodules(self, repo): # [1-test_submodules] diff --git a/test/test_fixture_health.py b/test/test_fixture_health.py index 63a1c6b8d..ed80e1a81 100644 --- a/test/test_fixture_health.py +++ b/test/test_fixture_health.py @@ -1,9 +1,12 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -"""Verify that the source repository is usable by git.""" +"""Verify that generated fixtures are isolated, reusable, and usable by Git.""" +import os import subprocess +import shutil +import sys from pathlib import Path import pytest @@ -13,7 +16,37 @@ REPO_ROOT = Path(__file__).resolve().parent.parent -FIXTURE_DIRS = [pytest.param(REPO_ROOT, id="repo_root")] + +def test_shared_fixture_works_without_checkout_history(tmp_path): + source = tmp_path / "source" + source.mkdir() + shutil.copytree(REPO_ROOT / "test/lib", source / "test/lib", ignore=shutil.ignore_patterns("__pycache__")) + for name in ("__init__.py", "cleanup.py"): + shutil.copyfile(REPO_ROOT / "test" / name, source / "test" / name) + shutil.copytree( + REPO_ROOT / "tests/fixtures", + source / "tests/fixtures", + ignore=shutil.ignore_patterns("generated-*", "__pycache__"), + ) + result = subprocess.run( + [ + sys.executable, + "-c", + "from test.lib import TestBase; " + "TestBase.setUpClass(); " + "assert TestBase.rorepo.head.commit.parents; " + "TestBase.tearDownClass()", + ], + cwd=source, + env={ + key: value + for key, value in os.environ.items() + if key in {"PATH", "SYSTEMROOT", "TEMP", "TMP", "TMPDIR", "GIT_PYTHON_BACKEND"} + }, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr def test_scripted_readonly_fixture_reuses_initial_state(): @@ -63,58 +96,11 @@ def fail(*args): assert caught.value.__suppress_context__ -def test_source_tree_has_no_gitlinks() -> None: - """All formerly external dependencies are regular monorepo directories.""" - if not (REPO_ROOT / ".git").exists(): - pytest.skip(f"{REPO_ROOT} is not a git checkout") - try: - result = subprocess.run( - ["git", "ls-files", "--stage"], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=True, - ) - except FileNotFoundError: - pytest.skip("git is not installed or not on PATH") - gitlinks = [line for line in result.stdout.splitlines() if line.startswith("160000 ")] - assert not gitlinks, "Unexpected submodules:\n" + "\n".join(gitlinks) - - -@pytest.mark.parametrize("fixture_dir", FIXTURE_DIRS) -def test_fixture_dir_is_trusted_by_git(fixture_dir: Path) -> None: - """git accepts ``fixture_dir`` as its own repository owned by a trusted user. - - Run ``git -C rev-parse --show-toplevel`` and assert it - succeeds and reports ``fixture_dir`` itself as the toplevel. Failure - typically means the directory's on-disk ownership doesn't match the - running user and the CI workflow's ``safe.directory`` list is missing - an entry that would override the check. - """ - if not (fixture_dir / ".git").exists(): - pytest.skip(f"{fixture_dir} is not a git checkout") - try: - result = subprocess.run( - ["git", "-C", str(fixture_dir), "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=False, - ) - except FileNotFoundError: - pytest.skip("git is not installed or not on PATH") - assert result.returncode == 0, ( - f"git refuses to operate in {fixture_dir}.\n" - f"stderr: {result.stderr.strip()}\n" - "The directory's owner doesn't match the running user and no " - "`safe.directory` entry overrides the check. On CI, the " - "workflow's `safe.directory` list typically needs an entry for " - "this path. Locally, this is unexpected and usually indicates " - "an ownership problem worth investigating." - ) - reported = Path(result.stdout.strip()) - assert reported.samefile(fixture_dir), ( - f"git reports the toplevel as {reported}, " - f"not as {fixture_dir} itself. " - "This usually means the directory is not an initialized git " - "repository (its `.git` marker may be stale or pointing elsewhere)." +@pytest.mark.parametrize("name", ["repo", "gitdb", "smmap"]) +def test_generated_repository_is_self_contained_and_trusted(name): + path = read_only_fixture("sample-repositories.sh") / name + assert not (path / ".git/objects/info/alternates").exists() + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], cwd=path, capture_output=True, text=True, check=True ) + assert Path(result.stdout.strip()).samefile(path) diff --git a/test/test_fun.py b/test/test_fun.py index 5ae101824..f392f5de9 100644 --- a/test/test_fun.py +++ b/test/test_fun.py @@ -33,12 +33,10 @@ def _assert_index_entries(self, entries, trees): # END assert entry matches fully def test_aggressive_tree_merge(self): - # Head tree with additions, removals and modification compared to its - # predecessor. + # Trees with additions, removals and modifications, including a submodule. repo = self.rorepo - HC = self.rorepo.commit("6c1faef799095f3990e9970bc2cb10aa0221cf9c") - H = HC.tree - B = HC.parents[0].tree + H = repo.tree("submodule-current") + B = repo.tree("side") # Entries from single tree. trees = [H.binsha] @@ -50,16 +48,16 @@ def test_aggressive_tree_merge(self): # Three way, no conflict. tree = self.rorepo.tree - B = tree("35a09c0534e89b2d43ec4101a5fb54576b577905") - H = tree("4fe5cfa0e063a8d51a1eb6f014e2aaa994e5e7d4") - M = tree("1f2b19de3301e76ab3a6187a49c9c93ff78bafbd") + B = tree("manifest-update") + H = tree("merge^1") + M = tree("side") trees = [B.binsha, H.binsha, M.binsha] self._assert_index_entries(self.merge_trees(repo, trees), trees) # Three-way, conflict in at least one file, both modified. - B = tree("a7a4388eeaa4b6b94192dce67257a34c4a6cbd26") - H = tree("f9cec00938d9059882bb8eabdaf2f775943e00e5") - M = tree("44a601a068f4f543f73fd9c49e264c931b1e1652") + B = tree("base") + H = tree("conflict-ours") + M = tree("conflict-theirs") trees = [B.binsha, H.binsha, M.binsha] self._assert_index_entries(self.merge_trees(repo, trees), trees) @@ -134,7 +132,7 @@ def test_linked_worktree_traversal(self): self.assertIsNotNone(gitdir) self.assertTrue(stat(gitdir).st_mode & S_IFDIR) - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_tree_names_from_git_preserve_undecodable_bytes(self, repo): tree = Tree(repo, repo._null_binsha, path="") tree._cache = [(b"a" * repo._oid_size, 0o100644, "\udc9f")] diff --git a/test/test_git.py b/test/test_git.py index dac9c11ed..5b1dd49de 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -558,15 +558,15 @@ def test_it_accepts_environment_variables(self): def test_persistent_cat_file_command(self): # Read header only. - hexsha = "b2339455342180c7cc1e9bba3e9f181f7baa5167" + hexsha = self.rorepo.commit("root").hexsha g = self.git.cat_file(batch_check=True, istream=subprocess.PIPE, as_process=True) - g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.write((hexsha + "\n").encode("ascii")) g.stdin.flush() obj_info = g.stdout.readline() # Read header + data. g = self.git.cat_file(batch=True, istream=subprocess.PIPE, as_process=True) - g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.write((hexsha + "\n").encode("ascii")) g.stdin.flush() obj_info_two = g.stdout.readline() self.assertEqual(obj_info, obj_info_two) @@ -577,7 +577,7 @@ def test_persistent_cat_file_command(self): g.stdout.read(1) # Now we should be able to read a new object. - g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.write((hexsha + "\n").encode("ascii")) g.stdin.flush() self.assertEqual(g.stdout.readline(), obj_info) @@ -588,7 +588,7 @@ def test_persistent_cat_file_command(self): self.assertEqual(size, size_two) def test_object_header_sends_each_name_as_one_record(self): - hexsha = "b2339455342180c7cc1e9bba3e9f181f7baa5167" + hexsha = self.rorepo.commit("root").hexsha git = Git(self.rorepo.working_dir) header = git.get_object_header(hexsha) diff --git a/test/test_index.py b/test/test_index.py index 63d33fbd1..ab7ae46ea 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -215,7 +215,7 @@ def _cmp_tree_index(self, tree, index): } assert {(entry.path, entry.stage) for entry in index.iter_entries()} == expected - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_index_lock_handling(self, rw_repo): index = rw_repo.index index_path = Path(index.path) @@ -240,7 +240,7 @@ def fail_copy(source, stream): assert not lock_path.exists() assert rw_repo.index.entry(*("foo", 0)).mode == 0o100644 - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_read_tree_methods_reject_index_output(self, rw_repo): output_path = (Path(rw_repo.working_tree_dir) / "alternate-index").as_posix() unsafe_option = f"--index-output={output_path}" @@ -256,11 +256,11 @@ def test_read_tree_methods_reject_index_output(self, rw_repo): with pytest.raises(UnsafeOptionError): rw_repo.index.merge_tree("HEAD", base=unsafe_option) - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_index_file_from_tree(self, rw_repo): - common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541" - cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573" - other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9" + common_ancestor_sha = "base" + cur_sha = "conflict-ours" + other_sha = "conflict-theirs" # Simple index from tree. base_index = IndexFile.from_tree(rw_repo, common_ancestor_sha) @@ -309,7 +309,7 @@ def merge_required(t): # END for each blob self.assertEqual(num_blobs, len([(e.path, e.stage) for e in three_way_index.iter_entries()])) - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_index_merge_tree(self, rw_repo): # A bit out of place, but we need a different repo for this: self.assertNotEqual(self.rorepo, rw_repo) @@ -317,7 +317,7 @@ def test_index_merge_tree(self, rw_repo): # SINGLE TREE MERGE # Current index is at the (virtual) cur_commit. - next_commit = "4c39f9da792792d4e73fc3a5effde66576ae128c" + next_commit = "manifest-update" parent_commit = rw_repo.head.commit.parents[0] manifest_key = IndexFile.entry_key("MANIFEST.in", 0) manifest_entry = rw_repo.index.entry(*manifest_key) @@ -372,7 +372,7 @@ def test_index_merge_tree(self, rw_repo): self.assertEqual(len(unmerged_blobs), 1) self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0]) - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_index_file_diffing(self, rw_repo): # Default IndexFile instance points to our index. index = IndexFile(rw_repo) @@ -388,7 +388,7 @@ def test_index_file_diffing(self, rw_repo): # Resetting the head will leave the index in a different state, and the diff # will yield a few changes. cur_head_commit = rw_repo.head.reference.commit - rw_repo.head.reset("HEAD~6", index=True, working_tree=False) + rw_repo.head.reset("HEAD~2", index=True, working_tree=False) # Diff against same index is 0. diff = index.diff() @@ -530,7 +530,7 @@ def _count_existing(self, repo, files): # END num existing helper - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_index_mutation(self, rw_repo): with xfail_if_raises( sys.platform == "win32" and (Git().config("core.symlinks") == "true" or symlinks_supported()), @@ -681,11 +681,11 @@ def mixed_iterator(): entries = index.reset(new_commit).add([osp.join("lib", "git", "*.py")], fprogress=self._fprogress_add) self._assert_entries(entries) self._assert_fprogress(entries) - self.assertEqual(len(entries), 14) + self.assertEqual({entry.path for entry in entries}, {"lib/git/__init__.py", "lib/git/blob.py"}) # Same file. entries = index.reset(new_commit).add( - [osp.join(rw_repo.working_tree_dir, "lib", "git", "head.py")] * 2, + [osp.join(rw_repo.working_tree_dir, "lib", "git", "blob.py")] * 2, fprogress=self._fprogress_add, ) self._assert_entries(entries) @@ -838,8 +838,8 @@ def rewriter(entry): def make_paths(): """Help out the test by yielding two existing paths and one new path.""" yield "CHANGES" - yield "ez_setup.py" - yield index.entry(*index.entry_key("README", 0)) + yield "setup.py" + yield index.entry(*index.entry_key("README.md", 0)) yield index.entry(*index.entry_key(".gitignore", 0)) for fid in range(3): @@ -916,9 +916,9 @@ def test_index_single_addremove(self): assert repo.index.remove(fp) def test_index_new(self): - B = self.rorepo.tree("6d9b1f4f9fa8c9f030e3207e7deacc5d5f8bba4e") - H = self.rorepo.tree("25dca42bac17d511b7e2ebdd9d1d679e7626db5f") - M = self.rorepo.tree("e746f96bcc29238b79118123028ca170adc4ff0f") + B = self.rorepo.tree("base") + H = self.rorepo.tree("conflict-ours") + M = self.rorepo.tree("conflict-theirs") for args in ((B.binsha,), (B.hexsha, H), (B, H.binsha, M.hexsha.encode("ascii"))): index = IndexFile.new(self.rorepo, *args) diff --git a/test/test_refs.py b/test/test_refs.py index bd0febf82..efeee1d04 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -10,7 +10,7 @@ import tarfile import unittest from itertools import chain -from pathlib import Path +from pathlib import Path, PurePosixPath from unittest.mock import Mock, patch import pytest @@ -270,7 +270,7 @@ def test_merge_base_preserves_nested_reference_arguments(object_format, ref_form @pytest.mark.parametrize("object_format", ["sha1", "sha256"]) @pytest.mark.parametrize("ref_format", ["files", "reftable"]) -@pytest.mark.parametrize("path_type", [Path, PathLikeMock]) +@pytest.mark.parametrize("path_type", [PurePosixPath, PathLikeMock]) def test_revision_arguments_preserve_pathlike_paths(object_format, ref_format, path_type): with fixture_repo("exact-refs.sh", object_format, ref_format, "shadowed") as repo: local = repo.head.commit @@ -486,14 +486,14 @@ def test_tag_base(self): # END if we have a tag object # END for tag in repo-tags assert tag_object_refs - assert isinstance(self.rorepo.tags["0.1.5"], TagReference) + assert isinstance(self.rorepo.tags["base"], TagReference) def test_tags_author(self): tag = self.rorepo.tags[0] tagobj = tag.tag assert isinstance(tagobj.tagger, Actor) tagger_name = tagobj.tagger.name - assert tagger_name == "Michael Trier" + assert tagger_name == "Fixture Author" def test_tag_headers_split_at_line_feeds_only(self): with fixture_repo("refs.sh", "tag-headers") as repo: @@ -638,7 +638,7 @@ def test_is_valid(self): def test_orig_head(self): assert type(self.rorepo.head.orig_head()) is SymbolicReference - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_head_checkout_detached_head(self, rw_repo): res = rw_repo.remotes.origin.refs.HEAD.reference.checkout() assert isinstance(res, SymbolicReference) @@ -697,7 +697,7 @@ def test_head_commands_allow_explicit_pathspec_from_file(self, rw_repo): assert head.reset(**options) is head assert branch.checkout(**options) == branch - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_head_reset(self, rw_repo): cur_head = rw_repo.head old_head_commit = cur_head.commit @@ -902,7 +902,7 @@ def test_head_reset(self, rw_repo): assert len(new_head.commit.diff(None)) # Create a new branch that is likely to touch the file we changed. - far_away_head = rw_repo.create_head("far_head", "HEAD~100") + far_away_head = rw_repo.create_head("far_head", "root") self.assertRaises(GitCommandError, far_away_head.checkout) assert active_branch == active_branch.checkout(force=True) assert rw_repo.head.reference != far_away_head @@ -914,7 +914,7 @@ def test_head_reset(self, rw_repo): assert ref.path == full_ref assert ref.object == rw_repo.head.commit - self.assertRaises(OSError, Reference.create, rw_repo, full_ref, "HEAD~20") + self.assertRaises(OSError, Reference.create, rw_repo, full_ref, "root") # It works if it is at the same spot though and points to the same reference. assert Reference.create(rw_repo, full_ref, "HEAD").path == full_ref Reference.delete(rw_repo, full_ref) @@ -1071,7 +1071,7 @@ def test_head_reset(self, rw_repo): # END for each path - @with_rw_repo("0.1.6") + @with_rw_repo("release") def test_tag_message(self, rw_repo): tag_ref = TagReference.create(rw_repo, "test-message-1", message="test") assert tag_ref.tag.message == "test" diff --git a/test/test_remote.py b/test/test_remote.py index 529c37aaf..aba4ddce7 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -443,7 +443,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo): @pytest.mark.net @skipIf(sys.platform == "win32", "FIXME: Freezes!") - @with_rw_and_rw_remote_repo("0.1.6") + @with_rw_and_rw_remote_repo("release") def test_base(self, rw_repo, remote_repo): num_remotes = 0 remote_set = set() @@ -646,7 +646,7 @@ def test_fetch_error(self, rw_repo): with self.assertRaisesRegex(GitCommandError, msg): rem.fetch("__BAD_REF__") - @with_rw_repo("0.1.6", bare=False) + @with_rw_repo("release", bare=False) def test_push_error(self, repo): rem = repo.remote("origin") with self.assertRaisesRegex(GitCommandError, "src refspec __BAD_REF__ does not match any"): @@ -1033,7 +1033,7 @@ def test_ls_remote_unsafe_options_allowed(self, rw_repo): rw_repo.git.ls_remote(".", **unsafe_option, allow_unsafe_options=True) @pytest.mark.net - @with_rw_and_rw_remote_repo("0.1.6") + @with_rw_and_rw_remote_repo("release") def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo): # Create branch with a name containing a NBSP bad_branch_name = f"branch_with_{chr(160)}_nbsp" diff --git a/test/test_repo.py b/test/test_repo.py index 38ac3e630..378208b30 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -4,7 +4,6 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ import gc -import glob import io import itertools import os @@ -39,7 +38,7 @@ from git.compat import safe_decode from git.exc import BadObject, UnsafeOptionError, UnsafeProtocolError from git.repo.fun import find_worktree_git_dir -from git.util import bin_to_hex, cwd, rmfile, rmtree +from git.util import bin_to_hex, cwd, rmtree from test.lib import PathLikeMock, TestBase, fixture, with_rw_directory, with_rw_repo from test.lib.fixtures import fixture_repo, read_only_fixture, writable_fixture @@ -53,23 +52,8 @@ def flatten(lol): return list(iter_flatten(lol)) -_tc_lock_fpaths = osp.join(osp.dirname(__file__), "../../.git/*.lock") - - -def _rm_lock_files(): - for lfp in glob.glob(_tc_lock_fpaths): - rmfile(lfp) - - class TestRepo(TestBase): - def setUp(self): - _rm_lock_files() - def tearDown(self): - for lfp in glob.glob(_tc_lock_fpaths): - if osp.isfile(lfp): - raise AssertionError("Previous TC left hanging git-lock file: {}".format(lfp)) - gc.collect() def test_new_should_raise_on_invalid_repo_location(self): @@ -224,7 +208,7 @@ def test_repo_discovery_preserves_object_directory(self): def test_repo_creation_from_different_paths(self): path = read_only_fixture( - "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "0.3.2.1", "worktree" + "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "tree-layout", "worktree" ) rw_repo = Repo(path / "repo") r_from_gitdir = Repo(rw_repo.git_dir) @@ -235,7 +219,7 @@ def test_repo_creation_from_different_paths(self): def test_repo_creation_pathlib(self): path = read_only_fixture( - "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "0.3.2.1", "worktree" + "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "tree-layout", "worktree" ) rw_repo = Repo(path / "repo") r_from_gitdir = Repo(pathlib.Path(rw_repo.git_dir)) @@ -243,7 +227,7 @@ def test_repo_creation_pathlib(self): def test_repo_creation_pathlike(self): path = read_only_fixture( - "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "0.3.2.1", "worktree" + "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, "tree-layout", "worktree" ) rw_repo = Repo(path / "repo") r_from_gitdir = Repo(PathLikeMock(rw_repo.git_dir)) @@ -270,7 +254,7 @@ def test_heads_should_populate_head_data(self): self.assertEqual(self.rorepo.heads[active_branch.name], active_branch) def test_tree_from_revision(self): - tree = self.rorepo.tree("0.1.6") + tree = self.rorepo.tree("release") self.assertEqual(len(tree.hexsha), 40) self.assertEqual(tree.type, "tree") self.assertEqual(self.rorepo.tree(tree), tree) @@ -282,7 +266,7 @@ def test_pickleable(self): pickle.loads(pickle.dumps(self.rorepo)) def test_commit_from_revision(self): - commit = self.rorepo.commit("0.1.4") + commit = self.rorepo.commit("annotated") self.assertEqual(commit.type, "commit") self.assertEqual(self.rorepo.commit(commit), commit) @@ -294,31 +278,30 @@ def test_commit_from_tag_starting_with_at(self): self.assertEqual(repo.commit("@foo"), commit) def test_commits(self): - mc = 10 - commits = list(self.rorepo.iter_commits("0.1.6", max_count=mc)) + mc = 5 + commits = list(self.rorepo.iter_commits("release", max_count=mc)) self.assertEqual(len(commits), mc) c = commits[0] - self.assertEqual("9a4b1d4d11eee3c5362a4152216376e634bd14cf", c.hexsha) - self.assertEqual(["c76852d0bff115720af3f27acdb084c59361e5f6"], [p.hexsha for p in c.parents]) - self.assertEqual("ce41fc29549042f1aa09cc03174896cf23f112e3", c.tree.hexsha) - self.assertEqual("Michael Trier", c.author.name) - self.assertEqual("mtrier@gmail.com", c.author.email) - self.assertEqual(1232829715, c.authored_date) - self.assertEqual(5 * 3600, c.author_tz_offset) - self.assertEqual("Michael Trier", c.committer.name) - self.assertEqual("mtrier@gmail.com", c.committer.email) - self.assertEqual(1232829715, c.committed_date) - self.assertEqual(5 * 3600, c.committer_tz_offset) - self.assertEqual("Bumped version 0.1.6\n", c.message) + self.assertEqual(self.rorepo.git.rev_parse("release"), c.hexsha) + self.assertEqual([self.rorepo.commit("base")], list(c.parents)) + self.assertEqual(self.rorepo.git.rev_parse("release^{tree}"), c.tree.hexsha) + self.assertEqual("Fixture Author", c.author.name) + self.assertEqual("author@example.com", c.author.email) + self.assertEqual(978307200, c.authored_date) + self.assertEqual(0, c.author_tz_offset) + self.assertEqual(c.author, c.committer) + self.assertEqual(c.authored_date, c.committed_date) + self.assertEqual(0, c.committer_tz_offset) + self.assertEqual("Release sample project\n", c.message) c = commits[1] self.assertIsInstance(c.parents, tuple) def test_trees(self): - mc = 30 + mc = 5 num_trees = 0 - for tree in self.rorepo.iter_trees("0.1.5", max_count=mc): + for tree in self.rorepo.iter_trees("base", max_count=mc): num_trees += 1 self.assertIsInstance(tree, Tree) # END for each tree @@ -555,10 +538,10 @@ def test_index(self): self.assertIsInstance(index, IndexFile) def test_tag(self): - assert self.rorepo.tag("refs/tags/0.1.5").commit + assert self.rorepo.tag("refs/tags/base").commit def test_tag_to_full_tag_path(self): - tags = ["0.1.5", "tags/0.1.5", "refs/tags/0.1.5"] + tags = ["base", "tags/base", "refs/tags/base"] value_errors = [] for tag in tags: try: @@ -569,7 +552,7 @@ def test_tag_to_full_tag_path(self): def test_archive(self): with tempfile.NamedTemporaryFile("wb", suffix="archive-test", delete=False) as stream: - self.rorepo.archive(stream, "0.1.6", path="doc") + self.rorepo.archive(stream, "release", path="doc") assert stream.tell() os.remove(stream.name) # Do it this way so we can inspect the file on failure. @@ -577,15 +560,15 @@ def test_archive_rejects_unsafe_options(self): tdir = str(read_only_fixture("repo.sh", "empty")) output_marker = osp.join(tdir, "pwn") with self.assertRaises(UnsafeOptionError): - self.rorepo.archive(io.BytesIO(), "0.1.6", exec=f"touch {output_marker}") + self.rorepo.archive(io.BytesIO(), "release", exec=f"touch {output_marker}") assert not osp.exists(output_marker) with self.assertRaises(UnsafeOptionError): - self.rorepo.archive(io.BytesIO(), "0.1.6", output=output_marker) + self.rorepo.archive(io.BytesIO(), "release", output=output_marker) assert not osp.exists(output_marker) with self.assertRaises(UnsafeOptionError): - self.rorepo.archive(io.BytesIO(), "0.1.6", add_file=output_marker) + self.rorepo.archive(io.BytesIO(), "release", add_file=output_marker) with self.assertRaises(UnsafeOptionError): - self.rorepo.archive(io.BytesIO(), "0.1.6", add_virtual_file="file:content") + self.rorepo.archive(io.BytesIO(), "release", add_virtual_file="file:content") def test_archive_rejects_unsafe_remote_protocol(self): tdir = str(read_only_fixture("repo.sh", "empty")) @@ -996,13 +979,13 @@ def _assert_rev_parse(self, name): self._assert_rev_parse_types(rev, obj2) # History with number - ni = 11 - history = [obj.parents[0]] - for _ in range(ni): - history.append(history[-1].parents[0]) - # END get given amount of commits + history = [] + ancestor = obj + while ancestor.parents and len(history) < 11: + ancestor = ancestor.parents[0] + history.append(ancestor) - for pn in range(11): + for pn in range(len(history)): rev = name + "~%i" % (pn + 1) obj2 = rev_parse(rev) self.assertEqual(obj2, history[pn]) @@ -1033,7 +1016,8 @@ def test_rev_parse(self): rev_parse = self.rorepo.rev_parse # Try special case: This one failed at some point, make sure its fixed. - self.assertEqual(rev_parse("33ebe").hexsha, "33ebe7acec14b25c5f84f35a664803fcab2f7781") + root_sha = self.rorepo.git.rev_parse("root") + self.assertEqual(rev_parse(root_sha[:5]).hexsha, root_sha) # Start from reference. num_resolved = 0 @@ -1058,7 +1042,7 @@ def test_rev_parse(self): assert num_resolved # It works with tags! - tag = self._assert_rev_parse("0.1.4") + tag = self._assert_rev_parse("annotated") self.assertEqual(tag.type, "tag") # try full sha directly (including type conversion). @@ -1066,13 +1050,13 @@ def test_rev_parse(self): self._assert_rev_parse_types(tag.object.hexsha, tag.object) # Multiple tree types result in the same tree: HEAD^{tree}^{tree}:CHANGES - rev = "0.1.4^{tree}^{tree}" + rev = "annotated^{tree}^{tree}" self.assertEqual(rev_parse(rev), tag.object.tree) self.assertEqual(rev_parse(rev + ":CHANGES"), tag.object.tree["CHANGES"]) # Try to get parents from first revision - it should fail as no such revision # exists. - first_rev = "33ebe7acec14b25c5f84f35a664803fcab2f7781" + first_rev = root_sha commit = rev_parse(first_rev) self.assertEqual(len(commit.parents), 0) self.assertEqual(commit.hexsha, first_rev) @@ -1089,9 +1073,9 @@ def test_rev_parse(self): # Needs a tag which points to a blob. # ref^0 returns commit being pointed to, same with ref~0, ^{}, and ^{commit} - tag = rev_parse("0.1.4") + tag = rev_parse("annotated") for token in ("~0", "^0", "^{}", "^{commit}"): - self.assertEqual(tag.object, rev_parse("0.1.4%s" % token)) + self.assertEqual(tag.object, rev_parse("annotated%s" % token)) # END handle multiple tokens # Try partial parsing. @@ -1108,10 +1092,10 @@ def test_rev_parse(self): # END for each binsha in repo # Missing closing brace: commit^{tree - self.assertRaises(BadName, rev_parse, "0.1.4^{tree") + self.assertRaises(BadName, rev_parse, "annotated^{tree") # Missing starting brace. - self.assertRaises(BadName, rev_parse, "0.1.4^tree}") + self.assertRaises(BadName, rev_parse, "annotated^tree}") # REVLOG ####### @@ -1153,7 +1137,7 @@ def test_submodules(self): "history.sh", self.rorepo.working_dir, self.rorepo.head.commit.hexsha, - "c15a6e1923a14bc760851913858a3942a4193cdb", + "submodule-current", "bare", ) rwrepo = Repo(path / "repo") @@ -1198,7 +1182,7 @@ def last_commit(repo, rev, path): for _ in range(64): for repo_type in (GitCmdObjectDB,): repo = Repo(self.rorepo.working_tree_dir, odbt=repo_type) - last_commit(repo, "HEAD", "test/test_base.py") + last_commit(repo, "HEAD", "git/objects/blob.py") # END for each repository type # END for each iteration @@ -1273,9 +1257,9 @@ def test_active_branch_raises_type_error_when_head_is_detached(self): def test_merge_base(self): repo = self.rorepo - c1 = "f6aa8d1" - c2 = repo.commit("d46e3fe") - c3 = "763ef75" + c1 = "merge^1" + c2 = repo.commit("side") + c3 = "merge" self.assertRaises(ValueError, repo.merge_base) self.assertRaises(ValueError, repo.merge_base, "foo") @@ -1284,7 +1268,7 @@ def test_merge_base(self): self.assertIsInstance(res, list) self.assertEqual(len(res), 1) self.assertIsInstance(res[0], Commit) - self.assertTrue(res[0].hexsha.startswith("3936084")) + self.assertEqual(res[0], repo.commit("manifest-update")) for kw in ("a", "all"): res = repo.merge_base(c1, c2, c3, **{kw: True}) @@ -1301,8 +1285,8 @@ def test_is_ancestor(self): raise RuntimeError("git merge-base --is-ancestor feature unsupported (test needs git 1.8.0 or later)") repo = self.rorepo - c1 = "f6aa8d1" - c2 = "763ef75" + c1 = "base" + c2 = "merge" self.assertTrue(repo.is_ancestor(c1, c1)) self.assertTrue(repo.is_ancestor("HEAD", "HEAD")) self.assertTrue(repo.is_ancestor(c1, c2)) @@ -1314,10 +1298,10 @@ def test_is_ancestor(self): def test_is_valid_object(self): repo = self.rorepo - commit_sha = "f6aa8d1" - blob_sha = "1fbe3e4375" - tree_sha = "960b40fe36" - tag_sha = "42c2f60c43" + commit_sha = repo.git.rev_parse("release")[:10] + blob_sha = repo.git.rev_parse("HEAD:README.md")[:10] + tree_sha = repo.git.rev_parse("HEAD^{tree}")[:10] + tag_sha = repo.git.rev_parse("annotated")[:10] # Check for valid objects. self.assertTrue(repo.is_valid_object(commit_sha)) @@ -1355,11 +1339,11 @@ def test_git_work_tree_from_bare_repo(self): assert Git(worktree).rev_parse("--is-bare-repository") == "false" assert not repo.bare assert osp.samefile(repo.working_tree_dir, worktree) - subdir = worktree / "git" / "repo" + subdir = worktree / "git" / "objects" with Repo(subdir, search_parent_directories=True) as sub_repo: assert not sub_repo.bare assert osp.samefile(sub_repo.working_dir, worktree) - expected = repo.git.log("-n", "1", "--format=%H", "--", "git/repo") + expected = repo.git.log("-n", "1", "--format=%H", "--", "git/objects") assert sub_repo.git.log("-n", "1", "--format=%H", "--", str(subdir)) == expected def test_git_work_tree_dotgit_relative(self): diff --git a/test/test_submodule.py b/test/test_submodule.py index c2620e9d9..751235e21 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1231,6 +1231,21 @@ def update(self, op, cur_count, max_count, message=""): prog = TestRootProgress() +@pytest.mark.parametrize("state", ["index", "worktree", "untracked"]) +@pytest.mark.parametrize("dry_run", [False, True]) +def test_remove_preserves_dirty_submodule(state, dry_run): + source = read_only_fixture("sample-repositories.sh") / "smmap" + with writable_fixture("submodule-cases.sh", "remove-dirty-" + state, source) as temporary: + with git.Repo(temporary.path() / "parent") as repo: + submodule = repo.submodules[0] + path = Path(submodule.abspath, "untracked" if state == "untracked" else "file") + with pytest.raises(InvalidGitRepositoryError): + submodule.remove(dry_run=dry_run) + assert submodule.exists() and submodule.module_exists() + assert path.read_text() == "keep this data\n" + assert not repo.is_dirty(working_tree=False) + + class TestSubmodule(TestBase): def _fixture(self, scenario, *, write=True, execute=False): source = self._small_repo_url() @@ -1243,9 +1258,9 @@ def _fixture(self, scenario, *, write=True, execute=False): def tearDown(self): gc.collect() - k_subm_current = "c15a6e1923a14bc760851913858a3942a4193cdb" - k_subm_changed = "394ed7006ee5dc8bddfd132b64001d5dfc0ffdd3" - k_no_subm_tag = "0.1.6" + k_subm_current = "submodule-current" + k_subm_changed = "submodule-old" + k_no_subm_tag = "release" def _do_base_tests(self, rwrepo): """Perform all tests in the given repository, it may be bare or nonbare""" @@ -1264,7 +1279,7 @@ def _do_base_tests(self, rwrepo): assert sm.path == "git/ext/gitdb" assert sm.path != sm.name # In our case, we have ids there, which don't equal the path. - assert sm.url.endswith("github.com/gitpython-developers/gitdb.git") + assert sm.url == "../gitdb" assert sm.branch_path == "refs/heads/master" # the default ... assert sm.branch_name == "master" assert sm.parent_commit == rwrepo.head.commit @@ -1296,7 +1311,7 @@ def _do_base_tests(self, rwrepo): new_csmclone_path = None # if rwrepo.bare: with self.assertRaises(InvalidGitRepositoryError): - with sm.config_writer() as cw: + with sm.config_writer(): pass else: with sm.config_writer() as writer: @@ -1467,6 +1482,7 @@ def _do_base_tests(self, rwrepo): # If the head is detached, it still works (but warns). smref = sm.module().head.ref sm.module().head.ref = "HEAD~1" + sm.module().head.reset(index=True, working_tree=True) # If there is no tracking branch, we get a warning as well. csm_tracking_branch = csm.module().head.ref.tracking_branch() csm.module().head.ref.set_tracking_branch(None) @@ -1480,60 +1496,28 @@ def _do_base_tests(self, rwrepo): sm.module().head.ref = smref csm.module().head.ref.set_tracking_branch(csm_tracking_branch) - # REMOVAL OF REPOSITORY - ####################### - # Must delete something. - self.assertRaises(ValueError, csm.remove, module=False, configuration=False) - - # module() is supposed to point to gitdb, which has a child-submodule whose - # URL is still pointing to GitHub. To save time, we will change it to: - csm.set_parent_commit(csm.repo.head.commit) - with csm.config_writer() as cw: - cw.set_value("url", self._small_repo_url()) - csm.repo.index.commit("adjusted URL to point to local source, instead of the internet", skip_hooks=True) - - # We have modified the configuration, hence the index is dirty, and the - # deletion will fail. - # NOTE: As we did a few updates in the meanwhile, the indices were reset. - # Hence we create some changes. + # Test removal from an explicitly clean parent and a dirty child. + sm.module().head.reset(index=True, working_tree=True) csm.set_parent_commit(csm.repo.head.commit) - with sm.config_writer() as writer: - writer.set_value("somekey", "somevalue") - with csm.config_writer() as writer: - writer.set_value("okey", "ovalue") + self.assertRaises(ValueError, csm.remove, module=False, configuration=False) + fn = join_path_native(csm.module().working_tree_dir, "newfile") + with open(fn, "w") as fd: + fd.write("untracked child data") + self.assertRaises(InvalidGitRepositoryError, csm.remove) self.assertRaises(InvalidGitRepositoryError, sm.remove) - # If we remove the dirty index, it would work. - sm.module().index.reset() - # Still, we have the file modified. self.assertRaises(InvalidGitRepositoryError, sm.remove, dry_run=True) - sm.module().index.reset(working_tree=True) - - # Enforce the submodule to be checked out at the right spot as well. - csm.update() - assert csm.module_exists() - assert csm.exists() - assert osp.isdir(csm.module().working_tree_dir) + assert Path(fn).read_text() == "untracked child data" - # This would work. assert sm.remove(force=True, dry_run=True) is sm - assert sm.module_exists() - sm.remove(force=True, dry_run=True) - assert sm.module_exists() - - # But... we have untracked files in the child submodule. - fn = join_path_native(csm.module().working_tree_dir, "newfile") - with open(fn, "w") as fd: - fd.write("hi") - self.assertRaises(InvalidGitRepositoryError, sm.remove) + assert sm.module_exists() and csm.module_exists() + assert csm.exists() + assert Path(fn).read_text() == "untracked child data" - # Forcibly delete the child repository. prev_count = len(sm.children()) csm.remove(force=True) assert not csm.exists() assert not csm.module_exists() assert len(sm.children()) == prev_count - 1 - # Now we have a changed index, as configuration was altered. - # Fix this. sm.module().index.reset(working_tree=True) # Now delete only the module of the main submodule. diff --git a/test/test_tree.py b/test/test_tree.py index eea3f574f..8d29319fb 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -105,7 +105,7 @@ class TestTree(TestBase): @with_rw_repo("HEAD") def test_serializable(self, rw_repo): # Tree at the given commit contains a submodule as well. - roottree = rw_repo.tree("6c1faef799095f3990e9970bc2cb10aa0221cf9c") + roottree = rw_repo.tree("submodule-current") for item in roottree.traverse(ignore_self=False): if item.type != Tree.type: continue @@ -154,7 +154,7 @@ def test_tree_modifier_ordering(self): """TreeModifier.set_done() sorts files in the same order git does.""" git_file_names_in_order = self._get_git_ordered_files() - hexsha = "6c1faef799095f3990e9970bc2cb10aa0221cf9c" + hexsha = self.rorepo.commit("submodule-current").hexsha roottree = self.rorepo.tree(hexsha) blob_mode = Tree.blob_id << 12 tree_mode = Tree.tree_id << 12 @@ -303,7 +303,7 @@ def test_gitmodules_short_name_near_misses_round_trip(self, rw_repo, name): assert Tree(rw_repo, stored.binsha, path="")._cache == cache def test_traverse(self): - root = self.rorepo.tree("0.1.6") + root = self.rorepo.tree("release") num_recursive = 0 all_items = [] for obj in root.traverse(): @@ -358,47 +358,47 @@ def lib_folder(t, _d): assert found_slash def test_repo_lookup_string_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") blob = tree / ".gitignore" assert isinstance(blob, Blob) - assert blob.hexsha == "787b3d442a113b78e343deb585ab5531eb7187fa" + assert blob.data_stream.read() == b"*.pyc\n" def test_repo_lookup_pathlike_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") blob = tree / PathLikeMock(".gitignore") assert isinstance(blob, Blob) - assert blob.hexsha == "787b3d442a113b78e343deb585ab5531eb7187fa" + assert blob.data_stream.read() == b"*.pyc\n" def test_repo_lookup_invalid_string_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") with pytest.raises(KeyError): tree / "doesnotexist" def test_repo_lookup_invalid_pathlike_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") with pytest.raises(KeyError): tree / PathLikeMock("doesnotexist") def test_repo_lookup_nested_string_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") blob = tree / "git/__init__.py" assert isinstance(blob, Blob) - assert blob.hexsha == "d87dcbdbb65d2782e14eea27e7f833a209c052f3" + assert blob.data_stream.read() == b"# Sample module\nvalue = 1\n" def test_repo_lookup_nested_pathlike_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") blob = tree / PathLikeMock("git/__init__.py") assert isinstance(blob, Blob) - assert blob.hexsha == "d87dcbdbb65d2782e14eea27e7f833a209c052f3" + assert blob.data_stream.read() == b"# Sample module\nvalue = 1\n" def test_repo_lookup_folder_string_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") tree = tree / "git" assert isinstance(tree, Tree) - assert tree.hexsha == "ec8ae429156d65afde4bbb3455570193b56f0977" + assert [entry.name for entry in tree] == ["__init__.py", "objects"] def test_repo_lookup_folder_pathlike_path(self): - tree = self.rorepo.tree("0.3.2.1") + tree = self.rorepo.tree("tree-layout") tree = tree / PathLikeMock("git") assert isinstance(tree, Tree) - assert tree.hexsha == "ec8ae429156d65afde4bbb3455570193b56f0977" + assert [entry.name for entry in tree] == ["__init__.py", "objects"] diff --git a/tests/fixtures/benchmark-comparison.sh b/tests/fixtures/benchmark-comparison.sh index a555b81f1..33084b69b 100644 --- a/tests/fixtures/benchmark-comparison.sh +++ b/tests/fixtures/benchmark-comparison.sh @@ -3,12 +3,14 @@ set -eu for name in cli gix; do backend=$name fixture_revision=fixture + gitpython_source=source result_digest=equal cli_processes=0 if test "$name" = gix; then case "$1" in backend) backend=different ;; fixture_revision) fixture_revision=different ;; + gitpython_source) gitpython_source=different ;; result_digest) result_digest=different ;; cli_processes) cli_processes=1 ;; equal) ;; @@ -20,7 +22,7 @@ for name in cli gix; do "version": "1.0", "benchmarks": [{"runs": [{"values": [0.1, 0.2]}]}], "metadata": { - "gitpython_revision": "source", "fixture_revision": "$fixture_revision", + "gitpython_source": "$gitpython_source", "fixture_revision": "$fixture_revision", "fixture_tree": "tree", "fixture_status": "clean", "fixture_object_format": "sha1", "fixture_ref_format": "files", "git_version": "git version 2.52.0", "python_version": "3.12.14", "result_digest": "$result_digest", "backend_decisions": "[]", diff --git a/tests/fixtures/benchmark-source.sh b/tests/fixtures/benchmark-source.sh new file mode 100644 index 000000000..4976e6f0a --- /dev/null +++ b/tests/fixtures/benchmark-source.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eu +mkdir -p source/git source/test/performance +printf '1.0\n' > source/VERSION +printf 'value = 1\n' > source/git/module.py +printf 'value = 1\n' > source/test/performance/bench_repository.py +cp -R source copy +cp -R source moved +cp -R source harness +printf 'value = 2\n' > copy/git/module.py +printf 'value = 2\n' > harness/test/performance/bench_repository.py diff --git a/tests/fixtures/benchmark.sh b/tests/fixtures/benchmark.sh index 9ac4b0f90..ea1dee073 100644 --- a/tests/fixtures/benchmark.sh +++ b/tests/fixtures/benchmark.sh @@ -1,9 +1,23 @@ #!/usr/bin/env bash set -eu -git init --object-format=sha1 --ref-format=files repo -git -C repo fetch --no-tags "$1" 6ba2c0a2f9ee7feffd7e079621c4845820180c9a -git -C repo checkout -b benchmark FETCH_HEAD +export GIT_AUTHOR_NAME='Benchmark' GIT_AUTHOR_EMAIL='benchmark@example.invalid' +export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" +export GIT_AUTHOR_DATE='2001-01-01T00:00:00 +0000' +export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" +git init --object-format=sha1 --ref-format=files -b main dependency +git -C dependency commit --allow-empty -m 'Dependency' +git init --object-format=sha1 --ref-format=files -b benchmark repo +mkdir -p repo/git/objects repo/dependency +printf 'Sample objects\n' > repo/git/objects/readme +printf '[submodule "dependency"]\n\tpath = dependency\n\turl = ../dependency\n' > repo/.gitmodules +git -C repo update-index --add --cacheinfo "160000,$(git -C dependency rev-parse HEAD),dependency" +for n in $(seq 1 30); do + printf 'Revision %s\n' "$n" >> repo/README.md + git -C repo add . + git -C repo commit -m "Revision $n" +done +git -C repo tag -a v1.0 HEAD~10 -m 'Sample release' git -C repo config core.excludesFile /dev/null git -C repo config core.attributesFile /dev/null git -C repo config core.fsmonitor false diff --git a/tests/fixtures/dependencies.sh b/tests/fixtures/dependencies.sh deleted file mode 100755 index 09177bf52..000000000 --- a/tests/fixtures/dependencies.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eu - -source=$1 -# Include the source revision in the cache identity even though checkout uses history. -source_revision=$2 -name=$3 -case "$name" in - gitdb) revision=2da3232f9d58e7761e384ac6d32f7b1ed77a74a2 ;; - smmap) revision=8ce61ad5cc4016bffaf25080bc0d69b3acbe8555 ;; - *) printf 'Unknown dependency: %s\n' "$name" >&2; exit 1 ;; -esac - -git -C "$source" cat-file -e "$source_revision^{commit}" -git clone --shared --no-checkout "$source" repo -git -C repo checkout -B master "$revision" -if test "$name" = smmap; then - git -C repo tag -a -f v0.8.1 master~10 -m 'Test fixture tag' -fi -git -C repo gc diff --git a/tests/fixtures/history.sh b/tests/fixtures/history.sh index 68f983595..c1dc6ee7f 100755 --- a/tests/fixtures/history.sh +++ b/tests/fixtures/history.sh @@ -27,10 +27,4 @@ else git -C repo update-ref -m 'Create test branch' refs/heads/master "$revision" if test "$layout" != bare; then git -C repo checkout --force master; fi - if test "$layout" = baseline; then - test -n "$(git -C repo tag --list '[0-9]*' 'v[0-9]*')" - git -C repo tag -f __testing_point__ "$source_revision" - for _ in 1 2 3; do git -C repo reset --hard HEAD~1; done - git -C repo reset --hard "$source_revision" - fi fi diff --git a/tests/fixtures/sample-repositories.sh b/tests/fixtures/sample-repositories.sh new file mode 100644 index 000000000..c1f01b046 --- /dev/null +++ b/tests/fixtures/sample-repositories.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -eu + +export GIT_AUTHOR_NAME='Fixture Author' GIT_AUTHOR_EMAIL='author@example.com' +export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" +export GIT_AUTHOR_DATE='2001-01-01T00:00:00 +0000' +export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + +# Small standalone clone sources, including a nested submodule and older releases. +for project in smmap gitdb; do + git init -q --object-format=sha1 --ref-format=files -b master "$project" + printf 'from setuptools import setup\nsetup()\n' > "$project/setup.py" + printf 'Sample dependency\n' > "$project/README.md" + printf 'Fixture license\n' > "$project/LICENSE" + mkdir -p "$project/smmap" "$project/doc" + printf '# Sample module\n' > "$project/smmap/__init__.py" + printf 'Sample documentation\n' > "$project/doc/readme" + if test "$project" = gitdb; then + rm gitdb/smmap/__init__.py + printf '[submodule "smmap"]\n\tpath = smmap\n\turl = ../smmap\n' > gitdb/.gitmodules + git -C gitdb update-index --add --cacheinfo "160000,$(git -C smmap rev-parse HEAD),smmap" + fi + for n in $(seq 1 30); do + printf 'revision %s\n' "$n" > "$project/file" + git -C "$project" add . + git -C "$project" commit -qm "Dependency revision $n" + if test "$n" = 20; then git -C "$project" tag -a v1.0 -m 'Sample release'; fi + done +done + +git init -q --object-format=sha1 --ref-format=files -b master repo +cd repo +printf 'Initial changes\n' > CHANGES +git add CHANGES +git commit -qm 'Initial commit' +git tag root + +mkdir -p lib/git git/objects doc test +printf 'Fixture Author\n' > AUTHORS +printf 'Sample repository\n' > README.md +printf '3.0.0\n' > VERSION +printf 'Fixture license\n' > LICENSE +printf 'Sample tests\n' > test/readme +printf '*.pyc\n' > .gitignore +printf 'include README.md\n' > MANIFEST.in +printf 'Sample documentation\n' > doc/readme.txt +printf 'from setuptools import setup\nsetup()\n' > setup.py +printf '# Sample module\nvalue = 1\n' > lib/git/__init__.py +printf '# Another module\n' > lib/git/blob.py +printf '# Sample module\nvalue = 1\n' > git/__init__.py +printf '# Sample object\nvalue = 1\n' > git/objects/blob.py +git add . +git commit -qm 'Add project files' +git tag -a annotated -m 'Annotated release' +git tag tree-layout + +for n in $(seq 1 5); do + printf 'Change %s\n' "$n" >> CHANGES + git commit -qam "Change $n" +done +git tag base +printf 'Release\n' >> CHANGES +git commit -qam 'Release sample project' +git tag release +printf 'include AUTHORS\n' >> MANIFEST.in +git commit -qam 'Update manifest' +git tag manifest-update + +# Two branches with two unique ancestors exercise breadth/depth traversal. +git checkout -qb side +printf 'Side contributor\n' >> AUTHORS +git commit -qam 'Add side contributor' +printf 'Side work\n' > side.txt +git add side.txt +git commit -qm 'Side work' +git checkout -q master +printf 'Main contributor\n' > main.txt +git add main.txt +git commit -qm 'Main work' +printf 'Main changes\n' >> CHANGES +git commit -qam 'More main work' +git merge -q --no-ff side -m 'Merge side work' +git tag merge + +# Independent conflicting changes for index and tree merge tests. +git checkout -qb conflict-ours base +printf 'ours\n' > lib/git/__init__.py +git commit -qam 'Our change' +git tag conflict-ours +git checkout -qb conflict-theirs base +printf 'theirs\n' > lib/git/__init__.py +git commit -qam 'Their change' +git tag conflict-theirs +git checkout -q master +git branch -D conflict-ours conflict-theirs + +git mv side.txt renamed.txt +git commit -qm 'Rename file' +git tag renamed +GIT_AUTHOR_DATE='2009-10-08T18:17:05 +0200' GIT_COMMITTER_DATE='2009-10-08T20:22:51 +0200' \ + git commit --allow-empty -qm 'Different author and committer dates' +git tag dated + +# Historical submodule states use generated dependency commits, never remote data. +mkdir -p git/ext/gitdb +printf '[submodule "old-name"]\n\tpath = git/ext/gitdb\n\turl = ../gitdb\n' > .gitmodules +git add .gitmodules +git update-index --add --cacheinfo "160000,$(git -C ../gitdb rev-parse HEAD~2),git/ext/gitdb" +git commit -qm 'Add submodule' +git update-index --cacheinfo "160000,$(git -C ../gitdb rev-parse HEAD~1),git/ext/gitdb" +git commit -qm 'Advance submodule' +git tag submodule-old +git config -f .gitmodules --rename-section submodule.old-name submodule.gitdb +git add .gitmodules +git update-index --cacheinfo "160000,$(git -C ../gitdb rev-parse HEAD),git/ext/gitdb" +git commit -qm 'Rename and advance submodule' +git tag submodule-current +git rm -q --cached git/ext/gitdb .gitmodules +rm .gitmodules +git commit -qm 'Remove submodule' + +for n in $(seq 1 30); do + printf 'Work %s\n' "$n" >> README.md + git commit -qam "Work $n" +done +cd .. +git clone -q --bare repo origin +git -C repo remote add origin ../origin +git -C repo fetch -q origin diff --git a/tests/fixtures/submodule-cases.sh b/tests/fixtures/submodule-cases.sh index c510c513c..873dafcfa 100644 --- a/tests/fixtures/submodule-cases.sh +++ b/tests/fixtures/submodule-cases.sh @@ -15,6 +15,13 @@ add_module() { git -C parent commit -qm 'Add submodule' } case "$scenario" in + remove-dirty-index|remove-dirty-worktree|remove-dirty-untracked) + add_module module module + path="file" + if test "$scenario" = remove-dirty-untracked; then path=untracked; fi + printf 'keep this data\n' > "parent/module/$path" + if test "$scenario" = remove-dirty-index; then git -C parent/module add file; fi + ;; deinit|deinit-latest|deinit-gitlink|deinit-nonempty|deinit-nested) git init -q -b master source printf '%s' 'submodule content' > source/file