././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.hg_archival.txt0000644000000000000000000000016714751647721012727 0ustar00repo: 06366111af3c6a2ffa06333ed60d3ed3b9ec0763 node: f075a628c77f4f1f190baf8dd50c11a884bef931 branch: 1.2.x tag: 1.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.gitignore0000644000000000000000000000011414751647721011621 0ustar00*.pyc tests/*.err tests/.testtimes build dist *.egg-info *.orig \#*\# .\#* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.gitlab-ci.yml0000644000000000000000000002111214751647721012266 0ustar00stages: - images - current - compat - archives .base: image: registry.heptapod.net:443/mercurial/hg-git/ci/$CI_COMMIT_HG_BRANCH:hg-$HG-py$PYTHON timeout: 30m retry: 1 services: - name: registry.heptapod.net:443/mercurial/hg-git/git-server alias: git-server variables: CI_CLEVER_CLOUD_FLAVOR: XS CI_TEST_GIT_NETWORKING: 1 script: - adduser -D test - chown -R test * - su test -c contrib/ci.sh - mv tests/.coverage coverage-$CI_JOB_ID artifacts: &test-artifacts expire_in: 1 day paths: - coverage-$CI_JOB_ID reports: junit: tests-$CI_JOB_ID.xml # First, test with the latest and greatest Mercurial, across the # versions Python supported. We generally assume that if e.g. CPython # 3.6 and 3.9 work, anything in between will work as well. Latest: extends: .base stage: current variables: CI_CLEVER_CLOUD_FLAVOR: M parallel: matrix: - &latest-versions PYTHON: - "3.9" - "3.13" HG: - "6.9" # This is the authoritative list of versions of Mercurial that this # extension is supported and tested with; it should be kept in sync # with __init__.py. # # Versions prior to the version that ships in the latest Ubuntu LTS # release (4.5.3 for 18.04; 5.3.1 for 20.04) may be dropped if they # interfere with new development. Supported: extends: .base stage: compat parallel: matrix: - &supported-versions PYTHON: "3.9" HG: - "6.6" - "6.7" - "6.8" # Test that it is possible to use and run hg-git on versions of Alpine # that include Dulwich, and with the oldest Dulwich supported. Alpine: stage: compat image: name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:$ALPINE variables: CI_CLEVER_CLOUD_FLAVOR: XS timeout: 20m retry: 1 script: - >- apk add gnupg git unzip openssh mercurial py3-coverage py3-pygments py3-setuptools_scm py3-urllib3 - | if test -z "$DULWICH" then apk add py3-dulwich else apk add build-base python3-dev py3-pip pip install --break-system-packages dulwich==$DULWICH fi - adduser -D test - chown -R test * - SETUPTOOLS_SCM_PRETEND_VERSION=1 PYTHON=3 su test -c contrib/ci.sh - mv tests/.coverage coverage-$CI_JOB_ID artifacts: *test-artifacts parallel: matrix: - ALPINE: "3.19" DULWICH: - "0.21.6" - ALPINE: - "3.19" # Test that the tests pass against the current branches Development: extends: .base stage: compat rules: &upcoming-rules # allow opting out of upcoming releases on legacy branches - if: '$CI_SKIP_UPCOMING' when: never # disallow failures for scheduled builds, so we get a nice # notification - if: '$CI_PIPELINE_SOURCE == "schedule"' allow_failure: false # but we don't want to prevent merging unrelated work, so allow # failures for normal builds -- all of this is to avoid a detached # build for merge requests - if: '$CI_PIPELINE_SOURCE == "push"' allow_failure: true - if: '$CI_PIPELINE_SOURCE == "web"' when: manual allow_failure: true parallel: matrix: - &development-versions PYTHON: - "3.13" HG: - "stable" - "default" Coverage: stage: .post image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:alpine variables: CI_CLEVER_CLOUD_FLAVOR: XS allow_failure: true coverage: /^TOTAL.+?([.0-9]+\%)$/ script: - pip install coverage - coverage combine coverage-* - coverage xml --ignore-errors - coverage html --ignore-errors - coverage report artifacts: expire_in: 1 week paths: - htmlcov reports: coverage_report: coverage_format: cobertura path: coverage.xml # Build images for the above tasks; this should be a scheduled job, as # it is quite unnecessary to run on every invocation. CI images: stage: images tags: - container-registry-push rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' - if: '$CI_BUILD_IMAGES == "1"' image: name: gcr.io/kaniko-project/executor:debug entrypoint: [ "" ] script: - | cat > /kaniko/.docker/config.json < /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/contrib/docker/Dockerfile.alpine --build-arg PYTHON=$PYTHON --build-arg HG=$HG --single-snapshot --cleanup --destination registry.heptapod.net:443/mercurial/hg-git/ci/$CI_COMMIT_HG_BRANCH:hg-$HG-py$PYTHON parallel: matrix: - *latest-versions - *supported-versions - *development-versions # Builds the image used by tests/test-networking.t Git service image: stage: images tags: - container-registry-push rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' - if: '$CI_BUILD_IMAGES == "1"' image: name: gcr.io/kaniko-project/executor:debug entrypoint: [ "" ] timeout: 5m script: - mkdir -p /kaniko/.docker /kaniko/ssl/certs - | cat > /kaniko/.docker/config.json < /kaniko/ssl/certs/Heptapod_Tooling_CA.crt << EOF -----BEGIN CERTIFICATE----- MIIDfjCCAmagAwIBAgIUZ7t3vKco1mvle/l1TfTRooLWduMwDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcSGVwdGFwb2QgSW50ZXJuYWwgVG9vbGluZyBDQTAeFw0y MDExMzAyMTMzMTBaFw0zMDExMjgyMTMzMTBaMCcxJTAjBgNVBAMMHEhlcHRhcG9k IEludGVybmFsIFRvb2xpbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQCnxPEwRwppDzuHGccnfdVpppX5s1mWlWl1FqiqdtnJWrkTzKiolJurZXms Kt6iEB75yMD9r6KiTSN0vRcaH+qhZ2rA/frfKpkhSShp6TZke87tRdLufgUNxVNt ObAWaXEV2qePRHzLwx016uc/TwrdBvBqjPrRv2gwXLk9gadIPmAMVqSZE9I4qKru 1RciZ2J2R8zDcJuZ4Pi/uQi3XGD0Tm0iVyjIO5zou3+5R6khx4tBcZQX+XZD/usU 10emcZoFHIkc0+uKos+AEB1CBkXCYZDy2G3u8+tYXvNQaMk12Y31/Fv1LdjGwtYG qEi9k05x/nxOwSK/idPPSeeogcdxAgMBAAGjgaEwgZ4wHQYDVR0OBBYEFNnphf7w hzIVfEaGTRgGA+COTFfvMGIGA1UdIwRbMFmAFNnphf7whzIVfEaGTRgGA+COTFfv oSukKTAnMSUwIwYDVQQDDBxIZXB0YXBvZCBJbnRlcm5hbCBUb29saW5nIENBghRn u3e8pyjWa+V7+XVN9NGigtZ24zAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjAN BgkqhkiG9w0BAQsFAAOCAQEAmW/80xxQBSQdcEYw3euGDsK72ENztS4P4x12H55j lULg7DaoWgGDkIIBuhCK+5Y3wzhQyvsBrSB+LcVyVLQbS6yIIgzBVZPf/ZPMrC5x HjwXKGyZQFqFR/NGxVMmLidHXADF28EqFqSfdfWtdntnUL34hzAHZQkGn+d5S/d3 8qze9BWueo213tLCBtZxoTpneJDhq/fW++5BTagipzwpsmT26ycJ+k4uBwbkcnBo JP7Hk/I4BC3bZg8dDQShpPCGRlp9b8R0XSGoOoFRGW6z3nhh88kcdFdoPiMgEOuq FDmDjn14Jct6uFGIHEadGvenxDLivFxV+UnSS8u6tVwYTw== -----END CERTIFICATE----- EOF - > /kaniko/executor --context $CI_PROJECT_DIR/contrib/docker/git-server --dockerfile $CI_PROJECT_DIR/contrib/docker/git-server/Dockerfile --single-snapshot --cleanup --destination registry.heptapod.net:443/mercurial/hg-git/git-server Wheels: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python timeout: 15m variables: CI_CLEVER_CLOUD_FLAVOR: XS stage: archives variables: SETUPTOOLS_SCM_PRETEND_VERSION: "${CI_COMMIT_TAG}" rules: &wheel-rules # run on tags - if: $CI_COMMIT_TAG # run on protected references - if: '$CI_COMMIT_REF_PROTECTED == "true"' # and when explicitly requested - if: '$CI_BUILD_ARCHIVES == "1"' script: - pip install mercurial build - python -m build artifacts: paths: - dist Upload: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:alpine timeout: 5m variables: CI_CLEVER_CLOUD_FLAVOR: XS TWINE_NON_INTERACTIVE: 1 TWINE_USERNAME: 'gitlab-ci-token' TWINE_PASSWORD: '${CI_JOB_TOKEN}' TWINE_REPOSITORY_URL: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi' stage: .post rules: *wheel-rules script: - pip install twine - twine upload --disable-progress-bar --verbose --skip-existing dist/* Release: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:alpine timeout: 5m variables: CI_CLEVER_CLOUD_FLAVOR: XS TWINE_NON_INTERACTIVE: 1 TWINE_USERNAME: '__token__' TWINE_PASSWORD: '$PYPI_TOKEN' TWINE_REPOSITORY_URL: '$PYPI_REPOSITORY' stage: .post rules: # run on tags - if: '$CI_COMMIT_TAG && $PYPI_TOKEN && $PYPI_REPOSITORY' when: manual script: - pip install twine - twine upload --disable-progress-bar --verbose dist/* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.hgignore0000644000000000000000000000020614751647721011436 0ustar00syntax: re ^hggit/__version__\.py$ syntax: glob *.pyc tests/*.err tests/.testtimes build dist *.egg-info *.orig \#*\# .\#* .coverage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.hgsigs0000644000000000000000000003756014751647721011135 0ustar002f7bd8db709f8cf27107839951e88a38f915709d 0 iQIcBAABAgAGBQJSrI/JAAoJELnJ3IJKpb3VTroP/iK6eHVLtwDlsOqyEHCGP9CYw0Gd8bCq46YWZsVmz3bj01dKFwWSPKtFv+PNKFMGa8UV5VsEKCMoby2hzX6+beKs7oNLMU2YUGhP9A1QvQtpB5MArEMXWrQXeJTJZoZfA+2K2AgPJdeo1cLWKHIBc8IcG6yl9K55ummHwyg96KDna3fWrARxgLMNOBAva7otCSW/nQ0raABXi+JklBtB2C+Glxc3amnQQaWO659/DK0V2YS/54tiHJ73iJDlvm0bk+cgMUwtsgGJHFkLtCkbVA0tmDmsZBPaokRkgiJTEnqBLjKXoOqeoktAxdxSVHSEbIxx6guPhOtV0/cZsseShWQ7+3L1rCw6ujvqm9aBithb04LgfkJRZyXuV6NdbYaS+Y1guLJAEbkjmJplswxMI/laEFV5yTyPTQ2a4pTYuOx9Ix4jtDgpqzKztEc1hVk+dX/B2WWOjH+eLOYriAJSIUC+57GLA6WpeKMYYRZoIUhwM0TUjD3ePX9bIkUp4vIFUTdZV45TCsanJ+3rTIXiZh/C/O/jauNQ6wa2rkpmD4YRKb89YSbst6pdIaHDNBrnQppxD9DvYAO9ToFLCp+IfPxBdM5jvb9Wsmcw9p/3awfdII3W9mk/MOAJzEfPJLjWZbWaGHKrnPlQdBl/BP03ueCqZ/pw4Chm20WSAAFfqWam 29f51877a61078371d428a424799272e22d00810 0 iQIcBAABAgAGBQJTcpG4AAoJELnJ3IJKpb3VuEgP/RKdNxF5qRaHMtW3AEboeHamVSuAqaro+2W2RYjEuawK97agByGUcjxU4FuvzuRTvVUIrcim1PsXOw5pr30AA9alyQ7zAzs0KOx/lUkQ0cXJ4a0kKF/Um2Angx4uVausloa7A21fgB5GBdDP/47pN5G7ycAoi9rDkRNk9xvHnVXeAmIrg9IWrMoj31f/wQal7mmX0GS5SNAsWCjEfnUe5UYIsxTcWkAzyCggv/fSJdOfZ3qExeBACHr5Bo2twtY5GWlUWKYA3Pk9FthC4sQ1uPSghaHhTuXS38ZxSouU9WSq+hBkmv6dg8DFKlqHMiuet80ex1EWU6VHANmNnndHi33fsKJnojMzVmBJ28u2TLKoZLhIn46OdLs9hhj4+aPACPI1I0+3CIeQ57MNWstTL3CU9uiTgZ89akbEHax8EtsEElB83f5aXWQ7jerLPr1XGyv788NTeuTjBvYdAJz0ymkmhm4867TPAYU4A9rwEpgrlksVXmZD3aK0M2yQovlgfYmQaQ34DnzPTQNNKrd1fzp6PG4MTqecHz0O0S/ie+9AJSN4dht3rnsHyoIP20V4I1cjqqfQVEVXfKv+hPbdw2TDleRUr6EaPVcViM1LlZFezE0UUKRhz3n+PFkrC3Ssf9EfHAkjH439GULe3ezgfXCzsGubtaRnIuF7IB/XJ/gS 4e418736814f33e3351dd0a63628d06347c75082 0 iQIcBAABAgAGBQJT255gAAoJELnJ3IJKpb3VO0oQAMPpsoslxXiTpM+Q/bU56dwNb5OQoFcj5EHXfFDu4Lx1RKGjG2OtbhGLj7GIAJw3Yz64glC91i6NJNGkRQODrb4gJUsCM0NBaT+Zy9gq/udNLqYBDnpxRQExYO/ZVd0kWXIWqfAiAJGy+Ut5osoCgqaom1/IbQ5T3fbFFYmYLDVvw4TgsulM+iILstu4JS8/Rd0sBz7wofKU0d02+o5b1cyCyYJHjIGjkst/22Z+bOg8qHY8p9ZYM+rktHatN+bYY5OxYQYRGhPDXbx6fkTHPSo4MfKz+nH3oEWxV892+jHvTsiJSxDQXGFMm/1Pnn4+IvYw3v3Ohw0jj9/ndnktRK44sa/aEKwZO+VGUejkIXGSr6COmf1unNPqME1W6LFekYA+Ou4nFcYGTNmD/sV20tZTXkNL2nWX2gR2qGmsI8szdm5smQTFctmJvUiI+3QhASjXo8kty/JShvfhn7aUpqV+E/ddDecZx66I6JdCpsqIKZG6uRyMRHgYBwl6rC0lX9RVWLthr9Ed/XGnhxn1wClNO/mqZj3Ja34AB2ZCEdgse11w0+mnUxCn3MPeDAKz5tJaH68UnfG+79FyDJJtSZJb3xI4aC1Z3vWQWD/KeanTqjphkx4cy0t38+D5Q4SbHyiL5/hXQYBPnpm1DuHke1iJU6tVGVUvRzjxlKCM81u2 aedd9b232115b6fa5f7e894a1921bb45ac5b447b 0 iQIcBAABAgAGBQJUy6+GAAoJELnJ3IJKpb3VF2EP/0M467IBUGRWbJaQT/SghmYrsxypIUJ4Wq3KPgzb50RV/fd76Bb2fiM9rL0vWLe0fGK9iGkMKrCx4a+XVGkw5HV1mqyFEBo0ubwNJXPObohyhADaOChtEYi0o0vInuTA7cHgIYpLlmTEPArNlX/tDJanznehgu+VjaPfv5nKwj7mtm0kOItQta5lD7LUU8HH3JXkeUrDVovQyC+F8woKU8oF4fed8l3fXv6vjFckY2gcsdXwx1C8tHjHqE51k0+rxFpXtQAoeI6LwaqReqdUP3/GK9tTl42ywcl/KrA+UmdSehTNReG6SL1ZWsnikU7wWdNn8/ZKRkJZJ8KaI1bNxK0KiWe+BcVie8BI/18xTKtJ0p5sOX1gAcveDC0sy7ajOqJmlieu1HCXQtcs03WKoyOqYE7X1EcNf2ZKbyeP/jFgg6hgTHQt96JfCT0c+WLdpKz4H0gBfiPOnaXjHgJ/hi9Deu0cehsR1AnEI+ou6r/+wGA3mipQX7ooh4L80ONJMJNe0JIvnGkFWxbWRzXY70IGVF1NtwmJQ1HLB85hSj39RrKuRcza5aajMLLvQWEPd5wfPiM9ZnJoEeUvBtdtzVPowXMqQI0vz5vMDTbrku772kGlu+iHtESc+qcyDyxC8Vd5VorqX1brZLbvwWmlPymqI5aqe6mj4pJr3p+zH8Rp 1bd9d9a0201c15c7233ece182d98087e4915805a 0 iQIVAwUAWPbquUemf/qjRqrOAQhP0RAAphf232y3ozyXhZAbFyF+EaJHpvN0UqHF7otPp/5eoobO0liD3ffLCcCY018bs/LixWY5fkdbXsd7SZI8uQKfzitdosnbAUJoL2+Sw0H+E8MUQp1bCbh9Gn1oFHgtDkYzKaFYSWwM0KyLaFQx+UC8ffyVv4GhTRSfg/rYvgBOgorRjQbkns9bvQRxv3q9Zme6EQnM7yoKLh+shqRf7XjONZ+Yp/P7beQTot4oBGUOP9PZw+t2fJwE+yP9NcamM7mRAtYDH6+yINed/R7JMdpNyWhCpa38R6/t5mOJbDR/1qEGGUZn9Kb4CE5CvmyVuUJC9w0Y+7PtrPC5q9s7k3tUZ3RD2/GDi7yv7cS688Uw3H3ZuRpk5s+HkPIyiYm3blcipR9uFItpXX6ANm9/2tnzcxrP4mzB3UCygL3cwIibG3xyRiN4iUZa+QbUJmy5rRL/yrYU7tDKfUL+dvq5Qvx774XMDm5VZZTUwA3tEXACIXkUH8MUUjDllqK9f7FJ/CRSYyUnANFle7X5XfuszQ9l0CIXM5AVUosiEix0XnJr+RodBb0Kq9AUCzXTEX5cCxI2AhnWl7+Kg+Cc9/V8dF56EvLjH4H583e7tmI3S5oc4fA2d1C90/Gq/UnIJnV2Z+EkN2j7OabZ2xPnMezpyo1NKoRS45YeV1HaxApcamtkXB4= 620a1095f3a92c81d4087ab264501e37743c6e84 0 iQIVAwUAWXZUFUemf/qjRqrOAQhz5g/+N1gGY+7Awd5ko74kY1si+asBX6viR2lz/UTCXdAbyKI9DygFhGhMQz1cd2XVj0VJ4UZJnqimwMic3sIXjaQbMeDhP5PuptygGipvx4S6kRzvfJOArEj6y5337zJZ4ARF0imIN5zQdERJtfjECZd4l2JP95gj8tfxI88SH+57wCTO0MlEpXTjRL2ZJ+KlfQ54cAziCU3qraahLd2rkBhWYkFI4ZRr8GdzP3pDk6yIUUBcWUP8ePSQyiB16qkTjpjENMkDerRsJimclXzq7rplCOiKxNl9llyNdy22gPZbJOGjgF/463j3QrHUTKnakXJKztjF3Q7ICea9Las9L9SZCZ9Jqrykmgk1kCbwE4vOWsUx+ov/gyS49LCYg6f605e3SA8M3/7NXw0+TToPDLbZPlcc3vVEXNgPU5xGKcHPnoTtdGkcP1vexWP4e+AmhmYFU4i/qvagXbSXlaP67tpUnVk3LEqa0bWPVQzwuRNQpXCoj5zYYouweGPDvS4n/B5jtXz6P2WNyatkyK6gpGRk8nLu7W2Go+/nCdB60jfHxWpsVNcSM6uZouEvw4l7Vkg2peZsG5+96TJ+tGf6zG8KmvAkixbhACPjkFM2ZSKP3WH3ALrixGV2apCo7IXF+6vIacsBY+9CVkrRZdZ2dPAJrxAEdf2cFI971CjVv+iBr3I= 631d42fbf8a27cfaa25b04914f2671351b2ea6ac 0 iQIVAwUAWXZhUUemf/qjRqrOAQjPyA//Xd5TUkiEumFMe3bgddFdwyyu15RQV3bYiNP4/yhICfL8+Tq8oz8z8fwvp+i8T548m24y3LfZp+v5mZLv32zn/nKFL+lm3glVBApYI51tgjL0zFdl6CeCqkRqKKdVOXDp9rfeDGZfs64esUHWSnOn2tWVVxqN6dyAeSv2pwl95+HI4nO3f+jTKcMpriQRyrKHG1zdCj7xxfVPDzOscmK3XlHsloWgnKmCL/KoLjc/ngKz6ZDv2YS7TMqFGS8JDomfjyGG1aN2AYcs9D5SIELyWbwzBvEYGD66uUkKUeE0cf2cK5xkx5HFuTjqkxnhfWJeedFMvsudh/hIhlrQkT28D+SJAIQvsYL6xq8RC0E2Pp9WjWLzsI3XNnfzO7cW+lgZPp+f4qvecw+QcBex4ET1FDG+JtlR4ZceSz/SVvdrAZxhoHKMYU4sCyiA7IiqbfC8axH5ag18HxhK21qh083axBCPnZ4RWY+EDWdcXB/ifqQahA36KcIXrWLonq20t4Hv6osNiwnvpbz2F8iuWZj4RSerBm/XwgZ4v7UkonERVvv09C/UqaxYAbniNyYNxl5sDoFO2NWKvkkqLwVtQSBemMkK21kl4nT8pDvRuvB4wkJmiCHVjQKo5d1Fz7/cI58SlXaOMlrb5zXhyh8ZTcRgmgaG7SqsJbhgvUt9VRjRe9E= 88de3215e1389966053b6b10e93e5a3132625060 0 iQIVAwUAWYy2tkemf/qjRqrOAQjoVhAAh3aYB7e1I4lIFAd7sCemnHpjSRmHi3Lf2Ge0c/9nquNbWmkYjNtFcJsnZgd90YYvKladHJ+qCGg/a++OPVxqt/39SZduML1U7eGPpkQ7M9WAHWxDWcU9EZJZkOUKxF2X81T3thcL77zMtxRyVgTPzc8jy5FYfx7ILg3ooecSQxSoVwRWhizp8GdBnWhCTIW/XegjSYQRE5QvfpS1z/Uy5qHvCuWkvyLYiLO3V5kCjzSMG4B027iC+41Ofz7/THureOHQ1Ask4QIi81lIcBa+VS5XrGufyMf5AoPD13txQjodI1ZePtOi+mUB4JagTvk/n/b2maRKs/NFB+mDUjRQY95a6KbipdTAbsxSdJEDBBbSKXiYIXuG/pQLR3nMxa9MoEHJZVqCfJoHUUIKZQHgVKpffIArxecWhxD+Cpw09I/N6EN1/5+NR2keTSZloYvY73iIXfENMIf1RAoJGAIkNRSKdCHOKg9oNG+ItZyRssMXVZ4Q13EDtIIV7r+uwP+2ek6nXTrsAr/m5g3P6oFLBHNV+FXbbTbpTUIVjfrNjxvgq4hZoSU5+x6h83jXWr+4PeSBNjK7FousCEyvxSogxRl3VpshQfAIxV1yq1Hyxq3YIHLi6YPTiGNPDstwZODszMzp3Grh410xkij6bmx8+45BA63MpL3NkdrD0utqTNw= 6ef27582bfa5952e2c98ae8d9331c76e899fc353 0 iQIzBAABCAAdFiEEOoFVFj0OIKUw/LeGR6Z/+qNGqs4FAln6c7wACgkQR6Z/+qNGqs69xA//UuZ1vG3nOJUfsyiUyQT3xhg8YddpeGAU3RUPdXCQ84bMwapOaVAWrcfY5nZhQbfpkPWMPxZINuw1/DxTKbXFcunGeWrpwP+WBRMLy/KEXNjcRL1g9uAZyaQdo6N1Ly0za8W7hcwk8Tz1LG2mTvIXGhIvux5ExtpUEq2BSzs7pIGHz+PhTko/6BpK94V/Muo0rMs7oKEHmTWnWkFO9Yuq56MJAPFIOzpw+4fPid1NHOJm5gaGBVVodwzGF7LWJzqHLSRjYAkY9vQW0Bf3zQ6TXu41HYHDYYrIavFbtqUsCHynNoP8+WOhAenDzSVuEPvwaPT8jcN5DLM9Wnr5TRjapOZX0c/hsgELKFi+Mss6j5/54/C8GOrFZDmE++vaLTLDkGcp9FJByFCtkABpVUiETQ3cdd5etwUDFtmjo/y1NJgi0arC0nmKWyetdwr3LXZdazy7GmPF3HIHBKsiotbptMalTfwt4h52jsvIIUfYGPyOBZ5m6uRypfi3SJlSldYsWpEYfRZEbHbMXMq+vCyC9pqlE+qZ788QVEYNtFd0E65Q8dumzkIwuiBXG6n9zjo4pmbu2wp2dfJ7q4Ae8uZCfU8z9lPU8oKqrdmWXexMJH44RnF4e6tHZghoPp/iB4zg++AG6Dbgy7pwyPzG1hEK/hLqKt6ESPpbV7fbXmCIr2E= b90c69681d2d97aca47299d653c738ed688b60d7 0 iQIzBAABCAAdFiEEOoFVFj0OIKUw/LeGR6Z/+qNGqs4FAlqR87MACgkQR6Z/+qNGqs7PbQ//diiBZZJNbAopNFfdpmw4HtyIULds/FEfdZle4zC+DSkrlFS/24lAqPBw4Fze7RP8AW/oyg2ezzaJlnx7qh7N1D3A6rlDlNzKVYBygv4pFLOsfGqedCSXR/UorjneyqZbz7kfClE3SlMQrLlvff7vWjrZ5+GX9uyw8O939i0nhR32N7QGWsnCRmNceEys0OOSF0SbZdbdg1MbXbGe9n42rd5GW+e1xcg6WETWUJRTqqzS3TWr4Sol9l7L/pqJTceKCKw9SFnqVnsKaNAf8CFrX6Ybs3j9ZBW4jstYWdPJajNRwqZNNq3CUwo3clKaT/Gnu58xOmH0APqeXWz7ztyO10wFlNmIiC1WmSexSRUg1+197a9cjvyhCtuOb9E5S5fwgCn2yHBOxJ1u5yeKSqvC0Iw3rRpzkYllE94JkIiaQ2yJ3GIdEEVdWKqhs3F01gK7I5FCruIF3G60JZMoQTB5rmb/5qD6JUXv7rIgRPBFzEFeS4+XHuIkNG5ELgqJ0KLwz0Ad4Zat5d+jT6nLuax7TLKXytSTPgh17F3zw7tsptfna31573/DVF3902UM3wYVb5fxaQXoj7CtIscp5p6EV5E58x4RUMSlD5hkC3sYa+tj2u3E3qBXhqrMPX0R3uQkK57+MUGwMJohjftGHCE37i6d7yK9mVXhOlqISk/65iY= c651bb6fcf332a156701c3d738920676e251facb 0 iQIzBAABCAAdFiEEPxrXdSfhOn1nvxt8WjzUwwQvzGEFAlvD/twACgkQWjzUwwQvzGH5ORAAnhKM4BgIEXQCja7emCbtMFQ6cqqYZ2jtxnuYgx0iJ518HHbY1BrEyu3AJErU4WLtyWqrhECXHWG1/eoeVA/zsD16abEV1poCNFyMzzGNl3KGnIIEm0E22S8PpBnWIukd7u3UhOlIbLGaChKTdGDPQ5598de4gNn5XOHZq/c3PKa/4Ei5/dcxemtyRRzENz00h6iqHso3VDC0mLq0k3ffBAMeZEZo7TjoWZaF2c3sXNTTtkG4Uv5x/AEG6pANSF3a1gsH/XF6sd/KpjIM8wdyZMqnJmj+omcEcEN7ckKPolwCCp7APg56bc4yIc8olHHgTYfRZqXItigjlhdAVRmyLEzwmbakmFkzSE5M4p2s2vJe7TeC8jpwjK6XEYNYq5DIb4Unl/oO7dVoSoNBhlY4S+gY8YjR9eVm1P5XULE7O+t8wmBOuW+swLtqg9SKVcxs9DfVVXVxbl2IgpwA9pEqiIwuJZlEHHKS3HZ37BcNkMiytncYJiF661i87BjyYqhx+eZulAK+eBW7ZNjT2jkFnQr5Uz6hSqbdNtTLojDhBrPx4ESbw37PC80aE/Lj4LwAw5rZpQCb9xFoySYYFWX+kZshatM9HH3r1bjtPzoeAOuA9o0ZRqZqhsMKCuRRdTknlDhRbV98khrwwr3/1NkLd7Bizk8Fk+v7tC22tq2She0= 15457fc676316f0720e37b8bce02e621f78699b1 0 iQIzBAABCAAdFiEEPxrXdSfhOn1nvxt8WjzUwwQvzGEFAl3Siu8ACgkQWjzUwwQvzGHiiQ//YtLmysTSbXxZbhTP0lC0SC3JK6iYS5eK8RSw3xzQqqM0t+g+OhNOAPHP3EDzEwCqQB4aonItq4hC2Ho2mG1WKlkEiBmuz1VZzSqHKQgGgpCY7hx9d5JLzsH5lxJ/tilIgkwAD8hluzYWiDzWKQxWPntzUkH9fUV8tapPMmRQjC1I6D7w7o8LmSBpWXhZcZlEquGJHQMeYBBQidIA5SfHKc/2gcMjJTWVfafjtVGBvfHbhdWNRzE9IvnTv9ogM7pneucyWBNUuHJ0LLOqKfDY8e4Y3Ku8INZjhdSa/ioHaMqhEQ1lcetJSN/yah3F72KDaqmglVbhoERgXVlYEyJWhBQcXVSnMFDUdzCIlBY0oaXGAF5gCTXrFOkVi+YxHlIn69ll2jhWOGPR5DMZtHR4/cO/8w1fgklD73DRa7t+vCVqbK1VpXiteh8ado8YakLMR+AVmzBwlkUKcSghWYzooWqJhIWVgD404Mv20zYU2UH3KM+iN25yz+IrhCwMYRT+T1EhCOcsbvWchRxV4S/wgfPxPAEATIj38BF7yc0akxY5okUp5PrwVJotKlqdSB4mNkiSinIQTGQfIFCP/I4waafgvCk++madYzljxpQcnZ0BZHl6VntR27YGvhWPpY28lM5NnSLWWy0qZ74njUXeq708Mde9/Ce8mMs7lGtjkHo= 5771f0cbe2ced455b1d3b17be481bcabce47c97d 0 iQJQBAABCAA6FiEEv1RW9NxiVEOEm25Y7iDKRO9pHTkFAl68HO8cHGdlb3JnZXMucmFjaW5ldEBvY3RvYnVzLm5ldAAKCRDuIMpE72kdOZuyD/0Uz+odiqYCLhl9ibUi4vM2n8yLIEFzExSYsgbGxmfrx7nTwJVzlim5nKn4tFNzdknXDZ++5/2XXE2Uff4/Mgt0D4D1hhDEuN/XlAV8C6c21XN3mIvcHTOJDLT8+vaqcy0JtQ7gq1FRTywclhAvuyzpeylgMi/dyhwmER6fscsoI81bPfxRbG6mPmaq8CCi7qfUT0o5+9R/0fCyA98pxuAo1iOHv5Hndtm4BChZgUhLMv3vV/IqXMqh+dq3l7U78GFAvdoeelnZDC/iiXrGR9JLdZxgR2tnYQLUzqLz9ySRgqtxZOVmzzC1Ygwww60iYNOBFPnWRiyCa4hl1TGmttDhE3T54pV7Wju21gvDoApht9Q2ykpRVo/AfaVuqtem6dzUq5Ba7CBTGj76oecI42r/KIeT2VP6ZEetibb3M6SmlFM3TmnTh/456HiFPee71xBIys6PUPteabZpzIeRRWlLl30hDRD2c15EC0pHcOLCyb1amU1PgGg2yaN7pVkwjyDcRfyxKUcnVvmOtx30xAYKrEA8mYQe/MdqdTfWznmpgp60INwcbd3JpoMPmih474AbcB74U3XRG/WMibDN0vgzti7gm3M25AV8grAVveBMSFNp0U3lB2cqF3Q6L6sk9rOVu93gYd2ZFFb2Uev4pVNLLYJ9L69vw9JvSmzf2nhmIA== c17c6c915646bee1fd241e66e636d711be563b39 0 iQIzBAABCAAdFiEEv1RW9NxiVEOEm25Y7iDKRO9pHTkFAl8sHUkACgkQ7iDKRO9pHTlQKRAAsLPQG8EiYP5CxltbR7p2NQC6B5iEvGDdcnb7WFjqgWowxa9IRYnZxJnAU1wMWhqtLy7sensjfQWdDPtpHPTfdlRwqEzhy9KCGgR5PI1qBZnZhHFlUIs3TN1WTn3hH+wfxTKVeF1IkKt1vTuJ1xhefdITnY2gNZ/h9etBtb/Dk2DyN5RdLPOKQKf37Xct0t9pKsprs3t7c7Ivbc3GBnbPGdRXSuNTgwy3iYwK2QmAPEDOWuNd7WPW6k4Y8+oJlXtQWPqg65m9PYQQe5U9VVRObAGf2iMmMsGzGRWWwp9HaoEBiQyOxyDE+SZb0L4rA7phpEJ/Qvf8JSP+L0kM/gR3L48sPWK+3ugLlpPIeO/Jne24/8dOmsxrSQv9sQRvBRgYXjOmlEswRFcU3z8VWpHhtT8Oto7fmk6VXG6HCBGKzwmRhyDkSl8LwptlZDDcs9wh5/PE3kwa6HX4K8slpuT+hOj09qltJOeoZAPxMesjaaTBNJpc26BgUozmDpRFbQdbZpUCxZgBnvKy9ULKPqICbLiGjbsoaWVbpbTOZGUFQtOA2wbpc8DRS2U9hfbd1z3s8n9vywm+cTeDBPu/QfG5wANywdv/xGkhDCHqZEkzYrHbl99HIjCzodhoTgAJAAqSwwpIcBrDtuZZ2uaWTVaTNiRJ6D3/7FhkabSlh9IgUG0= d3af25aa2864e0e6ccda66963687772157a8f849 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYBg28QAKCRCirMgbQc9ONFGAAQCK7utuzuXRqmS+RxTlVr8MTJtOUUdiIGMFH3c4ONe10gEAu5Z/6HtzVk64TStoKG5EakrZsZU8uZwUDwsi4ku9NgY= d3af25aa2864e0e6ccda66963687772157a8f849 0 iQIzBAABCgAdFiEEv1RW9NxiVEOEm25Y7iDKRO9pHTkFAmAYOwEACgkQ7iDKRO9pHTk1cg/+N8/YzGXkVEgnmDqnlMjudfoN8yWbeDnk0VoW/ed+dlu6c4ZG42l5ZR1gyndrZv5VCn9xbIyNRUol3Mi1jsJCllAGHt0lXetNDN4c6gYrGOMoQIkgZ/0t+LTepct1qgpiFNcNM0YF+EcO1BzrG99+QvvlL4CTSEyqYvKrvgskqArR4MileteNzrpUaOLwXXAt5xi1v+j3SEEFHmtozePQMQWLzJIfRzK+FjsXJEPKtAE1AWXo+fZ7mS78E4hidPLHZZF4tjru4DgTiOF/lIuP5WfBF9lyNgzVRdmWS4Ei7IW14xxdUzH+51bBuMe0dV5YNbeFo82hBx46YlxLc/yKB39l8wHASwamI+PZj8t29Uz0gufCGpfN7TdQZWPFtNt3raW6cjlcIyb9PyWC3gq7oV+MkhagQ68nqUIo+ylAv09oNC/62vb/eJENwgCYPL45SX7f/ryvQycfAVJYn6JzfLGd9cLqql1KV9t6YkAo+VtVCdi/MYc0A+fkgDco0x9BCsgPlOeLBMcLLQhY9iEEvMlakFwOMGFEgas7uT9MF2gHkEqqd643p7veaszVBawZ+hK36ND68gpLKp1ZEaugS8mXGZ3/5X+O1MhjERA/FxZD8Pt3WxlAODbyn1fBqlQtb/44K+mx0zXybg2FzyEU8PHpl9UuvxjZmkQc8euCyBI= 5de9567da3791c310e6dc5e20231dafe309c51ad 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYJz02AAKCRCirMgbQc9ONNmSAP9Q6mPWABi7nvsFKG2ngkmBvb5bA3L4WlyM7WPtARiomwEA6OVdaVy/HJoXmjfKqM/KY5eDCxN65Fvu6WzFTZUELA8= 96807318ffa61a38f61eb33063f32998383a7f08 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYQUtRgAKCRCirMgbQc9ONPoxAP4m/6IOnZEnjPC2apz09YL/USmvffA6L+CO8rJPtqbLHQEAkVWMR2R3eML5jMxZ4vCUuUjRMLe5Tdb/2khNXqYi3AM= bc61b3723e0facb3b631f620a5546682e731f9e4 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYZPGGgAKCRCirMgbQc9ONCZ4AP9cikvLXO+5OEFcEYvmQlQstn8epP5ylsyvKdU2U/UCdwEAuCD0U/prlqnptmoPZ/EOtGO/5dQpdi0qCQSVLDZldAQ= 32ac3b4f13a9f8974aaeb9a7cf666a26ae699366 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYZPVbgAKCRCirMgbQc9ONFrEAPwI2GV6dq3EcCRJohaXJmNc04fEGRShgHqlZuAXEFy19wD8CQmKYNPpKgEbAxHzEw/8fB2jtezcaOY+V/UAto0ybwg= f39e71f82bc68672ffc14ab4ed7e61a3f31c9056 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYfGFkgAKCRCirMgbQc9ONLAJAQCF54/VA36YTRXD2z3cyjv0g0m6x0Q0UAtafMVS47kEDQD+NMH/DTHc0dbh0rK7ivS8jIB7josJiyiNfnVOkPZF2gk= bc5339fcea1e1b149602ffc2a06a3626dc44d2f4 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYfGHqAAKCRCirMgbQc9ONLyzAP9ckjA5a2QaVKObjRxR5fjE27GRHfb5kM8OW62L/KrMdwD/U51ZwVTplaKk6FAGQGrnhHtpST1VjV63Pf4XIT3HmgA= 311e9a57959e5a725c1a0b73f95f2c80161760ef 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYfGVyQAKCRCirMgbQc9ONIkKAQCt6+ejAUTeVNYQxT+pffrbzU6t+geViCV+s7PB7dMxbgEA9RYaaequsjWDUYrkyaQitjB2pKeqwq6h9ljrZ95sfQQ= d31a72cf70bd402aa5d3d4270871fff83ac47cd0 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYioVGgAKCRCirMgbQc9ONNXKAP9vxsKN7H1cK45yewQ7hkbod1PQfrhfXKp+XJoonOaeuAEApfmJsYzh9nQQ6xfVX2RhFyjqEdKoCLEJPGcoE/FLRwA= 6f22e3887d8220863620664077c8554da9d487c4 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCYkiYYQAKCRCirMgbQc9ONJpxAQCJk/7x/461bgYzDVC/4ugU1KnlgKjOprn9GE4m7MuuBQD9EPkQsuP8hjJ3HvNX+xQhnBGlwlQf1EYQBSuSaTOw+Qg= 3ca3ee14a659a96c3f593567fc0dc3d735e06c28 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCY2VrkgAKCRCirMgbQc9ONLILAQD1uMUeOHc9eD2kXjy2fMeAisWljk8Cpn8OmbsY385/hAD/YdVePqhVK0zAqNBH56PNvv603TR+MDQu4cHS9ULo9wQ= 6582b75a71a413e95e7345ab0e6aa10a5c150711 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZAIM/wAKCRCirMgbQc9ONHDhAQD6M6tlj5FDYY3iGtn3970WuU0MKDjtnYEQIef0yo+pawEA7s1AHYXtqdEcjJnKje5L+8IkYdsDF15/SiPuYMFGjgc= e5d68ea1c1fc0cfcafa9d8900fb00df4a0d5ea54 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZUpengAKCRCirMgbQc9ONCWhAQD0dxrYd05nq5QpxpgvX1HdquOI1241/LYYWCMmj944DQD/bkfuorwLYl8rHKz+oLVFeXPuRtlVQ0zMXrhCkpNoFwE= 28ac6274650a5351f017e9b2f06e79d4574e2cfe 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZUugiAAKCRCirMgbQc9ONLbbAQCJV6JlxmgVoV4IDSP+14TTWgyOlKwcQYsxsOYZkkqpXAD+NNjdftuXvF5rEBQ2DhmV7vrLqzx8j2wZY5kY4QXIHAw= 61aa75ce241d0c7c17cc7351bb126e35931701c0 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZaJa4QAKCRCirMgbQc9ONMwcAP400aR+BZFreYZU5V10ScAuSmDtt2o7QgIgcljMlzxMqQEA/XsxunS8QDxqL7OyUn9gzlD2yEa2td8lkCh4p9MJfgk= 3e149df435a632209087fd8927264687e9b3b558 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZaJezAAKCRCirMgbQc9ONI93AQDc7RS+554XkC9hNYk6ekOf1shsKz9j+knQHGxbnmkgpQEAnbfd5b1jGMuD0V9UJv+G4CSfhLY/hnEd2EK53F6+xA4= 2c5a0a45a6914afc9062d0332383f85c14772b18 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZehCvwAKCRCirMgbQc9ONLcAAQDE+PObvGgfPyIUbHTIY1LdHwPXniHwRNuub8zcJLM0yQD/aRoQgCJQh8naGe+eyBUNVuiCtIOqEO6lBsWyXbS67go= 1683b66b49f009d157619cd25520e7726b7b288a 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZehEtAAKCRCirMgbQc9ONPLqAP9PnxS72A9iDj1k51CmpawMXIIA4S2FQ6BlFE5m/2eA+wEA0VSo0NfmZaLwKyV+Q7Bjfa4kapre8DXIH+aiS1vnIQ4= ada78f968d8d7cf86d03b064625a4081a834c807 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZmIiAwAKCRCirMgbQc9ONOdcAQDYRVvoM3XFB5K3I+6Y7ecCD7KhHzK85938uhHjnZolEAD/fO2ayfqtF/sjZyagEEaXSBa8cwJO7R3gfMg2VjfIIA4= b3a1e7993aed765bc092ba53773607be795d1835 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZnrnngAKCRCirMgbQc9ONJsyAQDBe4iJLjvoxE1FWXS5R0k6YSKXqaIowVD4Tk3xz8nwkQD/cUJcmmXbwhQwNi8WyX1jmkjR4ipUv8r6oPiBuldhSAc= 63bc68fcfe73806f74e00406d244c584d79e8390 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZztfGAAKCRCirMgbQc9ONKNaAP0bE1MxCJpNihYbqdV1erkhSfwgJGVWE7J4Ise8R1HnrgD/VaZOwmIVsjLSX2MMeM0XcgD1MVNYiv9+KNrVC79lSQo= fabf47fd49c60a0f8b3983f929f3a72e2a44d6b4 0 iHUEABYIAB0WIQRBf9ypgVcBvOhGUkSirMgbQc9ONAUCZzt3wwAKCRCirMgbQc9ONEIpAP0U6+DoBwHaj9cvQj8Y5YasIkRx4vEufEoNuTO1dorRiwEAlv0t2PezSvPUDZGDSkOyPb0lSEInUm5iZg0ftcX4ZQ4= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/.hgtags0000644000000000000000000000567014751647721011123 0ustar00505d7cdca19838bfb270193e0709154a2dad5c19 0.1.0 77d6c9eb02fb96abb6a933170a912e94e3141f22 0.2.0 21ead8190d9c5f2a5a7500bab1d89b5373eda040 0.2.1 a222399a59d7a960f9ac41b89d3d0041bdf3e78c 0.2.2 5d39b98e5083a0897328f97c4c9795e64d45aac8 0.2.3 b53421918a89a20f35dc6b6c3974234fe45a5512 0.2.4 bc72dd89c2c911b3b844bd6a1e3841ca16cec59c 0.2.5 46d390f404da3add53c1f8de91216388f791cd82 0.2.6 fa3edeec7ed16dec6a16bd3e99bc3feba93115c3 0.3.0 556c3c586c4aa52f587ccc4d2d65b370a7e9037f 0.3.1 a9c0b93488d4d082f813c6d91c8e473505a026c4 0.3.2 9d44dafbb31c14126be151b78c7a41b3c110fd97 0.3.3 586b7aa9646641b3b1083ab349bb186c79aa646b 0.3.4 a3c3b8077cbeec7c381a4b312d722d575a738610 0.4.0 ef41e87ea11afdabfa6583e2c86dec375c67b2a4 0.5.0 9f20b66027c259e498a96454cf4c380e9440d769 0.6.0 f67747f18f20e97eff459aa24a70a23a2b3730a9 0.6.1 fc63d0e2653d3601dcc6e1adb99c13813cefae98 0.7.0 cf3dafce0611cf940945fdedb4cf5cb1091bc30c 0.8.0 e6489cf3fe8c204ad62846ba7755445755814330 0.8.1 e183fdc198f01affa094d15e6587a74de4eb063a 0.8.2 93d70993447c49d180a9c5d0ef130620fc956e33 0.8.3 d7ad67f850b279c1c3caa8349d4a47cf626e6f9a 0.8.3 22a12bf143a386f0a0be2bceb9ce1e42b1b27dd7 0.8.4 53d514c9c7e6ac99e2c4d6df6c520a7db1e63d7f 0.8.5 1bd9d9a0201c15c7233ece182d98087e4915805a 0.8.6 620a1095f3a92c81d4087ab264501e37743c6e84 0.8.7 631d42fbf8a27cfaa25b04914f2671351b2ea6ac 0.8.8 88de3215e1389966053b6b10e93e5a3132625060 0.8.9 6ef27582bfa5952e2c98ae8d9331c76e899fc353 0.8.10 b90c69681d2d97aca47299d653c738ed688b60d7 0.8.11 c651bb6fcf332a156701c3d738920676e251facb 0.8.12 15457fc676316f0720e37b8bce02e621f78699b1 0.8.13 5771f0cbe2ced455b1d3b17be481bcabce47c97d 0.9.0a1 c17c6c915646bee1fd241e66e636d711be563b39 0.9.0 d3af25aa2864e0e6ccda66963687772157a8f849 0.10.0 5de9567da3791c310e6dc5e20231dafe309c51ad 0.10.1 96807318ffa61a38f61eb33063f32998383a7f08 0.10.2 bc61b3723e0facb3b631f620a5546682e731f9e4 0.10.3 bc61b3723e0facb3b631f620a5546682e731f9e4 0.10.3 32ac3b4f13a9f8974aaeb9a7cf666a26ae699366 0.10.3 f39e71f82bc68672ffc14ab4ed7e61a3f31c9056 0.10.4 f39e71f82bc68672ffc14ab4ed7e61a3f31c9056 0.10.4 bc5339fcea1e1b149602ffc2a06a3626dc44d2f4 0.10.4 311e9a57959e5a725c1a0b73f95f2c80161760ef 1.0.0b1 d31a72cf70bd402aa5d3d4270871fff83ac47cd0 1.0.0b2 ff6274c7c614f0a6b1e6568c16c9f882ea7c4bc7 1.0.0b2.post1 ff6274c7c614f0a6b1e6568c16c9f882ea7c4bc7 1.0.0b2.post1 a6775b524ee1259619f28d6bee69c0962a543263 1.0.0 6f22e3887d8220863620664077c8554da9d487c4 1.0.0 29d1515aac3bbdd9cfc5e4ea4797bb5b45d3afe1 1.0.1 3ca3ee14a659a96c3f593567fc0dc3d735e06c28 1.0.1 6582b75a71a413e95e7345ab0e6aa10a5c150711 1.0.2 e5d68ea1c1fc0cfcafa9d8900fb00df4a0d5ea54 1.0.3 28ac6274650a5351f017e9b2f06e79d4574e2cfe 1.1.0b1 61aa75ce241d0c7c17cc7351bb126e35931701c0 1.0.4 3e149df435a632209087fd8927264687e9b3b558 1.1.0 2c5a0a45a6914afc9062d0332383f85c14772b18 1.0.5 1683b66b49f009d157619cd25520e7726b7b288a 1.1.1 ada78f968d8d7cf86d03b064625a4081a834c807 1.1.2 b3a1e7993aed765bc092ba53773607be795d1835 1.1.3 63bc68fcfe73806f74e00406d244c584d79e8390 1.1.4 fabf47fd49c60a0f8b3983f929f3a72e2a44d6b4 1.2.0b1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/CONTRIBUTING.rst0000644000000000000000000000612014751647721012275 0ustar00====================== Contributing to hg-git ====================== The short version: * Patches should have a good summary line for first line of commit message * Patch series should be sent primarily as `merge requests`_ or to the `Google Group`_ at . * Patch needs to do exactly one thing. * The test suite passes, as enforced by the continuous integration on Heptapod. .. _merge requests: https://foss.heptapod.net/mercurial/hg-git .. _Google Group: https://groups.google.com/forum/#!forum/hg-git Long version ------------ We use a variant of Mercurial's `own contribution guidelines`_. Key differences are (by rule number): .. _own contribution guidelines: https://www.mercurial-scm.org/wiki/ContributingChanges 2 You can cross-reference Heptapod issues in the ``#NNN`` format. To submit a Merge Request, please ask for Developer rights using the `Request access`_ link. We usually respond to that relatively quickly, but you can expedite the request by sending a mail to the list. Please note that we generally do a quick cursory check of people requesting access to ensure that there's a human at the other end. Then, following the instructions of the `Heptapod tutorial`_ should be enough. .. _Request access: https://foss.heptapod.net/mercurial/hg-git/-/project_members/request_access .. _Heptapod tutorial: https://heptapod.net/pages/quick-start-guide.html If you do a merge request, we're still going to expect you to provide a clean history, and to be willing to rework history so it's clean before we push the "merge" button. If you're uncomfortable with history editing, we'll clean up the commits before merging. Compatibility policy -------------------- We generally follow `semantic versioning`_ guidelines: Major releases, that is ``x.0.0``, are for significant changes to user experience, especially new features or changes likely to affect workflow. Minor releases, ``x.y.0``, are for less significant changes or adjustments to user experience, including bug fixes, but they needn't retain _exact_ compatibility. Patch releases, ``x.y.z``, are exclusively for breaking bugs and compatibility. .. _semantic versioning: https://semver.org We can drop compatibility for unsupported versions of Python, Mercurial or Dulwich in anything but patch releases. There is no fixed policy for which versions of Mercurial and Dulwich we support, but as loose guideline, versions prior to the version that ships in the latest Ubuntu LTS release may be dropped if they are older than a year and interfere with new development. Unfortunately, due to dropping support for Python 2.7, hg-git does not work on any shipped Ubuntu LTS, as-is, since 20.04 used 5.3.1 on Python 2.7, but only shipped Dulwich for Python 3.6. Once 22.04 is out, that will be our bare minimum. We commit to supporting any version of Python at least as long as it receives security updates from ``python.org``, unless it conflicts with the requirements of a supported Ubuntu LTS. The authoritative source for dependency requirements is ``.gitlab-ci.yml``, although ``setup.cfg`` and ``hggit/__init__.py`` also list them. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/COPYING0000644000000000000000000004325414751647721010700 0ustar00 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/MANIFEST.in0000644000000000000000000000042114751647721011370 0ustar00include COPYING include CONTRIBUTING.rst NEWS.rst README.rst include MANIFEST.in include Makefile recursive-include hggit *.py *.rst recursive-include tests *.t *.py *.sh *.py.out recursive-include tests hghave latin-1-encoding testutil recursive-include tests/gpg *.gpg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/Makefile0000644000000000000000000000114214751647721011273 0ustar00PYTHON=python HG=$(shell which hg) HGPYTHON=$(shell $(HG) debuginstall -T '{pythonexe}') TESTFLAGS ?= $(shell echo $$HGTESTFLAGS) help: @echo 'Commonly used make targets:' @echo ' tests - run all tests in the automatic test suite' @echo ' all-version-tests - run all tests against many hg versions' @echo ' tests-%s - run all tests in the specified hg version' all: help tests: cd tests && $(HGPYTHON) run-tests.py --with-hg=$(HG) $(TESTFLAGS) test-%: @+$(MAKE) tests TESTFLAGS="$(strip $(TESTFLAGS) $@)" release: $(PYTHON) setup.py sdist .PHONY: help all tests release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/NEWS.rst0000644000000000000000000003711714751647721011154 0ustar00hg-git 1.2.0 (2025-02-08) ========================= This is a feature release that contains changes all changes from 1.2.0b1, as well as the following change: * Drop support for Python 3.8. This release requires Mercurial 6.6, or later, Dulwich 0.21.6 or later and Python 3.9 or later. hg-git 1.2.0b1 (2024-11-18) =========================== This is a preview of an upcoming feature release that contains changes to user-facing behaviour. Changes to behaviour: * Change the defaults for ``hggit.usephases``; similar to Mercurial, remote repositories now default to publishing. Enhancements: * Add support for a ``git.blame.ignoreRevsFile`` configuration setting, that works similarly to the setting for ``git blame``. * Add limited and experimental support for including hg-git metadata in Mercurial bundles and when pulling or pushing from remote Mercurial repositories, see below. (#156) * ``hg git-cleanup`` now also removes broken Git refs. * Always pull any annotated tags pointing to any known commits, equivalent to passing the ``--tags`` option in Git. Previously, such tags would only be pulled when either not using ``--rev`` or similar, or when listing the tag explicitly. * Transparently compress the objects when pushing (or exporting) to Git. This is done in background threads, and by default uses up to either four of them or the system CPU count, whichever is lower. The ``hggit.threads`` configuration option allows adjusting the default. This release requires Mercurial 6.6, or later, Dulwich 0.21.6 or later and Python 3.8 or later. Transferring ``hg-git`` metadata -------------------------------- As noted, this is experimental, and can be enabled using the following configuration settings:: [experimental] hg-git-bundle = yes hg-git-serve = yes With these set, pulling or pushing any commit already pushed (or converted) to Git will also update the mapping remotely, avoiding the need for converting these changes once more. In addition, Git tags are also transferred. However, this support is limited, and there is no synchronisation of later changes. There is no support for reinstating or synchronising lost or old state, and no support for transferring tags to old changesets. The first option enables embedding the metadata using ``hg bundle`` and the second option enables the support with served repositories. hg-git 1.1.4 (2024-11-18) ========================= This is a minor release, focusing on bugs and compatibility. * Mark Dulwich 0.22.0 as fully supported; the differences are assumed intentional for now. * Mark Mercurial 6.9 as tested and supported. * Fix tests with Python 3.13. hg-git 1.1.3 (2024-06-25) ========================= This is a minor release, focusing on bugs and compatibility. * Mark Dulwich 0.22.0 and 0.22.1 as unsupported. The compatibility hack didn't work in practice. * Mark Mercurial 6.8 as tested and supported. hg-git 1.1.2 (2024-06-06) ========================= This is a minor release, focusing on bugs and compatibility. * Always advance ``draft`` phase, even if pulling from an explicit URL that isn't a named path. * Always save Git tags into the local, cached Git repository. * Add support for Dulwich 0.22. hg-git 1.1.1 (2024-03-06) ========================= This is a minor release, focusing on bugs and compatibility. It includes all changes from 1.0.5 as well as the following: * Fix pulling after marking the ``tip`` as obsolete. * Mark Mercurial 6.7 as supported. hg-git 1.0.5 (2024-03-06) ========================= This is a minor release, focusing on bugs and compatibility. * Fix ``--publish`` when topics extension is enabled. Thanks to @av6 for contributing changes to the release! hg-git 1.1.0 (2024-01-13) ========================= This is a feature release that contains changes all changes from 1.1.0b1 and 1.0.4, as well as the following minor change: * Remove some compatibility for now-unsupported versions of Dulwich. This release requires Mercurial 6.1, or later, Dulwich 0.20.11 or later and Python 3.8 or later. hg-git 1.0.4 (2024-01-13) ========================= This is a minor release, focusing on bugs and compatibility. * Address regression with Mercurial 6.4 and later where remote tags weren't updated on push. hg-git 1.1.0b1 (2023-11-08) =========================== This is a preview of an upcoming feature release that contains changes to user-facing behaviour. Changes to behaviour: * The ``gclear`` command is inherently dangerous, and has been replaced with a debug command instead. * The ``.hgsub`` and ``.gitmodules`` files are no longer retained when pushing to or pulling from Git, respectively. Instead, changes to each will be applied during the conversion. Enhancements: * Minor adjustments to categorisation of internal commands, and ensure that they all start with ``git-*``. * Move configuration from the ``README`` file to contained within the extension, so that it is now self-documenting like Mercurial. * The ``-B/--bookmark`` flag for ``push`` will now restrict bookmarks by name rather than revision. (Please note that this is unsupported when the ``git.branch_bookmark_suffix`` configuration option is set.) * Pushing an unknown bookmark with the ``-B/--bookmark`` option now has the same effect as when pushing to a Mercurial repository, and will delete the remote Git branch. * You can now specify what to publish with the ``paths`` section. For example:: [paths] default = https://github.com/example/test default:pushurl = git+ssh://git@github.com/example default:hg-git.publish = yes * Pushing and pulling from Git now triggers ``incoming``, ``outgoing`` and ``changegroup`` hooks, along with the corresponding ``pre*`` hooks. In addition, the ``gitexport`` and ``gitimport`` hooks allow intercepting when commits are converted. As a result, you can now use the ``notify`` extension when interacting with Git repositories. (#402) * Git subrepositories will now be pushed as Git submodules. This release requires Mercurial 6.1, or later, Dulwich 0.20.11 or later and Python 3.8 or later. hg-git 1.0.3 (2023-11-07) ========================= This is a minor release, focusing on bugs and compatibility. * Fix tests with Mercurial 6.5 * Handle failures to save refs, such as when they use characters forbidden by the file system; this is most easily noticed on Windows and macOS. (#397) * Fix pulling annotated tags with ``-r``/``--rev``. hg-git 1.0.2 (2023-03-03) ========================= This is a minor release, focusing on bugs and compatibility. * Fix ``--source``/``-s`` argument to ``transplant`` with Hg-Git enabled. (#392) * Fix cloning repositories using the old static HTTP support with Hg-Git enabled. * Handle pushing tags to Git that cannot be stored as references such as double-quotes on Windows. (#397) * Avoid converting unrelated refs on pull, such as Github PR-related refs. (#386) * Fix tests with GNU Grep 3.8 and later, by avoiding the ``egrep`` alias (#400) * Support reading remote refs even if packed. * Add support for Dulwich 0.21 and later. * Mark Mercurial 6.4 as supported and tested. * Address slowness when pulling large repositories, caused by writing unchanged references. (#401) Thanks to @icp1994 and @jmb for contributing changes to the release! hg-git 1.0.1 (2022-11-04) ========================= This is a minor release, focusing on bugs and compatibility. * Ignore any ``GIT_SSH_COMMAND`` environment variable, rather than dying with an error. (#369) * Fix bug with unusual progress lines from Azure Repo (#391) * Fix incorrect use of localisation APIs (#387) * Fix pushing with Dulwich 0.2.49 or later. * Fix tests with Git 2.37. * Fix bug with tags or remote refs in the local Git repository that point to missing commits. * Mark Mercurial 6.2 and 6.3 as supported and tested. Thanks to Pierre Augier and Aay Jay Chan for contributing to this release! hg-git 1.0.0 (2022-04-01) ========================= This is the first stable release in the 1.0 series. In addition to all the features and fixes in the betas, it includes: * Handle errors in ``.gitmodules`` gracefully, allowing the conversion to continue. (#329) * Don't die with an error when ``.hgsub`` contains comments. (#128) * Suppress errors on export related to history editing of certain commits with unusual authorship and messages. (#383) * Fix tests with Git 2.35. Other changes: * Increase test coverage by using different versions of Alpine Linux and Dulwich. This release requires Mercurial 5.2 or later and Python 3.6 or later. hg-git 1.0b2 (2022-03-10) ========================= This is a follow-up to the previous beta, that fixes the following bugs: * Fix tests with Mercurial 6.1. * Avoid prompting for authentication after a successful push, by storing the authenticated client. (#379) This release requires Mercurial 5.2 or later and Python 3.6 or later. hg-git 1.0b1 (2022-01-26) ========================= This is a preview of an upcoming major release that contains changes to user-facing behaviour, as well as a fair amount of internal changes. The primary focus is on adjusting the user experience to be more intuitive and consistent with Git and Mercurial. The internal changes are mainly refactoring to make the code more consistent and maintainable. Performance should also be much better; a simple clone of a medium-sized repository is about 40% faster. This release requires Mercurial 5.2 or later and Python 3.6 or later. Changes to behaviour: * When a pull detects that a Git remote branch vanishes, it will remove the corresponding local tags, such as ``default/branch``. This is equivalent to using ``git fetch --prune``, and adjustable using the ``git.pull-prune-remote-branches`` configuration option. * Similarly, delete the actual bookmarks corresponding to a remote branch, unless the bookmarks was moved since the last pull from Git. This is enabled by default and adjustable using the ``git.pull-prune-bookmarks`` configuration option. * Speed up ``pull`` by using a single transaction per map save interval. * Similarly, speed up ``hg clone`` by always using a single transaction and map save interval, as Mercurial will delete the repository on errors. * Change the default ``hggit.mapsavefrequency`` to 1,000 commits rather than just saving at the end. * Abort with a helpful error when a user attempts to push to Git from a Mercurial repository without any bookmarks nor tags. Previously, that would either invent a bookmark —— *once* — or just report that nothing was found. * Only update e.g. ``default/master`` when actually pulling from ``default``. Enhancements: * Add a ``gittag()`` revset. * Print a message describing which bookmarks changed during a pull. * Let Mercurial report on the incoming changes once each transaction is saved, similar to when pulling from a regular repository. * Remove some unnecessary caching in an attempt to decrease memory footprint. * Advance phases during the pull rather than at the end. * With ``hggit.usephases``, allow publishing tags and specific remotes on pull, as well as publishing the remote ``HEAD`` on push. * Change defaults to drop illegal paths rather than aborting the conversion; this is adjustable using the ``hggit.invalidpaths`` configuration option. * Allow updating bookmarks from obsolete commits to their successors. Bug fixes: * Adjust publishing of branches to correspond to the documentation. Previously, e.g. listing ``master`` would publish a local bookmark even if diverged from the remote. * Handle corrupt repositories gracefully in the ``gverify`` command, and allow checking repository integrity. * Only apply extension wrappers when the extension is actually enabled rather than just loaded. * Fix pulling with ``phases.new-commit`` set to ``secret``. (#266) * Detect divergence with a branch bookmark suffix. * Fix flawed handling of remote messages on pull and push, which caused most such messages to be discarded. * Report a helpful error when attempting to push or convert with commits missing in the Git repository. Also, issue a warning when creating a new Git repository with a non-empty map, as that may lead to the former. * Ensure that ``gimport`` also synchronises tags. * Address a bug where updating bookmarks might fail with certain obsolete commits. * Handle missing Git commits gracefully. (#376) Other changes: * Require ``setuptools`` for building, and use ``setuptools_scm`` for determining the version of the extension. * Refactoring and reformatting of the code base. hg-git 0.10.4 (2022-01-26) ========================== This is a minor release, focusing on bugs and compatibility. Bug fixes: * Fix compatibility with the ``mercurial_keyring`` extension. (#360) * Add missing test files to the source archive. (#375) * Fix tests with Git 2.34. hg-git 0.10.3 (2021-11-16) ========================== This is a minor release, focusing on bugs and compatibility. Enhancements: * Add support for Mercurial 6.0. hg-git 0.10.2 (2021-07-31) ========================== This is a minor release, focusing on bugs and compatibility. Enhancements: * Add support for Mercurial 5.9. Bug fixes: * Fix the ``git.authors`` configuration option, broken in Python 3. hg-git 0.10.1 (2021-05-12) ========================== This is a minor release, focusing on bugs and compatibility. Enhancements: * Add support for Mercurial 5.8. Bug fixes: * Fix some documentation issues. * Don't overwrite annotated tags on push. * Fix an issue where pushing a repository without any bookmarks would push secret changesets. hg-git 0.10.0 (2021-02-01) ========================== The 0.10.x series will be the last one supporting Python 2.7 and Python 3.5. Future feature releases will only support Python 3.6 and later and Mercurial 5.2 or later. Enhancements: * Add support for proper HTTP authentication, using either ``~/.git-credentials`` or just as with any other Mercurial remote repository. Previously, the only place to specify credentials was in the URL. * Add ``--git`` option to ``hg tag`` for creating lightweight Git tags. * Always show Git tags and remotes in ``hg log``, even if marked as obsolete. * Support ``{gitnode}`` keyword in templates for incoming changes. * Support HTTP authentication using either the Mercurial configuration, ``git-credentials`` or a user prompt. * Support accessing Git repositories using ``file://`` URIs. * Optimise writing the map between Mercurial and Git commits. * Add ``debuggitdir`` command that prints the path to the cached Git repository. Bug fixes: * Fix pulling changes that build on obsoleted changesets. * Fix using ``git-cleanup`` from a shared repository. * Fix scp-style “URIs†on Windows. * Fix ``hg status`` crashing when using ``.gitignore`` and a directory is not readable. * Fix support for ``.gitignore`` from shared repositories and when using a Mercurial built with Rust extensions. * Add ``brotli`` to list of modules ignored by Mercurial's ``demandimport``, so ``urllib3`` can detect its absence on Python 2.7. * Fix the ``git`` protocol on Python 3. * Address a deprecation in Dulwich 0.20.6 when pushing to Git. * Fix configuration path sub-options such as ``remote:pushurl``. * Fix pushing to Git when invalid references exist by disregarding them. * Always save the commit map after an import. * Add support for using Python 3 on Windows. * Mark ``gimport``, ``gexport`` and ``gclear`` as advanced as they are either complicated to understand or dangerous. * Handle backslashes in ``.gitignore`` correctly on Windows. * Fix path auditing on Windows, so that e.g. ``.hg`` and ``.git`` trigger the appropriate behaviour. Other changes: * More robust tests and CI infrastructure. * Drop support for Mercurial 4.3. * Updated documentation. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/README.rst0000644000000000000000000001361314751647721011330 0ustar00Hg-Git Mercurial Plugin ======================= Homepage: https://wiki.mercurial-scm.org/HgGit Repository: https://foss.heptapod.net/mercurial/hg-git Old homepage, no longer maintained: https://hg-git.github.io/ Discussion: `hg-git@googlegroups.com `_ (`Google Group `_) and `#hg-git:matrix.org `_ This is the Hg-Git plugin for Mercurial, adding the ability to push and pull to/from a Git server repository from Hg. This means you can collaborate on Git based projects from Hg, or use a Git server as a collaboration point for a team with developers using both Git and Hg. The Hg-Git plugin can convert commits/changesets losslessly from one system to another, so you can push via a Mercurial repository and another Hg client can pull it and their changeset node ids will be identical - Mercurial data does not get lost in translation. It is intended that Hg users may wish to use this to collaborate even if no Git users are involved in the project, and it may even provide some advantages if you're using Bookmarks (see below). Dependencies ============ This plugin is implemented entirely in Python — there are no Git binary dependencies, and you do not need to have Git installed on your system. The only dependencies are: * Mercurial 6.6 * Dulwich 0.21.6 * Python 3.8 Please note that these are the earliest versions known to work; later versions generally work better. Installing ========== We recommend installing the plugin using your a package manager, such as pip:: python -m pip install hg-git Alternatively, you can clone this repository somewhere and install it from the directory:: hg clone https://foss.heptapod.net/mercurial/hg-git/ cd hg-git python -m pip install . And enable it from somewhere in your ``$PYTHONPATH``:: [extensions] hggit = Contributing ============ The primary development location for Hg-Git is `Heptapod `_, and you can follow their guide on `how to contribute patches `_. Alternatively, you can follow the `guide on how to contribute to Mercurial itself `_, and send patches to `the list `_. Usage ===== You can clone a Git repository from Mercurial by running ``hg clone [dest]``. For example, if you were to run:: $ hg clone git://github.com/hg-git/hg-git.git Hg-Git would clone the repository and convert it to a Mercurial repository for you. Other protocols are also supported, see ``hg help git`` for details. If you are starting from an existing Mercurial repository, you have to set up a Git repository somewhere that you have push access to, add a path entry for it in your .hg/hgrc file, and then run ``hg push [name]`` from within your repository. For example:: $ cd hg-git # (a Mercurial repository) $ # edit .hg/hgrc and add the target git url in the paths section $ hg push This will convert all your Mercurial data into Git objects and push them to the Git server. Now that you have a Mercurial repository that can push/pull to/from a Git repository, you can fetch updates with ``hg pull``:: $ hg pull That will pull down any commits that have been pushed to the server in the meantime and give you a new head that you can merge in. Hg-Git pushes your bookmarks up to the Git server as branches and will pull Git branches down and set them up as bookmarks. Hg-Git can also be used to convert a Mercurial repository to Git. You can use a local repository or a remote repository accessed via SSH, HTTP or HTTPS. Use the following commands to convert the repository, it assumes you're running this in ``$HOME``:: $ mkdir git-repo; cd git-repo; git init; cd .. $ cd hg-repo $ hg bookmarks hg $ hg push ../git-repo The ``hg`` bookmark is necessary to prevent problems as otherwise hg-git pushes to the currently checked out branch, confusing Git. The snippet above will create a branch named ``hg`` in the Git repository. To get the changes in ``master`` use the following command (only necessary in the first run, later just use ``git merge`` or ``git rebase``). :: $ cd git-repo $ git checkout -b master hg To import new changesets into the Git repository just rerun the ``hg push`` command and then use ``git merge`` or ``git rebase`` in your Git repository. ``.gitignore`` and ``.hgignore`` -------------------------------- If present, ``.gitignore`` will be taken into account provided that there is no ``.hgignore``. In the latter case, the rules from ``.hgignore`` apply, regardless of what ``.gitignore`` prescribes. Please note that Mercurial doesn't support exclusion patterns, so any ``.gitignore`` pattern starting with ``!`` will trigger a warning. This has been so since version 0.5.0, released in 2013. Further reading =============== See ``hg help -e hggit`` and ``hg help hggit-config``. Alternatives ============ Since version 5.4, Mercurial includes an extension called ``git``. It interacts with a Git repository directly, avoiding the intermediate conversion. This has certain advantages: * Each commit only has one node ID, which is the Git hash. * Data is stored only once, so the on-disk footprint is much lower. The extension has certain drawbacks, however: * It cannot handle all Git repositories. In particular, it cannot handle `octopus merges`_, i.e. merge commits with more than two parents. If any such commit is included in the history, conversion will fail. * You cannot interact with Mercurial repositories. .. _octopus merges: https://git-scm.com/docs/git-merge Another extension packaged with Mercurial, the ``convert`` extension, also has Git support. Other alternatives exist for Git users wanting to access Mercurial repositories, such as `git-remote-hg`_. .. _git-remote-hg: https://pypi.org/project/git-remote-hg/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/check-commit0000755000000000000000000000630314751647721013570 0ustar00#!/usr/bin/env python3 # # Copyright 2014 Matt Mackall # # A tool/hook to run basic sanity checks on commits/patches for # submission to Mercurial. Install by adding the following to your # .hg/hgrc: # # [hooks] # pretxncommit = contrib/check-commit # # The hook can be temporarily bypassed with: # # $ BYPASS= hg commit # # See also: https://mercurial-scm.org/wiki/ContributingChanges from __future__ import absolute_import, print_function import os import re import sys commitheader = r"^(?:# [^\n]*\n)*" afterheader = commitheader + r"(?!#)" beforepatch = afterheader + r"(?!\n(?!@@))" errors = [ (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"), ( beforepatch + r".*[(]issue \d\d\d", "no space allowed between issue and number", ), (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"), (commitheader + r"# User [^@\n]+\n", "username is not an email address"), ( commitheader + r"(?!merge with )[^#]\S+[^:] ", "summary line doesn't start with 'topic: '", ), (afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"), (afterheader + r"^\S+: *[A-Z][a-z]\S+", "don't capitalize summary lines"), ( afterheader + r"\S*[^A-Za-z0-9-_]\S*: ", "summary keyword should be most user-relevant one-word command or topic", ), (afterheader + r".*\.\s*\n", "don't add trailing period on summary line"), (afterheader + r".{79,}", "summary line too long (limit is 78)"), ] word = re.compile(r'\S') def nonempty(first, second): if word.search(first): return first return second def checkcommit(commit, node=None): exitcode = 0 printed = node is None hits = [] signtag = ( afterheader + r'Added (tag [^ ]+|signature) for changeset [a-f0-9]{12}' ) if re.search(signtag, commit): return 0 for exp, msg in errors: for m in re.finditer(exp, commit): end = m.end() trailing = re.search(r'(\\n)+$', exp) if trailing: end -= len(trailing.group()) / 2 hits.append((end, exp, msg)) if hits: hits.sort() pos = 0 last = '' for n, l in enumerate(commit.splitlines(True)): pos += len(l) while len(hits): end, exp, msg = hits[0] if pos < end: break if not printed: printed = True print("node: %s" % node) print("%d: %s" % (n, msg)) print(" %s" % nonempty(l, last)[:-1]) if "BYPASS" not in os.environ: exitcode = 1 del hits[0] last = nonempty(l, last) return exitcode def readcommit(node): return os.popen("hg export %s" % node).read() if __name__ == "__main__": exitcode = 0 node = os.environ.get("HG_NODE") if node: commit = readcommit(node) exitcode = checkcommit(commit) elif sys.argv[1:]: for node in sys.argv[1:]: exitcode |= checkcommit(readcommit(node), node) else: commit = sys.stdin.read() exitcode = checkcommit(commit) sys.exit(exitcode) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/ci.sh0000755000000000000000000000036614751647721012234 0ustar00#!/bin/sh set -x git version hg debuginstall --config extensions.hggit=./hggit hg version -v --config extensions.hggit=./hggit exec python$PYTHON tests/run-tests.py \ --color=always \ --cover \ --xunit $PWD/tests-$CI_JOB_ID.xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/Dockerfile.alpine0000644000000000000000000000020114751647721015776 0ustar00ARG PYTHON=3 FROM python:$PYTHON-alpine ARG HG=stable ARG PYTHON=3 COPY contrib/docker/installhg.sh /tmp RUN /tmp/installhg.sh ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/Dockerfile.ubuntu0000644000000000000000000000043214751647721016056 0ustar00FROM ubuntu:20.10 RUN \ set -x && \ apt-get -qq update && \ DEBIAN_FRONTEND=noninteractive \ apt-get -qq install --no-install-recommends \ mercurial git pyflakes3 unzip ssh gpg gpg-agent \ python3-dulwich python3-setuptools && \ apt-get clean ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/Dockerfile0000644000000000000000000000266014751647721016631 0ustar00FROM alpine:3.12 # "--no-cache" is new in Alpine 3.3 and it avoid using # "--update + rm -rf /var/cache/apk/*" (to remove cache) RUN apk add --no-cache \ openssh git-daemon cgit nginx spawn-fcgi fcgiwrap # SSH autorun # RUN rc-update add sshd WORKDIR /git-server/ # -D flag avoids password generation # -s flag changes user's shell RUN mkdir /git-server/keys \ && adduser -h /git-server -D -s /usr/bin/git-shell git \ && passwd -u git # This is a login shell for SSH accounts to provide restricted Git access. # It permits execution only of server-side Git commands implementing the # pull/push functionality, plus custom commands present in a subdirectory # named git-shell-commands in the user’s home directory. # More info: https://git-scm.com/docs/git-shell COPY git-shell-commands /home/git/git-shell-commands # sshd_config file is edited to enable key access and disable access password COPY ssh/ssh* /etc/ssh/ RUN chmod 600 /etc/ssh/ssh_host_*_key COPY start.sh start.sh # add the default key COPY ssh/id_ed25519.pub /git-server/.ssh/authorized_keys RUN chmod 755 /git-server/.ssh RUN chmod 644 /git-server/.ssh/authorized_keys RUN chown -R git /git-server/.ssh # setup nginx RUN mkdir /run/nginx COPY htpasswd /git-server/htpasswd COPY nginx.conf /git-server/nginx.conf # create an empty repository RUN git init --bare --shared /srv/repo.git RUN chown -R git /srv/repo.git EXPOSE 22 EXPOSE 80 EXPOSE 9418 CMD ["sh", "start.sh"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/git-shell-commands/no-interactive-login0000755000000000000000000000026314751647721024306 0ustar00#!/bin/sh printf '%s\n' "Welcome to git-server-docker!" printf '%s\n' "You've successfully authenticated, but I do not" printf '%s\n' "provide interactive shell access." exit 128 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/htpasswd0000644000000000000000000000005214751647721016410 0ustar00git:$apr1$.ZE/6K.M$QxhrmfJON6BBBXSv9DtJk1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/nginx.conf0000644000000000000000000000361214751647721016627 0ustar00daemon off; user git; events { worker_connections 8; } http { server { listen 80; access_log /dev/fd/1; error_log /dev/fd/2; client_max_body_size 0; root /srv; location / { auth_basic "Git Access"; auth_basic_user_file /git-server/htpasswd; fastcgi_pass unix:/run/git.socket; fastcgi_read_timeout 600; fastcgi_param SCRIPT_FILENAME /usr/libexec/git-core/git-http-backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_NAME /usr/libexec/git-core/git-http-backend; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REQUEST_SCHEME $scheme; fastcgi_param HTTPS $https if_not_empty; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTEUSER $remote_user; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param GIT_PROJECT_ROOT $document_root; fastcgi_param GIT_HTTP_EXPORT_ALL ""; fastcgi_param PATH_INFO $uri; } } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/ssh/id_ed255190000644000000000000000000000061714751647721017031 0ustar00-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACB9K4vsdpN7dkeKdxGx+gjYTw3SNWXlYVamuC9+sB34PwAAAJB3c52sd3Od rAAAAAtzc2gtZWQyNTUxOQAAACB9K4vsdpN7dkeKdxGx+gjYTw3SNWXlYVamuC9+sB34Pw AAAEBdHBxxgT5RZxodxwFMi51johF6ftxpoovGp6+sZf9CHn0ri+x2k3t2R4p3EbH6CNhP DdI1ZeVhVqa4L36wHfg/AAAADWRhbkBkb2gubG9jYWw= -----END OPENSSH PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/ssh/id_ed25519.pub0000644000000000000000000000014014751647721017605 0ustar00ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0ri+x2k3t2R4p3EbH6CNhPDdI1ZeVhVqa4L36wHfg/ git@git-server ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/ssh/ssh_host_ed25519_key0000644000000000000000000000061714751647721021137 0ustar00-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDa5Ppf+28Eli4tXlhjV/cZcck7x/54cLTOkI38feK1XwAAAJBUBd7lVAXe 5QAAAAtzc2gtZWQyNTUxOQAAACDa5Ppf+28Eli4tXlhjV/cZcck7x/54cLTOkI38feK1Xw AAAECRu9sw3KSikfgHx5frpS88mo97pyVqVGjOR9H5H5LioNrk+l/7bwSWLi1eWGNX9xlx yTvH/nhwtM6Qjfx94rVfAAAADWRhbkBkb2gubG9jYWw= -----END OPENSSH PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/ssh/ssh_host_ed25519_key.pub0000644000000000000000000000013714751647721021721 0ustar00ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrk+l/7bwSWLi1eWGNX9xlxyTvH/nhwtM6Qjfx94rVf dan@doh.local ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/ssh/sshd_config0000644000000000000000000000752614751647721017653 0ustar00# $OpenBSD: sshd_config,v 1.98 2016/02/17 05:29:04 djm Exp $ # This is the sshd server system-wide configuration file. See # sshd_config(5) for more information. # This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin # The strategy used for options in the default sshd_config shipped with # OpenSSH is to specify options with their default value where # possible, but leave them commented. Uncommented options override the # default value. #Port 22 #AddressFamily any #ListenAddress 0.0.0.0 #ListenAddress :: # The default requires explicit activation of protocol 1 #Protocol 2 # HostKey for protocol version 1 #HostKey /etc/ssh/ssh_host_key # HostKeys for protocol version 2 #HostKey /etc/ssh/ssh_host_rsa_key #HostKey /etc/ssh/ssh_host_dsa_key #HostKey /etc/ssh/ssh_host_ecdsa_key #HostKey /etc/ssh/ssh_host_ed25519_key # Lifetime and size of ephemeral version 1 server key #KeyRegenerationInterval 1h #ServerKeyBits 1024 # Ciphers and keying #RekeyLimit default none # Logging # obsoletes QuietMode and FascistLogging #SyslogFacility AUTH #LogLevel INFO # Authentication: #LoginGraceTime 2m #PermitRootLogin prohibit-password #StrictModes yes #MaxAuthTries 6 #MaxSessions 10 PubkeyAuthentication yes # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 # but this is overridden so installations will only check .ssh/authorized_keys AuthorizedKeysFile .ssh/authorized_keys #AuthorizedKeysFile /home/git/.ssh/authorized_keys #AuthorizedPrincipalsFile none #AuthorizedKeysCommand none #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts #RhostsRSAAuthentication no # similar for protocol version 2 #HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # RhostsRSAAuthentication and HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files #IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication no #PermitEmptyPasswords no # Change to no to disable s/key passwords #ChallengeResponseAuthentication yes # Kerberos options (deprecated) #KerberosAuthentication no #KerberosOrLocalPasswd yes #KerberosTicketCleanup yes #KerberosGetAFSToken no # GSSAPI options (deprecated) #GSSAPIAuthentication no #GSSAPICleanupCredentials yes # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will # be allowed through the ChallengeResponseAuthentication and # PasswordAuthentication. Depending on your PAM configuration, # PAM authentication via ChallengeResponseAuthentication may bypass # the setting of "PermitRootLogin without-password". # If you just want the PAM account and session checks to run without # PAM authentication, then enable this but set PasswordAuthentication # and ChallengeResponseAuthentication to 'no'. #UsePAM no #AllowAgentForwarding yes #AllowTcpForwarding yes #GatewayPorts no #X11Forwarding no #X11DisplayOffset 10 #X11UseLocalhost yes #PermitTTY yes PrintMotd no #PrintLastLog yes #TCPKeepAlive yes #UseLogin no #UsePrivilegeSeparation sandbox #PermitUserEnvironment no #Compression delayed #ClientAliveInterval 0 #ClientAliveCountMax 3 #UseDNS no #PidFile /run/sshd.pid #MaxStartups 10:30:100 #PermitTunnel no #ChrootDirectory none #VersionAddendum none # no default banner path #Banner none # override default of no subsystems Subsystem sftp /usr/lib/ssh/sftp-server # the following are HPN related configuration options # tcp receive buffer polling. disable in non autotuning kernels #TcpRcvBufPoll yes # disable hpn performance boosts #HPNDisabled no # buffer size for hpn to non-hpn connections #HPNBufferSize 2048 # Example of overriding settings on a per-user basis #Match User anoncvs # X11Forwarding no # AllowTcpForwarding no # PermitTTY no # ForceCommand cvs server ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/git-server/start.sh0000755000000000000000000000076014751647721016332 0ustar00#!/bin/sh # for each daemon, we take care to ensure that all logging happens to # stdout & stderr # start the fcgi daemon /usr/bin/spawn-fcgi \ -u git -U git -n \ -d /srv \ -s /run/git.socket \ /usr/bin/fcgiwrap & # start the simple daemon git daemon \ --export-all \ --user=git \ --base-path=/srv \ /srv & # start nginx /usr/sbin/nginx \ -c /git-server/nginx.conf & # finally, start ssh # -D flag avoids executing sshd as a daemon exec /usr/sbin/sshd -D ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/contrib/docker/installhg.sh0000755000000000000000000000163614751647721015076 0ustar00#!/bin/sh set -e BUILDDEPENDS="curl jq coreutils gcc gettext musl-dev" RUNDEPENDS="git git-daemon unzip openssh gnupg" PIPDEPENDS="black coverage dulwich pyflakes pygments pylint setuptools_scm" PIP="python -m pip --no-cache-dir" set -xe apk add --no-cache $BUILDDEPENDS $RUNDEPENDS # update pip itself, due to issue #11123 in pip $PIP install -U pip setuptools wheel $PIP install $PIPDEPENDS # handle pre-release versions get_version() { curl -s "https://pypi.org/pypi/$1/json" \ | jq -r '.releases | keys_unsorted | .[]' \ | grep "^$2" \ | sort --version-sort \ | tail -1 } hgversion=$(get_version mercurial $HG) if test -n "$hgversion" then $PIP install mercurial==$hgversion else # unreleased, so fetch directly from Heptapod itself $PIP install \ https://foss.heptapod.net/octobus/mercurial-devel/-/archive/branch/$HG/hg.tar.bz2 fi apk del $BUILDDEPENDS ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/__init__.py0000644000000000000000000001624714751647721013062 0ustar00# git.py - git server bridge # # Copyright 2008 Scott Chacon # also some code (and help) borrowed from durin42 # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. r'''push and pull from a Git server This extension lets you communicate (push and pull) with a Git server. This way you can use Git hosting for your project or collaborate with a project that is in Git. A bridger of worlds, this plugin be. Try :hg:`clone git+https://github.com/dulwich/dulwich` or :hg:`clone git+ssh://example.com/repo.git`. Basic Use --------- You can clone a Git repository from Mercurial by running :hg:`clone [dest]`. For example, if you were to run:: $ hg clone git://github.com/schacon/hg-git.git Hg-Git would clone the repository and convert it to a Mercurial repository for you. There are a number of different protocols that can be used for Git repositories. Examples of Git repository URLs include:: git+https://github.com/hg-git/hg-git.git git+http://github.com/hg-git/hg-git.git git+ssh://git@github.com/hg-git/hg-git.git git://github.com/hg-git/hg-git.git file:///path/to/hg-git ../hg-git (local file path) These also work:: git+ssh://git@github.com:hg-git/hg-git.git git@github.com:hg-git/hg-git.git Please note that you need to prepend HTTP, HTTPS, and SSH URLs with ``git+`` in order differentiate them from Mercurial URLs. For example, an HTTPS URL would start with ``git+https://``. Also, note that Git doesn't require the specification of the protocol for SSH, but Mercurial does. Hg-Git automatically detects whether file paths should be treated as Git repositories by their contents. If you are starting from an existing Mercurial repository, you have to set up a Git repository somewhere that you have push access to, add a path entry for it in your .hg/hgrc file, and then run :hg:`push [name]` from within your repository. For example:: $ cd hg-git # (a Mercurial repository) $ # edit .hg/hgrc and add the target Git URL in the paths section $ hg push This will convert all your Mercurial changesets into Git objects and push them to the Git server. Pulling new revisions into a repository is the same as from any other Mercurial source. Within the earlier examples, the following commands are all equivalent:: $ hg pull $ hg pull default $ hg pull git://github.com/hg-git/hg-git.git Git branches are exposed in Mercurial as bookmarks, while Git remote branches are exposed as unchangable Mercurial local tags. See :hg:`help bookmarks` and :hg:`help tags` for further information. Finding and displaying Git revisions ------------------------------------ For displaying the Git revision ID, Hg-Git provides a template keyword: :gitnode: String. The Git changeset identification hash, as a 40 hexadecimal digit string. For example:: $ hg log --template='{rev}:{node|short}:{gitnode|short} {desc}\n' $ hg log --template='hg: {node}\ngit: {gitnode}\n{date|isodate} {author}\n{desc}\n\n' For finding changesets from Git, Hg-Git extends revsets to provide two new selectors: :fromgit: Select changesets that originate from Git. Takes no arguments. :gitnode: Select changesets that originate in a specific Git revision. Takes a revision argument. For example:: $ hg log -r 'fromgit()' $ hg log -r 'gitnode(84f75b909fc3)' Revsets are accepted by several Mercurial commands for specifying revisions. See :hg:`help revsets` for details. Creating Git tags ----------------- You can create a lightweight Git tag using using :hg:`tag --git`. Please note that this requires an explicit -r/--rev, and does not support any of the other flags for :hg:`hg tag`. Support for Git tags is somewhat minimal. The Git documentation heavily discourages changing tags once pushed, and suggests that users always create a new one instead. (Unlike Mercurial, Git does not track and version its tags within the repository.) As result, there's no support for removing and changing preexisting tags. Similarly, there's no support for annotated tags, i.e. tags with messages, nor for signing tags. For those, either use Git directly or use the integrated web interface for tags and releases offered by most hosting solutions, including GitHub and GitLab. Invalid and dangerous paths --------------------------- Both Mercurial and Git consider paths as just bytestrings internally, and allow almost anything. The difference, however, is in the _almost_ part. For example, many Git servers will reject a push for security reasons if it contains a nested Git repository. Similarly, Mercurial cannot checkout commits with a nested repository, and it cannot even store paths containing an embedded newline or carrage return character. The default is to issue a warning and skip these paths. You can change this by setting ``hggit.invalidpaths`` in :hg:`config`:: [hggit] invalidpaths = keep Possible values are ``keep``, ``skip`` or ``abort``. Ignoring files -------------- In addition to the regular :hg:`help hgignore` support, ``hg-git`` adds fallback support for Git ignore files. If no ``.hgignore`` file exists at the root of the repository, Mercurial will then read any ``.gitignore`` files that exist. Please note that Mercurial doesn't support exclusion patterns, so any ``.gitignore`` pattern starting with ``!`` will trigger a warning. ''' import dulwich from mercurial import ( demandimport, exthelper, pycompat, ) from . import bundle from . import commands from . import config from . import debugcommands from . import gitdirstate from . import gitrepo from . import hgrepo from . import overlay from . import revsets from . import schemes from . import templates demandimport.IGNORES |= { b'collections', } testedwith = b'6.6 6.7 6.8 6.9' minimumhgversion = b'6.6' buglink = b'https://foss.heptapod.net/mercurial/hg-git/issues' eh = exthelper.exthelper() cmdtable = eh.cmdtable configtable = eh.configtable filesetpredicate = eh.filesetpredicate revsetpredicate = eh.revsetpredicate templatekeyword = eh.templatekeyword uisetup = eh.finaluisetup extsetup = eh.finalextsetup reposetup = eh.finalreposetup uipopulate = eh.finaluipopulate for _mod in ( bundle, commands, config, debugcommands, gitdirstate, gitrepo, hgrepo, overlay, revsets, templates, schemes, ): eh.merge(_mod.eh) def getversion(): """return version with dependencies for hg --version -v""" # first, try to get a built version try: from .__version__ import version except ImportError: version = None # if that fails, try to determine the version from the checkout if version is None: try: from setuptools_scm import get_version version = get_version( root='..', relative_to=__file__, version_scheme="release-branch-semver", ) except: # something went wrong, but we need to provide something version = "unknown" # include the dulwich version, as it's fairly important for the # level of functionality dulver = '.'.join(map(str, dulwich.__version__)) return pycompat.sysbytes("%s (dulwich %s)" % (version, dulver)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/_ssh.py0000644000000000000000000000255214751647721012251 0ustar00import subprocess from dulwich.client import SSHGitClient, SubprocessWrapper from mercurial import pycompat from mercurial.utils import procutil class SSHVendor(object): """Parent class for ui-linked Vendor classes.""" def generate_ssh_vendor(ui): """ Allows dulwich to use hg's ui.ssh config. The dulwich.client.get_ssh_vendor property should point to the return value. """ class _Vendor(SSHVendor): def run_command( self, host, command, username=None, port=None, **kwargs ): assert isinstance(command, str) command = command.encode(SSHGitClient.DEFAULT_ENCODING) sshcmd = ui.config(b"ui", b"ssh", b"ssh") args = procutil.sshargs( sshcmd, pycompat.bytesurl(host), username, port ) cmd = b'%s %s %s' % (sshcmd, args, procutil.shellquote(command)) # consistent with mercurial ui.debug(b'running %s\n' % cmd) # we cannot use Mercurial's procutil.popen4() since it # always redirects stderr into a pipe proc = subprocess.Popen( procutil.tonativestr(cmd), shell=True, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) return SubprocessWrapper(proc) return _Vendor ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/bundle.py0000644000000000000000000000752014751647721012566 0ustar00import io from mercurial import bundle2 from mercurial import exchange from mercurial import exthelper from mercurial.node import bin, hex eh = exthelper.exthelper() CAPABILITY_MAP = b'exp-hg-git-map' CAPABILITY_TAGS = b'exp-hg-git-tags' BUNDLEPART_MAP = b'exp-hg-git-map' BUNDLEPART_TAGS = b'exp-hg-git-tags' @eh.wrapfunction(bundle2, 'getrepocaps') def getrepocaps(orig, repo, **kwargs): caps = orig(repo, **kwargs) if repo.ui.configbool(b'experimental', b'hg-git-serve'): caps[CAPABILITY_MAP] = () caps[CAPABILITY_TAGS] = () return caps def addpartrevgitmap(repo, bundler, outgoing): if repo.githandler: # this is different from what we store in the repository, # and uses binary node ids: <20 bytes> <20 bytes> repo.ui.debug(b'bundling git map\n') chunks = ( bin(repo.githandler._map_hg[hex(hgnode)]) + hgnode for hgnode in outgoing.missing if hex(hgnode) in repo.githandler._map_hg ) bundler.newpart(BUNDLEPART_MAP, data=chunks, mandatory=False) def addpartrevgittags(repo, bundler, outgoing): if repo.githandler.tags: # this is consistent with the format used in the repository: # <40 hex digits> repo.ui.debug(b'bundling git tags\n') chunks = ( b"%s %s\n" % (sha, name) for name, sha in sorted(repo.githandler.tags.items()) if bin(sha) in outgoing.missing ) bundler.newpart(BUNDLEPART_TAGS, data=chunks, mandatory=False) @eh.wrapfunction(bundle2, '_addpartsfromopts') def _addpartsfromopts(orig, ui, repo, bundler, source, outgoing, opts): orig(ui, repo, bundler, source, outgoing, opts) if opts.get(CAPABILITY_MAP, False) or ui.configbool( b'experimental', b'hg-git-bundle' ): addpartrevgitmap(repo, bundler, outgoing) if opts.get(CAPABILITY_TAGS, False) or ui.configbool( b'experimental', b'hg-git-bundle' ): addpartrevgittags(repo, bundler, outgoing) @eh.extsetup def install_server_support(ui): @bundle2.parthandler(BUNDLEPART_MAP) def handlebundlemap(op, inpart): ui.debug(b'unbundling git map\n') while not inpart.consumed: # this is different from what we store in the repository, # and uses binary node ids: <20 bytes> <20 bytes> gitsha = hex(inpart.read(20)) hgsha = hex(inpart.read(20)) if gitsha and hgsha: op.repo.githandler.map_set(gitsha, hgsha) op.repo.githandler.save_map() @exchange.getbundle2partsgenerator(BUNDLEPART_MAP) def gitmapbundle( bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs ): if b2caps is None or CAPABILITY_MAP not in b2caps: return ui.debug(b'bundling git map\n') outgoing = exchange._computeoutgoing( repo, kwargs['heads'], kwargs['common'], ) addpartrevgitmap(repo, bundler, outgoing) @bundle2.parthandler(BUNDLEPART_TAGS) def handlebundletags(op, inpart): with io.BytesIO() as buf: while not inpart.consumed: buf.write(inpart.read()) buf.seek(0) # we're consistent, and always load everything, so just # let the handler do its thing op.repo.githandler._read_tags_from(buf) op.repo.githandler.save_tags() @exchange.getbundle2partsgenerator(BUNDLEPART_TAGS) def gittagbundle( bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs ): if b2caps is None or CAPABILITY_TAGS not in b2caps: return outgoing = exchange._computeoutgoing( repo, kwargs['heads'], kwargs['common'], ) addpartrevgittags(repo, bundler, outgoing) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/commands.py0000644000000000000000000001476114751647721013123 0ustar00# git.py - git server bridge # # Copyright 2008 Scott Chacon # also some code (and help) borrowed from durin42 # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # global modules import os from dulwich import porcelain from mercurial.node import hex, nullhex from mercurial.i18n import _ from mercurial import ( cmdutil, error, exthelper, pycompat, registrar, scmutil, ) # local modules from . import verify eh = exthelper.exthelper() @eh.command( b'git-import|gimport', helpcategory=registrar.command.CATEGORY_IMPORT_EXPORT, ) def gimport(ui, repo, remote_name=None): '''import commits from Git to Mercurial (ADVANCED) This command is equivalent to pulling from a Git source, but without actually accessing the network. Internally, hg-git relies on a local, cached git repository containing changes equivalent to the Mercurial repository. If you modify that Git repository somehow, use this command to import those changes. ''' with repo.wlock(): repo.githandler.import_commits(remote_name) @eh.command( b'git-export|gexport', helpcategory=registrar.command.CATEGORY_IMPORT_EXPORT, ) def gexport(ui, repo): '''export commits from Mercurial to Git (ADVANCED) This command is equivalent to pushing to a Git source, but without actually access the network. Internally, hg-git relies on a local, cached git repository containing changes equivalent to the Mercurial repository. If you wish to see what the Git commits would be, use this command to export those changes. As an example, it ensures that all changesets have a corresponding Git node. ''' repo.githandler.export_commits() @eh.command( b'git-verify|gverify', [ (b'r', b'rev', b'', _(b'revision to verify'), _(b'REV')), (b'c', b'fsck', False, _(b'verify repository integrity as well')), ], _(b'[-r REV]'), helpcategory=registrar.command.CATEGORY_MAINTENANCE, ) def gverify(ui, repo, **opts): '''verify that a Mercurial rev matches the corresponding Git rev Given a Mercurial revision that has a corresponding Git revision in the map, this attempts to answer whether that revision has the same contents as the corresponding Git revision. ''' if opts.get('fsck'): for badsha, e in porcelain.fsck(repo.githandler.git): raise error.Abort(b'git repository is corrupt!') ctx = scmutil.revsingle(repo, opts.get('rev'), b'.') return verify.verify(ui, repo, ctx) @eh.command(b'git-cleanup', helpcategory=registrar.command.CATEGORY_MAINTENANCE) def git_cleanup(ui, repo): '''clean up Git commit map after history editing''' new_map = [] gh = repo.githandler for line in gh.vfs(gh.map_file): gitsha, hgsha = line.strip().split(b' ', 1) if hgsha in repo: ui.debug(b'keeping GIT:%s -> HG:%s\n' % (gitsha, hgsha)) new_map.append(b'%s %s\n' % (gitsha, hgsha)) else: ui.note(b'dropping GIT:%s -> HG:%s\n' % (gitsha, hgsha)) with repo.githandler.store_repo.wlock(): f = gh.vfs(gh.map_file, b'wb') f.writelines(new_map) for ref, gitsha in gh.git.refs.as_dict().items(): if gitsha in gh.git: ui.debug(b'keeping %s -> GIT:%s\n' % (ref, gitsha)) else: ui.note(b'dropping %s -> GIT:%s\n' % (ref, gitsha)) del gh.git.refs[ref] ui.status(_(b'git commit map cleaned\n')) @eh.wrapcommand( b'tag', opts=[ ( b'', b'git', False, b'''create a lightweight Git tag; this requires an explicit -r/--rev, and does not support any of the other flags''', ) ], ) def tag(orig, ui, repo, *names, **opts): if not opts.get('git'): return orig(ui, repo, *names, **opts) opts = pycompat.byteskwargs(opts) # check for various unimplemented arguments cmdutil.check_incompatible_arguments( opts, b'git', [ # conflict b'local', # we currently don't convert or expose this metadata, so # disallow setting it on creation b'edit', b'message', b'date', b'user', ], ) cmdutil.check_at_most_one_arg(opts, b'rev', b'remove') if opts[b'remove']: opts[b'rev'] = b'null' if not opts.get(b'rev'): raise error.Abort( _(b'git tags require an explicit revision'), hint=b'please specify -r/--rev', ) # the semantics of git tag editing are quite confusing, so we # don't bother; if you _really_ want to, use another tool to do # this, and ensure all contributors prune their tags -- otherwise, # it'll reappear next time someone pushes tags (ah, the wonders of # nonversioned markers!) # # see also https://git-scm.com/docs/git-tag#_discussion if opts[b'force']: raise error.Abort( b'cannot move git tags', hint=b'the git documentation heavily discourages editing tags', ) names = [t.strip() for t in names] if len(names) != len(set(names)): raise error.Abort(_('tag names must be unique')) with repo.wlock(), repo.lock(): target = hex(repo.lookup(opts[b'rev'])) # see above if target == nullhex: raise error.Abort( b'cannot remove git tags', hint=b'the git documentation heavily discourages editing tags', ) repo.githandler.add_tag(target, *names) @eh.wrapcommand(b'annotate') def annotate(orig, ui, repo, *pats, **opts): skiprevs = opts.get(b'skip', []) ignorerevscfg = ui.config(b'git', b'blame.ignoreRevsFile') if ignorerevscfg and repo.githandler: ignorerevsfile = repo.wjoin(ignorerevscfg) if os.path.isfile(ignorerevsfile): with open(ignorerevsfile, 'rb') as fp: for line in fp: git_sha = line.strip().split(b'#', 1)[0] if not git_sha: continue hg_sha = repo.githandler.map_hg_get(git_sha) if hg_sha is not None: ui.debug( b'skipping %s -> %s\n' % (git_sha[:12], hg_sha[:12]) ) skiprevs.append(hg_sha) opts['skip'] = skiprevs return orig(ui, repo, *pats, **opts) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/config.py0000644000000000000000000000470514751647721012564 0ustar00import bisect import collections from mercurial import exthelper from mercurial import help from mercurial.i18n import _ from mercurial.utils import stringutil, urlutil from . import util eh = exthelper.exthelper() CONFIG_DEFAULTS = { b'experimental': { b'hg-git-bundle': False, b'hg-git-serve': False, }, b'git': { b'authors': None, b'branch_bookmark_suffix': None, b'findcopiesharder': False, b'intree': None, b'mindate': None, b'public': list, b'renamelimit': 400, b'similarity': 0, b'pull-prune-remote-branches': True, b'pull-prune-bookmarks': True, b'blame.ignoreRevsFile': None, }, b'hggit': { b'fetchbuffer': 100, b'mapsavefrequency': 1000, b'usephases': None, b'retries': 3, b'invalidpaths': b'skip', b'threads': -1, }, } for section, items in CONFIG_DEFAULTS.items(): for item, default in items.items(): eh.configitem(section, item, default=default) publishoption = collections.namedtuple( 'publishoption', ['use_phases', 'publish_defaults', 'refs_to_publish'] ) def get_publishing_option(ui, remote_names): refs = set(ui.configlist(b'git', b'public')) use_phases = ui.configbool(b'hggit', b'usephases', None) if use_phases is None: use_phases = any( not p.url.islocal() for n in remote_names for p in ui.paths.get(n) ) publish_defaults = not refs return publishoption(use_phases, publish_defaults, refs) @eh.extsetup def extsetup(ui): @urlutil.pathsuboption(b'hg-git.publish', 'hggit_publish') def pathsuboption(ui, path, value): b = stringutil.parsebool(value) if b is True: return publishoption(True, True, frozenset()) elif b is False: return publishoption(False, False, frozenset()) else: return publishoption( True, False, frozenset(stringutil.parselist(value)) ) def insertconfigurationhelp(ui, topic, doc): doc += ( b'\n\n' + util.get_package_resource("helptext/config.rst").strip() ) return doc help.addtopichook(b'config', insertconfigurationhelp) entry = ( [b'hggit-config'], _(b"Configuring hg-git"), lambda ui: util.get_package_resource("helptext/config.rst"), help.TOPIC_CATEGORY_CONFIG, ) bisect.insort(help.helptable, entry) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/debugcommands.py0000644000000000000000000000724214751647721014126 0ustar00# hggitperf.py - performance test routines '''helper extension to measure performance of hg-git operations This requires both the hggit and hggitperf extensions to be enabled and available. ''' from __future__ import generator_stop import functools import os import tempfile import time from mercurial import exthelper from mercurial import registrar eh = exthelper.exthelper() # the timer functions are copied from mercurial/contrib/perf.py def gettimer(ui, opts=None): """return a timer function and formatter: (timer, formatter) This functions exist to gather the creation of formatter in a single place instead of duplicating it in all performance command.""" # enforce an idle period before execution to counteract power management time.sleep(ui.configint(b"perf", b"presleep", 1)) if opts is None: opts = {} # redirect all to stderr ui = ui.copy() ui.fout = ui.ferr # get a formatter fm = ui.formatter(b'perf', opts) return functools.partial(_timer, fm), fm def _timer(fm, func, title=None): results = [] begin = time.time() count = 0 while True: ostart = os.times() cstart = time.time() r = func() cstop = time.time() ostop = os.times() count += 1 a, b = ostart, ostop results.append((cstop - cstart, b[0] - a[0], b[1] - a[1])) if cstop - begin > 3 and count >= 100: break if cstop - begin > 10 and count >= 3: break fm.startitem() if title: fm.write(b'title', b'! %s\n', title) if r: fm.write(b'result', b'! result: %s\n', r) m = min(results) fm.plain(b'!') fm.write(b'wall', b' wall %f', m[0]) fm.write(b'comb', b' comb %f', m[1] + m[2]) fm.write(b'user', b' user %f', m[1]) fm.write(b'sys', b' sys %f', m[2]) fm.write(b'count', b' (best of %d)', count) fm.plain(b'\n') @eh.command( b'debugperfgitloadmap', helpcategory=registrar.command.CATEGORY_MISC, ) def perfgitloadmap(ui, repo): '''time loading the rev map of a repository''' ui.status(b'timing map load from %s\n' % repo.path) timer, fm = gettimer(ui) timer(repo.githandler.load_map) fm.end() @eh.command( b'debugperfgitsavemap', helpcategory=registrar.command.CATEGORY_MISC, ) def perfgitsavemap(ui, repo): '''time saving the rev map of a repository''' ui.status(b'timing map save in %s\n' % repo.path) timer, fm = gettimer(ui) repo.githandler.load_map() tmpfp = tempfile.NamedTemporaryFile(prefix=b'.git-mapfile-', dir=repo.path) with tmpfp: def perf(): with open(tmpfp.name, 'wb') as fp: repo.githandler._write_map_to(fp) timer(perf) fm.end() @eh.command( b'debugperfgitloadremotes', helpcategory=registrar.command.CATEGORY_MISC, ) def perfgitloadremotes(ui, repo): timer, fm = gettimer(ui) timer(repo.githandler.load_remote_refs) fm.end() @eh.command( b'debuggitdir', helpcategory=registrar.command.CATEGORY_WORKING_DIRECTORY, ) def gitdir(ui, repo): '''get the root of the git repository''' repo.ui.write(os.path.normpath(repo.githandler.gitdir), b'\n') @eh.command( b'debug-remove-hggit-state', helpcategory=registrar.command.CATEGORY_MAINTENANCE, ) def removestate(ui, repo): '''remove all Git-related cache and metadata (DANGEROUS) Strips all Git-related metadata from the repo, including the mapping between Git and Mercurial changesets. This is an irreversible destructive operation that may prevent further interaction with other clones. ''' repo.ui.status(b"clearing out the git cache data\n") repo.githandler.clear() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/gc.py0000644000000000000000000001017414751647721011705 0ustar00"""Packing support for when exporting to Git""" import multiprocessing import queue import threading import typing from dulwich.object_store import PackBasedObjectStore from mercurial import ui as uimod class Worker(threading.Thread): """Worker thread that we can stop. Deliberately not a deamon thread so that we avoid leaking threads for long-running processes such as TortoiseHg. """ # Check for shutdown at this interval def __init__(self, task_queue: queue.Queue): super().__init__() self.shutdown_flag = threading.Event() self.task_queue = task_queue def run(self): while not self.shutdown_flag.is_set(): try: ui, object_store, shas = self.task_queue.get( block=True, timeout=0.1, ) except queue.Empty: continue try: _process_batch(ui, object_store, shas) except: ui.traceback() ui.warn(b'warning: fail to pack %d loose objects\n' % len(shas)) finally: self.task_queue.task_done() def shutdown(self): """Stop the worker""" self.shutdown_flag.set() def _process_batch(ui, object_store, shas): ui.note(b'packing %d loose objects...\n' % len(shas)) objects = {(object_store._get_loose_object(sha), None) for sha in shas} # some progress would be nice here, but the API isn't conductive # to it object_store.add_objects(list(objects)) for obj, path in objects: object_store._remove_loose_object(obj.id) ui.debug(b'packed %d loose objects!\n' % len(shas)) class GCPacker: """Pack loose objects into packs. Normally, Git will run a detached gc on regular intervals. This does _some_ of that work by packing loose objects into individual packs. As packing is mostly an I/O and compression-bound operation, we use a queue to schedule the operations for worker threads, allowing us some actual concurrency. Please note that all methods in class are executed on the calling thread; any actual threading happens in the worker class. """ ui: uimod.ui object_store: PackBasedObjectStore queue: typing.Optional[queue.Queue] seen: typing.Set[bytes] def __init__(self, ui: uimod.ui, object_store: PackBasedObjectStore): self.ui = ui self.object_store = object_store self.seen = set() threads = ui.configint(b'hggit', b'threads', -1) if threads < 0: # some systems have a _lot_ of cores, and it seems # unlikely we need all of them; four seems a suitable # default, so that we can have up to three worker threads # concurrently packing; one seems to suffice in most cases threads = min(multiprocessing.cpu_count(), 4) if threads == 1: # synchronous operation self.queue = None self.workers = [] else: self.queue = queue.Queue(0) # we know that there's a conversion going on in the main # thread, so the worker count is one less than the thread # count self.workers = [Worker(self.queue) for _ in range(threads - 1)] for thread in self.workers: thread.start() def pack(self, synchronous=False): # remove any objects already scheduled for packing, as we # perform packing asynchronously, and we may have other # threads concurrently packing all_loose = set(self.object_store._iter_loose_objects()) todo = all_loose - self.seen self.seen |= todo if synchronous or self.queue is None: _process_batch(self.ui, self.object_store, todo) else: self.queue.put((self.ui, self.object_store, todo)) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if self.queue is not None: self.queue.join() for worker in self.workers: worker.shutdown() for worker in self.workers: worker.join() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/git2hg.py0000644000000000000000000002610314751647721012477 0ustar00# git2hg.py - convert Git repositories and commits to Mercurial ones import collections import io import itertools from dulwich import config as dul_config from dulwich.objects import Commit, Tag from dulwich.refs import ( ANNOTATED_TAG_SUFFIX, LOCAL_BRANCH_PREFIX, LOCAL_TAG_PREFIX, ) from mercurial.i18n import _ from mercurial.node import bin, short from mercurial import error, util as hgutil from mercurial import phases from . import config def get_public(ui, refs, remote_names): cfg = config.get_publishing_option(ui, remote_names) paths = list( itertools.chain.from_iterable( ui.paths.get(name) for name in remote_names ) ) # we may have multiple paths listed, so parse their configuration # and deduplicate it configs = {path.hggit_publish for path in paths} # and if we find more then one, we don't know which is correct # (but if we actually had the original path object somehow, we # wouldn't have to do this) if len(configs) > 1: raise error.Abort( b'different publishing configurations for the same remote ' b'location', hint=(b'conflicting paths: ' + b", ".join(sorted(remote_names))), ) if configs and configs != {None}: cfg = configs.pop() use_phases, publish_defaults, refs_to_publish = cfg if not use_phases: return {} to_publish = set() for remote_name in remote_names: refs_to_publish |= { ref[len(remote_name) + 1 :] for ref in refs_to_publish if ref.startswith(remote_name + b'/') } for ref_name, sha in refs.items(): if ref_name.startswith(LOCAL_BRANCH_PREFIX): branch = ref_name[len(LOCAL_BRANCH_PREFIX) :] if branch in refs_to_publish: ui.note(b"publishing branch %s\n" % branch) to_publish.add(sha) elif ref_name.startswith(LOCAL_TAG_PREFIX): tag = ref_name[len(LOCAL_TAG_PREFIX) :] if publish_defaults or tag in refs_to_publish: ui.note( b"publishing tag %s\n" % ref_name[len(LOCAL_TAG_PREFIX) :] ) to_publish.add(sha) elif publish_defaults and ref_name == b'HEAD': ui.note(b"publishing remote HEAD\n") to_publish.add(sha) return to_publish def find_incoming(ui, git_object_store, git_map, refs, remote): '''find what commits need to be imported git_object_store: is a dulwich object store. git_map: is a map with keys being Git commits that have already been imported refs: is a map of refs to SHAs that we're interested in. ''' public = get_public(ui, refs, remote) done = set() # sort by commit date def commitdate(sha): obj = git_object_store[sha] return obj.commit_time - obj.commit_timezone # get a list of all the head shas def get_heads(refs): todo = [] seenheads = set() for ref, sha in refs.items(): # refs could contain refs on the server that we haven't pulled down # the objects for; also make sure it's a sha and not a symref if ref != b'HEAD' and sha in git_object_store: obj = git_object_store[sha] while isinstance(obj, Tag): obj_type, sha = obj.object obj = git_object_store[sha] if isinstance(obj, Commit) and sha not in seenheads: seenheads.add(sha) todo.append(sha) todo.sort(key=commitdate, reverse=True) return todo def get_unseen_commits(todo): '''get all unseen commits reachable from todo in topological order 'unseen' means not reachable from the done set and not in the git map. Mutates todo and the done set in the process.''' commits = [] while todo: sha = todo[-1] if sha in done or sha in git_map: todo.pop() continue assert isinstance(sha, bytes) obj = git_object_store[sha] assert isinstance(obj, Commit) for p in obj.parents: if sha in public: public.add(p) if p not in done and p not in git_map: todo.append(p) # process parents of a commit before processing the # commit itself, and come back to this commit later break else: commits.append(sha) done.add(sha) todo.pop() return commits todo = get_heads(refs) commits = get_unseen_commits(todo) for sha in reversed(commits): for p in git_object_store[sha].parents: if sha in public: public.add(p) return [ GitIncomingCommit( sha, phases.public if sha in public else phases.draft, ) for sha in commits ] class GitIncomingCommit: '''struct to store result from find_incoming''' __slots__ = 'sha', 'phase' def __init__(self, sha, phase): self.sha = sha self.phase = phase @property def node(self): return bin(self.sha) @property def short(self): return short(self.node) def __bytes__(self): return self.sha def extract_hg_metadata(message, git_extra): split = message.split(b"\n--HG--\n", 1) # Renames are explicitly stored in Mercurial but inferred in Git. For # commits that originated in Git we'd like to optionally infer rename # information to store in Mercurial, but for commits that originated in # Mercurial we'd like to disable this. How do we tell whether the commit # originated in Mercurial or in Git? We rely on the presence of extra # hg-git fields in the Git commit. # # - Commits exported by hg-git versions past 0.7.0 always store at least # one hg-git field. # # - For commits exported by hg-git versions before 0.7.0, this becomes a # heuristic: if the commit has any extra hg fields, it definitely # originated in Mercurial. If the commit doesn't, we aren't really sure. # # If we think the commit originated in Mercurial, we set renames to a # dict. If we don't, we set renames to None. Callers can then determine # whether to infer rename information. renames = None extra = {} branch = None if len(split) == 2: renames = {} message, meta = split lines = meta.split(b"\n") for line in lines: if line == b'': continue if b' : ' not in line: break command, data = line.split(b" : ", 1) if command == b'rename': before, after = data.split(b" => ", 1) renames[after] = before if command == b'branch': branch = data if command == b'extra': k, v = data.split(b" : ", 1) extra[k] = hgutil.urlreq.unquote(v) git_fn = 0 for field, data in git_extra: if field.startswith(b'HG:'): if renames is None: renames = {} command = field[3:] if command == b'rename': before, after = data.split(b':', 1) renames[hgutil.urlreq.unquote(after)] = hgutil.urlreq.unquote( before ) elif command == b'extra': k, v = data.split(b':', 1) extra[hgutil.urlreq.unquote(k)] = hgutil.urlreq.unquote(v) else: # preserve ordering in Git by using an incrementing integer for # each field. Note that extra metadata in Git is an ordered list # of pairs. hg_field = b'GIT%d-%s' % (git_fn, field) git_fn += 1 extra[hgutil.urlreq.quote(hg_field)] = hgutil.urlreq.quote(data) return (message, renames, branch, extra) def convert_git_int_mode(mode): # TODO: make these into constants convert = {0o100644: b'', 0o100755: b'x', 0o120000: b'l'} if mode in convert: return convert[mode] return b'' def set_committer_from_author(commit): commit.committer = commit.author commit.commit_time = commit.author_time commit.commit_timezone = commit.author_timezone def filter_refs(refs, heads): '''For a dictionary of refs: shas, if heads is None then return refs that match the heads. Otherwise, return refs that are heads or tags. ''' filteredrefs = [] if heads is not None: # contains pairs of ('refs/(heads|tags|...)/foo', 'foo') # if ref is just '', then we get ('foo', 'foo') stripped_refs = [ (r, r[r.find(b'/', r.find(b'/') + 1) + 1 :]) for r in refs ] for h in heads: if h.endswith(b'/*'): prefix = h[:-1] # include the / but not the * r = [ pair[0] for pair in stripped_refs if pair[1].startswith(prefix) ] r.sort() filteredrefs.extend(r) else: r = [pair[0] for pair in stripped_refs if pair[1] == h] if not r: msg = _(b"unknown revision '%s'") % h raise error.RepoLookupError(msg) elif len(r) == 1: filteredrefs.append(r[0]) else: msg = _(b"ambiguous reference %s: %s") msg %= ( h, b', '.join(sorted(r)), ) raise error.RepoLookupError(msg) else: for ref, sha in refs.items(): if not ref.endswith(ANNOTATED_TAG_SUFFIX) and ( ref.startswith(LOCAL_BRANCH_PREFIX) or ref.startswith(LOCAL_TAG_PREFIX) or ref == b'HEAD' ): filteredrefs.append(ref) filteredrefs.sort() # the choice of OrderedDict vs plain dict has no impact on stock # hg-git, but allows extensions to customize the order in which refs # are returned return collections.OrderedDict((r, refs[r]) for r in filteredrefs) def parse_gitmodules(git, tree_obj): """Parse .gitmodules from a git tree specified by tree_obj Returns a list of tuples (submodule path, url, name), where name is hgutil.urlreq.quoted part of the section's name Raises KeyError if no modules exist, or ValueError if they're invalid """ unused_mode, gitmodules_sha = tree_obj[b'.gitmodules'] gitmodules_content = git[gitmodules_sha].data with io.BytesIO(gitmodules_content) as fp: cfg = dul_config.ConfigFile.from_file(fp) return dul_config.parse_submodules(cfg) def git_file_readlines(git, tree_obj, fname): """Read content of a named entry from the git commit tree :return: list of lines """ if fname in tree_obj: unused_mode, sha = tree_obj[fname] content = git[sha].data return content.splitlines() return [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/git_handler.py0000644000000000000000000025076314751647721013606 0ustar00import collections import itertools import os import re import shutil from dulwich.client import HTTPUnauthorized from dulwich.errors import HangupException, GitProtocolError, ApplyDeltaError from dulwich.objects import Blob, Commit, Tag, Tree, parse_timezone from dulwich.pack import apply_delta from dulwich.refs import ( ANNOTATED_TAG_SUFFIX, LOCAL_BRANCH_PREFIX, LOCAL_TAG_PREFIX, ) from dulwich.repo import Repo, check_ref_format from dulwich import client from dulwich import config as dul_config from dulwich import diff_tree from dulwich import object_store from mercurial.i18n import _ from mercurial.node import hex, bin, nullid, nullhex, short from mercurial.utils import dateutil, urlutil from mercurial import ( bookmarks, context, encoding, error, hg, obsutil, phases, pycompat, url, util as hgutil, scmutil, tags as tagsmod, ) from . import _ssh from . import gc from . import git2hg from . import hg2git from . import util from .overlay import overlayrepo REMOTE_BRANCH_PREFIX = b'refs/remotes/' RE_GIT_AUTHOR = re.compile(br'^(.*?) ?\<(.*?)(?:\>(.*))?$') RE_GIT_SANITIZE_AUTHOR = re.compile(br'[<>\n]') RE_GIT_AUTHOR_EXTRA = re.compile(br'^(.*?)\ ext:\((.*)\) <(.*)\>$') RE_GIT_EXTRA_KEY = re.compile(br'GIT([0-9]*)-(.*)') # Test for git:// and git+ssh:// URI. # Support several URL forms, including separating the # host and path with either a / or : (sepr) RE_GIT_URI = re.compile( br'^(?Pgit([+]ssh)?://)(?P.*?)(:(?P\d+))?' br'(?P[:/])(?P.*)$' ) RE_NEWLINES = re.compile(br'[\r\n]$') RE_GIT_DETERMINATE_PROGRESS = re.compile(br'\((\d+)/(\d+)\)') RE_GIT_INDETERMINATE_PROGRESS = re.compile(br'(\d+)') RE_GIT_TOTALS_LINE = re.compile( br'Total \d+ \(delta \d+\), reused \d+ \(delta \d+\)', ) RE_AUTHOR_FILE = re.compile(br'\s*=\s*') class GitProgress(object): """convert git server progress strings into mercurial progress but also detect the intertwined "remote" messages """ def __init__(self, ui): self.ui = ui self._progress = None self.msgbuf = b'' def progress(self, message): # 'Counting objects: 33640, done.\n' # 'Compressing objects: 0% (1/9955) \r lines = (self.msgbuf + pycompat.sysbytes(message)).splitlines( keepends=True ) self.msgbuf = b'' for msg in lines: # if it's still a partial line, postpone processing if not RE_NEWLINES.search(msg): self.msgbuf = msg return # anything that endswith a newline, we should probably print out if msg.endswith(b'\n'): # except some final statistics if RE_GIT_TOTALS_LINE.search(msg) or msg.endswith(b', done.\n'): self.ui.note(_(b'remote: %s\n') % msg[:-1]) else: self.ui.status(_(b'remote: %s\n') % msg[:-1]) self.flush() continue # this is a progress message assert msg.endswith(b'\r'), f"{msg} is not a progress message" td = msg.split(b':', 1) data = td.pop() try: topic = td[0] except IndexError: topic = b'' determinate = RE_GIT_DETERMINATE_PROGRESS.search(data) indeterminate = RE_GIT_INDETERMINATE_PROGRESS.search(data) if self._progress and self._progress.topic != topic: return False if not self._progress: self._progress = self.ui.makeprogress(topic) if determinate: pos, total = map(int, determinate.group(1, 2)) elif indeterminate: pos = int(indeterminate.group(1)) total = None else: continue self._progress.update(pos, total=total) def flush(self, msg=b''): if self._progress is not None: self._progress.complete() self._progress = None self.progress(b'') class heads_tags(object): __slots__ = "heads", "tags" def __init__(self, heads=(), tags=()): self.heads = set(heads) self.tags = set(tags) def __iter__(self): return itertools.chain(self.heads, self.tags) def __bool__(self): return bool(self.heads) or bool(self.tags) def __repr__(self): return f"heads_tags(heads={self.heads}, tags={self.tags})" def get_repo_and_gitdir(repo): if repo.local() and repo.shared(): repo = hg.sharedreposource(repo) if repo.ui.configbool(b'git', b'intree'): gitdir = repo.wvfs.join(b'.git') else: gitdir = repo.vfs.join(b'git') return repo, gitdir def has_gitrepo(repo): if not hasattr(repo, 'vfs'): return False repo, gitdir = get_repo_and_gitdir(repo) return os.path.isdir(gitdir) class GitHandler(object): map_file = b'git-mapfile' tags_file = b'git-tags' def __init__(self, dest_repo, ui): self.repo = dest_repo self.store_repo, self.gitdir = get_repo_and_gitdir(self.repo) self.ui = ui self.init_author_file() self.branch_bookmark_suffix = ui.config( b'git', b'branch_bookmark_suffix' ) self._map_git_real = None self._map_hg_real = None self.load_tags() self._remote_refs = None self._pwmgr = url.passwordmgr(self.ui, self.ui.httppasswordmgrdb) self._clients = {} # the HTTP authentication realm -- this specifies that we've # tried an unauthenticated request, gotten a realm, and are now # ready to prompt the user, if necessary self._http_auth_realm = None def __bool__(self): return bool(self._map_git or self._map_hg) @property def vfs(self): return self.store_repo.vfs @property def is_clone(self): """detect whether the current operation is an 'hg clone'""" # a bit of a hack, but it has held true for quite some time return self.ui.configsource(b'paths', b'default') == b'clone' @property def _map_git(self): """mapping of `git-sha` to `hg-sha`""" if self._map_git_real is None: self.load_map() return self._map_git_real @property def _map_hg(self): """mapping of `hg-sha` to `git-sha`""" if self._map_hg_real is None: self.load_map() return self._map_hg_real @property def remote_refs(self): if self._remote_refs is None: self.load_remote_refs() return self._remote_refs @hgutil.propertycache def git(self): # Dulwich is going to try and join unicode ref names against # the repository path to try and read unpacked refs. This # doesn't match hg's bytes-only view of filesystems, we just # have to cope with that. As a workaround, try decoding our # (bytes) path to the repo in hg's active encoding and hope # for the best. gitpath = self.gitdir.decode( pycompat.sysstr(encoding.encoding), pycompat.sysstr(encoding.encodingmode), ) # make the git data directory if os.path.exists(self.gitdir): return Repo(gitpath) else: if self._map_git: self.ui.warn( b'warning: created new git repository at %s\n' % self.gitdir, ) os.mkdir(self.gitdir) return Repo.init_bare(gitpath) def init_author_file(self): self.author_map = {} authors_path = self.ui.config(b'git', b'authors') if authors_path: with open(self.repo.wvfs.join(authors_path), 'rb') as f: for line in f: line = line.strip() if not line or line.startswith(b'#'): continue from_, to = RE_AUTHOR_FILE.split(line, 2) self.author_map[from_] = to # FILE LOAD AND SAVE METHODS def map_set(self, gitsha, hgsha): self._map_git[gitsha] = hgsha self._map_hg[hgsha] = gitsha def map_hg_get(self, gitsha, deref=False): if deref: try: unpeeled, peeled = object_store.peel_sha( self.git.object_store, gitsha ) gitsha = peeled.id except KeyError: self.ui.note(b'note: failed to dereference %s\n' % gitsha) return None return self._map_git.get(gitsha) def map_git_get(self, hgsha): return self._map_hg.get(hgsha) def load_map(self): map_git_real = {} map_hg_real = {} if os.path.exists(self.vfs.join(self.map_file)): for line in self.vfs(self.map_file): # format is <40 hex digits> <40 hex digits>\n if len(line) != 82: raise ValueError( _(b'corrupt mapfile: incorrect line length %d') % len(line) ) gitsha = line[:40] hgsha = line[41:81] map_git_real[gitsha] = hgsha map_hg_real[hgsha] = gitsha self._map_git_real = map_git_real self._map_hg_real = map_hg_real def save_map(self): self.ui.debug( _(b"saving git map to %s\n") % self.vfs.join(self.map_file), ) with self.repo.lock(): with self.vfs(self.map_file, b'wb+', atomictemp=True) as fp: self._write_map_to(fp) def _write_map_to(self, fp): bwrite = fp.write for hgsha, gitsha in self._map_hg.items(): bwrite(b"%s %s\n" % (gitsha, hgsha)) def load_tags(self): self.tags = {} if os.path.exists(self.vfs.join(self.tags_file)): with self.vfs(self.tags_file) as fp: self._read_tags_from(fp) def _read_tags_from(self, fp): for line in fp: sha, name = line.strip().split(b' ', 1) if sha in self.repo.unfiltered(): self.tags[name] = sha def save_tags(self): with self.repo.lock(): with self.vfs(self.tags_file, b'w+', atomictemp=True) as fp: self._write_tags_to(fp) def _write_tags_to(self, fp): for name, sha in sorted(self.tags.items()): if not self.repo.tagtype(name) == b'global': fp.write(b"%s %s\n" % (sha, name)) def load_remote_refs(self): self._remote_refs = {} # don't do anything if there's no git repository, as accessing # `self.git` will create it if not os.path.isdir(self.gitdir): return # if no paths are set, we should still check 'default' pathnames = list(self.ui.paths) or [b'default'] for pathname in pathnames: base = b'refs/remotes/%s/' % pathname for ref in self.git.refs.subkeys(base): ref = base + ref sha = self.git.refs[ref] if sha in self._map_git: node = bin(self._map_git[sha]) if node in self.repo.unfiltered(): self._remote_refs[ref[13:]] = node # END FILE LOAD AND SAVE METHODS # COMMANDS METHODS def import_commits(self, remote_name): remote_names = [remote_name] if remote_name is not None else [] refs = self.git.refs.as_dict() self.import_git_objects(b'gimport', remote_names, refs) def fetch(self, remote, heads): result = self.fetch_pack(remote.path, heads) remote_names = self.remote_names(remote.path, False) oldheads = self.repo.changelog.heads() if result.refs: imported = self.import_git_objects( b'pull', remote_names, result.refs, heads=heads, ) else: imported = 0 if imported == 0: return 0 # determine whether to activate a bookmark on clone if self.is_clone: if heads: # -r/--rev was specified, so try to activate any first # bookmark specified, which is what mercurial would # update to -- _except_ if that also happens to # resolve to a branch or tag. that seems fairly # esoteric, though, so we can live with that activate = heads[0] else: # no heads means no -r/--rev and that everything was # pulled, so activate the remote HEAD headname, headnode = self.get_result_head(result) if headname is not None: # head is a symref, pick the corresponding # bookmark activate = headname elif headnode is not None and self.repo[headnode].bookmarks(): # head is detached, but there's a bookmark # pointing to it activate = self.repo[headnode].bookmarks()[0] else: # head is fully detached, so don't do anything # special other than issue a warning (at some # point in the furture, we could convert HEAD into # @) self.ui.warn( b"warning: the git source repository has a " b"detached head\n" b"(you may want to update to a bookmark)\n" ) activate = None if activate is not None: activate += self.branch_bookmark_suffix or b'' if activate in self.repo._bookmarks: bookmarks.activate(self.repo, activate) # code taken from localrepo.py:addchangegroup dh = 0 if oldheads: heads = self.repo.changelog.heads() dh = len(heads) - len(oldheads) for h in heads: if h not in oldheads and self.repo[h].closesbranch(): dh -= 1 if dh < 0: return dh - 1 else: return dh + 1 def export_commits(self): try: self.export_git_objects() self.export_hg_tags() return self.update_references() finally: self.save_map() def get_refs(self, remote): exportable = self.export_commits() old_refs = {} new_refs = {} def changed(refs): old_refs.update(refs) new_refs.update(self.get_changed_refs(refs, exportable, True)) return refs # always return the same refs to make the send a no-op try: self._call_client( remote, 'send_pack', changed, lambda have, want: [] ) changed_refs = [ ref for ref, sha in new_refs.items() if sha != old_refs.get(ref) ] new = [ bin(sha) for sha in map(self.map_hg_get, changed_refs) if sha ] old = {} for ref, sha in old_refs.items(): # make sure we don't accidentally dereference and lose # annotated tags old_target = self.map_hg_get(sha, deref=True) if old_target: self.ui.debug(b'unchanged ref %s: %s\n' % (ref, old_target)) old[bin(old_target)] = 1 else: self.ui.debug(b'changed ref %s\n' % (ref)) return old, new except (HangupException, GitProtocolError) as e: raise error.Abort( _(b"git remote error: ") + pycompat.sysbytes(str(e)) ) def push(self, remote, revs, bookmarks, force): self.repo.hook( b"preoutgoing", git=True, source=b'push', url=remote, ) old_refs, new_refs = self.upload_pack(remote, revs, bookmarks, force) remote_names = self.remote_names(remote, True) remote_desc = remote_names[0] if remote_names else b'' ref_status = new_refs.ref_status new_refs = new_refs.refs for ref, new_sha in sorted(new_refs.items()): old_sha = old_refs.get(ref) if ref_status.get(ref) is not None: self.ui.warn( b'warning: failed to update %s; %s\n' % (ref, pycompat.sysbytes(ref_status[ref])), ) elif new_sha == nullhex: self.ui.status(b"deleting reference %s\n" % ref) elif old_sha is None: if self.ui.verbose: self.ui.note( b"adding reference %s::%s => GIT:%s\n" % (remote_desc, ref, new_sha[0:8]) ) else: self.ui.status(b"adding reference %s\n" % ref) elif new_sha != old_sha: if self.ui.verbose: self.ui.note( b"updating reference %s::%s => GIT:%s\n" % (remote_desc, ref, new_sha[0:8]) ) else: self.ui.status(b"updating reference %s\n" % ref) else: self.ui.debug( b"unchanged reference %s::%s => GIT:%s\n" % (remote_desc, ref, new_sha[0:8]) ) if new_refs and remote_names: # make sure that we know the remote head, for possible # publishing new_refs_with_head = new_refs.copy() try: new_refs_with_head.update( self.fetch_pack(remote, [b'HEAD']).refs, ) except error.RepoLookupError: self.ui.debug(b'remote repository has no HEAD\n') self.update_remote_branches(remote_names, new_refs_with_head) if old_refs == new_refs: if revs or not old_refs: # fast path to skip the check below self.ui.status(_(b"no changes found\n")) else: # check whether any commits were skipped due to # missing names; this is equivalent to the stock # (ignoring %d secret commits) message, but specific # to pushing to Git, which doesn't have anonymous # heads served = self.repo.filtered(b'served') exported = set( filter( None, ( self.map_hg_get(sha, deref=True) for sha in old_refs.values() ), ) ) unexported = served.revs( b"not ancestors(%s)" % b" or ".join(exported), ) if not unexported: self.ui.status(_(b"no changes found\n")) else: self.ui.status( b"no changes found " b"(ignoring %d changesets without bookmarks or tags)\n" % len(unexported), ) ret = None elif len(new_refs) > len(old_refs): ret = 1 + (len(new_refs) - len(old_refs)) elif len(old_refs) > len(new_refs): ret = -1 - (len(new_refs) - len(old_refs)) else: ret = 1 return ret def clear(self): mapfile = self.vfs.join(self.map_file) tagsfile = self.vfs.join(self.tags_file) if os.path.exists(self.gitdir): shutil.rmtree(self.gitdir) if os.path.exists(mapfile): os.remove(mapfile) if os.path.exists(tagsfile): os.remove(tagsfile) # incoming support def getremotechanges(self, remote, revs): self.export_commits() result = self.fetch_pack(remote.path, revs) # refs contains all remote refs. Prune to only those requested. if revs: reqrefs = {} for rev in revs: for n in (LOCAL_BRANCH_PREFIX + rev, LOCAL_TAG_PREFIX + rev): if n in result.refs: reqrefs[n] = result.refs[n] else: reqrefs = result.refs commits = [ c.node for c in self.get_git_incoming( reqrefs, self.remote_names(remote.path, push=False) ) ] b = overlayrepo(self, commits, result.refs) return (b, commits, lambda: None) # CHANGESET CONVERSION METHODS def export_git_objects(self): self.ui.note(_(b"finding unexported changesets\n")) repo = self.repo clnode = repo.changelog.node nodes = (clnode(n) for n in repo) to_export = ( repo[node] for node in nodes if not hex(node) in self._map_hg ) todo_total = len(repo) - len(self._map_hg) topic = b'searching' unit = b'commits' with repo.ui.makeprogress(topic, unit, todo_total) as progress: export = [] for ctx in to_export: progress.increment(item=short(ctx.node())) if ctx.extra().get(b'hg-git', None) != b'octopus': export.append(ctx) total = len(export) if not total: return self.ui.note(_(b"exporting %d changesets\n") % total) self.repo.hook(b'gitexport', nodes=[c.hex() for c in export], git=True) # By only exporting deltas, the assertion is that all previous objects # for all other changesets are already present in the Git repository. # This assertion is necessary to prevent redundant work. Here, nodes, # and therefore export, is in topological order. By definition, # export[0]'s parents must be present in Git, so we start the # incremental exporter from there. pctx = export[0].p1() pnode = pctx.node() if pnode == nullid: gitcommit = None else: gitsha = self._map_hg[hex(pnode)] with util.abort_push_on_keyerror(): gitcommit = self.git[gitsha] exporter = hg2git.IncrementalChangesetExporter( self.repo, pctx, self.git.object_store, gitcommit ) mapsavefreq = self.ui.configint(b'hggit', b'mapsavefrequency') progress = self.repo.ui.makeprogress(b'exporting', total=total) packer = gc.GCPacker(self.ui, self.git.object_store) with progress, packer: for i, ctx in enumerate(export, 1): progress.increment(item=short(ctx.node())) self.export_hg_commit(ctx.node(), exporter) if mapsavefreq and i % mapsavefreq == 0: self.save_map() packer.pack() packer.pack(synchronous=True) # convert this commit into git objects # go through the manifest, convert all blobs/trees we don't have # write the commit object (with metadata info) def export_hg_commit(self, rev, exporter): self.ui.note(_(b"converting revision %s\n") % hex(rev)) oldenc = util.swap_out_encoding() ctx = self.repo[rev] extra = ctx.extra() commit = Commit() (time, timezone) = ctx.date() # work around to bad timezone offets - dulwich does not handle # sub minute based timezones. In the one known case, it was a # manual edit that led to the unusual value. Based on that, # there is no reason to round one way or the other, so do the # simplest and round down. timezone -= timezone % 60 commit.author = self.get_git_author(ctx) commit.author_time = int(time) commit.author_timezone = -timezone if b'committer' in extra: try: # fixup timezone (name, timestamp, timezone) = extra[b'committer'].rsplit( b' ', 2 ) commit.committer = name commit.commit_time = int(timestamp) # work around a timezone format change if int(timezone) % 60 != 0: # pragma: no cover timezone = parse_timezone(timezone) # Newer versions of Dulwich return a tuple here if isinstance(timezone, tuple): timezone, neg_utc = timezone commit._commit_timezone_neg_utc = neg_utc else: timezone = -int(timezone) commit.commit_timezone = timezone except ValueError: self.ui.traceback() git2hg.set_committer_from_author(commit) else: git2hg.set_committer_from_author(commit) commit.parents = [] for parent in self.get_git_parents(ctx): hgsha = hex(parent.node()) git_sha = self.map_git_get(hgsha) if git_sha is not None: if git_sha not in self.git.object_store: raise error.ProgrammingError( b'%s is not present in the local git cache' % git_sha ) commit.parents.append(git_sha) commit.message, extra = self.get_git_message_and_extra(ctx) commit._extra.extend(extra) if b'encoding' in extra: commit.encoding = extra[b'encoding'] if b'gpgsig' in extra: commit.gpgsig = extra[b'gpgsig'] for obj in exporter.update_changeset(ctx): if obj.id not in self.git.object_store: self.git.object_store.add_object(obj) tree_sha = exporter.root_tree_sha if tree_sha not in self.git.object_store: raise error.ProgrammingError( b'%s is not present in the local git cache' % tree_sha ) commit.tree = tree_sha if commit.id not in self.git.object_store: self.git.object_store.add_object(commit) self.map_set(commit.id, ctx.hex()) util.swap_out_encoding(oldenc) return commit.id @staticmethod def get_valid_git_username_email(name): r"""Sanitize usernames and emails to fit git's restrictions. The following is taken from the man page of git's fast-import command: [...] Likewise LF means one (and only one) linefeed [...] committer The committer command indicates who made this commit, and when they made it. Here is the person's display name (for example "Com M Itter") and is the person's email address ("cm@example.com[1]"). LT and GT are the literal less-than (\x3c) and greater-than (\x3e) symbols. These are required to delimit the email address from the other fields in the line. Note that and are free-form and may contain any sequence of bytes, except LT, GT and LF. is typically UTF-8 encoded. Accordingly, this function makes sure that there are none of the characters <, >, or \n in any string which will be used for a git username or email. Before this, it first removes left angle brackets and spaces from the beginning, and right angle brackets and spaces from the end, of this string, to convert such things as " " to "john@doe.com" for convenience. TESTS: >>> g = GitHandler.get_valid_git_username_email >>> g(b'John Doe') 'John Doe' >>> g(b'john@doe.com') 'john@doe.com' >>> g(b' ') 'john@doe.com' >>> g(b' > > ') 'random???garbage?' >>> g(b'Typo in hgrc >but.hg-git@handles.it.gracefully>') 'Typo in hgrc ?but.hg-git@handles.it.gracefully' """ return RE_GIT_SANITIZE_AUTHOR.sub( b'?', name.lstrip(b'< ').rstrip(b'> ') ) def get_git_author(self, ctx): # hg authors might not have emails author = ctx.user() # see if a translation exists author = self.author_map.get(author, author) # check for git author pattern compliance a = RE_GIT_AUTHOR.match(author) if a: name = self.get_valid_git_username_email(a.group(1)) email = self.get_valid_git_username_email(a.group(2)) if a.group(3) is not None and len(a.group(3)) != 0: name += b' ext:(' + hgutil.urlreq.quote(a.group(3)) + b')' author = b'%s <%s>' % ( self.get_valid_git_username_email(name), self.get_valid_git_username_email(email), ) elif b'@' in author: author = b'%s <%s>' % ( self.get_valid_git_username_email(author), self.get_valid_git_username_email(author), ) else: author = self.get_valid_git_username_email(author) + b' ' if b'author' in ctx.extra(): try: author = b"".join(apply_delta(author, ctx.extra()[b'author'])) except (ApplyDeltaError, AssertionError): self.ui.traceback() self.ui.warn( b"warning: disregarding possibly invalid metadata in %s\n" % ctx ) return author def get_git_parents(self, ctx): def is_octopus_part(ctx): olist = (b'octopus', b'octopus-done') return ctx.extra().get(b'hg-git', None) in olist parents = [] if ctx.extra().get(b'hg-git', None) == b'octopus-done': # implode octopus parents part = ctx while is_octopus_part(part): (p1, p2) = part.parents() assert ctx.extra().get(b'hg-git', None) != b'octopus' parents.append(p1) part = p2 parents.append(p2) else: parents = ctx.parents() return parents def get_git_message_and_extra(self, ctx): extra = ctx.extra() message = ctx.description() + b"\n" if b'message' in extra: try: message = b"".join(apply_delta(message, extra[b'message'])) except (ApplyDeltaError, AssertionError): self.ui.traceback() self.ui.warn( b"warning: disregarding possibly invalid metadata in %s\n" % ctx ) # HG EXTRA INFORMATION extra_message = b'' git_extra = [] if ctx.branch() != b'default': # we always store the branch in the extra message extra_message += b"branch : " + ctx.branch() + b"\n" # Git native extra items always come first, followed by hg renames, # followed by hg extra keys git_extraitems = [] for key, value in extra.items(): m = RE_GIT_EXTRA_KEY.match(key) if m is not None: git_extraitems.append((int(m.group(1)), m.group(2), value)) del extra[key] git_extraitems.sort() for i, field, value in git_extraitems: git_extra.append( (hgutil.urlreq.unquote(field), hgutil.urlreq.unquote(value)) ) if extra.get(b'hg-git-rename-source', None) != b'git': renames = [] for f in ctx.files(): if f not in ctx.manifest(): continue rename = ctx.filectx(f).renamed() if rename: renames.append((rename[0], f)) if renames: for oldfile, newfile in renames: spec = b'%s:%s' % ( hgutil.urlreq.quote(oldfile), hgutil.urlreq.quote(newfile), ) git_extra.append((b'HG:rename', spec)) # hg extra items always go at the end for key, value in sorted(extra.items()): if key in ( b'author', b'committer', b'encoding', b'message', b'branch', b'hg-git', b'hg-git-rename-source', ): continue else: spec = b'%s:%s' % ( hgutil.urlreq.quote(key), hgutil.urlreq.quote(value), ) git_extra.append((b'HG:extra', spec)) if extra_message: message += b"\n--HG--\n" + extra_message if ( extra.get(b'hg-git-rename-source', None) != b'git' and not git_extra and extra_message == b'' ): # We need to store this if no other metadata is stored. This # indicates that when reimporting the commit into Mercurial we'll # know not to detect renames. git_extra.append((b'HG:rename-source', b'hg')) return message, git_extra def get_git_incoming(self, refs, remote_names): return git2hg.find_incoming( self.ui, self.git.object_store, self._map_git, refs, remote_names, ) def get_transaction(self, desc=b"hg-git"): """obtain a transaction specific for the repository this ensures that we only save the map on close """ tr = self.repo.transaction(desc) tr.addfinalize(b'hg-git-save', lambda tr: self.save_map()) scmutil.registersummarycallback(self.repo, tr, b'pull') return tr def get_result_head(self, result): symref = result.symrefs.get(b'HEAD') if symref and symref.startswith(LOCAL_BRANCH_PREFIX): rhead = symref[len(LOCAL_BRANCH_PREFIX) :] if symref in result.refs: rsha = result.refs.get(symref) else: rsha = None else: rhead = None rsha = result.refs.get(b'HEAD') if rsha is not None and rsha in self._map_git: return rhead, bin(self._map_git[rsha]) else: return None, None def import_git_objects(self, command, remote_names, refs, heads=None): self.repo.hook( b'gitimport', source=command, git=True, names=remote_names, refs=refs, heads=heads, ) filteredrefs = git2hg.filter_refs(self.filter_min_date(refs), heads) commits = self.get_git_incoming(filteredrefs, remote_names) # import each of the commits, oldest first total = len(commits) if total: self.ui.status(_(b"importing %d git commits\n") % total) else: self.ui.status(_(b"no changes found\n")) # don't bother saving the map if we're in a clone, as Mercurial # deletes the repository on errors if self.is_clone: mapsavefreq = 0 else: mapsavefreq = self.ui.configint(b'hggit', b'mapsavefrequency') chunksize = max(mapsavefreq or total, 1) progress = self.ui.makeprogress( b'importing', unit=b'commits', total=total ) self.ui.note(b"processing commits in batches of %d\n" % chunksize) with progress, self.repo.lock(): # the weird range below speeds up conversion by batching # commits in a transaction, while ensuring that we always # get at least one chunk for offset in range(0, max(total, 1), chunksize): with self.get_transaction(b"gimport"): cl = self.repo.unfiltered().changelog oldtiprev = cl.tiprev() for commit in commits[offset : offset + chunksize]: progress.increment(item=commit.short) self.import_git_commit( command, self.git[commit.sha], commit.phase, ) lastrev = cl.tiprev() self.import_tags(refs) self.update_hg_bookmarks(remote_names, refs) self.update_remote_branches(remote_names, refs) if oldtiprev != lastrev: first = cl.node(oldtiprev + 1) last = cl.node(lastrev) self.repo.hook( b"changegroup", source=b'push', git=True, node=hex(first), node_last=hex(last), ) # TODO if the tags cache is used, remove any dangling tag references return total def import_git_commit(self, command, commit, phase): self.ui.debug(_(b"importing: %s\n") % commit.id) unfiltered = self.repo.unfiltered() detect_renames = False ( strip_message, hg_renames, hg_branch, extra, ) = git2hg.extract_hg_metadata(commit.message or b'', commit._extra) if hg_renames is None: detect_renames = True # We have to store this unconditionally, even if there are no # renames detected from Git. This is because we export an extra # 'HG:rename-source' Git parameter when this isn't set, which will # break bidirectionality. extra[b'hg-git-rename-source'] = b'git' else: renames = hg_renames gparents = pycompat.maplist(self.map_hg_get, commit.parents) for parent in gparents: if parent not in unfiltered: raise error.Abort( _( b'you appear to have run strip - ' b'please run hg git-cleanup' ) ) # get a list of the changed, added, removed files and gitlinks files, gitlinks, git_renames = self.get_files_changed( commit, detect_renames ) if detect_renames: renames = git_renames git_commit_tree = self.git[commit.tree] # Analyze hgsubstate and build an updated version using SHAs from # gitlinks. Order of application: # - preexisting .hgsubstate in git tree # - .hgsubstate from hg parent # - changes in gitlinks hgsubstate = util.parse_hgsubstate( git2hg.git_file_readlines(self.git, git_commit_tree, b'.hgsubstate') ) parentsubdata = b'' if gparents: p1ctx = unfiltered[gparents[0]] if b'.hgsubstate' in p1ctx: parentsubdata = p1ctx.filectx(b'.hgsubstate').data() parentsubdata = parentsubdata.splitlines() parentsubstate = util.parse_hgsubstate(parentsubdata) for path, sha in parentsubstate.items(): hgsubstate[path] = sha for path, sha in gitlinks.items(): if sha is None: hgsubstate.pop(path, None) else: hgsubstate[path] = sha # in case .hgsubstate wasn't among changed files # force its inclusion if it wasn't already deleted hgsubdeleted = files.get(b'.hgsubstate') if hgsubdeleted: hgsubdeleted = hgsubdeleted[0] if hgsubdeleted or (not hgsubstate and parentsubdata): files[b'.hgsubstate'] = True, None, None elif util.serialize_hgsubstate(hgsubstate) != parentsubdata: files[b'.hgsubstate'] = False, 0o100644, None # Analyze .hgsub and merge with .gitmodules hgsub = None try: gitmodules = git2hg.parse_gitmodules(self.git, git_commit_tree) except KeyError: gitmodules = None except ValueError: self.ui.traceback() self.ui.warn( b'warning: failed to parse .gitmodules in %s\n' % commit.id[:12], ) gitmodules = None if gitmodules is not None: hgsub = util.parse_hgsub( git2hg.git_file_readlines(self.git, git_commit_tree, b'.hgsub') ) for sm_path, sm_url, sm_name in gitmodules: hgsub[sm_path] = b'[git]' + sm_url for path in hgsubstate.keys() - hgsub.keys(): del hgsubstate[path] files[b'.hgsub'] = (False, 0o100644, None) files.pop(b'.gitmodules', None) elif ( commit.parents and b'.gitmodules' in self.git[self.git[commit.parents[0]].tree] ): # no .gitmodules in this commit, however present in the parent # mark its hg counterpart as deleted (assuming .hgsub is there # due to the same import_git_commit process files[b'.hgsub'] = (True, 0o100644, None) date = (commit.author_time, -commit.author_timezone) text = strip_message origtext = text try: text.decode('utf-8') except UnicodeDecodeError: text = util.decode_guess(text, commit.encoding) text = b'\n'.join(l.rstrip() for l in text.splitlines()).strip(b'\n') if text + b'\n' != origtext: extra[b'message'] = util.create_delta(text + b'\n', origtext) author = commit.author # convert extra data back to the end if b' ext:' in commit.author: m = RE_GIT_AUTHOR_EXTRA.match(commit.author) if m: name = m.group(1) ex = hgutil.urlreq.unquote(m.group(2)) email = m.group(3) author = name + b' <' + email + b'>' + ex if b' ' in commit.author: author = commit.author[:-12] try: author.decode('utf-8') except UnicodeDecodeError: origauthor = author author = util.decode_guess(author, commit.encoding) extra[b'author'] = util.create_delta(author, origauthor) oldenc = util.swap_out_encoding() def findconvergedfiles(p1, p2): # If any files have the same contents in both parents of a merge # (and are therefore not reported as changed by Git) but are at # different file revisions in Mercurial (because they arrived at # those contents in different ways), we need to include them in # the list of changed files so that Mercurial can join up their # filelog histories (same as if the merge was done in Mercurial to # begin with). if p2 == nullid: return [] manifest1 = unfiltered[p1].manifest() manifest2 = unfiltered[p2].manifest() return [ path for path, node1 in manifest1.items() if path not in files and manifest2.get(path, node1) != node1 ] def getfilectx(repo, memctx, f): info = files.get(f) if info is not None: # it's a file reported as modified from Git delete, mode, sha = info if delete: return None if not sha: # indicates there's no git counterpart e = b'' copied_path = None if b'.hgsubstate' == f: data = util.serialize_hgsubstate(hgsubstate) elif b'.hgsub' == f: data = util.serialize_hgsub(hgsub) else: data = self.git[sha].data copied_path = renames.get(f) e = git2hg.convert_git_int_mode(mode) else: # it's a converged file fc = context.filectx(unfiltered, f, changeid=memctx.p1().rev()) data = fc.data() e = fc.flags() copied_path = None copied = fc.renamed() if copied: copied_path = copied[0] return context.memfilectx( unfiltered, memctx, f, data, islink=b'l' in e, isexec=b'x' in e, copysource=copied_path, ) p1, p2 = (nullid, nullid) octopus = False if len(gparents) > 1: # merge, possibly octopus def commit_octopus(p1, p2): ctx = context.memctx( unfiltered, (p1, p2), text, list(files) + findconvergedfiles(p1, p2), getfilectx, author, date, {b'hg-git': b'octopus'}, ) # See comment below about setting substate to None. ctx.substate = None with util.forcedraftcommits(): return hex(unfiltered.commitctx(ctx)) octopus = len(gparents) > 2 p2 = gparents.pop() p1 = gparents.pop() while len(gparents) > 0: p2 = commit_octopus(p1, p2) p1 = gparents.pop() else: if gparents: p1 = gparents.pop() # if named branch, add to extra if hg_branch: extra[b'branch'] = hg_branch else: extra[b'branch'] = b'default' # if committer is different than author, add it to extra if ( commit.author != commit.committer or commit.author_time != commit.commit_time or commit.author_timezone != commit.commit_timezone ): extra[b'committer'] = b"%s %d %d" % ( commit.committer, commit.commit_time, -commit.commit_timezone, ) if commit.encoding: extra[b'encoding'] = commit.encoding if commit.gpgsig: extra[b'gpgsig'] = commit.gpgsig if octopus: extra[b'hg-git'] = b'octopus-done' ctx = context.memctx( unfiltered, (p1, p2), text, list(files) + findconvergedfiles(p1, p2), getfilectx, author, date, extra, ) # Starting Mercurial commit d2743be1bb06, memctx imports from # committablectx. This means that it has a 'substate' property that # contains the subrepo state. Ordinarily, Mercurial expects the subrepo # to be present while making a new commit -- since hg-git is importing # purely in-memory commits without backing stores for the subrepos, # that won't work. Forcibly set the substate to None so that there's no # attempt to read subrepos. ctx.substate = None with util.forcedraftcommits(): node = unfiltered.commitctx(ctx) util.swap_out_encoding(oldenc) with self.repo.lock(), self.repo.transaction(b"phase") as tr: phases.advanceboundary( self.repo, tr, phase, [node], ) # save changeset to mapping file cs = hex(node) self.map_set(commit.id, cs) self.repo.hook( b'incoming', git=True, source=command, node=cs, git_node=commit.id, ) # PACK UPLOADING AND FETCHING def upload_pack(self, remote, revs, bookmarks, force): if bookmarks and self.branch_bookmark_suffix: raise error.Abort( b"the -B/--bookmarks option is not supported when " b"branch_bookmark_suffix is set", ) all_exportable = self.export_commits() old_refs = {} change_totals = {} def changed(refs): self.ui.status(_(b"searching for changes\n")) old_refs.update(refs) if revs is None: exportable = all_exportable else: exportable = {} for rev in (hex(r) for r in revs): if rev == nullhex: # a deletion exportable[rev] = heads_tags( heads={ LOCAL_BRANCH_PREFIX + bm for bm in bookmarks if bm not in self.repo._bookmarks } ) elif rev not in all_exportable: raise error.Abort( b"revision %s cannot be pushed since" b" it doesn't have a bookmark" % self.repo[rev] ) elif bookmarks: # we should only push the listed bookmarks, # and not any other bookmarks that might point # to the same changeset exportable[rev] = heads_tags( heads=all_exportable[rev].heads & {LOCAL_BRANCH_PREFIX + bm for bm in bookmarks}, ) else: exportable[rev] = all_exportable[rev] changes = self.get_changed_refs(refs, exportable, force) self.repo.hook( b"prechangegroup", source=b'push', git=True, url=remote, changes=changes, ) return changes def genpack(have, want, progress=None, ofs_delta=True): commits = [] with util.abort_push_on_keyerror(): for sha, name in object_store.MissingObjectFinder( self.git.object_store, have, want, progress=progress, ): o = self.git.object_store[sha] t = type(o) change_totals[t] = change_totals.get(t, 0) + 1 if isinstance(o, Commit): commits.append(sha) self.repo.hook( b"outgoing", source=b'push', git=True, url=remote, node=self.map_hg_get(sha), git_node=sha, ) commit_count = len(commits) self.ui.note(_(b"%d commits found\n") % commit_count) if commit_count > 0: self.ui.debug(_(b"list of commits:\n")) for commit in commits: self.ui.debug(b"%s\n" % commit) self.ui.status(_(b"adding objects\n")) return self.git.object_store.generate_pack_data( have, want, progress=progress or progressfunc, ofs_delta=ofs_delta, ) progress = GitProgress(self.ui) progressfunc = progress.progress try: new_refs = self._call_client( remote, 'send_pack', changed, genpack, progress=progressfunc ) if len(change_totals) > 0: self.ui.status( _(b"added %d commits with %d trees" b" and %d blobs\n") % ( change_totals.get(Commit, 0), change_totals.get(Tree, 0), change_totals.get(Blob, 0), ) ) return old_refs, new_refs except (HangupException, GitProtocolError) as e: raise error.Abort( _(b"git remote error: ") + pycompat.sysbytes(str(e)) ) finally: progress.flush() def get_changed_refs(self, refs, exportable, force): new_refs = refs.copy() if not any(exportable.values()): raise error.Abort( b'no bookmarks or tags to push to git', hint=b'see "hg help bookmarks" for details on creating them', ) # mapped nodes might be hidden unfiltered = self.repo.unfiltered() for rev, rev_refs in exportable.items(): ctx = self.repo[rev] # Check if the tags the server is advertising are annotated tags, # by attempting to retrieve it from the our git repo, and building # a list of these tags. # # This is possible, even though (currently) annotated tags are # dereferenced and stored as lightweight ones, as the annotated tag # is still stored in the git repo. uptodate_annotated_tags = [] for ref in rev_refs.tags: # Check tag. if ref not in refs: continue try: # We're not using Repo.tag(), as it's deprecated. tag = self.git.get_object(refs[ref]) if not isinstance(tag, Tag): continue except KeyError: continue # If we've reached here, the tag's good. uptodate_annotated_tags.append(ref) for ref in rev_refs: if ctx.node() == nullid: if ref not in new_refs: # this is reasonably consistent with # mercurial; git aborts with an error in this # case self.ui.warn( b"warning: unable to delete '%s' as it does not " b"exist on the remote repository\n" % ref, ) else: new_refs[ref] = nullhex elif ref not in refs and util.ref_exists(ref, self.git.refs): if ref not in self.git.refs: self.ui.note( b'note: cannot update %s\n' % (ref), ) else: gitobj = self.git.get_object(self.git.refs[ref]) if isinstance(gitobj, Tag): new_refs[ref] = gitobj.id else: new_refs[ref] = self.map_git_get(ctx.hex()) elif ref not in new_refs: new_refs[ref] = self.map_git_get(rev) elif new_refs[ref] in self._map_git: rctx = unfiltered[self.map_hg_get(new_refs[ref])] if rctx.ancestor(ctx) == rctx or force: new_refs[ref] = self.map_git_get(ctx.hex()) else: raise error.Abort( b"pushing %s overwrites %s" % (ref, ctx) ) elif ref in uptodate_annotated_tags: # we already have the annotated tag. pass else: raise error.Abort( b"branch '%s' changed on the server, " b"please pull and merge before pushing" % ref ) return new_refs def fetch_pack(self, remote, heads=None): # The dulwich default walk only checks refs/heads/. We also want to # consider remotes when doing discovery, so we build our own list. We # can't just do 'refs/' here because the tag class doesn't have a # parents function for walking, and older versions of dulwich don't # like that. haveheads = list(self.git.refs.as_dict(REMOTE_BRANCH_PREFIX).values()) haveheads.extend(self.git.refs.as_dict(LOCAL_BRANCH_PREFIX).values()) graphwalker = self.git.get_graph_walker(heads=haveheads) def determine_wants(refs): if refs is None: return None filteredrefs = git2hg.filter_refs(refs, heads) # equivalent to the `--tags` option to `git pull`; always # pull tags pointing to known revisions, including # annotated tags for ref, sha in refs.items(): if ref.endswith(ANNOTATED_TAG_SUFFIX) and sha in self._map_git: actual_ref = ref[: -len(ANNOTATED_TAG_SUFFIX)] filteredrefs.setdefault(actual_ref, refs[actual_ref]) return [x for x in filteredrefs.values() if x not in self.git] progress = GitProgress(self.ui) try: with util.add_pack(self.git.object_store) as f: ret = self._call_client( remote, 'fetch_pack', determine_wants, graphwalker, f.write, progress.progress, ) # For empty repos dulwich gives us None, but since later # we want to iterate over this, we really want an empty # iterable if ret is None: ret = {} return ret except (HangupException, GitProtocolError) as e: raise error.Abort( _(b"git remote error: ") + pycompat.sysbytes(str(e)) ) finally: progress.flush() def _call_client(self, remote, method, *args, **kwargs): if not isinstance(remote, bytes): remote = remote.loc if remote in self._clients: clientobj, path = self._clients[remote] return getattr(clientobj, method)(path, *args, **kwargs) for ignored in range(self.ui.configint(b'hggit', b'retries')): clientobj, path = self._get_transport_and_path(remote) func = getattr(clientobj, method) try: ret = func(path, *args, **kwargs) # it worked, so save the client for later! self._clients[remote] = clientobj, path return ret except (HTTPUnauthorized, GitProtocolError) as e: self.ui.traceback() if isinstance(e, HTTPUnauthorized): # this is a fallback just in case the header isn't # specified self._http_auth_realm = 'Git' if e.www_authenticate: m = re.search(r'realm="([^"]*)"', e.www_authenticate) if m: self._http_auth_realm = m.group(1) elif 'unexpected http resp 407' in e.args[0]: raise error.Abort( b'HTTP proxy requires authentication', ) else: raise raise error.Abort(_(b'authorization failed')) # REFERENCES HANDLING def filter_min_date(self, refs): '''filter refs by minimum date This only works for refs that are available locally.''' min_date = self.ui.config(b'git', b'mindate') if min_date is None: return refs # filter refs older than min_timestamp min_timestamp, min_offset = dateutil.parsedate(min_date) def check_min_time(obj): if isinstance(obj, Tag): return obj.tag_time >= min_timestamp else: return obj.commit_time >= min_timestamp return collections.OrderedDict( (ref, sha) for ref, sha in refs.items() if check_min_time(self.git[sha]) ) def update_references(self): exportable = self.get_exportable() new_refs = {} # Create a local Git branch name for each # Mercurial bookmark. for hg_sha, refs in exportable.items(): for git_ref in refs.heads: git_sha = self.map_git_get(hg_sha) # prior to 0.20.22, dulwich couldn't handle refs # pointing to missing objects, so don't add them if git_sha and git_sha in self.git: new_refs[git_ref] = git_sha self.git.refs.add_packed_refs(new_refs) return exportable def export_hg_tags(self): new_refs = {} for tag, (sha, hist) in tagsmod.findglobaltags( self.ui, self.repo ).items(): tag = tag.replace(b' ', b'_') target = self.map_git_get(hex(sha)) if target is None: self.repo.ui.warn( b"warning: not exporting tag '%s' " b"due to missing git " b"revision\n" % tag ) continue tag_refname = LOCAL_TAG_PREFIX + tag if not check_ref_format(tag_refname): self.repo.ui.warn( b"warning: not exporting tag '%s' " b"due to invalid name\n" % tag ) continue # check whether the tag already exists and is # annotated if util.ref_exists(tag_refname, self.git.refs): reftarget = self.git.refs[tag_refname] try: gitobj = self.git.get_object(reftarget) except KeyError: self.ui.note(b'note: failed to peel tag %s' % (tag_refname)) gitobj = None if isinstance(gitobj, Tag): self.repo.ui.warn( b"warning: not overwriting annotated " b"tag '%s'\n" % tag ) # and never overwrite annotated tags, # otherwise it'd happen on every pull target = reftarget new_refs[tag_refname] = target self.tags[tag] = hex(sha) self.git.refs.add_packed_refs(new_refs) def get_filtered_bookmarks(self): bms = self.repo._bookmarks if not self.branch_bookmark_suffix: return [(bm, bm, n) for bm, n in bms.items()] else: def _filter_bm(bm): if bm.endswith(self.branch_bookmark_suffix): return bm[0 : -(len(self.branch_bookmark_suffix))] else: return bm return [(_filter_bm(bm), bm, n) for bm, n in bms.items()] def get_exportable(self): res = collections.defaultdict(heads_tags) for filtered_bm, bm, node in self.get_filtered_bookmarks(): ref_name = LOCAL_BRANCH_PREFIX + filtered_bm if node not in self.repo.filtered(b'served'): # technically, we don't _know_ that it's secret, # but it's a very good guess self.repo.ui.warn( b"warning: not exporting secret bookmark '%s'\n" % bm ) elif check_ref_format(ref_name): res[hex(node)].heads.add(ref_name) else: self.repo.ui.warn( b"warning: not exporting bookmark '%s' " b"due to invalid name\n" % bm ) for tag, sha in self.tags.items(): res[sha].tags.add(LOCAL_TAG_PREFIX + tag) return res def import_tags(self, refs): if not refs: return repotags = self.repo.tags() new_refs = {} for k in refs: if k.endswith(ANNOTATED_TAG_SUFFIX) or not k.startswith( LOCAL_TAG_PREFIX ): continue ref_name = k[len(LOCAL_TAG_PREFIX) :] # refs contains all the refs in the server, not just # the ones we are pulling if refs[k] not in self.git.object_store: continue new_refs[k] = refs[k] if ref_name not in repotags: sha = self.map_hg_get(refs[k], deref=True) if sha is not None and sha is not None: self.tags[ref_name] = sha self.git.refs.add_packed_refs(new_refs) self.save_tags() def add_tag(self, target, *tags): for tag in tags: scmutil.checknewlabel(self.repo, tag, b'tag') # -f/--force is deliberately unimplemented and unmentioned # as its git semantics are quite confusing if scmutil.isrevsymbol(self.repo, tag): raise error.Abort(b"the name '%s' already exists" % tag) if not check_ref_format(LOCAL_TAG_PREFIX + tag): raise error.Abort( b"the name '%s' is not a valid git " b"tag" % tag ) self.export_commits() gittarget = self.map_git_get(target) if not gittarget: raise error.Abort( b"warning: cannot create tag '%s' due to missing git " b"revision\n" % tag ) self.ui.debug(b'adding git tag %s\n' % tag) self.git.refs.add_packed_refs( {LOCAL_TAG_PREFIX + tag: gittarget for tag in tags} ) self.tags.update({tag: target for tag in tags}) self.save_tags() def _get_ref_nodes(self, remote_names, refs): """get a {ref_name → node} mapping We generally assume that `refs` contains all the refs in the server, not just the ones we are pulling. Please note that this function returns binary node ids. A node ID of `nullid` means that the commit isn't present locally; `None` means that the branch was deleted. """ ref_nodes = {} for ref, git_sha in refs.items(): if not ref.startswith(LOCAL_BRANCH_PREFIX): continue h = ref[len(LOCAL_BRANCH_PREFIX) :] hg_sha = self.map_hg_get(git_sha) # refs contains all the refs in the server, # not just the ones we are pulling ref_nodes[h] = bin(hg_sha) if hg_sha is not None else nullid # detect deletions; do this last to retain ordering if self.ui.configbool(b'git', b'pull-prune-bookmarks'): for remote_name in remote_names: prefix = remote_name + b'/' for remote_ref in self.remote_refs: if remote_ref.startswith(prefix): h = remote_ref[len(prefix) :] ref_nodes.setdefault(h, None) return ref_nodes def update_hg_bookmarks(self, remote_names, refs): bms = self.repo._bookmarks unfiltered = self.repo.unfiltered() changes = [] for ref_name, wanted_node in self._get_ref_nodes( remote_names, refs ).items(): bm = ref_name + (self.branch_bookmark_suffix or b'') current_node = bms.get(bm) if current_node is not None and current_node == wanted_node: self.ui.note(_(b"bookmark %s is up-to-date\n") % bm) elif wanted_node == nullid: self.ui.note(_(b"bookmark %s is not known yet\n") % bm) elif wanted_node is None and current_node is None: self.ui.note(b"bookmark %s is deleted locally as well\n" % bm) elif wanted_node is None: # possibly deleted branch, check if we have a # matching remote ref unmoved = any( self.remote_refs.get(b'%s/%s' % (remote_name, ref_name)) == current_node for remote_name in remote_names ) # only delete unmoved bookmarks if unmoved: changes.append((bm, None)) self.ui.status(_(b"deleting bookmark %s\n") % bm) else: self.ui.status(b"not deleting diverged bookmark %s\n" % bm) elif current_node is None: # new branch changes.append((bm, wanted_node)) # only log additions on subsequent pulls if not self.is_clone: self.ui.status(_(b"adding bookmark %s\n") % bm) elif unfiltered[current_node].isancestorof(unfiltered[wanted_node]): # fast forward changes.append((bm, wanted_node)) self.ui.status(_(b"updating bookmark %s\n") % bm) elif unfiltered.obsstore and wanted_node in obsutil.foreground( unfiltered, [current_node] ): # this is fast-forward or a rebase, across # obsolescence markers too. (ideally we would have # a background thingy that is more efficient that # the foreground one.) changes.append((bm, wanted_node)) self.ui.status(_(b"updating bookmark %s\n") % bm) else: self.ui.status( _(b"not updating diverged bookmark %s\n") % bm, ) if changes: with self.repo.wlock(), self.repo.lock(): with self.repo.transaction(b"hg-git") as tr: bms.applychanges(self.repo, tr, changes) def _update_remote_branches_for(self, remote_name, refs): remote_refs = self.remote_refs new_refs = {} if self.ui.configbool(b'git', b'pull-prune-remote-branches'): # since we re-write all refs for this remote each time, # prune all entries matching this remote from our refs # list now so that we avoid any stale refs hanging around # forever for t in list(remote_refs): if t.startswith(remote_name + b'/'): del remote_refs[t] if ( LOCAL_BRANCH_PREFIX + t[len(remote_name) + 1 :] not in refs ): new_refs[REMOTE_BRANCH_PREFIX + t] = None for ref_name, sha in refs.items(): if ( ref_name.endswith(ANNOTATED_TAG_SUFFIX) or sha not in self.git.object_store ): # the sha points to a peeled tag; we should either # pick it up through the tag itself, or ignore it -- # relatedly, we can sometimes pull the commit an # annotated tag points to, without the tag itself continue hgsha = self.map_hg_get(sha, deref=True) if ( ref_name.startswith(LOCAL_BRANCH_PREFIX) and hgsha is not None and hgsha in self.repo ): head = ref_name[len(LOCAL_BRANCH_PREFIX) :] remote_head = b'/'.join((remote_name, head)) # actually update the remote ref remote_refs[remote_head] = bin(hgsha) new_ref = REMOTE_BRANCH_PREFIX + remote_head new_refs[new_ref] = sha self.git.refs.add_packed_refs(new_refs) def update_remote_branches(self, remote_names, refs): for remote_name in remote_names: self._update_remote_branches_for(remote_name, refs) with self.repo.lock(), self.repo.transaction(b"hg-git-phases") as tr: all_remote_nodeids = set() for ref_name, sha in refs.items(): hgsha = self.map_hg_get(sha) if hgsha: all_remote_nodeids.add(bin(hgsha)) # sanity check: ensure that all corresponding commits # are at least draft; this can happen on no-op pulls # where the commit already exists, but is secret phases.advanceboundary( self.repo, tr, phases.draft, all_remote_nodeids, ) # ensure that we update phases on push and no-op pulls nodeids_to_publish = set() for sha in git2hg.get_public(self.ui, refs, remote_names): hgsha = self.map_hg_get(sha, deref=True) if hgsha: nodeids_to_publish.add(bin(hgsha)) phases.advanceboundary( self.repo, tr, phases.public, nodeids_to_publish, ) # UTILITY FUNCTIONS def get_file(self, commit, f): otree = self.git.tree(commit.tree) parts = f.split(b'/') for part in parts: (mode, sha) = otree[part] obj = self.git.get_object(sha) if isinstance(obj, Blob): return (mode, sha, obj._text) elif isinstance(obj, Tree): otree = obj def get_files_changed(self, commit, detect_renames): tree = commit.tree btree = None if commit.parents: btree = self.git[commit.parents[0]].tree files = {} gitlinks = {} renames = None rename_detector = None if detect_renames: renames = {} rename_detector = self._rename_detector # this set is unused if rename detection isn't enabled -- that makes # the code below simpler renamed_out = set() changes = diff_tree.tree_changes( self.git.object_store, btree, tree, rename_detector=rename_detector ) for change in changes: oldfile, oldmode, oldsha = change.old newfile, newmode, newsha = change.new # actions are described by the following table ('no' means 'does # not exist'): # old new | action # no file | record file # no gitlink | record gitlink # file no | delete file # file file | record file # file gitlink | delete file and record gitlink # gitlink no | delete gitlink # gitlink file | delete gitlink and record file # gitlink gitlink | record gitlink # # There's an edge case here -- symlink <-> regular file transitions # are returned by dulwich as separate deletes and adds, not # modifications. The order of those results is unspecified and # could be either way round. Handle both cases: delete first, then # add -- delete stored in 'old = file' case, then overwritten by # 'new = file' case. add first, then delete -- record stored in # 'new = file' case, then membership check fails in 'old = file' # case so is not overwritten there. This is not an issue for # gitlink <-> {symlink, regular file} transitions because they # write to separate dictionaries. # # There's a similar edge case when rename detection is enabled: if # a file is renamed and then replaced by a symlink (typically to # the new location), it is returned by dulwich as an add and a # rename. The order of those results is unspecified. Handle both # cases: rename first, then add -- delete stored in 'new = file' # case with renamed_out, then renamed_out check passes in 'old = # file' case so is overwritten. add first, then rename -- add # stored in 'old = file' case, then membership check fails in 'new # = file' case so is overwritten. if newmode == 0o160000: if not self.audit_hg_path(newfile): # disregard illegal or inconvenient paths continue # new = gitlink gitlinks[newfile] = newsha if change.type == diff_tree.CHANGE_RENAME: # don't record the rename because only file -> file renames # make sense in Mercurial gitlinks[oldfile] = None if oldmode is not None and oldmode != 0o160000: # file -> gitlink files[oldfile] = True, None, None continue if oldmode == 0o160000 and newmode != 0o160000: # gitlink -> no/file (gitlink -> gitlink is covered above) gitlinks[oldfile] = None continue if newfile is not None: if not self.audit_hg_path(newfile): continue # new = file files[newfile] = False, newmode, newsha if renames is not None and newfile != oldfile: renames[newfile] = oldfile renamed_out.add(oldfile) # the membership check is explained in a comment above if ( change.type == diff_tree.CHANGE_RENAME and oldfile not in files ): files[oldfile] = True, None, None else: # old = file # files renamed_out | action # no * | write # yes no | ignore # yes yes | write if oldfile not in files or oldfile in renamed_out: files[oldfile] = True, None, None return files, gitlinks, renames @hgutil.propertycache def _rename_detector(self): # disabled by default to avoid surprises similarity = self.ui.configint(b'git', b'similarity') if similarity < 0 or similarity > 100: raise error.Abort(_(b'git.similarity must be between 0 and 100')) if similarity == 0: return None # default is borrowed from Git max_files = self.ui.configint(b'git', b'renamelimit') if max_files < 0: raise error.Abort(_(b'git.renamelimit must be non-negative')) if max_files == 0: max_files = None find_copies_harder = self.ui.configbool(b'git', b'findcopiesharder') return diff_tree.RenameDetector( self.git.object_store, rename_threshold=similarity, max_files=max_files, find_copies_harder=find_copies_harder, ) def remote_names(self, remote, push): if self.is_clone: return [b'default'] if not isinstance(remote, bytes): if remote.name is not None: return [remote.name] url = remote.url else: url = urlutil.url(remote) # we actually get the unexpand path; triggered by # test-branch-bookmark-suffix.t if url.islocal() and not url.isabs(): url = urlutil.url(os.path.normpath(self.repo.wvfs.join(remote))) names = set() if url.islocal() and not url.isabs(): remote = os.path.abspath(url.localpath()) for name, paths in self.ui.paths.items(): if name is None: continue for path in paths: # ignore aliases if hasattr(path, 'raw_url') and path.raw_url.scheme == b'path': continue if push: path = path.get_push_variant() if bytes(path.url) == bytes(url): names.add(name) return list(names) def audit_hg_path(self, path): if b'.hg' in path.split(b'/') or b'\r' in path or b'\n' in path: ui = self.ui # escape the path when printing it out prettypath = path.decode('latin1').encode('unicode-escape') opt = ui.config(b'hggit', b'invalidpaths') if opt == b'abort': raise error.Abort( b"invalid path '%s' rejected by configuration" % prettypath, hint=b"see 'hg help config.hggit.invalidpaths for details", ) elif opt == b'keep' and b'\r' not in path and b'\n' not in path: ui.warn( b"warning: path '%s' contains an invalid path component\n" % prettypath, ) return True else: # undocumented: just let anything else mean "skip" ui.warn(b"warning: skipping invalid path '%s'\n" % prettypath) return False return True def _get_transport_and_path(self, uri): """Method that sets up the transport (either ssh or http(s)) Tests: >>> from dulwich.client import HttpGitClient, SSHGitClient >>> from mercurial import ui >>> class SubHandler(GitHandler): ... def __init__(self): ... self.ui = ui.ui() ... self._http_auth_realm = None ... self._pwmgr = url.passwordmgr( ... self.ui, self.ui.httppasswordmgrdb, ... ) >>> tp = SubHandler()._get_transport_and_path >>> client, url = tp(b'http://fqdn.com/test.git') >>> print(isinstance(client, HttpGitClient)) True >>> print(url.decode()) http://fqdn.com/test.git >>> client, url = tp(b'c:/path/to/repo.git') >>> print(isinstance(client, SSHGitClient)) False >>> client, url = tp(b'git@fqdn.com:user/repo.git') >>> print(isinstance(client, SSHGitClient)) True >>> print(url.decode()) user/repo.git >>> print(client.host) git@fqdn.com """ kwargs = dict( include_tags=True, ) # pass hg's ui.ssh config to dulwich if not issubclass(client.get_ssh_vendor, _ssh.SSHVendor): client.get_ssh_vendor = _ssh.generate_ssh_vendor(self.ui) # test for raw git ssh uri here so that we can reuse the logic below if util.isgitsshuri(uri): uri = b"git+ssh://" + uri git_match = RE_GIT_URI.match(uri) if git_match: res = git_match.groupdict() host, port, sepr = res['host'], res['port'], res['sepr'] transport = client.TCPGitClient if b'ssh' in res['scheme']: util.checksafessh(pycompat.bytesurl(host)) transport = client.SSHGitClient path = res['path'] if sepr == b'/' and not path.startswith(b'~'): path = b'/' + path # strip trailing slash for heroku-style URLs # ssh+git://git@heroku.com:project.git/ if sepr == b':' and path.endswith(b'.git/'): path = path.rstrip(b'/') if port: client.port = port return transport(pycompat.strurl(host), port=port, **kwargs), path if uri.startswith(b'git+http://') or uri.startswith(b'git+https://'): uri = uri[4:] if uri.startswith(b'http://') or uri.startswith(b'https://'): ua = b'git/20x6 (hg-git ; uses dulwich and hg ; like git-core)' config = dul_config.ConfigDict() config.set(b'http', b'useragent', ua) proxy = self.ui.config(b'http_proxy', b'host') if proxy: config.set(b'http', b'proxy', b'http://' + proxy) if self.ui.config(b'http_proxy', b'passwd'): self.ui.warn( b"warning: proxy authentication is unsupported\n", ) str_uri = uri.decode('utf-8') urlobj = urlutil.url(uri) auth = client.get_credentials_from_store( urlobj.scheme, urlobj.host, urlobj.user, ) if self._http_auth_realm: # since we've tried an unauthenticated request, and # obtain a realm, we can do a "full" search, including # a prompt username, password = self._pwmgr.find_user_password( self._http_auth_realm, str_uri, ) # NB: probably bytes here elif auth is not None: username, password = auth # NB: probably string here else: username, password = self._pwmgr.find_stored_password(str_uri) # NB: probably string here if isinstance(username, bytes): username = username.decode('utf-8') if isinstance(password, bytes): password = password.decode('utf-8') return ( client.HttpGitClient( str_uri, config=config, username=username, password=password, **kwargs, ), uri, ) if uri.startswith(b'file://'): # the local Git client doesn't support include_tags, and # report_activity doesn't make sense, so just drop it return client.LocalGitClient(), urlutil.url(uri).path # if its not git or git+ssh, try a local url.. return client.SubprocessGitClient(**kwargs), uri ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/gitdirstate.py0000644000000000000000000002167314751647721013645 0ustar00import os import stat import re import errno from mercurial import ( dirstate, error, exthelper, match as matchmod, pathutil, pycompat, util, ) from mercurial.i18n import _ from . import git_handler from . import gitrepo eh = exthelper.exthelper() def gignorepats(orig, lines, root=None): '''parse lines (iterable) of .gitignore text, returning a tuple of (patterns, parse errors). These patterns should be given to compile() to be validated and converted into a match function.''' syntaxes = {b're': b'relre:', b'regexp': b'relre:', b'glob': b'relglob:'} syntax = b'glob:' patterns = [] warnings = [] for line in lines: if b"#" in line: _commentre = re.compile(br'((^|[^\\])(\\\\)*)#.*') # remove comments prefixed by an even number of escapes line = _commentre.sub(br'\1', line) # fixup properly escaped comments that survived the above line = line.replace(b"\\#", b"#") line = line.rstrip() if not line: continue if line.startswith(b'!'): warnings.append(_(b"unsupported ignore pattern '%s'") % line) continue if re.match(br'(:?.*/)?\.hg(:?/|$)', line): continue rootprefix = b'%s/' % root if root else b'' if line and line[0] in br'\/': line = line[1:] rootsuffixes = [b''] else: rootsuffixes = [b'', b'**/'] for rootsuffix in rootsuffixes: pat = syntax + rootprefix + rootsuffix + line for s, rels in syntaxes.items(): if line.startswith(rels): pat = line break elif line.startswith(s + b':'): pat = rels + line[len(s) + 1 :] break patterns.append(pat) return patterns, warnings def gignore(root, files, ui, extrapatterns=None): allpats = [] pats = [(f, [b'include:%s' % f]) for f in files] for f, patlist in pats: allpats.extend(patlist) if extrapatterns: allpats.extend(extrapatterns) if not allpats: return util.never try: ignorefunc = matchmod.match(root, b'', [], allpats) except error.Abort: ui.traceback() for f, patlist in pats: matchmod.match(root, b'', [], patlist) if extrapatterns: matchmod.match(root, b'', [], extrapatterns) return ignorefunc class gitdirstate(dirstate.dirstate): @dirstate.rootcache(b'.hgignore') def _ignore(self): files = [self._join(b'.hgignore')] for name, path in self._ui.configitems(b"ui"): if name == b'ignore' or name.startswith(b'ignore.'): files.append(util.expandpath(path)) patterns = [] # Only use .gitignore if there's no .hgignore if not os.access(files[0], os.R_OK): for fn in self._finddotgitignores(): d = os.path.dirname(fn) fn = self.pathto(fn) if not os.path.exists(fn): continue fp = open(fn, 'rb') pats, warnings = gignorepats(None, fp, root=d) for warning in warnings: self._ui.warn(b"%s: %s\n" % (fn, warning)) patterns.extend(pats) return gignore(self._root, files, self._ui, extrapatterns=patterns) def _finddotgitignores(self): """A copy of dirstate.walk. This is called from the new _ignore method, which is called by dirstate.walk, which would cause infinite recursion, except _finddotgitignores calls the superclass _ignore directly.""" match = matchmod.match( self._root, self.getcwd(), [b'relglob:.gitignore'] ) # TODO: need subrepos? subrepos = [] unknown = True ignored = False def fwarn(f, msg): self._ui.warn( b'%s: %s\n' % ( self.pathto(f), pycompat.sysbytes(msg), ) ) return False ignore = super()._ignore dirignore = self._dirignore if ignored: ignore = util.never dirignore = util.never elif not unknown: # if unknown and ignored are False, skip step 2 ignore = util.always dirignore = util.always matchfn = match.matchfn matchalways = match.always() matchtdir = match.traversedir dmap = self._map lstat = os.lstat dirkind = stat.S_IFDIR regkind = stat.S_IFREG lnkkind = stat.S_IFLNK join = self._join exact = skipstep3 = False if matchfn == match.exact: # match.exact exact = True dirignore = util.always # skip step 2 elif match.files() and not match.anypats(): # match.match, no patterns skipstep3 = True if not exact and self._checkcase: normalize = self._normalize skipstep3 = False else: normalize = None # step 1: find all explicit files results, work, dirsnotfound = self._walkexplicit(match, subrepos) skipstep3 = skipstep3 and not (work or dirsnotfound) work = [nd for nd, d in work if not dirignore(d)] wadd = work.append # step 2: visit subdirectories while work: nd = work.pop() skip = None if nd != b'': skip = b'.hg' try: entries = util.listdir(join(nd), stat=True, skip=skip) except OSError as inst: if inst.errno in (errno.EACCES, errno.ENOENT): fwarn(nd, inst.strerror) continue raise for f, kind, st in entries: if normalize: nf = normalize(nd and (nd + b"/" + f) or f, True, True) else: nf = nd and (nd + b"/" + f) or f if nf not in results: if kind == dirkind: if not ignore(nf): if matchtdir: matchtdir(nf) wadd(nf) if nf in dmap and (matchalways or matchfn(nf)): results[nf] = None elif kind == regkind or kind == lnkkind: if nf in dmap: if matchalways or matchfn(nf): results[nf] = st elif (matchalways or matchfn(nf)) and not ignore(nf): results[nf] = st elif nf in dmap and (matchalways or matchfn(nf)): results[nf] = None for s in subrepos: del results[s] del results[b'.hg'] # step 3: report unseen items in the dmap hash if not skipstep3 and not exact: if not results and matchalways: visit = dmap.keys() else: visit = [f for f in dmap if f not in results and matchfn(f)] visit.sort() if unknown: # unknown == True means we walked the full directory tree # above. So if a file is not seen it was either a) not matching # matchfn b) ignored, c) missing, or d) under a symlink # directory. audit_path = pathutil.pathauditor(self._root) for nf in iter(visit): # Report ignored items in the dmap as long as they are not # under a symlink directory. if audit_path.check(nf): try: results[nf] = lstat(join(nf)) except OSError: # file doesn't exist results[nf] = None else: # It's either missing or under a symlink directory results[nf] = None else: # We may not have walked the full directory tree above, # so stat everything we missed. nf = next(iter(visit)) for st in util.statfiles([join(i) for i in visit]): results[nf()] = st return results.keys() def _rust_status(self, *args, **kwargs): # intercept a rust status call and force the fallback, # otherwise our patching won't work if not os.path.lexists(self._join(b'.hgignore')): self._ui.debug(b'suppressing rust status to intercept gitignores\n') raise dirstate.rustmod.FallbackError else: return super()._rust_status(*args, **kwargs) @eh.reposetup def reposetup(ui, repo): if isinstance(repo, gitrepo.gitrepo): return if git_handler.has_gitrepo(repo): dirstate.dirstate = gitdirstate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/gitrepo.py0000644000000000000000000001512514751647721012766 0ustar00from dulwich.refs import LOCAL_BRANCH_PREFIX from mercurial import ( bundlerepo, discovery, error, exchange, exthelper, hg, pycompat, wireprotov1peer, ) from mercurial.interfaces import repository from mercurial.utils import urlutil from . import util eh = exthelper.exthelper() class gitrepo(repository.peer): def __init__(self, ui, path=None, create=False, intents=None, **kwargs): if create: # pragma: no cover raise error.Abort(b'Cannot create a git repository.') self._ui = ui self.path = path self.localrepo = None _peercapabilities = [b'lookup'] def _capabilities(self): return self._peercapabilities def capabilities(self): return self._peercapabilities @property def ui(self): return self._ui def url(self): return self.path @util.makebatchable def lookup(self, key): assert isinstance(key, bytes) # FIXME: this method is supposed to return a 20-byte node hash return key def local(self): if not self.path: raise error.RepoError def filtered(self, name: bytes): assert name == b'visible' return self @util.makebatchable def heads(self): return [] @util.makebatchable def listkeys(self, namespace): if namespace == b'namespaces': return {b'bookmarks': b''} elif namespace == b'bookmarks': if self.localrepo is not None: handler = self.localrepo.githandler result = handler.fetch_pack(self.path, heads=[]) # map any git shas that exist in hg to hg shas stripped_refs = { ref[len(LOCAL_BRANCH_PREFIX) :]: handler.map_hg_get(val) or val for ref, val in result.refs.items() if ref.startswith(LOCAL_BRANCH_PREFIX) } return stripped_refs return {} @util.makebatchable def pushkey(self, namespace, key, old, new): return False def branchmap(self): raise NotImplementedError def canpush(self): return True def close(self): pass def debugwireargs(self): raise NotImplementedError def getbundle(self): raise NotImplementedError def iterbatch(self): raise NotImplementedError def known(self): raise NotImplementedError def peer(self, path=None, remotehidden=False): return self def stream_out(self): raise NotImplementedError def unbundle(self): raise NotImplementedError def commandexecutor(self): return wireprotov1peer.peerexecutor(self) def _submitbatch(self, req): for op, argsdict in req: yield None def _submitone(self, op, args): return None instance = gitrepo def islocal(path): if util.isgitsshuri(path): return True u = urlutil.url(path) return not u.scheme or u.scheme == b'file' # defend against tracebacks if we specify -r in 'hg pull' @eh.wrapfunction(hg, 'addbranchrevs') def safebranchrevs(orig, lrepo, otherrepo, branches, revs, **kwargs): revs, co = orig(lrepo, otherrepo, branches, revs, **kwargs) if isinstance(otherrepo, gitrepo): # FIXME: Unless it's None, the 'co' result is passed to the lookup() # remote command. Since our implementation of the lookup() remote # command is incorrect, we set it to None to avoid a crash later when # the incorect result of the lookup() remote command would otherwise be # used. This can, in undocumented corner-cases, result in that a # different revision is updated to when passing both -u and -r to # 'hg pull'. An example of such case is in tests/test-addbranchrevs.t # (for the non-hg-git case). co = None return revs, co @eh.wrapfunction(discovery, 'findcommonoutgoing') def findcommonoutgoing(orig, repo, other, *args, **kwargs): if isinstance(other, gitrepo): heads = repo.githandler.get_refs(other.path)[0] kw = {} kw.update(kwargs) for val, k in zip( args, ('onlyheads', 'force', 'commoninc', 'portable') ): kw[k] = val force = kw.get('force', False) commoninc = kw.get('commoninc', None) if commoninc is None: commoninc = discovery.findcommonincoming( repo, other, heads=heads, force=force ) kw['commoninc'] = commoninc return orig(repo, other, **kw) return orig(repo, other, *args, **kwargs) @eh.wrapfunction(bundlerepo, 'getremotechanges') def getremotechanges(orig, ui, repo, other, onlyheads, *args, **opts): if isinstance(other, gitrepo): return repo.githandler.getremotechanges(other, onlyheads) return orig(ui, repo, other, onlyheads, *args, **opts) @eh.wrapfunction(exchange, 'pull') @util.transform_notgit def exchangepull( orig, repo, remote, heads=None, force=False, bookmarks=(), **kwargs ): if isinstance(remote, gitrepo): pullop = exchange.pulloperation( repo, remote, heads, force, bookmarks=bookmarks ) pullop.trmanager = exchange.transactionmanager( repo, b'pull', remote.url() ) with repo.wlock(), repo.lock(), pullop.trmanager: pullop.cgresult = repo.githandler.fetch(remote, heads) return pullop else: return orig( repo, remote, heads=heads, force=force, bookmarks=bookmarks, **kwargs, ) # TODO figure out something useful to do with the newbranch param @eh.wrapfunction(exchange, 'push') @util.transform_notgit def exchangepush( orig, repo, remote, force=False, revs=None, newbranch=False, bookmarks=(), opargs=None, **kwargs ): if isinstance(remote, gitrepo): pushop = exchange.pushoperation( repo, remote, force, revs, newbranch, bookmarks, **pycompat.strkwargs(opargs or {}), ) pushop.cgresult = repo.githandler.push( remote.path, revs, bookmarks, force ) return pushop else: return orig( repo, remote, force, revs, newbranch, bookmarks=bookmarks, opargs=opargs, **kwargs, ) def make_peer( ui, path, create, intents=None, createopts=None, remotehidden=False ): return gitrepo(ui, path, create) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/helptext/__init__.py0000644000000000000000000000000014751647721014673 0ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/helptext/config.rst0000644000000000000000000002406714751647721014604 0ustar00``git`` ------- Control how the Hg-Git extension interacts with Git. ``authors`` Git uses a strict convention for "author names" when representing changesets, using the form ``[realname] [email address]``. Mercurial encourages this convention as well but is not as strict, so it's not uncommon for a Mercurial repository to have authors listed as, for example, simple usernames. hg-git by default will attempt to translate Mercurial usernames using the following rules: - If the Mercurial username fits the pattern ``NAME ``, the Git name will be set to NAME and the email to EMAIL. - If the Mercurial username looks like an email (if it contains an ``@``), the Git name and email will both be set to that email. - If the Mercurial username consists of only a name, the email will be set to ``none@none``. - Illegal characters (stray ``<``\ s or ``>``\ s) will be stripped out, and for ``NAME `` usernames, any content after the right-bracket (for example, a second ``>``) will be turned into a url-encoded sigil like ``ext:(%3E)`` in the Git author name. Since these default behaviors may not be what you want (``none@none``, for example, shows up unpleasantly on GitHub as "illegal email address"), the ``git.authors`` option provides for an "authors translation file" that will be used during outgoing transfers from Mercurial to Git only, by modifying ``hgrc`` as such: :: [git] authors = authors.txt Where ``authors.txt`` is the name of a text file containing author name translations, one per each line, using the following format: :: johnny = John Smith dougie = Doug Johnson Empty lines and lines starting with a ``#`` are ignored. It should be noted that this translation is in *the Mercurial to Git direction only*. Changesets coming from Git back to Mercurial will not translate back into Mercurial usernames, so it's best that the same username/email combination be used on both the Mercurial and Git sides; the author file is mostly useful for translating legacy changesets. ``branch_bookmark_suffix`` Hg-Git does not convert between Mercurial named branches and git branches as the two are conceptually different; instead, it uses Mercurial bookmarks to represent the concept of a Git branch. Therefore, when translating a Mercurial repository over to Git, you typically need to create bookmarks to mirror all the named branches that you'd like to see transferred over to Git. The major caveat with this is that you can't use the same name for your bookmark as that of the named branch, and furthermore there's no feasible way to rename a branch in Mercurial. For the use case where one would like to transfer a Mercurial repository over to Git, and maintain the same named branches as are present on the hg side, the ``branch_bookmark_suffix`` might be all that's needed. This presents a string "suffix" that will be recognized on each bookmark name, and stripped off as the bookmark is translated to a Git branch: :: [git] branch_bookmark_suffix=_bookmark Above, if a Mercurial repository had a named branch called ``release_6_maintenance``, you could then link it to a bookmark called ``release_6_maintenance_bookmark``. hg-git will then strip off the ``_bookmark`` suffix from this bookmark name, and create a Git branch called ``release_6_maintenance``. When pulling back from Git to hg, the ``_bookmark`` suffix is then applied back, if and only if a Mercurial named branch of that name exists. E.g., when changes to the ``release_6_maintenance`` branch are checked into Git, these will be placed into the ``release_6_maintenance_bookmark`` bookmark on hg. But if a new branch called ``release_7_maintenance`` were pulled over to hg, and there was not a ``release_7_maintenance`` named branch already, the bookmark will be named ``release_7_maintenance`` with no usage of the suffix. The ``branch_bookmark_suffix`` option is, like the ``authors`` option, intended for migrating legacy hg named branches. Going forward, a Mercurial repository that is to be linked with a Git repository should only use bookmarks for named branching. ``findcopiesharder`` Whether to consider unmodified files as copy sources. This is a very expensive operation for large projects, so use it with caution. Similar to ``git diff``'s --find-copies-harder option. ``intree`` Hg-Git keeps a Git repository clone for reading and updating. By default, the Git clone is the subdirectory ``git`` in your local Mercurial repository. If you would like this Git clone to be at the same level of your Mercurial repository instead (named ``.git``), add the following to your ``hgrc``: :: [git] intree = True Please note that changing this setting in an existing repository doesn't move the local Git repository. You will either have to do so yourself, or issue an :hg:`pull` after the fact to repopulate the new location. ``mindate`` If set, branches where the latest commit's commit time is older than this will not be imported. Accepts any date formats that Mercurial does -- see :hg:`help dates` for more. ``public`` A list of Git branches that should be considered "published", and therefore converted to Mercurial in the 'public' phase. This is only used if ``hggit.usephases`` is set. ``pull-prune-remote-branches`` Before fetching, remove any remote-tracking references, or pseudo-tags, that no longer exist on the remote. This is equivalent to the ``--prune`` option to ``git fetch``, and means that pseudo-tags for remotes -- such as ``default/master`` -- always actually reflect what's on the remote. This option is enabled by default. ``pull-prune-bookmarks`` On pull, delete any unchanged bookmarks removed on the remote. In other words, if e.g. the ``thebranch`` bookmark remains at ``default/thebranch``, and the branch is deleted in Git, pulling deletes the bookmark. This option is enabled by default. ``renamelimit`` The number of files to consider when performing the copy/rename detection. Detection is disabled if the number of files modified in a commit is above the limit. Detection is O(N^2) in the number of files modified, so be sure not to set the limit too high. Similar to Git's ``diff.renameLimit`` config. The default is "400", the same as Git. ``similarity`` Specify how similar files modified in a Git commit must be to be imported as Mercurial renames or copies, as a percentage between "0" (disabled) and "100" (files must be identical). For example, "90" means that a delete/add pair will be imported as a rename if more than 90% of the file has stayed the same. The default is "0" (disabled). ``blame.ignoreRevsFile`` Specify a file that lists Git commits to ignore when invoking :hg:`annotate`. ``hggit`` --------- Control behavior of the Hg-Git extension. ``mapsavefrequency`` By default, hg-git only saves the results of a conversion at the end. Use this option to enable resuming long-running pulls and pushes. Set this to a number greater than 0 to allow resuming after converting that many commits. This can help when the conversion encounters an error partway through a large batch of changes. Otherwise, an error or interruption will roll back the transaction, similar to regular Mercurial. Defaults to 1000. Please note that this is disregarded for an initial clone, as any error or interruption will delete the destination. So instead of cloning a large Git repository, you might want to pull instead: :: $ hg init linux $ cd linux $ echo "[paths]\ndefault = https://github.com/torvalds/linux" > .hg/hgrc $ hg pull ...and be extremely patient. Please note that converting very large repositories may take *days* rather than mere *hours*, and may run into issues with available memory for very long running clones. Even any small, undiscovered leak will build up when processing hundreds of thousands of files and commits. Cloning the Linux kernel is likely a pathological case, but other storied repositories such as CPython do work well, even if the initial clone requires a some patience. ``threads`` During a push to Git, hg-git will pack loose objects at regular intervals whenever it saves its map. As this is a rather expensive operation, it's done in separate threads. Defaults to the system CPU count or 4, whichever is lower. ``usephases`` When converting Git revisions to Mercurial, place them in the 'public' phase as appropriate. Namely, revisions that are reachable from the remote Git repository's default branch, or ``HEAD``, will be marked *public*. For most repositories, this means the remote ``master`` branch will be converted as public. The same applies to any commits tagged in the remote. To restrict publishing to specific branches or tags, use the ``git.public`` option. Publishing commits prevents their modification, and speeds up many local Mercurial operations, such as :hg:`shelve`. ``fetchbuffer`` Data fetched from Git is buffered in memory, unless it exceeds the given limit, in megabytes. By default, flush the buffer to disk when it exceeds 100MB. ``retries`` Interacting with a remote Git repository may require authentication. Normally, this will trigger a prompt and a retry, and this option restricts the amount of retries. Defaults to 3. ``invalidpaths`` Both Mercurial and Git consider paths as just bytestrings internally, and allow almost anything. The difference, however, is in the _almost_ part. For example, many Git servers will reject a push for security reasons if it contains a nested Git repository. Similarly, Mercurial cannot checkout commits with a nested repository, and it cannot even store paths containing an embedded newline or carrage return character. The default is to issue a warning and skip these paths. You can change this by setting ``hggit.invalidpaths`` in ``.hgrc``: :: [hggit] invalidpaths = keep Possible values are ``keep``, ``skip`` or ``abort``. Prior to 1.0, the default was ``abort``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/hg2git.py0000644000000000000000000004247214751647721012506 0ustar00# -*- coding: utf-8 -*- # This file contains code dealing specifically with converting Mercurial # repositories to Git repositories. Code in this file is meant to be a generic # library and should be usable outside the context of hg-git or an hg command. import io import os import stat import dulwich.config as dulcfg import dulwich.objects as dulobjs from mercurial import ( subrepoutil, encoding, error, ) def flags2mode(flags): if b'l' in flags: return 0o120000 elif b'x' in flags: return 0o100755 else: return 0o100644 class IncrementalChangesetExporter(object): """Incrementally export Mercurial changesets to Git trees. The purpose of this class is to facilitate Git tree export that is more optimal than brute force. A "dumb" implementations of Mercurial to Git export would iterate over every file present in a Mercurial changeset and would convert each to a Git blob and then conditionally add it to a Git repository if it didn't yet exist. This is suboptimal because the overhead associated with obtaining every file's raw content and converting it to a Git blob is not trivial! This class works around the suboptimality of brute force export by leveraging the information stored in Mercurial - the knowledge of what changed between changesets - to only export Git objects corresponding to changes in Mercurial. In the context of converting Mercurial repositories to Git repositories, we only export objects Git (possibly) hasn't seen yet. This prevents a lot of redundant work and is thus faster. Callers instantiate an instance of this class against a mercurial.localrepo instance. They then associate it with a specific changesets by calling update_changeset(). On each call to update_changeset(), the instance computes the difference between the current and new changesets and emits Git objects that haven't yet been encountered during the lifetime of the class instance. In other words, it expresses Mercurial changeset deltas in terms of Git objects. Callers then (usually) take this set of Git objects and add them to the Git repository. This class only emits Git blobs and trees, not commits. The tree calculation part of this class is essentially a reimplementation of dulwich.index.commit_tree. However, since our implementation reuses Tree instances and only recalculates SHA-1 when things change, we are more efficient. """ def __init__(self, hg_repo, start_ctx, git_store, git_commit): """Create an instance against a mercurial.localrepo. start_ctx: the context for a Mercurial commit that has a Git equivalent, passed in as git_commit. The incremental computation will be started from this commit. git_store: the Git object store the commit comes from. start_ctx can be repo[nullid], in which case git_commit should be None. """ self._hg = hg_repo # Our current revision's context. self._ctx = start_ctx # Path to dulwich.objects.Tree. self._init_dirs(git_store, git_commit) # Mercurial file nodeid to Git blob SHA-1. Used to prevent redundant # blob calculation. self._blob_cache = {} def _init_dirs(self, store, commit): """Initialize self._dirs for a Git object store and commit.""" self._dirs = {} if commit is None: return dirkind = stat.S_IFDIR # depth-first order, chosen arbitrarily todo = [(b'', store[commit.tree])] while todo: path, tree = todo.pop() self._dirs[path] = tree for entry in tree.items(): if entry.mode == dirkind: if path == b'': newpath = entry.path else: newpath = path + b'/' + entry.path todo.append((newpath, store[entry.sha])) @property def root_tree_sha(self): """The SHA-1 of the root Git tree. This is needed to construct a Git commit object. """ return self._dirs[b''].id def audit_path(self, path): r"""Check for path components that case-fold to .git. Returns ``True`` for normal paths. The results for insecure paths depend on the ``hggit.invalidpaths`` configuration in ``ui``: ``abort`` means insecure paths abort the conversion — this was the default prior to 1.0. ``keep`` means issue a warning and keep the path, returning ``True``. Anything else, but documented as ``skip``, means issue a warning and disregard the path, returning ``False``. """ ui = self._hg.ui dangerous = False for c in path.split(b'/'): if encoding.hfsignoreclean(c) == b'.git': dangerous = True break elif b'~' in c: base, tail = c.split(b'~', 1) if tail.isdigit() and base.upper().startswith(b'GIT'): dangerous = True break if dangerous: opt = ui.config(b'hggit', b'invalidpaths') # escape the path when printing it out prettypath = path.decode('latin1').encode('unicode-escape') if opt == b'abort': raise error.Abort( b"invalid path '%s' rejected by configuration" % prettypath, hint=b"see 'hg help config.hggit.invalidpaths for details", ) elif opt == b'keep': ui.warn( b"warning: path '%s' contains an invalid path component\n" % prettypath, ) return True else: # undocumented: just let anything else mean "skip" ui.warn(b"warning: skipping invalid path '%s'\n" % prettypath) return False elif path == b'.gitmodules': ui.warn( b"warning: ignoring modifications to '%s' file; " b"please use '.hgsub' instead\n" % path ) return False else: return True def filter_unsafe_paths(self, paths): """Return a copy of the given list, with dangerous paths removed. Please note that the configuration can suppress the removal; see above. """ return [path for path in paths if self.audit_path(path)] def update_changeset(self, newctx): """Set the tree to track a new Mercurial changeset. This is a generator of 2-tuples. The first item in each tuple is a dulwich object, either a Blob or a Tree. The second item is the corresponding Mercurial nodeid for the item, if any. Only blobs will have nodeids. Trees do not correspond to a specific nodeid, so it does not make sense to emit a nodeid for them. When exporting trees from Mercurial, callers typically write the returned dulwich object to the Git repo via the store's add_object(). Some emitted objects may already exist in the Git repository. This class does not know about the Git repository, so it's up to the caller to conditionally add the object, etc. Emitted objects are those that have changed since the last call to update_changeset. If this is the first call to update_chanageset, all objects in the tree are emitted. """ # Our general strategy is to accumulate dulwich.objects.Blob and # dulwich.objects.Tree instances for the current Mercurial changeset. # We do this incremental by iterating over the Mercurial-reported # changeset delta. We rely on the behavior of Mercurial to lazy # calculate a Tree's SHA-1 when we modify it. This is critical to # performance. # In theory we should be able to look at changectx.files(). This is # *much* faster. However, it may not be accurate, especially with older # repositories, which may not record things like deleted files # explicitly in the manifest (which is where files() gets its data). # The only reliable way to get the full set of changes is by looking at # the full manifest. And, the easy way to compare two manifests is # localrepo.status(). status = self._hg.status(self._ctx, newctx) modified, added, removed = map( self.filter_unsafe_paths, (status.modified, status.added, status.removed), ) # We track which directories/trees have modified in this update and we # only export those. dirty_trees = set() gitmodules = b'' subadded, subremoved = [], [] for s in modified, added, removed: if b'.hgsub' in s or b'.hgsubstate' in s: gitmodules, subadded, subremoved = self._handle_subrepos(newctx) break # We first process subrepo and file removals so we can prune dead # trees. for path in subremoved: self._remove_path(path, dirty_trees) for path in removed: if path == b'.hgsubstate' or path == b'.hgsub': continue self._remove_path(path, dirty_trees) for path, sha in subadded: if b'.gitmodules' in newctx: modified.append(b'.gitmodules') else: added.append(b'.gitmodules') d = os.path.dirname(path) tree = self._dirs.setdefault(d, dulobjs.Tree()) dirty_trees.add(d) tree.add(os.path.basename(path), dulobjs.S_IFGITLINK, sha) # For every file that changed or was added, we need to calculate the # corresponding Git blob and its tree entry. We emit the blob # immediately and update trees to be aware of its presence. for path in set(modified) | set(added): if path == b'.hgsubstate' or path == b'.hgsub': continue d = os.path.dirname(path) tree = self._dirs.setdefault(d, dulobjs.Tree()) dirty_trees.add(d) if path == b'.gitmodules': blob = dulobjs.Blob.from_string(gitmodules) entry = dulobjs.TreeEntry( path, flags2mode(newctx[b'.hgsub'].flags()), blob.id, ) else: fctx = newctx[path] entry, blob = self.tree_entry(fctx) if blob is not None: yield blob tree.add(*entry) # Now that all the trees represent the current changeset, recalculate # the tree IDs and emit them. Note that we wait until now to calculate # tree SHA-1s. This is an important difference between us and # dulwich.index.commit_tree(), which builds new Tree instances for each # series of blobs. for obj in self._populate_tree_entries(dirty_trees): yield obj self._ctx = newctx def _remove_path(self, path, dirty_trees): """Remove a path (file or git link) from the current changeset. If the tree containing this path is empty, it might be removed.""" d = os.path.dirname(path) tree = self._dirs.get(d, dulobjs.Tree()) del tree[os.path.basename(path)] dirty_trees.add(d) # If removing this file made the tree empty, we should delete this # tree. This could result in parent trees losing their only child # and so on. if not len(tree): self._remove_tree(d) else: self._dirs[d] = tree def _remove_tree(self, path): """Remove a (presumably empty) tree from the current changeset. A now-empty tree may be the only child of its parent. So, we traverse up the chain to the root tree, deleting any empty trees along the way. """ try: del self._dirs[path] except KeyError: return # Now we traverse up to the parent and delete any references. if path == b'': return basename = os.path.basename(path) parent = os.path.dirname(path) while True: tree = self._dirs.get(parent, None) # No parent entry. Nothing to remove or update. if tree is None: return try: del tree[basename] except KeyError: return if len(tree): return # The parent tree is empty. Se, we can delete it. del self._dirs[parent] if parent == b'': return basename = os.path.basename(parent) parent = os.path.dirname(parent) def _populate_tree_entries(self, dirty_trees): self._dirs.setdefault(b'', dulobjs.Tree()) # Fill in missing directories. for path in list(self._dirs): parent = os.path.dirname(path) while parent != b'': parent_tree = self._dirs.get(parent, None) if parent_tree is not None: break self._dirs[parent] = dulobjs.Tree() parent = os.path.dirname(parent) for dirty in list(dirty_trees): parent = os.path.dirname(dirty) while parent != b'': if parent in dirty_trees: break dirty_trees.add(parent) parent = os.path.dirname(parent) # The root tree is always dirty but doesn't always get updated. dirty_trees.add(b'') # We only need to recalculate and export dirty trees. for d in sorted(dirty_trees, key=len, reverse=True): # Only happens for deleted directories. try: tree = self._dirs[d] except KeyError: continue yield tree if d == b'': continue parent_tree = self._dirs[os.path.dirname(d)] # Accessing the tree's ID is what triggers SHA-1 calculation and is # the expensive part (at least if the tree has been modified since # the last time we retrieved its ID). Also, assigning an entry to a # tree (even if it already exists) invalidates the existing tree # and incurs SHA-1 recalculation. So, it's in our interest to avoid # invalidating trees. Since we only update the entries of dirty # trees, this should hold true. parent_tree[os.path.basename(d)] = (stat.S_IFDIR, tree.id) def _handle_subrepos(self, newctx): state = subrepoutil.state(self._ctx, self._hg.ui) newstate = subrepoutil.state(newctx, self._hg.ui) # For each path, the logic is described by the following table. 'no' # stands for 'the subrepo doesn't exist', 'git' stands for 'git # subrepo', and 'hg' stands for 'hg or other subrepo'. # # old new | action # * git | link (1) # git hg | delete (2) # git no | delete (3) # # All other combinations are 'do nothing'. # # git links without corresponding submodule paths are stored as # subrepos with a substate but without an entry in .hgsub. # 'added' is both modified and added added, removed = [], [] if b'.gitmodules' not in newctx: config = dulcfg.ConfigFile() else: with io.BytesIO(newctx[b'.gitmodules'].data()) as buf: config = dulcfg.ConfigFile.from_file(buf) submodules = { path: name for (path, url, name) in dulcfg.parse_submodules(config) } for path, (remote, sha, t) in state.items(): if t != b'git': # old = hg -- will be handled in next loop continue # old = git if path not in newstate or newstate[path][2] != b'git': # new = hg or no, case (2) or (3) removed.append(path) submodules.pop(path, None) for path, (remote, sha, t) in newstate.items(): if t != b'git': # new = hg or no; the only cases we care about are handled # above continue # case (1) added.append((path, sha)) section = b"submodule", submodules.get(path, path) config.set(section, b"path", path) config.set(section, b"url", remote) with io.BytesIO() as buf: config.write_to_file(buf) gitmodules = buf.getvalue() return gitmodules, added, removed def tree_entry(self, fctx): """Compute a dulwich TreeEntry from a filectx. A side effect is the TreeEntry is stored in the blob cache. Returns a 2-tuple of (dulwich.objects.TreeEntry, dulwich.objects.Blob). """ blob_id = self._blob_cache.get(fctx.filenode(), None) blob = None if blob_id is None: blob = dulobjs.Blob.from_string(fctx.data()) blob_id = blob.id self._blob_cache[fctx.filenode()] = blob_id return ( dulobjs.TreeEntry( os.path.basename(fctx.path()), flags2mode(fctx.flags()), blob_id, ), blob, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/hgrepo.py0000644000000000000000000000530514751647721012600 0ustar00from mercurial import exthelper from mercurial import repoview from mercurial import statichttprepo from mercurial import util as hgutil from mercurial.node import bin from .git_handler import GitHandler from .gitrepo import gitrepo from . import util eh = exthelper.exthelper() @eh.reposetup def reposetup(ui, repo): if isinstance(repo, (statichttprepo.statichttprepository, gitrepo)): return if hasattr(repo, '_wlockfreeprefix'): repo._wlockfreeprefix |= { GitHandler.map_file, GitHandler.tags_file, } class hgrepo(repo.__class__): @util.transform_notgit def findoutgoing(self, remote, base=None, heads=None, force=False): if isinstance(remote, gitrepo): base, heads = self.githandler.get_refs(remote.path) out, h = super().findoutgoing(remote, base, heads, force) return out else: # pragma: no cover return super().findoutgoing(remote, base, heads, force) def _findtags(self): (tags, tagtypes) = super()._findtags() for tag, rev in self.githandler.tags.items(): if tag not in tags: assert isinstance(tag, bytes) tags[tag] = bin(rev) tagtypes[tag] = b'git' for tag, rev in self.githandler.remote_refs.items(): assert isinstance(tag, bytes) tags[tag] = rev tagtypes[tag] = b'git-remote' tags.update(self.githandler.remote_refs) return (tags, tagtypes) @hgutil.propertycache def githandler(self): '''get the GitHandler for an hg repo This only makes sense if the repo talks to at least one git remote. ''' return GitHandler(self, self.ui) def tags(self): # TODO consider using self._tagscache tagscache = super().tags() tagscache.update(self.githandler.remote_refs) for tag, rev in self.githandler.tags.items(): if tag in tagscache: continue tagscache[tag] = bin(rev) return tagscache repo.__class__ = hgrepo @eh.wrapfunction(repoview, 'pinnedrevs') def pinnedrevs(orig, repo): pinned = orig(repo) # Mercurial pins bookmarks, even if obsoleted, so that they always # appear in e.g. log; do the same with git tags and remotes. if repo.local() and hasattr(repo, 'githandler'): rev = repo.changelog.rev pinned.update(rev(bin(r)) for r in repo.githandler.tags.values()) pinned.update(rev(r) for r in repo.githandler.remote_refs.values()) return pinned ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/overlay.py0000644000000000000000000003375114751647721013003 0ustar00# overlay classes for repositories # unifies access to unimported git objects and committed hg objects # designed to support incoming # # incomplete, implemented on demand from mercurial import ( ancestor, changelog, context, exthelper, manifest, match as matchmod, namespaces, node, util, ) from mercurial.utils import stringutil from mercurial.node import bin, hex, nullid from dulwich.refs import ( LOCAL_BRANCH_PREFIX, LOCAL_TAG_PREFIX, ) eh = exthelper.exthelper() def _maybehex(n): if len(n) == 20: return hex(n) return n class overlaymanifest(object): def __init__(self, repo, sha): self.repo = repo self.tree = repo.handler.git.get_object(sha) self._map = None self._flags = None def withflags(self): self.load() return {path for path, flag in self._flags.items() if flag != b''} def copy(self): return overlaymanifest(self.repo, self.tree.id) def keys(self): self.load() return self._map.keys() def iterkeys(self): return iter(self.keys()) def load(self): if self._map is not None: return self._map = {} self._flags = {} def hgflag(gitflag): if gitflag & 0o100: return b'x' elif gitflag & 0o20000: return b'l' else: return b'' def addtree(tree, dirname): for entry in tree.items(): if entry.mode & 0o40000: # expand directory subtree = self.repo.handler.git.get_object(entry.sha) addtree(subtree, dirname + entry.path + b'/') else: path = dirname + entry.path self._map[path] = bin(entry.sha) self._flags[path] = hgflag(entry.mode) addtree(self.tree, b'') def matches(self, match): '''generate a new manifest filtered by the match argument''' if match.always(): return self.copy() mf = self.copy() for fn in mf.keys(): if not match(fn): del mf[fn] return mf def iteritems(self): self.load() return self._map.items() def __iter__(self): self.load() return self._map.__iter__() def __getitem__(self, path): self.load() return self._map[path] def __contains__(self, path): self.load() return path in self._map def get(self, path, default=None): self.load() return self._map.get(path, default) def flags(self, key, default=''): return self._flags.get(key, default) def diff(self, m2, match=None, clean=False): self.load() if isinstance(m2, overlaymanifest): m2.load() # below code copied from manifest.py:manifestdict.diff diff = {} if match is None: match = matchmod.always(b'', b'') for fn, n1 in self.iteritems(): if not match(fn): continue fl1 = self._flags.get(fn) n2 = m2.get(fn, None) fl2 = m2.flags(fn) if n2 is None: fl2 = b'' if n1 != n2 or fl1 != fl2: diff[fn] = ((n1, fl1), (n2, fl2)) elif clean: diff[fn] = None for fn, n2 in m2.iteritems(): if fn not in self: if not match(fn): continue fl2 = m2.flags(fn) diff[fn] = ((None, b''), (n2, fl2)) return diff def __delitem__(self, path): del self._map[path] @eh.wrapfunction(manifest.manifestdict, 'diff') def wrapmanifestdictdiff(orig, self, m2, match=None, clean=False): kwargs = {'clean': clean, 'match': match} if isinstance(m2, overlaymanifest): diff = m2.diff(self, **kwargs) # since we calculated the diff with m2 vs m1, flip it around for fn in diff: c1, c2 = diff[fn] diff[fn] = c2, c1 return diff else: return orig(self, m2, **kwargs) class overlayfilectx(object): def __init__(self, repo, path, fileid=None): self._repo = repo self._path = path self.fileid = fileid def repo(self): return self._repo # this is a hack to skip copy detection def ancestors(self): return [self, self] def filenode(self): return nullid def rev(self): return -1 def path(self): return self._path def filelog(self): return self.fileid def data(self): blob = self._repo.handler.git.get_object(_maybehex(self.fileid)) return blob.data def isbinary(self): return stringutil.binary(self.data()) class overlaychangectx(context.changectx): def __init__(self, repo, sha, maybe_filtered=True): # Can't store this in self._repo because the base class uses that field self._hgrepo = repo if isinstance(sha, bytes): pass elif isinstance(sha, str): sha = sha.encode('ascii') else: sha = sha.hex() self.commit = repo.handler.git.get_object(_maybehex(sha)) self._overlay = getattr(repo, 'gitoverlay', repo) self._rev = self._overlay.rev(bin(self.commit.id)) self._maybe_filtered = maybe_filtered def __getattr__(self, name): # Since hg 4.6, context.changectx doesn't define self._repo, # but it is still used by self.obsolete() (and friends) # So, if the attribute wasn't found, fallback to _hgrepo if name == '_repo': return self._hgrepo return super().__getattr__(name) def repo(self): return self._hgrepo def node(self): return bin(self.commit.id) def rev(self): return self._rev def date(self): return self.commit.author_time, self.commit.author_timezone def branch(self): return b'default' def user(self): return self.commit.author def files(self): return [] def extra(self): return {} def description(self): return self.commit.message @util.propertycache def _parents(self): cl = self.repo().changelog parents = cl.parents(cl.node(self._rev)) if not parents: return [self.repo()[b'null']] if parents[1] == nullid: parents = parents[:-1] return [self.repo()[sha] for sha in parents] def manifestnode(self): return bin(self.commit.tree) def hex(self): return self.commit.id def tags(self): return [] def bookmarks(self): return [] def manifest(self): return overlaymanifest(self._overlay, self.commit.tree) def filectx(self, path, filelog=None): mf = self.manifest() return overlayfilectx(self._overlay, path, mf[path]) def flags(self, path): mf = self.manifest() return mf.flags(path) def __bool__(self): return True def phase(self): try: from mercurial import phases return phases.draft except (AttributeError, ImportError): return 1 def totuple(self): return ( self.commit.tree, self.user(), self.date(), self.files(), self.description(), self.extra(), ) class overlayrevlog(object): def __init__(self, repo, base): self.repo = repo self.base = base def parents(self, n): gitrev = self.repo.revmap.get(n) if gitrev is None: # we've reached a revision we have return self.base.parents(n) commit = self.repo.handler.git.get_object(_maybehex(n)) if not commit.parents: return [nullid, nullid] def gitorhg(n): hn = self.repo.handler.map_hg_get(hex(n)) if hn is not None: return bin(hn) return n # currently ignores the octopus p1 = gitorhg(bin(commit.parents[0])) if len(commit.parents) > 1: p2 = gitorhg(bin(commit.parents[1])) else: p2 = nullid return [p1, p2] def ancestor(self, a, b): anode = self.repo.nodemap.get(a) bnode = self.repo.nodemap.get(b) if anode is None and bnode is None: return self.base.ancestor(a, b) ancs = ancestor.ancestors(self.parentrevs, a, b) if ancs: return min(map(self.node, ancs)) return nullid def parentrevs(self, rev): return [self.rev(p) for p in self.parents(self.node(rev))] def node(self, rev): gitnode = self.repo.nodemap.get(rev) if gitnode is None: return self.base.node(rev) return gitnode def rev(self, n): gitrev = self.repo.revmap.get(n) if gitrev is None: return self.base.rev(n) return gitrev def __len__(self): return len(self.repo.handler.repo) + len(self.repo.revmap) class overlaymanifestrevlog(overlayrevlog): pass class overlaymanifestctx(object): def __init__(self, repo, node): self._repo = repo self._node = node def read(self): return overlaymanifest(self._repo, self._node) class overlaymanifestlog(manifest.manifestlog): def __init__(self, repo): self._repo = repo def get(self, dir, node): if dir: raise RuntimeError(b"hggit doesn't support treemanifests") if node == nullid: return manifest.manifestctx() return overlaymanifestctx(self._repo, node) class overlaychangelog(overlayrevlog): def read(self, sha): if isinstance(sha, int): sha = self.node(sha) if sha == nullid: return (nullid, b"", (0, 0), [], b"", {}) try: return self.base.read(sha) except LookupError: return overlaychangectx(self.repo, sha).totuple() def changelogrevision(self, noderev): values = self.read(noderev) return changelog._changelogrevision( manifest=values[0], user=values[1], date=values[2], files=values[3], description=values[4], extra=values[5], ) class overlaygithandler(object): # This was added to support the {gitnode} keyword in templates for incoming. # For this, falling back to the underlying repository is not necessary. def __init__(self, repo): self.repo = repo def map_git_get(self, sha): # Normally, the parameter contains the hg hash. However, for incoming # changesets, it contains the git hash, so we can return it as-is # (after sanity-checking that it is the hash of an incoming commit). assert node.bin(sha) in self.repo.revmap return sha class overlayrepo(object): def __init__(self, handler, commits, refs): self.handler = handler self._activebookmark = None self.changelog = overlaychangelog(self, handler.repo.changelog) self.manifestlog = overlaymanifestlog(self) self.nodeconstants = node.sha1nodeconstants # for incoming -p self.root = handler.repo.root self.getcwd = handler.repo.getcwd # self.status = handler.repo.status self.ui = handler.repo.ui self.revmap = None self.nodemap = None self.refmap = None self.tagmap = None self._makemaps(commits, refs) self.names = namespaces.namespaces() self.githandler = overlaygithandler(self) def _constructmanifest(self): return overlaymanifestrevlog( self, self.handler.repo._constructmanifest() ) def __getitem__(self, n): if isinstance(n, int): n = self.changelog.node(n) if n not in self.revmap: return self.handler.repo[n] return overlaychangectx(self, n) def _handlerhack(self, method, *args, **kwargs): nothing = object() r = self.handler.repo oldhandler = getattr(r, 'handler', nothing) oldoverlay = getattr(r, 'gitoverlay', nothing) r.handler = self.handler r.gitoverlay = self try: return getattr(r, method)(*args, **kwargs) finally: if oldhandler is nothing: del r.handler else: r.handler = oldhandler if oldoverlay is nothing: del r.gitoverlay else: r.gitoverlay = oldoverlay def status(self, *args, **kwargs): return self._handlerhack(b'status', *args, **kwargs) def node(self, n): """Returns an Hg or Git hash for the specified Git hash""" if bin(n) in self.revmap: return n return self.handler.map_hg_get(n) def nodebookmarks(self, n): return self.refmap.get(n, []) def nodetags(self, n): return self.tagmap.get(n, []) def rev(self, n): return self.revmap[n] def filectx(self, path, fileid=None): return overlayfilectx(self, path, fileid=fileid) def unfiltered(self): return self.handler.repo.unfiltered() def _makemaps(self, commits, refs): baserev = self.handler.repo[b'tip'].rev() self.revmap = {} self.nodemap = {} for i, n in enumerate(commits): rev = baserev + i + 1 self.revmap[n] = rev self.nodemap[rev] = n self.refmap = {} self.tagmap = {} for ref in refs: if ref.startswith(LOCAL_BRANCH_PREFIX): refname = ref[len(LOCAL_BRANCH_PREFIX) :] self.refmap.setdefault(bin(refs[ref]), []).append(refname) elif ref.startswith(LOCAL_TAG_PREFIX): tagname = ref[len(LOCAL_TAG_PREFIX) :] self.tagmap.setdefault(bin(refs[ref]), []).append(tagname) def narrowmatch(self, *args, **kwargs): return self.handler.repo.narrowmatch(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/revsets.py0000644000000000000000000000526414751647721013013 0ustar00from mercurial import error from mercurial import exthelper from mercurial import revset from mercurial.i18n import _ from mercurial.node import bin, hex from mercurial.utils import stringutil eh = exthelper.exthelper() @eh.revsetpredicate(b'fromgit') def revset_fromgit(repo, subset, x): '''``fromgit()`` Select changesets that originate from Git. ''' revset.getargs(x, 0, 0, b"fromgit takes no arguments") git = repo.githandler node = repo.changelog.node return revset.baseset( r for r in subset if git.map_git_get(hex(node(r))) is not None ) @eh.revsetpredicate(b'gitnode') def revset_gitnode(repo, subset, x): '''``gitnode(hash)`` Select the changeset that originates in the given Git revision. The hash may be abbreviated: `gitnode(a5b)` selects the revision whose Git hash starts with `a5b`. Aborts if multiple changesets match the abbreviation. ''' args = revset.getargs(x, 1, 1, b"gitnode takes one argument") rev = revset.getstring(args[0], b"the argument to gitnode() must be a hash") git = repo.githandler node = repo.changelog.node def matches(r): gitnode = git.map_git_get(hex(node(r))) if gitnode is None: return False return gitnode.startswith(rev) result = revset.baseset(r for r in subset if matches(r)) if 0 <= len(result) < 2: return result raise error.AmbiguousPrefixLookupError( rev, git.map_file, _(b'ambiguous identifier'), ) @eh.revsetpredicate(b'gittag') def revset_gittag(repo, subset, x): """``gittag([name])`` The specified Git tag by name, or all revisions tagged with Git if no name is given. Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. """ # mostly copied from tag() mercurial/revset.py # i18n: "tag" is a keyword args = revset.getargs(x, 0, 1, _(b"tag takes one or no arguments")) cl = repo.changelog git = repo.githandler if args: pattern = revset.getstring( args[0], # i18n: "tag" is a keyword _(b'the argument to tag must be a string'), ) kind, pattern, matcher = stringutil.stringmatcher(pattern) if kind == b'literal': # avoid resolving all tags tn = git.tags.get(pattern, None) if tn is None: raise error.RepoLookupError( _(b"git tag '%s' does not exist") % pattern ) s = {repo[bin(tn)].rev()} else: s = {cl.rev(bin(n)) for t, n in git.tags.items() if matcher(t)} else: s = {cl.rev(bin(n)) for t, n in git.tags.items()} return subset & s ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/schemes.py0000644000000000000000000001052314751647721012741 0ustar00# git.py - git server bridge # # Copyright 2008 Scott Chacon # also some code (and help) borrowed from durin42 # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # global modules import os from mercurial import ( bookmarks, exthelper, hg, localrepo, ) from mercurial.utils import urlutil # local modules from . import gitrepo from . import util eh = exthelper.exthelper() def isgitdir(path): """True if the given file path is a git repo.""" if os.path.exists(os.path.join(path, b'.hg')): return False if os.path.exists(os.path.join(path, b'.git')): # is full git repo return True if ( os.path.exists(os.path.join(path, b'HEAD')) and os.path.exists(os.path.join(path, b'objects')) and os.path.exists(os.path.join(path, b'refs')) ): # is bare git repo return True return False class RepoFactory: """thin wrapper to dispatch between git repos and mercurial ones""" __slots__ = ('__orig',) def __init__(self, orig): self.__orig = orig @property def islocal(self): '''indirection that allows us to only claim we're local if the wrappee is''' if hasattr(self.__orig, 'islocal'): return self.__islocal else: raise AttributeError('islocal') def __islocal(self, path: bytes) -> bool: if isgitdir(path): return True # detect git ssh urls (which mercurial thinks is a file-like path) if util.isgitsshuri(path): return False return self.__orig.islocal(path) def instance(self, ui, path, *args, **kwargs): if isinstance(path, bytes): url = urlutil.url(path) else: url = path.url p = url.localpath() # detect git ssh urls (which mercurial thinks is a file-like path) if isgitdir(p) or util.isgitsshuri(p) or p.endswith(b'.git'): fn = gitrepo.instance else: fn = self.__orig.instance return fn(ui, path, *args, **kwargs) def make_peer(self, ui, path, *args, **kwargs): p = path.url.localpath() # detect git ssh urls (which mercurial thinks is a file-like path) if isgitdir(p) or util.isgitsshuri(p) or p.endswith(b'.git'): fn = gitrepo.instance elif hasattr(self.__orig, 'make_peer'): fn = self.__orig.make_peer return fn(ui, path, *args, **kwargs) @eh.wrapfunction(hg, 'defaultdest') def defaultdest(orig, source): if source.endswith(b'.git'): return orig(source[:-4]) return orig(source) @eh.wrapfunction(hg, 'peer') def peer(orig, uiorrepo, *args, **opts): newpeer = orig(uiorrepo, *args, **opts) if isinstance(newpeer, gitrepo.gitrepo): if isinstance(uiorrepo, localrepo.localrepository): newpeer.localrepo = uiorrepo return newpeer @eh.wrapfunction(hg, 'clone') def clone(orig, *args, **opts): srcpeer, destpeer = orig(*args, **opts) # HACK: suppress bookmark activation with `--noupdate` if isinstance(srcpeer, gitrepo.gitrepo) and not opts.get('update'): bookmarks.deactivate(destpeer._repo) return srcpeer, destpeer @eh.wrapfunction(urlutil.path, '_isvalidlocalpath') def isvalidlocalpath(orig, self, path): return orig(self, path) or isgitdir(path) @eh.wrapfunction(urlutil.url, 'islocal') def isurllocal(orig, path): # recognise git scp-style paths when cloning return orig(path) and not util.isgitsshuri(path._origpath) @eh.wrapfunction(hg, 'islocal') def islocal(orig, path): # recognise git scp-style paths when cloning return orig(path) and not util.isgitsshuri(path) @eh.wrapfunction(urlutil, 'hasscheme') def hasscheme(orig, path): # recognise git scp-style paths return orig(path) or util.isgitsshuri(path) @eh.extsetup def extsetup(ui): hg.peer_schemes[b'https'] = RepoFactory(hg.peer_schemes[b'https']) hg.peer_schemes[b'http'] = RepoFactory(hg.peer_schemes[b'http']) hg.repo_schemes[b'file'] = RepoFactory(hg.repo_schemes[b'file']) # support for `hg clone git://github.com/defunkt/facebox.git` # also hg clone git+ssh://git@github.com/schacon/simplegit.git for _scheme in util.gitschemes: hg.peer_schemes[_scheme] = gitrepo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/templates.py0000644000000000000000000000155314751647721013313 0ustar00# git.py - git server bridge # # Copyright 2008 Scott Chacon # also some code (and help) borrowed from durin42 # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from mercurial import exthelper eh = exthelper.exthelper() def _gitnodekw(node, repo): if not hasattr(repo, 'githandler'): return None gitnode = repo.githandler.map_git_get(node.hex()) if gitnode is None: gitnode = b'' return gitnode @eh.templatekeyword(b'gitnode', requires={b'ctx', b'repo'}) def gitnodekw(context, mapping): """:gitnode: String. The Git changeset identification hash, as a 40 hexadecimal digit string.""" node = context.resource(mapping, b'ctx') repo = context.resource(mapping, b'repo') return _gitnodekw(node, repo) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/hggit/util.py0000644000000000000000000001504214751647721012270 0ustar00"""Compatibility functions for old Mercurial versions and other utility functions.""" import collections import contextlib import functools import importlib.resources import re from dulwich import pack from dulwich import errors from dulwich.object_store import PackBasedObjectStore from mercurial.i18n import _ from mercurial import ( error, extensions, phases, util as hgutil, pycompat, wireprotov1peer, ) gitschemes = (b'git', b'git+ssh', b'git+http', b'git+https') @contextlib.contextmanager def abort_push_on_keyerror(): """raise a rather verbose error on missing commits""" try: yield except KeyError as e: raise error.Abort( b'cannot push git commit %s as it is not present locally' % e.args[0][:12], hint=( b'please try pulling first, or as a fallback run git-cleanup ' b'to re-export the missing commits' ), ) @contextlib.contextmanager def forcedraftcommits(): """Context manager that forces new commits to at least draft, regardless of configuration. """ with extensions.wrappedfunction( phases, 'newcommitphase', lambda orig, ui: phases.draft, ): yield def parse_hgsub(lines): """Fills OrderedDict with hgsub file content passed as list of lines""" # TODO: get rid of this code and rely on mercurial infrastructure rv = collections.OrderedDict() for l in lines: ls = l.strip() if not ls or ls.startswith(b'#'): continue name, value = l.split(b'=', 1) rv[name.strip()] = value.strip() return rv def serialize_hgsub(data): """Produces a string from OrderedDict hgsub content""" return b''.join(b'%s = %s\n' % (n, v) for n, v in data.items()) def parse_hgsubstate(lines): """Fills OrderedDict with hgsubtate file content passed as list of lines""" # TODO: get rid of this code and rely on mercurial infrastructure rv = collections.OrderedDict() for l in lines: ls = l.strip() if not ls or ls.startswith(b'#'): continue value, name = l.split(b' ', 1) rv[name.strip()] = value.strip() return rv def serialize_hgsubstate(data): """Produces a string from OrderedDict hgsubstate content""" return b''.join(b'%s %s\n' % (data[n], n) for n in sorted(data)) def transform_notgit(f): '''use as a decorator around functions that call into dulwich''' def inner(*args, **kwargs): try: return f(*args, **kwargs) except errors.NotGitRepository: raise error.Abort(b'not a git repository') return inner def isgitsshuri(uri): """Method that returns True if a uri looks like git-style uri Tests: >>> print(isgitsshuri(b'http://fqdn.com/hg')) False >>> print(isgitsshuri(b'http://fqdn.com/test.git')) False >>> print(isgitsshuri(b'git@github.com:user/repo.git')) True >>> print(isgitsshuri(b'github-123.com:user/repo.git')) True >>> print(isgitsshuri(b'git@127.0.0.1:repo.git')) True >>> print(isgitsshuri(b'git@[2001:db8::1]:repository.git')) True """ for scheme in gitschemes: if uri.startswith(b'%s://' % scheme): return False if uri.startswith(b'http:') or uri.startswith(b'https:'): return False m = re.match(br'(?:.+@)*([\[]?[\w\d\.\:\-]+[\]]?):(.*)', uri) if m: # here we're being fairly conservative about what we consider to be git # urls giturl, repopath = m.groups() # definitely a git repo if len(giturl) > 1 and repopath.endswith(b'.git'): return True # use a simple regex to check if it is a fqdn regex fqdn_re = ( br'(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}' br'(?= 42", "setuptools_scm[toml]>=6.0", "wheel", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "hggit/__version__.py" version_scheme = "release-branch-semver" [tool.black] line-length = 80 exclude = ''' build/ | wheelhouse/ | dist/ | packages/ | \.hg/ | \.mypy_cache/ | \.venv/ | tests/ | hggit/__version__.py ''' skip-string-normalization = true [tool.pylint.master] # # Current checks: # - W0102: no mutable default argument # - C0321: more than one statement on a single line # # Unique to hg-git: # - W1401: anomalous backslash in string # - W1402: anomalous unicode escape in string # - C0411: third party import order # reports = "no" disable = "all" enable = ["W0102", "C0321", "W1401", "W1402", "C0411"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/setup.cfg0000644000000000000000000000146214751647721011461 0ustar00[metadata] name = hg-git author = Scott Chacon, Augie Fackler, Kevin Bullock and others maintainer = The hg-git maintainers maintainer_email = hg-git@googlegroups.com url = http://foss.heptapod.net/mercurial/hg-git description = push to and pull from a Git repository using Mercurial long_description = file:README.rst long_description_content_type = text/x-rst keywords = hg git mercurial license = GPLv2 [options] include_package_data=True zip_safe=False python_requires = >=3.9 packages = hggit hggit.helptext install_requires= dulwich>=0.21.6,<0.23.0 [options.package_data] * = *.txt, *.rst [flake8] # E,W will ignore all pep8 ignore=E129 exclude=./tests,./contrib/ [coverage:report] precision = 2 omit = hggit/__version__.py include = hggit/* exclude_lines = raise NotImplementedError ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/setup.py0000644000000000000000000000036214751647721011350 0ustar00import setuptools # don't guard this with a try; users should use pip for # installing/building hg-git, as it ensures the proper dependencies # are present import setuptools_scm assert setuptools_scm # silence pyflakes setuptools.setup() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/gpg/pubring.gpg0000644000000000000000000000112514751647721013720 0ustar00™ J5\|êÆqµ“¦íû@Ðã‚"­ðÁÓZýïQF÷µ êl"ñˆ†ƒiI”Àeÿ¥æÃÈˋӂQ|£á ÿÅ®HôMÞ®6Z ŸØzÚBq™íœµ‘Ô=¢á¡ ¥ó}ÏþŒ¦ó -! d4L@{yâAA]Gÿ‚õ,®@”éûݨm©+¶ ŠscyʽÒaž¢"%×jÆ¿†môºL?ã&”([¢5ÜßÍ<8YO?¦‰ _“ŠdŸßÄ„8h÷7Žx:BêV>¼Ðzi¤>YÚÁBÈ\$E3Ô¦z¾§,ÐSZØ·ëøÚo ðß”C¦¯G[´hgtest‰6 J5\| € ¢- §N$ñòÑ懻mDzg”&Zoç=£+kel–Òørß¹(Ú©„…³“µw1 k{Ê÷— ÏÔ=¡Þª‡}N_C1ª ¦A8‰•ægùÉΧO@$Ûîvýõå¼U¼‰ÊÛ¸ÿKQiºô^Û€ÁD/G;º.»Þ*]™ ´p;9Ä÷×q„S’'¥`–¢Ôâ¶t€3*¿ $|Ý1Ê5äijTfÄE®!Ç3ykïnÞ·Ç8ÖØä;YÔ8¢=IûMîðFä«…è+",#è´äÍXþjO„û³ '\ ¢]ÉC%›Ìšã…Æß«®ŠËjêK$•¹ƒù¶R?§¢F®dǰ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/gpg/secring.gpg0000644000000000000000000000234014751647721013704 0ustar00•˜J5\|êÆqµ“¦íû@Ðã‚"­ðÁÓZýïQF÷µ êl"ñˆ†ƒiI”Àeÿ¥æÃÈˋӂQ|£á ÿÅ®HôMÞ®6Z ŸØzÚBq™íœµ‘Ô=¢á¡ ¥ó}ÏþŒ¦ó -! d4L@{yâAA]Gÿ‚õ,®@”éûݨm©+¶ ŠscyʽÒaž¢"%×jÆ¿†môºL?ã&”([¢5ÜßÍ<8YO?¦‰ _“ŠdŸßÄ„8h÷7Žx:BêV>¼Ðzi¤>YÚÁBÈ\$E3Ô¦z¾§,ÐSZØ·ëøÚo ðß”C¦¯G[þ1 ¢- §N$ñò WÖC–™ üÁ~»>Ã+¦Ê^y././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/helpers-testrepo.sh0000644000000000000000000000366714751647721014654 0ustar00# In most cases, the mercurial repository can be read by the bundled hg, but # that isn't always true because third-party extensions may change the store # format, for example. In which case, the system hg installation is used. # # We want to use the hg version being tested when interacting with the test # repository, and the system hg when interacting with the mercurial source code # repository. # # The mercurial source repository was typically orignally cloned with the # system mercurial installation, and may require extensions or settings from # the system installation. if [ -n "$HGTESTEXTRAEXTENSIONS" ]; then for extension in $HGTESTEXTRAEXTENSIONS; do extraoptions="$extraoptions --config extensions.$extension=!" done fi syshg () { ( syshgenv exec hg "$@" ) } # Revert the environment so that running "hg" runs the system hg # rather than the test hg installation. syshgenv () { . "$HGTEST_RESTOREENV" HGPLAIN=1 export HGPLAIN } # The test-repo is a live hg repository which may have evolution markers # created, e.g. when a ~/.hgrc enabled evolution. # # Tests may be run using a custom HGRCPATH, which do not enable evolution # markers by default. # # If test-repo includes evolution markers, and we do not enable evolution # markers, hg will occasionally complain when it notices them, which disrupts # tests resulting in sporadic failures. # # Since we aren't performing any write operations on the test-repo, there's # no harm in telling hg that we support evolution markers, which is what the # following lines for the hgrc file do: cat >> "$HGRCPATH" << EOF [experimental] evolution = createmarkers EOF # Use the system hg command if the bundled hg can't read the repository with # no warning nor error. if [ -n "`hg id -R "$TESTDIR/.." 2>&1 >/dev/null`" ]; then alias testrepohg=syshg alias testrepohgenv=syshgenv else alias testrepohg="hg $extraoptions" alias testrepohgenv=: fi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/heredoctest.py0000644000000000000000000000112014751647721013654 0ustar00import sys def flush(): sys.stdout.flush() sys.stderr.flush() globalvars = {} lines = sys.stdin.readlines() while lines: l = lines.pop(0) if l.startswith('SALT'): print(l[:-1]) elif l.startswith('>>> '): snippet = l[4:] while lines and lines[0].startswith('... '): l = lines.pop(0) snippet += l[4:] c = compile(snippet, '', 'single') try: flush() exec(c, globalvars) flush() except Exception as inst: flush() print(repr(inst)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/hghave0000755000000000000000000000341714751647721012174 0ustar00#!/usr/bin/env python """Test the running system for features availability. Exit with zero if all features are there, non-zero otherwise. If a feature name is prefixed with "no-", the absence of feature is tested. """ import hghave import optparse import os import sys checks = hghave.checks def list_features(): for name, feature in sorted(checks.items()): desc = feature[1] print(name + ':', desc) def test_features(): failed = 0 for name, feature in checks.items(): check, _ = feature try: check() except Exception as e: print("feature %s failed: %s" % (name, e)) failed += 1 return failed parser = optparse.OptionParser("%prog [options] [features]") parser.add_option( "--test-features", action="store_true", help="test available features" ) parser.add_option( "--list-features", action="store_true", help="list available features" ) def _loadaddon(): if 'TESTDIR' in os.environ: # loading from '.' isn't needed, because `hghave` should be # running at TESTTMP in this case path = os.environ['TESTDIR'] else: path = '.' if not os.path.exists(os.path.join(path, 'hghaveaddon.py')): return sys.path.insert(0, path) try: import hghaveaddon assert hghaveaddon # silence pyflakes except BaseException as inst: sys.stderr.write( 'failed to import hghaveaddon.py from %r: %s\n' % (path, inst) ) sys.exit(2) sys.path.pop(0) if __name__ == '__main__': options, args = parser.parse_args() _loadaddon() if options.list_features: list_features() sys.exit(0) if options.test_features: sys.exit(test_features()) hghave.require(args) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/hghave.py0000755000000000000000000007523514751647721012632 0ustar00import distutils.version import os import re import socket import stat import subprocess import sys import tempfile tempprefix = 'hg-hghave-' checks = { "true": (lambda: True, "yak shaving"), "false": (lambda: False, "nail clipper"), "known-bad-output": (lambda: True, "use for currently known bad output"), "missing-correct-output": (lambda: False, "use for missing good output"), } try: import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) except ImportError: pass stdout = getattr(sys.stdout, 'buffer', sys.stdout) stderr = getattr(sys.stderr, 'buffer', sys.stderr) def _sys2bytes(p): if p is None: return p return p.encode('utf-8') def _bytes2sys(p): if p is None: return p return p.decode('utf-8') def check(name, desc): """Registers a check function for a feature.""" def decorator(func): checks[name] = (func, desc) return func return decorator def checkvers(name, desc, vers): """Registers a check function for each of a series of versions. vers can be a list or an iterator. Produces a series of feature checks that have the form without any punctuation (even if there's punctuation in 'vers'; i.e. this produces 'py38', not 'py3.8' or 'py-38').""" def decorator(func): def funcv(v): def f(): return func(v) return f for v in vers: v = str(v) f = funcv(v) checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v) return func return decorator def checkfeatures(features): result = { 'error': [], 'missing': [], 'skipped': [], } for feature in features: negate = feature.startswith('no-') if negate: feature = feature[3:] if feature not in checks: result['missing'].append(feature) continue check, desc = checks[feature] try: available = check() except Exception as e: result['error'].append('hghave check %s failed: %r' % (feature, e)) continue if not negate and not available: result['skipped'].append('missing feature: %s' % desc) elif negate and available: result['skipped'].append('system supports %s' % desc) return result def require(features): """Require that features are available, exiting if not.""" result = checkfeatures(features) for missing in result['missing']: stderr.write( ('skipped: unknown feature: %s\n' % missing).encode('utf-8') ) for msg in result['skipped']: stderr.write(('skipped: %s\n' % msg).encode('utf-8')) for msg in result['error']: stderr.write(('%s\n' % msg).encode('utf-8')) if result['missing']: sys.exit(2) if result['skipped'] or result['error']: sys.exit(1) def matchoutput(cmd, regexp, ignorestatus=False): """Return the match object if cmd executes successfully and its output is matched by the supplied regular expression. """ # Tests on Windows have to fake USERPROFILE to point to the test area so # that `~` is properly expanded on py3.8+. However, some tools like black # make calls that need the real USERPROFILE in order to run `foo --version`. env = os.environ if os.name == 'nt': env = os.environ.copy() env['USERPROFILE'] = env['REALUSERPROFILE'] r = re.compile(regexp) p = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, ) s = p.communicate()[0] ret = p.returncode return (ignorestatus or not ret) and r.search(s) @check("baz", "GNU Arch baz client") def has_baz(): return matchoutput('baz --version 2>&1', br'baz Bazaar version') @check("bzr", "Breezy library and executable version >= 3.1") def has_bzr(): try: # Test the Breezy python lib import breezy import breezy.bzr.bzrdir import breezy.errors import breezy.revision import breezy.revisionspec breezy.revisionspec.RevisionSpec if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1): return False except (AttributeError, ImportError): return False # Test the executable return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ') @check("chg", "running with chg") def has_chg(): return 'CHG_INSTALLED_AS_HG' in os.environ @check("rhg", "running with rhg as 'hg'") def has_rhg(): return 'RHG_INSTALLED_AS_HG' in os.environ @check("pyoxidizer", "running with pyoxidizer build as 'hg'") def has_pyoxidizer(): return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ @check( "pyoxidizer-in-memory", "running with pyoxidizer build as 'hg' with embedded resources", ) def has_pyoxidizer_mem(): return 'PYOXIDIZED_IN_MEMORY_RSRC' in os.environ @check( "pyoxidizer-in-filesystem", "running with pyoxidizer build as 'hg' with external resources", ) def has_pyoxidizer_fs(): return 'PYOXIDIZED_FILESYSTEM_RSRC' in os.environ @check("cvs", "cvs client/server") def has_cvs(): re = br'Concurrent Versions System.*?server' return matchoutput('cvs --version 2>&1', re) and not has_msys() @check("cvs112", "cvs client/server 1.12.* (not cvsnt)") def has_cvs112(): re = br'Concurrent Versions System \(CVS\) 1.12.*?server' return matchoutput('cvs --version 2>&1', re) and not has_msys() @check("cvsnt", "cvsnt client/server") def has_cvsnt(): re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)' return matchoutput('cvsnt --version 2>&1', re) @check("darcs", "darcs client") def has_darcs(): return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True) @check("mtn", "monotone client (>= 1.0)") def has_mtn(): return matchoutput('mtn --version', br'monotone', True) and not matchoutput( 'mtn --version', br'monotone 0\.', True ) @check("eol-in-paths", "end-of-lines in paths") def has_eol_in_paths(): try: fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r') os.close(fd) os.remove(path) return True except (IOError, OSError): return False @check("execbit", "executable bit") def has_executablebit(): try: EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix) try: os.close(fh) m = os.stat(fn).st_mode & 0o777 new_file_has_exec = m & EXECFLAGS os.chmod(fn, m ^ EXECFLAGS) exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m finally: os.unlink(fn) except (IOError, OSError): # we don't care, the user probably won't be able to commit anyway return False return not (new_file_has_exec or exec_flags_cannot_flip) @check("suidbit", "setuid and setgid bit") def has_suidbit(): if ( getattr(os, "statvfs", None) is None or getattr(os, "ST_NOSUID", None) is None ): return False return bool(os.statvfs('.').f_flag & os.ST_NOSUID) @check("icasefs", "case insensitive file system") def has_icasefs(): # Stolen from mercurial.util fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fd) try: s1 = os.stat(path) d, b = os.path.split(path) p2 = os.path.join(d, b.upper()) if path == p2: p2 = os.path.join(d, b.lower()) try: s2 = os.stat(p2) return s2 == s1 except OSError: return False finally: os.remove(path) @check("fifo", "named pipes") def has_fifo(): if getattr(os, "mkfifo", None) is None: return False name = tempfile.mktemp(dir='.', prefix=tempprefix) try: os.mkfifo(name) os.unlink(name) return True except OSError: return False @check("killdaemons", 'killdaemons.py support') def has_killdaemons(): return True @check("cacheable", "cacheable filesystem") def has_cacheable_fs(): from mercurial import util fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fd) try: return util.cachestat(_sys2bytes(path)).cacheable() finally: os.remove(path) @check("lsprof", "python lsprof module") def has_lsprof(): try: import _lsprof _lsprof.Profiler # silence unused import warning return True except ImportError: return False def _gethgversion(): m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)') if not m: return (0, 0) return (int(m.group(1)), int(m.group(2))) _hgversion = None def gethgversion(): global _hgversion if _hgversion is None: _hgversion = _gethgversion() return _hgversion @checkvers( "hg", "Mercurial >= %s", list([(1.0 * x) / 10 for x in range(9, 99)]) ) def has_hg_range(v): major, minor = v.split('.')[0:2] return gethgversion() >= (int(major), int(minor)) @check("rust", "Using the Rust extensions") def has_rust(): """Check is the mercurial currently running is using some rust code""" cmd = 'hg debuginstall --quiet 2>&1' match = br'checking module policy \(([^)]+)\)' policy = matchoutput(cmd, match) if not policy: return False return b'rust' in policy.group(1) @check("hg08", "Mercurial >= 0.8") def has_hg08(): if checks["hg09"][0](): return True return matchoutput('hg help annotate 2>&1', '--date') @check("hg07", "Mercurial >= 0.7") def has_hg07(): if checks["hg08"][0](): return True return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM') @check("hg06", "Mercurial >= 0.6") def has_hg06(): if checks["hg07"][0](): return True return matchoutput('hg --version --quiet 2>&1', 'Mercurial version') @check("gettext", "GNU Gettext (msgfmt)") def has_gettext(): return matchoutput('msgfmt --version', br'GNU gettext-tools') @check("git", "git command line client") def has_git(): return matchoutput('git --version 2>&1', br'^git version') def getgitversion(): m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)') if not m: return (0, 0) return (int(m.group(1)), int(m.group(2))) @check("dulwich", "Dulwich Python library") def has_dulwich(): try: from dulwich import client client.ZERO_SHA # silence unused import return True except ImportError: return False @checkvers("dulwich", "Dulwich >= %s", ['%d.%d.%d' % vers for vers in ()]) def has_dulwich_range(v): import dulwich return dulwich.__version__ >= tuple(map(int, v.split('.'))) @check("dulwich-rust", "Dulwich Python library using Rust bindings") def has_dulwich_rust(): try: from dulwich import objects, __version__ return ( __version__ > (0, 22, 0) and objects._parse_tree_py is not objects.parse_tree ) except ImportError: return False @check("pygit2", "pygit2 Python library") def has_pygit2(): try: import pygit2 pygit2.Oid # silence unused import return True except ImportError: return False # https://github.com/git-lfs/lfs-test-server @check("lfs-test-server", "git-lfs test server") def has_lfsserver(): exe = 'lfs-test-server' if has_windows(): exe = 'lfs-test-server.exe' return any( os.access(os.path.join(path, exe), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) ) @checkvers("git", "git client (with ext::sh support) version >= %s", (1.9,)) def has_git_range(v): major, minor = v.split('.')[0:2] return getgitversion() >= (int(major), int(minor)) @check("docutils", "Docutils text processing library") def has_docutils(): try: import docutils.core docutils.core.publish_cmdline # silence unused import return True except ImportError: return False def getsvnversion(): m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)') if not m: return (0, 0) return (int(m.group(1)), int(m.group(2))) @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5)) def has_svn_range(v): major, minor = v.split('.')[0:2] return getsvnversion() >= (int(major), int(minor)) @check("svn", "subversion client and admin tools") def has_svn(): return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput( 'svnadmin --version 2>&1', br'^svnadmin, version' ) @check("svn-bindings", "subversion python bindings") def has_svn_bindings(): try: import svn.core version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR if version < (1, 4): return False return True except ImportError: return False @check("p4", "Perforce server and client") def has_p4(): return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput( 'p4d -V', br'Rev\. P4D/' ) @check("symlink", "symbolic links") def has_symlink(): # mercurial.windows.checklink() is a hard 'no' at the moment if os.name == 'nt' or getattr(os, "symlink", None) is None: return False name = tempfile.mktemp(dir='.', prefix=tempprefix) try: os.symlink(".", name) os.unlink(name) return True except (OSError, AttributeError): return False @check("hardlink", "hardlinks") def has_hardlink(): from mercurial import util fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fh) name = tempfile.mktemp(dir='.', prefix=tempprefix) try: util.oslink(_sys2bytes(fn), _sys2bytes(name)) os.unlink(name) return True except OSError: return False finally: os.unlink(fn) @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems") def has_hardlink_whitelisted(): from mercurial import util try: fstype = util.getfstype(b'.') except OSError: return False return fstype in util._hardlinkfswhitelist @check("rmcwd", "can remove current working directory") def has_rmcwd(): ocwd = os.getcwd() temp = tempfile.mkdtemp(dir='.', prefix=tempprefix) try: os.chdir(temp) # On Linux, 'rmdir .' isn't allowed, but the other names are okay. # On Solaris and Windows, the cwd can't be removed by any names. os.rmdir(os.getcwd()) return True except OSError: return False finally: os.chdir(ocwd) # clean up temp dir on platforms where cwd can't be removed try: os.rmdir(temp) except OSError: pass @check("tla", "GNU Arch tla client") def has_tla(): return matchoutput('tla --version 2>&1', br'The GNU Arch Revision') @check("gpg", "gpg client") def has_gpg(): return matchoutput('gpg --version 2>&1', br'GnuPG') @check("gpg2", "gpg client v2") def has_gpg2(): return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.') @check("gpg21", "gpg client v2.1+") def has_gpg21(): return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)') @check("unix-permissions", "unix-style permissions") def has_unix_permissions(): d = tempfile.mkdtemp(dir='.', prefix=tempprefix) try: fname = os.path.join(d, 'foo') for umask in (0o77, 0o07, 0o22): os.umask(umask) f = open(fname, 'w') f.close() mode = os.stat(fname).st_mode os.unlink(fname) if mode & 0o777 != ~umask & 0o666: return False return True finally: os.rmdir(d) @check("unix-socket", "AF_UNIX socket family") def has_unix_socket(): return getattr(socket, 'AF_UNIX', None) is not None @check("root", "root permissions") def has_root(): return getattr(os, 'geteuid', None) and os.geteuid() == 0 @check("pyflakes", "Pyflakes python linter") def has_pyflakes(): try: import pyflakes pyflakes.__version__ except ImportError: return False else: return True @check("pylint", "Pylint python linter") def has_pylint(): try: import pylint pylint.__version__ except ImportError: return False else: return True @check("clang-format", "clang-format C code formatter (>= 11)") def has_clang_format(): m = matchoutput('clang-format --version', br'clang-format version (\d+)') # style changed somewhere between 10.x and 11.x if m: return int(m.group(1)) >= 11 # Assist Googler contributors, they have a centrally-maintained version of # clang-format that is generally very fresh, but unlike most builds (both # official and unofficial), it does *not* include a version number. return matchoutput( 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)' ) @check("jshint", "JSHint static code analysis tool") def has_jshint(): return matchoutput("jshint --version 2>&1", br"jshint v") @check("pygments", "Pygments source highlighting library") def has_pygments(): try: import pygments pygments.highlight # silence unused import warning return True except ImportError: return False def getpygmentsversion(): try: import pygments v = pygments.__version__ parts = v.split(".") return (int(parts[0]), int(parts[1])) except ImportError: return (0, 0) @checkvers("pygments", "Pygments version >= %s", (2.5, 2.11, 2.14)) def has_pygments_range(v): major, minor = v.split('.')[0:2] return getpygmentsversion() >= (int(major), int(minor)) @check("outer-repo", "outer repo") def has_outer_repo(): # failing for other reasons than 'no repo' imply that there is a repo return not matchoutput('hg root 2>&1', br'abort: no repository found', True) @check("ssl", "ssl module available") def has_ssl(): try: import ssl ssl.CERT_NONE return True except ImportError: return False @check("defaultcacertsloaded", "detected presence of loaded system CA certs") def has_defaultcacertsloaded(): import ssl from mercurial import sslutil, ui as uimod ui = uimod.ui.load() cafile = sslutil._defaultcacerts(ui) ctx = ssl.create_default_context() if cafile: ctx.load_verify_locations(cafile=cafile) else: ctx.load_default_certs() return len(ctx.get_ca_certs()) > 0 @check("tls1.2", "TLS 1.2 protocol support") def has_tls1_2(): from mercurial import sslutil return b'tls1.2' in sslutil.supportedprotocols @check("windows", "Windows") def has_windows(): return os.name == 'nt' @check("system-sh", "system() uses sh") def has_system_sh(): return os.name != 'nt' @check("serve", "platform and python can manage 'hg serve -d'") def has_serve(): return True @check("setprocname", "whether osutil.setprocname is available or not") def has_setprocname(): try: from mercurial.utils import procutil procutil.setprocname return True except AttributeError: return False @check("test-repo", "running tests from repository") def has_test_repo(): t = os.environ["TESTDIR"] return os.path.isdir(os.path.join(t, "..", ".hg")) @check("network-io", "whether tests are allowed to access 3rd party services") def has_network_io(): t = os.environ.get("HGTESTS_ALLOW_NETIO") return t == "1" @check("curses", "terminfo compiler and curses module") def has_curses(): try: import curses curses.COLOR_BLUE # Windows doesn't have a `tic` executable, but the windows_curses # package is sufficient to run the tests without it. if os.name == 'nt': return True return has_tic() except (ImportError, AttributeError): return False @check("tic", "terminfo compiler") def has_tic(): return matchoutput('test -x "`which tic`"', br'') @check("xz", "xz compression utility") def has_xz(): # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which # only knows `where`, not `which`. So invoke MSYS shell explicitly. return matchoutput("sh -c 'test -x \"`which xz`\"'", b'') @check("msys", "Windows with MSYS") def has_msys(): return os.getenv('MSYSTEM') @check("aix", "AIX") def has_aix(): return sys.platform.startswith("aix") @check("osx", "OS X") def has_osx(): return sys.platform == 'darwin' @check("osxpackaging", "OS X packaging tools") def has_osxpackaging(): try: return ( matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1) and matchoutput( 'productbuild', br'Usage: productbuild ', ignorestatus=1 ) and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1) and matchoutput('xar --help', br'Usage: xar', ignorestatus=1) ) except ImportError: return False @check('linuxormacos', 'Linux or MacOS') def has_linuxormacos(): # This isn't a perfect test for MacOS. But it is sufficient for our needs. return sys.platform.startswith(('linux', 'darwin')) @check("docker", "docker support") def has_docker(): pat = br'A self-sufficient runtime for' if matchoutput('docker --help', pat): if 'linux' not in sys.platform: # TODO: in theory we should be able to test docker-based # package creation on non-linux using boot2docker, but in # practice that requires extra coordination to make sure # $TESTTEMP is going to be visible at the same path to the # boot2docker VM. If we figure out how to verify that, we # can use the following instead of just saying False: # return 'DOCKER_HOST' in os.environ return False return True return False @check("debhelper", "debian packaging tools") def has_debhelper(): # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first # quote), so just accept anything in that spot. dpkg = matchoutput( 'dpkg --version', br"Debian .dpkg' package management program" ) dh = matchoutput( 'dh --help', br'dh is a part of debhelper.', ignorestatus=True ) dh_py2 = matchoutput( 'dh_python2 --help', br'other supported Python versions' ) # debuild comes from the 'devscripts' package, though you might want # the 'build-debs' package instead, which has a dependency on devscripts. debuild = matchoutput( 'debuild --help', br'to run debian/rules with given parameter' ) return dpkg and dh and dh_py2 and debuild @check( "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)" ) def has_debdeps(): # just check exit status (ignoring output) path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR'] return matchoutput('dpkg-checkbuilddeps %s' % path, br'') @check("demandimport", "demandimport enabled") def has_demandimport(): # chg disables demandimport intentionally for performance wins. return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable' # Add "py36", "py37", ... as possible feature checks. Note that there's no # punctuation here. @checkvers("py", "Python >= %s", (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)) def has_python_range(v): major, minor = v.split('.')[0:2] py_major, py_minor = sys.version_info.major, sys.version_info.minor return (py_major, py_minor) >= (int(major), int(minor)) @check("py3", "running with Python 3.x") def has_py3(): return 3 == sys.version_info[0] @check("py3exe", "a Python 3.x interpreter is available") def has_python3exe(): py = 'python3' if os.name == 'nt': py = 'py -3' return matchoutput('%s -V' % py, br'^Python 3.(6|7|8|9|10|11)') @check("pure", "running with pure Python code") def has_pure(): return any( [ os.environ.get("HGMODULEPOLICY") == "py", os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure", ] ) @check("slow", "allow slow tests (use --allow-slow-tests)") def has_slow(): return os.environ.get('HGTEST_SLOW') == 'slow' @check("hypothesis", "Hypothesis automated test generation") def has_hypothesis(): try: import hypothesis hypothesis.given return True except ImportError: return False @check("unziplinks", "unzip(1) understands and extracts symlinks") def unzip_understands_symlinks(): return matchoutput('unzip --help', br'Info-ZIP') @check("zstd", "zstd Python module available") def has_zstd(): try: import mercurial.zstd mercurial.zstd.__version__ return True except ImportError: return False @check("devfull", "/dev/full special file") def has_dev_full(): return os.path.exists('/dev/full') @check("ensurepip", "ensurepip module") def has_ensurepip(): try: import ensurepip ensurepip.bootstrap return True except ImportError: return False @check("virtualenv", "virtualenv support") def has_virtualenv(): try: import virtualenv # --no-site-package became the default in 1.7 (Nov 2011), and the # argument was removed in 20.0 (Feb 2020). Rather than make the # script complicated, just ignore ancient versions. return int(virtualenv.__version__.split('.')[0]) > 1 except (AttributeError, ImportError, IndexError): return False @check("fsmonitor", "running tests with fsmonitor") def has_fsmonitor(): return 'HGFSMONITOR_TESTS' in os.environ @check("fuzzywuzzy", "Fuzzy string matching library") def has_fuzzywuzzy(): try: import fuzzywuzzy fuzzywuzzy.__version__ return True except ImportError: return False @check("clang-libfuzzer", "clang new enough to include libfuzzer") def has_clang_libfuzzer(): mat = matchoutput('clang --version', br'clang version (\d)') if mat: # libfuzzer is new in clang 6 return int(mat.group(1)) > 5 return False @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)") def has_clang60(): return matchoutput('clang-6.0 --version', br'clang version 6\.') @check("xdiff", "xdiff algorithm") def has_xdiff(): try: from mercurial import policy bdiff = policy.importmod('bdiff') return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)] except (ImportError, AttributeError): return False @check('extraextensions', 'whether tests are running with extra extensions') def has_extraextensions(): return 'HGTESTEXTRAEXTENSIONS' in os.environ def getrepofeatures(): """Obtain set of repository features in use. HGREPOFEATURES can be used to define or remove features. It contains a space-delimited list of feature strings. Strings beginning with ``-`` mean to remove. """ # Default list provided by core. features = { 'bundlerepo', 'revlogstore', 'fncache', } # Features that imply other features. implies = { 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'], } for override in os.environ.get('HGREPOFEATURES', '').split(' '): if not override: continue if override.startswith('-'): if override[1:] in features: features.remove(override[1:]) else: features.add(override) for imply in implies.get(override, []): if imply.startswith('-'): if imply[1:] in features: features.remove(imply[1:]) else: features.add(imply) return features @check('reporevlogstore', 'repository using the default revlog store') def has_reporevlogstore(): return 'revlogstore' in getrepofeatures() @check('reposimplestore', 'repository using simple storage extension') def has_reposimplestore(): return 'simplestore' in getrepofeatures() @check('repobundlerepo', 'whether we can open bundle files as repos') def has_repobundlerepo(): return 'bundlerepo' in getrepofeatures() @check('repofncache', 'repository has an fncache') def has_repofncache(): return 'fncache' in getrepofeatures() @check('dirstate-v2', 'using the v2 format of .hg/dirstate') def has_dirstate_v2(): # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py` return matchoutput( 'hg config format.use-dirstate-v2', b'(?i)1|yes|true|on|always' ) @check('sqlite', 'sqlite3 module and matching cli is available') def has_sqlite(): try: import sqlite3 version = sqlite3.sqlite_version_info except ImportError: return False if version < (3, 8, 3): # WITH clause not supported return False return matchoutput('sqlite3 -version', br'^3\.\d+') @check('vcr', 'vcr http mocking library (pytest-vcr)') def has_vcr(): try: import vcr vcr.VCR return True except (ImportError, AttributeError): pass return False @check('emacs', 'GNU Emacs') def has_emacs(): # Our emacs lisp uses `with-eval-after-load` which is new in emacs # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last # 24 release) return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)') @check('black', 'the black formatter for python (>= 22.3)') def has_black(): try: import black version = black.__version__ except ImportError: version = None sv = distutils.version.StrictVersion return version and sv(version) >= sv('22.3') @check('pytype', 'the pytype type checker') def has_pytype(): pytypecmd = 'pytype --version' version = matchoutput(pytypecmd, b'[0-9a-b.]+') sv = distutils.version.StrictVersion return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17') @check("rustfmt", "rustfmt tool at version nightly-2021-11-02") def has_rustfmt(): # We use Nightly's rustfmt due to current unstable config options. return matchoutput( '`rustup which --toolchain nightly-2021-11-02 rustfmt` --version', b'rustfmt', ) @check("cargo", "cargo tool") def has_cargo(): return matchoutput('`rustup which cargo` --version', b'cargo') @check("lzma", "python lzma module") def has_lzma(): try: import _lzma _lzma.FORMAT_XZ return True except ImportError: return False @check("bash", "bash shell") def has_bash(): return matchoutput("bash -c 'echo hi'", b'^hi$') @check("unicodefs", "Unicode-only file system") def has_unicode_filesystem(): try: with tempfile.NamedTemporaryFile( prefix="bøf".encode("latin-1"), dir=b"." ): return False except Exception: return True @check("bigendian", "big-endian CPU") def has_bigendian(): return sys.byteorder == 'big' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/killdaemons.py0000755000000000000000000001026714751647721013664 0ustar00#!/usr/bin/env python3 import os import signal import sys import time if os.name == 'nt': import ctypes _BOOL = ctypes.c_long _DWORD = ctypes.c_ulong _UINT = ctypes.c_uint _HANDLE = ctypes.c_void_p ctypes.windll.kernel32.CloseHandle.argtypes = [_HANDLE] ctypes.windll.kernel32.CloseHandle.restype = _BOOL ctypes.windll.kernel32.GetLastError.argtypes = [] ctypes.windll.kernel32.GetLastError.restype = _DWORD ctypes.windll.kernel32.OpenProcess.argtypes = [_DWORD, _BOOL, _DWORD] ctypes.windll.kernel32.OpenProcess.restype = _HANDLE ctypes.windll.kernel32.TerminateProcess.argtypes = [_HANDLE, _UINT] ctypes.windll.kernel32.TerminateProcess.restype = _BOOL ctypes.windll.kernel32.WaitForSingleObject.argtypes = [_HANDLE, _DWORD] ctypes.windll.kernel32.WaitForSingleObject.restype = _DWORD def _check(ret, expectederr=None): if ret == 0: winerrno = ctypes.GetLastError() if winerrno == expectederr: return True raise ctypes.WinError(winerrno) def kill(pid, logfn, tryhard=True): logfn('# Killing daemon process %d' % pid) PROCESS_TERMINATE = 1 PROCESS_QUERY_INFORMATION = 0x400 SYNCHRONIZE = 0x00100000 WAIT_OBJECT_0 = 0 WAIT_TIMEOUT = 258 WAIT_FAILED = _DWORD(0xFFFFFFFF).value handle = ctypes.windll.kernel32.OpenProcess( PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_INFORMATION, False, pid, ) if handle is None: _check(0, 87) # err 87 when process not found return # process not found, already finished try: r = ctypes.windll.kernel32.WaitForSingleObject(handle, 100) if r == WAIT_OBJECT_0: pass # terminated, but process handle still available elif r == WAIT_TIMEOUT: _check(ctypes.windll.kernel32.TerminateProcess(handle, -1)) elif r == WAIT_FAILED: _check(0) # err stored in GetLastError() # TODO?: forcefully kill when timeout # and ?shorter waiting time? when tryhard==True r = ctypes.windll.kernel32.WaitForSingleObject(handle, 100) # timeout = 100 ms if r == WAIT_OBJECT_0: pass # process is terminated elif r == WAIT_TIMEOUT: logfn('# Daemon process %d is stuck') elif r == WAIT_FAILED: _check(0) # err stored in GetLastError() except: # re-raises ctypes.windll.kernel32.CloseHandle(handle) # no _check, keep error raise _check(ctypes.windll.kernel32.CloseHandle(handle)) else: def kill(pid, logfn, tryhard=True): try: os.kill(pid, 0) logfn('# Killing daemon process %d' % pid) os.kill(pid, signal.SIGTERM) if tryhard: for i in range(10): time.sleep(0.05) os.kill(pid, 0) else: time.sleep(0.1) os.kill(pid, 0) logfn('# Daemon process %d is stuck - really killing it' % pid) os.kill(pid, signal.SIGKILL) except ProcessLookupError: pass def killdaemons(pidfile, tryhard=True, remove=False, logfn=None): if not logfn: logfn = lambda s: s # Kill off any leftover daemon processes try: pids = [] with open(pidfile) as fp: for line in fp: try: pid = int(line) if pid <= 0: raise ValueError except ValueError: logfn( '# Not killing daemon process %s - invalid pid' % line.rstrip() ) continue pids.append(pid) for pid in pids: kill(pid, logfn, tryhard) if remove: os.unlink(pidfile) except IOError: pass if __name__ == '__main__': if len(sys.argv) > 1: (path,) = sys.argv[1:] else: path = os.environ["DAEMON_PIDS"] killdaemons(path, remove=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/latin-1-encoding0000644000000000000000000000062114751647721013752 0ustar00# -*- coding: latin-1 -*- # this file contains some latin-1 messages for test-encoding GIT_AUTHOR_NAME='tést èncödîng'; export GIT_AUTHOR_NAME echo beta > beta git add beta fn_git_commit -m 'add beta' echo gamma > gamma git add gamma fn_git_commit -m 'add gämmâ' # test the commit encoding field git config i18n.commitencoding latin-1 echo delta > delta git add delta fn_git_commit -m 'add déltà' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/run-tests.py0000755000000000000000000042632714751647721013336 0ustar00#!/usr/bin/env python3 # # run-tests.py - Run a set of tests on Mercurial # # Copyright 2006 Olivia Mackall # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. # Modifying this script is tricky because it has many modes: # - serial (default) vs parallel (-jN, N > 1) # - no coverage (default) vs coverage (-c, -C, -s) # - temp install (default) vs specific hg script (--with-hg, --local) # - tests are a mix of shell scripts and Python scripts # # If you change this script, it is recommended that you ensure you # haven't broken it by running it in various modes with a representative # sample of test scripts. For example: # # 1) serial, no coverage, temp install: # ./run-tests.py test-s* # 2) serial, no coverage, local hg: # ./run-tests.py --local test-s* # 3) serial, coverage, temp install: # ./run-tests.py -c test-s* # 4) serial, coverage, local hg: # ./run-tests.py -c --local test-s* # unsupported # 5) parallel, no coverage, temp install: # ./run-tests.py -j2 test-s* # 6) parallel, no coverage, local hg: # ./run-tests.py -j2 --local test-s* # 7) parallel, coverage, temp install: # ./run-tests.py -j2 -c test-s* # currently broken # 8) parallel, coverage, local install: # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) # 9) parallel, custom tmp dir: # ./run-tests.py -j2 --tmpdir /tmp/myhgtests # 10) parallel, pure, tests that call run-tests: # ./run-tests.py --pure `grep -l run-tests.py *.t` # # (You could use any subset of the tests: test-s* happens to match # enough that it's worth doing parallel runs, few enough that it # completes fairly quickly, includes both shell and Python scripts, and # includes some scripts that run daemon processes.) import argparse import collections import contextlib import difflib import errno import functools import json import multiprocessing import os import platform import queue import random import re import shlex import shutil import signal import socket import subprocess import sys import sysconfig import tempfile import threading import time import unittest import uuid import xml.dom.minidom as minidom if sys.version_info < (3, 5, 0): print( '%s is only supported on Python 3.5+, not %s' % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])) ) sys.exit(70) # EX_SOFTWARE from `man 3 sysexit` MACOS = sys.platform == 'darwin' WINDOWS = os.name == r'nt' shellquote = shlex.quote processlock = threading.Lock() pygmentspresent = False try: # is pygments installed import pygments import pygments.lexers as lexers import pygments.lexer as lexer import pygments.formatters as formatters import pygments.token as token import pygments.style as style if WINDOWS: hgpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(hgpath) try: from mercurial import win32 # pytype: disable=import-error # Don't check the result code because it fails on heptapod, but # something is able to convert to color anyway. win32.enablevtmode() finally: sys.path = sys.path[:-1] pygmentspresent = True difflexer = lexers.DiffLexer() terminal256formatter = formatters.Terminal256Formatter() except ImportError: pass if pygmentspresent: class TestRunnerStyle(style.Style): default_style = "" skipped = token.string_to_tokentype("Token.Generic.Skipped") failed = token.string_to_tokentype("Token.Generic.Failed") skippedname = token.string_to_tokentype("Token.Generic.SName") failedname = token.string_to_tokentype("Token.Generic.FName") styles = { skipped: '#e5e5e5', skippedname: '#00ffff', failed: '#7f0000', failedname: '#ff0000', } class TestRunnerLexer(lexer.RegexLexer): testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?' tokens = { 'root': [ (r'^Skipped', token.Generic.Skipped, 'skipped'), (r'^Failed ', token.Generic.Failed, 'failed'), (r'^ERROR: ', token.Generic.Failed, 'failed'), ], 'skipped': [ (testpattern, token.Generic.SName), (r':.*', token.Generic.Skipped), ], 'failed': [ (testpattern, token.Generic.FName), (r'(:| ).*', token.Generic.Failed), ], } runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle) runnerlexer = TestRunnerLexer() origenviron = os.environ.copy() def _sys2bytes(p): if p is None: return p return p.encode('utf-8') def _bytes2sys(p): if p is None: return p return p.decode('utf-8') original_env = os.environ.copy() osenvironb = getattr(os, 'environb', None) if osenvironb is None: # Windows lacks os.environb, for instance. A proxy over the real thing # instead of a copy allows the environment to be updated via bytes on # all platforms. class environbytes: def __init__(self, strenv): self.__len__ = strenv.__len__ self.clear = strenv.clear self._strenv = strenv def __getitem__(self, k): v = self._strenv.__getitem__(_bytes2sys(k)) return _sys2bytes(v) def __setitem__(self, k, v): self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v)) def __delitem__(self, k): self._strenv.__delitem__(_bytes2sys(k)) def __contains__(self, k): return self._strenv.__contains__(_bytes2sys(k)) def __iter__(self): return iter([_sys2bytes(k) for k in iter(self._strenv)]) def get(self, k, default=None): v = self._strenv.get(_bytes2sys(k), _bytes2sys(default)) return _sys2bytes(v) def pop(self, k, default=None): v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default)) return _sys2bytes(v) osenvironb = environbytes(os.environ) getcwdb = getattr(os, 'getcwdb') if not getcwdb or WINDOWS: getcwdb = lambda: _sys2bytes(os.getcwd()) if WINDOWS: _getcwdb = getcwdb def getcwdb(): cwd = _getcwdb() if re.match(b'^[a-z]:', cwd): # os.getcwd() is inconsistent on the capitalization of the drive # letter, so adjust it. see https://bugs.python.org/issue40368 cwd = cwd[0:1].upper() + cwd[1:] return cwd # For Windows support wifexited = getattr(os, "WIFEXITED", lambda x: False) # Whether to use IPv6 def checksocketfamily(name, port=20058): """return true if we can listen on localhost using family=name name should be either 'AF_INET', or 'AF_INET6'. port being used is okay - EADDRINUSE is considered as successful. """ family = getattr(socket, name, None) if family is None: return False try: s = socket.socket(family, socket.SOCK_STREAM) s.bind(('localhost', port)) s.close() return True except (socket.error, OSError) as exc: if exc.errno == errno.EADDRINUSE: return True elif exc.errno in ( errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT, errno.EAFNOSUPPORT, ): return False else: raise else: return False # useipv6 will be set by parseargs useipv6 = None def checkportisavailable(port): """return true if a port seems free to bind on localhost""" if useipv6: family = socket.AF_INET6 else: family = socket.AF_INET try: with contextlib.closing(socket.socket(family, socket.SOCK_STREAM)) as s: s.bind(('localhost', port)) return True except PermissionError: return False except socket.error as exc: if WINDOWS and exc.errno == errno.WSAEACCES: return False if exc.errno not in ( errno.EADDRINUSE, errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT, ): raise return False closefds = os.name == 'posix' def Popen4(cmd, wd, timeout, env=None): processlock.acquire() p = subprocess.Popen( _bytes2sys(cmd), shell=True, bufsize=-1, cwd=_bytes2sys(wd), env=env, close_fds=closefds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) processlock.release() p.fromchild = p.stdout p.tochild = p.stdin p.childerr = p.stderr p.timeout = False if timeout: def t(): start = time.time() while time.time() - start < timeout and p.returncode is None: time.sleep(0.1) p.timeout = True vlog('# Timout reached for process %d' % p.pid) if p.returncode is None: terminate(p) threading.Thread(target=t).start() return p if sys.executable: sysexecutable = sys.executable elif os.environ.get('PYTHONEXECUTABLE'): sysexecutable = os.environ['PYTHONEXECUTABLE'] elif os.environ.get('PYTHON'): sysexecutable = os.environ['PYTHON'] else: raise AssertionError('Could not find Python interpreter') PYTHON = _sys2bytes(sysexecutable.replace('\\', '/')) IMPL_PATH = b'PYTHONPATH' if 'java' in sys.platform: IMPL_PATH = b'JYTHONPATH' default_defaults = { 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()), 'timeout': ('HGTEST_TIMEOUT', 360), 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500), 'port': ('HGTEST_PORT', 20059), 'shell': ('HGTEST_SHELL', 'sh'), } defaults = default_defaults.copy() def canonpath(path): return os.path.realpath(os.path.expanduser(path)) def which(exe): # shutil.which only accept bytes from 3.8 cmd = _bytes2sys(exe) real_exec = shutil.which(cmd) return _sys2bytes(real_exec) def parselistfiles(files, listtype, warn=True): entries = dict() for filename in files: try: path = os.path.expanduser(os.path.expandvars(filename)) f = open(path, "rb") except FileNotFoundError: if warn: print("warning: no such %s file: %s" % (listtype, filename)) continue for line in f.readlines(): line = line.split(b'#', 1)[0].strip() if line: # Ensure path entries are compatible with os.path.relpath() entries[os.path.normpath(line)] = filename f.close() return entries def parsettestcases(path): """read a .t test file, return a set of test case names If path does not exist, return an empty set. """ cases = [] try: with open(path, 'rb') as f: for l in f: if l.startswith(b'#testcases '): cases.append(sorted(l[11:].split())) except FileNotFoundError: pass return cases def getparser(): """Obtain the OptionParser used by the CLI.""" parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]') selection = parser.add_argument_group('Test Selection') selection.add_argument( '--allow-slow-tests', action='store_true', help='allow extremely slow tests', ) selection.add_argument( "--blacklist", action="append", help="skip tests listed in the specified blacklist file", ) selection.add_argument( "--changed", help="run tests that are changed in parent rev or working directory", ) selection.add_argument( "-k", "--keywords", help="run tests matching keywords" ) selection.add_argument( "-r", "--retest", action="store_true", help="retest failed tests" ) selection.add_argument( "--test-list", action="append", help="read tests to run from the specified file", ) selection.add_argument( "--whitelist", action="append", help="always run tests listed in the specified whitelist file", ) selection.add_argument( 'tests', metavar='TESTS', nargs='*', help='Tests to run' ) harness = parser.add_argument_group('Test Harness Behavior') harness.add_argument( '--bisect-repo', metavar='bisect_repo', help=( "Path of a repo to bisect. Use together with " "--known-good-rev" ), ) harness.add_argument( "-d", "--debug", action="store_true", help="debug mode: write output of test scripts to console" " rather than capturing and diffing it (disables timeout)", ) harness.add_argument( "-f", "--first", action="store_true", help="exit on the first test failure", ) harness.add_argument( "-i", "--interactive", action="store_true", help="prompt to accept changed output", ) harness.add_argument( "-j", "--jobs", type=int, help="number of jobs to run in parallel" " (default: $%s or %d)" % defaults['jobs'], ) harness.add_argument( "--keep-tmpdir", action="store_true", help="keep temporary directory after running tests", ) harness.add_argument( '--known-good-rev', metavar="known_good_rev", help=( "Automatically bisect any failures using this " "revision as a known-good revision." ), ) harness.add_argument( "--list-tests", action="store_true", help="list tests instead of running them", ) harness.add_argument( "--loop", action="store_true", help="loop tests repeatedly" ) harness.add_argument( '--random', action="store_true", help='run tests in random order' ) harness.add_argument( '--order-by-runtime', action="store_true", help='run slowest tests first, according to .testtimes', ) harness.add_argument( "-p", "--port", type=int, help="port on which servers should listen" " (default: $%s or %d)" % defaults['port'], ) harness.add_argument( '--profile-runner', action='store_true', help='run statprof on run-tests', ) harness.add_argument( "-R", "--restart", action="store_true", help="restart at last error" ) harness.add_argument( "--runs-per-test", type=int, dest="runs_per_test", help="run each test N times (default=1)", default=1, ) harness.add_argument( "--shell", help="shell to use (default: $%s or %s)" % defaults['shell'] ) harness.add_argument( '--showchannels', action='store_true', help='show scheduling channels' ) harness.add_argument( "--slowtimeout", type=int, help="kill errant slow tests after SLOWTIMEOUT seconds" " (default: $%s or %d)" % defaults['slowtimeout'], ) harness.add_argument( "-t", "--timeout", type=int, help="kill errant tests after TIMEOUT seconds" " (default: $%s or %d)" % defaults['timeout'], ) harness.add_argument( "--tmpdir", help="run tests in the given temporary directory" " (implies --keep-tmpdir)", ) harness.add_argument( "-v", "--verbose", action="store_true", help="output verbose messages" ) hgconf = parser.add_argument_group('Mercurial Configuration') hgconf.add_argument( "--chg", action="store_true", help="install and use chg wrapper in place of hg", ) hgconf.add_argument( "--chg-debug", action="store_true", help="show chg debug logs", ) hgconf.add_argument( "--rhg", action="store_true", help="install and use rhg Rust implementation in place of hg", ) hgconf.add_argument( "--pyoxidized", action="store_true", help="build the hg binary using pyoxidizer", ) hgconf.add_argument("--compiler", help="compiler to build with") hgconf.add_argument( '--extra-config-opt', action="append", default=[], help='set the given config opt in the test hgrc', ) hgconf.add_argument( "-l", "--local", action="store_true", help="shortcut for --with-hg=/../hg, " "--with-rhg=/../rust/target/release/rhg if --rhg is set, " "and --with-chg=/../contrib/chg/chg if --chg is set", ) hgconf.add_argument( "--ipv6", action="store_true", help="prefer IPv6 to IPv4 for network related tests", ) hgconf.add_argument( "--pure", action="store_true", help="use pure Python code instead of C extensions", ) hgconf.add_argument( "--rust", action="store_true", help="use Rust code alongside C extensions", ) hgconf.add_argument( "--no-rust", action="store_true", help="do not use Rust code even if compiled", ) hgconf.add_argument( "--with-chg", metavar="CHG", help="use specified chg wrapper in place of hg", ) hgconf.add_argument( "--with-rhg", metavar="RHG", help="use specified rhg Rust implementation in place of hg", ) hgconf.add_argument( "--with-hg", metavar="HG", help="test using specified hg script rather than a " "temporary installation", ) reporting = parser.add_argument_group('Results Reporting') reporting.add_argument( "-C", "--annotate", action="store_true", help="output files annotated with coverage", ) reporting.add_argument( "--color", choices=["always", "auto", "never"], default=os.environ.get('HGRUNTESTSCOLOR', 'auto'), help="colorisation: always|auto|never (default: auto)", ) reporting.add_argument( "-c", "--cover", action="store_true", help="print a test coverage report", ) reporting.add_argument( '--exceptions', action='store_true', help='log all exceptions and generate an exception report', ) reporting.add_argument( "-H", "--htmlcov", action="store_true", help="create an HTML report of the coverage of the files", ) reporting.add_argument( "--json", action="store_true", help="store test result data in 'report.json' file", ) reporting.add_argument( "--outputdir", help="directory to write error logs to (default=test directory)", ) reporting.add_argument( "-n", "--nodiff", action="store_true", help="skip showing test changes" ) reporting.add_argument( "-S", "--noskips", action="store_true", help="don't report skip tests verbosely", ) reporting.add_argument( "--time", action="store_true", help="time how long each test takes" ) reporting.add_argument("--view", help="external diff viewer") reporting.add_argument( "--xunit", help="record xunit results at specified path" ) for option, (envvar, default) in defaults.items(): defaults[option] = type(default)(os.environ.get(envvar, default)) parser.set_defaults(**defaults) return parser def parseargs(args, parser): """Parse arguments with our OptionParser and validate results.""" options = parser.parse_args(args) # jython is always pure if 'java' in sys.platform or '__pypy__' in sys.modules: options.pure = True if platform.python_implementation() != 'CPython' and options.rust: parser.error('Rust extensions are only available with CPython') if options.pure and options.rust: parser.error('--rust cannot be used with --pure') if options.rust and options.no_rust: parser.error('--rust cannot be used with --no-rust') if options.local: if options.with_hg or options.with_rhg or options.with_chg: parser.error( '--local cannot be used with --with-hg or --with-rhg or --with-chg' ) if options.pyoxidized: parser.error('--pyoxidized does not work with --local (yet)') testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) reporootdir = os.path.dirname(testdir) pathandattrs = [(b'hg', 'with_hg')] if options.chg: pathandattrs.append((b'contrib/chg/chg', 'with_chg')) if options.rhg: pathandattrs.append((b'rust/target/release/rhg', 'with_rhg')) for relpath, attr in pathandattrs: binpath = os.path.join(reporootdir, relpath) if not (WINDOWS or os.access(binpath, os.X_OK)): parser.error( '--local specified, but %r not found or ' 'not executable' % binpath ) setattr(options, attr, _bytes2sys(binpath)) if options.with_hg: options.with_hg = canonpath(_sys2bytes(options.with_hg)) if not ( os.path.isfile(options.with_hg) and os.access(options.with_hg, os.X_OK) ): parser.error('--with-hg must specify an executable hg script') if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']: msg = 'warning: --with-hg should specify an hg script, not: %s\n' msg %= _bytes2sys(os.path.basename(options.with_hg)) sys.stderr.write(msg) sys.stderr.flush() if (options.chg or options.with_chg) and WINDOWS: parser.error('chg does not work on %s' % os.name) if (options.rhg or options.with_rhg) and WINDOWS: parser.error('rhg does not work on %s' % os.name) if options.pyoxidized and not (MACOS or WINDOWS): parser.error('--pyoxidized is currently macOS and Windows only') if options.with_chg: options.chg = False # no installation to temporary location options.with_chg = canonpath(_sys2bytes(options.with_chg)) if not ( os.path.isfile(options.with_chg) and os.access(options.with_chg, os.X_OK) ): parser.error('--with-chg must specify a chg executable') if options.with_rhg: options.rhg = False # no installation to temporary location options.with_rhg = canonpath(_sys2bytes(options.with_rhg)) if not ( os.path.isfile(options.with_rhg) and os.access(options.with_rhg, os.X_OK) ): parser.error('--with-rhg must specify a rhg executable') if options.chg and options.with_hg: # chg shares installation location with hg parser.error( '--chg does not work when --with-hg is specified ' '(use --with-chg instead)' ) if options.rhg and options.with_hg: # rhg shares installation location with hg parser.error( '--rhg does not work when --with-hg is specified ' '(use --with-rhg instead)' ) if options.rhg and options.chg: parser.error('--rhg and --chg do not work together') if options.color == 'always' and not pygmentspresent: sys.stderr.write( 'warning: --color=always ignored because ' 'pygments is not installed\n' ) if options.bisect_repo and not options.known_good_rev: parser.error("--bisect-repo cannot be used without --known-good-rev") global useipv6 if options.ipv6: useipv6 = checksocketfamily('AF_INET6') else: # only use IPv6 if IPv4 is unavailable and IPv6 is available useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily( 'AF_INET6' ) options.anycoverage = options.cover or options.annotate or options.htmlcov if options.anycoverage: try: import coverage coverage.__version__ # silence unused import warning except ImportError: parser.error('coverage options now require the coverage package') if options.anycoverage and options.local: # this needs some path mangling somewhere, I guess parser.error( "sorry, coverage options do not work when --local " "is specified" ) if options.anycoverage and options.with_hg: parser.error( "sorry, coverage options do not work when --with-hg " "is specified" ) global verbose if options.verbose: verbose = '' if options.tmpdir: options.tmpdir = canonpath(options.tmpdir) if options.jobs < 1: parser.error('--jobs must be positive') if options.interactive and options.debug: parser.error("-i/--interactive and -d/--debug are incompatible") if options.debug: if options.timeout != defaults['timeout']: sys.stderr.write('warning: --timeout option ignored with --debug\n') if options.slowtimeout != defaults['slowtimeout']: sys.stderr.write( 'warning: --slowtimeout option ignored with --debug\n' ) options.timeout = 0 options.slowtimeout = 0 if options.blacklist: options.blacklist = parselistfiles(options.blacklist, 'blacklist') if options.whitelist: options.whitelisted = parselistfiles(options.whitelist, 'whitelist') else: options.whitelisted = {} if options.showchannels: options.nodiff = True return options def rename(src, dst): """Like os.rename(), trade atomicity and opened files friendliness for existing destination support. """ shutil.copy(src, dst) os.remove(src) def makecleanable(path): """Try to fix directory permission recursively so that the entire tree can be deleted""" for dirpath, dirnames, _filenames in os.walk(path, topdown=True): for d in dirnames: p = os.path.join(dirpath, d) try: os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx except OSError: pass _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff) def getdiff(expected, output, ref, err): servefail = False lines = [] for line in _unified_diff(expected, output, ref, err): if line.startswith(b'+++') or line.startswith(b'---'): line = line.replace(b'\\', b'/') if line.endswith(b' \n'): line = line[:-2] + b'\n' lines.append(line) if not servefail and line.startswith( b'+ abort: child process failed to start' ): servefail = True return servefail, lines verbose = False def vlog(*msg): """Log only when in verbose mode.""" if verbose is False: return return log(*msg) # Bytes that break XML even in a CDATA block: control characters 0-31 # sans \t, \n and \r CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]") # Match feature conditionalized output lines in the form, capturing the feature # list in group 2, and the preceeding line output in group 1: # # output..output (feature !)\n optline = re.compile(br'(.*) \((.+?) !\)\n$') def cdatasafe(data): """Make a string safe to include in a CDATA block. Certain control characters are illegal in a CDATA block, and there's no way to include a ]]> in a CDATA either. This function replaces illegal bytes with ? and adds a space between the ]] so that it won't break the CDATA block. """ return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>') def log(*msg): """Log something to stdout. Arguments are strings to print. """ with iolock: if verbose: print(verbose, end=' ') for m in msg: print(m, end=' ') print() sys.stdout.flush() def highlightdiff(line, color): if not color: return line assert pygmentspresent return pygments.highlight( line.decode('latin1'), difflexer, terminal256formatter ).encode('latin1') def highlightmsg(msg, color): if not color: return msg assert pygmentspresent return pygments.highlight(msg, runnerlexer, runnerformatter) def terminate(proc): """Terminate subprocess""" vlog('# Terminating process %d' % proc.pid) try: proc.terminate() except OSError: pass def killdaemons(pidfile): import killdaemons as killmod return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog) # sysconfig is not thread-safe (https://github.com/python/cpython/issues/92452) sysconfiglock = threading.Lock() class Test(unittest.TestCase): """Encapsulates a single, runnable test. While this class conforms to the unittest.TestCase API, it differs in that instances need to be instantiated manually. (Typically, unittest.TestCase classes are instantiated automatically by scanning modules.) """ # Status code reserved for skipped tests (used by hghave). SKIPPED_STATUS = 80 def __init__( self, path, outputdir, tmpdir, keeptmpdir=False, debug=False, first=False, timeout=None, startport=None, extraconfigopts=None, shell=None, hgcommand=None, slowtimeout=None, usechg=False, chgdebug=False, useipv6=False, ): """Create a test from parameters. path is the full path to the file defining the test. tmpdir is the main temporary directory to use for this test. keeptmpdir determines whether to keep the test's temporary directory after execution. It defaults to removal (False). debug mode will make the test execute verbosely, with unfiltered output. timeout controls the maximum run time of the test. It is ignored when debug is True. See slowtimeout for tests with #require slow. slowtimeout overrides timeout if the test has #require slow. startport controls the starting port number to use for this test. Each test will reserve 3 port numbers for execution. It is the caller's responsibility to allocate a non-overlapping port range to Test instances. extraconfigopts is an iterable of extra hgrc config options. Values must have the form "key=value" (something understood by hgrc). Values of the form "foo.key=value" will result in "[foo] key=value". shell is the shell to execute tests in. """ if timeout is None: timeout = defaults['timeout'] if startport is None: startport = defaults['port'] if slowtimeout is None: slowtimeout = defaults['slowtimeout'] self.path = path self.relpath = os.path.relpath(path) self.bname = os.path.basename(path) self.name = _bytes2sys(self.bname) self._testdir = os.path.dirname(path) self._outputdir = outputdir self._tmpname = os.path.basename(path) self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname) self._threadtmp = tmpdir self._keeptmpdir = keeptmpdir self._debug = debug self._first = first self._timeout = timeout self._slowtimeout = slowtimeout self._startport = startport self._extraconfigopts = extraconfigopts or [] self._shell = _sys2bytes(shell) self._hgcommand = hgcommand or b'hg' self._usechg = usechg self._chgdebug = chgdebug self._useipv6 = useipv6 self._aborted = False self._daemonpids = [] self._finished = None self._ret = None self._out = None self._skipped = None self._testtmp = None self._chgsockdir = None self._refout = self.readrefout() def readrefout(self): """read reference output""" # If we're not in --debug mode and reference output file exists, # check test output against it. if self._debug: return None # to match "out is None" elif os.path.exists(self.refpath): with open(self.refpath, 'rb') as f: return f.read().splitlines(True) else: return [] # needed to get base class __repr__ running @property def _testMethodName(self): return self.name def __str__(self): return self.name def shortDescription(self): return self.name def setUp(self): """Tasks to perform before run().""" self._finished = False self._ret = None self._out = None self._skipped = None try: os.mkdir(self._threadtmp) except FileExistsError: pass name = self._tmpname self._testtmp = os.path.join(self._threadtmp, name) os.mkdir(self._testtmp) # Remove any previous output files. if os.path.exists(self.errpath): try: os.remove(self.errpath) except FileNotFoundError: # We might have raced another test to clean up a .err file, # so ignore FileNotFoundError when removing a previous .err # file. pass if self._usechg: self._chgsockdir = os.path.join( self._threadtmp, b'%s.chgsock' % name ) os.mkdir(self._chgsockdir) def run(self, result): """Run this test and report results against a TestResult instance.""" # This function is extremely similar to unittest.TestCase.run(). Once # we require Python 2.7 (or at least its version of unittest), this # function can largely go away. self._result = result result.startTest(self) try: try: self.setUp() except (KeyboardInterrupt, SystemExit): self._aborted = True raise except Exception: result.addError(self, sys.exc_info()) return success = False try: self.runTest() except KeyboardInterrupt: self._aborted = True raise except unittest.SkipTest as e: result.addSkip(self, str(e)) # The base class will have already counted this as a # test we "ran", but we want to exclude skipped tests # from those we count towards those run. result.testsRun -= 1 except self.failureException as e: # This differs from unittest in that we don't capture # the stack trace. This is for historical reasons and # this decision could be revisited in the future, # especially for PythonTest instances. if result.addFailure(self, str(e)): success = True except Exception: result.addError(self, sys.exc_info()) else: success = True try: self.tearDown() except (KeyboardInterrupt, SystemExit): self._aborted = True raise except Exception: result.addError(self, sys.exc_info()) success = False if success: result.addSuccess(self) finally: result.stopTest(self, interrupted=self._aborted) def runTest(self): """Run this test instance. This will return a tuple describing the result of the test. """ env = self._getenv() self._genrestoreenv(env) self._daemonpids.append(env['DAEMON_PIDS']) self._createhgrc(env['HGRCPATH']) vlog('# Test', self.name) ret, out = self._run(env) self._finished = True self._ret = ret self._out = out def describe(ret): if ret < 0: return 'killed by signal: %d' % -ret return 'returned error code %d' % ret self._skipped = False if ret == self.SKIPPED_STATUS: if out is None: # Debug mode, nothing to parse. missing = ['unknown'] failed = None else: missing, failed = TTest.parsehghaveoutput(out) if not missing: missing = ['skipped'] if failed: self.fail('hg have failed checking for %s' % failed[-1]) else: self._skipped = True raise unittest.SkipTest(missing[-1]) elif ret == 'timeout': self.fail('timed out') elif ret is False: self.fail('no result code from test') elif out != self._refout: # Diff generation may rely on written .err file. if ( (ret != 0 or out != self._refout) and not self._skipped and not self._debug ): with open(self.errpath, 'wb') as f: for line in out: f.write(line) # The result object handles diff calculation for us. with firstlock: if self._result.addOutputMismatch(self, ret, out, self._refout): # change was accepted, skip failing return if self._first: global firsterror firsterror = True if ret: msg = 'output changed and ' + describe(ret) else: msg = 'output changed' self.fail(msg) elif ret: self.fail(describe(ret)) def tearDown(self): """Tasks to perform after run().""" for entry in self._daemonpids: killdaemons(entry) self._daemonpids = [] if self._keeptmpdir: log( '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' % ( _bytes2sys(self._testtmp), _bytes2sys(self._threadtmp), ) ) else: try: shutil.rmtree(self._testtmp) except OSError: # unreadable directory may be left in $TESTTMP; fix permission # and try again makecleanable(self._testtmp) shutil.rmtree(self._testtmp, True) shutil.rmtree(self._threadtmp, True) if self._usechg: # chgservers will stop automatically after they find the socket # files are deleted shutil.rmtree(self._chgsockdir, True) if ( (self._ret != 0 or self._out != self._refout) and not self._skipped and not self._debug and self._out ): with open(self.errpath, 'wb') as f: for line in self._out: f.write(line) vlog("# Ret was:", self._ret, '(%s)' % self.name) def _run(self, env): # This should be implemented in child classes to run tests. raise unittest.SkipTest('unknown test type') def abort(self): """Terminate execution of this test.""" self._aborted = True def _portmap(self, i): offset = b'' if i == 0 else b'%d' % i return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset) def _getreplacements(self): """Obtain a mapping of text replacements to apply to test output. Test output needs to be normalized so it can be compared to expected output. This function defines how some of that normalization will occur. """ r = [ # This list should be parallel to defineport in _getenv self._portmap(0), self._portmap(1), self._portmap(2), (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'), (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'), ] r.append((self._escapepath(self._testtmp), b'$TESTTMP')) if WINDOWS: # JSON output escapes backslashes in Windows paths, so also catch a # double-escape. replaced = self._testtmp.replace(b'\\', br'\\') r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP')) replacementfile = os.path.join(self._testdir, b'common-pattern.py') if os.path.exists(replacementfile): data = {} with open(replacementfile, mode='rb') as source: # the intermediate 'compile' step help with debugging code = compile(source.read(), replacementfile, 'exec') exec(code, data) for value in data.get('substitutions', ()): if len(value) != 2: msg = 'malformatted substitution in %s: %r' msg %= (replacementfile, value) raise ValueError(msg) r.append(value) return r def _escapepath(self, p): if WINDOWS: return b''.join( c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c for c in [p[i : i + 1] for i in range(len(p))] ) else: return re.escape(p) def _localip(self): if self._useipv6: return b'::1' else: return b'127.0.0.1' def _genrestoreenv(self, testenv): """Generate a script that can be used by tests to restore the original environment.""" # Put the restoreenv script inside self._threadtmp scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh') testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath) # Only restore environment variable names that the shell allows # us to export. name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$') # Do not restore these variables; otherwise tests would fail. reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'} with open(scriptpath, 'w') as envf: for name, value in origenviron.items(): if not name_regex.match(name): # Skip environment variables with unusual names not # allowed by most shells. continue if name in reqnames: continue envf.write('%s=%s\n' % (name, shellquote(value))) for name in testenv: if name in origenviron or name in reqnames: continue envf.write('unset %s\n' % (name,)) def _getenv(self): """Obtain environment variables to use during test execution.""" def defineport(i): offset = '' if i == 0 else '%s' % i env["HGPORT%s" % offset] = '%s' % (self._startport + i) env = os.environ.copy() with sysconfiglock: env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or '' env['HGEMITWARNINGS'] = '1' env['TESTTMP'] = _bytes2sys(self._testtmp) # the FORWARD_SLASH version is useful when running `sh` on non unix # system (e.g. Windows) env['TESTTMP_FORWARD_SLASH'] = env['TESTTMP'].replace(os.sep, '/') uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID') env['HGTEST_UUIDFILE'] = uid_file env['TESTNAME'] = self.name env['HOME'] = _bytes2sys(self._testtmp) if WINDOWS: env['REALUSERPROFILE'] = env['USERPROFILE'] # py3.8+ ignores HOME: https://bugs.python.org/issue36264 env['USERPROFILE'] = env['HOME'] formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1]) env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout) # This number should match portneeded in _getport for port in range(3): # This list should be parallel to _portmap in _getreplacements defineport(port) env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc')) env["DAEMON_PIDS"] = _bytes2sys( os.path.join(self._threadtmp, b'daemon.pids') ) env["HGEDITOR"] = ( '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"' ) env["HGUSER"] = "test" env["HGENCODING"] = "ascii" env["HGENCODINGMODE"] = "strict" env["HGHOSTNAME"] = "test-hostname" env['HGIPV6'] = str(int(self._useipv6)) # See contrib/catapipe.py for how to use this functionality. if 'HGTESTCATAPULTSERVERPIPE' not in env: # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the # non-test one in as a default, otherwise set to devnull env['HGTESTCATAPULTSERVERPIPE'] = env.get( 'HGCATAPULTSERVERPIPE', os.devnull ) extraextensions = [] for opt in self._extraconfigopts: section, key = opt.split('.', 1) if section != 'extensions': continue name = key.split('=', 1)[0] extraextensions.append(name) if extraextensions: env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions) # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw # IP addresses. env['LOCALIP'] = _bytes2sys(self._localip()) # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c, # but this is needed for testing python instances like dummyssh, # dummysmtpd.py, and dumbhttp.py. if WINDOWS: env['PYTHONLEGACYWINDOWSSTDIO'] = '1' # Modified HOME in test environment can confuse Rust tools. So set # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is # present and these variables aren't already defined. cargo_home_path = os.path.expanduser('~/.cargo') rustup_home_path = os.path.expanduser('~/.rustup') if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb: env['CARGO_HOME'] = cargo_home_path if ( os.path.exists(rustup_home_path) and b'RUSTUP_HOME' not in osenvironb ): env['RUSTUP_HOME'] = rustup_home_path # Reset some environment variables to well-known values so that # the tests produce repeatable output. env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C' env['TZ'] = 'GMT' env["EMAIL"] = "Foo Bar " env['COLUMNS'] = '80' env['TERM'] = 'xterm' dropped = [ 'CDPATH', 'CHGDEBUG', 'EDITOR', 'GREP_OPTIONS', 'HG', 'HGMERGE', 'HGPLAIN', 'HGPLAINEXCEPT', 'HGPROF', 'http_proxy', 'no_proxy', 'NO_PROXY', 'PAGER', 'VISUAL', ] for k in dropped: if k in env: del env[k] # unset env related to hooks for k in list(env): if k.startswith('HG_'): del env[k] if self._usechg: env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server') if self._chgdebug: env['CHGDEBUG'] = 'true' return env def _createhgrc(self, path): """Create an hgrc file for this test.""" with open(path, 'wb') as hgrc: hgrc.write(b'[ui]\n') hgrc.write(b'slash = True\n') hgrc.write(b'interactive = False\n') hgrc.write(b'detailed-exit-code = True\n') hgrc.write(b'merge = internal:merge\n') hgrc.write(b'mergemarkers = detailed\n') hgrc.write(b'promptecho = True\n') dummyssh = os.path.join(self._testdir, b'dummyssh') hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh)) hgrc.write(b'timeout.warn=15\n') hgrc.write(b'[chgserver]\n') hgrc.write(b'idletimeout=60\n') hgrc.write(b'[defaults]\n') hgrc.write(b'[devel]\n') hgrc.write(b'all-warnings = true\n') hgrc.write(b'default-date = 0 0\n') hgrc.write(b'[largefiles]\n') hgrc.write( b'usercache = %s\n' % (os.path.join(self._testtmp, b'.cache/largefiles')) ) hgrc.write(b'[lfs]\n') hgrc.write( b'usercache = %s\n' % (os.path.join(self._testtmp, b'.cache/lfs')) ) hgrc.write(b'[web]\n') hgrc.write(b'address = localhost\n') hgrc.write(b'ipv6 = %r\n' % self._useipv6) hgrc.write(b'server-header = testing stub value\n') for opt in self._extraconfigopts: section, key = _sys2bytes(opt).split(b'.', 1) assert b'=' in key, ( 'extra config opt %s must ' 'have an = for assignment' % opt ) hgrc.write(b'[%s]\n%s\n' % (section, key)) def fail(self, msg): # unittest differentiates between errored and failed. # Failed is denoted by AssertionError (by default at least). raise AssertionError(msg) def _runcommand(self, cmd, env, normalizenewlines=False): """Run command in a sub-process, capturing the output (stdout and stderr). Return a tuple (exitcode, output). output is None in debug mode. """ if self._debug: proc = subprocess.Popen( _bytes2sys(cmd), shell=True, close_fds=closefds, cwd=_bytes2sys(self._testtmp), env=env, ) ret = proc.wait() return (ret, None) proc = Popen4(cmd, self._testtmp, self._timeout, env) def cleanup(): terminate(proc) ret = proc.wait() if ret == 0: ret = signal.SIGTERM << 8 killdaemons(env['DAEMON_PIDS']) return ret proc.tochild.close() try: output = proc.fromchild.read() except KeyboardInterrupt: vlog('# Handling keyboard interrupt') cleanup() raise ret = proc.wait() if wifexited(ret): ret = os.WEXITSTATUS(ret) if proc.timeout: ret = 'timeout' if ret: killdaemons(env['DAEMON_PIDS']) for s, r in self._getreplacements(): output = re.sub(s, r, output) if normalizenewlines: output = output.replace(b'\r\n', b'\n') return ret, output.splitlines(True) class PythonTest(Test): """A Python-based test.""" @property def refpath(self): return os.path.join(self._testdir, b'%s.out' % self.bname) def _run(self, env): # Quote the python(3) executable for Windows cmd = b'"%s" "%s"' % (PYTHON, self.path) vlog("# Running", cmd.decode("utf-8")) result = self._runcommand(cmd, env, normalizenewlines=WINDOWS) if self._aborted: raise KeyboardInterrupt() return result # Some glob patterns apply only in some circumstances, so the script # might want to remove (glob) annotations that otherwise should be # retained. checkcodeglobpats = [ # On Windows it looks like \ doesn't require a (glob), but we know # better. re.compile(br'^pushing to \$TESTTMP/.*[^)]$'), re.compile(br'^moving \S+/.*[^)]$'), re.compile(br'^pulling from \$TESTTMP/.*[^)]$'), # Not all platforms have 127.0.0.1 as loopback (though most do), # so we always glob that too. re.compile(br'.*\$LOCALIP.*$'), ] bchr = lambda x: bytes([x]) WARN_UNDEFINED = 1 WARN_YES = 2 WARN_NO = 3 MARK_OPTIONAL = b" (?)\n" def isoptional(line): return line.endswith(MARK_OPTIONAL) class TTest(Test): """A "t test" is a test backed by a .t file.""" SKIPPED_PREFIX = b'skipped: ' FAILED_PREFIX = b'hghave check failed: ' NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)} ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'}) def __init__(self, path, *args, **kwds): # accept an extra "case" parameter case = kwds.pop('case', []) self._case = case self._allcases = {x for y in parsettestcases(path) for x in y} super(TTest, self).__init__(path, *args, **kwds) if case: casepath = b'#'.join(case) self.name = '%s#%s' % (self.name, _bytes2sys(casepath)) self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath) self._tmpname += b'-%s' % casepath.replace(b'#', b'-') self._have = {} @property def refpath(self): return os.path.join(self._testdir, self.bname) def _run(self, env): with open(self.path, 'rb') as f: lines = f.readlines() # .t file is both reference output and the test input, keep reference # output updated with the the test input. This avoids some race # conditions where the reference output does not match the actual test. if self._refout is not None: self._refout = lines salt, script, after, expected = self._parsetest(lines) # Write out the generated script. fname = b'%s.sh' % self._testtmp with open(fname, 'wb') as f: for l in script: f.write(l) cmd = b'%s "%s"' % (self._shell, fname) vlog("# Running", cmd.decode("utf-8")) exitcode, output = self._runcommand(cmd, env) if self._aborted: raise KeyboardInterrupt() # Do not merge output if skipped. Return hghave message instead. # Similarly, with --debug, output is None. if exitcode == self.SKIPPED_STATUS or output is None: return exitcode, output return self._processoutput(exitcode, output, salt, after, expected) def _hghave(self, reqs): allreqs = b' '.join(reqs) self._detectslow(reqs) if allreqs in self._have: return self._have.get(allreqs) # TODO do something smarter when all other uses of hghave are gone. runtestdir = osenvironb[b'RUNTESTDIR'] tdir = runtestdir.replace(b'\\', b'/') proc = Popen4( b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs), self._testtmp, 0, self._getenv(), ) stdout, stderr = proc.communicate() ret = proc.wait() if wifexited(ret): ret = os.WEXITSTATUS(ret) if ret == 2: print(stdout.decode('utf-8')) sys.exit(1) if ret != 0: self._have[allreqs] = (False, stdout) return False, stdout self._have[allreqs] = (True, None) return True, None def _detectslow(self, reqs): """update the timeout of slow test when appropriate""" if b'slow' in reqs: self._timeout = self._slowtimeout def _iftest(self, args): # implements "#if" reqs = [] for arg in args: if arg.startswith(b'no-') and arg[3:] in self._allcases: if arg[3:] in self._case: return False elif arg in self._allcases: if arg not in self._case: return False else: reqs.append(arg) self._detectslow(reqs) return self._hghave(reqs)[0] def _parsetest(self, lines): # We generate a shell script which outputs unique markers to line # up script results with our source. These markers include input # line number and the last return code. salt = b"SALT%d" % time.time() def addsalt(line, inpython): if inpython: script.append(b'%s %d 0\n' % (salt, line)) else: script.append(b'echo %s %d $?\n' % (salt, line)) activetrace = [] session = str(uuid.uuid4()).encode('ascii') hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv( 'HGCATAPULTSERVERPIPE' ) def toggletrace(cmd=None): if not hgcatapult or hgcatapult == os.devnull: return if activetrace: script.append( b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (session, activetrace[0]) ) if cmd is None: return if isinstance(cmd, str): quoted = shellquote(cmd.strip()) else: quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8') quoted = quoted.replace(b'\\', b'\\\\') script.append( b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (session, quoted) ) activetrace[0:] = [quoted] script = [] # After we run the shell script, we re-unify the script output # with non-active parts of the source, with synchronization by our # SALT line number markers. The after table contains the non-active # components, ordered by line number. after = {} # Expected shell script output. expected = {} pos = prepos = -1 # The current stack of conditionnal section. # Each relevant conditionnal section can have the following value: # - True: we should run this block # - False: we should skip this block # - None: The parent block is skipped, # (no branch of this one will ever run) condition_stack = [] def run_line(): """return True if the current line should be run""" if not condition_stack: return True return bool(condition_stack[-1]) def push_conditional_block(should_run): """Push a new conditional context, with its initial state i.e. entry a #if block""" if not run_line(): condition_stack.append(None) else: condition_stack.append(should_run) def flip_conditional(): """reverse the current condition state i.e. enter a #else """ assert condition_stack if condition_stack[-1] is not None: condition_stack[-1] = not condition_stack[-1] def pop_conditional(): """exit the current skipping context i.e. reach the #endif""" assert condition_stack condition_stack.pop() # We keep track of whether or not we're in a Python block so we # can generate the surrounding doctest magic. inpython = False if self._debug: script.append(b'set -x\n') if os.getenv('MSYSTEM'): script.append(b'alias pwd="pwd -W"\n') if hgcatapult and hgcatapult != os.devnull: hgcatapult = hgcatapult.encode('utf8') cataname = self.name.encode('utf8') # Kludge: use a while loop to keep the pipe from getting # closed by our echo commands. The still-running file gets # reaped at the end of the script, which causes the while # loop to exit and closes the pipe. Sigh. script.append( b'rtendtracing() {\n' b' echo END %(session)s %(name)s >> %(catapult)s\n' b' rm -f "$TESTTMP/.still-running"\n' b'}\n' b'trap "rtendtracing" 0\n' b'touch "$TESTTMP/.still-running"\n' b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done ' b'> %(catapult)s &\n' b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n' b'echo START %(session)s %(name)s >> %(catapult)s\n' % { b'name': cataname, b'session': session, b'catapult': hgcatapult, } ) if self._case: casestr = b'#'.join(self._case) if isinstance(casestr, str): quoted = shellquote(casestr) else: quoted = shellquote(casestr.decode('utf8')).encode('utf8') script.append(b'TESTCASE=%s\n' % quoted) script.append(b'export TESTCASE\n') n = 0 for n, l in enumerate(lines): if not l.endswith(b'\n'): l += b'\n' if l.startswith(b'#require'): lsplit = l.split() if len(lsplit) < 2 or lsplit[0] != b'#require': after.setdefault(pos, []).append( b' !!! invalid #require\n' ) if run_line(): haveresult, message = self._hghave(lsplit[1:]) if not haveresult: script = [b'echo "%s"\nexit 80\n' % message] break after.setdefault(pos, []).append(l) elif l.startswith(b'#if'): lsplit = l.split() if len(lsplit) < 2 or lsplit[0] != b'#if': after.setdefault(pos, []).append(b' !!! invalid #if\n') push_conditional_block(self._iftest(lsplit[1:])) after.setdefault(pos, []).append(l) elif l.startswith(b'#else'): if not condition_stack: after.setdefault(pos, []).append(b' !!! missing #if\n') flip_conditional() after.setdefault(pos, []).append(l) elif l.startswith(b'#endif'): if not condition_stack: after.setdefault(pos, []).append(b' !!! missing #if\n') pop_conditional() after.setdefault(pos, []).append(l) elif not run_line(): after.setdefault(pos, []).append(l) elif l.startswith(b' >>> '): # python inlines after.setdefault(pos, []).append(l) prepos = pos pos = n if not inpython: # We've just entered a Python block. Add the header. inpython = True addsalt(prepos, False) # Make sure we report the exit code. script.append(b'"%s" -m heredoctest < '): # continuations after.setdefault(prepos, []).append(l) script.append(l[4:]) elif l.startswith(b' '): # results # Queue up a list of expected results. expected.setdefault(pos, []).append(l[2:]) else: if inpython: script.append(b'EOF\n') inpython = False # Non-command/result. Queue up for merged output. after.setdefault(pos, []).append(l) if inpython: script.append(b'EOF\n') if condition_stack: after.setdefault(pos, []).append(b' !!! missing #endif\n') addsalt(n + 1, False) # Need to end any current per-command trace if activetrace: toggletrace() return salt, script, after, expected def _processoutput(self, exitcode, output, salt, after, expected): # Merge the script output back into a unified test. warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not if exitcode != 0: warnonly = WARN_NO pos = -1 postout = [] for out_rawline in output: out_line, cmd_line = out_rawline, None if salt in out_rawline: out_line, cmd_line = out_rawline.split(salt, 1) pos, postout, warnonly = self._process_out_line( out_line, pos, postout, expected, warnonly ) pos, postout = self._process_cmd_line(cmd_line, pos, postout, after) if pos in after: postout += after.pop(pos) if warnonly == WARN_YES: exitcode = False # Set exitcode to warned. return exitcode, postout def _process_out_line(self, out_line, pos, postout, expected, warnonly): while out_line: if not out_line.endswith(b'\n'): out_line += b' (no-eol)\n' # Find the expected output at the current position. els = [None] if expected.get(pos, None): els = expected[pos] optional = [] for i, el in enumerate(els): r = False if el: r, exact = self.linematch(el, out_line) if isinstance(r, str): if r == '-glob': out_line = ''.join(el.rsplit(' (glob)', 1)) r = '' # Warn only this line. elif r == "retry": postout.append(b' ' + el) else: log('\ninfo, unknown linematch result: %r\n' % r) r = False if r: els.pop(i) break if el: if isoptional(el): optional.append(i) else: m = optline.match(el) if m: conditions = [c for c in m.group(2).split(b' ')] if not self._iftest(conditions): optional.append(i) if exact: # Don't allow line to be matches against a later # line in the output els.pop(i) break if r: if r == "retry": continue # clean up any optional leftovers for i in optional: postout.append(b' ' + els[i]) for i in reversed(optional): del els[i] postout.append(b' ' + el) else: if self.NEEDESCAPE(out_line): out_line = TTest._stringescape( b'%s (esc)\n' % out_line.rstrip(b'\n') ) postout.append(b' ' + out_line) # Let diff deal with it. if r != '': # If line failed. warnonly = WARN_NO elif warnonly == WARN_UNDEFINED: warnonly = WARN_YES break else: # clean up any optional leftovers while expected.get(pos, None): el = expected[pos].pop(0) if el: if not isoptional(el): m = optline.match(el) if m: conditions = [c for c in m.group(2).split(b' ')] if self._iftest(conditions): # Don't append as optional line continue else: continue postout.append(b' ' + el) return pos, postout, warnonly def _process_cmd_line(self, cmd_line, pos, postout, after): """process a "command" part of a line from unified test output""" if cmd_line: # Add on last return code. ret = int(cmd_line.split()[1]) if ret != 0: postout.append(b' [%d]\n' % ret) if pos in after: # Merge in non-active test bits. postout += after.pop(pos) pos = int(cmd_line.split()[0]) return pos, postout @staticmethod def rematch(el, l): try: # parse any flags at the beginning of the regex. Only 'i' is # supported right now, but this should be easy to extend. flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2] flags = flags or b'' el = flags + b'(?:' + el + b')' # use \Z to ensure that the regex matches to the end of the string if WINDOWS: return re.match(el + br'\r?\n\Z', l) return re.match(el + br'\n\Z', l) except re.error: # el is an invalid regex return False @staticmethod def globmatch(el, l): # The only supported special characters are * and ? plus / which also # matches \ on windows. Escaping of these characters is supported. if el + b'\n' == l: if os.altsep: # matching on "/" is not needed for this line for pat in checkcodeglobpats: if pat.match(el): return True return b'-glob' return True el = el.replace(b'$LOCALIP', b'*') i, n = 0, len(el) res = b'' while i < n: c = el[i : i + 1] i += 1 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/': res += el[i - 1 : i + 1] i += 1 elif c == b'*': res += b'.*' elif c == b'?': res += b'.' elif c == b'/' and os.altsep: res += b'[/\\\\]' else: res += re.escape(c) return TTest.rematch(res, l) def linematch(self, el, l): if el == l: # perfect match (fast) return True, True retry = False if isoptional(el): retry = "retry" el = el[: -len(MARK_OPTIONAL)] + b"\n" else: m = optline.match(el) if m: conditions = [c for c in m.group(2).split(b' ')] el = m.group(1) + b"\n" if not self._iftest(conditions): # listed feature missing, should not match return "retry", False if el.endswith(b" (esc)\n"): el = el[:-7].decode('unicode_escape') + '\n' el = el.encode('latin-1') if el == l or WINDOWS and el[:-1] + b'\r\n' == l: return True, True if el.endswith(b" (re)\n"): return (TTest.rematch(el[:-6], l) or retry), False if el.endswith(b" (glob)\n"): # ignore '(glob)' added to l by 'replacements' if l.endswith(b" (glob)\n"): l = l[:-8] + b"\n" return (TTest.globmatch(el[:-8], l) or retry), False if os.altsep: _l = l.replace(b'\\', b'/') if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l: return True, True return retry, True @staticmethod def parsehghaveoutput(lines): """Parse hghave log lines. Return tuple of lists (missing, failed): * the missing/unknown features * the features for which existence check failed""" missing = [] failed = [] for line in lines: if line.startswith(TTest.SKIPPED_PREFIX): line = line.splitlines()[0] missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :])) elif line.startswith(TTest.FAILED_PREFIX): line = line.splitlines()[0] failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :])) return missing, failed @staticmethod def _escapef(m): return TTest.ESCAPEMAP[m.group(0)] @staticmethod def _stringescape(s): return TTest.ESCAPESUB(TTest._escapef, s) iolock = threading.RLock() firstlock = threading.RLock() firsterror = False base_class = unittest.TextTestResult class TestResult(base_class): """Holds results when executing via unittest.""" def __init__(self, options, *args, **kwargs): super(TestResult, self).__init__(*args, **kwargs) self._options = options # unittest.TestResult didn't have skipped until 2.7. We need to # polyfill it. self.skipped = [] # We have a custom "ignored" result that isn't present in any Python # unittest implementation. It is very similar to skipped. It may make # sense to map it into skip some day. self.ignored = [] self.times = [] self._firststarttime = None # Data stored for the benefit of generating xunit reports. self.successes = [] self.faildata = {} if options.color == 'auto': isatty = self.stream.isatty() # For some reason, redirecting stdout on Windows disables the ANSI # color processing of stderr, which is what is used to print the # output. Therefore, both must be tty on Windows to enable color. if WINDOWS: isatty = isatty and sys.stdout.isatty() self.color = pygmentspresent and isatty elif options.color == 'never': self.color = False else: # 'always', for testing purposes self.color = pygmentspresent def onStart(self, test): """Can be overriden by custom TestResult""" def onEnd(self): """Can be overriden by custom TestResult""" def addFailure(self, test, reason): self.failures.append((test, reason)) if self._options.first: self.stop() else: with iolock: if reason == "timed out": self.stream.write('t') else: if not self._options.nodiff: self.stream.write('\n') # Exclude the '\n' from highlighting to lex correctly formatted = 'ERROR: %s output changed\n' % test self.stream.write(highlightmsg(formatted, self.color)) self.stream.write('!') self.stream.flush() def addSuccess(self, test): with iolock: super(TestResult, self).addSuccess(test) self.successes.append(test) def addError(self, test, err): super(TestResult, self).addError(test, err) if self._options.first: self.stop() # Polyfill. def addSkip(self, test, reason): self.skipped.append((test, reason)) with iolock: if self.showAll: self.stream.writeln('skipped %s' % reason) else: self.stream.write('s') self.stream.flush() def addIgnore(self, test, reason): self.ignored.append((test, reason)) with iolock: if self.showAll: self.stream.writeln('ignored %s' % reason) else: if reason not in ('not retesting', "doesn't match keyword"): self.stream.write('i') else: self.testsRun += 1 self.stream.flush() def addOutputMismatch(self, test, ret, got, expected): """Record a mismatch in test output for a particular test.""" if self.shouldStop or firsterror: # don't print, some other test case already failed and # printed, we're just stale and probably failed due to our # temp dir getting cleaned up. return accepted = False lines = [] with iolock: if self._options.nodiff: pass elif self._options.view: v = self._options.view subprocess.call( r'"%s" "%s" "%s"' % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)), shell=True, ) else: servefail, lines = getdiff( expected, got, test.refpath, test.errpath ) self.stream.write('\n') for line in lines: line = highlightdiff(line, self.color) self.stream.flush() self.stream.buffer.write(line) self.stream.buffer.flush() if servefail: raise test.failureException( 'server failed to start (HGPORT=%s)' % test._startport ) # handle interactive prompt without releasing iolock if self._options.interactive: if test.readrefout() != expected: self.stream.write( 'Reference output has changed (run again to prompt ' 'changes)' ) else: self.stream.write('Accept this change? [y/N] ') self.stream.flush() answer = sys.stdin.readline().strip() if answer.lower() in ('y', 'yes'): if test.path.endswith(b'.t'): rename(test.errpath, test.path) else: rename(test.errpath, b'%s.out' % test.path) accepted = True if not accepted: self.faildata[test.name] = b''.join(lines) return accepted def startTest(self, test): super(TestResult, self).startTest(test) # os.times module computes the user time and system time spent by # child's processes along with real elapsed time taken by a process. # This module has one limitation. It can only work for Linux user # and not for Windows. Hence why we fall back to another function # for wall time calculations. test.started_times = os.times() # TODO use a monotonic clock once support for Python 2.7 is dropped. test.started_time = time.time() if self._firststarttime is None: # thread racy but irrelevant self._firststarttime = test.started_time def stopTest(self, test, interrupted=False): super(TestResult, self).stopTest(test) test.stopped_times = os.times() stopped_time = time.time() starttime = test.started_times endtime = test.stopped_times origin = self._firststarttime self.times.append( ( test.name, endtime[2] - starttime[2], # user space CPU time endtime[3] - starttime[3], # sys space CPU time stopped_time - test.started_time, # real time test.started_time - origin, # start date in run context stopped_time - origin, # end date in run context ) ) if interrupted: with iolock: self.stream.writeln( 'INTERRUPTED: %s (after %d seconds)' % (test.name, self.times[-1][3]) ) def getTestResult(): """ Returns the relevant test result """ if "CUSTOM_TEST_RESULT" in os.environ: testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"]) return testresultmodule.TestResult else: return TestResult class TestSuite(unittest.TestSuite): """Custom unittest TestSuite that knows how to execute Mercurial tests.""" def __init__( self, testdir, jobs=1, whitelist=None, blacklist=None, keywords=None, loop=False, runs_per_test=1, loadtest=None, showchannels=False, *args, **kwargs ): """Create a new instance that can run tests with a configuration. testdir specifies the directory where tests are executed from. This is typically the ``tests`` directory from Mercurial's source repository. jobs specifies the number of jobs to run concurrently. Each test executes on its own thread. Tests actually spawn new processes, so state mutation should not be an issue. If there is only one job, it will use the main thread. whitelist and blacklist denote tests that have been whitelisted and blacklisted, respectively. These arguments don't belong in TestSuite. Instead, whitelist and blacklist should be handled by the thing that populates the TestSuite with tests. They are present to preserve backwards compatible behavior which reports skipped tests as part of the results. keywords denotes key words that will be used to filter which tests to execute. This arguably belongs outside of TestSuite. loop denotes whether to loop over tests forever. """ super(TestSuite, self).__init__(*args, **kwargs) self._jobs = jobs self._whitelist = whitelist self._blacklist = blacklist self._keywords = keywords self._loop = loop self._runs_per_test = runs_per_test self._loadtest = loadtest self._showchannels = showchannels def run(self, result): # We have a number of filters that need to be applied. We do this # here instead of inside Test because it makes the running logic for # Test simpler. tests = [] num_tests = [0] for test in self._tests: def get(): num_tests[0] += 1 if getattr(test, 'should_reload', False): return self._loadtest(test, num_tests[0]) return test if not os.path.exists(test.path): result.addSkip(test, "Doesn't exist") continue is_whitelisted = self._whitelist and ( test.relpath in self._whitelist or test.bname in self._whitelist ) if not is_whitelisted: is_blacklisted = self._blacklist and ( test.relpath in self._blacklist or test.bname in self._blacklist ) if is_blacklisted: result.addSkip(test, 'blacklisted') continue if self._keywords: with open(test.path, 'rb') as f: t = f.read().lower() + test.bname.lower() ignored = False for k in self._keywords.lower().split(): if k not in t: result.addIgnore(test, "doesn't match keyword") ignored = True break if ignored: continue for _ in range(self._runs_per_test): tests.append(get()) runtests = list(tests) done = queue.Queue() running = 0 channels_lock = threading.Lock() channels = [""] * self._jobs def job(test, result): with channels_lock: for n, v in enumerate(channels): if not v: channel = n break else: raise ValueError('Could not find output channel') channels[channel] = "=" + test.name[5:].split(".")[0] r = None try: test(result) except KeyboardInterrupt: pass except: # re-raises r = ('!', test, 'run-test raised an error, see traceback') raise finally: try: channels[channel] = '' except IndexError: pass done.put(r) def stat(): count = 0 while channels: d = '\n%03s ' % count for n, v in enumerate(channels): if v: d += v[0] channels[n] = v[1:] or '.' else: d += ' ' d += ' ' with iolock: sys.stdout.write(d + ' ') sys.stdout.flush() for x in range(10): if channels: time.sleep(0.1) count += 1 stoppedearly = False if self._showchannels: statthread = threading.Thread(target=stat, name="stat") statthread.start() try: while tests or running: if not done.empty() or running == self._jobs or not tests: try: done.get(True, 1) running -= 1 if result and result.shouldStop: stoppedearly = True break except queue.Empty: continue if tests and not running == self._jobs: test = tests.pop(0) if self._loop: if getattr(test, 'should_reload', False): num_tests[0] += 1 tests.append(self._loadtest(test, num_tests[0])) else: tests.append(test) if self._jobs == 1: job(test, result) else: t = threading.Thread( target=job, name=test.name, args=(test, result) ) t.start() running += 1 # If we stop early we still need to wait on started tests to # finish. Otherwise, there is a race between the test completing # and the test's cleanup code running. This could result in the # test reporting incorrect. if stoppedearly: while running: try: done.get(True, 1) running -= 1 except queue.Empty: continue except KeyboardInterrupt: for test in runtests: test.abort() channels = [] return result # Save the most recent 5 wall-clock runtimes of each test to a # human-readable text file named .testtimes. Tests are sorted # alphabetically, while times for each test are listed from oldest to # newest. def loadtimes(outputdir): times = [] try: with open(os.path.join(outputdir, b'.testtimes')) as fp: for line in fp: m = re.match('(.*?) ([0-9. ]+)', line) times.append( (m.group(1), [float(t) for t in m.group(2).split()]) ) except FileNotFoundError: pass return times def savetimes(outputdir, result): saved = dict(loadtimes(outputdir)) maxruns = 5 skipped = {str(t[0]) for t in result.skipped} for tdata in result.times: test, real = tdata[0], tdata[3] if test not in skipped: ts = saved.setdefault(test, []) ts.append(real) ts[:] = ts[-maxruns:] fd, tmpname = tempfile.mkstemp( prefix=b'.testtimes', dir=outputdir, text=True ) with os.fdopen(fd, 'w') as fp: for name, ts in sorted(saved.items()): fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts]))) timepath = os.path.join(outputdir, b'.testtimes') try: os.unlink(timepath) except OSError: pass try: os.rename(tmpname, timepath) except OSError: pass class TextTestRunner(unittest.TextTestRunner): """Custom unittest test runner that uses appropriate settings.""" def __init__(self, runner, *args, **kwargs): super(TextTestRunner, self).__init__(*args, **kwargs) self._runner = runner self._result = getTestResult()( self._runner.options, self.stream, self.descriptions, self.verbosity ) def listtests(self, test): test = sorted(test, key=lambda t: t.name) self._result.onStart(test) for t in test: print(t.name) self._result.addSuccess(t) if self._runner.options.xunit: with open(self._runner.options.xunit, "wb") as xuf: self._writexunit(self._result, xuf) if self._runner.options.json: jsonpath = os.path.join(self._runner._outputdir, b'report.json') with open(jsonpath, 'w') as fp: self._writejson(self._result, fp) return self._result def run(self, test): self._result.onStart(test) test(self._result) failed = len(self._result.failures) skipped = len(self._result.skipped) ignored = len(self._result.ignored) with iolock: self.stream.writeln('') if not self._runner.options.noskips: for test, msg in sorted( self._result.skipped, key=lambda s: s[0].name ): formatted = 'Skipped %s: %s\n' % (test.name, msg) msg = highlightmsg(formatted, self._result.color) self.stream.write(msg) for test, msg in sorted( self._result.failures, key=lambda f: f[0].name ): formatted = 'Failed %s: %s\n' % (test.name, msg) self.stream.write(highlightmsg(formatted, self._result.color)) for test, msg in sorted( self._result.errors, key=lambda e: e[0].name ): self.stream.writeln('Errored %s: %s' % (test.name, msg)) if self._runner.options.xunit: with open(self._runner.options.xunit, "wb") as xuf: self._writexunit(self._result, xuf) if self._runner.options.json: jsonpath = os.path.join(self._runner._outputdir, b'report.json') with open(jsonpath, 'w') as fp: self._writejson(self._result, fp) self._runner._checkhglib('Tested') savetimes(self._runner._outputdir, self._result) if failed and self._runner.options.known_good_rev: self._bisecttests(t for t, m in self._result.failures) self.stream.writeln( '# Ran %d tests, %d skipped, %d failed.' % (self._result.testsRun, skipped + ignored, failed) ) if failed: self.stream.writeln( 'python hash seed: %s' % os.environ['PYTHONHASHSEED'] ) if self._runner.options.time: self.printtimes(self._result.times) if self._runner.options.exceptions: exceptions = aggregateexceptions( os.path.join(self._runner._outputdir, b'exceptions') ) self.stream.writeln('Exceptions Report:') self.stream.writeln( '%d total from %d frames' % (exceptions['total'], len(exceptions['exceptioncounts'])) ) combined = exceptions['combined'] for key in sorted(combined, key=combined.get, reverse=True): frame, line, exc = key totalcount, testcount, leastcount, leasttest = combined[key] self.stream.writeln( '%d (%d tests)\t%s: %s (%s - %d total)' % ( totalcount, testcount, frame, exc, leasttest, leastcount, ) ) self.stream.flush() return self._result def _bisecttests(self, tests): bisectcmd = ['hg', 'bisect'] bisectrepo = self._runner.options.bisect_repo if bisectrepo: bisectcmd.extend(['-R', os.path.abspath(bisectrepo)]) def pread(args): env = os.environ.copy() env['HGPLAIN'] = '1' p = subprocess.Popen( args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env ) data = p.stdout.read() p.wait() return data for test in tests: pread(bisectcmd + ['--reset']), pread(bisectcmd + ['--bad', '.']) pread(bisectcmd + ['--good', self._runner.options.known_good_rev]) # TODO: we probably need to forward more options # that alter hg's behavior inside the tests. opts = '' withhg = self._runner.options.with_hg if withhg: opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg)) rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test) data = pread(bisectcmd + ['--command', rtc]) m = re.search( ( br'\nThe first (?Pbad|good) revision ' br'is:\nchangeset: +\d+:(?P[a-f0-9]+)\n.*\n' br'summary: +(?P[^\n]+)\n' ), data, (re.MULTILINE | re.DOTALL), ) if m is None: self.stream.writeln( 'Failed to identify failure point for %s' % test ) continue dat = m.groupdict() verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed' self.stream.writeln( '%s %s by %s (%s)' % ( test, verb, dat['node'].decode('ascii'), dat['summary'].decode('utf8', 'ignore'), ) ) def printtimes(self, times): # iolock held by run self.stream.writeln('# Producing time report') times.sort(key=lambda t: (t[3])) cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s' self.stream.writeln( '%-7s %-7s %-7s %-7s %-7s %s' % ('start', 'end', 'cuser', 'csys', 'real', 'Test') ) for tdata in times: test = tdata[0] cuser, csys, real, start, end = tdata[1:6] self.stream.writeln(cols % (start, end, cuser, csys, real, test)) @staticmethod def _writexunit(result, outf): # See http://llg.cubic.org/docs/junit/ for a reference. timesd = {t[0]: t[3] for t in result.times} doc = minidom.Document() s = doc.createElement('testsuite') s.setAttribute('errors', "0") # TODO s.setAttribute('failures', str(len(result.failures))) s.setAttribute('name', 'run-tests') s.setAttribute( 'skipped', str(len(result.skipped) + len(result.ignored)) ) s.setAttribute('tests', str(result.testsRun)) doc.appendChild(s) for tc in result.successes: t = doc.createElement('testcase') t.setAttribute('name', tc.name) tctime = timesd.get(tc.name) if tctime is not None: t.setAttribute('time', '%.3f' % tctime) s.appendChild(t) for tc, err in sorted(result.faildata.items()): t = doc.createElement('testcase') t.setAttribute('name', tc) tctime = timesd.get(tc) if tctime is not None: t.setAttribute('time', '%.3f' % tctime) # createCDATASection expects a unicode or it will # convert using default conversion rules, which will # fail if string isn't ASCII. err = cdatasafe(err).decode('utf-8', 'replace') cd = doc.createCDATASection(err) # Use 'failure' here instead of 'error' to match errors = 0, # failures = len(result.failures) in the testsuite element. failelem = doc.createElement('failure') failelem.setAttribute('message', 'output changed') failelem.setAttribute('type', 'output-mismatch') failelem.appendChild(cd) t.appendChild(failelem) s.appendChild(t) for tc, message in result.skipped: # According to the schema, 'skipped' has no attributes. So store # the skip message as a text node instead. t = doc.createElement('testcase') t.setAttribute('name', tc.name) binmessage = message.encode('utf-8') message = cdatasafe(binmessage).decode('utf-8', 'replace') cd = doc.createCDATASection(message) skipelem = doc.createElement('skipped') skipelem.appendChild(cd) t.appendChild(skipelem) s.appendChild(t) outf.write(doc.toprettyxml(indent=' ', encoding='utf-8')) @staticmethod def _writejson(result, outf): timesd = {} for tdata in result.times: test = tdata[0] timesd[test] = tdata[1:] outcome = {} groups = [ ('success', ((tc, None) for tc in result.successes)), ('failure', result.failures), ('skip', result.skipped), ] for res, testcases in groups: for tc, __ in testcases: if tc.name in timesd: diff = result.faildata.get(tc.name, b'') try: diff = diff.decode('unicode_escape') except UnicodeDecodeError as e: diff = '%r decoding diff, sorry' % e tres = { 'result': res, 'time': ('%0.3f' % timesd[tc.name][2]), 'cuser': ('%0.3f' % timesd[tc.name][0]), 'csys': ('%0.3f' % timesd[tc.name][1]), 'start': ('%0.3f' % timesd[tc.name][3]), 'end': ('%0.3f' % timesd[tc.name][4]), 'diff': diff, } else: # blacklisted test tres = {'result': res} outcome[tc.name] = tres jsonout = json.dumps( outcome, sort_keys=True, indent=4, separators=(',', ': ') ) outf.writelines(("testreport =", jsonout)) def sorttests(testdescs, previoustimes, shuffle=False): """Do an in-place sort of tests.""" if shuffle: random.shuffle(testdescs) return if previoustimes: def sortkey(f): f = f['path'] if f in previoustimes: # Use most recent time as estimate return -(previoustimes[f][-1]) else: # Default to a rather arbitrary value of 1 second for new tests return -1.0 else: # keywords for slow tests slow = { b'svn': 10, b'cvs': 10, b'hghave': 10, b'largefiles-update': 10, b'run-tests': 10, b'corruption': 10, b'race': 10, b'i18n': 10, b'check': 100, b'gendoc': 100, b'contrib-perf': 200, b'merge-combination': 100, } perf = {} def sortkey(f): # run largest tests first, as they tend to take the longest f = f['path'] try: return perf[f] except KeyError: try: val = -os.stat(f).st_size except FileNotFoundError: perf[f] = -1e9 # file does not exist, tell early return -1e9 for kw, mul in slow.items(): if kw in f: val *= mul if f.endswith(b'.py'): val /= 10.0 perf[f] = val / 1000.0 return perf[f] testdescs.sort(key=sortkey) class TestRunner: """Holds context for executing tests. Tests rely on a lot of state. This object holds it for them. """ # Programs required to run tests. REQUIREDTOOLS = [ b'diff', b'grep', b'unzip', b'gunzip', b'bunzip2', b'sed', ] # Maps file extensions to test class. TESTTYPES = [ (b'.py', PythonTest), (b'.t', TTest), ] def __init__(self): self.options = None self._hgroot = None self._testdir = None self._outputdir = None self._hgtmp = None self._installdir = None self._bindir = None # a place for run-tests.py to generate executable it needs self._custom_bin_dir = None self._pythondir = None # True if we had to infer the pythondir from --with-hg self._pythondir_inferred = False self._coveragefile = None self._createdfiles = [] self._hgcommand = None self._hgpath = None self._portoffset = 0 self._ports = {} def run(self, args, parser=None): """Run the test suite.""" oldmask = os.umask(0o22) try: parser = parser or getparser() options = parseargs(args, parser) tests = [_sys2bytes(a) for a in options.tests] if options.test_list is not None: for listfile in options.test_list: with open(listfile, 'rb') as f: tests.extend(t for t in f.read().splitlines() if t) self.options = options self._checktools() testdescs = self.findtests(tests) if options.profile_runner: import statprof statprof.start() result = self._run(testdescs) if options.profile_runner: statprof.stop() statprof.display() return result finally: os.umask(oldmask) def _run(self, testdescs): testdir = getcwdb() # assume all tests in same folder for now if testdescs: pathname = os.path.dirname(testdescs[0]['path']) if pathname: testdir = os.path.join(testdir, pathname) self._testdir = osenvironb[b'TESTDIR'] = testdir osenvironb[b'TESTDIR_FORWARD_SLASH'] = osenvironb[b'TESTDIR'].replace( os.sep.encode('ascii'), b'/' ) if self.options.outputdir: self._outputdir = canonpath(_sys2bytes(self.options.outputdir)) else: self._outputdir = getcwdb() if testdescs and pathname: self._outputdir = os.path.join(self._outputdir, pathname) previoustimes = {} if self.options.order_by_runtime: previoustimes = dict(loadtimes(self._outputdir)) sorttests(testdescs, previoustimes, shuffle=self.options.random) if 'PYTHONHASHSEED' not in os.environ: # use a random python hash seed all the time # we do the randomness ourself to know what seed is used os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32)) # Rayon (Rust crate for multi-threading) will use all logical CPU cores # by default, causing thrashing on high-cpu-count systems. # Setting its limit to 3 during tests should still let us uncover # multi-threading bugs while keeping the thrashing reasonable. os.environ.setdefault("RAYON_NUM_THREADS", "3") if self.options.tmpdir: self.options.keep_tmpdir = True tmpdir = _sys2bytes(self.options.tmpdir) if os.path.exists(tmpdir): # Meaning of tmpdir has changed since 1.3: we used to create # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if # tmpdir already exists. print("error: temp dir %r already exists" % tmpdir) return 1 os.makedirs(tmpdir) else: d = None if WINDOWS: # without this, we get the default temp dir location, but # in all lowercase, which causes troubles with paths (issue3490) d = osenvironb.get(b'TMP', None) tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d) self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir) self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin') os.makedirs(self._custom_bin_dir) # detect and enforce an alternative way to specify rust extension usage if ( not (self.options.pure or self.options.rust or self.options.no_rust) and os.environ.get("HGWITHRUSTEXT") == "cpython" ): self.options.rust = True if self.options.with_hg: self._installdir = None whg = self.options.with_hg self._bindir = os.path.dirname(os.path.realpath(whg)) assert isinstance(self._bindir, bytes) self._hgcommand = os.path.basename(whg) normbin = os.path.normpath(os.path.abspath(whg)) normbin = normbin.replace(_sys2bytes(os.sep), b'/') # Other Python scripts in the test harness need to # `import mercurial`. If `hg` is a Python script, we assume # the Mercurial modules are relative to its path and tell the tests # to load Python modules from its directory. with open(whg, 'rb') as fh: initial = fh.read(1024) if re.match(b'#!.*python', initial): self._pythondir = self._bindir # If it looks like our in-repo Rust binary, use the source root. # This is a bit hacky. But rhg is still not supported outside the # source directory. So until it is, do the simple thing. elif re.search(b'/rust/target/[^/]+/hg', normbin): self._pythondir = os.path.dirname(self._testdir) # Fall back to the legacy behavior. else: self._pythondir = self._bindir self._pythondir_inferred = True else: self._installdir = os.path.join(self._hgtmp, b"install") self._bindir = os.path.join(self._installdir, b"bin") self._hgcommand = b'hg' self._pythondir = os.path.join(self._installdir, b"lib", b"python") # Force the use of hg.exe instead of relying on MSYS to recognize hg is # a python script and feed it to python.exe. Legacy stdio is force # enabled by hg.exe, and this is a more realistic way to launch hg # anyway. if WINDOWS and not self._hgcommand.endswith(b'.exe'): self._hgcommand += b'.exe' real_hg = os.path.join(self._bindir, self._hgcommand) osenvironb[b'HGTEST_REAL_HG'] = real_hg # set CHGHG, then replace "hg" command by "chg" chgbindir = self._bindir if self.options.chg or self.options.with_chg: osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1' osenvironb[b'CHGHG'] = real_hg else: # drop flag for hghave osenvironb.pop(b'CHG_INSTALLED_AS_HG', None) if self.options.chg: self._hgcommand = b'chg' elif self.options.with_chg: chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg)) self._hgcommand = os.path.basename(self.options.with_chg) # configure fallback and replace "hg" command by "rhg" rhgbindir = self._bindir if self.options.rhg or self.options.with_rhg: # Affects hghave.py osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1' # Affects configuration. Alternatives would be setting configuration through # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include # `--config` but that disrupts tests that print command lines and check expected # output. osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback' osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg else: # drop flag for hghave osenvironb.pop(b'RHG_INSTALLED_AS_HG', None) if self.options.rhg: self._hgcommand = b'rhg' elif self.options.with_rhg: rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg)) self._hgcommand = os.path.basename(self.options.with_rhg) if self.options.pyoxidized: testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) reporootdir = os.path.dirname(testdir) # XXX we should ideally install stuff instead of using the local build exe = b'hg' triple = b'' if WINDOWS: triple = b'x86_64-pc-windows-msvc' exe = b'hg.exe' elif MACOS: # TODO: support Apple silicon too triple = b'x86_64-apple-darwin' bin_path = b'build/pyoxidizer/%s/release/app/%s' % (triple, exe) full_path = os.path.join(reporootdir, bin_path) self._hgcommand = full_path # Affects hghave.py osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1' else: osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None) osenvironb[b"BINDIR"] = self._bindir osenvironb[b"PYTHON"] = PYTHON fileb = _sys2bytes(__file__) runtestdir = os.path.abspath(os.path.dirname(fileb)) osenvironb[b'RUNTESTDIR'] = runtestdir osenvironb[b'RUNTESTDIR_FORWARD_SLASH'] = runtestdir.replace( os.sep.encode('ascii'), b'/' ) sepb = _sys2bytes(os.pathsep) path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb) if os.path.islink(__file__): # test helper will likely be at the end of the symlink realfile = os.path.realpath(fileb) realdir = os.path.abspath(os.path.dirname(realfile)) path.insert(2, realdir) if chgbindir != self._bindir: path.insert(1, chgbindir) if rhgbindir != self._bindir: path.insert(1, rhgbindir) if self._testdir != runtestdir: path = [self._testdir] + path path = [self._custom_bin_dir] + path osenvironb[b"PATH"] = sepb.join(path) # Include TESTDIR in PYTHONPATH so that out-of-tree extensions # can run .../tests/run-tests.py test-foo where test-foo # adds an extension to HGRC. Also include run-test.py directory to # import modules like heredoctest. pypath = [self._pythondir, self._testdir, runtestdir] # Setting PYTHONPATH with an activated venv causes the modules installed # in it to be ignored. Therefore, include the related paths in sys.path # in PYTHONPATH. virtual_env = osenvironb.get(b"VIRTUAL_ENV") if virtual_env: virtual_env = os.path.join(virtual_env, b'') for p in sys.path: p = _sys2bytes(p) if p.startswith(virtual_env): pypath.append(p) # We have to augment PYTHONPATH, rather than simply replacing # it, in case external libraries are only available via current # PYTHONPATH. (In particular, the Subversion bindings on OS X # are in /opt/subversion.) oldpypath = osenvironb.get(IMPL_PATH) if oldpypath: pypath.append(oldpypath) osenvironb[IMPL_PATH] = sepb.join(pypath) if self.options.pure: os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure" os.environ["HGMODULEPOLICY"] = "py" if self.options.rust: os.environ["HGMODULEPOLICY"] = "rust+c" if self.options.no_rust: current_policy = os.environ.get("HGMODULEPOLICY", "") if current_policy.startswith("rust+"): os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :] os.environ.pop("HGWITHRUSTEXT", None) if self.options.allow_slow_tests: os.environ["HGTEST_SLOW"] = "slow" elif 'HGTEST_SLOW' in os.environ: del os.environ['HGTEST_SLOW'] self._coveragefile = os.path.join(self._testdir, b'.coverage') if self.options.exceptions: exceptionsdir = os.path.join(self._outputdir, b'exceptions') try: os.makedirs(exceptionsdir) except FileExistsError: pass # Remove all existing exception reports. for f in os.listdir(exceptionsdir): os.unlink(os.path.join(exceptionsdir, f)) osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir logexceptions = os.path.join(self._testdir, b'logexceptions.py') self.options.extra_config_opt.append( 'extensions.logexceptions=%s' % logexceptions.decode('utf-8') ) vlog("# Using TESTDIR", _bytes2sys(self._testdir)) vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR'])) vlog("# Using HGTMP", _bytes2sys(self._hgtmp)) vlog("# Using PATH", os.environ["PATH"]) vlog( "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]), ) vlog("# Writing to directory", _bytes2sys(self._outputdir)) try: return self._runtests(testdescs) or 0 finally: time.sleep(0.1) self._cleanup() def findtests(self, args): """Finds possible test files from arguments. If you wish to inject custom tests into the test harness, this would be a good function to monkeypatch or override in a derived class. """ if not args: if self.options.changed: proc = Popen4( b'hg st --rev "%s" -man0 .' % _sys2bytes(self.options.changed), None, 0, ) stdout, stderr = proc.communicate() args = stdout.strip(b'\0').split(b'\0') else: args = os.listdir(b'.') expanded_args = [] for arg in args: if os.path.isdir(arg): if not arg.endswith(b'/'): arg += b'/' expanded_args.extend([arg + a for a in os.listdir(arg)]) else: expanded_args.append(arg) args = expanded_args testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))') tests = [] for t in args: case = [] if not ( os.path.basename(t).startswith(b'test-') and (t.endswith(b'.py') or t.endswith(b'.t')) ): m = testcasepattern.match(os.path.basename(t)) if m is not None: t_basename, casestr = m.groups() t = os.path.join(os.path.dirname(t), t_basename) if casestr: case = casestr.split(b'#') else: continue if t.endswith(b'.t'): # .t file may contain multiple test cases casedimensions = parsettestcases(t) if casedimensions: cases = [] def addcases(case, casedimensions): if not casedimensions: cases.append(case) else: for c in casedimensions[0]: addcases(case + [c], casedimensions[1:]) addcases([], casedimensions) if case and case in cases: cases = [case] elif case: # Ignore invalid cases cases = [] else: pass tests += [{'path': t, 'case': c} for c in sorted(cases)] else: tests.append({'path': t}) else: tests.append({'path': t}) if self.options.retest: retest_args = [] for test in tests: errpath = self._geterrpath(test) if os.path.exists(errpath): retest_args.append(test) tests = retest_args return tests def _runtests(self, testdescs): def _reloadtest(test, i): # convert a test back to its description dict desc = {'path': test.path} case = getattr(test, '_case', []) if case: desc['case'] = case return self._gettest(desc, i) try: if self.options.restart: orig = list(testdescs) while testdescs: desc = testdescs[0] errpath = self._geterrpath(desc) if os.path.exists(errpath): break testdescs.pop(0) if not testdescs: print("running all tests") testdescs = orig tests = [self._gettest(d, i) for i, d in enumerate(testdescs)] num_tests = len(tests) * self.options.runs_per_test jobs = min(num_tests, self.options.jobs) failed = False kws = self.options.keywords if kws is not None: kws = kws.encode('utf-8') suite = TestSuite( self._testdir, jobs=jobs, whitelist=self.options.whitelisted, blacklist=self.options.blacklist, keywords=kws, loop=self.options.loop, runs_per_test=self.options.runs_per_test, showchannels=self.options.showchannels, tests=tests, loadtest=_reloadtest, ) verbosity = 1 if self.options.list_tests: verbosity = 0 elif self.options.verbose: verbosity = 2 runner = TextTestRunner(self, verbosity=verbosity) osenvironb.pop(b'PYOXIDIZED_IN_MEMORY_RSRC', None) osenvironb.pop(b'PYOXIDIZED_FILESYSTEM_RSRC', None) if self.options.list_tests: result = runner.listtests(suite) else: install_start_time = time.monotonic() self._usecorrectpython() if self._installdir: self._installhg() self._checkhglib("Testing") if self.options.chg: assert self._installdir self._installchg() if self.options.rhg: assert self._installdir self._installrhg() elif self.options.pyoxidized: self._build_pyoxidized() self._use_correct_mercurial() install_end_time = time.monotonic() if self._installdir: msg = 'installed Mercurial in %.2f seconds' msg %= install_end_time - install_start_time log(msg) log( 'running %d tests using %d parallel processes' % (num_tests, jobs) ) result = runner.run(suite) if result.failures or result.errors: failed = True result.onEnd() if self.options.anycoverage: self._outputcoverage() except KeyboardInterrupt: failed = True print("\ninterrupted!") if failed: return 1 def _geterrpath(self, test): # test['path'] is a relative path if 'case' in test: # for multiple dimensions test cases casestr = b'#'.join(test['case']) errpath = b'%s#%s.err' % (test['path'], casestr) else: errpath = b'%s.err' % test['path'] if self.options.outputdir: self._outputdir = canonpath(_sys2bytes(self.options.outputdir)) errpath = os.path.join(self._outputdir, errpath) return errpath def _getport(self, count): port = self._ports.get(count) # do we have a cached entry? if port is None: portneeded = 3 # above 100 tries we just give up and let test reports failure for tries in range(100): allfree = True port = self.options.port + self._portoffset for idx in range(portneeded): if not checkportisavailable(port + idx): allfree = False break self._portoffset += portneeded if allfree: break self._ports[count] = port return port def _gettest(self, testdesc, count): """Obtain a Test by looking at its filename. Returns a Test instance. The Test may not be runnable if it doesn't map to a known type. """ path = testdesc['path'] lctest = path.lower() testcls = Test for ext, cls in self.TESTTYPES: if lctest.endswith(ext): testcls = cls break refpath = os.path.join(getcwdb(), path) tmpdir = os.path.join(self._hgtmp, b'child%d' % count) # extra keyword parameters. 'case' is used by .t tests kwds = {k: testdesc[k] for k in ['case'] if k in testdesc} t = testcls( refpath, self._outputdir, tmpdir, keeptmpdir=self.options.keep_tmpdir, debug=self.options.debug, first=self.options.first, timeout=self.options.timeout, startport=self._getport(count), extraconfigopts=self.options.extra_config_opt, shell=self.options.shell, hgcommand=self._hgcommand, usechg=bool(self.options.with_chg or self.options.chg), chgdebug=self.options.chg_debug, useipv6=useipv6, **kwds ) t.should_reload = True return t def _cleanup(self): """Clean up state from this test invocation.""" if self.options.keep_tmpdir: return vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp)) shutil.rmtree(self._hgtmp, True) for f in self._createdfiles: try: os.remove(f) except OSError: pass def _usecorrectpython(self): """Configure the environment to use the appropriate Python in tests.""" # Tests must use the same interpreter as us or bad things will happen. if WINDOWS: pyexe_names = [b'python', b'python3', b'python.exe'] else: pyexe_names = [b'python', b'python3'] # os.symlink() is a thing with py3 on Windows, but it requires # Administrator rights. if not WINDOWS and getattr(os, 'symlink', None): msg = "# Making python executable in test path a symlink to '%s'" msg %= sysexecutable vlog(msg) for pyexename in pyexe_names: mypython = os.path.join(self._custom_bin_dir, pyexename) try: if os.readlink(mypython) == sysexecutable: continue os.unlink(mypython) except FileNotFoundError: pass if self._findprogram(pyexename) != sysexecutable: try: os.symlink(sysexecutable, mypython) self._createdfiles.append(mypython) except FileExistsError: # child processes may race, which is harmless pass elif WINDOWS and not os.getenv('MSYSTEM'): raise AssertionError('cannot run test on Windows without MSYSTEM') else: # Generate explicit file instead of symlink # # This is especially important as Windows doesn't have # `python3.exe`, and MSYS cannot understand the reparse point with # that name provided by Microsoft. Create a simple script on PATH # with that name that delegates to the py3 launcher so the shebang # lines work. esc_executable = _sys2bytes(shellquote(sysexecutable)) for pyexename in pyexe_names: stub_exec_path = os.path.join(self._custom_bin_dir, pyexename) with open(stub_exec_path, 'wb') as f: f.write(b'#!/bin/sh\n') f.write(b'%s "$@"\n' % esc_executable) if WINDOWS: # adjust the path to make sur the main python finds it own dll path = os.environ['PATH'].split(os.pathsep) main_exec_dir = os.path.dirname(sysexecutable) extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir] # Binaries installed by pip into the user area like pylint.exe may # not be in PATH by default. appdata = os.environ.get('APPDATA') vi = sys.version_info if appdata is not None: python_dir = 'Python%d%d' % (vi[0], vi[1]) scripts_path = [appdata, 'Python', python_dir, 'Scripts'] scripts_dir = os.path.join(*scripts_path) extra_paths.append(scripts_dir) os.environ['PATH'] = os.pathsep.join(extra_paths + path) def _use_correct_mercurial(self): target_exec = os.path.join(self._custom_bin_dir, b'hg') if self._hgcommand != b'hg': # shutil.which only accept bytes from 3.8 real_exec = which(self._hgcommand) if real_exec is None: raise ValueError('could not find exec path for "%s"', real_exec) if real_exec == target_exec: # do not overwrite something with itself return if WINDOWS: with open(target_exec, 'wb') as f: f.write(b'#!/bin/sh\n') escaped_exec = shellquote(_bytes2sys(real_exec)) f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec)) else: os.symlink(real_exec, target_exec) self._createdfiles.append(target_exec) def _installhg(self): """Install hg into the test environment. This will also configure hg with the appropriate testing settings. """ vlog("# Performing temporary installation of HG") installerrs = os.path.join(self._hgtmp, b"install.err") compiler = '' if self.options.compiler: compiler = '--compiler ' + self.options.compiler setup_opts = b"" if self.options.pure: setup_opts = b"--pure" elif self.options.rust: setup_opts = b"--rust" elif self.options.no_rust: setup_opts = b"--no-rust" # Run installer in hg root compiler = _sys2bytes(compiler) script = _sys2bytes(os.path.realpath(sys.argv[0])) exe = _sys2bytes(sysexecutable) hgroot = os.path.dirname(os.path.dirname(script)) self._hgroot = hgroot os.chdir(hgroot) nohome = b'--home=""' if WINDOWS: # The --home="" trick works only on OS where os.sep == '/' # because of a distutils convert_path() fast-path. Avoid it at # least on Windows for now, deal with .pydistutils.cfg bugs # when they happen. nohome = b'' cmd = ( b'"%(exe)s" setup.py %(setup_opts)s clean --all' b' build %(compiler)s --build-base="%(base)s"' b' install --force --prefix="%(prefix)s"' b' --install-lib="%(libdir)s"' b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1' % { b'exe': exe, b'setup_opts': setup_opts, b'compiler': compiler, b'base': os.path.join(self._hgtmp, b"build"), b'prefix': self._installdir, b'libdir': self._pythondir, b'bindir': self._bindir, b'nohome': nohome, b'logfile': installerrs, } ) # setuptools requires install directories to exist. def makedirs(p): try: os.makedirs(p) except FileExistsError: pass makedirs(self._pythondir) makedirs(self._bindir) vlog("# Running", cmd.decode("utf-8")) if subprocess.call(_bytes2sys(cmd), shell=True, env=original_env) == 0: if not self.options.verbose: try: os.remove(installerrs) except FileNotFoundError: pass else: with open(installerrs, 'rb') as f: for line in f: sys.stdout.buffer.write(line) sys.exit(1) os.chdir(self._testdir) hgbat = os.path.join(self._bindir, b'hg.bat') if os.path.isfile(hgbat): # hg.bat expects to be put in bin/scripts while run-tests.py # installation layout put it in bin/ directly. Fix it with open(hgbat, 'rb') as f: data = f.read() if br'"%~dp0..\python" "%~dp0hg" %*' in data: data = data.replace( br'"%~dp0..\python" "%~dp0hg" %*', b'"%~dp0python" "%~dp0hg" %*', ) with open(hgbat, 'wb') as f: f.write(data) else: print('WARNING: cannot fix hg.bat reference to python.exe') if self.options.anycoverage: custom = os.path.join( osenvironb[b'RUNTESTDIR'], b'sitecustomize.py' ) target = os.path.join(self._pythondir, b'sitecustomize.py') vlog('# Installing coverage trigger to %s' % target) shutil.copyfile(custom, target) rc = os.path.join(self._testdir, b'.coveragerc') vlog('# Installing coverage rc to %s' % rc) osenvironb[b'COVERAGE_PROCESS_START'] = rc covdir = os.path.join(self._installdir, b'..', b'coverage') try: os.mkdir(covdir) except FileExistsError: pass osenvironb[b'COVERAGE_DIR'] = covdir def _checkhglib(self, verb): """Ensure that the 'mercurial' package imported by python is the one we expect it to be. If not, print a warning to stderr.""" if self._pythondir_inferred: # The pythondir has been inferred from --with-hg flag. # We cannot expect anything sensible here. return expecthg = os.path.join(self._pythondir, b'mercurial') actualhg = self._gethgpath() if os.path.abspath(actualhg) != os.path.abspath(expecthg): sys.stderr.write( 'warning: %s with unexpected mercurial lib: %s\n' ' (expected %s)\n' % (verb, actualhg, expecthg) ) def _gethgpath(self): """Return the path to the mercurial package that is actually found by the current Python interpreter.""" if self._hgpath is not None: return self._hgpath cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"' cmd = _bytes2sys(cmd % PYTHON) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) out, err = p.communicate() self._hgpath = out.strip() return self._hgpath def _installchg(self): """Install chg into the test environment""" vlog('# Performing temporary installation of CHG') assert os.path.dirname(self._bindir) == self._installdir assert self._hgroot, 'must be called after _installhg()' cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % { b'make': b'make', # TODO: switch by option or environment? b'prefix': self._installdir, } cwd = os.path.join(self._hgroot, b'contrib', b'chg') vlog("# Running", cmd) proc = subprocess.Popen( cmd, shell=True, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) out, _err = proc.communicate() if proc.returncode != 0: sys.stdout.buffer.write(out) sys.exit(1) def _installrhg(self): """Install rhg into the test environment""" vlog('# Performing temporary installation of rhg') assert os.path.dirname(self._bindir) == self._installdir assert self._hgroot, 'must be called after _installhg()' cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % { b'make': b'make', # TODO: switch by option or environment? b'prefix': self._installdir, } cwd = self._hgroot vlog("# Running", cmd) proc = subprocess.Popen( cmd, shell=True, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) out, _err = proc.communicate() if proc.returncode != 0: sys.stdout.buffer.write(out) sys.exit(1) def _build_pyoxidized(self): """build a pyoxidized version of mercurial into the test environment Ideally this function would be `install_pyoxidier` and would both build and install pyoxidier. However we are starting small to get pyoxidizer build binary to testing quickly. """ vlog('# build a pyoxidized version of Mercurial') assert os.path.dirname(self._bindir) == self._installdir assert self._hgroot, 'must be called after _installhg()' target = b'' if WINDOWS: target = b'windows' elif MACOS: target = b'macos' cmd = b'"%(make)s" pyoxidizer-%(platform)s-tests' % { b'make': b'make', b'platform': target, } cwd = self._hgroot vlog("# Running", cmd) proc = subprocess.Popen( _bytes2sys(cmd), shell=True, cwd=_bytes2sys(cwd), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) out, _err = proc.communicate() if proc.returncode != 0: sys.stdout.buffer.write(out) sys.exit(1) cmd = _bytes2sys(b"%s debuginstall -Tjson" % self._hgcommand) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) out, err = p.communicate() props = json.loads(out)[0] # Affects hghave.py osenvironb.pop(b'PYOXIDIZED_IN_MEMORY_RSRC', None) osenvironb.pop(b'PYOXIDIZED_FILESYSTEM_RSRC', None) if props["hgmodules"] == props["pythonexe"]: osenvironb[b'PYOXIDIZED_IN_MEMORY_RSRC'] = b'1' else: osenvironb[b'PYOXIDIZED_FILESYSTEM_RSRC'] = b'1' def _outputcoverage(self): """Produce code coverage output.""" import coverage coverage = coverage.coverage vlog('# Producing coverage report') # chdir is the easiest way to get short, relative paths in the # output. os.chdir(self._hgroot) covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage') cov = coverage( data_file=os.path.join(_bytes2sys(self._outputdir), '.coverage'), ) # Map install directory paths back to source directory. cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)] cov.combine(data_paths=[ os.path.join(covdir, p) for p in os.listdir(covdir) ]) cov.save() omit = [ _bytes2sys(os.path.join(x, b'*')) for x in [self._bindir, self._testdir] ] cov.report(ignore_errors=True, omit=omit) if self.options.htmlcov: htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov') cov.html_report(directory=htmldir, omit=omit) if self.options.annotate: adir = os.path.join(_bytes2sys(self._outputdir), 'annotated') if not os.path.isdir(adir): os.mkdir(adir) cov.annotate(directory=adir, omit=omit) def _findprogram(self, program): """Search PATH for a executable program""" dpb = _sys2bytes(os.defpath) sepb = _sys2bytes(os.pathsep) for p in osenvironb.get(b'PATH', dpb).split(sepb): name = os.path.join(p, program) if WINDOWS or os.access(name, os.X_OK): return _bytes2sys(name) return None def _checktools(self): """Ensure tools required to run tests are present.""" for p in self.REQUIREDTOOLS: if WINDOWS and not p.endswith(b'.exe'): p += b'.exe' found = self._findprogram(p) p = p.decode("utf-8") if found: vlog("# Found prerequisite", p, "at", found) else: print("WARNING: Did not find prerequisite tool: %s " % p) def aggregateexceptions(path): exceptioncounts = collections.Counter() testsbyfailure = collections.defaultdict(set) failuresbytest = collections.defaultdict(set) for f in os.listdir(path): with open(os.path.join(path, f), 'rb') as fh: data = fh.read().split(b'\0') if len(data) != 5: continue exc, mainframe, hgframe, hgline, testname = data exc = exc.decode('utf-8') mainframe = mainframe.decode('utf-8') hgframe = hgframe.decode('utf-8') hgline = hgline.decode('utf-8') testname = testname.decode('utf-8') key = (hgframe, hgline, exc) exceptioncounts[key] += 1 testsbyfailure[key].add(testname) failuresbytest[testname].add(key) # Find test having fewest failures for each failure. leastfailing = {} for key, tests in testsbyfailure.items(): fewesttest = None fewestcount = 99999999 for test in sorted(tests): if len(failuresbytest[test]) < fewestcount: fewesttest = test fewestcount = len(failuresbytest[test]) leastfailing[key] = (fewestcount, fewesttest) # Create a combined counter so we can sort by total occurrences and # impacted tests. combined = {} for key in exceptioncounts: combined[key] = ( exceptioncounts[key], len(testsbyfailure[key]), leastfailing[key][0], leastfailing[key][1], ) return { 'exceptioncounts': exceptioncounts, 'total': sum(exceptioncounts.values()), 'combined': combined, 'leastfailing': leastfailing, 'byfailure': testsbyfailure, 'bytest': failuresbytest, } if __name__ == '__main__': if WINDOWS and not os.getenv('MSYSTEM'): print('cannot run test on Windows without MSYSTEM', file=sys.stderr) print( '(if you need to do so contact the mercurial devs: ' 'mercurial@mercurial-scm.org)', file=sys.stderr, ) sys.exit(255) runner = TestRunner() try: import msvcrt msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) except ImportError: pass sys.exit(runner.run(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/sitecustomize.py0000644000000000000000000000064214751647721014262 0ustar00import os if os.environ.get('COVERAGE_PROCESS_START'): try: import coverage import uuid covpath = os.path.join( os.environ['COVERAGE_DIR'], 'cov.%s' % uuid.uuid1() ) cov = coverage.coverage(data_file=covpath, auto_data=True) cov._warn_no_data = False cov._warn_unimported_source = False cov.start() except ImportError: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-addbranchrevs.t0000644000000000000000000000105714751647721014752 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" This test doesn’t test any git-related functionality. It checks that a previous bug is not present where mercurial.hg.addbranchrevs() was erroneously monkey-patched such that the 'checkout' return value was always None. This caused the pull to not update to the passed revision. $ hg init orig $ cd orig $ echo a > a; hg add a; hg ci -m a $ hg branch foo -q $ echo b > b; hg add b; hg ci -m b $ cd .. $ hg clone orig clone -r 0 -q $ cd clone $ hg pull -u -r 1 -q $ hg id -n 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-ambiguousprefix.t0000644000000000000000000000153114751647721015352 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' This commit is called gamma10 so that its hash will have the same initial digit as commit alpha. This lets us test ambiguous abbreviated identifiers. $ echo gamma10 > gamma10 $ git add gamma10 $ fn_git_commit -m 'add gamma10' $ cd .. $ hg clone gitrepo hgrepo importing 3 git commits new changesets ff7a2f2d8d70:8e3f0ecc9aef (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log -r 'gitnode(7e)' abort: git-mapfile@7e: ambiguous identifier!? (re) [50] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-annotate.t0000644000000000000000000000352714751647721013761 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> "$HGRCPATH" << EOF > [ui] > merge = :merge3 > EOF init $ hg init repo $ cd repo commit $ cat < a > a > a > EOF $ hg add a $ fn_hg_commit -m 1 $ cat < a > a > a > a > EOF $ fn_hg_commit -m 2 $ cat < a > a > b > a > EOF $ fn_hg_commit -m 3 annotate multiple files $ hg annotate a 0: a 2: b 1: a $ hg annotate --skip 1 a 0: a 2: b 0* a $ hg gexport $ hg log -T '{rev}:{node} {gitnode}\n' 2:beb139b96eec386addc02d48db524b7646ef1605 19388575d02e71e917e7013aa854d4a21c509819 1:a9a255d66663f9216bdcf8dda69211d7280f7278 debec50a14cc4830584dd4fa1507c51cce1c098f 0:8d4731bd0f4a57e123a79463b5294325be6cf8f0 88f28c06a1ede9a70852ab1bf9818150fabaaaa9 $ cat < .git-blame-ignore-revs > # this is a comment, and the next line should be ignored > # 19388575d02e71e917e7013aa854d4a21c509819 > debec50a14cc4830584dd4fa1507c51cce1c098f > b4145d431a9fc5712ffe35f30b631eab89f7cb7f > EOF $ hg annotate a 0: a 2: b 1: a $ hg annotate a \ > --debug \ > --config git.blame.ignoreRevsFile=.git-blame-ignore-revs skipping debec50a14cc -> a9a255d66663 0: a 2: b 0* a $ hg add .git-blame-ignore-revs $ hg annotate a \ > --debug \ > --config git.blame.ignoreRevsFile=.git-blame-ignore-revs skipping debec50a14cc -> a9a255d66663 0: a 2: b 0* a $ hg annotate a \ > --config git.blame.ignoreRevsFile=badfile 0: a 2: b 1: a $ hg annotate -T'{lines % "{rev}:{node|short} {gitnode|short}: {line}"}' a 0:8d4731bd0f4a 88f28c06a1ed: a 2:beb139b96eec 19388575d02e: b 1:a9a255d66663 debec50a14cc: a $ cd .. $ hg -R repo annotate repo/a \ > --debug \ > --config git.blame.ignoreRevsFile=.git-blame-ignore-revs skipping debec50a14cc -> a9a255d66663 0: a 2: b 0* a ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-bookmark-workflow.t0000644000000000000000000001556614751647721015633 0ustar00This test demonstrates how Hg works with remote Hg bookmarks compared with remote branches via Hg-Git. Ideally, they would behave identically. In practice, some differences are unavoidable, but we should try to minimize them. This test should not bother testing the behavior of bookmark creation, deletion, activation, deactivation, etc. These behaviors, while important to the end user, don't vary at all when Hg-Git is in use. Only the synchonization of bookmarks should be considered "under test", and mutation of bookmarks locally is only to provide a test fixture. Load commonly used test logic $ . "$TESTDIR/testutil" $ gitcount=10 $ gitcommit() > { > GIT_AUTHOR_DATE="2007-01-01 00:00:$gitcount +0000" > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error" > gitcount=`expr $gitcount + 1` > } $ hgcount=10 $ hgcommit() > { > HGDATE="2007-01-01 00:00:$hgcount +0000" > hg commit -u "test " -d "$HGDATE" "$@" >/dev/null 2>/dev/null || echo "hg commit error" > hgcount=`expr $hgcount + 1` > } $ gitstate() > { > git log --format=" %h \"%s\" refs:%d" $@ | sed 's/HEAD, //' > } $ hgstate() > { > hg log --template " {rev} {node|short} \"{desc}\" bookmarks: [{bookmarks}]\n" $@ > } $ hggitstate() > { > hg log --template " {rev} {node|short} {gitnode|short} \"{desc}\" bookmarks: [{bookmarks}]\n" $@ > } Initialize remote hg and git repos with equivalent initial contents $ hg init hgremoterepo $ cd hgremoterepo $ hg bookmark master $ for f in alpha beta gamma delta; do > echo $f > $f; hg add $f; hgcommit -m "add $f" > done $ hg bookmark -r 1 b1 $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [] 1 7bcd915dc873 "add beta" bookmarks: [b1] 0 3442585be8a6 "add alpha" bookmarks: [] $ cd .. $ git init -q gitremoterepo $ cd gitremoterepo $ for f in alpha beta gamma delta; do > echo $f > $f; git add $f; gitcommit -m "add $f" > done $ git branch b1 9497a4e $ gitstate 55b133e "add delta" refs: (*master) (glob) d338971 "add gamma" refs: 9497a4e "add beta" refs: (b1) 7eeab2e "add alpha" refs: $ cd .. Cloning transfers all bookmarks from remote to local $ hg clone -q hgremoterepo purehglocalrepo $ cd purehglocalrepo $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [] 1 7bcd915dc873 "add beta" bookmarks: [b1] 0 3442585be8a6 "add alpha" bookmarks: [] $ cd .. $ hg clone -q gitremoterepo hggitlocalrepo --config hggit.usephases=True $ cd hggitlocalrepo $ hggitstate 3 03769a650ded 55b133e1d558 "add delta" bookmarks: [master] 2 ca33a262eb46 d338971a96e2 "add gamma" bookmarks: [] 1 7fe02317c63d 9497a4ee62e1 "add beta" bookmarks: [b1] 0 ff7a2f2d8d70 7eeab2ea75ec "add alpha" bookmarks: [] Make sure that master is public $ hg phase -r master 3: public $ cd .. No changes $ cd purehglocalrepo $ hg incoming -B comparing with $TESTTMP/hgremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg outgoing comparing with $TESTTMP/hgremoterepo searching for changes no changes found [1] $ hg outgoing -B comparing with $TESTTMP/hgremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg push pushing to $TESTTMP/hgremoterepo searching for changes no changes found [1] $ cd .. $ cd hggitlocalrepo $ hg incoming -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg outgoing comparing with $TESTTMP/gitremoterepo searching for changes no changes found [1] $ hg outgoing -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg push pushing to $TESTTMP/gitremoterepo searching for changes no changes found [1] $ cd .. Bookmarks on existing revs: - change b1 on local repo - introduce b2 on local repo - introduce b3 on remote repo Bookmarks on new revs - introduce b4 on a new rev on the remote $ cd hgremoterepo $ hg bookmark -r master b3 $ hg bookmark -r master b4 $ hg update -q b4 $ echo epsilon > epsilon; hg add epsilon; hgcommit -m 'add epsilon' $ hgstate 4 d979bb8e0fbb "add epsilon" bookmarks: [b4] 3 fc2664cac217 "add delta" bookmarks: [b3 master] 2 d85ced7ae9d6 "add gamma" bookmarks: [] 1 7bcd915dc873 "add beta" bookmarks: [b1] 0 3442585be8a6 "add alpha" bookmarks: [] $ cd .. $ cd purehglocalrepo $ hg bookmark -fr 2 b1 $ hg bookmark -r 0 b2 $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [b1] 1 7bcd915dc873 "add beta" bookmarks: [] 0 3442585be8a6 "add alpha" bookmarks: [b2] $ hg incoming -B comparing with $TESTTMP/hgremoterepo searching for changed bookmarks b3 fc2664cac217 b4 d979bb8e0fbb $ hg outgoing comparing with $TESTTMP/hgremoterepo searching for changes no changes found [1] As of 2.3, Mercurial's outgoing -B doesn't actually show changed bookmarks It only shows "new" bookmarks. Thus, b1 doesn't show up. This changed in 3.4 to start showing changed and deleted bookmarks again. $ hg outgoing -B | grep -v -E -w 'b1|b3|b4' comparing with $TESTTMP/hgremoterepo searching for changed bookmarks b2 3442585be8a6 $ cd .. $ cd gitremoterepo $ git branch b3 master $ git checkout -b b4 master Switched to a new branch 'b4' $ echo epsilon > epsilon $ git add epsilon $ gitcommit -m 'add epsilon' $ gitstate fcfd2c0 "add epsilon" refs: (*b4) (glob) 55b133e "add delta" refs: (master, b3) d338971 "add gamma" refs: 9497a4e "add beta" refs: (b1) 7eeab2e "add alpha" refs: $ cd .. $ cd hggitlocalrepo $ hg bookmark -fr 2 b1 $ hg bookmark -r 0 b2 $ hgstate 3 03769a650ded "add delta" bookmarks: [master] 2 ca33a262eb46 "add gamma" bookmarks: [b1] 1 7fe02317c63d "add beta" bookmarks: [] 0 ff7a2f2d8d70 "add alpha" bookmarks: [b2] $ hg incoming -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks b3 03769a650ded b4 fcfd2c0262db $ hg outgoing comparing with $TESTTMP/gitremoterepo searching for changes no changes found [1] As of 2.3, Mercurial's outgoing -B doesn't actually show changed bookmarks It only shows "new" bookmarks. Thus, b1 doesn't show up. This changed in 3.4 to start showing changed and deleted bookmarks again. $ hg outgoing -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks b1 ca33a262eb46 b2 ff7a2f2d8d70 b3 b4 $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-branch-bookmark-suffix.t0000644000000000000000000002036114751647721016505 0ustar00#testcases with-path without-path Load commonly used test logic $ . "$TESTDIR/testutil" $ echo "[git]" >> $HGRCPATH $ echo "branch_bookmark_suffix=_bookmark" >> $HGRCPATH $ git init -q --bare repo.git $ hg clone repo.git hgrepo updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo #if without-path $ rm .hg/hgrc #endif $ hg branch -q branch1 $ hg bookmark branch1_bookmark $ echo f1 > f1 $ hg add f1 $ fn_hg_commit -m "add f1" $ hg branch -q branch2 $ hg bookmark branch2_bookmark $ echo f2 > f2 $ hg add f2 $ fn_hg_commit -m "add f2" $ hg log --graph @ changeset: 1:600de9b6d498 | branch: branch2 | bookmark: branch2_bookmark | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add f2 | o changeset: 0:40a840c1f8ae branch: branch1 bookmark: branch1_bookmark user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add f1 $ hg push -B asdasd ../repo.git pushing to ../repo.git abort: the -B/--bookmarks option is not supported when branch_bookmark_suffix is set [255] $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 2 commits with 2 trees and 2 blobs adding reference refs/heads/branch1 adding reference refs/heads/branch2 $ cd .. $ cd repo.git $ git symbolic-ref HEAD refs/heads/branch1 $ git branch * branch1 branch2 $ cd .. $ git clone repo.git gitrepo Cloning into 'gitrepo'... done. $ cd gitrepo $ git checkout -q branch1 $ echo g1 >> f1 $ git add f1 $ fn_git_commit -m "append f1" $ git checkout -q branch2 $ echo g2 >> f2 $ git add f2 $ fn_git_commit -m "append f2" $ git checkout -b branch3 Switched to a new branch 'branch3' $ echo g3 >> f3 $ git add f3 $ fn_git_commit -m "append f3" $ git push origin branch1 branch2 branch3 To $TESTTMP/repo.git bbfe79a..d8aef79 branch1 -> branch1 288e92b..f8f8de5 branch2 -> branch2 * [new branch] branch3 -> branch3 make sure the commit doesn't have an HG:rename-source annotation $ git cat-file commit d8aef79 tree b5644d8071b8a5963b8d1fd089fb3fdfb14b1203 parent bbfe79acf62dcd6a97763e2a67424a6de8a96941 author test 1167609612 +0000 committer test 1167609612 +0000 append f1 $ cd .. $ cd hgrepo $ hg paths default = $TESTTMP/repo.git (with-path !) $ hg pull ../repo.git pulling from ../repo.git importing 3 git commits updating bookmark branch1_bookmark updating bookmark branch2_bookmark adding bookmark branch3_bookmark new changesets 8211cade99e4:faf44fc3a4e8 (3 drafts) (run 'hg heads' to see heads) $ hg log --graph o changeset: 4:faf44fc3a4e8 | bookmark: branch3_bookmark | tag: default/branch3 (with-path !) | tag: tip | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: append f3 | o changeset: 3:ae8eb55f7090 | bookmark: branch2_bookmark | tag: default/branch2 (with-path !) | parent: 1:600de9b6d498 | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: append f2 | | o changeset: 2:8211cade99e4 | | bookmark: branch1_bookmark | | tag: default/branch1 (with-path !) | | parent: 0:40a840c1f8ae | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: append f1 | | @ | changeset: 1:600de9b6d498 |/ branch: branch2 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add f2 | o changeset: 0:40a840c1f8ae branch: branch1 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add f1 $ cd .. Try cloning a bookmark, and make sure it gets checked out: $ rm -r hgrepo $ hg clone -r branch3 repo.git hgrepo importing 4 git commits new changesets 40a840c1f8ae:faf44fc3a4e8 (4 drafts) updating to bookmark branch3_bookmark 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg bookmarks branch2_bookmark 2:ae8eb55f7090 * branch3_bookmark 3:faf44fc3a4e8 $ hg log --graph @ changeset: 3:faf44fc3a4e8 | bookmark: branch3_bookmark | tag: default/branch3 | tag: tip | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: append f3 | o changeset: 2:ae8eb55f7090 | bookmark: branch2_bookmark | tag: default/branch2 | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: append f2 | o changeset: 1:600de9b6d498 | branch: branch2 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add f2 | o changeset: 0:40a840c1f8ae branch: branch1 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add f1 $ cd .. Try cloning something that's both a bookmark and a branch, and see the results. They're a bit suprising as the bookmark does get activated, but the branch get checked out. Although this does seem a bit odd, so does the scenario. $ rm -r hgrepo $ hg clone -r branch1 repo.git hgrepo importing 2 git commits new changesets 40a840c1f8ae:8211cade99e4 (2 drafts) updating to branch branch1 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg bookmarks * branch1_bookmark 1:8211cade99e4 $ hg log --graph o changeset: 1:8211cade99e4 | bookmark: branch1_bookmark | tag: default/branch1 | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: append f1 | @ changeset: 0:40a840c1f8ae branch: branch1 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add f1 $ cd .. Now try pulling a diverged bookmark: $ rm -r hgrepo #if with-path $ hg clone -U repo.git hgrepo importing 5 git commits new changesets 40a840c1f8ae:faf44fc3a4e8 (5 drafts) #else $ hg init hgrepo $ hg -R hgrepo pull repo.git pulling from repo.git importing 5 git commits adding bookmark branch1_bookmark adding bookmark branch2_bookmark adding bookmark branch3_bookmark new changesets 40a840c1f8ae:faf44fc3a4e8 (5 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) #endif $ cd gitrepo $ git checkout -q branch1 $ fn_git_rebase branch3 $ git push -f To $TESTTMP/repo.git + d8aef79...ce1d1c5 branch1 -> branch1 (forced update) $ cd ../hgrepo $ hg pull ../repo.git pulling from ../repo.git importing 1 git commits not updating diverged bookmark branch1_bookmark new changesets 895d0307f8b7 (1 drafts) (run 'hg update' to get a working copy) $ hg log --graph o changeset: 5:895d0307f8b7 | tag: default/branch1 (with-path !) | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: append f1 | o changeset: 4:faf44fc3a4e8 | bookmark: branch3_bookmark | tag: default/branch3 (with-path !) | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: append f3 | o changeset: 3:ae8eb55f7090 | bookmark: branch2_bookmark | tag: default/branch2 (with-path !) | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: append f2 | o changeset: 2:600de9b6d498 | branch: branch2 | parent: 0:40a840c1f8ae | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add f2 | | o changeset: 1:8211cade99e4 |/ bookmark: branch1_bookmark | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: append f1 | o changeset: 0:40a840c1f8ae branch: branch1 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add f1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-bundle.t0000644000000000000000000001766214751647721013426 0ustar00 Load commonly used test logic $ . "$TESTDIR/testutil" Enable bundling, and add a nice template for inspecting Git state. $ cat >> $HGRCPATH < [experimental] > hg-git-bundle = yes > [templates] > git = {rev}:{node|short} | {gitnode|short} | {tags} |\n > EOF Create a Git repository containing a couple of commits and two non-tip tags: $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag thetag $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git tag -m 'an annotated tag' theothertag $ echo gamma > gamma $ git add gamma $ git add . $ fn_git_commit -m 'add gamma' $ git tag -ln theothertag an annotated tag thetag add alpha $ cd .. Clone it! $ hg clone gitrepo hgrepo importing 3 git commits new changesets ff7a2f2d8d70:ca33a262eb46 (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo Create a bundle with our metadata, and inspect it: $ hg bundle --all ../bundle-w-git.hg 3 changesets found $ hg debugbundle --all ../bundle-w-git.hg | grep hg-git exp-hg-git-map -- {} (mandatory: False) exp-hg-git-tags -- {} (mandatory: False) $ hg debugbundle --all ../bundle-w-git.hg > bundle-w-git.out Create a bundle without our metadata, and inspect it: $ hg bundle --all ../bundle-wo-git.hg --config experimental.hg-git-bundle=no 3 changesets found $ hg debugbundle --all ../bundle-wo-git.hg | grep hg-git [1] Verify that those are different: $ hg debugbundle --all ../bundle-wo-git.hg > bundle-wo-git.out $ cmp -s bundle-w-git.out bundle-wo-git.out [1] Now create a bundle without hg-git enabled at all, which should be exactly similar to what you get when you disable metadata embedding; this verifies we don't accidentally pollute bundles. $ hg bundle --all --config extensions.hggit=! ../bundle-wo-hggit.hg 3 changesets found $ hg debugbundle --all ../bundle-wo-hggit.hg > bundle-wo-hggit.out $ cmp -s bundle-wo-git.hg bundle-wo-hggit.hg [2] $ cmp -s bundle-wo-git.out bundle-wo-hggit.out $ cd .. $ rm -r hgrepo Does unbundling transfer state? $ hg init hgrepo $ hg -R hgrepo unbundle bundle-w-git.hg adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) (run 'hg update' to get a working copy) $ hg -R hgrepo log -T git 2:ca33a262eb46 | d338971a96e2 | tip | 1:7fe02317c63d | 9497a4ee62e1 | theothertag | 0:ff7a2f2d8d70 | 7eeab2ea75ec | thetag | $ hg -R hgrepo pull gitrepo pulling from gitrepo warning: created new git repository at $TESTTMP/hgrepo/.hg/git no changes found adding bookmark master $ rm -r hgrepo Can we unbundle something without git state? $ hg init hgrepo $ hg -R hgrepo unbundle bundle-wo-git.hg adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) (run 'hg update' to get a working copy) $ hg -R hgrepo log -T git 2:ca33a262eb46 | | tip | 1:7fe02317c63d | | | 0:ff7a2f2d8d70 | | | $ hg -R hgrepo pull gitrepo pulling from gitrepo importing 3 git commits adding bookmark master (run 'hg update' to get a working copy) $ rm -r hgrepo Regular mercurial shouldn't choke on our bundle $ hg init hgrepo $ cat >> hgrepo/.hg/hgrc < [extensions] > hggit = ! > EOF $ hg -R hgrepo unbundle bundle-wo-git.hg adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) (run 'hg update' to get a working copy) $ hg -R hgrepo log -T git 2:ca33a262eb46 | | tip | 1:7fe02317c63d | | | 0:ff7a2f2d8d70 | | | $ hg -R hgrepo pull gitrepo pulling from gitrepo abort: repository gitrepo not found!? (re) [255] $ rm -r hgrepo What happens if we unbundle twice? $ hg init hgrepo $ hg -R hgrepo unbundle bundle-w-git.hg adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) (run 'hg update' to get a working copy) $ hg -R hgrepo unbundle bundle-w-git.hg adding changesets adding manifests adding file changes added 0 changesets with 0 changes to 3 files (run 'hg update' to get a working copy) $ hg -R hgrepo log -T git 2:ca33a262eb46 | d338971a96e2 | tip | 1:7fe02317c63d | 9497a4ee62e1 | theothertag | 0:ff7a2f2d8d70 | 7eeab2ea75ec | thetag | $ hg -R hgrepo pull gitrepo pulling from gitrepo warning: created new git repository at $TESTTMP/hgrepo/.hg/git no changes found adding bookmark master $ rm -r hgrepo Alas, cloning a bundle doesn't work yet: (Mercurial is apparently quite dumb here, so we won't try to fix this for now, but this test mostly exists so that we notice if ever starts working, or breaks entirely.) $ hg clone bundle-w-git.hg hgrepo requesting all changes adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo log -T git 2:ca33a262eb46 | | tip | 1:7fe02317c63d | | | 0:ff7a2f2d8d70 | | | $ rm -r hgrepo Now, lets try to be a bit evil. How does pulling partial state work? First, more git happenings: $ cd gitrepo $ git checkout -b otherbranch thetag Switched to a new branch 'otherbranch' $ echo 42 > baz $ git add baz $ fn_git_commit -m 3 $ cd .. Pull, 'em, and create a partial bundle: $ hg clone gitrepo hgrepo importing 4 git commits new changesets ff7a2f2d8d70:d87bf3ef6a53 (4 drafts) updating to bookmark otherbranch 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo bundle --base 'p1(tip)' -r tip bundle-w-git-2.hg 1 changesets found $ rm -r hgrepo Now, load only that bundle into a repository without any git state $ hg clone -r 1 bundle-w-git.hg hgrepo --config extensions.hggit=! adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files new changesets * (glob) updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg unbundle ../bundle-w-git-2.hg adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) new changesets * (glob) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg pull ../gitrepo pulling from ../gitrepo warning: created new git repository at $TESTTMP/hgrepo/.hg/git importing 3 git commits adding bookmark master adding bookmark otherbranch new changesets ca33a262eb46 (1 drafts) (run 'hg update' to get a working copy) $ cd .. $ rm -r hgrepo Now, try pushing with only the metadata: $ hg init hgrepo $ cd hgrepo $ hg unbundle -u ../bundle-w-git.hg adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets * (glob) 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo kaflaflibob > bajizmo $ fn_hg_commit -A -m 4 $ hg book -r tip master $ hg push ../gitrepo pushing to ../gitrepo warning: created new git repository at $TESTTMP/hgrepo/.hg/git abort: cannot push git commit d338971a96e2 as it is not present locally (please try pulling first, or as a fallback run git-cleanup to re-export the missing commits) [255] Try to repopulate the git state from a bundle $ hg debug-remove-hggit-state clearing out the git cache data $ hg log -qr 'fromgit()' $ hg unbundle -u ../bundle-w-git.hg adding changesets adding manifests adding file changes added 0 changesets with 0 changes to 3 files 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -qr 'fromgit()' 0:ff7a2f2d8d70 1:7fe02317c63d 2:ca33a262eb46 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-check-black.t0000644000000000000000000000022414751647721014266 0ustar00#require black run black on all sources; configuration should match development setup $ cd "$TESTDIR"/.. $ $PYTHON -m black --check --quiet . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-check-commit.t0000644000000000000000000000200114751647721014475 0ustar00#require test-repo py3 Load commonly used test logic $ . "$TESTDIR/testutil" Enable obsolescence to avoid the warning issue when obsmarker are found $ . "$TESTDIR/helpers-testrepo.sh" We might be running the tests as a different user than checked out the code $ cat >> $HGRCPATH < [trusted] > users = * > groups = * > EOF Go back in the hg repo $ cd $TESTDIR/.. $ REVSET='not public() and ::. and not desc("# no-check-commit")' $ mkdir "$TESTTMP/p" $ REVS=`testrepohg log -r "$REVSET" -T.` $ if [ -n "$REVS" ] ; then > testrepohg export --git -o "$TESTTMP/p/%n-%h" -r "$REVSET" > for f in `ls "$TESTTMP/p"`; do > contrib/check-commit < "$TESTTMP/p/$f" > "$TESTTMP/check-commit.out" > if [ $? -ne 0 ]; then > node="${f##*-}" > echo "Revision $node does not comply with rules" > echo '------------------------------------------------------' > cat ${TESTTMP}/check-commit.out > echo > fi > done > fi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-check-pyflakes.t0000644000000000000000000000104514751647721015032 0ustar00#require test-repo pyflakes hg10 Load commonly used test logic $ . "$TESTDIR/testutil" $ . "$TESTDIR/helpers-testrepo.sh" run pyflakes on all tracked files ending in .py or without a file ending (skipping binary file random-seed) $ cat > test.py < print(undefinedname) > EOF $ "$PYTHON" -m pyflakes test.py 2>/dev/null test.py:1:* undefined name 'undefinedname' (glob) [1] $ cd "`dirname "$TESTDIR"`" $ testrepohg files -I 'relglob:*.py' -I 'grep("^#!.*python*")' -X tests \ > | xargs $PYTHON -m pyflakes 2>/dev/null ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-check-pylint.t0000644000000000000000000000077314751647721014542 0ustar00#require test-repo pylint hg10 Run pylint for known rules we care about. ----------------------------------------- There should be no recorded failures; fix the codebase before introducing a new check. See the rc file for a list of checks. $ $PYTHON -m pylint --rcfile=$TESTDIR/../pyproject.toml \ > $TESTDIR/../hggit | sed 's/\r$//' Using config file *pyproject.toml (glob) (?) (?) ------------------------------------* (glob) (?) Your code has been rated at 10.00/10* (glob) (?) (?) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-clone.t0000644000000000000000000002061614751647721013246 0ustar00#testcases secret draft Load commonly used test logic $ . "$TESTDIR/testutil" #if secret The phases setting should not affect hg-git $ cat >> $HGRCPATH < [phases] > new-commit = secret > EOF #endif $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag alpha $ git checkout -b beta 2>&1 | sed s/\'/\"/g Switched to a new branch "beta" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -b gamma 2>&1 | sed s/\'/\"/g Switched to a new branch "gamma" $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git checkout -q beta $ cd .. clone a tag $ hg clone -r alpha gitrepo hgrepo-a importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-a bookmarks master 0:ff7a2f2d8d70 $ hg -R hgrepo-a log --graph --template=phases @ changeset: 0:ff7a2f2d8d70 bookmark: master tag: alpha tag: default/master tag: tip phase: draft user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ git --git-dir hgrepo-a/.hg/git for-each-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha Make sure this is still draft since we didn't pull remote's HEAD $ hg -R hgrepo-a phase -r alpha 0: draft clone a branch $ hg clone -r beta gitrepo hgrepo-b importing 2 git commits new changesets ff7a2f2d8d70:7fe02317c63d (2 drafts) updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-b bookmarks * beta 1:7fe02317c63d master 0:ff7a2f2d8d70 $ hg -R hgrepo-b log --graph @ changeset: 1:7fe02317c63d | bookmark: beta | tag: default/beta | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 bookmark: master tag: alpha tag: default/master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ git --git-dir hgrepo-b/.hg/git for-each-ref 9497a4ee62e16ee641860d7677cdb2589ea15554 commit refs/remotes/default/beta 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha Make sure that a deleted .hgsubstate does not confuse hg-git $ cd gitrepo $ echo 'HASH random' > .hgsubstate $ git add .hgsubstate $ fn_git_commit -m 'add bogus .hgsubstate' $ git rm -q .hgsubstate $ fn_git_commit -m 'remove bogus .hgsubstate' $ cd .. $ hg clone -r beta gitrepo hgrepo-c importing 4 git commits new changesets ff7a2f2d8d70:47d12948785d (4 drafts) updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-c bookmarks * beta 3:47d12948785d master 0:ff7a2f2d8d70 $ hg --cwd hgrepo-c status $ git --git-dir hgrepo-c/.hg/git for-each-ref b5329119ed77cb37a31fe523621d684eb55779a4 commit refs/remotes/default/beta 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha test shared repositories $ hg clone gitrepo hgrepo-base importing 5 git commits new changesets ff7a2f2d8d70:47d12948785d (5 drafts) updating to bookmark beta 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-base bookmarks * beta 4:47d12948785d gamma 2:ca33a262eb46 master 0:ff7a2f2d8d70 $ hg --config extensions.share= share hgrepo-base hgrepo-shared updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-shared pull gitrepo pulling from gitrepo no changes found adding bookmark beta adding bookmark gamma adding bookmark master $ hg -R hgrepo-shared push gitrepo pushing to gitrepo searching for changes no changes found [1] $ ls hgrepo-shared/.hg | grep git [1] $ hg -R hgrepo-shared git-cleanup git commit map cleaned $ rm -rf hgrepo-base hgrepo-shared test cloning HEAD $ cd gitrepo $ git checkout -q master $ cd .. $ hg clone gitrepo hgrepo-2 importing 5 git commits new changesets ff7a2f2d8d70:47d12948785d (5 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ git --git-dir hgrepo-2/.hg/git for-each-ref b5329119ed77cb37a31fe523621d684eb55779a4 commit refs/remotes/default/beta d338971a96e20113bb980a5dc4355ba77eed3714 commit refs/remotes/default/gamma 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha $ rm -rf hgrepo-2 clone empty repo $ git init empty Initialized empty Git repository in $TESTTMP/empty/.git/ $ hg clone empty emptyhg updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ rm -rf empty emptyhg test cloning detached HEAD, but pointing to a branch; we detect this and activate the corresponding bookmark $ cd gitrepo $ git checkout -q -d master $ cd .. $ hg clone gitrepo hgrepo-2 importing 5 git commits new changesets ff7a2f2d8d70:47d12948785d (5 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-2 book beta 4:47d12948785d gamma 2:ca33a262eb46 * master 0:ff7a2f2d8d70 $ hg -R hgrepo-2 tags -v tip 4:47d12948785d default/beta 4:47d12948785d git-remote default/gamma 2:ca33a262eb46 git-remote default/master 0:ff7a2f2d8d70 git-remote alpha 0:ff7a2f2d8d70 git $ git --git-dir hgrepo-2/.hg/git for-each-ref b5329119ed77cb37a31fe523621d684eb55779a4 commit refs/remotes/default/beta d338971a96e20113bb980a5dc4355ba77eed3714 commit refs/remotes/default/gamma 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha $ rm -rf hgrepo-2 test cloning fully detached HEAD; we don't convert the anonymous/detached head, so we just issue a warning and don't do anything special $ cd gitrepo $ git checkout -q -d master $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ cd .. $ hg clone gitrepo hgrepo-2 importing 5 git commits new changesets ff7a2f2d8d70:47d12948785d (5 drafts) warning: the git source repository has a detached head (you may want to update to a bookmark) updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-2 book beta 4:47d12948785d gamma 2:ca33a262eb46 master 0:ff7a2f2d8d70 $ hg -R hgrepo-2 id --tags default/beta tip $ git --git-dir hgrepo-2/.hg/git for-each-ref b5329119ed77cb37a31fe523621d684eb55779a4 commit refs/remotes/default/beta d338971a96e20113bb980a5dc4355ba77eed3714 commit refs/remotes/default/gamma 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/alpha $ rm -rf hgrepo-2 test that cloning a regular mercurial repository does not introduce git state $ hg init hgrepo-base $ cd hgrepo-base $ touch flaf $ fn_hg_commit -A -m flaf $ cd .. $ hg clone -U hgrepo-base hgrepo-copy requesting all changes (secret !) $ ls hgrepo-copy/.hg | grep git [1] $ hg clone -U --pull hgrepo-base hgrepo-pull requesting all changes adding changesets (draft !) adding manifests (draft !) adding file changes (draft !) added 1 changesets with 1 changes to 1 files (draft !) new changesets 76c919376257 (draft !) $ ls hgrepo-pull | grep git [1] $ rm -r hgrepo-base hgrepo-copy hgrepo-pull ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-conflict-1.t0000644000000000000000000000402014751647721014074 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r1 2>&1 | sed 's/-C ./-C/' | grep -E -v '^merging afile' | sed 's/incomplete.*/failed!/' warning: conflicts.* (re) 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg *' to abandon (glob) resolve using first parent $ echo C > afile $ hg resolve -m afile | grep -E -v 'no more unresolved files' || true $ hg ci -m "merge to C" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 3:2,1 6c53bc0f062f 1970-01-01 00:00 +0000 test |\ merge to C | | | o 2:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init -q --bare repo.git $ cd hgrepo1 $ hg bookmark -r tip master $ hg push -r master ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 4 commits with 3 trees and 3 blobs adding reference refs/heads/master $ cd .. $ hg clone repo.git hgrepo2 importing 4 git commits new changesets 5d1a6b64f9d0:6c53bc0f062f (4 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 3:1,2 6c53bc0f062f 1970-01-01 00:00 +0000 test |\ merge to C | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-conflict-2.t0000644000000000000000000000402114751647721014076 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r1 2>&1 | sed 's/-C ./-C/' | grep -E -v '^merging afile' | sed 's/incomplete.*/failed!/' warning: conflicts.* (re) 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg *' to abandon (glob) resolve using second parent $ echo B > afile $ hg resolve -m afile | grep -E -v 'no more unresolved files' || true $ hg ci -m "merge to B" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 3:2,1 120385945d08 1970-01-01 00:00 +0000 test |\ merge to B | | | o 2:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init -q --bare repo.git $ cd hgrepo1 $ hg bookmark -r tip master $ hg push -r master ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 4 commits with 3 trees and 3 blobs adding reference refs/heads/master $ cd .. $ hg clone repo.git hgrepo2 importing 4 git commits new changesets 5d1a6b64f9d0:120385945d08 (4 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 3:1,2 120385945d08 1970-01-01 00:00 +0000 test |\ merge to B | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-convergedmerge.t0000644000000000000000000000404414751647721015137 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ echo C > afile $ hg ci -m "B->C" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r2 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg ci -m "merge" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 4:3,2 eaa21d002113 1970-01-01 00:00 +0000 test |\ merge | | | o 3:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 2 0dbe4ac1a758 1970-01-01 00:00 +0000 test | | B->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init -q --bare repo.git $ cd hgrepo1 $ hg bookmark -r4 master $ hg push -r master ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 5 commits with 3 trees and 3 blobs adding reference refs/heads/master $ cd .. $ hg clone repo.git hgrepo2 importing 5 git commits new changesets 5d1a6b64f9d0:eaa21d002113 (5 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 4:1,3 eaa21d002113 1970-01-01 00:00 +0000 test |\ merge | | | o 3 0dbe4ac1a758 1970-01-01 00:00 +0000 test | | B->C | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ hg -R hgrepo2 gverify verifying rev eaa21d002113 against git commit fb8c9e2afe5418cfff337eeed79fad5dd58826f0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-doctest.py0000644000000000000000000000752014751647721013777 0ustar00# this is hack to make sure no escape characters are inserted into the output import doctest import os import re import subprocess import sys # add hggit/ to sys.path sys.path.insert(0, os.path.join(os.environ["TESTDIR"], "..")) if 'TERM' in os.environ: del os.environ['TERM'] class py3docchecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u'' got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b'' # py3: : b'' -> : # : -> : got2 = re.sub( r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3', got2, flags=re.MULTILINE, ) got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, flags=re.MULTILINE) return any( doctest.OutputChecker.check_output(self, w, g, optionflags) for w, g in [(want, got), (want2, got2)] ) def testmod(name, optionflags=0, testtarget=None): __import__(name) mod = sys.modules[name] if testtarget is not None: mod = getattr(mod, testtarget) # minimal copy of doctest.testmod() finder = doctest.DocTestFinder() checker = py3docchecker() runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags) for test in finder.find(mod, name): runner.run(test) runner.summarize() DONT_RUN = [] # Exceptions to the defaults for a given detected module. The value for each # module name is a list of dicts that specify the kwargs to pass to testmod. # testmod is called once per item in the list, so an empty list will cause the # module to not be tested. testmod_arg_overrides = {} fileset = 'set:(**.py)' cwd = os.path.dirname(os.environ["TESTDIR"]) if not os.path.isdir(os.path.join(cwd, ".hg")): sys.exit(0) files = subprocess.check_output( "hg files --print0 \"%s\"" % fileset, shell=True, cwd=cwd, stderr=subprocess.DEVNULL, ).split(b'\0') if sys.version_info[0] >= 3: cwd = os.fsencode(cwd) mods_tested = set() for f in files: if not f: continue with open(os.path.join(cwd, f), "rb") as fh: if not re.search(br'\n\s*>>>', fh.read()): continue f = f.decode() modname = f.replace('.py', '').replace('\\', '.').replace('/', '.') # Third-party modules aren't our responsibility to test, and the modules in # contrib generally do not have doctests in a good state, plus they're hard # to import if this test is running with py2, so we just skip both for now. if modname.startswith('mercurial.thirdparty.') or modname.startswith( 'contrib.' ): continue for kwargs in testmod_arg_overrides.get(modname, [{}]): mods_tested.add((modname, '%r' % (kwargs,))) if modname.startswith('tests.'): # On py2, we can't import from tests.foo, but it works on both py2 # and py3 with the way that PYTHONPATH is setup to import without # the 'tests.' prefix, so we do that. modname = modname[len('tests.') :] testmod(modname, **kwargs) # Meta-test: let's make sure that we actually ran what we expected to, above. # Each item in the set is a 2-tuple of module name and stringified kwargs passed # to testmod. expected_mods_tested = set( [ ('hggit.git_handler', '{}'), ('hggit.util', '{}'), ] ) unexpectedly_run = mods_tested.difference(expected_mods_tested) not_run = expected_mods_tested.difference(mods_tested) if unexpectedly_run: print('Unexpectedly ran (probably need to add to list):') for r in sorted(unexpectedly_run): print(' %r' % (r,)) if not_run: print('Expected to run, but was not run (doctest removed?):') for r in sorted(not_run): print(' %r' % (r,)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-empty-working-tree.t0000644000000000000000000000214514751647721015714 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git commit --allow-empty -m empty >/dev/null 2>/dev/null || echo "git commit error" $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 1 git commits new changesets 01708ca54a8f (1 drafts) updating to bookmark master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log -r tip --template 'files: {files}\n' files: $ hg gverify verifying rev 01708ca54a8f against git commit 678256865a8c85ae925bf834369264193c88f8de $ hg debug-remove-hggit-state clearing out the git cache data $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs adding reference refs/heads/master $ cd .. $ git --git-dir=repo.git log --pretty=medium commit 678256865a8c85ae925bf834369264193c88f8de Author: test Date: Mon Jan 1 00:00:00 2007 +0000 empty ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-encoding.t0000644000000000000000000002312514751647721013732 0ustar00# -*- coding: utf-8 -*- Test fails on Windows, it seems, as the messages aren't latin1-encoded. This may be caused by e.g. environment variables being Unicode on Python 3, or something else. Just running this test on POSIX systems should suffice, for now. (Mercurial generally conforms to the UNIX & Python 2 custom of text being ASCII-like binary data with an optional encoding. As this is generally unsuitable for an internationalised UI, Windows and most other desktop environments enforce a particular encoding. Due to compatibility, Windows gets weird by having _two_ possible encodings: an 8-bit codepage or full UTF-16. Way back, this lead to all sorts of discussions w.r.t. Mercurial, but in this case, we can just skip the test and hope for the best.) #require no-windows Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo utf-8 encoded commit message $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add älphà' Create some commits using latin1 encoding The warning message changed in Git 1.8.0 $ . $TESTDIR/latin-1-encoding Warning: commit message (did|does) not conform to UTF-8. (re) You may want to amend it after fixing the message, or set the config variable i18n.commit[eE]ncoding to the encoding your project uses. (re) Warning: commit message (did|does) not conform to UTF-8. (re) You may want to amend it after fixing the message, or set the config variable i18n.commit[eE]ncoding to the encoding your project uses. (re) $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 4 git commits new changesets 87cd29b67a91:b8a0ac52f339 (4 drafts) updating to bookmark master 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ HGENCODING=utf-8 hg log --graph --debug @ changeset: 3:b8a0ac52f339ccd6d5729508bac4aee6e8b489d8 | bookmark: master | tag: default/master | tag: tip | phase: draft | parent: 2:8bc4d64940260d4a1e70b54c099d3a76c83ff41e | parent: -1:0000000000000000000000000000000000000000 | manifest: 3:ea49f93388380ead5601c8fcbfa187516e7c2ed8 | user: tést èncödîng | date: Mon Jan 01 00:00:13 2007 +0000 | files+: delta | extra: author=$ \x90\x01\x01\xe9\x91\x03\x03\x01\xe8\x91\x08\x02\x01\xf6\x91\x0c\x01\x01\xee\x91\x0f\x15 | extra: branch=default | extra: committer=test 1167609613 0 | extra: encoding=latin-1 | extra: hg-git-rename-source=git | extra: message=\x0c\n\x90\x05\x01\xe9\x91\x07\x02\x01\xe0\x91\x0b\x01 | description: | add d\xc3\xa9lt\xc3\xa0 (esc) | | o changeset: 2:8bc4d64940260d4a1e70b54c099d3a76c83ff41e | phase: draft | parent: 1:f35a3100b78e57a0f5e4589a438f952a14b26ade | parent: -1:0000000000000000000000000000000000000000 | manifest: 2:f580e7da3673c137370da2b931a1dee83590d7b4 | user: t\xc3\xa9st \xc3\xa8nc\xc3\xb6d\xc3\xaeng (esc) | date: Mon Jan 01 00:00:12 2007 +0000 | files+: gamma | extra: branch=default | extra: committer=test 1167609612 0 | extra: hg-git-rename-source=git | description: | add g\xc3\xa4mm\xc3\xa2 (esc) | | o changeset: 1:f35a3100b78e57a0f5e4589a438f952a14b26ade | phase: draft | parent: 0:87cd29b67a9159eec3b5495b0496ef717b2769f5 | parent: -1:0000000000000000000000000000000000000000 | manifest: 1:f0bd6fbafbaebe4bb59c35108428f6fce152431d | user: t\xc3\xa9st \xc3\xa8nc\xc3\xb6d\xc3\xaeng (esc) | date: Mon Jan 01 00:00:11 2007 +0000 | files+: beta | extra: branch=default | extra: committer=test 1167609611 0 | extra: hg-git-rename-source=git | description: | add beta | | o changeset: 0:87cd29b67a9159eec3b5495b0496ef717b2769f5 phase: draft parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default extra: hg-git-rename-source=git description: add \xc3\xa4lph\xc3\xa0 (esc) $ hg debug-remove-hggit-state clearing out the git cache data $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 4 commits with 4 trees and 4 blobs adding reference refs/heads/master $ cd .. $ git --git-dir=repo.git log --pretty=medium commit e85fef6b515d555e45124a5dc39a019cf8db9ff0 Author: t\xe9st \xe8nc\xf6d\xeeng (esc) Date: Mon Jan 1 00:00:13 2007 +0000 add d\xe9lt\xe0 (esc) commit bd576458238cbda49ffcfbafef5242e103f1bc24 Author: * (glob) Date: Mon Jan 1 00:00:12 2007 +0000 add g*mm* (glob) commit 7a7e86fc1b24db03109c9fe5da28b352de59ce90 Author: * (glob) Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 0530b75d8c203e10dc934292a6a4032c6e958a83 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add älphà Stashing binary deltas in other extra keys may wasn't the most forward-looking of choices, as it can lead to weird results if you edit those keys: $ cp -r hgrepo hgrepo-evolve $ cd hgrepo-evolve $ cat >> .hg/hgrc < [experimental] > evolution = all > [extensions] > amend = > rebase = > EOF $ hg pull -u pulling from $TESTTMP/gitrepo importing 1 git commits 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log --debug -r . changeset: 3:b8a0ac52f339ccd6d5729508bac4aee6e8b489d8 bookmark: master tag: default/master tag: tip phase: draft parent: 2:8bc4d64940260d4a1e70b54c099d3a76c83ff41e parent: -1:0000000000000000000000000000000000000000 manifest: 3:ea49f93388380ead5601c8fcbfa187516e7c2ed8 user: t?st ?nc?d?ng date: Mon Jan 01 00:00:13 2007 +0000 files+: delta extra: author=$ \x90\x01\x01\xe9\x91\x03\x03\x01\xe8\x91\x08\x02\x01\xf6\x91\x0c\x01\x01\xee\x91\x0f\x15 extra: branch=default extra: committer=test 1167609613 0 extra: encoding=latin-1 extra: hg-git-rename-source=git extra: message=\x0c\n\x90\x05\x01\xe9\x91\x07\x02\x01\xe0\x91\x0b\x01 description: add d?lt? $ hg amend -u 'simple user ' -m 42 $ hg gexport warning: disregarding possibly invalid metadata in ea036eaa4643 warning: disregarding possibly invalid metadata in ea036eaa4643 $ cd .. create a tag with a latin-1 name -- this is horrible, as tags normally are utf-8, but this allows us to check two things: 1) that tags safely roundtrip regardless of local encoding 2) we can't store such tags on UTF-8 only file systems The first case is handled by always reading the tags from the repository. The second case allows us to check issue #397 on macOS and Linux, i.e. refs we cannot store. That's much easier to run into on Windows, e.g. with double quotes, but we don't have CI coverage for that platform. $ hg clone -U repo.git hgrepo-tags importing 4 git commits new changesets 87cd29b67a91:aabeccdc8b1e (4 drafts) $ cd hgrepo-tags $ hg up tip 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ fn_hg_tag ascii-tag $ "$PYTHON" << EOF > with open('.hgtags', 'a', encoding='utf-8') as f: > f.write('aabeccdc8b1e82054dfce21373bda3b2455900e2 uni-täg\n') > with open('.hgtags', 'a', encoding='latin1') as f: > f.write('aabeccdc8b1e82054dfce21373bda3b2455900e2 lat-täg\n') > EOF $ fn_hg_commit --amend -m 'add loads of tags, some good, some bad' $ cat .hgtags aabeccdc8b1e82054dfce21373bda3b2455900e2 ascii-tag aabeccdc8b1e82054dfce21373bda3b2455900e2 uni-t\xc3\xa4g (esc) aabeccdc8b1e82054dfce21373bda3b2455900e2 lat-t\xe4g (esc) #if unicodefs $ hg push pushing to $TESTTMP/repo.git searching for changes remote: found 0 deltas to reuse remote: error: cannot lock ref 'refs/tags/lat-t\xe4g': * (glob) (esc) adding reference refs/tags/ascii-tag warning: failed to update refs/tags/lat-t\xe4g; failed to update ref (esc) adding reference refs/tags/uni-t\xc3\xa4g (esc) $ HGENCODING=latin-1 hg push pushing to $TESTTMP/repo.git searching for changes remote: found 0 deltas to reuse remote: error: cannot lock ref 'refs/tags/lat-t\xe4g': * (glob) (esc) warning: failed to update refs/tags/lat-t\xe4g; failed to update ref (esc) $ HGENCODING=utf-8 hg push pushing to $TESTTMP/repo.git searching for changes remote: found 0 deltas to reuse remote: error: cannot lock ref 'refs/tags/lat-t\xe4g': * (glob) (esc) warning: failed to update refs/tags/lat-t\xe4g; failed to update ref (esc) #else $ hg push pushing to $TESTTMP/repo.git searching for changes remote: found 0 deltas to reuse adding reference refs/tags/ascii-tag adding reference refs/tags/lat-t\xe4g (esc) adding reference refs/tags/uni-t\xc3\xa4g (esc) $ HGENCODING=latin-1 hg push pushing to $TESTTMP/repo.git searching for changes no changes found (ignoring 1 changesets without bookmarks or tags) [1] $ HGENCODING=utf-8 hg push pushing to $TESTTMP/repo.git searching for changes no changes found (ignoring 1 changesets without bookmarks or tags) [1] #endif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-extra.t0000644000000000000000000001407614751647721013274 0ustar00Test that extra metadata (renames, copies, and other extra metadata) roundtrips across from hg to git $ . "$TESTDIR/testutil" $ git init -q gitrepo $ cd gitrepo $ touch a $ git add a $ fn_git_commit -ma $ git checkout -b not-master 2>&1 | sed s/\'/\"/g Switched to a new branch "not-master" $ cd .. $ hg clone gitrepo hgrepo importing 1 git commits new changesets aa9eb6424386 (1 drafts) updating to bookmark not-master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg mv a b $ fn_hg_commit -mb $ hg up 0 1 files updated, 0 files merged, 1 files removed, 0 files unresolved (leaving bookmark not-master) $ touch c $ hg add c $ fn_hg_commit -mc Rebase will add a rebase_source $ hg --config extensions.rebase= rebase -s 1 -d 2 rebasing 1:4c7da7adf18b * (glob) saved backup bundle to $TESTTMP/*.hg (glob) $ hg up 2 1 files updated, 0 files merged, 1 files removed, 0 files unresolved Add a commit with multiple extra fields $ hg bookmark b1 $ touch d $ hg add d $ fn_hg_commitextra --field zzzzzzz=datazzz --field aaaaaaa=dataaaa $ hg log --graph --template "{rev} {node} {desc|firstline}\n{join(extras, ' ')}\n\n" @ 3 f01651cfcc9337fbd9700d5018ca637a2911ed28 | aaaaaaa=dataaaa branch=default zzzzzzz=datazzz | o 2 03f4cf3c429050e2204fb2bda3a0f93329bdf4fd b | branch=default rebase_source=4c7da7adf18b785726a7421ef0d585bb5762990d | o 1 a735dc0cd7cc0ccdbc16cfa4326b19c707c360f4 c | branch=default | o 0 aa9eb6424386df2b0638fe6f480c3767fdd0e6fd a branch=default hg-git-rename-source=git $ hg push -r b1 pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 3 commits with 3 trees and 0 blobs adding reference refs/heads/b1 $ hg bookmark b2 $ hg mv c c2 $ hg mv d d2 $ fn_hg_commitextra --field yyyyyyy=datayyy --field bbbbbbb=databbb Test some nutty filenames $ hg book b3 #if windows $ hg mv c2 'c2 => c3' abort: filename contains '>', which is reserved on Windows: "c2 => c3" [255] $ hg mv c2 c3 $ fn_hg_commit -m 'dummy commit' $ hg mv c3 c4 $ fn_hg_commit -m 'dummy commit' #else $ hg mv c2 'c2 => c3' warning: filename contains '>', which is reserved on Windows: 'c2 => c3' $ fn_hg_commit -m 'test filename with arrow' $ hg mv 'c2 => c3' 'c3 => c4' warning: filename contains '>', which is reserved on Windows: 'c3 => c4' $ fn_hg_commit -m 'test filename with arrow 2' $ hg log --graph --template "{rev} {node} {desc|firstline}\n{join(extras, ' ')}\n\n" -l 3 --config "experimental.graphstyle.missing=|" @ 6 bca4ba69a6844c133b069e227dfa043d41e3c197 test filename with arrow 2 | branch=default | o 5 864caad1f3493032f8d06f44a89dc9f1c039b09f test filename with arrow | branch=default | o 4 58f855ae26f4930ce857e648d3dd949901cce817 | bbbbbbb=databbb branch=default yyyyyyy=datayyy | #endif $ hg push -r b2 -r b3 pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 3 commits with 3 trees and 0 blobs adding reference refs/heads/b2 adding reference refs/heads/b3 $ cd ../gitrepo $ git cat-file commit b1 tree 1b773a2eb70f29397356f8069c285394835ff85a parent 54776dace5849bdf273fb26737a48ef64804909d author test 1167609613 +0000 committer test 1167609613 +0000 HG:extra aaaaaaa:dataaaa HG:extra zzzzzzz:datazzz $ git cat-file commit b2 tree 34ad62c6d6ad9464bfe62db5b3d2fa16aaa9fa9e parent 15beadd92324c9b88060a4ec4ffb350f988d7075 author test 1167609614 +0000 committer test 1167609614 +0000 HG:rename c:c2 HG:rename d:d2 HG:extra bbbbbbb:databbb HG:extra yyyyyyy:datayyy #if no-windows $ git cat-file commit b3 tree e63df52695f9b06e54b37e7ef60d0c43994de620 parent 5cafe2555a0666fcf661a3943277a9812a694a98 author test 1167609616 +0000 committer test 1167609616 +0000 HG:rename c2%20%3D%3E%20c3:c3%20%3D%3E%20c4 test filename with arrow 2 #endif $ cd ../gitrepo $ git checkout b1 Switched to branch 'b1' $ commit_sha=$(git rev-parse HEAD) $ tree_sha=$(git rev-parse HEAD^{tree}) There's no way to create a Git repo with extra metadata via the CLI. Dulwich lets you do that, though. >>> from dulwich.objects import Commit >>> from dulwich.porcelain import open_repo >>> repo = open_repo('.') >>> c = Commit() >>> c.author = b'test ' >>> c.author_time = 0 >>> c.author_timezone = 0 >>> c.committer = c.author >>> c.commit_time = 0 >>> c.commit_timezone = 0 >>> c.parents = [b'$commit_sha'] >>> c.tree = b'$tree_sha' >>> c.message = b'extra commit\n' >>> c.extra.extend([(b'zzz:zzz', b'data:zzz'), (b'aaa:aaa', b'data:aaa'), ... (b'HG:extra', b'hgaaa:dataaaa'), ... (b'HG:extra', b'hgzzz:datazzz')]) >>> repo.object_store.add_object(c) >>> repo.refs.set_if_equals(b'refs/heads/master', None, c.id) True $ git cat-file commit master tree 1b773a2eb70f29397356f8069c285394835ff85a parent 15beadd92324c9b88060a4ec4ffb350f988d7075 author test 0 +0000 committer test 0 +0000 zzz:zzz data:zzz aaa:aaa data:aaa HG:extra hgaaa:dataaaa HG:extra hgzzz:datazzz extra commit $ cd .. $ hg clone -qU gitrepo hgrepo2 $ cd hgrepo2 $ hg log -G -r :5 -T "{rev} {node} {desc|firstline}\n{join(extras, ' ')}\n\n" o 5 58f855ae26f4930ce857e648d3dd949901cce817 | bbbbbbb=databbb branch=default yyyyyyy=datayyy | | o 4 90acc8c23fcfaeb0930c03c849923a696fd9013c extra commit |/ GIT0-zzz%3Azzz=data%3Azzz GIT1-aaa%3Aaaa=data%3Aaaa branch=default hgaaa=dataaaa hgzzz=datazzz | o 3 f01651cfcc9337fbd9700d5018ca637a2911ed28 | aaaaaaa=dataaaa branch=default zzzzzzz=datazzz | o 2 03f4cf3c429050e2204fb2bda3a0f93329bdf4fd b | branch=default rebase_source=4c7da7adf18b785726a7421ef0d585bb5762990d | o 1 a735dc0cd7cc0ccdbc16cfa4326b19c707c360f4 c | branch=default | o 0 aa9eb6424386df2b0638fe6f480c3767fdd0e6fd a branch=default hg-git-rename-source=git ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-file-removal.t0000644000000000000000000001742714751647721014536 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ mkdir foo $ echo blah > foo/bar $ git add foo $ fn_git_commit -m 'add foo' $ git rm alpha rm 'alpha' $ fn_git_commit -m 'remove alpha' $ git rm foo/bar rm 'foo/bar' $ fn_git_commit -m 'remove foo/bar' $ ln -s beta betalink $ git add betalink $ fn_git_commit -m 'add symlink to beta' replace symlink with file $ rm betalink $ echo betalink > betalink $ git add betalink $ fn_git_commit -m 'replace symlink with file' replace file with symlink $ rm betalink $ ln -s beta betalink $ git add betalink $ fn_git_commit -m 'replace file with symlink' $ git rm betalink rm 'betalink' $ fn_git_commit -m 'remove betalink' final manifest in git is just beta $ git ls-files beta $ git log --pretty=medium commit 5ee11eeae239d6a99df5a99901ec00ffafbcc46b Author: test Date: Mon Jan 1 00:00:18 2007 +0000 remove betalink commit 2c7b324faeccb1acf89c35b7ad38e7956f5705fa Author: test Date: Mon Jan 1 00:00:17 2007 +0000 replace file with symlink commit ff0478d2ecc2571d01eb6d406ac29e4e63e5d3d5 Author: test Date: Mon Jan 1 00:00:16 2007 +0000 replace symlink with file commit 5492e6e410e42df527956be945286cd1ae45acb8 Author: test Date: Mon Jan 1 00:00:15 2007 +0000 add symlink to beta commit b991de8952c482a7cd51162674ffff8474862218 Author: test Date: Mon Jan 1 00:00:14 2007 +0000 remove foo/bar commit b0edaf0adac19392cf2867498b983bc5192b41dd Author: test Date: Mon Jan 1 00:00:13 2007 +0000 remove alpha commit f2d0d5bfa905e12dee728b509b96cf265bb6ee43 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add foo commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 9 git commits new changesets ff7a2f2d8d70:0995b8a0a943 (9 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph @ changeset: 8:0995b8a0a943 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:18 2007 +0000 | summary: remove betalink | o changeset: 7:a316d3a96c89 | user: test | date: Mon Jan 01 00:00:17 2007 +0000 | summary: replace file with symlink | o changeset: 6:1804acb71f3e | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: replace symlink with file | o changeset: 5:e19c85becc87 | user: test | date: Mon Jan 01 00:00:15 2007 +0000 | summary: add symlink to beta | o changeset: 4:0d3086c3f8c3 | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: remove foo/bar | o changeset: 3:b2406125ef5c | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: remove alpha | o changeset: 2:8b3b2f4b4158 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add foo | o changeset: 1:7fe02317c63d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha make sure alpha is not in this manifest $ hg manifest -r 3 beta foo/bar make sure that only beta is in the manifest $ hg manifest beta $ hg debug-remove-hggit-state clearing out the git cache data $ ls .hg | grep git [1] $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 9 commits with 8 trees and 5 blobs adding reference refs/heads/master $ cd .. $ git --git-dir=repo.git log --pretty=medium commit 5ee11eeae239d6a99df5a99901ec00ffafbcc46b Author: test Date: Mon Jan 1 00:00:18 2007 +0000 remove betalink commit 2c7b324faeccb1acf89c35b7ad38e7956f5705fa Author: test Date: Mon Jan 1 00:00:17 2007 +0000 replace file with symlink commit ff0478d2ecc2571d01eb6d406ac29e4e63e5d3d5 Author: test Date: Mon Jan 1 00:00:16 2007 +0000 replace symlink with file commit 5492e6e410e42df527956be945286cd1ae45acb8 Author: test Date: Mon Jan 1 00:00:15 2007 +0000 add symlink to beta commit b991de8952c482a7cd51162674ffff8474862218 Author: test Date: Mon Jan 1 00:00:14 2007 +0000 remove foo/bar commit b0edaf0adac19392cf2867498b983bc5192b41dd Author: test Date: Mon Jan 1 00:00:13 2007 +0000 remove alpha commit f2d0d5bfa905e12dee728b509b96cf265bb6ee43 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add foo commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha test with rename detection enabled $ hg --config git.similarity=100 clone gitrepo hgreporenames importing 9 git commits new changesets ff7a2f2d8d70:0995b8a0a943 (9 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgreporenames $ hg log --graph @ changeset: 8:0995b8a0a943 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:18 2007 +0000 | summary: remove betalink | o changeset: 7:a316d3a96c89 | user: test | date: Mon Jan 01 00:00:17 2007 +0000 | summary: replace file with symlink | o changeset: 6:1804acb71f3e | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: replace symlink with file | o changeset: 5:e19c85becc87 | user: test | date: Mon Jan 01 00:00:15 2007 +0000 | summary: add symlink to beta | o changeset: 4:0d3086c3f8c3 | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: remove foo/bar | o changeset: 3:b2406125ef5c | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: remove alpha | o changeset: 2:8b3b2f4b4158 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add foo | o changeset: 1:7fe02317c63d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-gc.t0000644000000000000000000001064314751647721012536 0ustar00Garbage collection ================== This test excercises our internal transparent packing of loose objects. Load commonly used test logic $ . "$TESTDIR/testutil" Create a git repository with 100 commits, that touches 10 different files. We also have 10 tags. $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ for i in $(seq 10) > do > for f in $(seq 10) > do > n=$(expr $i \* $f) > echo $n > $f > git add $f > fn_git_commit -m $n > done > fn_git_tag -m $i v$i > done $ cd .. $ hg clone -U gitrepo hgrepo importing 100 git commits new changesets 1c8407413fa3:eda59117ba04 (100 drafts) $ cd hgrepo $ hg debug-remove-hggit-state clearing out the git cache data ----------- Test garbage collection of loose objects into packs. We first test this with two threads, which is closest to the expected usage scenario, as almost all computers have at least two cores these days. The main downside is that this makes the output order unreliable, so we just sort it. $ hg gexport --config hggit.mapsavefrequency=33 --config hggit.threads=2 --debug | grep pack | sort packed 3 loose objects! packed 75 loose objects! packed 78 loose objects! packed 86 loose objects! packing 3 loose objects... packing 75 loose objects... packing 78 loose objects... packing 86 loose objects... $ hg debug-remove-hggit-state clearing out the git cache data Test the actual order of operations -- this uses a single thread, which means that the packing happens synchronously in the main thread, giving us a reliable output order. In addition, the transaction size is set up such that we happen to do nothing in the final, synchronous packing that happens on every pull. Lots of other tests have a map save frequency higher than the total amount of commits pulled, but let's just trigger that other odd occurence here. $ hg gexport --debug \ > --config hggit.mapsavefrequency=10 --config hggit.threads=1 | \ > sed 's/^converting revision.*/./' finding unexported changesets exporting 100 changesets . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 30 loose objects... packed 30 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 25 loose objects... packed 25 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 25 loose objects... packed 25 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 24 loose objects... packed 24 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 24 loose objects... packed 24 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 24 loose objects... packed 24 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 24 loose objects... packed 24 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 23 loose objects... packed 23 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 22 loose objects... packed 22 loose objects! . . . . . . . . . . saving git map to $TESTTMP/hgrepo/.hg/git-mapfile packing 21 loose objects... packed 21 loose objects! packing 0 loose objects... packed 0 loose objects! saving git map to $TESTTMP/hgrepo/.hg/git-mapfile $ find .hg/git/objects -type f | grep -Fv .idx | sort .hg/git/objects/pack/pack-33903607b479000b976a29a349fe0f4dffb0aaac.pack .hg/git/objects/pack/pack-40d9440e392d9eab62fa38a2ed66cc763d77aca3.pack .hg/git/objects/pack/pack-4ab2dac268f94e407788d52d6ba087b626c41651.pack .hg/git/objects/pack/pack-543e3b37bd36218a4dc6611a96d7c218afb78429.pack .hg/git/objects/pack/pack-5fc80292253ee10d1b86b5c4d9c51b29d2b4ba47.pack .hg/git/objects/pack/pack-9c636f5f16302fc5fadf0cc4ed42aeb67fc51f6a.pack .hg/git/objects/pack/pack-ae74b1f0197dfb45cfb13889453860a40103969a.pack .hg/git/objects/pack/pack-b432e2f477cb765fc0aeaa850d56e04b10392e6c.pack .hg/git/objects/pack/pack-cf7023660ce10ede2896d1be117f6ba93a261ff9.pack .hg/git/objects/pack/pack-e601b2af6a91a9cf6817d71f4eb660d2218d4094.pack ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-git-clone.t0000644000000000000000000000367014751647721014030 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ hg clone gitrepo hgrepo importing 2 git commits new changesets ff7a2f2d8d70:7fe02317c63d (2 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo log --graph @ changeset: 1:7fe02317c63d | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha we should have some bookmarks $ hg -R hgrepo book * master 1:7fe02317c63d $ hg -R hgrepo gverify verifying rev 7fe02317c63d against git commit 9497a4ee62e16ee641860d7677cdb2589ea15554 test for ssh vulnerability $ cat >> $HGRCPATH << EOF > [ui] > ssh = ssh -o ConnectTimeout=1 > EOF $ hg clone -q 'git+ssh://-oProxyCommand=rm${IFS}nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm${IFS}nonexistent' [255] $ hg clone -q 'git+ssh://%2DoProxyCommand=rm${IFS}nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm${IFS}nonexistent' [255] $ hg clone -q 'git+ssh://fakehost|rm${IFS}nonexistent/path' ssh: * fakehost%7?rm%24%7????%7?nonexistent* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] $ hg clone -q 'git+ssh://fakehost%7Crm${IFS}nonexistent/path' ssh: * fakehost%7?rm%24%7????%7?nonexistent* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-git-gpg.t0000644000000000000000000000347714751647721013512 0ustar00#require gpg Load commonly used test logic $ . "$TESTDIR/testutil" $ export GNUPGHOME="$(mktemp -d)" $ cp -R "$TESTDIR"/gpg/* "$GNUPGHOME" Start gpg-agent, which is required by GnuPG v2 #if gpg21 $ gpg-connect-agent -q --subst /serverpid '/echo ${get serverpid}' /bye \ > >> $DAEMON_PIDS #endif and migrate secret keys #if gpg2 $ gpg --no-permission-warning --no-secmem-warning --list-secret-keys \ > > /dev/null 2>&1 #endif $ alias gpg='gpg --no-permission-warning --no-secmem-warning --no-auto-check-trustdb' Set up two identical git repos. $ mkdir gitrepo $ cd gitrepo $ git init Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ touch a $ git add a $ git commit -m "initial commit" [master (root-commit) *] initial commit (glob) 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 a $ cd .. $ git clone gitrepo gitrepo2 Cloning into 'gitrepo2'... done. Add a signed commit to the first clone. $ cd gitrepo $ git checkout -b signed Switched to a new branch 'signed' $ touch b $ git add b $ git commit -m "message" -Shgtest [signed *] message (glob) 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 b $ cd .. Hg clone it $ hg clone gitrepo hgrepo importing 2 git commits new changesets ab60c5e55bd6:[0-9a-f]{12,12} \(2 drafts\) (re) updating to bookmark signed 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg push ../gitrepo2 -B signed pushing to ../gitrepo2 searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs adding reference refs/heads/signed $ cd .. Verify the commit $ cd gitrepo2 $ git show --show-signature signed | grep "Good signature from" gpg: Good signature from "hgtest" [ultimate] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-git-submodules.t0000644000000000000000000003523414751647721015113 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo1 Initialized empty Git repository in $TESTTMP/gitrepo1/.git/ $ cd gitrepo1 $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ cd .. $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ mkdir gitrepo2 $ cd gitrepo2 $ rmpwd="import sys; print(sys.stdin.read().replace('$(dirname $(pwd))/', ''))" different versions of git spell the dir differently. Older versions use the full path to the directory all the time, whereas newer version spell it sanely as it was given (eg . in a newer version, while older git will use the full normalized path for .) $ clonefilt='s/Cloning into/Initialized empty Git repository in/;s/in .*/in .../' $ git clone ../gitrepo1 . 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ git submodule add ../gitsubrepo subrepo 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ git commit -m 'add subrepo' | sed 's/, 0 deletions(-)//' [master e42b08b] add subrepo 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 subrepo $ cd subrepo $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ cd .. $ git add subrepo $ git commit -m 'change subrepo commit' [master a000567] change subrepo commit 1 file changed, 1 insertion(+), 1 deletion(-) $ git submodule add ../gitsubrepo subrepo2 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ git commit -m 'add another subrepo' | sed 's/, 0 deletions(-)//' [master 6e21952] add another subrepo 2 files changed, 4 insertions(+) create mode 160000 subrepo2 remove one subrepo, replace with file $ git rm --cached subrepo rm 'subrepo' we'd ordinarily use sed here, but BSD sed doesn't support two-address formats like +2 -- so use grep with the stuff we want to keep $ grep 'submodule "subrepo2"' -A2 .gitmodules > .gitmodules-new $ mv .gitmodules-new .gitmodules $ git add .gitmodules $ git config --unset submodule.subrepo.url $ rm -rf subrepo $ echo subrepo > subrepo $ git add subrepo $ git commit -m 'replace subrepo with file' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master f6436a4] replace subrepo with file 2 files changed, 1 insertion(+), 4 deletions(-) mode change 160000 => 100644 subrepo replace file with subrepo -- apparently, git complains about the subrepo if the same name has existed at any point historically, so use alpha instead of subrepo $ git rm alpha rm 'alpha' $ git submodule add ../gitsubrepo alpha 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ git commit -m 'replace file with subrepo' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master 8817116] replace file with subrepo 2 files changed, 4 insertions(+), 1 deletion(-) mode change 100644 => 160000 alpha $ ln -s foo foolink $ git add foolink $ git commit -m 'add symlink' [master 2d1c135] add symlink 1 file changed, 1 insertion(+) create mode 120000 foolink replace symlink with subrepo $ git rm foolink rm 'foolink' $ git submodule add ../gitsubrepo foolink 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ git commit -m 'replace symlink with subrepo' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master e3288fa] replace symlink with subrepo 2 files changed, 4 insertions(+), 1 deletion(-) mode change 120000 => 160000 foolink replace subrepo with symlink $ cat > .gitmodules < [submodule "subrepo2"] > path = subrepo2 > url = ../gitsubrepo > [submodule "alpha"] > path = alpha > url = ../gitsubrepo > EOF $ git add .gitmodules $ git rm --cached foolink rm 'foolink' $ rm -rf foolink $ ln -s foo foolink $ git add foolink $ git commit -m 'replace subrepo with symlink' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master d283640] replace subrepo with symlink 2 files changed, 1 insertion(+), 4 deletions(-) mode change 160000 => 120000 foolink $ git show commit d28364013fe1a0fde56c0e1921e49ecdeee8571d Author: test Date: Mon Jan 1 00:00:12 2007 +0000 replace subrepo with symlink diff --git a/.gitmodules b/.gitmodules index b511494..813e20b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "alpha"] path = alpha url = ../gitsubrepo -[submodule "foolink"] - path = foolink - url = ../gitsubrepo diff --git a/foolink b/foolink deleted file mode 160000 index 6e4ad8d..0000000 --- a/foolink +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e4ad8da50204560c00fa25e4987eb2e239029ba diff --git a/foolink b/foolink new file mode 120000 index 0000000..1910281 --- /dev/null +++ b/foolink @@ -0,0 +1 @@ +foo \ No newline at end of file $ git rm --cached subrepo2 rm 'subrepo2' $ git rm --cached alpha rm 'alpha' $ git rm .gitmodules rm '.gitmodules' $ git commit -m 'remove all subrepos' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master 15ba949] remove all subrepos 3 files changed, 8 deletions(-) delete mode 100644 .gitmodules delete mode 160000 alpha delete mode 160000 subrepo2 $ git log --pretty=oneline 15ba94929481c654814178aac1dbca06ae688718 remove all subrepos d28364013fe1a0fde56c0e1921e49ecdeee8571d replace subrepo with symlink e3288fa737d429a60637b3b6782cb25b8298bc00 replace symlink with subrepo 2d1c135447d11df4dfe96dd5d4f37926dc5c821d add symlink 88171163bf4795b5570924e51d5f8ede33f8bc28 replace file with subrepo f6436a472da00f581d8d257e9bbaf3c358a5e88c replace subrepo with file 6e219527869fa40eb6ffbdd013cd86d576b26b01 add another subrepo a000567ceefbd9a2ce364e0dea6e298010b02b6d change subrepo commit e42b08b3cb7069b4594a4ee1d9cb641ee47b2355 add subrepo 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 add alpha $ cd .. $ hg clone gitrepo2 hgrepo importing 10 git commits new changesets ff7a2f2d8d70:0ad944b2c4d8 (10 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph @ changeset: 9:0ad944b2c4d8 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: remove all subrepos | o changeset: 8:33da452ef22f | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace subrepo with symlink | o changeset: 7:acebec53c0fc | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace symlink with subrepo | o changeset: 6:78c2ea52db4b | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add symlink | o changeset: 5:c0d52ffc59b8 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace file with subrepo | o changeset: 4:73e078a178a0 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace subrepo with file | o changeset: 3:29e236ba4c06 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add another subrepo | o changeset: 2:a6075a162f62 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: change subrepo commit | o changeset: 1:a4036e758995 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add subrepo | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ hg book * master 9:0ad944b2c4d8 (add subrepo) $ hg cat -r 1 .hgsubstate 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo $ hg cat -r 1 .hgsub subrepo = [git]../gitsubrepo $ hg gverify -r 1 verifying rev a4036e758995 against git commit e42b08b3cb7069b4594a4ee1d9cb641ee47b2355 (change subrepo commit) $ hg cat -r 2 .hgsubstate aa2ead20c29b5cc6256408e1d9ef704870033afb subrepo $ hg cat -r 2 .hgsub subrepo = [git]../gitsubrepo $ hg gverify -r 2 verifying rev a6075a162f62 against git commit a000567ceefbd9a2ce364e0dea6e298010b02b6d (add another subrepo) $ hg cat -r 3 .hgsubstate aa2ead20c29b5cc6256408e1d9ef704870033afb subrepo 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo2 $ hg cat -r 3 .hgsub subrepo = [git]../gitsubrepo subrepo2 = [git]../gitsubrepo $ hg gverify -r 3 verifying rev 29e236ba4c06 against git commit 6e219527869fa40eb6ffbdd013cd86d576b26b01 (replace subrepo with file) $ hg cat -r 4 .hgsubstate 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo2 $ hg cat -r 4 .hgsub subrepo2 = [git]../gitsubrepo $ hg manifest -r 4 .hgsub .hgsubstate alpha subrepo $ hg gverify -r 4 verifying rev 73e078a178a0 against git commit f6436a472da00f581d8d257e9bbaf3c358a5e88c (replace file with subrepo) $ hg cat -r 5 .hgsubstate 6e4ad8da50204560c00fa25e4987eb2e239029ba alpha 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo2 $ hg cat -r 5 .hgsub subrepo2 = [git]../gitsubrepo alpha = [git]../gitsubrepo $ hg manifest -r 5 .hgsub .hgsubstate subrepo $ hg gverify -r 5 verifying rev c0d52ffc59b8 against git commit 88171163bf4795b5570924e51d5f8ede33f8bc28 (replace symlink with subrepo) $ hg cat -r 7 .hgsub subrepo2 = [git]../gitsubrepo alpha = [git]../gitsubrepo foolink = [git]../gitsubrepo $ hg cat -r 7 .hgsubstate 6e4ad8da50204560c00fa25e4987eb2e239029ba alpha 6e4ad8da50204560c00fa25e4987eb2e239029ba foolink 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo2 $ hg gverify -r 7 verifying rev acebec53c0fc against git commit e3288fa737d429a60637b3b6782cb25b8298bc00 (replace subrepo with symlink) $ hg cat -r 8 .hgsub .hgsubstate subrepo2 = [git]../gitsubrepo alpha = [git]../gitsubrepo 6e4ad8da50204560c00fa25e4987eb2e239029ba alpha 6e4ad8da50204560c00fa25e4987eb2e239029ba subrepo2 $ hg gverify -r 8 verifying rev 33da452ef22f against git commit d28364013fe1a0fde56c0e1921e49ecdeee8571d (remove all subrepos) $ hg cat -r 9 .hgsub .hgsubstate .hgsub: no such file in rev 0ad944b2c4d8 .hgsubstate: no such file in rev 0ad944b2c4d8 [1] $ hg gverify -r 9 verifying rev 0ad944b2c4d8 against git commit 15ba94929481c654814178aac1dbca06ae688718 $ hg debug-remove-hggit-state clearing out the git cache data $ hg gexport $ cd .hg/git $ git log --pretty=oneline 73c15b74fb81fa0cc60e9c59c73787a9f26c778b remove all subrepos d28364013fe1a0fde56c0e1921e49ecdeee8571d replace subrepo with symlink e3288fa737d429a60637b3b6782cb25b8298bc00 replace symlink with subrepo 2d1c135447d11df4dfe96dd5d4f37926dc5c821d add symlink 88171163bf4795b5570924e51d5f8ede33f8bc28 replace file with subrepo f6436a472da00f581d8d257e9bbaf3c358a5e88c replace subrepo with file 6e219527869fa40eb6ffbdd013cd86d576b26b01 add another subrepo a000567ceefbd9a2ce364e0dea6e298010b02b6d change subrepo commit e42b08b3cb7069b4594a4ee1d9cb641ee47b2355 add subrepo 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 add alpha test with rename detection enabled -- simply checking that the Mercurial hashes are the same is enough $ cd ../../.. $ hg --config git.similarity=100 clone gitrepo2 hgreporenames importing 10 git commits new changesets ff7a2f2d8d70:0ad944b2c4d8 (10 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgreporenames $ hg log --graph @ changeset: 9:0ad944b2c4d8 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: remove all subrepos | o changeset: 8:33da452ef22f | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace subrepo with symlink | o changeset: 7:acebec53c0fc | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace symlink with subrepo | o changeset: 6:78c2ea52db4b | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add symlink | o changeset: 5:c0d52ffc59b8 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace file with subrepo | o changeset: 4:73e078a178a0 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: replace subrepo with file | o changeset: 3:29e236ba4c06 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add another subrepo | o changeset: 2:a6075a162f62 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: change subrepo commit | o changeset: 1:a4036e758995 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add subrepo | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. test handling of an invalid .gitmodules file (#380) $ git init --quiet gitrepo-issue380 $ cd gitrepo-issue380 $ git submodule add ../gitsubrepo Cloning into '$TESTTMP/gitrepo-issue380/gitsubrepo'... done. $ fn_git_commit -m 'add a submodule' $ cat >> .gitmodules < <<<<<<< HEAD > EOF $ fn_git_commit -a -m 'b0rken .gitmodules' $ git status fatal: bad config line 4 in file $TESTTMP/gitrepo-issue380/.gitmodules [128] $ sed -i.orig /HEAD/d .gitmodules $ fn_git_commit -a -m 'fix .gitmodules' $ git status On branch master Untracked files: (use "git add ..." to include in what will be committed) .gitmodules.orig nothing added to commit but untracked files present (use "git add" to track) $ cd .. $ git clone gitrepo-issue380 gitrepo-issue380~ Cloning into 'gitrepo-issue380~'... done. $ hg clone -U gitrepo-issue380 hgrepo-issue380 importing 3 git commits warning: failed to parse .gitmodules in 2e4ec4293822 new changesets ed60e5fbc192:9dfc0cdf1787 (3 drafts) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-git-tags.t0000644000000000000000000002452514751647721013670 0ustar00#testcases secret draft Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [templates] > shorttags = '{rev}:{node|short} {phase} {tags}{if(obsolete, " X")}\n' > EOF #if secret The phases setting should not affect hg-git $ cat >> $HGRCPATH < [phases] > new-commit = secret > EOF #endif Create a bare upstream repository $ git init --bare repo.git Initialized empty Git repository in $TESTTMP/repo.git/ Create a couple of commits from Git $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ fn_git_tag -a -m 'added tag beta' beta $ git remote add origin $TESTTMP/repo.git $ git push --quiet --tags --set-upstream origin master Branch 'master' set up to track remote branch 'master' from 'origin'. (?) $ cd .. Clone it: $ hg clone repo.git hgrepo importing 2 git commits new changesets ff7a2f2d8d70:7fe02317c63d (2 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo Verify that annotated tags are unaffected by reexports: $ GIT_DIR=.hg/git git tag -ln beta added tag beta $ hg gexport $ GIT_DIR=.hg/git git tag -ln beta added tag beta Error checking on tag creation $ hg tag --git beta --remove abort: cannot remove git tags (the git documentation heavily discourages editing tags) [255] $ hg tag --git beta -r null abort: cannot remove git tags (the git documentation heavily discourages editing tags) [255] $ hg tag --git beta --remove -r 0 abort: cannot specify both --rev and --remove [10] $ hg tag --git alpha abort: git tags require an explicit revision (please specify -r/--rev) [255] $ hg tag --git alpha alpha -r 0 abort: tag names must be unique [255] $ hg tag --git alpha -r 0 -e abort: cannot specify both --git and --edit [10] $ hg tag --git alpha -r 0 -m 42 abort: cannot specify both --git and --message [10] $ hg tag --git alpha -r 0 -d 42 abort: cannot specify both --git and --date [10] $ hg tag --git alpha -r 0 -u user@example.com abort: cannot specify both --git and --user [10] $ hg tag --git 'with space' -r 0 abort: the name 'with space' is not a valid git tag [255] $ hg tag --git ' beta' -r 0 abort: the name 'beta' already exists [255] $ hg tag --git master -r 0 abort: the name 'master' already exists [255] $ hg tag --git tip -r 0 abort: the name 'tip' is reserved [10] Create a git tag from hg $ hg tag --git alpha --debug -r 0 finding unexported changesets saving git map to $TESTTMP/hgrepo/.hg/git-mapfile adding git tag alpha $ hg log --graph @ changeset: 1:7fe02317c63d | bookmark: master | tag: beta | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 tag: alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ echo beta-fix >> beta $ fn_hg_commit -m 'fix for beta' #if secret $ hg phase -d #endif $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master adding reference refs/tags/alpha Verify that amending commits known to remotes doesn't break anything $ cat >> $HGRCPATH << EOF > [experimental] > evolution = createmarkers > evolution.createmarkers = yes > EOF $ hg tags tip 2:61175962e488 default/master 2:61175962e488 beta 1:7fe02317c63d alpha 0:ff7a2f2d8d70 $ echo beta-fix-again >> beta $ fn_hg_commit --amend $ hg log -T shorttags 3:3094b9e8da41 draft tip 2:61175962e488 draft default/master X 1:7fe02317c63d draft beta 0:ff7a2f2d8d70 draft alpha $ hg tags tip 3:3094b9e8da41 default/master 2:61175962e488 beta 1:7fe02317c63d alpha 0:ff7a2f2d8d70 $ hg push pushing to $TESTTMP/repo.git searching for changes abort: pushing refs/heads/master overwrites 3094b9e8da41 [255] $ hg push -f pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master Now create a tag for the old, obsolete master $ cd ../repo.git $ git tag detached $(hg log -R ../hgrepo --hidden -r 2 -T '{gitnode}\n') $ git tag alpha beta detached $ cd ../hgrepo $ hg pull pulling from $TESTTMP/repo.git no changes found $ hg log -T shorttags 3:3094b9e8da41 draft default/master tip 2:61175962e488 draft detached X 1:7fe02317c63d draft beta 0:ff7a2f2d8d70 draft alpha $ hg tags tip 3:3094b9e8da41 default/master 3:3094b9e8da41 detached 2:61175962e488 beta 1:7fe02317c63d alpha 0:ff7a2f2d8d70 $ hg push pushing to $TESTTMP/repo.git searching for changes no changes found [1] $ cd .. Verify that revsets can point out git tags; for that we need an untagged commit. $ cd hgrepo $ touch gamma $ fn_hg_commit -A -m 'add gamma' #if secret $ hg phase -d #endif $ hg log -T shorttags -r 'gittag()' 0:ff7a2f2d8d70 draft alpha 1:7fe02317c63d draft beta 2:61175962e488 draft detached X $ hg log -T shorttags -r 'gittag(detached)' 2:61175962e488 draft detached X $ hg log -T shorttags -r 'gittag("re:a$")' 0:ff7a2f2d8d70 draft alpha 1:7fe02317c63d draft beta Create a git tag from hg, but pointing to a new commit: $ hg tag --git gamma --debug -r tip invalid branch cache (visible): tip differs (?) finding unexported changesets exporting 1 changesets converting revision 0eb1ab0073a885a498d4ae3dc5cf0c26e750fa3d packing 3 loose objects... packed 3 loose objects! saving git map to $TESTTMP/hgrepo/.hg/git-mapfile adding git tag gamma $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master adding reference refs/tags/gamma $ cd ../gitrepo $ git fetch --quiet --tags $ git tag alpha beta detached gamma $ cd .. Try to overwrite an annotated tag: $ cd hgrepo $ hg tags -v tip 4:0eb1ab0073a8 gamma 4:0eb1ab0073a8 git default/master 4:0eb1ab0073a8 git-remote detached 2:61175962e488 git beta 1:7fe02317c63d git alpha 0:ff7a2f2d8d70 git $ hg book not-master $ hg tag beta abort: tag 'beta' already exists (use -f to force) [10] $ hg tag -f beta #if secret $ hg phase -d #endif $ hg push pushing to $TESTTMP/repo.git warning: not overwriting annotated tag 'beta' searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/not-master $ hg tags tip 5:c49682c7cba4 default/not-master 5:c49682c7cba4 gamma 4:0eb1ab0073a8 default/master 4:0eb1ab0073a8 beta 4:0eb1ab0073a8 detached 2:61175962e488 alpha 0:ff7a2f2d8d70 $ cd .. Check whether `gimport` handles tags $ cd hgrepo $ rm .hg/git-tags .hg/git-mapfile $ hg gimport importing 6 git commits $ hg tags -q tip default/not-master gamma default/master beta detached alpha $ cd .. Test how pulling an explicit branch with an annotated tag: $ hg clone -r master repo.git hgrepo-2 importing 4 git commits new changesets ff7a2f2d8d70:0eb1ab0073a8 (4 drafts) updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -r 'ancestors(master) and tagged()' -T shorttags -R hgrepo-2 0:ff7a2f2d8d70 draft alpha 1:7fe02317c63d draft beta 3:0eb1ab0073a8 draft default/master gamma tip $ hg tags -v -R hgrepo-2 tip 3:0eb1ab0073a8 gamma 3:0eb1ab0073a8 git default/master 3:0eb1ab0073a8 git-remote beta 1:7fe02317c63d git alpha 0:ff7a2f2d8d70 git $ GIT_DIR=hgrepo-2/.hg/git git fetch --quiet repo.git $ rm -rf hgrepo-2 $ hg clone -r master repo.git hgrepo-2 importing 4 git commits new changesets ff7a2f2d8d70:0eb1ab0073a8 (4 drafts) updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -r 'tagged()' -T shorttags -R hgrepo-2 0:ff7a2f2d8d70 draft alpha 1:7fe02317c63d draft beta 3:0eb1ab0073a8 draft default/master gamma tip This used to die: $ hg -R hgrepo-2 gexport This used to fail, since we didn't actually pull the annotated tag: $ hg -R hgrepo-2 push pushing to $TESTTMP/repo.git searching for changes no changes found [1] $ rm -rf hgrepo-2 Check that pulling will update phases only: $ cd hgrepo $ hg phase -fs gamma detached $ hg pull pulling from $TESTTMP/repo.git no changes found $ hg log -T shorttags -r gamma -r detached 4:0eb1ab0073a8 draft beta default/master gamma 2:61175962e488 draft detached X $ cd .. Check that we pull new tags to existing commits: $ cd gitrepo $ git tag alpha beta detached gamma $ fn_git_tag extra-simple-tag $ fn_git_tag -m annotated extra-annotated-tag $ git push --tags To $TESTTMP/repo.git * [new tag] extra-annotated-tag -> extra-annotated-tag * [new tag] extra-simple-tag -> extra-simple-tag $ cd ../hgrepo $ hg pull -r master pulling from $TESTTMP/repo.git no changes found $ hg tags -v | grep extra extra-simple-tag 1:7fe02317c63d git extra-annotated-tag 1:7fe02317c63d git ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-git-workflow.t0000644000000000000000000001027014751647721014574 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo $ cd hgrepo $ hg debuggitdir $TESTTMP/hgrepo/.hg/git $ echo alpha > alpha $ hg add alpha $ fn_hg_commit -m "add alpha" $ hg log --graph --debug | grep -v phase: @ changeset: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 tag: tip parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default description: add alpha $ cd .. configure for use from git $ hg clone hgrepo gitrepo updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd gitrepo $ hg book master $ hg up null 0 files updated, 0 files merged, 1 files removed, 0 files unresolved (leaving bookmark master) $ echo "[git]" >> .hg/hgrc $ echo "intree = True" >> .hg/hgrc $ hg debuggitdir $TESTTMP/gitrepo/.git $ hg gexport do some work $ git config core.bare false $ git checkout master 2>&1 | sed s/\'/\"/g Already on "master" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' get things back to hg $ hg gimport importing 1 git commits updating bookmark master new changesets 9f124f3c1fc2 (1 drafts) $ hg log --graph --debug | grep -v phase: o changeset: 1:9f124f3c1fc29a14f5eb027c24811b0ac9d5ff10 | bookmark: master | tag: tip | parent: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 | parent: -1:0000000000000000000000000000000000000000 | manifest: 1:f0bd6fbafbaebe4bb59c35108428f6fce152431d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | files+: beta | extra: branch=default | extra: hg-git-rename-source=git | description: | add beta | | o changeset: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default description: add alpha gimport should have updated the bookmarks as well $ hg bookmarks master 1:9f124f3c1fc2 gimport support for git.mindate $ cat >> .hg/hgrc << EOF > [git] > mindate = 2014-01-02 00:00:00 +0000 > EOF $ echo oldcommit > oldcommit $ git add oldcommit $ GIT_AUTHOR_DATE="2014-03-01 00:00:00 +0000" \ > GIT_COMMITTER_DATE="2009-01-01 00:00:00 +0000" \ > git commit -m oldcommit > /dev/null || echo "git commit error" $ hg gimport no changes found $ hg log --graph o changeset: 1:9f124f3c1fc2 | bookmark: master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:0221c246a567 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ echo newcommit > newcommit $ git add newcommit $ GIT_AUTHOR_DATE="2014-01-01 00:00:00 +0000" \ > GIT_COMMITTER_DATE="2014-01-02 00:00:00 +0000" \ > git commit -m newcommit > /dev/null || echo "git commit error" $ hg gimport importing 2 git commits updating bookmark master new changesets befdecd14df5:3d10b7289d79 (2 drafts) $ hg log --graph o changeset: 3:3d10b7289d79 | bookmark: master | tag: tip | user: test | date: Wed Jan 01 00:00:00 2014 +0000 | summary: newcommit | o changeset: 2:befdecd14df5 | user: test | date: Sat Mar 01 00:00:00 2014 +0000 | summary: oldcommit | o changeset: 1:9f124f3c1fc2 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:0221c246a567 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-gitignore-permissions.t0000644000000000000000000000170114751647721016500 0ustar00#require no-windows Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init repo $ cd repo $ if test `whoami` = root > then > echo "skipped: must run as unprivileged user, not root" > exit 80 > fi Create a commit and export it to Git $ touch thefile $ hg add thefile $ hg ci -A -m commit $ hg gexport Create a file that we can ignore $ touch nothingtoseehere And something we can't read $ mkdir not_readable $ chmod 000 not_readable Add a .gitignore, and to make sure that we're using it, make it ignore something. $ echo nothingtoseehere > .gitignore $ hg status not_readable: Permission denied not_readable: Permission denied ? .gitignore And notice that we really did ignore it! For comparison, how does a normal status handle this? $ hg status --config extensions.hggit=! not_readable: Permission denied ? .gitignore ? nothingtoseehere So the duplicated output is actually a bug... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-gitignore-share.t0000644000000000000000000000155514751647721015236 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [extensions] > share = > EOF $ git init --quiet --bare repo.git $ hg init hgrepo $ cd hgrepo $ cat > .hg/hgrc < [paths] > default = $TESTTMP/repo.git > EOF $ echo ignored > .gitignore $ hg add .gitignore $ hg ci -m ignore $ hg book master $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/master $ cd .. We should also ignore the file in a shared repository: $ hg share hgrepo sharerepo updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd sharerepo $ hg paths default = $TESTTMP/repo.git $ cat .gitignore ignored $ touch ignored $ hg status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-gitignore-windows.t0000644000000000000000000000077314751647721015627 0ustar00#require windows Load commonly used test logic $ . "$TESTDIR/testutil" Even though its documentation says otherwise, Git does accept \ in .gitignore $ hg init repo $ cd repo $ touch thefile $ hg ci -qAm thefile $ hg gexport $ touch ignored-file $ mkdir ignored-dir $ touch ignored-dir/also-ignored-file $ hg status ? ignored-dir/also-ignored-file ? ignored-file $ cat >> .gitignore < \ignored-file > ignored-dir\\ > \.directory > EOF $ hg status ? .gitignore ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-gitignore.t0000644000000000000000000000556314751647721014141 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init repo $ cd repo Create a commit that we can export later on $ touch thefile $ hg commit -A -m "initial commit" adding thefile We should only read .gitignore files in a hg-git repo (i.e. one with .hg/git directory) otherwise, a rogue .gitignore could slow down a hg-only repo $ touch foo $ touch foobar $ touch bar $ echo 'foo*' > .gitignore $ hg status ? .gitignore ? bar ? foo ? foobar Notice that foo appears above. Now export the commit to git and verify it's gone: $ hg gexport $ hg status ? .gitignore ? bar $ echo '*bar' > .gitignore $ hg status ? .gitignore ? foo $ mkdir dir $ touch dir/foo $ echo 'foo' > .gitignore $ hg status ? .gitignore ? bar ? foobar $ echo '/foo' > .gitignore $ hg status ? .gitignore ? bar ? dir/foo ? foobar $ rm .gitignore $ echo 'foo' > dir/.gitignore $ hg status ? bar ? dir/.gitignore ? foo ? foobar $ touch dir/bar $ echo 'bar' > .gitignore $ hg status ? .gitignore ? dir/.gitignore ? foo ? foobar $ echo '/bar' > .gitignore $ hg status ? .gitignore ? dir/.gitignore ? dir/bar ? foo ? foobar $ echo 'foo*' > .gitignore $ echo '!*bar' >> .gitignore $ hg status .gitignore: unsupported ignore pattern '!*bar' ? .gitignore ? bar ? dir/.gitignore ? dir/bar $ echo '.hg/' > .gitignore $ hg status ? .gitignore ? bar ? dir/.gitignore ? dir/bar ? foo ? foobar $ echo 'dir/.hg/' > .gitignore $ hg status ? .gitignore ? bar ? dir/.gitignore ? dir/bar ? foo ? foobar $ echo '.hg/foo' > .gitignore $ hg status ? .gitignore ? bar ? dir/.gitignore ? dir/bar ? foo ? foobar $ touch foo.hg $ echo 'foo.hg' > .gitignore $ hg status ? .gitignore ? bar ? dir/.gitignore ? dir/bar ? foo ? foobar $ rm foo.hg $ touch .hgignore $ hg status ? .gitignore ? .hgignore ? bar ? dir/.gitignore ? dir/bar ? dir/foo ? foo ? foobar $ echo 'syntax: re' > .hgignore $ echo 'foo.*$(?> .hgignore $ echo 'dir/foo' >> .hgignore $ hg status ? .gitignore ? .hgignore ? bar ? dir/.gitignore ? dir/bar ? foobar $ hg add .gitignore $ hg commit -m "add and commit .gitignore" $ rm .gitignore $ rm .hgignore $ hg status ! .gitignore ? bar ? dir/.gitignore ? dir/bar ? foo ? foobar show pattern error in hgignore file as expected (issue197) ---------------------------------------------------------- $ cat > $TESTTMP/invalidhgignore < # invalid syntax in regexp > foo( > EOF $ hg status --config ui.ignore=$TESTTMP/invalidhgignore abort: $TESTTMP/invalidhgignore: invalid pattern (relre): foo( [255] $ cat > .hgignore < # invalid syntax in regexp > foo( > EOF $ hg status abort: $TESTTMP/repo/.hgignore: invalid pattern (relre): foo( [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-help.t0000644000000000000000000002762314751647721013103 0ustar00Tests that the various help files are properly registered Load commonly used test logic $ . "$TESTDIR/testutil" $ hg help | grep 'git' | sed 's/ */ /g' git-cleanup clean up Git commit map after history editing (?) git-verify verify that a Mercurial rev matches the corresponding Git rev hggit push and pull from a Git server hggit-config Configuring hg-git $ hg help hggit-config Configuring hg-git """""""""""""""""" "git" ----- Control how the Hg-Git extension interacts with Git. "authors" Git uses a strict convention for "author names" when representing changesets, using the form "[realname] [email address]". Mercurial encourages this convention as well but is not as strict, so it's not uncommon for a Mercurial repository to have authors listed as, for example, simple usernames. hg-git by default will attempt to translate Mercurial usernames using the following rules: - If the Mercurial username fits the pattern "NAME ", the Git name will be set to NAME and the email to EMAIL. - If the Mercurial username looks like an email (if it contains an "@"), the Git name and email will both be set to that email. - If the Mercurial username consists of only a name, the email will be set to "none@none". - Illegal characters (stray "<"\ s or ">"\ s) will be stripped out, and for "NAME " usernames, any content after the right-bracket (for example, a second ">") will be turned into a url-encoded sigil like "ext:(%3E)" in the Git author name. Since these default behaviors may not be what you want ("none@none", for example, shows up unpleasantly on GitHub as "illegal email address"), the "git.authors" option provides for an "authors translation file" that will be used during outgoing transfers from Mercurial to Git only, by modifying "hgrc" as such: [git] authors = authors.txt Where "authors.txt" is the name of a text file containing author name translations, one per each line, using the following format: johnny = John Smith dougie = Doug Johnson Empty lines and lines starting with a "#" are ignored. It should be noted that this translation is in *the Mercurial to Git direction only*. Changesets coming from Git back to Mercurial will not translate back into Mercurial usernames, so it's best that the same username/email combination be used on both the Mercurial and Git sides; the author file is mostly useful for translating legacy changesets. "branch_bookmark_suffix" Hg-Git does not convert between Mercurial named branches and git branches as the two are conceptually different; instead, it uses Mercurial bookmarks to represent the concept of a Git branch. Therefore, when translating a Mercurial repository over to Git, you typically need to create bookmarks to mirror all the named branches that you'd like to see transferred over to Git. The major caveat with this is that you can't use the same name for your bookmark as that of the named branch, and furthermore there's no feasible way to rename a branch in Mercurial. For the use case where one would like to transfer a Mercurial repository over to Git, and maintain the same named branches as are present on the hg side, the "branch_bookmark_suffix" might be all that's needed. This presents a string "suffix" that will be recognized on each bookmark name, and stripped off as the bookmark is translated to a Git branch: [git] branch_bookmark_suffix=_bookmark Above, if a Mercurial repository had a named branch called "release_6_maintenance", you could then link it to a bookmark called "release_6_maintenance_bookmark". hg-git will then strip off the "_bookmark" suffix from this bookmark name, and create a Git branch called "release_6_maintenance". When pulling back from Git to hg, the "_bookmark" suffix is then applied back, if and only if a Mercurial named branch of that name exists. E.g., when changes to the "release_6_maintenance" branch are checked into Git, these will be placed into the "release_6_maintenance_bookmark" bookmark on hg. But if a new branch called "release_7_maintenance" were pulled over to hg, and there was not a "release_7_maintenance" named branch already, the bookmark will be named "release_7_maintenance" with no usage of the suffix. The "branch_bookmark_suffix" option is, like the "authors" option, intended for migrating legacy hg named branches. Going forward, a Mercurial repository that is to be linked with a Git repository should only use bookmarks for named branching. "findcopiesharder" Whether to consider unmodified files as copy sources. This is a very expensive operation for large projects, so use it with caution. Similar to "git diff"'s --find-copies-harder option. "intree" Hg-Git keeps a Git repository clone for reading and updating. By default, the Git clone is the subdirectory "git" in your local Mercurial repository. If you would like this Git clone to be at the same level of your Mercurial repository instead (named ".git"), add the following to your "hgrc": [git] intree = True Please note that changing this setting in an existing repository doesn't move the local Git repository. You will either have to do so yourself, or issue an 'hg pull' after the fact to repopulate the new location. "mindate" If set, branches where the latest commit's commit time is older than this will not be imported. Accepts any date formats that Mercurial does -- see 'hg help dates' for more. "public" A list of Git branches that should be considered "published", and therefore converted to Mercurial in the 'public' phase. This is only used if "hggit.usephases" is set. "pull-prune-remote-branches" Before fetching, remove any remote-tracking references, or pseudo-tags, that no longer exist on the remote. This is equivalent to the "--prune" option to "git fetch", and means that pseudo-tags for remotes -- such as "default/master" -- always actually reflect what's on the remote. This option is enabled by default. "pull-prune-bookmarks" On pull, delete any unchanged bookmarks removed on the remote. In other words, if e.g. the "thebranch" bookmark remains at "default/thebranch", and the branch is deleted in Git, pulling deletes the bookmark. This option is enabled by default. "renamelimit" The number of files to consider when performing the copy/rename detection. Detection is disabled if the number of files modified in a commit is above the limit. Detection is O(N^2) in the number of files modified, so be sure not to set the limit too high. Similar to Git's "diff.renameLimit" config. The default is "400", the same as Git. "similarity" Specify how similar files modified in a Git commit must be to be imported as Mercurial renames or copies, as a percentage between "0" (disabled) and "100" (files must be identical). For example, "90" means that a delete/add pair will be imported as a rename if more than 90% of the file has stayed the same. The default is "0" (disabled). "blame.ignoreRevsFile" Specify a file that lists Git commits to ignore when invoking 'hg annotate'. "hggit" ------- Control behavior of the Hg-Git extension. "mapsavefrequency" By default, hg-git only saves the results of a conversion at the end. Use this option to enable resuming long-running pulls and pushes. Set this to a number greater than 0 to allow resuming after converting that many commits. This can help when the conversion encounters an error partway through a large batch of changes. Otherwise, an error or interruption will roll back the transaction, similar to regular Mercurial. Defaults to 1000. Please note that this is disregarded for an initial clone, as any error or interruption will delete the destination. So instead of cloning a large Git repository, you might want to pull instead: $ hg init linux $ cd linux $ echo "[paths]\ndefault = https://github.com/torvalds/linux" > .hg/hgrc $ hg pull ...and be extremely patient. Please note that converting very large repositories may take *days* rather than mere *hours*, and may run into issues with available memory for very long running clones. Even any small, undiscovered leak will build up when processing hundreds of thousands of files and commits. Cloning the Linux kernel is likely a pathological case, but other storied repositories such as CPython do work well, even if the initial clone requires a some patience. "threads" During a push to Git, hg-git will pack loose objects at regular intervals whenever it saves its map. As this is a rather expensive operation, it's done in separate threads. Defaults to the system CPU count or 4, whichever is lower. "usephases" When converting Git revisions to Mercurial, place them in the 'public' phase as appropriate. Namely, revisions that are reachable from the remote Git repository's default branch, or "HEAD", will be marked *public*. For most repositories, this means the remote "master" branch will be converted as public. The same applies to any commits tagged in the remote. To restrict publishing to specific branches or tags, use the "git.public" option. Publishing commits prevents their modification, and speeds up many local Mercurial operations, such as 'hg shelve'. "fetchbuffer" Data fetched from Git is buffered in memory, unless it exceeds the given limit, in megabytes. By default, flush the buffer to disk when it exceeds 100MB. "retries" Interacting with a remote Git repository may require authentication. Normally, this will trigger a prompt and a retry, and this option restricts the amount of retries. Defaults to 3. "invalidpaths" Both Mercurial and Git consider paths as just bytestrings internally, and allow almost anything. The difference, however, is in the _almost_ part. For example, many Git servers will reject a push for security reasons if it contains a nested Git repository. Similarly, Mercurial cannot checkout commits with a nested repository, and it cannot even store paths containing an embedded newline or carrage return character. The default is to issue a warning and skip these paths. You can change this by setting "hggit.invalidpaths" in ".hgrc": [hggit] invalidpaths = keep Possible values are "keep", "skip" or "abort". Prior to 1.0, the default was "abort". $ hg help config.hggit | head -10 "hggit" ------- Control behavior of the Hg-Git extension. "mapsavefrequency" By default, hg-git only saves the results of a conversion at the end. Use this option to enable resuming long-running pulls and pushes. Set this to a number greater than 0 to allow resuming after converting that many commits. This can help when the conversion encounters an error ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-hg-author.t0000644000000000000000000001670714751647721014052 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init -q --bare repo.git $ git clone repo.git gitrepo Cloning into 'gitrepo'... warning: You appear to have cloned an empty repository. done. $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git push --quiet --set-upstream origin master Branch 'master' set up to track remote branch 'master' from 'origin'. (?) $ cd .. $ hg clone repo.git hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg book master $ echo beta > beta $ hg add beta $ fn_hg_commit -u "test" -m 'add beta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo gamma >> beta $ fn_hg_commit -u "test (comment)" -m 'modify beta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -u "" -m 'add gamma' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo delta > delta $ hg add delta $ fn_hg_commit -u "name" -m 'add delta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo epsilon > epsilon $ hg add epsilon $ fn_hg_commit -u "name zeta $ hg add zeta $ fn_hg_commit -u " test " -m 'add zeta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo eta > eta $ hg add eta $ fn_hg_commit -u "test < test@example.com >" -m 'add eta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo theta > theta $ hg add theta $ fn_hg_commit -u "test >test@example.com>" -m 'add theta' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg log --graph @ changeset: 8:c5d1976ab12c | bookmark: master | tag: default/master | tag: tip | user: test >test@example.com> | date: Mon Jan 01 00:00:18 2007 +0000 | summary: add theta | o changeset: 7:0e2fb4d21667 | user: test < test@example.com > | date: Mon Jan 01 00:00:17 2007 +0000 | summary: add eta | o changeset: 6:faa3aae96199 | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: add zeta | o changeset: 5:2cf6ad5a1afc | user: name | date: Mon Jan 01 00:00:14 2007 +0000 | summary: add delta | o changeset: 3:6b854d65d0d6 | user: | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add gamma | o changeset: 2:46303c652e79 | user: test (comment) | date: Mon Jan 01 00:00:12 2007 +0000 | summary: modify beta | o changeset: 1:47580592d3d6 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ hg clone repo.git hgrepo2 importing 9 git commits new changesets ff7a2f2d8d70:1fbf3aa91221 (9 drafts) updating to bookmark master 8 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo2 log --graph @ changeset: 8:1fbf3aa91221 | bookmark: master | tag: default/master | tag: tip | user: test ?test@example.com | date: Mon Jan 01 00:00:18 2007 +0000 | summary: add theta | o changeset: 7:20310508f06d | user: test | date: Mon Jan 01 00:00:17 2007 +0000 | summary: add eta | o changeset: 6:e3d81af8a8c1 | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: add zeta | o changeset: 5:78f609fd208f | user: name | date: Mon Jan 01 00:00:15 2007 +0000 | summary: add epsilon | o changeset: 4:42fa61d57718 | user: name | date: Mon Jan 01 00:00:14 2007 +0000 | summary: add delta | o changeset: 3:6b854d65d0d6 | user: | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add gamma | o changeset: 2:46303c652e79 | user: test (comment) | date: Mon Jan 01 00:00:12 2007 +0000 | summary: modify beta | o changeset: 1:47580592d3d6 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ git --git-dir=repo.git log --pretty=medium master commit 2fe60ba69727981e6ede78be70354c3a9e30e21d Author: test ?test@example.com Date: Mon Jan 1 00:00:18 2007 +0000 add theta commit 9f2f7cafdbf2e467928db98de8275141001d3081 Author: test Date: Mon Jan 1 00:00:17 2007 +0000 add eta commit 172a6f8d8064d73dff7013e395a9fe3cfc3ff807 Author: test Date: Mon Jan 1 00:00:16 2007 +0000 add zeta commit 71badb8e343a7da391a9b5d98909fbd2ca7d78f2 Author: name Date: Mon Jan 1 00:00:15 2007 +0000 add epsilon commit 9a9ae7b7f310d4a1a3e732a747ca26f06934f8d8 Author: name Date: Mon Jan 1 00:00:14 2007 +0000 add delta commit e4149a32e81e380193f59aa8773349201b8ed7f7 Author: Date: Mon Jan 1 00:00:13 2007 +0000 add gamma commit fae95aef5889a80103c2fbd5d14ff6eb8c9daf93 Author: test ext:(%20%28comment%29) Date: Mon Jan 1 00:00:12 2007 +0000 modify beta commit 0f378ab6c2c6b5514bd873d3faf8ac4b8095b001 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-hg-branch.t0000644000000000000000000000514514751647721013777 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ hg clone gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark not-master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg co master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ hg mv alpha beta $ fn_hg_commit -m 'rename alpha to beta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ hg branch gamma | grep -v 'permanent and global' marked working directory as branch gamma $ fn_hg_commit -m 'started branch gamma' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ hg log --graph @ changeset: 2:400db38f4f64 | branch: gamma | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: started branch gamma | o changeset: 1:3baa67317a4d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: rename alpha to beta | o changeset: 0:ff7a2f2d8d70 bookmark: not-master tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ hg clone -U gitrepo hgrepo2 importing 3 git commits new changesets ff7a2f2d8d70:400db38f4f64 (3 drafts) $ hg -R hgrepo2 log --graph o changeset: 2:400db38f4f64 | branch: gamma | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: started branch gamma | o changeset: 1:3baa67317a4d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: rename alpha to beta | o changeset: 0:ff7a2f2d8d70 bookmark: not-master tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-hg-clone.t0000644000000000000000000000444414751647721013643 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag alpha $ cd .. $ hg clone -U gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) By default, the Git state isn't preserved across a copying/linking clone $ hg clone -U hgrepo otherhgrepo $ cd otherhgrepo $ find .hg -name 'git*' | sort $ hg tags -v tip 0:ff7a2f2d8d70 $ hg log -r 'fromgit()' -T '{rev}:{node|short} {gitnode|short}\n' $ cd .. $ rm -r otherhgrepo Nor using a pull clone $ hg clone -U --pull hgrepo otherhgrepo requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files new changesets ff7a2f2d8d70 $ cd otherhgrepo $ find .hg -name 'git*' | sort $ hg tags -v tip 0:ff7a2f2d8d70 $ hg log -r 'fromgit()' -T '{rev}:{node|short} {gitnode|short}\n' $ cd .. $ rm -r otherhgrepo But we can enable it! $ cat >> $HGRCPATH < [experimental] > hg-git-serve = yes > EOF Check transferring between Mercurial repositories using a copying/linking clone $ hg clone -U hgrepo otherhgrepo $ cd otherhgrepo $ find .hg -name 'git*' | sort $ hg tags -q tip $ hg log -r 'fromgit()' -T '{rev}:{node|short} {gitnode|short}\n' $ cd .. Checking using a pull clone $ rm -rf otherhgrepo $ hg clone -U --pull hgrepo otherhgrepo requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files new changesets ff7a2f2d8d70 $ cd otherhgrepo $ hg tags -q tip alpha $ hg log -r 'fromgit()' -T '{rev}:{node|short} {gitnode|short}\n' 0:ff7a2f2d8d70 7eeab2ea75ec $ cd .. Can we repopulate the state from a Mercurial repository? $ cd otherhgrepo $ hg debug-remove-hggit-state clearing out the git cache data $ hg log -qr 'fromgit()' $ hg tags tip 0:ff7a2f2d8d70 $ hg pull pulling from $TESTTMP/hgrepo searching for changes no changes found $ hg log -qr 'fromgit()' $ hg tags tip 0:ff7a2f2d8d70 Sadly, no. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-hg-tags.t0000644000000000000000000000446214751647721013501 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init -q --bare repo.git $ git clone repo.git gitrepo Cloning into 'gitrepo'... warning: You appear to have cloned an empty repository. done. $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git push --quiet --set-upstream origin master Branch 'master' set up to track remote branch 'master' from 'origin'. (?) $ cd .. $ hg clone -U repo.git hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) $ cd hgrepo $ hg co master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ fn_hg_tag alpha $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master adding reference refs/tags/alpha $ hg log --graph @ changeset: 1:e8b150f84560 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alpha for changeset ff7a2f2d8d70 | o changeset: 0:ff7a2f2d8d70 tag: alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ cd gitrepo git should have the tag alpha $ git fetch origin From $TESTTMP/repo 7eeab2e..bbae830 master -> origin/master * [new tag] alpha -> alpha $ cd .. $ hg clone repo.git hgrepo2 importing 2 git commits new changesets ff7a2f2d8d70:e8b150f84560 (2 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo2 log --graph @ changeset: 1:e8b150f84560 | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alpha for changeset ff7a2f2d8d70 | o changeset: 0:ff7a2f2d8d70 tag: alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha the tag should be in .hgtags $ cat hgrepo2/.hgtags ff7a2f2d8d7099694ae1e8b03838d40575bebb63 alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-hook.t0000644000000000000000000001236414751647721013107 0ustar00commit hooks can see env vars (and post-transaction one are run unlocked) $ . testutil $ fn_commit() { > echo $2 > $2 > $1 add $2 > fn_${1}_commit -m $2 > } $ hg init hgrepo $ cd hgrepo $ cat > .hg/hgrc < [hooks] > gitimport = python:testlib.hooks.showargs > gitexport = python:testlib.hooks.showargs > pretxncommit = echo : pretxncommit > preoutgoing = python:testlib.hooks.showargs > prechangegroup = python:testlib.hooks.showargs > changegroup = python:testlib.hooks.showargs > incoming = python:testlib.hooks.showargs > EOF $ fn_commit hg a $ hg book master $ git init -q --bare ../repo.git $ cat >> .hg/hgrc < [paths] > default = $TESTTMP/repo.git > EOF Test pushing a single commit: (The order of outgoing isn't stable, so only try it here.) $ hg push --config hooks.outgoing=python:testlib.hooks.showargs pushing to $TESTTMP/repo.git | preoutgoing.git=True | preoutgoing.source=push | preoutgoing.url=$TESTTMP/repo.git | gitexport.nodes=[b'cc0ffa47c67ebcb08dc50f69afaecb5d622cc777'] | gitexport.git=True searching for changes | prechangegroup.source=push | prechangegroup.git=True | prechangegroup.url=$TESTTMP/repo.git | outgoing.source=push | outgoing.git=True | outgoing.url=$TESTTMP/repo.git | outgoing.node=cc0ffa47c67ebcb08dc50f69afaecb5d622cc777 | outgoing.git_node=681fb452693218a33986174228560272a6fad87a adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/master $ git clone -q ../repo.git ../gitrepo $ cd ../gitrepo $ fn_commit git b $ fn_commit git c $ git push To $TESTTMP/hgrepo/../repo.git 681fb45..1dab31e master -> master $ cd ../hgrepo Hooks on pull? $ hg pull -u pulling from $TESTTMP/repo.git | gitimport.source=pull | gitimport.git=True | gitimport.names=[b'default'] | gitimport.refs={b'HEAD': b'1dab31e7bc9691ba42a2fe7b14680694770bc527', b'refs/heads/master': b'1dab31e7bc9691ba42a2fe7b14680694770bc527'} | gitimport.heads=None importing 2 git commits : pretxncommit | incoming.git=True | incoming.source=pull | incoming.node=382ad5fa1d7727210384d40fa1539af52ca632c5 | incoming.git_node=92150d1529ccaea34a6b36fe4144993193080499 : pretxncommit | incoming.git=True | incoming.source=pull | incoming.node=892115eea5c32152e09ae4013c9a119d7b534049 | incoming.git_node=1dab31e7bc9691ba42a2fe7b14680694770bc527 updating bookmark master | changegroup.source=push | changegroup.git=True | changegroup.node=382ad5fa1d7727210384d40fa1539af52ca632c5 | changegroup.node_last=892115eea5c32152e09ae4013c9a119d7b534049 new changesets 382ad5fa1d77:892115eea5c3 (2 drafts) updating to active bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Hooks on push? $ fn_commit hg d $ fn_commit hg e $ hg push pushing to $TESTTMP/repo.git | preoutgoing.git=True | preoutgoing.source=push | preoutgoing.url=$TESTTMP/repo.git | gitexport.nodes=[b'cc6164a17449d58d7811ff3918f33f89c2c83fa5', b'46737f6a4c9d8307b681cbb2e9e2e5419cc87f82'] | gitexport.git=True searching for changes | prechangegroup.source=push | prechangegroup.git=True | prechangegroup.url=$TESTTMP/repo.git adding objects remote: found 0 deltas to reuse added 2 commits with 2 trees and 2 blobs updating reference refs/heads/master And what does Mercurial do? $ cat >> .hg/hgrc < [hooks] > outgoing = python:testlib.hooks.showargs > EOF On push: $ hg init ../hgrepo-copy $ hg push ../hgrepo-copy pushing to ../hgrepo-copy searching for changes | preoutgoing.source=push | outgoing.node=cc0ffa47c67ebcb08dc50f69afaecb5d622cc777 | outgoing.source=push adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 5 files With more than one head: $ rm -r ../hgrepo-copy $ hg init ../hgrepo-copy $ hg book -i $ hg branch -q abranch $ fn_commit hg x $ hg up -q default $ hg branch -q alsoabranch $ fn_commit hg y $ hg push ../hgrepo-copy pushing to ../hgrepo-copy searching for changes | preoutgoing.source=push | outgoing.node=cc0ffa47c67ebcb08dc50f69afaecb5d622cc777 | outgoing.source=push adding changesets adding manifests adding file changes added 7 changesets with 7 changes to 7 files (+1 heads) On pull: $ hg debugstrip --no-backup tip 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg pull ../hgrepo-copy pulling from ../hgrepo-copy searching for changes | prechangegroup.txnname=pull file://$TESTTMP/hgrepo-copy | prechangegroup.source=pull | prechangegroup.url=file:$TESTTMP/hgrepo-copy adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) new changesets d4097d98a390 | changegroup.txnname=pull file://$TESTTMP/hgrepo-copy | changegroup.source=pull | changegroup.url=file:$TESTTMP/hgrepo-copy | changegroup.node=d4097d98a3905be88e8a566039b1fdcca06e0d2e | changegroup.node_last=d4097d98a3905be88e8a566039b1fdcca06e0d2e | incoming.txnname=pull file://$TESTTMP/hgrepo-copy | incoming.source=pull | incoming.url=file:$TESTTMP/hgrepo-copy | incoming.node=d4097d98a3905be88e8a566039b1fdcca06e0d2e (run 'hg heads' to see heads) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-illegal-contents.t0000644000000000000000000001356614751647721015420 0ustar00Check for contents we should refuse to export to git repositories (or at least warn). Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hg $ cd hg $ mkdir -p .git/hooks $ cat > .git/hooks/post-update < #!/bin/sh > echo pwned > EOF $ fn_touch_escaped foo/git~100/wat bar/.gi\\u200ct/wut this/is/safe $ hg addremove adding .git/hooks/post-update adding bar/.gi\xe2\x80\x8ct/wut (esc) adding foo/git~100/wat adding this/is/safe $ hg ci -m "we should refuse to export this" $ hg book master $ hg gexport warning: skipping invalid path '.git/hooks/post-update' warning: skipping invalid path 'bar/.gi\xe2\x80\x8ct/wut' warning: skipping invalid path 'foo/git~100/wat' $ GIT_DIR=.hg/git git ls-tree -r --name-only master this/is/safe $ hg debug-remove-hggit-state clearing out the git cache data $ hg gexport --config hggit.invalidpaths=keep warning: path '.git/hooks/post-update' contains an invalid path component warning: path 'bar/.gi\xe2\x80\x8ct/wut' contains an invalid path component warning: path 'foo/git~100/wat' contains an invalid path component $ GIT_DIR=.hg/git git ls-tree -r --name-only master .git/hooks/post-update "bar/.gi\342\200\214t/wut" foo/git~100/wat this/is/safe $ cd .. $ rm -rf hg $ hg init hg $ cd hg $ mkdir -p nested/.git/hooks/ $ cat > nested/.git/hooks/post-update < #!/bin/sh > echo pwnd > EOF $ chmod +x nested/.git/hooks/post-update $ hg addremove adding nested/.git/hooks/post-update $ hg ci -m "also refuse to export this" $ hg book master $ hg gexport warning: skipping invalid path 'nested/.git/hooks/post-update' $ git clone .hg/git git Cloning into 'git'... done. $ rm -rf git We can trigger an error: $ hg -q debug-remove-hggit-state $ hg --config hggit.invalidpaths=abort gexport abort: invalid path 'nested/.git/hooks/post-update' rejected by configuration (see 'hg help config.hggit.invalidpaths for details) [255] We can override if needed: $ hg --config hggit.invalidpaths=keep gexport warning: path 'nested/.git/hooks/post-update' contains an invalid path component $ cd .. $ # different git versions give different return codes $ git clone hg/.hg/git git || true Cloning into 'git'... done. error: [Ii]nvalid path 'nested/\.git/hooks/post-update' (re) fatal: unable to checkout working tree (?) warning: Clone succeeded, but checkout failed. (?) You can inspect what was checked out with 'git status' (?) and retry( the checkout)? with '.*' (re) (?) (?) Now check something that case-folds to .git, which might let you own Mac users: $ cd .. $ rm -rf hg $ hg init hg $ cd hg $ mkdir -p .GIT/hooks/ $ cat > .GIT/hooks/post-checkout < #!/bin/sh > echo pwnd > EOF $ chmod +x .GIT/hooks/post-checkout $ hg addremove adding .GIT/hooks/post-checkout $ hg ci -m "also refuse to export this" $ hg book master $ hg gexport $ cd .. And the NTFS case: $ cd .. $ rm -rf hg $ hg init hg $ cd hg $ mkdir -p GIT~1/hooks/ $ cat > GIT~1/hooks/post-checkout < #!/bin/sh > echo pwnd > EOF $ chmod +x GIT~1/hooks/post-checkout $ hg addremove adding GIT~1/hooks/post-checkout $ hg ci -m "also refuse to export this" $ hg book master $ hg gexport warning: skipping invalid path 'GIT~1/hooks/post-checkout' $ cd .. Now check a Git repository containing a Mercurial repository, which you can't check out. $ rm -rf hg git nested $ git init -q git $ hg init nested $ mv nested git $ cd git $ git add nested $ fn_git_commit -m 'add a Mercurial repository' $ cd .. $ hg clone --config hggit.invalidpaths=abort git hg importing 1 git commits abort: invalid path 'nested/.hg/00changelog.i' rejected by configuration (see 'hg help config.hggit.invalidpaths for details) [255] $ rm -rf hg $ hg clone --config hggit.invalidpaths=keep git hg importing 1 git commits warning: path 'nested/.hg/00changelog.i' contains an invalid path component warning: path 'nested/.hg/requires' contains an invalid path component warning: path 'nested/.hg/store/requires' contains an invalid path component (?) new changesets [0-9a-f]{12,12} \(1 drafts\) (re) warning: path 'nested/.hg/store/requires' is within a nested repository, which Mercurial cannot check out. (?) updating to bookmark master abort: path 'nested/.hg/00changelog.i' is inside nested repo 'nested' [10] $ rm -rf hg $ hg clone git hg importing 1 git commits warning: skipping invalid path 'nested/.hg/00changelog.i' warning: skipping invalid path 'nested/.hg/requires' warning: skipping invalid path 'nested/.hg/store/requires' (?) new changesets 3ea18a67c0e6 (1 drafts) updating to bookmark master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd .. Now check a Git repository containing paths with carriage return and newline, which Mercurial expressly forbids (see https://bz.mercurial-scm.org/show_bug.cgi?id=352) $ rm -rf hg git $ git init -q git $ cd git $ fn_touch_escaped Icon\\r the\\nfile $ git add . $ fn_git_commit -m 'add files disallowed by mercurial' $ cd .. $ hg clone --config hggit.invalidpaths=abort git hg importing 1 git commits abort: invalid path 'Icon\r' rejected by configuration (see 'hg help config.hggit.invalidpaths for details) [255] $ hg clone --config hggit.invalidpaths=keep git hg importing 1 git commits warning: skipping invalid path 'Icon\r' warning: skipping invalid path 'the\nfile' new changesets 8354c06a5842 (1 drafts) updating to bookmark master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ rm -rf hg $ hg clone git hg importing 1 git commits warning: skipping invalid path 'Icon\r' warning: skipping invalid path 'the\nfile' new changesets 8354c06a5842 (1 drafts) updating to bookmark master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-incoming.t0000644000000000000000000000743614751647721013756 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ cd .. $ hg init hgrepo-empty $ hg -R hgrepo-empty incoming gitrepo comparing with gitrepo changeset: 0:7eeab2ea75ec bookmark: master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ hg clone gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo incoming comparing with $TESTTMP/gitrepo no changes found [1] $ cd gitrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ hg -R hgrepo incoming comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 bookmark: master user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ cd gitrepo $ git checkout -b b1 HEAD^ Switched to a new branch 'b1' $ mkdir d $ echo gamma > d/gamma $ git add d/gamma $ fn_git_commit -m'add d/gamma' $ git tag t1 $ echo gamma 2 >> d/gamma $ git add d/gamma $ fn_git_commit -m'add d/gamma line 2' $ cd ../hgrepo $ hg incoming -p comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 bookmark: master user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta diff -r ff7a2f2d8d70 -r 9497a4ee62e1 beta --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/beta Mon Jan 01 00:00:11 2007 +0000 @@ -0,0 +1,1 @@ +beta changeset: 2:9865e289be73 tag: t1 parent: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma diff -r ff7a2f2d8d70 -r 9865e289be73 d/gamma --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/d/gamma Mon Jan 01 00:00:12 2007 +0000 @@ -0,0 +1,1 @@ +gamma changeset: 3:5202f48c20c9 bookmark: b1 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add d/gamma line 2 diff -r 9865e289be73 -r 5202f48c20c9 d/gamma --- a/d/gamma Mon Jan 01 00:00:12 2007 +0000 +++ b/d/gamma Mon Jan 01 00:00:13 2007 +0000 @@ -1,1 +1,2 @@ gamma +gamma 2 $ hg incoming -r master --template 'changeset: {rev}:{node|short}\ngitnode: {gitnode}\n' comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 gitnode: 9497a4ee62e16ee641860d7677cdb2589ea15554 incoming -r $ hg incoming -r master comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 bookmark: master user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg incoming -r b1 comparing with $TESTTMP/gitrepo changeset: 1:9865e289be73 tag: t1 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma changeset: 2:5202f48c20c9 bookmark: b1 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add d/gamma line 2 $ hg incoming -r t1 comparing with $TESTTMP/gitrepo changeset: 1:9865e289be73 tag: t1 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma nothing incoming after pull $ hg pull pulling from $TESTTMP/gitrepo importing 3 git commits adding bookmark b1 updating bookmark master new changesets 7fe02317c63d:248d83ebf472 (3 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg incoming comparing with $TESTTMP/gitrepo no changes found [1] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-invalid-refs.t0000644000000000000000000000722114751647721014526 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ hg clone -U gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) $ cd hgrepo $ hg up master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ fn_hg_tag alph#a $ fn_hg_tag bet*a $ fn_hg_tag 'gamm a' $ hg book -r . delt#a $ hg book -r . epsil*on $ hg gexport warning: not exporting tag 'bet*a' due to invalid name warning: not exporting bookmark 'epsil*on' due to invalid name $ hg push pushing to $TESTTMP/gitrepo warning: not exporting tag 'bet*a' due to invalid name warning: not exporting bookmark 'epsil*on' due to invalid name searching for changes adding objects remote: found 0 deltas to reuse added 3 commits with 3 trees and 3 blobs adding reference refs/heads/delt#a updating reference refs/heads/master adding reference refs/tags/alph#a adding reference refs/tags/gamm_a $ hg log --graph @ changeset: 3:0950ab44ea23 | bookmark: delt#a | bookmark: epsil*on | bookmark: master | tag: default/delt#a | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: Added tag gamm a for changeset 0b27ab2b3df6 | o changeset: 2:0b27ab2b3df6 | tag: gamm a | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: Added tag bet*a for changeset 491ceeb1b0f1 | o changeset: 1:491ceeb1b0f1 | tag: bet*a | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alph#a for changeset ff7a2f2d8d70 | o changeset: 0:ff7a2f2d8d70 bookmark: not-master tag: alph#a tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ cd gitrepo git should have only the valid tag alph#a but have full commit log including the missing invalid bet*a tag commit $ git tag -l alph#a gamm_a $ cd .. $ hg clone -U gitrepo hgrepo2 importing 4 git commits new changesets ff7a2f2d8d70:0950ab44ea23 (4 drafts) $ hg -R hgrepo2 log --graph o changeset: 3:0950ab44ea23 | bookmark: delt#a | bookmark: master | tag: default/delt#a | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: Added tag gamm a for changeset 0b27ab2b3df6 | o changeset: 2:0b27ab2b3df6 | tag: gamm a | tag: gamm_a | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: Added tag bet*a for changeset 491ceeb1b0f1 | o changeset: 1:491ceeb1b0f1 | tag: bet*a | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alph#a for changeset ff7a2f2d8d70 | o changeset: 0:ff7a2f2d8d70 bookmark: not-master tag: alph#a tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha the tag should be in .hgtags $ hg cat -r master hgrepo2/.hgtags ff7a2f2d8d7099694ae1e8b03838d40575bebb63 alph#a 491ceeb1b0f10d65d956dfcdd3470ac2bc2c96a8 bet*a 0b27ab2b3df69c6f7defd7040b93e539136db5be gamm a ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-keywords.t0000644000000000000000000000340114751647721014006 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' This commit is called gamma10 so that its hash will have the same initial digit as commit alpha. This lets us test ambiguous abbreviated identifiers. $ echo gamma10 > gamma10 $ git add gamma10 $ fn_git_commit -m 'add gamma10' $ cd .. $ hg clone gitrepo hgrepo importing 3 git commits new changesets ff7a2f2d8d70:8e3f0ecc9aef (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ echo gamma > gamma $ hg add gamma $ hg commit -m 'add gamma' $ hg log --template "{rev} {node} {node|short} {gitnode} {gitnode|short}\n" 3 965bf7d08d3ac847dd8eb9e72ee0bf547d1a65d9 965bf7d08d3a 2 8e3f0ecc9aefd4ea2fdf8e2d5299cac548762a1c 8e3f0ecc9aef 7e2a5465ff4e3b992c429bb87a392620a0ac97b7 7e2a5465ff4e 1 7fe02317c63d9ee324d4b5df7c9296085162da1b 7fe02317c63d 9497a4ee62e16ee641860d7677cdb2589ea15554 9497a4ee62e1 0 ff7a2f2d8d7099694ae1e8b03838d40575bebb63 ff7a2f2d8d70 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 7eeab2ea75ec $ hg log --template "fromgit {rev}\n" --rev "fromgit()" fromgit 0 fromgit 1 fromgit 2 $ hg log --template "gitnode_existsA {rev}\n" --rev "gitnode(9497a4ee62e16ee641860d7677cdb2589ea15554)" gitnode_existsA 1 $ hg log --template "gitnode_existsB {rev}\n" --rev "gitnode(7eeab)" gitnode_existsB 0 $ hg log --rev "gitnode(7e)" abort: git-mapfile@7e: ambiguous identifier!? (re) [50] $ hg log --template "gitnode_notexists {rev}\n" --rev "gitnode(1234567890ab)" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-merge.t0000644000000000000000000000447414751647721013251 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git checkout -b beta 2>&1 | sed s/\'/\"/g Switched to a new branch "beta" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' clean merge $ git merge -q -m "Merge branch 'beta'" beta $ git show --oneline 5806851 Merge branch 'beta' $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 4 git commits new changesets ff7a2f2d8d70:89ca4a68d6b9 (4 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo clear the cache to be sure it is regenerated correctly $ hg debug-remove-hggit-state clearing out the git cache data $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 4 commits with 4 trees and 3 blobs adding reference refs/heads/beta adding reference refs/heads/master $ cd .. git log in repo pushed from hg $ git --git-dir=repo.git log --pretty=medium master | sed 's/\.\.\.//g' commit 5806851511aaf3bfe813ae3a86c5027165fa9b96 Merge: e5023f9 9497a4e Author: test Date: Mon Jan 1 00:00:12 2007 +0000 Merge branch 'beta' commit e5023f9e5cb24fdcec7b6c127cec45d8888e35a9 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add gamma commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha $ git --git-dir=repo.git log --pretty=medium beta | sed 's/\.\.\.//g' commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-multiple-remotes.t0000644000000000000000000000455014751647721015454 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ git clone --bare --quiet gitrepo repo.git $ hg init hgrepo $ cd hgrepo $ cat > .hg/hgrc < [paths] > default:multi-urls = yes > default = path://git, path://bare > git = $TESTTMP/gitrepo > also-git = $TESTTMP/gitrepo > bare = $TESTTMP/repo.git > also-bare = $TESTTMP/repo.git > EOF $ hg pull pulling from $TESTTMP/gitrepo importing 1 git commits adding bookmark master adding bookmark not-master new changesets ff7a2f2d8d70 (1 drafts) (run 'hg update' to get a working copy) pulling from $TESTTMP/repo.git no changes found $ hg tags tip 0:ff7a2f2d8d70 git/not-master 0:ff7a2f2d8d70 git/master 0:ff7a2f2d8d70 bare/not-master 0:ff7a2f2d8d70 bare/master 0:ff7a2f2d8d70 also-git/not-master 0:ff7a2f2d8d70 also-git/master 0:ff7a2f2d8d70 also-bare/not-master 0:ff7a2f2d8d70 also-bare/master 0:ff7a2f2d8d70 $ hg up master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ echo beta > beta $ fn_hg_commit -A -m "add beta" $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg tags tip 1:47580592d3d6 git/master 1:47580592d3d6 bare/master 1:47580592d3d6 also-git/master 1:47580592d3d6 also-bare/master 1:47580592d3d6 git/not-master 0:ff7a2f2d8d70 bare/not-master 0:ff7a2f2d8d70 also-git/not-master 0:ff7a2f2d8d70 also-bare/not-master 0:ff7a2f2d8d70 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-octopus.t0000644000000000000000000001217614751647721013644 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git checkout -b branch1 2>&1 | sed s/\'/\"/g Switched to a new branch "branch1" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -b branch2 master 2>&1 | sed s/\'/\"/g Switched to a new branch "branch2" $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git checkout -b branch3 master 2>&1 | sed s/\'/\"/g Switched to a new branch "branch3" $ echo epsilon > epsilon $ git add epsilon $ fn_git_commit -m 'add epsilon' $ git checkout -b branch4 master 2>&1 | sed s/\'/\"/g Switched to a new branch "branch4" $ echo zeta > zeta $ git add zeta $ fn_git_commit -m 'add zeta' $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ git merge -m "Merge branches 'branch1' and 'branch2'" branch1 branch2 | sed "s/the '//;s/' strategy//" | sed 's/^Merge.*octopus.*$/Merge successful/;s/, 0 deletions.*//' | sed 's/| */| /' Trying simple merge with branch1 Trying simple merge with branch2 Merge successful beta | 1 + gamma | 1 + 2 files changed, 2 insertions(+) create mode 100644 beta create mode 100644 gamma $ git merge -m "Merge branches 'branch3' and 'branch4'" branch3 branch4 | sed "s/the '//;s/' strategy//" | sed 's/^Merge.*octopus.*$/Merge successful/;s/, 0 deletions.*//' | sed 's/| */| /' Trying simple merge with branch3 Trying simple merge with branch4 Merge successful epsilon | 1 + zeta | 1 + 2 files changed, 2 insertions(+) create mode 100644 epsilon create mode 100644 zeta $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 8 git commits new changesets ff7a2f2d8d70:307506d6ae8a (10 drafts) updating to bookmark master 6 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 9:7,8 307506d6ae8a 2007-01-01 00:00 +0000 test |\ Merge branches 'branch3' and 'branch4' | | | o 8:3,4 2b07220e422e 2007-01-01 00:00 +0000 test | |\ Merge branches 'branch3' and 'branch4' | | | o | | 7:5,6 ccf2d65d982c 2007-01-01 00:00 +0000 test |\ \ \ Merge branches 'branch1' and 'branch2' | | | | | o | | 6:1,2 690b40256117 2007-01-01 00:00 +0000 test | |\ \ \ Merge branches 'branch1' and 'branch2' | | | | | o | | | | 5:0 e459c0629ca4 2007-01-01 00:00 +0000 test | | | | | add delta | | | | | +-------o 4:0 e857c9a04474 2007-01-01 00:00 +0000 test | | | | add zeta | | | | +-----o 3:0 0071dec0de0e 2007-01-01 00:00 +0000 test | | | add epsilon | | | +---o 2:0 205a004356ef 2007-01-01 00:00 +0000 test | | add gamma | | | o 1 7fe02317c63d 2007-01-01 00:00 +0000 test |/ add beta | o 0 ff7a2f2d8d70 2007-01-01 00:00 +0000 test add alpha $ hg gverify -r 9 verifying rev 307506d6ae8a against git commit b32ff845df61df998206b630e4370a44f9b36845 $ hg gverify -r 8 abort: no git commit found for rev 2b07220e422e (if this is an octopus merge, verify against the last rev) [255] $ hg debug-remove-hggit-state clearing out the git cache data $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 8 commits with 8 trees and 6 blobs adding reference refs/heads/branch1 adding reference refs/heads/branch2 adding reference refs/heads/branch3 adding reference refs/heads/branch4 adding reference refs/heads/master $ cd .. $ git --git-dir=repo.git log --pretty=medium | sed s/\\.\\.\\.//g commit b32ff845df61df998206b630e4370a44f9b36845 Merge: 9ac68f9 7e9cd9f e695849 Author: test Date: Mon Jan 1 00:00:15 2007 +0000 Merge branches 'branch3' and 'branch4' commit 9ac68f982ae7426d9597ff16c74afb4e6053c582 Merge: d40f375 9497a4e e5023f9 Author: test Date: Mon Jan 1 00:00:15 2007 +0000 Merge branches 'branch1' and 'branch2' commit d40f375a81b7d033e92cbad89487115fe2dd472f Author: test Date: Mon Jan 1 00:00:15 2007 +0000 add delta commit e695849087f6c320c1a447620492b29a82ca41b1 Author: test Date: Mon Jan 1 00:00:14 2007 +0000 add zeta commit 7e9cd9f90b6d2c60579375eb796ce706d2d8bbe6 Author: test Date: Mon Jan 1 00:00:13 2007 +0000 add epsilon commit e5023f9e5cb24fdcec7b6c127cec45d8888e35a9 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add gamma commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-orphan-tags.t0000644000000000000000000000571414751647721014373 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" This test verifies that outgoing with orphaned annotated tags, and that actually pushing such a tag works. Initialize the bare repository $ mkdir repo.git $ cd repo.git $ git init -q --bare $ cd .. Populate the git repository $ git clone -q repo.git gitrepo warning: You appear to have cloned an empty repository. $ cd gitrepo $ touch foo1 $ git add foo1 $ fn_git_commit -m initial $ touch foo2 $ git add foo2 $ fn_git_commit -m "add foo2" Create a temporary branch and tag $ git checkout -qb the_branch $ touch foo3 $ git add foo3 $ fn_git_commit -m "add foo3" $ fn_git_tag the_tag -m "Tag message" $ git tag -ln the_tag Tag message $ git push --quiet --set-upstream origin the_branch Branch 'the_branch' set up to track remote branch 'the_branch' from 'origin'. (?) $ git push --tags To $TESTTMP/repo.git * [new tag] the_tag -> the_tag Continue the master branch $ git checkout -q master $ touch foo4 $ git add foo4 $ fn_git_commit -m "add foo4" $ git push To $TESTTMP/repo.git * [new branch] master -> master Delete the temporary branch $ git branch -D the_branch Deleted branch the_branch (was e128523). $ git push --delete origin the_branch To $TESTTMP/repo.git - [deleted] the_branch $ cd .. Create a Mercurial clone $ hg clone -U repo.git hgrepo importing 4 git commits new changesets b8e77484829b:387d03400596 (4 drafts) $ hg outgoing -R hgrepo comparing with $TESTTMP/repo.git searching for changes no changes found [1] $ hg push --debug -R hgrepo | grep -e reference -e found unchanged reference default::refs/heads/master => GIT:996e5084 unchanged reference default::refs/tags/the_tag => GIT:e4338156 no changes found Verify that we can push this tag, and that outgoing doesn't report them (#358) $ cd gitrepo $ git tag --delete the_tag Deleted tag 'the_tag' (was e433815) $ git push --delete origin the_tag To $TESTTMP/repo.git - [deleted] the_tag $ cd ../hgrepo $ hg outgoing comparing with $TESTTMP/repo.git searching for changes changeset: 2:7b35eb0afb3f tag: the_tag user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add foo3 $ hg push --debug pushing to $TESTTMP/repo.git finding unexported changesets saving git map to $TESTTMP/hgrepo/.hg/git-mapfile searching for changes remote: counting objects: 5, done. 1 commits found list of commits: e12852326ef72772e9696b008ad6546b5266ff13 adding objects remote: counting objects: 5, done. remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs unchanged reference default::refs/heads/master => GIT:996e5084 adding reference default::refs/tags/the_tag => GIT:e4338156 $ cd ../gitrepo $ git fetch From $TESTTMP/repo * [new tag] the_tag -> the_tag $ git tag -ln the_tag Tag message ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-outgoing.t0000644000000000000000000001067514751647721014005 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git branch alpha $ git show-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/alpha 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/master $ cd .. $ hg clone gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg book alpha 0:ff7a2f2d8d70 * master 0:ff7a2f2d8d70 $ hg update -q master $ echo beta > beta $ hg add beta $ fn_hg_commit -m 'add beta' $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -m 'add gamma' $ hg book -r 1 beta $ hg outgoing | grep -v 'searching for changes' comparing with $TESTTMP/gitrepo changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:953796e1cfd8 bookmark: master tag: tip user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg outgoing -r beta comparing with $TESTTMP/gitrepo searching for changes changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg outgoing -r master comparing with $TESTTMP/gitrepo searching for changes changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:953796e1cfd8 bookmark: master tag: tip user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ cd .. some more work on master from git $ cd gitrepo Check state of refs after outgoing $ git show-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/alpha 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/master $ git checkout master 2>&1 | sed s/\'/\"/g Already on "master" $ echo delta > delta $ git add delta $ fn_git_commit -m "add delta" $ cd .. $ cd hgrepo this will fail # maybe we should try to make it work $ hg outgoing comparing with $TESTTMP/gitrepo abort: branch 'refs/heads/master' changed on the server, please pull and merge before pushing [255] let's pull and try again $ hg pull pulling from */gitrepo (glob) importing 1 git commits not updating diverged bookmark master new changesets 25eed24f5e8f (1 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg log --graph o changeset: 3:25eed24f5e8f | tag: default/master | tag: tip | parent: 0:ff7a2f2d8d70 | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add delta | | @ changeset: 2:953796e1cfd8 | | bookmark: master | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: add gamma | | | o changeset: 1:47580592d3d6 |/ bookmark: beta | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 bookmark: alpha tag: default/alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ hg outgoing comparing with $TESTTMP/gitrepo searching for changes changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:953796e1cfd8 bookmark: master user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg outgoing -r beta comparing with $TESTTMP/gitrepo searching for changes changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg outgoing -r master comparing with $TESTTMP/gitrepo searching for changes changeset: 1:47580592d3d6 bookmark: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:953796e1cfd8 bookmark: master user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-phases-draft.t0000644000000000000000000000374114751647721014527 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git config receive.denyCurrentBranch ignore $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" cloning without hggit.usephases does not publish local changesets $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg phase -r master 0: draft pulling advances the draft phase, though $ hg phase -fs 0 $ hg pull pulling from $TESTTMP/gitrepo no changes found $ hg phase tip 0: draft even if we don't have a name? $ hg phase -fs 0 $ mv .hg/hgrc .hg/hgrc.bak $ hg pull ../gitrepo pulling from ../gitrepo no changes found $ hg phase tip 0: draft $ mv .hg/hgrc.bak .hg/hgrc $ cd .. pulling without hggit.usephases does not publish local changesets $ cd gitrepo $ git checkout -q master $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ cd hgrepo $ hg pull pulling from $TESTTMP/gitrepo importing 1 git commits updating bookmark master new changesets 7fe02317c63d (1 drafts) (run 'hg update' to get a working copy) $ hg phase -r master 1: draft pulling with git.public does not publish local changesets $ hg --config git.public=master pull pulling from $TESTTMP/gitrepo no changes found $ hg phase -r master 1: draft pushing without hggit.usephases does not publish local changesets $ hg update master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo gamma > gamma $ hg add gamma $ hg commit -m 'gamma' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg phase -r master 2: draft ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-phases-public.t0000644000000000000000000001052714751647721014705 0ustar00#testcases publish-defaults publish-specific Phases ====== This test verifies our behaviour with the ``hggit.usephases`` option. We run it in two modes: 1) The defaults, i.e. the remote HEAD. 2) Specificly set what should be published to correspond to the defaults. Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git config receive.denyCurrentBranch ignore $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ cd .. cloning with hggit.usephases publishes cloned HEAD $ hg --config hggit.usephases=True clone -U gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 $ cd hgrepo $ hg phase -r master 0: public $ cd .. pulled changesets are public $ cd gitrepo $ git checkout -q master $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -b not-master Switched to a new branch 'not-master' $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git tag thetag $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ git checkout master Switched to branch 'master' $ cd .. $ cd hgrepo $ cat >>$HGRCPATH < [paths] > other = $TESTTMP/gitrepo/.git > [hggit] > usephases = True > EOF $ hg phase -fd 'all()' we can restrict publishing to the remote HEAD and that tag, which happens to be the same thing here #if publish-specific $ cat >>$HGRCPATH < [git] > public = default/master, thetag > EOF #endif pulling publishes the branch $ hg phase -r master 0: draft #if publish-defaults $ hg pull -r master other pulling from $TESTTMP/gitrepo/.git importing 1 git commits updating bookmark master new changesets 7fe02317c63d 1 local changesets published (run 'hg update' to get a working copy) #else $ hg pull -r master other pulling from $TESTTMP/gitrepo/.git importing 1 git commits updating bookmark master new changesets 7fe02317c63d (1 drafts) (run 'hg update' to get a working copy) #endif $ hg phase -r master 1: draft (publish-specific !) 1: public (publish-defaults !) #if publish-defaults $ hg phase -fd master $ hg pull pulling from $TESTTMP/gitrepo importing 2 git commits adding bookmark not-master new changesets ca33a262eb46:03769a650ded (1 drafts) 1 local changesets published (run 'hg update' to get a working copy) #else $ hg pull pulling from $TESTTMP/gitrepo importing 2 git commits adding bookmark not-master new changesets ca33a262eb46:03769a650ded (1 drafts) 2 local changesets published (run 'hg update' to get a working copy) #endif $ hg phase -r master -r not-master -r thetag 1: public 3: draft 2: public public bookmark not pushed is not published after pull $ hg update 0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo delta > delta $ hg bookmark not-pushed $ hg add delta $ hg commit -m 'add delta' created new head $ hg phase -r 'all()' > $TESTTMP/before $ hg pull --config git.public=master,not-pushed pulling from $TESTTMP/gitrepo no changes found $ hg phase -r 'all()' > $TESTTMP/after $ cmp -s $TESTTMP/before $TESTTMP/after $ hg phase -r not-pushed 4: draft $ rm $TESTTMP/before $TESTTMP/after pushing public bookmark publishes local changesets, but only those actually pushed $ hg update master 1 files updated, 0 files merged, 1 files removed, 0 files unresolved (activating bookmark master) $ echo epsilon > epsilon $ hg add epsilon $ hg commit -m 'add epsilon' created new head $ hg phase -r 'all() - master' > $TESTTMP/before $ hg push -B not-pushed pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/not-pushed $ hg phase -r 'all() - master' > $TESTTMP/after $ diff $TESTTMP/before $TESTTMP/after | tr '<>' '-+' $ hg phase -r not-pushed -r master 4: draft 5: draft $ hg push -B master pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg phase -r 'all() - master' > $TESTTMP/after $ diff $TESTTMP/before $TESTTMP/after | tr '<>' '-+' $ hg phase -r master 5: public ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-phases-remote.t0000644000000000000000000001275214751647721014724 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git config receive.denyCurrentBranch ignore $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -q master $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -b not-master master~1 Switched to a new branch 'not-master' $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git checkout -qd master~1 $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ git tag thetag $ git checkout -q master $ cd .. $ hg clone --config hggit.usephases=True -U gitrepo hgrepo importing 4 git commits new changesets ff7a2f2d8d70:25eed24f5e8f (1 drafts) $ cd hgrepo $ hg log -G -T '{rev}|{phase}|{bookmarks}|{tags}\n' o 3|public||thetag tip | | o 2|draft|not-master|default/not-master |/ | o 1|public|master|default/master |/ o 0|public|| $ hg phase -r 'all()' | tee $TESTTMP/after-clone 0: public 1: public 2: draft 3: public $ cat >> .hg/hgrc < [paths] > other = $TESTTMP/gitrepo/.git > other:hg-git.publish = no > EOF $ cd .. that disables publishing from that remote $ cd hgrepo $ hg phase -fd 'all()' $ hg pull other pulling from $TESTTMP/gitrepo/.git no changes found $ hg log -qr 'public()' $ hg pull -v --config hggit.usephases=True other pulling from $TESTTMP/gitrepo/.git no changes found processing commits in batches of 1000 bookmark master is up-to-date bookmark not-master is up-to-date $ hg log -qr 'public()' $ cd .. but not default when enable by the global setting $ cd hgrepo $ hg phase -fd 'all()' no phases changed $ hg pull -v --config hggit.usephases=True pulling from $TESTTMP/gitrepo publishing remote HEAD publishing tag thetag no changes found processing commits in batches of 1000 bookmark master is up-to-date bookmark not-master is up-to-date publishing remote HEAD publishing tag thetag 3 local changesets published $ hg phase -r 'all()' > $TESTTMP/after-pull $ cmp $TESTTMP/after-clone $TESTTMP/after-pull $ cd .. or the path option $ cd hgrepo $ hg phase -fd 'all()' $ hg pull -v --config paths.default:hg-git.publish=yes pulling from $TESTTMP/gitrepo publishing remote HEAD publishing tag thetag no changes found processing commits in batches of 1000 bookmark master is up-to-date bookmark not-master is up-to-date publishing remote HEAD publishing tag thetag 3 local changesets published $ hg phase -r 'all()' > $TESTTMP/after-pull $ cmp $TESTTMP/after-clone $TESTTMP/after-pull $ cd .. but we can specify individual branches $ cd hgrepo $ hg phase -fd 'all()' $ hg pull -v --config paths.default:hg-git.publish=not-master pulling from $TESTTMP/gitrepo publishing branch not-master no changes found processing commits in batches of 1000 bookmark master is up-to-date bookmark not-master is up-to-date publishing branch not-master 2 local changesets published $ hg phase -r master -r not-master -r thetag 1: draft 2: public 3: draft $ cd .. and we can also specify the tag $ cd hgrepo $ hg phase -fd 'all()' $ hg pull -v --config paths.default:hg-git.publish=thetag pulling from $TESTTMP/gitrepo publishing tag thetag no changes found processing commits in batches of 1000 bookmark master is up-to-date bookmark not-master is up-to-date publishing tag thetag 2 local changesets published $ hg phase -r master -r not-master -r thetag 1: draft 2: draft 3: public $ cd .. Check multiple paths behavior ============================= $ cd hgrepo $ cat >> .hg/hgrc < [paths] > multi:multi-urls = yes > multi = path://other, path://default > recursive:multi-urls = yes > recursive = path://multi, default > EOF Using multiple path works fine: $ hg pull multi --config paths.default:hg-git.publish=yes abort: cannot use `path://multi`, "multi" is also defined as a `path://` [255] Recursive multiple path are tricker, but Mercurial don't work with them either. This test exist to make sure we bail out on our own. `yes` should abort (until we implement it) $ hg pull multi --config paths.default:hg-git.publish=yes abort: cannot use `path://multi`, "multi" is also defined as a `path://` [255] `some-value` should abort (until we implement it) $ hg pull multi --config paths.default:hg-git.publish=thetag abort: cannot use `path://multi`, "multi" is also defined as a `path://` [255] `no` is fine $ hg pull multi --config paths.default:hg-git.publish=no abort: cannot use `path://multi`, "multi" is also defined as a `path://` [255] $ cd .. Check conflicting paths behavior ================================ $ cd hgrepo $ cat > .hg/hgrc < [paths] > default = $TESTTMP/gitrepo > default:hg-git.publish = yes > also-default = $TESTTMP/gitrepo > EOF $ hg pull also-default pulling from $TESTTMP/gitrepo abort: different publishing configurations for the same remote location (conflicting paths: also-default, default) [255] $ hg pull --config paths.also-default:hg-git.publish=no pulling from $TESTTMP/gitrepo abort: different publishing configurations for the same remote location (conflicting paths: also-default, default) [255] $ hg pull --config paths.also-default:hg-git.publish=true pulling from $TESTTMP/gitrepo no changes found 1 local changesets published $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-pull-after-obsolete.t0000644000000000000000000000357314751647721016036 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [experimental] > evolution = all > EOF $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git tag thetag $ cd .. $ hg clone -U gitrepo hgrepo importing 2 git commits new changesets ff7a2f2d8d70:7fe02317c63d (2 drafts) $ cd hgrepo $ hg up master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ hg log --graph @ changeset: 1:7fe02317c63d | bookmark: master | tag: default/master | tag: thetag | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd ../gitrepo $ echo beta line 2 >> beta $ git add beta $ fn_git_commit -m 'add to beta' Create a commit, obsolete it, and pull, to ensure that we can pull if the tipmost commit is hidden. $ cd ../hgrepo $ hg bookmark --inactive $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -m 'add gamma' $ hg up master 0 files updated, 0 files merged, 1 files removed, 0 files unresolved (activating bookmark master) $ hg log -T '{rev}:{node} {desc}\n' -r tip 2:4090a1266584bc1a47ce562e9349b1e0f1b44611 add gamma $ hg debugobsolete 4090a1266584bc1a47ce562e9349b1e0f1b44611 1 new obsolescence markers obsoleted 1 changesets $ hg pull pulling from $TESTTMP/gitrepo importing 1 git commits updating bookmark master new changesets cc1e605d90db (1 drafts) (run 'hg update' to get a working copy) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-pull-after-rebase.t0000644000000000000000000002177314751647721015465 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [extensions] > rebase = > [experimental] > evolution = createmarkers > evolution.createmarkers = yes > [templates] > state = {bookmarks} {tags} {rev}:{node}\\n{desc}\\n > [alias] > state = log --graph --template state > EOF $ git init -q --bare repo.git $ git clone repo.git gitrepo > /dev/null 2>&1 $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout --quiet -b branch master~1 $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git push --all To $TESTTMP/repo.git * [new branch] branch -> branch * [new branch] master -> master $ cd .. Clone it and rebase the branch $ hg clone -U repo.git hgrepo importing 3 git commits new changesets ff7a2f2d8d70:205a004356ef (3 drafts) $ cd hgrepo $ hg state o branch default/branch tip 2:205a004356ef32b8da782afb89d9179d12ca31e9 | add gamma | o master default/master 1:7fe02317c63d9ee324d4b5df7c9296085162da1b |/ add beta o 0:ff7a2f2d8d7099694ae1e8b03838d40575bebb63 add alpha $ hg up branch 2 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark branch) $ hg rebase --quiet -d master $ hg state @ branch tip 3:52def9937d74e43b83dfded6ce0e5adf731b9d22 | add gamma | x default/branch 2:205a004356ef32b8da782afb89d9179d12ca31e9 | | add gamma o | master default/master 1:7fe02317c63d9ee324d4b5df7c9296085162da1b |/ add beta o 0:ff7a2f2d8d7099694ae1e8b03838d40575bebb63 add alpha $ hg push -fr tip pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/branch $ cd .. Now switch back to git and create a new commit based on what we just rebased $ cd gitrepo $ git checkout --quiet -b otherbranch branch $ git log --oneline --graph --all --decorate * e5023f9 (HEAD -> otherbranch, origin/branch, branch) add gamma | * 9497a4e (origin/master, master) add beta |/ * 7eeab2e add alpha $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ git push --quiet --set-upstream origin otherbranch Branch 'otherbranch' set up to track remote branch 'otherbranch' from 'origin'. (?) $ git log --oneline --graph --all --decorate * bba0011 (HEAD -> otherbranch, origin/otherbranch) add delta * e5023f9 (origin/branch, branch) add gamma | * 9497a4e (origin/master, master) add beta |/ * 7eeab2e add alpha $ cd .. Pull that $ cd hgrepo $ hg pull pulling from $TESTTMP/repo.git importing 1 git commits adding bookmark otherbranch 1 new orphan changesets new changesets 075302705298 (1 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg state * otherbranch default/otherbranch tip 4:0753027052980aef9c9c37adb7d76d5719e8d818 | add delta | @ branch default/branch 3:52def9937d74e43b83dfded6ce0e5adf731b9d22 | | add gamma x | 2:205a004356ef32b8da782afb89d9179d12ca31e9 | | add gamma | o master default/master 1:7fe02317c63d9ee324d4b5df7c9296085162da1b |/ add beta o 0:ff7a2f2d8d7099694ae1e8b03838d40575bebb63 add alpha $ cd .. To reproduce bug #386, do like github and save the old commit in a ref, and create a clone containing just the converted git commits: $ cd repo.git $ git update-ref refs/pr/1 otherbranch $ cd .. $ hg clone -U repo.git hgrepo-issue386 importing 5 git commits new changesets ff7a2f2d8d70:075302705298 (5 drafts) Now try rebasing that branch, from the Git side of things $ cd gitrepo $ git checkout -q otherbranch $ git log --oneline --graph --all --decorate * bba0011 (HEAD -> otherbranch, origin/otherbranch) add delta * e5023f9 (origin/branch, branch) add gamma | * 9497a4e (origin/master, master) add beta |/ * 7eeab2e add alpha $ fn_git_rebase --onto master branch otherbranch $ git log --oneline --graph --all --decorate * 9c58139 (HEAD -> otherbranch) add delta * 9497a4e (origin/master, master) add beta | * bba0011 (origin/otherbranch) add delta | * e5023f9 (origin/branch, branch) add gamma |/ * 7eeab2e add alpha $ git push -f To $TESTTMP/repo.git + bba0011...9c58139 otherbranch -> otherbranch (forced update) $ git log --oneline --graph --all --decorate * 9c58139 (HEAD -> otherbranch, origin/otherbranch) add delta * 9497a4e (origin/master, master) add beta | * e5023f9 (origin/branch, branch) add gamma |/ * 7eeab2e add alpha $ cd .. Now strip the old commit $ cd hgrepo-issue386 $ hg up null 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg id -qr otherbranch 075302705298 $ hg pull pulling from $TESTTMP/repo.git importing 1 git commits not updating diverged bookmark otherbranch new changesets d64bf0521af6 (1 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg debugstrip --hidden --no-backup otherbranch $ hg book -d otherbranch $ hg git-cleanup git commit map cleaned $ hg pull pulling from $TESTTMP/repo.git no changes found adding bookmark otherbranch $ cd .. And check that pulling something else doesn't delete that branch. $ cd hgrepo $ hg pull -r master pulling from $TESTTMP/repo.git no changes found $ cd .. A special case, is that we can pull into a repository, where a commit corresponding to the new branch exists, but that commit is obsolete. In order to avoid “pinning†the obsolete commit, and thereby making it visible, we first pull from Git as an unnamed remote. $ hg clone --config phases.publish=no hgrepo hgrepo-clone updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo-clone $ hg pull ../repo.git pulling from ../repo.git importing 4 git commits not updating diverged bookmark otherbranch new changesets d64bf0521af6 (1 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg debugobsolete d64bf0521af68fe2160791a1b4ab9baf282a3879 1 new obsolescence markers obsoleted 1 changesets $ cp ../hgrepo/.hg/hgrc .hg $ hg pull pulling from $TESTTMP/repo.git no changes found not updating diverged bookmark otherbranch $ cd .. $ rm -rf hgrepo-clone Another special case, is that we should update commits over obsolete boundaries: $ hg clone --config phases.publish=no hgrepo hgrepo-clone updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo-clone $ hg pull ../repo.git pulling from ../repo.git importing 4 git commits not updating diverged bookmark otherbranch new changesets d64bf0521af6 (1 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg debugobsolete 0753027052980aef9c9c37adb7d76d5719e8d818 d64bf0521af68fe2160791a1b4ab9baf282a3879 1 new obsolescence markers obsoleted 1 changesets $ hg book -r 075302705298 otherbranch $ cp ../hgrepo/.hg/hgrc .hg $ hg pull pulling from $TESTTMP/repo.git no changes found updating bookmark otherbranch $ cd .. $ rm -rf hgrepo-clone Now just pull it: $ cd hgrepo $ hg pull pulling from $TESTTMP/repo.git importing 1 git commits not updating diverged bookmark otherbranch new changesets d64bf0521af6 (1 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg state o default/otherbranch tip 5:d64bf0521af68fe2160791a1b4ab9baf282a3879 | add delta | * otherbranch 4:0753027052980aef9c9c37adb7d76d5719e8d818 | | add delta +---@ branch default/branch 3:52def9937d74e43b83dfded6ce0e5adf731b9d22 | | add gamma | x 2:205a004356ef32b8da782afb89d9179d12ca31e9 | | add gamma o | master default/master 1:7fe02317c63d9ee324d4b5df7c9296085162da1b |/ add beta o 0:ff7a2f2d8d7099694ae1e8b03838d40575bebb63 add alpha $ cd .. And finally, delete it: $ cd gitrepo $ git push origin :otherbranch To $TESTTMP/repo.git - [deleted] otherbranch $ cd .. And pull that: $ cd hgrepo $ hg pull pulling from $TESTTMP/repo.git no changes found not deleting diverged bookmark otherbranch $ hg state o tip 5:d64bf0521af68fe2160791a1b4ab9baf282a3879 | add delta | * otherbranch 4:0753027052980aef9c9c37adb7d76d5719e8d818 | | add delta +---@ branch default/branch 3:52def9937d74e43b83dfded6ce0e5adf731b9d22 | | add gamma | x 2:205a004356ef32b8da782afb89d9179d12ca31e9 | | add gamma o | master default/master 1:7fe02317c63d9ee324d4b5df7c9296085162da1b |/ add beta o 0:ff7a2f2d8d7099694ae1e8b03838d40575bebb63 add alpha $ cd .. We only get that message once: $ hg -R hgrepo pull pulling from $TESTTMP/repo.git no changes found Now try deleting one already gone locally, which shouldn't output anything: $ cd gitrepo $ git push origin :branch To $TESTTMP/repo.git - [deleted] branch $ cd ../hgrepo $ hg book -d branch $ hg pull pulling from $TESTTMP/repo.git no changes found $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-pull-after-strip.t0000644000000000000000000000475014751647721015361 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git tag thetag $ cd .. $ hg clone -U gitrepo hgrepo importing 2 git commits new changesets ff7a2f2d8d70:7fe02317c63d (2 drafts) $ cd hgrepo $ hg up master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ hg log --graph @ changeset: 1:7fe02317c63d | bookmark: master | tag: default/master | tag: thetag | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd ../gitrepo $ echo beta line 2 >> beta $ git add beta $ fn_git_commit -m 'add to beta' $ cd .. $ cd hgrepo $ hg debugstrip --no-backup tip 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg pull pulling from $TESTTMP/gitrepo importing 1 git commits abort: you appear to have run strip - please run hg git-cleanup [255] $ hg tags tip 0:ff7a2f2d8d70 $ hg git-cleanup git commit map cleaned pull works after 'hg git-cleanup' $ hg pull pulling from $TESTTMP/gitrepo importing 2 git commits updating bookmark master new changesets 7fe02317c63d:cc1e605d90db (2 drafts) (run 'hg update' to get a working copy) $ hg log --graph o changeset: 2:cc1e605d90db | bookmark: master | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add to beta | o changeset: 1:7fe02317c63d | tag: thetag | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. Check that we also remove bad refs: $ cd hgrepo $ echo e93b671cb24bff41779187edff99178e2597c2 > .hg/git/refs/tags/bad-tag $ hg git-cleanup git commit map cleaned $ test -e .hg/git/refs/tags/bad-tag [1] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-pull.t0000644000000000000000000003574214751647721013130 0ustar00#testcases secret draft Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [templates] > p = {rev}|{phase}|{bookmarks}|{tags}\n > EOF #if secret The phases setting should not affect hg-git $ cat >> $HGRCPATH < [phases] > new-commit = secret > EOF #endif set up a git repo with some commits, branches and a tag $ git init -q gitrepo $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag t_alpha $ git checkout -qb beta $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -qb delta master $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ cd .. pull without a name $ hg init hgrepo $ cd hgrepo $ hg pull ../gitrepo pulling from ../gitrepo importing 3 git commits adding bookmark beta adding bookmark delta adding bookmark master new changesets ff7a2f2d8d70:678ebee93e38 (3 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ git --git-dir .hg/git for-each-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/t_alpha $ hg log -Tp 2|draft|delta|tip 1|draft|beta| 0|draft|master|t_alpha $ cd .. $ rm -rf hgrepo pull with an implied name $ hg init hgrepo $ cd hgrepo $ echo "[paths]" >> .hg/hgrc $ echo "default=$TESTTMP/gitrepo" >> .hg/hgrc $ hg pull ../gitrepo pulling from ../gitrepo importing 3 git commits adding bookmark beta adding bookmark delta adding bookmark master new changesets ff7a2f2d8d70:678ebee93e38 (3 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ git --git-dir .hg/git for-each-ref 9497a4ee62e16ee641860d7677cdb2589ea15554 commit refs/remotes/default/beta 8cbeb817785fe2676ab0eda570534702b6b6f9cf commit refs/remotes/default/delta 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/t_alpha $ hg log -Tp 2|draft|delta|default/delta tip 1|draft|beta|default/beta 0|draft|master|default/master t_alpha $ cd .. $ rm -rf hgrepo pull with an explicit name $ hg init hgrepo $ cd hgrepo $ echo "[paths]" >> .hg/hgrc $ echo "default=$TESTTMP/gitrepo" >> .hg/hgrc $ hg pull pulling from $TESTTMP/gitrepo importing 3 git commits adding bookmark beta adding bookmark delta adding bookmark master new changesets ff7a2f2d8d70:678ebee93e38 (3 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ git --git-dir .hg/git for-each-ref 9497a4ee62e16ee641860d7677cdb2589ea15554 commit refs/remotes/default/beta 8cbeb817785fe2676ab0eda570534702b6b6f9cf commit refs/remotes/default/delta 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/remotes/default/master 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 commit refs/tags/t_alpha $ hg log -Tp 2|draft|delta|default/delta tip 1|draft|beta|default/beta 0|draft|master|default/master t_alpha $ cd .. $ rm -rf hgrepo pull a tag $ hg init hgrepo $ echo "[paths]" >> hgrepo/.hg/hgrc $ echo "default=$TESTTMP/gitrepo" >> hgrepo/.hg/hgrc $ hg -R hgrepo pull -r t_alpha pulling from $TESTTMP/gitrepo importing 1 git commits adding bookmark master new changesets ff7a2f2d8d70 (1 drafts) (run 'hg update' to get a working copy) $ hg -R hgrepo update t_alpha 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -Tp -R hgrepo 0|draft|master|default/master t_alpha tip no-op pull $ hg -R hgrepo pull -r t_alpha pulling from $TESTTMP/gitrepo no changes found no-op pull with added bookmark $ cd gitrepo $ git checkout -qb epsilon t_alpha $ cd .. $ hg -R hgrepo pull -r epsilon pulling from $TESTTMP/gitrepo no changes found adding bookmark epsilon pull something that doesn't exist $ hg -R hgrepo pull -r kaflaflibob pulling from $TESTTMP/gitrepo abort: unknown revision 'kaflaflibob'!? (re) [10] pull an ambiguous reference $ GIT_DIR=gitrepo/.git git branch t_alpha t_alpha $ hg -R hgrepo pull -r t_alpha pulling from $TESTTMP/gitrepo abort: ambiguous reference t_alpha: refs/heads/t_alpha, refs/tags/t_alpha!? (re) [10] $ GIT_DIR=gitrepo/.git git branch -qD t_alpha pull a branch $ hg -R hgrepo pull -r beta pulling from $TESTTMP/gitrepo importing 1 git commits adding bookmark beta new changesets 7fe02317c63d (1 drafts) (run 'hg update' to get a working copy) $ hg -R hgrepo log --graph --template=phases o changeset: 1:7fe02317c63d | bookmark: beta | tag: default/beta | tag: tip | phase: draft | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:ff7a2f2d8d70 bookmark: epsilon bookmark: master tag: default/epsilon tag: default/master tag: t_alpha phase: draft user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha no-op pull should affect phases $ hg -R hgrepo phase -fs beta $ hg -R hgrepo pull -r beta pulling from $TESTTMP/gitrepo no changes found $ hg -R hgrepo phase beta 1: draft add another commit and tag to the git repo $ cd gitrepo $ git checkout -q beta $ git tag t_beta $ git checkout -q master $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ cd .. pull everything else $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo importing 2 git commits adding bookmark delta updating bookmark master new changesets 678ebee93e38:6f898ad1f3e1 (2 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg -R hgrepo log --graph --template=phases o changeset: 3:6f898ad1f3e1 | bookmark: master | tag: default/master | tag: tip | phase: draft | parent: 0:ff7a2f2d8d70 | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add gamma | | o changeset: 2:678ebee93e38 |/ bookmark: delta | tag: default/delta | phase: draft | parent: 0:ff7a2f2d8d70 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add delta | | o changeset: 1:7fe02317c63d |/ bookmark: beta | tag: default/beta | tag: t_beta | phase: draft | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:ff7a2f2d8d70 bookmark: epsilon tag: default/epsilon tag: t_alpha phase: draft user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha add a merge to the git repo, and delete the branch $ cd gitrepo $ git merge -q -m "Merge branch 'beta'" beta $ git show --oneline 8642e88 Merge branch 'beta' $ git branch -d beta Deleted branch beta (was 9497a4e). $ cd .. pull the merge $ hg -R hgrepo tags | grep default/beta default/beta 1:7fe02317c63d $ hg -R hgrepo pull --config git.pull-prune-remote-branches=false pulling from $TESTTMP/gitrepo importing 1 git commits updating bookmark master deleting bookmark beta new changesets a02330f767a4 (1 drafts) (run 'hg update' to get a working copy) $ hg -R hgrepo tags | grep default/beta default/beta 1:7fe02317c63d $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo no changes found $ hg -R hgrepo tags | grep default/beta [1] $ hg -R hgrepo log --graph o changeset: 4:a02330f767a4 |\ bookmark: master | | tag: default/master | | tag: tip | | parent: 3:6f898ad1f3e1 | | parent: 1:7fe02317c63d | | user: test | | date: Mon Jan 01 00:00:13 2007 +0000 | | summary: Merge branch 'beta' | | | o changeset: 3:6f898ad1f3e1 | | parent: 0:ff7a2f2d8d70 | | user: test | | date: Mon Jan 01 00:00:13 2007 +0000 | | summary: add gamma | | | | o changeset: 2:678ebee93e38 | |/ bookmark: delta | | tag: default/delta | | parent: 0:ff7a2f2d8d70 | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: add delta | | o | changeset: 1:7fe02317c63d |/ tag: t_beta | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:ff7a2f2d8d70 bookmark: epsilon tag: default/epsilon tag: t_alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha pull with wildcards $ cd gitrepo $ git checkout -qb releases/v1 master $ echo zeta > zeta $ git add zeta $ fn_git_commit -m 'add zeta' $ git checkout -qb releases/v2 master $ echo eta > eta $ git add eta $ fn_git_commit -m 'add eta' $ git checkout -qb notreleases/v1 master $ echo theta > theta $ git add theta $ fn_git_commit -m 'add theta' ensure that releases/v1 and releases/v2 are pulled but not notreleases/v1 $ cd .. $ hg -R hgrepo pull -r 'releases/*' pulling from $TESTTMP/gitrepo importing 2 git commits adding bookmark releases/v1 adding bookmark releases/v2 new changesets 218b2d0660d3:a3f95e150b0a (2 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg -R hgrepo log --graph o changeset: 6:a3f95e150b0a | bookmark: releases/v2 | tag: default/releases/v2 | tag: tip | parent: 4:a02330f767a4 | user: test | date: Mon Jan 01 00:00:15 2007 +0000 | summary: add eta | | o changeset: 5:218b2d0660d3 |/ bookmark: releases/v1 | tag: default/releases/v1 | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: add zeta | o changeset: 4:a02330f767a4 |\ bookmark: master | | tag: default/master | | parent: 3:6f898ad1f3e1 | | parent: 1:7fe02317c63d | | user: test | | date: Mon Jan 01 00:00:13 2007 +0000 | | summary: Merge branch 'beta' | | | o changeset: 3:6f898ad1f3e1 | | parent: 0:ff7a2f2d8d70 | | user: test | | date: Mon Jan 01 00:00:13 2007 +0000 | | summary: add gamma | | | | o changeset: 2:678ebee93e38 | |/ bookmark: delta | | tag: default/delta | | parent: 0:ff7a2f2d8d70 | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: add delta | | o | changeset: 1:7fe02317c63d |/ tag: t_beta | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:ff7a2f2d8d70 bookmark: epsilon tag: default/epsilon tag: t_alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha add old and new commits to the git repo -- make sure we're using the commit date and not the author date $ cat >> $HGRCPATH < [git] > mindate = 2014-01-02 00:00:00 +0000 > EOF $ cd gitrepo $ git checkout -q master $ echo oldcommit > oldcommit $ git add oldcommit $ GIT_AUTHOR_DATE="2014-03-01 00:00:00 +0000" \ > GIT_COMMITTER_DATE="2009-01-01 00:00:00 +0000" \ > git commit -m oldcommit > /dev/null || echo "git commit error" also add an annotated tag $ git checkout -q master^ $ echo oldtag > oldtag $ git add oldtag $ GIT_AUTHOR_DATE="2014-03-01 00:00:00 +0000" \ > GIT_COMMITTER_DATE="2009-01-01 00:00:00 +0000" \ > git commit -m oldtag > /dev/null || echo "git commit error" $ GIT_COMMITTER_DATE="2009-02-01 00:00:00 +0000" \ > git tag -a -m 'tagging oldtag' oldtag $ cd .. Master is now filtered, so it's just stays there: $ hg -R hgrepo pull --config git.pull-prune-bookmarks=no pulling from $TESTTMP/gitrepo no changes found $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo no changes found $ hg -R hgrepo log -r master changeset: 4:a02330f767a4 bookmark: master tag: default/master parent: 3:6f898ad1f3e1 parent: 1:7fe02317c63d user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: Merge branch 'beta' $ cd gitrepo $ git checkout -q master $ echo newcommit > newcommit $ git add newcommit $ GIT_AUTHOR_DATE="2014-01-01 00:00:00 +0000" \ > GIT_COMMITTER_DATE="2014-01-02 00:00:00 +0000" \ > git commit -m newcommit > /dev/null || echo "git commit error" $ git checkout -q refs/tags/oldtag $ GIT_COMMITTER_DATE="2014-01-02 00:00:00 +0000" \ > git tag -a -m 'tagging newtag' newtag $ cd .. $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo importing 3 git commits updating bookmark master new changesets 49713da8f665:e103a73f33be (3 drafts) (run 'hg heads .' to see heads, 'hg merge' to merge) $ hg -R hgrepo heads changeset: 9:e103a73f33be bookmark: master tag: default/master tag: tip user: test date: Wed Jan 01 00:00:00 2014 +0000 summary: newcommit changeset: 7:49713da8f665 tag: newtag tag: oldtag parent: 4:a02330f767a4 user: test date: Sat Mar 01 00:00:00 2014 +0000 summary: oldtag changeset: 6:a3f95e150b0a bookmark: releases/v2 tag: default/releases/v2 parent: 4:a02330f767a4 user: test date: Mon Jan 01 00:00:15 2007 +0000 summary: add eta changeset: 5:218b2d0660d3 bookmark: releases/v1 tag: default/releases/v1 user: test date: Mon Jan 01 00:00:14 2007 +0000 summary: add zeta changeset: 2:678ebee93e38 bookmark: delta tag: default/delta parent: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add delta test for ssh vulnerability $ cat >> $HGRCPATH << EOF > [ui] > ssh = ssh -o ConnectTimeout=1 > EOF $ hg init a $ cd a $ hg pull -q 'git+ssh://-oProxyCommand=rm${IFS}nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm${IFS}nonexistent' [255] $ hg pull -q 'git+ssh://-oProxyCommand=rm%20nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm nonexistent' [255] $ hg pull -q 'git+ssh://fakehost|shellcommand/path' ssh: * fakehost%7?shellcommand* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] $ hg pull -q 'git+ssh://fakehost%7Cshellcommand/path' ssh: * fakehost%7?shellcommand* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-anonymous.t0000644000000000000000000000236114751647721015150 0ustar00Pushing to Git ============== Anonymous HEAD -------------- Git does not allow anonymous heads, so what happens if you try to push one? Well, you get nothing, since we only push bookmarks, but at we should inform the user of that. Load commonly used test logic $ . "$TESTDIR/testutil" Create a Git repository with a commit in it $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -d master HEAD is now at 7eeab2e add alpha $ cd .. Clone it, deactivate the bookmark, add a commit, and push! $ hg clone -U gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) $ cd hgrepo $ hg up tip 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo beta > beta $ hg add beta $ fn_hg_commit -m "add beta" Pushing that changeset should print a helpful message: $ hg push pushing to $TESTTMP/gitrepo searching for changes no changes found (ignoring 1 changesets without bookmarks or tags) [1] But what about untagged, but secret changesets? $ hg phase -fs tip $ hg push pushing to $TESTTMP/gitrepo searching for changes no changes found [1] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-authors.t0000644000000000000000000000633114751647721014606 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" Create a Git repository $ git init -q --bare repo.git Create a Mercurial repository $ hg clone repo.git hgrepo updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg book master Configure an author map $ touch authors.txt $ cat >> $HGRCPATH < [git] > authors = $TESTTMP/authors.txt > EOF Create a commit user that maps to a fully valid user $ cat >> $TESTTMP/authors.txt < user1 = User no. 1 > EOF $ touch alpha $ hg add alpha $ fn_hg_commit -m alpha -u user1 And one that maps to an email address $ cat >> $TESTTMP/authors.txt < user2@example.com = user2 > EOF $ touch beta $ hg add beta $ fn_hg_commit -m beta -u user2@example.com And one that maps to a "simple" user $ cat >> $TESTTMP/authors.txt < User #3 = user3@example.com > EOF $ touch gamma $ hg add gamma $ fn_hg_commit -m gamma -u "User #3 " And one that maps to nothing $ cat >> $TESTTMP/authors.txt < user4 = > EOF $ touch delta $ hg add delta $ fn_hg_commit -m delta -u user4 And one that doesn't map $ touch epsilon $ hg add epsilon $ fn_hg_commit -m epsilon -u "User #5 " Check the test default $ touch zeta $ hg add zeta $ fn_hg_commit -m zeta Push it! $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 6 commits with 6 trees and 1 blobs adding reference refs/heads/master Check the results: $ hg log --template='Commit: {gitnode}\nAuthor: {author}\n---\n' Commit: 869e310765d5d7ad92f83bf036e12b0341922a65 Author: test --- Commit: b5c0fcb75f876b158ece64859400d36b07570ce9 Author: User #5 --- Commit: 2833824a870810915f7a7a27c05cccad0448bfd7 Author: user4 --- Commit: fe63bf29ef0bd4af50e85b8aec8d2fbeff255845 Author: User #3 --- Commit: eba936dd13172a2f17936785e3604845aed9170d Author: user2@example.com --- Commit: 796162e5747a7ba57f31fb828b88319caf7b1f7b Author: user1 --- $ cd ../repo.git $ cat $TESTTMP/authors.txt user1 = User no. 1 user2@example.com = user2 User #3 = user3@example.com user4 = $ git log --pretty='tformat:Commit: %H%nAuthor: %an <%ae>%nCommitter: %cn <%ce>%n---' Commit: 869e310765d5d7ad92f83bf036e12b0341922a65 Author: test Committer: test --- Commit: b5c0fcb75f876b158ece64859400d36b07570ce9 Author: User #5 Committer: User #5 --- Commit: 2833824a870810915f7a7a27c05cccad0448bfd7 Author: Committer: --- Commit: fe63bf29ef0bd4af50e85b8aec8d2fbeff255845 Author: user3@example.com Committer: user3@example.com --- Commit: eba936dd13172a2f17936785e3604845aed9170d Author: user2 Committer: user2 --- Commit: 796162e5747a7ba57f31fb828b88319caf7b1f7b Author: User no. 1 Committer: User no. 1 --- $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-detached.t0000644000000000000000000000444414751647721014665 0ustar00Pushing to Git ============== Detached HEAD ------------- Most remote Git repositories reside on a hosting service, such as Github or GitLab, and have HEAD pointing to the default branch, usually `master` or `main`. Local repositories, can end up in a detached state, where HEAD rather than being a symref pointing to another ref, is a direct ref pointing to a commit. This test excercises that specific edge case: Pushing to a repository with a detached head. With publishing-on-push, there are two possible failure modes we want to prevent. 1) Did we assume that HEAD is a symref? 2) What happens when you push to a descendent of HEAD, but HEAD is draft? Load commonly used test logic $ . "$TESTDIR/testutil" Create a Git repository with a detached head $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -d master HEAD is now at 7eeab2e add alpha $ cd .. Verify that we can push to a Git repository that has a detached HEAD With detection of HEAD on push, it is easy to implicitly assume that HEAD is a symref. To prevent this, we specifically verify that pushing in this case continues to work. $ hg clone gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ echo beta > beta $ hg add beta $ fn_hg_commit -m "add beta" Pushing that changeset, with phases, publishes the detached HEAD. Whether this should happen is debatable, but it's a side effect from the fact that pushing to the remote HEAD, with HEAD being the usual symref, should publish it. $ hg push -v --config hggit.usephases=yes pushing to $TESTTMP/gitrepo finding unexported changesets exporting 1 changesets converting revision 47580592d3d6492421a1e6cebc5c2d701a2e858b packing 3 loose objects... searching for changes remote: counting objects: 5, done. 1 commits found adding objects remote: counting objects: 5, done. remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference default::refs/heads/master => GIT:0f378ab6 publishing remote HEAD $ hg phase 'all()' 0: public 1: draft $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-missing-commit.t0000644000000000000000000000533014751647721016056 0ustar00This test checks our behaviour when commits “disappear†from Git. In particular, we do the converstion incrementally, and assume that the Git commit corresponding to parents of exported commits actually exists in the Git repository. But what happens if it doesn't? Load commonly used test logic $ . "$TESTDIR/testutil" set up a git repo with one commit $ git init -q gitrepo $ cd gitrepo $ echo something >> thefile $ git add thefile $ fn_git_commit -m 'add thefile' $ cd .. push it to a bare repository so that we can safely push to it afterwards $ git clone --bare --quiet gitrepo repo.git clone it and create a commit building on the git history $ hg clone -U repo.git hgrepo importing 1 git commits new changesets fb68c5a534ce (1 drafts) $ cd hgrepo $ hg up -q master $ echo other > thefile $ fn_hg_commit -m 'change thefile' $ cd .. now remove the git commit from the cache repository used internally by hg-git — actually, changing `git.intree` is equivalent to this, and how a user noticed it in #376. $ rm -rf hgrepo/.hg/git what happens when we push it? $ hg -R hgrepo push pushing to $TESTTMP/repo.git warning: created new git repository at $TESTTMP/hgrepo/.hg/git abort: cannot push git commit 533d4e670a8b as it is not present locally (please try pulling first, or as a fallback run git-cleanup to re-export the missing commits) [255] try to follow the hint: (and just to see that the warning is useful, try re-resetting first) $ rm -rf hgrepo/.hg/git hgrepo/.git $ hg -R hgrepo pull pulling from $TESTTMP/repo.git warning: created new git repository at $TESTTMP/hgrepo/.hg/git no changes found not updating diverged bookmark master $ hg -R hgrepo push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master and as an extra test, what if we want to push a commit that's converted, but gone? simply pushing doesn't suffice: $ cd hgrepo $ rm -rf .hg/git $ hg push pushing to $TESTTMP/repo.git warning: created new git repository at $TESTTMP/hgrepo/.hg/git searching for changes no changes found [1] $ cd .. but we can't create another commit building on the git history, export it, and push: $ cd hgrepo $ echo not that > thefile $ fn_hg_commit -m 'change thefile again' $ hg gexport $ rm -rf .hg/git $ hg push pushing to $TESTTMP/repo.git warning: created new git repository at $TESTTMP/hgrepo/.hg/git searching for changes abort: cannot push git commit 61619410916a as it is not present locally (please try pulling first, or as a fallback run git-cleanup to re-export the missing commits) [255] $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-phases.t0000644000000000000000000000346314751647721014407 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [hggit] > usephases = yes > EOF $ git init -q --bare repo.git $ hg clone repo.git hgrepo updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo Create two commits, one secret: $ touch alpha $ hg add alpha $ fn_hg_commit -m alpha $ hg book -r . master $ touch beta $ hg add beta $ fn_hg_commit --secret -m beta $ hg book -r . secret $ hg push pushing to $TESTTMP/repo.git warning: not exporting secret bookmark 'secret' searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/master $ cd .. $ hg -R hgrepo log --graph --template phases @ changeset: 1:62966756ea96 | bookmark: secret | tag: tip | phase: secret | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: beta | o changeset: 0:d4b83afc35d1 bookmark: master tag: default/master phase: public user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: alpha What happens when we push the secret? $ hg -R hgrepo push -B secret pushing to $TESTTMP/repo.git warning: not exporting secret bookmark 'secret' searching for changes abort: revision 62966756ea96 cannot be pushed since it doesn't have a bookmark [255] Only one changeset was pushed: $ GIT_DIR=repo.git git log --graph --all --decorate=short * commit 2cc4e3d19551e459a0dd606f4cf890de571c7d33 (HEAD -> master) Author: test Date: Mon Jan 1 00:00:10 2007 +0000 alpha And this published the remote head: $ hg -R hgrepo phase 'all()' 0: public 1: secret ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-r.t0000644000000000000000000001077014751647721013364 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init test $ cd test $ cat >>afile < 0 > EOF $ hg add afile $ fn_hg_commit -m "0.0" $ cat >>afile < 1 > EOF $ fn_hg_commit -m "0.1" $ cat >>afile < 2 > EOF $ fn_hg_commit -m "0.2" $ cat >>afile < 3 > EOF $ fn_hg_commit -m "0.3" $ hg update -C 0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cat >>afile < 1 > EOF $ fn_hg_commit -m "1.1" $ cat >>afile < 2 > EOF $ fn_hg_commit -m "1.2" $ cat >fred < a line > EOF $ cat >>afile < 3 > EOF $ hg add fred $ fn_hg_commit -m "1.3" $ hg mv afile adifferentfile $ fn_hg_commit -m "1.3m" $ hg update -C 3 1 files updated, 0 files merged, 2 files removed, 0 files unresolved $ hg mv afile anotherfile $ fn_hg_commit -m "0.3m" $ cd .. $ for i in 0 1 2 3 4 5 6 7 8; do > mkdir test-"$i" > hg --cwd test-"$i" init > hg -R test push -r "$i" test-"$i" > cd test-"$i" > hg verify > cd .. > done pushing to test-0 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 1 changesets with 1 changes to 1 files pushing to test-1 searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 2 changesets with 2 changes to 1 files pushing to test-2 searching for changes adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 3 changesets with 3 changes to 1 files pushing to test-3 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 4 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 4 changesets with 4 changes to 1 files pushing to test-4 searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 2 changesets with 2 changes to 1 files pushing to test-5 searching for changes adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 3 changesets with 3 changes to 1 files pushing to test-6 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 5 changes to 2 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 4 changesets with 5 changes to 2 files pushing to test-7 searching for changes adding changesets adding manifests adding file changes added 5 changesets with 6 changes to 3 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 5 changesets with 6 changes to 3 files pushing to test-8 searching for changes adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 2 files checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 5 changesets with 5 changes to 2 files $ cd test-8 $ hg pull ../test-7 pulling from ../test-7 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 2 changes to 3 files (+1 heads) new changesets c29287bce33f:e70c8671c3d4 (?) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg verify checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 9 changesets with 7 changes to 4 files ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push-to-head.t0000644000000000000000000000600614751647721014441 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" Create a Git repository with a single, checked out commit in master: $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ cd .. Clone it, and push back to master: $ hg clone gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to bookmark master 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ echo beta > beta $ fn_hg_commit -A -m "add beta" The output is confusing, and this even more-so: $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse remote: error: refusing to update checked out branch: refs/heads/master remote: error: By default, updating the current branch in a non-bare repository remote: is denied, because it will make the index and work tree inconsistent remote: with what you pushed, and will require 'git reset --hard' to match remote: the work tree to HEAD. remote: remote: You can set the 'receive.denyCurrentBranch' configuration variable remote: to 'ignore' or 'warn' in the remote repository to allow pushing into remote: its current branch; however, this is not recommended unless you remote: arranged to update its work tree to match what you pushed in some remote: other way. remote: remote: To squelch this message and still keep the default behaviour, set remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'. added 1 commits with 1 trees and 1 blobs warning: failed to update refs/heads/master; branch is currently checked out $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse remote: error: refusing to update checked out branch: refs/heads/master remote: error: By default, updating the current branch in a non-bare repository remote: is denied, because it will make the index and work tree inconsistent remote: with what you pushed, and will require 'git reset --hard' to match remote: the work tree to HEAD. remote: remote: You can set the 'receive.denyCurrentBranch' configuration variable remote: to 'ignore' or 'warn' in the remote repository to allow pushing into remote: its current branch; however, this is not recommended unless you remote: arranged to update its work tree to match what you pushed in some remote: other way. remote: remote: To squelch this message and still keep the default behaviour, set remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'. added 1 commits with 1 trees and 1 blobs warning: failed to update refs/heads/master; branch is currently checked out Show that it really didn't get pushed: $ hg tags tip 1:47580592d3d6 default/master 0:ff7a2f2d8d70 $ cd ../gitrepo $ git log --all --oneline --decorate 7eeab2e (HEAD -> master) add alpha ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-push.t0000644000000000000000000002102214751647721013115 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master 2>&1 | sed s/\'/\"/g Switched to a new branch "not-master" $ cd .. $ hg clone -u tip gitrepo hgrepo importing 1 git commits new changesets ff7a2f2d8d70 (1 drafts) updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg bookmark -q master $ echo beta > beta $ hg add beta $ fn_hg_commit -m 'add beta' $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -m 'add gamma' $ hg book -r 1 beta $ hg push -r beta pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/beta $ cd .. should have two different branches $ cd gitrepo $ git branch -v beta 0f378ab add beta master 7eeab2e add alpha * not-master 7eeab2e add alpha some more work on master from git $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo delta > delta $ git add delta $ fn_git_commit -m "add delta" $ git checkout not-master 2>&1 | sed s/\'/\"/g Switched to branch "not-master" $ cd .. $ cd hgrepo this should fail $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes abort: branch 'refs/heads/master' changed on the server, please pull and merge before pushing [255] ... even with -f $ hg push -fr master pushing to $TESTTMP/gitrepo searching for changes abort: branch 'refs/heads/master' changed on the server, please pull and merge before pushing [255] $ hg pull 2>&1 | grep -v 'divergent bookmark' pulling from $TESTTMP/gitrepo importing 1 git commits not updating diverged bookmark master new changesets 25eed24f5e8f (1 drafts) (run 'hg heads' to see heads, 'hg merge' to merge) TODO shouldn't need to do this since we're (in theory) pushing master explicitly, which should not implicitly also push the not-master ref. $ hg book not-master -r default/not-master --force master and default/master should be diferent $ hg log -r master changeset: 2:953796e1cfd8 bookmark: master user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg log -r default/master changeset: 3:25eed24f5e8f tag: default/master tag: tip parent: 0:ff7a2f2d8d70 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add delta this should also fail $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes abort: pushing refs/heads/master overwrites 953796e1cfd8 [255] ... but succeed with -f $ hg push -fr master pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master this should fail, no changes to push $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes no changes found [1] hg-git issue103 -- directories can lose information at hg-git export time $ hg up master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ mkdir dir1 $ echo alpha > dir1/alpha $ hg add dir1/alpha $ fn_hg_commit -m 'add dir1/alpha' $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 2 trees and 0 blobs updating reference refs/heads/master $ echo beta > dir1/beta $ hg add dir1/beta $ fn_hg_commit -m 'add dir1/beta' $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 2 trees and 0 blobs updating reference refs/heads/master $ hg log -r master changeset: 5:ba0476ff1899 bookmark: master tag: default/master tag: tip user: test date: Mon Jan 01 00:00:15 2007 +0000 summary: add dir1/beta $ cat >> .hg/hgrc << EOF > [paths] > default:pushurl = file:///$TESTTMP/gitrepo > EOF NB: the triple slashes are intentional, due to windows $ hg push -r master pushing to file:///$TESTTMP/gitrepo searching for changes no changes found [1] $ cd .. $ hg clone -u tip gitrepo hgrepo-test importing 5 git commits new changesets ff7a2f2d8d70:ba0476ff1899 (5 drafts) updating to branch default 5 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-test log -r master changeset: 4:ba0476ff1899 bookmark: master tag: default/master tag: tip user: test date: Mon Jan 01 00:00:15 2007 +0000 summary: add dir1/beta $ hg tags -R hgrepo-test | grep ^default/ default/master 4:ba0476ff1899 default/beta 1:47580592d3d6 default/not-master 0:ff7a2f2d8d70 Push a fast-forward to a currently checked out branch, which sometimes fails: $ cd hgrepo $ hg book -r master not-master moving bookmark 'not-master' forward from ff7a2f2d8d70 $ hg push pushing to file:///$TESTTMP/gitrepo searching for changes warning: failed to update HEAD; unable to set b'HEAD' to b'7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03' (?) updating reference refs/heads/not-master That should have updated the tag: $ hg tags | grep ^default/ default/not-master 5:ba0476ff1899 default/master 5:ba0476ff1899 default/beta 1:47580592d3d6 $ cd .. We can push only one of two bookmarks on the same revision: $ cd hgrepo $ hg book -r 0 also-not-master really-not-master $ hg push -B also-not-master pushing to file:///$TESTTMP/gitrepo searching for changes adding reference refs/heads/also-not-master We can also push another bookmark to a path with another revision specified: $ hg book -r 3 also-not-master moving bookmark 'also-not-master' forward from ff7a2f2d8d70 $ hg push -B also-not-master "file:///$TESTTMP/gitrepo#master" pushing to file:///$TESTTMP/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/also-not-master And we can delete them again afterwards: $ hg book -d also-not-master really-not-master $ hg push -B also-not-master -B really-not-master pushing to file:///$TESTTMP/gitrepo searching for changes warning: unable to delete 'refs/heads/really-not-master' as it does not exist on the remote repository deleting reference refs/heads/also-not-master Push empty Hg repo to empty Git repo (issue #58) $ hg init hgrepo2 $ git init -q --bare repo.git $ hg -R hgrepo2 push repo.git pushing to repo.git searching for changes abort: no bookmarks or tags to push to git (see "hg help bookmarks" for details on creating them) [255] The remote repo is empty and the local one doesn't have any bookmarks/tags $ cd hgrepo2 $ echo init >> test.txt $ hg addremove adding test.txt $ fn_hg_commit -m init $ hg update null 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg push ../repo.git pushing to ../repo.git searching for changes abort: no bookmarks or tags to push to git (see "hg help bookmarks" for details on creating them) [255] $ hg summary parent: -1:000000000000 (no revision checked out) branch: default commit: (clean) update: 1 new changesets (update) phases: 1 draft That should not create any bookmarks $ hg bookmarks no bookmarks set And no tags for the remotes either: $ hg tags tip 0:8aded40be5af test for ssh vulnerability $ cat >> $HGRCPATH << EOF > [ui] > ssh = ssh -o ConnectTimeout=1 > EOF $ hg push -q 'git+ssh://-oProxyCommand=rm${IFS}nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm${IFS}nonexistent' [255] $ hg push -q 'git+ssh://-oProxyCommand=rm%20nonexistent/path' abort: potentially unsafe hostname: '-oProxyCommand=rm nonexistent' [255] $ hg push -q 'git+ssh://fakehost|rm%20nonexistent/path' ssh: * fakehost%7?rm%20nonexistent* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] $ hg push -q 'git+ssh://fakehost%7Crm%20nonexistent/path' ssh: * fakehost%7?rm%20nonexistent* (glob) abort: git remote error: The remote server unexpectedly closed the connection. [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-renames.t0000644000000000000000000003473714751647721013611 0ustar00Test that rename detection works $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [diff] > git = True > [git] > similarity = 50 > EOF $ git init -q gitrepo $ cd gitrepo $ for i in 1 2 3 4 5 6 7 8 9 10; do echo $i >> alpha; done $ git add alpha $ fn_git_commit -malpha Rename a file $ git mv alpha beta $ echo 11 >> beta $ git add beta $ fn_git_commit -mbeta Copy a file $ cp beta gamma $ echo 12 >> beta $ echo 13 >> gamma $ git add beta gamma $ fn_git_commit -mgamma Add a submodule (gitlink) and move it to a different spot: $ cd .. $ git init -q gitsubmodule $ cd gitsubmodule $ touch subalpha $ git add subalpha $ fn_git_commit -msubalpha $ cd ../gitrepo $ rmpwd="import sys; print(sys.stdin.read().replace('$(dirname $(pwd))/', ''))" $ clonefilt='s/Cloning into/Initialized empty Git repository in/;s/in .*/in .../' $ git submodule add ../gitsubmodule 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ fn_git_commit -m 'add submodule' $ sed -e 's/path = gitsubmodule/path = gitsubmodule2/' .gitmodules > .gitmodules-new $ mv .gitmodules-new .gitmodules $ mv gitsubmodule gitsubmodule2 Previous versions of git did not produce any output but 2.14 changed the output to warn the user about submodules $ git add .gitmodules gitsubmodule2 2>/dev/null $ git rm --cached gitsubmodule rm 'gitsubmodule' $ fn_git_commit -m 'move submodule' Rename a file elsewhere and replace it with a symlink: $ git mv beta beta-new $ ln -s beta-new beta $ git add beta $ fn_git_commit -m 'beta renamed' Rename the file back: $ git rm beta rm 'beta' $ git mv beta-new beta $ fn_git_commit -m 'beta renamed back' Rename a file elsewhere and replace it with a submodule: $ git mv gamma gamma-new $ git submodule add ../gitsubmodule gamma 2>&1 | python -c "$rmpwd" | sed "$clonefilt" | grep -E -v '^done\.$' Initialized empty Git repository in ... $ fn_git_commit -m 'rename and add submodule' Remove the submodule and rename the file back: $ grep 'submodule "gitsubmodule"' -A2 .gitmodules > .gitmodules-new $ mv .gitmodules-new .gitmodules $ git add .gitmodules $ git rm --cached gamma rm 'gamma' $ rm -rf gamma $ git mv gamma-new gamma $ fn_git_commit -m 'remove submodule and rename back' $ git init -q --bare ../repo.git $ git push ../repo.git master To ../repo.git * [new branch] master -> master $ cd .. $ hg clone -q repo.git hgrepo $ cd hgrepo $ hg book master -q $ hg log -p --graph --template "{rev} {node} {desc|firstline}\n{join(extras, ' ')}\n\n" @ 8 497105ddbe119aa40af691eb2b1a029c29bf5247 remove submodule and rename back | branch=default hg-git-rename-source=git | | diff --git a/.hgsub b/.hgsub | --- a/.hgsub | +++ b/.hgsub | @@ -1,2 +1,1 @@ | gitsubmodule2 = [git]../gitsubmodule | -gamma = [git]../gitsubmodule | diff --git a/.hgsubstate b/.hgsubstate | --- a/.hgsubstate | +++ b/.hgsubstate | @@ -1,2 +1,1 @@ | -5944b31ff85b415573d1a43eb942e2dea30ab8be gamma | 5944b31ff85b415573d1a43eb942e2dea30ab8be gitsubmodule2 | diff --git a/gamma-new b/gamma | rename from gamma-new | rename to gamma | o 7 adfc1ce8461d3174dcf8425e112e2fa848de3913 rename and add submodule | branch=default hg-git-rename-source=git | | diff --git a/.hgsub b/.hgsub | --- a/.hgsub | +++ b/.hgsub | @@ -1,1 +1,2 @@ | gitsubmodule2 = [git]../gitsubmodule | +gamma = [git]../gitsubmodule | diff --git a/.hgsubstate b/.hgsubstate | --- a/.hgsubstate | +++ b/.hgsubstate | @@ -1,1 +1,2 @@ | +5944b31ff85b415573d1a43eb942e2dea30ab8be gamma | 5944b31ff85b415573d1a43eb942e2dea30ab8be gitsubmodule2 | diff --git a/gamma b/gamma-new | rename from gamma | rename to gamma-new | o 6 62c1a4b07240b53a71be1b1a46e94e99132c5391 beta renamed back | branch=default hg-git-rename-source=git | | diff --git a/beta b/beta | old mode 120000 | new mode 100644 | --- a/beta | +++ b/beta | @@ -1,1 +1,12 @@ | -beta-new | \ No newline at end of file | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | diff --git a/beta-new b/beta-new | deleted file mode 100644 | --- a/beta-new | +++ /dev/null | @@ -1,12 +0,0 @@ | -1 | -2 | -3 | -4 | -5 | -6 | -7 | -8 | -9 | -10 | -11 | -12 | o 5 f93fefed957cff2220d3f0d11182398350b5fa9a beta renamed | branch=default hg-git-rename-source=git | | diff --git a/beta b/beta | old mode 100644 | new mode 120000 | --- a/beta | +++ b/beta | @@ -1,12 +1,1 @@ | -1 | -2 | -3 | -4 | -5 | -6 | -7 | -8 | -9 | -10 | -11 | -12 | +beta-new | \ No newline at end of file | diff --git a/beta b/beta-new | copy from beta | copy to beta-new | o 4 b9e63d96abc2783afc59246e798a6936cf05a35e move submodule | branch=default hg-git-rename-source=git | | diff --git a/.hgsub b/.hgsub | --- a/.hgsub | +++ b/.hgsub | @@ -1,1 +1,1 @@ | -gitsubmodule = [git]../gitsubmodule | +gitsubmodule2 = [git]../gitsubmodule | diff --git a/.hgsubstate b/.hgsubstate | --- a/.hgsubstate | +++ b/.hgsubstate | @@ -1,1 +1,1 @@ | -5944b31ff85b415573d1a43eb942e2dea30ab8be gitsubmodule | +5944b31ff85b415573d1a43eb942e2dea30ab8be gitsubmodule2 | o 3 55537ea256c28be1b5637f4f93a601fdde8a9a7f add submodule | branch=default hg-git-rename-source=git | | diff --git a/.hgsub b/.hgsub | new file mode 100644 | --- /dev/null | +++ b/.hgsub | @@ -0,0 +1,1 @@ | +gitsubmodule = [git]../gitsubmodule | diff --git a/.hgsubstate b/.hgsubstate | new file mode 100644 | --- /dev/null | +++ b/.hgsubstate | @@ -0,0 +1,1 @@ | +5944b31ff85b415573d1a43eb942e2dea30ab8be gitsubmodule | o 2 20f9e56b6d006d0403f853245e483d0892b8ac48 gamma | branch=default hg-git-rename-source=git | | diff --git a/beta b/beta | --- a/beta | +++ b/beta | @@ -9,3 +9,4 @@ | 9 | 10 | 11 | +12 | diff --git a/beta b/gamma | copy from beta | copy to gamma | --- a/beta | +++ b/gamma | @@ -9,3 +9,4 @@ | 9 | 10 | 11 | +13 | o 1 9f7744e68def81da3b394f11352f602ca9c8ab68 beta | branch=default hg-git-rename-source=git | | diff --git a/alpha b/beta | rename from alpha | rename to beta | --- a/alpha | +++ b/beta | @@ -8,3 +8,4 @@ | 8 | 9 | 10 | +11 | o 0 7bc844166f76e49562f81eacd54ea954d01a9e42 alpha branch=default hg-git-rename-source=git diff --git a/alpha b/alpha new file mode 100644 --- /dev/null +++ b/alpha @@ -0,0 +1,10 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 Make a new ordinary commit in Mercurial (no extra metadata) $ echo 14 >> gamma $ hg ci -m "gamma2" Make a new commit with a copy and a rename in Mercurial $ hg cp gamma delta $ echo 15 >> delta $ hg mv beta epsilon $ echo 16 >> epsilon $ hg ci -m "delta/epsilon" $ hg export . # HG changeset patch # User test # Date 0 0 # Thu Jan 01 00:00:00 1970 +0000 # Node ID ea6414fab78622fd53679e0593eddad96ff4178d # Parent ee9ec792d5866c313a4cb7a2f8772f2cffa90df4 delta/epsilon diff --git a/gamma b/delta copy from gamma copy to delta --- a/gamma +++ b/delta @@ -11,3 +11,4 @@ 11 13 14 +15 diff --git a/beta b/epsilon rename from beta rename to epsilon --- a/beta +++ b/epsilon @@ -10,3 +10,4 @@ 10 11 12 +16 $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 2 commits with 2 trees and 3 blobs updating reference refs/heads/master $ cd ../repo.git $ git log master --pretty=oneline 5f2948d029693346043f320620af99a615930dc4 delta/epsilon bbd2ec050f7fbc64f772009844f7d58a556ec036 gamma2 50d116676a308b7c22935137d944e725d2296f2a remove submodule and rename back 59fb8e82ea18f79eab99196f588e8948089c134f rename and add submodule f95497455dfa891b4cd9b524007eb9514c3ab654 beta renamed back 055f482277da6cd3dd37c7093d06983bad68f782 beta renamed d7f31298f27df8a9226eddb1e4feb96922c46fa5 move submodule c610256cb6959852d9e70d01902a06726317affc add submodule e1348449e0c3a417b086ed60fc13f068d4aa8b26 gamma cc83241f39927232f690d370894960b0d1943a0e beta 938bb65bb322eb4a3558bec4cdc8a680c4d1794c alpha Make sure the right metadata is stored $ git cat-file commit master^ tree 0adbde18545845f3b42ad1a18939ed60a9dec7a8 parent 50d116676a308b7c22935137d944e725d2296f2a author test 0 +0000 committer test 0 +0000 HG:rename-source hg gamma2 $ git cat-file commit master tree f8f32f4e20b56a5a74582c6a5952c175bf9ec155 parent bbd2ec050f7fbc64f772009844f7d58a556ec036 author test 0 +0000 committer test 0 +0000 HG:rename gamma:delta HG:rename beta:epsilon delta/epsilon Now make another clone and compare the hashes $ cd .. $ hg clone -q repo.git hgrepo2 $ cd hgrepo2 $ hg book master -qf $ hg export master # HG changeset patch # User test # Date 0 0 # Thu Jan 01 00:00:00 1970 +0000 # Node ID ea6414fab78622fd53679e0593eddad96ff4178d # Parent ee9ec792d5866c313a4cb7a2f8772f2cffa90df4 delta/epsilon diff --git a/gamma b/delta copy from gamma copy to delta --- a/gamma +++ b/delta @@ -11,3 +11,4 @@ 11 13 14 +15 diff --git a/beta b/epsilon rename from beta rename to epsilon --- a/beta +++ b/epsilon @@ -10,3 +10,4 @@ 10 11 12 +16 Regenerate the Git metadata and compare the hashes $ hg debug-remove-hggit-state clearing out the git cache data $ hg gexport $ cd .hg/git $ git log master --pretty=oneline f3f6592447685566af9447c03ae262aa5432511d delta/epsilon (dulwich-rust !) c51ce14ec367c5ea72bf428dee3f8576f2fe1bb0 gamma2 (dulwich-rust !) df749cae534e3c7a0ad664cd0f214dd36e0ac259 remove submodule and rename back (dulwich-rust !) 8f9ec605ad0cc2532202f73cef8e35d3241797ee rename and add submodule (dulwich-rust !) 8a00d0fb75377c51c9a46e92ff154c919007f0e2 delta/epsilon (no-dulwich-rust !) dd7d4f1adb942a8d349dce585019f6949184bc64 gamma2 (no-dulwich-rust !) 3f1cdaf8b603816fcda02bd29e75198ae4cb13db remove submodule and rename back (no-dulwich-rust !) 2a4abf1178a999e2054158ceb0c7768079665d03 rename and add submodule (no-dulwich-rust !) 88c416e8d5e0e9dd1187d45ebafaa46111764196 beta renamed back 027d2a6e050705bf6f7e226e7e97f02ce5ae3200 beta renamed dc70e620634887e70ac5dd108bcc7ebd99c60ec3 move submodule c610256cb6959852d9e70d01902a06726317affc add submodule e1348449e0c3a417b086ed60fc13f068d4aa8b26 gamma cc83241f39927232f690d370894960b0d1943a0e beta 938bb65bb322eb4a3558bec4cdc8a680c4d1794c alpha Test findcopiesharder $ cd $TESTTMP $ git init -q gitcopyharder $ cd gitcopyharder $ cat >> file0 << EOF > 1 > 2 > 3 > 4 > 5 > EOF $ git add file0 $ fn_git_commit -m file0 $ cp file0 file1 $ git add file1 $ fn_git_commit -m file1 $ cp file0 file2 $ echo 6 >> file2 $ git add file2 $ fn_git_commit -m file2 $ cd .. Clone without findcopiesharder does not find copies from unmodified files $ hg clone gitcopyharder hgnocopyharder importing 3 git commits new changesets b45d023c6842:ec77ccdbefe0 (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgnocopyharder export 1::2 # HG changeset patch # User test # Date 1167609621 0 # Mon Jan 01 00:00:21 2007 +0000 # Node ID 555831c93e2a250e5ba42efad45bf7ba71da13e4 # Parent b45d023c6842337ffe694663a44aa672d311081c file1 diff --git a/file1 b/file1 new file mode 100644 --- /dev/null +++ b/file1 @@ -0,0 +1,5 @@ +1 +2 +3 +4 +5 # HG changeset patch # User test # Date 1167609622 0 # Mon Jan 01 00:00:22 2007 +0000 # Node ID ec77ccdbefe023eb9898b0399f84f670c8c0f5fc # Parent 555831c93e2a250e5ba42efad45bf7ba71da13e4 file2 diff --git a/file2 b/file2 new file mode 100644 --- /dev/null +++ b/file2 @@ -0,0 +1,6 @@ +1 +2 +3 +4 +5 +6 findcopiesharder finds copies from unmodified files if similarity is met $ hg --config git.findcopiesharder=true clone gitcopyharder hgcopyharder0 importing 3 git commits new changesets b45d023c6842:9b3099834272 (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgcopyharder0 export 1::2 # HG changeset patch # User test # Date 1167609621 0 # Mon Jan 01 00:00:21 2007 +0000 # Node ID cd05a87103eed9d270fc05b62b00f48e174ab960 # Parent b45d023c6842337ffe694663a44aa672d311081c file1 diff --git a/file0 b/file1 copy from file0 copy to file1 # HG changeset patch # User test # Date 1167609622 0 # Mon Jan 01 00:00:22 2007 +0000 # Node ID 9b30998342729c7357d418bebed7399986cfe643 # Parent cd05a87103eed9d270fc05b62b00f48e174ab960 file2 diff --git a/file0 b/file2 copy from file0 copy to file2 --- a/file0 +++ b/file2 @@ -3,3 +3,4 @@ 3 4 5 +6 $ hg --config git.findcopiesharder=true --config git.similarity=95 clone gitcopyharder hgcopyharder1 importing 3 git commits new changesets b45d023c6842:d9d2e8cbf050 (3 drafts) updating to bookmark master 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgcopyharder1 export 1::2 # HG changeset patch # User test # Date 1167609621 0 # Mon Jan 01 00:00:21 2007 +0000 # Node ID cd05a87103eed9d270fc05b62b00f48e174ab960 # Parent b45d023c6842337ffe694663a44aa672d311081c file1 diff --git a/file0 b/file1 copy from file0 copy to file1 # HG changeset patch # User test # Date 1167609622 0 # Mon Jan 01 00:00:22 2007 +0000 # Node ID d9d2e8cbf050772be31dccf78851f71dc547d139 # Parent cd05a87103eed9d270fc05b62b00f48e174ab960 file2 diff --git a/file2 b/file2 new file mode 100644 --- /dev/null +++ b/file2 @@ -0,0 +1,6 @@ +1 +2 +3 +4 +5 +6 Config values out of range $ hg --config git.similarity=999 clone gitcopyharder hgcopyharder2 importing 3 git commits abort: git.similarity must be between 0 and 100 [255] Left-over on Windows with some pack files $ rm -rf hgcopyharder2 $ hg --config git.renamelimit=-5 clone gitcopyharder hgcopyharder2 importing 3 git commits abort: git.renamelimit must be non-negative [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-serve-ci.t0000644000000000000000000000756014751647721013666 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" We assume the git server is unavailable elsewhere. $ if test -z "$CI_TEST_GIT_NETWORKING" > then > echo 'requires CI networking' > exit 80 > fi Allow password prompts without a TTY: $ cat >> $HGRCPATH << EOF > [extensions] > getpass = $TESTDIR/testlib/ext-get-password-from-env.py > EOF Create a silly SSH configuration: $ cat >> $HGRCPATH << EOF > [ui] > ssh = ssh -o UserKnownHostsFile=$TESTDIR/known_hosts -o StrictHostKeyChecking=no -i $TESTTMP/id_ed25519 > EOF $ cp $RUNTESTDIR/../contrib/docker/git-server/ssh/id_ed25519 $TESTTMP $ chmod 0600 $TESTTMP/id_ed25519 Clone using the git protocol: $ hg clone git://git-server/repo.git repo-git updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved ..and HTTP: $ hg clone http://git-server/repo.git repo-http abort: http authorization required for http://git-server/repo.git [255] $ hg clone --config ui.interactive=yes \ > --config ui.interactive=yes \ > --config auth.git.prefix=http://git-server \ > --config auth.git.username=git \ > http://git-server/repo.git repo-http http authorization required for http://git-server/repo.git realm: Git Access user: git password: nope abort: authorization failed [255] $ PASSWD=git hg clone --config ui.interactive=yes \ > http://git-server/repo.git repo-http < git > EOF http authorization required for http://git-server/repo.git realm: Git Access user: git password: git updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved ..and finally SSH: $ hg clone git@git-server:/srv/repo.git repo-ssh Warning: Permanently added * (glob) updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved ..but also try SSH with GIT_SSH_COMMAND, which we just ignore: $ GIT_SSH_COMMAND="ignored" \ > hg clone git@git-server:/srv/repo.git repo-ssh-2 updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ rm -rf repo-ssh-2 So, that went well; now push... $ cd repo-ssh $ echo thefile > thefile $ hg add thefile $ fn_hg_commit -m 'add the file' $ hg book -r tip master $ hg path default git@git-server:/srv/repo.git $ hg push pushing to git@git-server:/srv/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs adding reference refs/heads/master $ cd .. And finally, pull the new commit: $ hg -R repo-git pull -u pulling from git://git-server/repo.git remote: warning: unable to access '/root/.config/git/attributes': Permission denied importing 1 git commits adding bookmark master new changesets fa22339f4ab8 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Straight HTTP doesn't work: $ hg -R repo-http pull -u pulling from http://git-server/repo.git abort: http authorization required for http://git-server/repo.git [255] But we can specify authentication in the configuration: $ hg -R repo-http \ > --config auth.git.prefix=http://git-server \ > --config auth.git.username=git \ > --config auth.git.password=git \ > pull -u pulling from http://git-server/repo.git remote: warning: unable to access '/root/.config/git/attributes': Permission denied importing 1 git commits adding bookmark master new changesets fa22339f4ab8 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Try using git credentials: NB: the use of printf is deliberate; otherwise the test fails due to dulwich considering the newline part of the url $ printf http://git:git@git-server > $TESTTMP/.git-credentials $ hg -R repo-http pull pulling from http://git-server/repo.git no changes found $ rm -f $TESTTMP/.git-credentials ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-serve-dulwich.t0000644000000000000000000001056314751647721014727 0ustar00#require serve Check cloning a Git repository over anonymous HTTP, served up by Dulwich. Load commonly used test logic $ . "$TESTDIR/testutil" Enable progress debugging: $ cat >> $HGRCPATH < [progress] > delay = 0 > refresh = 0 > width = 60 > format = topic unit total number item bar > assume-tty = yes > EOF Create a dummy repository $ git init -q gitrepo $ cd gitrepo $ echo foo > foo $ git add foo $ fn_git_commit -m test $ echo bar > bar $ git add bar $ fn_git_commit -m test $ cd .. And a bare one: $ git clone -q --bare gitrepo repo.git Serve them: $ $PYTHON $TESTDIR/testlib/daemonize.py dulwich.log \ > $TESTDIR/testlib/dulwich-serve.py $HGPORT Make sure that clone over unauthenticated HTTP doesn't break $ hg clone -U git+http://localhost:$HGPORT/gitrepo hgrepo 2>&1 || cat $TESTTMP/dulwich.log \r (no-eol) (esc) importing commits 1/2 b23744d34f97 [======> ]\r (no-eol) (esc) importing commits 2/2 3af9773036a9 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) importing 2 git commits new changesets c4d188f6e13d:221dd250e933 $ hg log -T 'HG:{node|short} GIT:{gitnode|short}\n' -R hgrepo HG:221dd250e933 GIT:3af9773036a9 HG:c4d188f6e13d GIT:b23744d34f97 $ hg tags -v -R hgrepo tip 1:221dd250e933 default/master 1:221dd250e933 git-remote Similarly, make sure that we detect repositories ending with .git $ hg clone -U http://localhost:$HGPORT/repo.git hgrepo-copy 2>&1 || cat $TESTTMP/dulwich.log \r (no-eol) (esc) importing commits 1/2 b23744d34f97 [======> ]\r (no-eol) (esc) importing commits 2/2 3af9773036a9 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) importing 2 git commits new changesets c4d188f6e13d:221dd250e933 $ hg tags -v -R hgrepo tip 1:221dd250e933 default/master 1:221dd250e933 git-remote $ cd hgrepo $ hg up master \r (no-eol) (esc) updating files 2/2 foo [================>]\r (no-eol) (esc) \r (no-eol) (esc) 2 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ echo baz > baz $ fn_hg_commit -A -m baz $ hg push || cat $TESTTMP/dulwich.log \r (no-eol) (esc) searching commits 1/1 daf1ae153bf8 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) \r (no-eol) (esc) exporting 1/1 daf1ae153bf8 [=====================>]\r (no-eol) (esc) \r (no-eol) (esc) \r (no-eol) (esc) checking for reusable deltas 0 [ <=> ]\r (no-eol) (esc) \r (no-eol) (esc) pushing to git+http://localhost:$HGPORT/gitrepo searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg log -T 'HG:{node|short} GIT:{gitnode|short} {tags}\n' -r . HG:daf1ae153bf8 GIT:ab88565d0614 default/master tip $ cd .. Verify that we can suppress publishing using a path option: $ hg clone --config paths.default:hg-git.publish=no -U git+http://localhost:$HGPORT/gitrepo hgrepo-public \r (no-eol) (esc) importing commits 1/3 b23744d34f97 [===> ]\r (no-eol) (esc) importing commits 2/3 3af9773036a9 [========> ]\r (no-eol) (esc) importing commits 3/3 ab88565d0614 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) importing 3 git commits new changesets c4d188f6e13d:daf1ae153bf8 (3 drafts) $ hg clone --config git.public=no -U git+http://localhost:$HGPORT/gitrepo hgrepo-public2 \r (no-eol) (esc) importing commits 1/3 b23744d34f97 [===> ]\r (no-eol) (esc) importing commits 2/3 3af9773036a9 [========> ]\r (no-eol) (esc) importing commits 3/3 ab88565d0614 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) importing 3 git commits new changesets c4d188f6e13d:daf1ae153bf8 (3 drafts) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-serve-git.t0000644000000000000000000000431214751647721014046 0ustar00#require serve Load commonly used test logic $ . "$TESTDIR/testutil" Enable progress debugging: $ cat >> $HGRCPATH < [progress] > delay = 0 > refresh = 0 > width = 60 > format = topic unit total number item bar > assume-tty = yes > EOF Create a dummy repository and serve it $ git init -q test $ cd test $ echo foo > foo $ git add foo $ fn_git_commit -m test $ echo bar > bar $ git add bar $ fn_git_commit -m test $ git daemon --listen=localhost --port=$HGPORT \ > --pid-file=$DAEMON_PIDS --detach --export-all --verbose \ > --base-path=$TESTTMP \ > || exit 80 $ cd .. Make sure that clone over the old git protocol doesn't break $ hg clone -U git://localhost:$HGPORT/test copy 2>&1 \r (no-eol) (esc) Counting objects 1/6 [=====> ]\r (no-eol) (esc) Counting objects 2/6 [===========> ]\r (no-eol) (esc) Counting objects 3/6 [=================> ]\r (no-eol) (esc) Counting objects 4/6 [=======================> ]\r (no-eol) (esc) Counting objects 5/6 [=============================> ]\r (no-eol) (esc) Counting objects 6/6 [===================================>]\r (no-eol) (esc) \r (no-eol) (esc) \r (no-eol) (esc) Compressing objects 1/3 [==========> ]\r (no-eol) (esc) Compressing objects 2/3 [=====================> ]\r (no-eol) (esc) Compressing objects 3/3 [================================>]\r (no-eol) (esc) \r (no-eol) (esc) \r (no-eol) (esc) importing commits 1/2 b23744d34f97 [======> ]\r (no-eol) (esc) importing commits 2/2 3af9773036a9 [=============>]\r (no-eol) (esc) \r (no-eol) (esc) importing 2 git commits new changesets c4d188f6e13d:221dd250e933 $ hg log -T 'HG:{node|short} GIT:{gitnode|short}\n' -R copy HG:221dd250e933 GIT:3af9773036a9 HG:c4d188f6e13d GIT:b23744d34f97 $ hg tags -v -R copy tip 1:221dd250e933 default/master 1:221dd250e933 git-remote ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-serve-hg-static.t0000644000000000000000000001321714751647721015152 0ustar00#require no-reposimplestore Copied from Mercurial's `tests/test-static-http.t`, with the following line added to load commonly used test logic: $ . "$TESTDIR/testutil" $ hg clone http://localhost:$HGPORT/ copy abort: * (glob) [100] $ test -d copy [1] This server doesn't do range requests so it's basically only good for one pull $ "$PYTHON" "$TESTDIR/testlib/dumbhttp.py" -p $HGPORT --pid dumb.pid \ > --logfile server.log $ cat dumb.pid >> $DAEMON_PIDS $ hg init remote $ cd remote $ echo foo > bar $ echo c2 > '.dotfile with spaces' $ hg add adding .dotfile with spaces adding bar $ hg commit -m"test" $ hg tip changeset: 0:02770d679fb8 tag: tip user: test date: Thu Jan 01 00:00:00 1970 +0000 summary: test $ cd .. $ hg clone static-http://localhost:$HGPORT/remote local requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 2 changes to 2 files new changesets 02770d679fb8 updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd local $ hg verify checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 1 changesets with 2 changes to 2 files $ cat bar foo $ cd ../remote $ echo baz > quux $ hg commit -A -mtest2 adding quux check for HTTP opener failures when cachefile does not exist $ rm .hg/cache/* $ cd ../local $ hg pull pulling from static-http://localhost:$HGPORT/remote searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files new changesets 4ac2e3648604 (run 'hg update' to get a working copy) trying to push $ hg update 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo more foo >> bar $ hg commit -m"test" $ hg push pushing to static-http://localhost:$HGPORT/remote abort: destination does not support push [255] trying clone -r $ cd .. $ hg clone -r doesnotexist static-http://localhost:$HGPORT/remote local0 abort: unknown revision 'doesnotexist'!? (re) [10] $ hg clone -r 0 static-http://localhost:$HGPORT/remote local0 adding changesets adding manifests adding file changes added 1 changesets with 2 changes to 2 files new changesets 02770d679fb8 updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved test with "/" URI (issue747) and subrepo $ hg init $ hg init sub $ touch sub/test $ hg -R sub commit -A -m "test" adding test $ hg -R sub tag not-empty $ echo sub=sub > .hgsub $ echo a > a $ hg add a .hgsub $ hg -q ci -ma $ hg clone static-http://localhost:$HGPORT/ local2 requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 3 changes to 3 files new changesets a9ebfbe8e587 updating to branch default cloning subrepo sub from static-http://localhost:$HGPORT/sub requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files new changesets be090ea66256:322ea90975df 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd local2 $ hg verify checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 1 changesets with 3 changes to 3 files checking subrepo links $ cat a a $ hg paths default = static-http://localhost:$HGPORT/ test with empty repo (issue965) $ cd .. $ hg init remotempty $ hg clone static-http://localhost:$HGPORT/remotempty local3 no changes found updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd local3 $ hg verify checking changesets checking manifests crosschecking files in changesets and manifests checking files checking dirstate (?) checked 0 changesets with 0 changes to 0 files $ hg paths default = static-http://localhost:$HGPORT/remotempty $ cd .. Clone with tags and branches works $ hg init remote-with-names $ cd remote-with-names $ echo 0 > foo $ hg -q commit -A -m initial $ echo 1 > foo $ hg commit -m 'commit 1' $ hg -q up 0 $ hg branch mybranch marked working directory as branch mybranch (branches are permanent and global, did you want a bookmark?) $ echo 2 > foo $ hg commit -m 'commit 2 (mybranch)' $ hg tag -r 1 'default-tag' $ hg tag -r 2 'branch-tag' $ cd .. $ hg clone static-http://localhost:$HGPORT/remote-with-names local-with-names requesting all changes adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 2 files (+1 heads) new changesets 68986213bd44:0c325bd2b5a7 updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Clone a specific branch works $ hg clone -r mybranch static-http://localhost:$HGPORT/remote-with-names local-with-names-branch adding changesets adding manifests adding file changes added 4 changesets with 4 changes to 2 files new changesets 68986213bd44:0c325bd2b5a7 updating to branch mybranch 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Clone a specific tag works $ hg clone -r default-tag static-http://localhost:$HGPORT/remote-with-names local-with-names-tag adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files new changesets 68986213bd44:4ee3fcef1c80 updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ killdaemons.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-serve-hg.t0000644000000000000000000000673614751647721013675 0ustar00#require serve Load commonly used test logic $ . "$TESTDIR/testutil" #testcases with-hggit without-hggit Load commonly used test logic $ . "$TESTDIR/testutil" #if with-hggit $ cat >> $HGRCPATH < [experimental] > hg-git-serve = yes > EOF #endif $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo foo>foo $ mkdir foo.d foo.d/bAr.hg.d foo.d/baR.d.hg $ git add . $ fn_git_commit -m 1 $ git tag thetag $ echo foo>foo.d/foo $ echo bar>foo.d/bAr.hg.d/BaR $ echo bar>foo.d/baR.d.hg/bAR $ git add . $ fn_git_commit -m 2 $ cd .. $ hg clone gitrepo hgrepo importing 2 git commits new changesets f488b65fa424:c61c38c3d614 (2 drafts) updating to bookmark master 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ cat >> .hg/hgrc < [push] > pushvars.server = true > [web] > allow-push = * > push_ssl = no > [hooks] > pretxnchangegroup = env | grep HG_USERVAR_ || true > EOF $ hg serve -p $HGPORT -d --pid-file=../hg1.pid -E ../error.log $ hg --config server.uncompressed=False serve -p $HGPORT1 -d --pid-file=../hg2.pid Test server address cannot be reused #if windows $ hg serve -p $HGPORT1 2>&1 abort: cannot start server at '*:$HGPORT1': * (glob) [255] #else $ hg serve -p $HGPORT1 2>&1 abort: cannot start server at '*:$HGPORT1': Address* in use (glob) [255] #endif $ cd .. $ cat hg1.pid hg2.pid >> $DAEMON_PIDS Make sure that clone regular mercurial repos over http doesn't break, and that we can transfer the hg-git metadata $ hg clone http://localhost:$HGPORT/ copy 2>&1 requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 4 changes to 4 files new changesets f488b65fa424:c61c38c3d614 (?) updating to branch default 4 files updated, 0 files merged, 0 files removed, 0 files unresolved And it shouldn't create a Git repository needlessly: $ test -e copy/git [1] $ cd copy #if without-hggit $ hg tags tip 1:c61c38c3d614 $ hg log -T '{rev}:{node|short} | {bookmarks} | {gitnode} |\n' 1:c61c38c3d614 | master | | 0:f488b65fa424 | | | $ hg pull -u ../gitrepo pulling from ../gitrepo importing 2 git commits 0 files updated, 0 files merged, 0 files removed, 0 files unresolved #else $ hg tags tip 1:c61c38c3d614 thetag 0:f488b65fa424 $ hg log -T '{rev}:{node|short} | {bookmarks} | {gitnode} |\n' 1:c61c38c3d614 | master | 95bcbb72932335c132c10950b5e5dc1066138ea1 | 0:f488b65fa424 | | a874aa4c9506ed30ef2c2c7313abd2c518e9e71e | $ hg pull -u ../gitrepo pulling from ../gitrepo warning: created new git repository at $TESTTMP/copy/.hg/git no changes found #endif $ hg tags tip 1:c61c38c3d614 thetag 0:f488b65fa424 $ hg log -T '{rev}:{node|short} | {bookmarks} | {gitnode} |\n' 1:c61c38c3d614 | master | 95bcbb72932335c132c10950b5e5dc1066138ea1 | 0:f488b65fa424 | | a874aa4c9506ed30ef2c2c7313abd2c518e9e71e | Furthermore, make sure that we pass all arguments when pushing: $ echo baz > baz $ fn_hg_commit -A -m baz $ hg push --pushvars FOO=BAR pushing to http://localhost:$HGPORT/ searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: HG_USERVAR_FOO=BAR remote: added 1 changesets with 1 changes to 1 files $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-subrepos-delete.t0000644000000000000000000000323714751647721015250 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [templates] > info = > commit: {rev}:{node|short} {desc|fill68} > added: {file_adds} > removed: {file_dels}\n > EOF This test ensures that we clean up properly when deleting Git submodules. $ git init --bare repo.git Initialized empty Git repository in $TESTTMP/repo.git/ Create a repository with a submodule: $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ git clone repo.git gitrepo Cloning into 'gitrepo'... warning: You appear to have cloned an empty repository. done. $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git submodule add ../gitsubrepo subrepo Cloning into '$TESTTMP/gitrepo/subrepo'... done. $ fn_git_commit -m 'add subrepo' Now delete all submodules: $ git rm .gitmodules subrepo rm '.gitmodules' rm 'subrepo' $ fn_git_commit -m 'delete subrepo' $ git push To $TESTTMP/repo.git * [new branch] master -> master $ cd .. And there should be nothing in Mercurial either: $ hg clone -U repo.git hgrepo importing 3 git commits new changesets e532b2bfda10:cc611d35fb62 (3 drafts) $ cd hgrepo $ hg log --graph --template info o | commit: 2:cc611d35fb62 delete subrepo | added: | removed: .hgsub .hgsubstate o | commit: 1:8d549bcc5179 add subrepo | added: .hgsub .hgsubstate | removed: o commit: 0:e532b2bfda10 add alpha added: alpha removed: $ hg manifest -r tip alpha $ cd .. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-subrepos-drop.t0000644000000000000000000000303714751647721014750 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ cat >> $HGRCPATH < [templates] > info = > commit: {rev}:{node|short} {desc|fill68} > added: {file_adds} > removed: {file_dels}\n > EOF Create a Git upstream $ git init --quiet --bare repo.git Create a Mercurial repository with a .gitmodules file: $ hg clone repo.git hgrepo updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg book master $ touch this $ fn_hg_commit -A -m 'add this' $ cat > .gitmodules < [submodule "subrepo"] > path = subrepo > url = ../gitsubrepo > EOF $ hg add .gitmodules $ fn_hg_commit -m "add .gitmodules file" $ cd .. What happens if we push that to Git? $ hg -R hgrepo push pushing to $TESTTMP/repo.git warning: ignoring modifications to '.gitmodules' file; please use '.hgsub' instead searching for changes adding objects remote: found 0 deltas to reuse added 2 commits with 1 trees and 1 blobs adding reference refs/heads/master But we don't get a warning if we don't touch .gitmodules: $ cd hgrepo $ touch that $ fn_hg_commit -A -m 'add that' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ cd .. Check that it didn't silenty come through, or something: $ git clone repo.git gitrepo Cloning into 'gitrepo'... done. $ ls -A gitrepo .git that this ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-subrepos-push.t0000644000000000000000000000367514751647721014773 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init --bare repo.git Initialized empty Git repository in $TESTTMP/repo.git/ $ hg init hgsubrepo $ cd hgsubrepo $ echo thefile > thefile $ hg add thefile $ fn_hg_commit -m 'add thefile' $ cd .. $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo thefile > thefile $ git add thefile $ fn_git_commit -m 'add thefile' $ cd .. $ hg clone repo.git hgrepo updating to branch default 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg book master $ echo alpha > alpha $ hg add alpha $ fn_hg_commit -m 'add alpha' $ touch .hgsub $ hg add .hgsub $ fn_hg_commit -m "add .hgsub" $ hg clone -q ../hgsubrepo hg $ echo "hg = ../hgsubrepo" >> .hgsub $ fn_hg_commit -m 'add hg subrepo' $ git clone --quiet ../gitsubrepo git $ echo "git = [git]../gitsubrepo" >> .hgsub $ fn_hg_commit -m 'add git subrepo' $ hg push pushing to $TESTTMP/repo.git pushing subrepo hg to $TESTTMP/hgsubrepo searching for changes no changes found searching for changes adding objects remote: found 0 deltas to reuse added 4 commits with 2 trees and 2 blobs adding reference refs/heads/master $ cat .hgsub hg = ../hgsubrepo git = [git]../gitsubrepo $ cat .hgsubstate aaae5224095dca7403147c0e20cbac1f450b0e95 git df643c539c7541d48eacc76745581e00cbaf3d45 hg $ cd .. Now clone it. Note that no Mercurial state persists: $ git clone --recurse-submodules repo.git gitrepo Cloning into 'gitrepo'... done. Submodule 'git' ($TESTTMP/gitsubrepo) registered for path 'git' Cloning into '$TESTTMP/gitrepo/git'... done. Submodule path 'git': checked out 'aaae5224095dca7403147c0e20cbac1f450b0e95' $ cd gitrepo $ ls -A .git .gitmodules alpha git $ cat .gitmodules [submodule "git"] path = git url = ../gitsubrepo $ ls -A git .git thefile ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-subrepos-syntax.t0000644000000000000000000000463514751647721015337 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" This is mostly equivalent to test-subrepos.t, but exercises a particular case where we cannot possibly retain bidirectionality: comments and [subpaths] in .hgsub $ git init --bare repo.git Initialized empty Git repository in $TESTTMP/repo.git/ $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ git clone repo.git gitrepo Cloning into 'gitrepo'... warning: You appear to have cloned an empty repository. done. $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git submodule add ../gitsubrepo subrepo1 Cloning into '*subrepo1'... (glob) done. $ fn_git_commit -m 'add subrepo1' $ git submodule add ../gitsubrepo xyz/subrepo2 Cloning into '*xyz/subrepo2'... (glob) done. $ fn_git_commit -m 'add subrepo2' $ git push To $TESTTMP/repo.git * [new branch] master -> master $ cd .. $ hg clone -U repo.git hgrepo importing 3 git commits new changesets e532b2bfda10:3c4fd561cbeb (3 drafts) $ cd hgrepo $ hg up master Cloning into '$TESTTMP/hgrepo/subrepo1'... done. Cloning into '$TESTTMP/hgrepo/xyz/subrepo2'... done. cloning subrepo subrepo1 from $TESTTMP/gitsubrepo cloning subrepo xyz/subrepo2 from $TESTTMP/gitsubrepo 3 files updated, 0 files merged, 0 files removed, 0 files unresolved (activating bookmark master) $ cat >> .hgsub < # this is a comment > [subpaths] > flaf = blyf > EOF $ fn_hg_commit -m 'add comment & subsection' $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ cd .. $ cd gitrepo $ git pull --ff-only From $TESTTMP/repo 89c22d7..106b34e master -> origin/master Updating 89c22d7..106b34e Fast-forward $ cat .gitmodules [submodule "subrepo1"] path = subrepo1 url = ../gitsubrepo [submodule "xyz/subrepo2"] path = xyz/subrepo2 url = ../gitsubrepo $ cd .. We broke bidirectionality: $ hg clone -U repo.git hgrepo2 importing 4 git commits new changesets e532b2bfda10:cbf584fe001b (4 drafts) $ hg id -r tip hgrepo c58a542b18bc default/master/tip master $ hg id -r tip hgrepo2 cbf584fe001b default/master/tip master ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-subrepos.t0000644000000000000000000001462014751647721014006 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init --bare repo.git Initialized empty Git repository in $TESTTMP/repo.git/ $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ git clone repo.git gitrepo Cloning into 'gitrepo'... warning: You appear to have cloned an empty repository. done. $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git submodule add ../gitsubrepo subrepo1 Cloning into '*subrepo1'... (glob) done. $ fn_git_commit -m 'add subrepo1' $ git submodule add ../gitsubrepo xyz/subrepo2 Cloning into '*xyz/subrepo2'... (glob) done. $ fn_git_commit -m 'add subrepo2' $ git push To $TESTTMP/repo.git * [new branch] master -> master $ cd .. Ensure gitlinks are transformed to .hgsubstate on hg pull from git $ hg clone -u tip repo.git hgrepo 2>&1 | grep -E -v '^(Cloning into|done)' importing 3 git commits new changesets e532b2bfda10:3c4fd561cbeb (3 drafts) updating to branch default cloning subrepo subrepo1 from $TESTTMP/gitsubrepo cloning subrepo xyz/subrepo2 from $TESTTMP/gitsubrepo 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg bookmarks -f -r default master 1. Ensure gitlinks are transformed to .hgsubstate on hg <- git pull .hgsub shall list two [git] subrepos $ cat .hgsub subrepo1 = [git]../gitsubrepo xyz/subrepo2 = [git]../gitsubrepo .hgsubstate shall list two idenitcal revisions $ cat .hgsubstate 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 56f0304c5250308f14cfbafdc27bd12d40154d17 xyz/subrepo2 hg status shall NOT report .hgsub and .hgsubstate as untracked - either ignored or unmodified $ hg status --unknown .hgsub .hgsubstate $ hg status --modified .hgsub .hgsubstate $ cd .. 2. Check gitmodules are preserved during hg -> git push $ cd gitsubrepo $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ cd .. $ cd hgrepo $ cd xyz/subrepo2 $ git pull --ff-only | sed 's/files/file/;s/insertions/insertion/;s/, 0 deletions.*//' | sed 's/| */| /' From $TESTTMP/gitsubrepo 56f0304..aabf7cd master -> origin/master Updating 56f0304..aabf7cd Fast-forward gamma | 1 + 1 file changed, 1 insertion(+) create mode 100644 gamma $ cd ../.. $ echo xxx >> alpha $ fn_hg_commit -m 'Update subrepo2 from hg' | grep -v "committing subrepository" || true $ hg push pushing to $TESTTMP/repo.git searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 2 trees and 1 blobs updating reference refs/heads/master $ cd .. $ cd gitrepo $ git pull --ff-only From $TESTTMP/repo 89c22d7..275b0a5 master -> origin/master Fetching submodule xyz/subrepo2 From $TESTTMP/gitsubrepo 56f0304..aabf7cd master -> origin/master Updating 89c22d7..275b0a5 Fast-forward alpha | 1 + xyz/subrepo2 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) there shall be two gitlink entries, with values matching that in .hgsubstate $ git ls-tree -r HEAD^{tree} | grep 'commit' 160000 commit 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 160000 commit aabf7cd015089aff0b84596e69aa37b24a3d090a xyz/subrepo2 bring working copy to HEAD state (it's not bare repo) $ git reset --hard HEAD is now at 275b0a5 Update subrepo2 from hg $ cd .. 3. Check .hgsub and .hgsubstate from git repository are merged, not overwritten $ hg init hgsub $ cd hgsub $ echo delta > delta $ hg add delta $ fn_hg_commit -m "add delta" $ hg tip --template '{node} hgsub\n' > ../gitrepo/.hgsubstate $ cat > ../gitrepo/.hgsub < hgsub = ../hgsub > EOF $ cd ../gitrepo $ git add .hgsubstate .hgsub $ fn_git_commit -m "Test3. Prepare .hgsub and .hgsubstate sources" $ git push To $TESTTMP/repo.git 275b0a5..e31d576 master -> master $ cd ../hgrepo $ hg pull pulling from $TESTTMP/repo.git importing 1 git commits updating bookmark master new changesets [0-9a-f]{12,12} \(1 drafts\) (re) (run 'hg update' to get a working copy) $ hg checkout -C updating to active bookmark master cloning subrepo hgsub from $TESTTMP/hgsub 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd .. pull shall bring .hgsub entry which was added to the git repo $ cat hgrepo/.hgsub hgsub = ../hgsub subrepo1 = [git]../gitsubrepo xyz/subrepo2 = [git]../gitsubrepo .hgsubstate shall list revision of the subrepo added through git repo $ cat hgrepo/.hgsubstate 481ec30d580f333ae3a77f94c973ce37b69d5bda hgsub 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 aabf7cd015089aff0b84596e69aa37b24a3d090a xyz/subrepo2 4. Try changing the subrepos from the Mercurial side $ cd hgrepo $ cat >> .hgsub < subrepo2 = [git]../gitsubrepo > EOF $ git clone ../gitsubrepo subrepo2 Cloning into 'subrepo2'... done. $ fn_hg_commit -m 'some stuff' $ hg push pushing to $TESTTMP/repo.git no changes made to subrepo hgsub since last push to $TESTTMP/hgsub searching for changes adding objects remote: found 0 deltas to reuse added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ cd .. 5. But we actually do something quite weird in this case: If a .gitmodules file exists in the repository, it always wins! In this case, we break the bidirectional convention, and modify the repository data. That's odd, so show it: $ hg id hgrepo 42c46c7eef3a default/master/tip master $ hg clone -U repo.git hgrepo2 importing 6 git commits new changesets e532b2bfda10:42c46c7eef3a (6 drafts) $ hg -R hgrepo2 up :master Cloning into '$TESTTMP/hgrepo2/subrepo1'... done. cloning subrepo hgsub from $TESTTMP/hgsub cloning subrepo subrepo1 from $TESTTMP/gitsubrepo checking out detached HEAD in subrepository "subrepo1" check out a git branch if you intend to make changes Cloning into '$TESTTMP/hgrepo2/subrepo2'... done. Cloning into '$TESTTMP/hgrepo2/xyz/subrepo2'... done. cloning subrepo subrepo2 from $TESTTMP/gitsubrepo cloning subrepo xyz/subrepo2 from $TESTTMP/gitsubrepo 3 files updated, 0 files merged, 0 files removed, 0 files unresolved We retained bidirectionality! $ git diff --stat hgrepo/.hgsub hgrepo2/.hgsub $ hg id hgrepo 42c46c7eef3a default/master/tip master $ hg id hgrepo2 42c46c7eef3a default/master/tip master ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-timezone.t0000644000000000000000000000152314751647721013774 0ustar00This test shows how dulwich fails to convert a commit accepted by hg. In the real world case, it was a hand edit by the user to change the timezone field in an export. However, if it is good enough for hg, we have to make it good enough for git. Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo $ cd hgrepo $ touch beta $ hg add beta $ fn_hg_commit -m "test commit" $ cat >patch2 < # HG changeset patch > # User J. User > # Date 1337962044 25201 > # Node ID 1111111111111111111111111111111111111111 > # Parent 0000000000000000000000000000000000000000 > > Patch with sub-minute time zone > diff --git a/alpha b/alpha > new file mode 100644 > --- /dev/null > +++ b/alpha > @@ -0,0 +1,1 @@ > +alpha > EOF $ hg import patch2 applying patch2 $ hg gexport ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-transactions.t0000644000000000000000000001200114751647721014643 0ustar00Transactions ============ This test excercises our transaction logic, and the behaviour when a conversion fails or is interrupted. Load commonly used test logic $ . "$TESTDIR/testutil" Enable a few other extensions: $ cat >> $HGRCPATH < [extensions] > breakage = $TESTDIR/testlib/ext-break-git-import.py > EOF Create a git repository with 100 commits, that touches 10 different files. We also have 10 tags. $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ for i in $(seq 10) > do > for f in $(seq 10) > do > n=$(expr $i \* $f) > echo $n > $f > git add $f > fn_git_commit -m $n > done > fn_git_tag -m $i v$i > done $ cd .. Map saving ---------- First, test that hggit.mapsavefrequency actually works clone with mapsavefreq set $ hg clone gitrepo hgrepo --config hggit.mapsavefrequency=10 --debug \ > | grep -c saving 1 $ rm -rf hgrepo pull with mapsavefreq set $ hg init hgrepo $ cat >> hgrepo/.hg/hgrc < [paths] > default = $TESTTMP/gitrepo > EOF $ hg -R hgrepo --config hggit.mapsavefrequency=10 pull --debug \ > | grep -c saving 10 $ rm -rf hgrepo The user experience ------------------- The map save interval affects how and when changes are reported to the user. First, create a repository, set up to pull from git, and where we can interrupt the conversion. $ hg init hgrepo $ cat >> hgrepo/.hg/hgrc < [paths] > default = $TESTTMP/gitrepo > EOF $ cd hgrepo A low save interval causes a lot of reports: $ hg --config hggit.mapsavefrequency=25 pull pulling from $TESTTMP/gitrepo importing 100 git commits new changesets 1c8407413fa3:abc468b9e51b (25 drafts) new changesets 217c308baf47:d5d14eeedd08 (25 drafts) new changesets d9807ef6abcb:4678067bd500 (25 drafts) adding bookmark master new changesets c31a154888bb:eda59117ba04 (25 drafts) (run 'hg update' to get a working copy) Reset the repository $ hg debugstrip --no-backup 'all()' $ hg debug-remove-hggit-state clearing out the git cache data And with phases? No mention of draft changesets, as we publish changes during the conversion: $ hg --config hggit.mapsavefrequency=25 --config hggit.usephases=yes pull pulling from $TESTTMP/gitrepo importing 100 git commits new changesets 1c8407413fa3:abc468b9e51b new changesets 217c308baf47:d5d14eeedd08 new changesets d9807ef6abcb:4678067bd500 updating bookmark master new changesets c31a154888bb:eda59117ba04 (run 'hg update' to get a working copy) Reset the repository $ hg debugstrip --no-backup 'all()' $ hg debug-remove-hggit-state clearing out the git cache data Interruptions ------------- How does hg-git behave if a conversion fails or is interrupted? Ideally, we would always save the results of whatever happened, but that causes a significant slowdown. Transactions are an important optimisation within Mercurial. Test an error in a pull: $ ABORT_AFTER=99 hg pull pulling from $TESTTMP/gitrepo importing 100 git commits transaction abort! rollback completed abort: aborted after 99 commits! [255] $ hg log -l 10 -T '{rev} {gitnode}\n' Test the user exiting in the first transaction: $ EXIT_AFTER=5 hg --config hggit.mapsavefrequency=10 pull pulling from $TESTTMP/gitrepo importing 100 git commits transaction abort! rollback completed interrupted! [255] $ hg log -l 10 -T '{rev} {gitnode}\n' Check that we have no state, but clear it just in case $ ls -d .hg/git* .hg/git $ hg debug-remove-hggit-state clearing out the git cache data Test the user exiting in the middle of a conversion, after the first transaction: $ EXIT_AFTER=15 hg --config hggit.mapsavefrequency=10 pull pulling from $TESTTMP/gitrepo importing 100 git commits new changesets 1c8407413fa3:7c8c534a5fbe (10 drafts) transaction abort! rollback completed interrupted! [255] $ hg log -l 10 -T '{rev} {gitnode}\n' 9 7cbb16ec981b308e1e2b181f8e1f22c8f409f44e 8 42da70ed92bbecf9f348ba59c93646be723d0bf2 7 17e841146e5744b81af9959634d82c20a5d7df52 6 c31065bf97bf014815e37cdfbdef2c32c687f314 5 fcf21b8e0520ec1cced1d7593d13f9ee54721269 4 46acd02d0352e4b92bd6a099bb0490305d847a18 3 61eeda444b37b8aa3892d5f04c66c5441d21dd66 2 e55db11bb0472791c7af3fc636772174cdea4a36 1 17a2672b3c24c02d568f99d8d55ccae2bf362d5c 0 4e195b4c6e77604b70a8ad3b01306adbb9b1c7e7 $ cd .. $ rm -rf hgrepo And with a clone into an existing directory using an in-tree repository. Mercurial deletes the repository on errors, and so should we do with the Git repository, ideally. The current design doesn't make that easy to do, so this test mostly exists to document the current behaviour. $ mkdir hgrepo $ EXIT_AFTER=15 \ > hg --config hggit.mapsavefrequency=10 --config git.intree=yes \ > --cwd hgrepo \ > clone -U $TESTTMP/gitrepo . importing 100 git commits transaction abort! rollback completed interrupted! [255] the leftover below appeared in Mercurial 5.9+; it is unintentional $ ls -A hgrepo .git $ rm -rf hgrepo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-transplant.t0000644000000000000000000000150414751647721014327 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" Check that hg-git doesn't break the -s/--source option for transplant https://foss.heptapod.net/mercurial/hg-git/-/issues/392 $ cat <> $HGRCPATH > [extensions] > transplant= > graphlog= > EOF $ hg init baserepo $ cd baserepo $ for c in A B C > do > echo $c > $c && hg add $c && fn_hg_commit -m $c > done $ hg clone -r 2 . ../otherrepo adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files new changesets d2296e4d4e8a:f21e074b4681 updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd ../otherrepo $ hg up 1 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg transplant -s ../baserepo tip no changes found ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-tree-decomposition.t0000644000000000000000000000311514751647721015752 0ustar00Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ mkdir d1 $ echo a > d1/f1 $ echo b > d1/f2 $ git add d1/f1 d1/f2 $ fn_git_commit -m initial $ mkdir d2 $ git mv d1/f2 d2/f2 $ fn_git_commit -m 'rename' $ rm -r d1 $ echo c > d1 $ git add --all d1 $ fn_git_commit -m 'replace a dir with a file' $ cd .. $ git init -q --bare repo.git $ hg clone gitrepo hgrepo importing 3 git commits new changesets d4d3d2417141:541f27994b81 (3 drafts) updating to bookmark master 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --template 'adds: {file_adds}\ndels: {file_dels}\n' adds: d1 dels: d1/f1 adds: d2/f2 dels: d1/f2 adds: d1/f1 d1/f2 dels: $ hg debug-remove-hggit-state clearing out the git cache data $ hg push ../repo.git pushing to ../repo.git searching for changes adding objects remote: found 0 deltas to reuse added 3 commits with 6 trees and 3 blobs adding reference refs/heads/master $ cd .. $ git --git-dir=repo.git log --pretty=medium commit 6e0dbd8cd92ed4823c69cb48d8a2b81f904e6e69 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 replace a dir with a file commit a1874d5cd0b1549ed729e36f0da4a93ed36259ee Author: test Date: Mon Jan 1 00:00:11 2007 +0000 rename commit 102c17a5deda49db3f10ec5573f9378867098b7c Author: test Date: Mon Jan 1 00:00:10 2007 +0000 initial ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-url-parsing.py0000755000000000000000000001061614751647721014600 0ustar00import sys try: import dulwich except ImportError: print("skipped: missing feature: dulwich") sys.exit(80) import os, tempfile, unittest, shutil from mercurial import ui, hg, commands, pycompat sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir)) from hggit.git_handler import GitHandler class TestUrlParsing(object): def setUp(self): # create a test repo location. self.tmpdir = tempfile.mkdtemp('hg-git_url-test').encode('utf-8') commands.init(ui.ui(), self.tmpdir) repo = hg.repository(ui.ui(), self.tmpdir) self.handler = GitHandler(repo, ui.ui()) def tearDown(self): # remove the temp repo shutil.rmtree(self.tmpdir) def assertEquals(self, l, r): """assert equality of l and r, converting bytes to str This is so we don't have to adapt the whole .t output. """ ls = pycompat.strurl(l) rs = pycompat.strurl(r) print('%% expect %r' % (r, )) print(ls) assert ls == rs, (l, r) def test_ssh_github_style_slash(self): url = b"git+ssh://git@github.com/webjam/webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_github_style_colon_number_starting_username(self): url = b"git+ssh://git@github.com:42qu/vps.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, '42qu/vps.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_github_style_colon(self): url = b"git+ssh://git@github.com:webjam/webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, 'webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_heroku_style(self): url = b"git+ssh://git@heroku.com:webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') # also test that it works even if heroku isn't in the name url = b"git+ssh://git@compatible.com:webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@compatible.com') def test_ssh_heroku_style_with_trailing_slash(self): # some versions of mercurial add a trailing slash even if # the user didn't supply one. url = b"git+ssh://git@heroku.com:webjam.git/" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') def test_heroku_style_with_port(self): url = b"git+ssh://git@heroku.com:999:webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') self.assertEquals(client.port, '999') def test_gitdaemon_style(self): url = b"git://github.com/webjam/webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') try: self.assertEquals(client._host, 'github.com') except AttributeError: self.assertEquals(client.host, 'github.com') def test_ssh_github_style_slash_with_port(self): url = b"git+ssh://git@github.com:10022/webjam/webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') self.assertEquals(client.port, '10022') def test_gitdaemon_style_with_port(self): url = b"git://github.com:19418/webjam/webjam.git" client, path = self.handler._get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') try: self.assertEquals(client._host, 'github.com') except AttributeError: self.assertEquals(client.host, 'github.com') self.assertEquals(client._port, '19418') if __name__ == '__main__': tc = TestUrlParsing() for test in sorted(t for t in dir(tc) if t.startswith('test_')): tc.setUp() getattr(tc, test)() tc.tearDown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-url-parsing.py.out0000644000000000000000000000155714751647721015407 0ustar00% expect '/webjam/webjam.git' /webjam/webjam.git % expect 'github.com' github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'github.com' github.com % expect '19418' 19418 % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com % expect '999' 999 % expect 'webjam/webjam.git' webjam/webjam.git % expect 'git@github.com' git@github.com % expect '42qu/vps.git' 42qu/vps.git % expect 'git@github.com' git@github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'git@github.com' git@github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'git@github.com' git@github.com % expect '10022' 10022 % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com % expect 'webjam.git' webjam.git % expect 'git@compatible.com' git@compatible.com % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/test-verify-fail.t0000644000000000000000000000646714751647721014373 0ustar00Other tests make sure that gverify passes. This makes sure that gverify detects inconsistencies. Since hg-git is ostensibly correct, we artificially create inconsistencies by placing different Mercurial and Git repos in the right spots. Unfortunately, these inconsistencies rely on stuff like the file mode, which we cannot set on Windows. #require no-windows $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo normalf > normalf $ echo missingf > missingf $ echo differentf > differentf (executable in git, non-executable in hg) $ echo exef > exef $ chmod +x exef (symlink in hg, regular file in git) equivalent to 'echo -n foo > linkf', but that doesn't work on OS X $ printf foo > linkf $ git add normalf missingf differentf exef linkf $ fn_git_commit -m 'add files' $ cd .. $ hg init hgrepo $ cd hgrepo $ echo normalf > normalf $ echo differentf2 > differentf $ echo unexpectedf > unexpectedf $ echo exef > exef $ ln -s foo linkf $ hg add normalf differentf unexpectedf exef linkf $ fn_hg_commit -m 'add files' $ git clone --mirror ../gitrepo .hg/git Cloning into bare repository '.hg/git'... done. $ echo "$(cd ../gitrepo && git rev-parse HEAD) $(hg log -r . --template '{node}')" >> .hg/git-mapfile $ hg gverify verifying rev 3f1601c3cf54 against git commit 039c1cd9fdda382c9d1e8ec85de6b5b59518ca80 difference in: differentf file has different flags: exef (hg '', git 'x') file has different flags: linkf (hg 'l', git '') file found in git but not hg: missingf file found in hg but not git: unexpectedf [1] $ echo newf > newf $ hg add newf $ fn_hg_commit -m 'new hg commit' $ hg gverify abort: no git commit found for rev 4e582b4eb862 (if this is an octopus merge, verify against the last rev) [255] invalid git SHA $ echo "ffffffffffffffffffffffffffffffffffffffff $(hg log -r . --template '{node}')" >> .hg/git-mapfile $ hg gverify abort: git equivalent ffffffffffffffffffffffffffffffffffffffff for rev 4e582b4eb862 not found! [255] git SHA is not a commit $ echo new2 >> newf $ fn_hg_commit -m 'new hg commit 2' this gets the tree pointed to by the commit at HEAD $ echo "$(cd ../gitrepo && git show --format=%T HEAD | head -n 1) $(hg log -r . --template '{node}')" >> .hg/git-mapfile $ hg gverify abort: git equivalent f477b00e4a9907617f346a529cc0fe9ba5d6f6d3 for rev 5c2eb98af3e2 is not a commit! [255] corrupt git repository $ hg debug-remove-hggit-state clearing out the git cache data $ hg gexport $ mv .hg/git/objects/pack $TESTTMP/pack-old $ for packfile in $TESTTMP/pack-old/*.pack > do > git --git-dir .hg/git unpack-objects < $packfile > done $ mv -f .hg/git/objects/82/166b4cbde0f025d20aacb93fd085aa1462cd4e .hg/git/objects/6d/ff77b710b6f0961ac0b6d91d85902195133d74 $ hg gverify --fsck abort: git repository is corrupt! [255] $ hg gverify abort: git equivalent 6dff77b710b6f0961ac0b6d91d85902195133d74 for rev 5c2eb98af3e2 is not a commit! [255] $ chmod +w .hg/git/objects/6d/ff77b710b6f0961ac0b6d91d85902195133d74 $ echo 42 > .hg/git/objects/6d/ff77b710b6f0961ac0b6d91d85902195133d74 $ hg gverify abort: git equivalent 6dff77b710b6f0961ac0b6d91d85902195133d74 for rev 5c2eb98af3e2 is corrupt! (re-run with --traceback for details) [255] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/daemonize.py0000755000000000000000000000061714751647721015001 0ustar00#!/usr/bin/env python3 # # Call another command and save its pid # import os import subprocess import sys with open(sys.argv[1], "xb") as outf: proc = subprocess.Popen( [sys.executable] + sys.argv[2:], stdout=outf, stderr=outf, close_fds=True, start_new_session=True, ) with open(os.getenv("DAEMON_PIDS"), "a") as fp: fp.write(f"{proc.pid}\n") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/dulwich-serve.py0000755000000000000000000000145614751647721015611 0ustar00#!/usr/bin/env python3 # # Wrapper for dulwich.web that forks a web server in a subprocess and # saves the PID. # import os import sys from dulwich import log_utils from dulwich import repo from dulwich import server from dulwich import web import dulwich class DirBackend(server.Backend): def open_repository(self, path): return repo.Repo(path[1:]) gitdir = os.getcwd() port = int(sys.argv[1]) log_utils.default_logging_config() log_utils.getLogger().info( f"serving {gitdir} on port {port} using dulwich v" + ".".join(map(str, dulwich.__version__)) ) backend = DirBackend() app = web.make_wsgi_chain(backend) server = web.make_server( "localhost", port, app, handler_class=web.WSGIRequestHandlerLogger, server_class=web.WSGIServerLogger, ) server.serve_forever() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/dumbhttp.py0000755000000000000000000000524314751647721014655 0ustar00#!/usr/bin/env python """ Small and dumb HTTP server for use in tests. """ import optparse import os import signal import socket import sys from mercurial import ( encoding, pycompat, server, util, ) httpserver = util.httpserver OptionParser = optparse.OptionParser if os.environ.get('HGIPV6', '0') == '1': class simplehttpserver(httpserver.httpserver): address_family = socket.AF_INET6 else: simplehttpserver = httpserver.httpserver class _httprequesthandler(httpserver.simplehttprequesthandler): def log_message(self, format, *args): httpserver.simplehttprequesthandler.log_message(self, format, *args) sys.stderr.flush() class simplehttpservice: def __init__(self, host, port): self.address = (host, port) def init(self): self.httpd = simplehttpserver(self.address, _httprequesthandler) def run(self): self.httpd.serve_forever() if __name__ == '__main__': parser = OptionParser() parser.add_option( '-p', '--port', dest='port', type='int', default=8000, help='TCP port to listen on', metavar='PORT', ) parser.add_option( '-H', '--host', dest='host', default='localhost', help='hostname or IP to listen on', metavar='HOST', ) parser.add_option('--logfile', help='file name of access/error log') parser.add_option( '--pid', dest='pid', help='file name where the PID of the server is stored', ) parser.add_option( '-f', '--foreground', dest='foreground', action='store_true', help='do not start the HTTP server in the background', ) parser.add_option('--daemon-postexec', action='append') (options, args) = parser.parse_args() signal.signal(signal.SIGTERM, lambda x, y: sys.exit(0)) if options.foreground and options.logfile: parser.error( "options --logfile and --foreground are mutually " "exclusive" ) if options.foreground and options.pid: parser.error("options --pid and --foreground are mutually exclusive") opts = { b'pid_file': options.pid, b'daemon': not options.foreground, b'daemon_postexec': pycompat.rapply( encoding.strtolocal, options.daemon_postexec ), } service = simplehttpservice(options.host, options.port) runargs = [sys.executable, __file__] + sys.argv[1:] runargs = [pycompat.fsencode(a) for a in runargs] server.runservice( opts, initfn=service.init, runfn=service.run, logfile=options.logfile, runargs=runargs, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/ext-break-git-import.py0000644000000000000000000000164714751647721017002 0ustar00"""This is a small custom extension that allows stopping a hg-git conversion after a specific number of commits. Configure it using the following environment variables: - ABORT_AFTER - EXIT_AFTER """ import os from mercurial import error, extensions counter = 0 hggit = extensions.find(b"hggit") def wrap(orig, *args, **kwargs): global counter counter += 1 try: return orig(*args, **kwargs) finally: abort_after = int(os.getenv("ABORT_AFTER", "0")) exit_after = int(os.getenv("EXIT_AFTER", "0")) if abort_after and counter > abort_after: raise error.Abort(b"aborted after %d commits!" % abort_after) elif exit_after and counter > exit_after: raise KeyboardInterrupt extensions.wrapfunction(hggit.git_handler.GitHandler, "export_hg_commit", wrap) extensions.wrapfunction( hggit.git_handler.GitHandler, "import_git_commit", wrap, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/ext-commit-extra.py0000644000000000000000000000200314751647721016221 0ustar00'''test helper extension to create commits with multiple extra fields''' from mercurial import cmdutil, commands, pycompat, scmutil cmdtable = {} try: from mercurial import registrar command = registrar.command(cmdtable) except (ImportError, AttributeError): command = cmdutil.command(cmdtable) testedwith = b'internal' @command(b'commitextra', [(b'', b'field', [], b'extra data to store', b'FIELD=VALUE'), ] + commands.commitopts + commands.commitopts2, b'commitextra') def commitextra(ui, repo, *pats, **opts): '''make a commit with extra fields''' fields = opts.get('field') extras = {} for field in fields: k, v = field.split(b'=', 1) extras[k] = v message = cmdutil.logmessage(ui, pycompat.byteskwargs(opts)) repo.commit(message, opts.get('user'), opts.get('date'), match=scmutil.match(repo[None], pats, pycompat.byteskwargs(opts)), extra=extras) return 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/ext-get-password-from-env.py0000644000000000000000000000063314751647721017765 0ustar00# # small dummy extension that obtains passwords from an environment # variable # import getpass import os import sys def newgetpass(args): try: passwd = os.environb.get(b'PASSWD', b'nope') print(passwd.encode()) except AttributeError: # python 2.7 passwd = os.environ.get('PASSWD', 'nope') print(passwd) sys.stdout.flush() return passwd getpass.getpass = newgetpass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testlib/hooks.py0000644000000000000000000000075314751647721014147 0ustar00import pprint from mercurial import pycompat def showargs(ui, repo, hooktype, **kwargs): if not kwargs: ui.write(b'| %s\n' % hooktype) for k, v in pycompat.byteskwargs(kwargs).items(): if k in (b"txnid", b"changes"): # ignore these; they are either unstable or too verbose continue if not isinstance(v, bytes): v = repr(v).encode('ascii', errors='backslashreplace') ui.write(b'| %s.%s=%s\n' % (hooktype, k, v)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739018193.0 hg-git-1.2.0/tests/testutil0000755000000000000000000000625114751647721012606 0ustar00#!/bin/sh # This file holds logic that is used in many tests. # It can be called in a test like this: # $ . "$TESTDIR/testutil" # force terminal width, otherwise terminal width may cause progress # tests to fail export COLUMNS=80 # our test suite relies on being able to create symlinks, even on # Windows export MSYS=winsymlinks:nativestrict # Always trust root - which may own the repository we're working off echo "[trusted]" >> $HGRCPATH echo "users=root" >> $HGRCPATH # silence some output related to templates mkdir -p $TESTTMP/gittemplates export GIT_TEMPLATE_DIR=$TESTTMP/gittemplates # Activate extensions echo "[extensions]" >> $HGRCPATH echo "hggit=$(echo $(dirname $TESTDIR))/hggit" >> $HGRCPATH # Enable git subrepository echo '[subrepos]' >> $HGRCPATH echo 'git:allowed = yes' >> $HGRCPATH # silence warning from recent git cat >> $TESTTMP/.gitconfig </dev/null || echo "git commit error" count=`expr $count + 1` } fn_git_rebase() { GIT_AUTHOR_DATE="$(fn_get_date)" GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git rebase --quiet "$@" >/dev/null || echo "git rebase error" count=`expr $count + 1` } fn_hg_commit() { HGDATE="$(fn_get_date)" hg commit -d "$HGDATE" "$@" >/dev/null || echo "hg commit error" count=`expr $count + 1` } fn_hg_commitextra() { HGDATE="$(fn_get_date)" hg --config extensions.commitextra=$TESTDIR/testlib/ext-commit-extra.py \ commitextra -d "$HGDATE" "$@" >/dev/null || echo "hg commit error" count=`expr $count + 1` } fn_git_tag() { GIT_AUTHOR_DATE="$(fn_get_date)" GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git tag "$@" >/dev/null || echo "git tag error" count=`expr $count + 1` } fn_hg_tag() { HGDATE="$(fn_get_date)" hg tag -d "$HGDATE" "$@" >/dev/null || echo "hg tag error" count=`expr $count + 1` } fn_touch_escaped() { python - "$@" <