# HG changeset patch # User Andrii Verbytskyi # Date 2024-12-24 19:09:27 # Node ID cdbc80b0143924cfdd306f5603a2d3ffcca64a13 # Parent 55426ac31096ef96c1d54e0b1d09b08ff33dd9eb # Parent 5aecabed7d8889681e8e94f801e3192696784bde release: version 5.4.0 diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 --- a/.bumpversion.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bumpversion] -current_version = 5.4.0 -message = release: Bump version {current_version} to {new_version} - -[bumpversion:file:rhodecode/VERSION] diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ clean: # test: run test-clean and tests test: make test-clean - unset RC_SQLALCHEMY_DB1_URL && unset RC_DB_URL && make test-only + make test-only .PHONY: test-clean @@ -71,7 +71,25 @@ test-only: PYTHONHASHSEED=random \ py.test -x -vv -r xw -p no:sugar \ --cov-report=term-missing --cov-report=html \ - --cov=rhodecode rhodecode + --cov=rhodecode rhodecode \ + --ignore=rhodecode/tests/vcs_operations \ + --ignore=rhodecode/tests/database + + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar \ + rhodecode/tests/vcs_operations + + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar \ + rhodecode/tests/database + +.PHONY: test-simple +# test-simple: Run tests only for main test suite witout coverage +test-simple: + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar \ + --ignore=rhodecode/tests/vcs_operations \ + --ignore=rhodecode/tests/database # >>> Docs commands diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -485,6 +485,10 @@ rc_cache.cache_general.expiration_time = ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_general.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_general.arguments.key_prefix = custom-prefix- + + ; ************************************************* ; `cache_perms` cache for permission tree, auth TTL ; for simplicity use rc.file_namespace backend, @@ -512,6 +516,10 @@ rc_cache.cache_perms.expiration_time = 3 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_perms.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_perms.arguments.key_prefix = custom-prefix- + + ; *************************************************** ; `cache_repo` cache for file tree, Readme, RSS FEEDS ; for simplicity use rc.file_namespace backend, @@ -539,6 +547,40 @@ rc_cache.cache_repo.expiration_time = 25 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_repo.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_repo.arguments.key_prefix = custom-prefix- + +; ********************************************* +; `cache_license` cache for storing license info +; for simplicity use rc.file_namespace backend, +; for performance and scale use rc.redis +; ********************************************* +rc_cache.cache_license.backend = dogpile.cache.rc.file_namespace +rc_cache.cache_license.expiration_time = 300 +; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set +#rc_cache.cache_license.arguments.filename = /tmp/cache_general_db + +; alternative `cache_license` redis backend with distributed lock +#rc_cache.cache_license.backend = dogpile.cache.rc.redis +#rc_cache.cache_license.expiration_time = 300 + +; redis_expiration_time needs to be greater then expiration_time +#rc_cache.cache_license.arguments.redis_expiration_time = 360 + +#rc_cache.cache_license.arguments.host = localhost +#rc_cache.cache_license.arguments.port = 6379 +#rc_cache.cache_license.arguments.db = 0 +#rc_cache.cache_license.arguments.socket_timeout = 30 +; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends +#rc_cache.cache_license.arguments.distributed_lock = true + +; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen +#rc_cache.cache_license.arguments.lock_auto_renewal = true + +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_license.arguments.key_prefix = custom-prefix- + + ; ############## ; BEAKER SESSION ; ############## @@ -547,7 +589,7 @@ rc_cache.cache_repo.expiration_time = 25 ; types are file, ext:redis, ext:database, ext:memcached ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session #beaker.session.type = file -#beaker.session.data_dir = %(here)s/data/sessions +#beaker.session.data_dir = /var/opt/rhodecode_data/sessions ; Redis based sessions beaker.session.type = ext:redis diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -453,6 +453,10 @@ rc_cache.cache_general.expiration_time = ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_general.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_general.arguments.key_prefix = custom-prefix- + + ; ************************************************* ; `cache_perms` cache for permission tree, auth TTL ; for simplicity use rc.file_namespace backend, @@ -480,6 +484,10 @@ rc_cache.cache_perms.expiration_time = 3 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_perms.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_perms.arguments.key_prefix = custom-prefix- + + ; *************************************************** ; `cache_repo` cache for file tree, Readme, RSS FEEDS ; for simplicity use rc.file_namespace backend, @@ -507,6 +515,40 @@ rc_cache.cache_repo.expiration_time = 25 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_repo.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_repo.arguments.key_prefix = custom-prefix- + +; ********************************************* +; `cache_license` cache for storing license info +; for simplicity use rc.file_namespace backend, +; for performance and scale use rc.redis +; ********************************************* +rc_cache.cache_license.backend = dogpile.cache.rc.file_namespace +rc_cache.cache_license.expiration_time = 300 +; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set +#rc_cache.cache_license.arguments.filename = /tmp/cache_general_db + +; alternative `cache_license` redis backend with distributed lock +#rc_cache.cache_license.backend = dogpile.cache.rc.redis +#rc_cache.cache_license.expiration_time = 300 + +; redis_expiration_time needs to be greater then expiration_time +#rc_cache.cache_license.arguments.redis_expiration_time = 360 + +#rc_cache.cache_license.arguments.host = localhost +#rc_cache.cache_license.arguments.port = 6379 +#rc_cache.cache_license.arguments.db = 0 +#rc_cache.cache_license.arguments.socket_timeout = 30 +; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends +#rc_cache.cache_license.arguments.distributed_lock = true + +; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen +#rc_cache.cache_license.arguments.lock_auto_renewal = true + +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_license.arguments.key_prefix = custom-prefix- + + ; ############## ; BEAKER SESSION ; ############## @@ -515,7 +557,7 @@ rc_cache.cache_repo.expiration_time = 25 ; types are file, ext:redis, ext:database, ext:memcached ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session #beaker.session.type = file -#beaker.session.data_dir = %(here)s/data/sessions +#beaker.session.data_dir = /var/opt/rhodecode_data/sessions ; Redis based sessions beaker.session.type = ext:redis diff --git a/conftest.py b/conftest.py --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,8 +27,11 @@ from rhodecode.tests.conftest_common imp pytest_plugins = [ - "rhodecode.tests.fixture_mods.fixture_pyramid", - "rhodecode.tests.fixture_mods.fixture_utils", + "rhodecode.tests.fixtures.fixture_pyramid", + "rhodecode.tests.fixtures.fixture_utils", + "rhodecode.tests.fixtures.function_scoped_baseapp", + "rhodecode.tests.fixtures.module_scoped_baseapp", + "rhodecode.tests.fixtures.rcextensions_fixtures", ] diff --git a/docs/.howto b/docs/.howto --- a/docs/.howto +++ b/docs/.howto @@ -3,4 +3,4 @@ # docker build --tag sphinx-doc-build-rc . # Build Docs -# docker run --rm -v $(PWD):/project --workdir=/project/docs sphinx-doc-build-rc make clean html \ No newline at end of file +# cd && docker run --rm -v $(pwd):/project --workdir=/project/docs sphinx-doc-build-rc make clean html diff --git a/docs/install/migrate-repos.rst b/docs/install/migrate-repos.rst --- a/docs/install/migrate-repos.rst +++ b/docs/install/migrate-repos.rst @@ -2,22 +2,24 @@ Migrating |repos| ----------------- - -If you have installed |RCE| and have |repos| that you wish to migrate into -the system, use the following instructions. +When you install |RCE| you will be presented with 2 choices: -1. On the |RCE| interface, check your |repo| storage location under - :menuselection:`Admin --> Settings --> System Info`. For example, - Storage location: /home/{username}/repos. +1. Mount existing folder on your local filesystem, and this is what |RCE| will use to store your repos. In this case |RCE| will automatically import repos present in that folder. You can also do :ref:`remap-rescan` from admin interface to force new repos discovery. +2. Use docker named volume ``rc_reposvolume`` - in this case you can either set create new |repo| in |RCE| UI and set it as a remote for your local repo, and then push. If your repo is hosted somewhere else, click "Import Existing Repository ?" and provide url to it. |RCE| will attempt to pull it and import. -2. Copy the |repos| that you want |RCE| to manage to this location. -3. Remap and rescan the |repos|, see :ref:`remap-rescan` .. important:: + Repo storage path inside docker is configurable in ``rhodecode.ini`` file: + ``repo_store.path = /var/opt/rhodecode_repo_store`` - Directories create |repo| groups inside |RCE|. + Mounting of local filesystem folder is configurable in ``docker-compose-apps.override.yaml`` + + Importing adds |RCE| git hooks to your |repos|. - Importing adds |RCE| git hooks to your |repos|. + You should verify if custom ``.hg`` or ``.hgrc`` files inside + repositories should be adjusted since |RCE| reads the content of them. - You should verify if custom ``.hg`` or ``.hgrc`` files inside - repositories should be adjusted since |RCE| reads the content of them. +.. note:: + There's another override file for dev setup, which mounts default workspace folder as a repo storage by default. + you can configure this by changing ``docker-compose-apps.dev.yaml`` + (and commenting out all lines ``$WORKSPACE_HOME:/var/opt/rhodecode_repo_store:delegated``) diff --git a/docs/release-notes/release-notes-5.3.1.rst b/docs/release-notes/release-notes-5.3.1.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-5.3.1.rst @@ -0,0 +1,41 @@ +|RCE| 5.3.1 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2024-09-21 + + +New Features +^^^^^^^^^^^^ + + + +General +^^^^^^^ + + + +Security +^^^^^^^^ + + + +Performance +^^^^^^^^^^^ + +- svn: small performance bump by restoring path cache in previous builds + + +Fixes +^^^^^ + +- Fixed problems with incorrect version string +- Fixed accidentally removed svn path check cache + + +Upgrade notes +^^^^^^^^^^^^^ + +- RhodeCode 5.3.1 is unscheduled security release to address some build issues with 5.X images diff --git a/docs/release-notes/release-notes-5.4.0.rst b/docs/release-notes/release-notes-5.4.0.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-5.4.0.rst @@ -0,0 +1,49 @@ +|RCE| 5.4.0 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + + + + +New Features +^^^^^^^^^^^^ + +- RhodeCode and VcsServer operate now on bytestrings instead of strings for diffs and files. This allows a mixed or non-utf8 + encoded files to be used. +- Remap and rescan: added more resilient remap and removal option, and also split the logic to either add or cleanup. +- Automatic merge of approved pull requests (available only for enterprise edition). +- Basic implementation of security scan (secrets, keys, passwords, etc) for repositories (available only for enterprise edition). + +General +^^^^^^^ + +- Ported build system to use pyproject.toml. +- Bumped version of mercurial and hg-evolve. + + +Security +^^^^^^^^ + +- Multiple library bumps to fix security issues found in those python packages. + + +Performance +^^^^^^^^^^^ + +- SVN: added same caches for stored file nodes as git and mercurial have for improved performance. +- GIT: moved certain operations into vcsserver + +Fixes +^^^^^ + +- Fixed UI visualization issues with long commit messages +- Fixed masking for some fields in admin settings +- Fixed issues with caches calculations when passed search fields were empty +- Fixed observer's ability to change a PR status + +Upgrade notes +^^^^^^^^^^^^^ + + diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,8 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-5.4.0.rst + release-notes-5.3.1.rst release-notes-5.3.0.rst release-notes-5.2.1.rst release-notes-5.2.0.rst diff --git a/docs/requirements_docs.txt b/docs/requirements_docs.txt --- a/docs/requirements_docs.txt +++ b/docs/requirements_docs.txt @@ -8,4 +8,4 @@ pygments==2.18.0 docutils<0.19 markupsafe==2.1.3 -jinja2==3.1.2 +jinja2==3.1.4 diff --git a/docs/tools/tools-cli.rst b/docs/tools/tools-cli.rst --- a/docs/tools/tools-cli.rst +++ b/docs/tools/tools-cli.rst @@ -12,6 +12,26 @@ The commands available with |RCT| can be - Local configuration commands used to help set up your |RCT| configuration. +.. _tools-rhodecode-list-instance: + +rhodecode-list-instances +------------------------ + +Use this command to list the instance details configured in the +:file:`/etc/rhodecode/conf/.rhoderc` file. + +.. code-block:: bash + + $ ./rcstack cli cmd rhodecode-list-instances --config=/etc/rhodecode/conf/.rhoderc + [instance:production] - Config only + API-HOST: https://some.url.com + API-KEY: some.auth.token + + [instance:development] - Config only + API-HOST: http://some.ip.address + API-KEY: some.auth.token + + rhodecode-tools --------------- @@ -163,6 +183,7 @@ Example usage: removing gist /home/brian/repos/.rc_gist_store/5 removing gist /home/brian/repos/.rc_gist_store/8FtCKdcbRKmEvRzTVsEt + rhodecode-cleanup-repos ----------------------- @@ -201,30 +222,37 @@ Example usage: .. code-block:: bash - # Cleaning up repos using tools installed with RCE 350 and above - $ ~/.rccontrol/enterprise-4/profile/bin/rhodecode-cleanup-repos \ - --instance-name=enterprise-4 --older-than=1d - Scanning for repositories in /home/brian/repos... - preparing to remove [2] found repositories older than 1 day, 0:00:00 (1d) + + # create a .rhoderc file in your host config directory (in :file:`config/_shared/.rhoderc`): + + [instance:rcstack-instance] + api_host = http://rhodecode:10020 + api_key = + repo_dir = /var/opt/rhodecode_repo_store + + # Run rcstack cli + ./rcstack cli cmd rhodecode-cleanup-repos --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc + + checking if config files needs bootstrapping + Scanning for repositories in /var/opt/rhodecode_repo_store... the following repositories will be deleted completely: - * REMOVED: 2015-08-05 00:23:18 | /home/brian/repos/rm__20150805_002318_831 - * REMOVED: 2015-08-04 01:22:10 | /home/brian/repos/rm__20150804_012210_336 + * REMOVED: 2015-08-05 00:23:18 | /var/opt/rhodecode_repo_store/rm__20150805_002318_831 + * REMOVED: 2015-08-04 01:22:10 | /var/opt/rhodecode_repo_store/rm__20150804_012210_336 are you sure you want to remove them? [y/N]: # Clean up repos older than 1 year - # If using virtualenv and pre RCE 350 tools installation - (venv)$ rhodecode-cleanup-repos --instance-name=enterprise-1 \ - --older-than=365d + ./rcstack cli cmd rhodecode-cleanup-repos --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc --older-than=365d - Scanning for repositories in /home/brian/repos... + checking if config files needs bootstrapping + Scanning for repositories in /var/opt/rhodecode_repo_store... preparing to remove [343] found repositories older than 365 days # clean up repos older than 3 days - # If using virtualenv and pre RCE 350 tools installation - (venv)$ rhodecode-cleanup-repos --instance-name=enterprise-1 \ - --older-than=3d - Scanning for repositories in /home/brian/repos... + ./rcstack cli cmd rhodecode-cleanup-repos --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc --older-than=3d + + checking if config files needs bootstrapping + Scanning for repositories in /var/opt/rhodecode_repo_store... preparing to remove [3] found repositories older than 3 days .. _tools-config: @@ -420,9 +448,8 @@ Example usage: } # Cat a file and pipe to gist - # in RCE 3.5.0 tools and above - $ cat ~/.rhoderc | ~/.rccontrol/{instance-id}/profile/bin/rhodecode-gist \ - --instance-name=enterprise-4 -d '.rhoderc copy' create + + $ cat ~/.rhoderc | ./rcstack cli cmd rhodecode-gist --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc -d '.rhoderc copy' create { "error": null, "id": 9253, @@ -512,41 +539,18 @@ Example usage: .. code-block:: bash - # Run the indexer - $ ~/.rccontrol/enterprise-4/profile/bin/rhodecode-index \ - --instance-name=enterprise-4 + # Create the indexing mapping file + $ ./rcstack cli cmd rhodecode-index --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc --create-mapping search_mapping.ini - # Run indexer based on search_mapping.ini file - # This is using pre-350 virtualenv - (venv)$ rhodecode-index --instance-name=enterprise-1 + # Run the indexer + $ ./rcstack cli cmd rhodecode-index --instance-name=rcstack-instance --config=/etc/rhodecode/conf/.rhoderc + + # Run indexer based on search_mapping.ini file using rhodecode-tools virtualenv + (venv)$ rhodecode-index --instance-name=rcstack-instance # Index from the command line without creating # the .rhoderc file - $ rhodecode-index --apikey=key --apihost=http://rhodecode.server \ - --instance-name=enterprise-2 --save-config - - # Create the indexing mapping file - $ ~/.rccontrol/enterprise-4/profile/bin/rhodecode-index \ - --create-mapping search_mapping.ini --instance-name=enterprise-4 - -.. _tools-rhodecode-list-instance: - -rhodecode-list-instances ------------------------- - -Use this command to list the instance details configured in the -:file:`~/.rhoderc` file. - -.. code-block:: bash - - $ .rccontrol/enterprise-1/profile/bin/rhodecode-list-instances - [instance:production] - Config only - API-HOST: https://some.url.com - API-KEY: some.auth.token - - [instance:development] - Config only - API-HOST: http://some.ip.address - API-KEY: some.auth.token + $ rhodecode-index --apikey=key --apihost=http://rhodecode.server --instance-name=rcstack-instance --save-config .. _tools-setup-config: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,137 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel", "pastescript"] +build-backend = "setuptools.build_meta" + +[project] +name = "rhodecode-enterprise-ce" +description = "Enterprise Source Code Management Platform" +authors = [ + {name = "RhodeCode GmbH", email = "support@rhodecode.com"}, +] +keywords = [ + 'rhodecode', 'mercurial', 'git', 'svn', + 'code review', + 'repo groups', 'ldap', 'repository management', 'hgweb', + 'hgwebdir', 'gitweb', 'serving hgweb', +] +license = {text = "GPL V3"} +requires-python = ">=3.10" +dynamic = ["version", "readme", "dependencies", "optional-dependencies"] +classifiers = [ + 'Development Status :: 6 - Mature', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Version Control', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Programming Language :: Python :: 3.11', +] + +[project.entry-points."paste.app_factory"] +main = "rhodecode.config.middleware:make_pyramid_app" + +[project.entry-points."pyramid.pshell_runner"] +ipython = "rhodecode.lib.pyramid_shell:ipython_shell_runner" + +[project.entry-points."beaker.backends"] +memorylru_base="rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase" +memorylru_debug="rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerDebug" + +[project.scripts] + +rc-setup-app = "rhodecode.lib.rc_commands.setup_rc:main" +rc-upgrade-db = "rhodecode.lib.rc_commands.upgrade_db:main" +rc-ishell = "rhodecode.lib.rc_commands.ishell:main" +rc-add-artifact = "rhodecode.lib.rc_commands.add_artifact:main" +rc-migrate-artifact = "rhodecode.lib.rc_commands.migrate_artifact:main" +rc-ssh-wrapper = "rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main" +rc-ssh-wrapper-v2 = "rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main" + +[tool.setuptools] +packages = ["rhodecode"] +include-package-data = true + +[tools.setuptools.package-data] +"" = ['*.txt', '*.rst'] +"configs" = ['*.ini'] +"rhodecode" = ['VERSION', 'i18n/*/LC_MESSAGES/*.mo', ] + +[tool.setuptools.exclude-package-data] +"rhodecode" = ["__pycache__"] + +[tool.setuptools.dynamic] +readme = {file = ["README.rst"], content-type = "text/rst"} +version = {file = "rhodecode/VERSION"} +dependencies = {file = ["requirements.txt"]} +optional-dependencies.tests = {file = ["requirements_test.txt"]} + +[tool.ruff] +# Line length max same as Black. +line-length = 120 + +# Assume Python 3.11 +target-version = "py311" + + +[tool.ruff.lint] +select = [ + # Pyflakes + "F", + # Pycodestyle + "E", + "W", + # isort + "I001" +] + +ignore = [ + "E501", # line too long, handled by black +] + +[tool.ruff.lint.isort] +known-first-party = ["rhodecode"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + + +[tool.bumpversion] +current_version = "5.4.0" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = false +sign_tags = false +tag_name = "v{new_version}" +tag_message = "release(version-bump): {current_version} → {new_version}" +allow_dirty = false +commit = false +message = "release(version-bump): {current_version} → {new_version}" +commit_args = "" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] + + +# message_extractors={ +# 'rhodecode': [ +# ('**.py', 'python', None), +# ('**.js', 'javascript', None), +# ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}), +# ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}), +# ('public/**', 'ignore', None), +# ] +# }, diff --git a/pytest.ini b/pytest.ini --- a/pytest.ini +++ b/pytest.ini @@ -4,8 +4,10 @@ norecursedirs = rhodecode/public rhodeco cache_dir = /tmp/.pytest_cache pyramid_config = rhodecode/tests/rhodecode.ini -vcsserver_protocol = http -vcsserver_config_http = rhodecode/tests/vcsserver_http.ini + +vcsserver_config = rhodecode/tests/vcsserver_http.ini +rhodecode_config = rhodecode/tests/rhodecode.ini +celery_config = rhodecode/tests/rhodecode.ini addopts = --pdbcls=IPython.terminal.debugger:TerminalPdb diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,39 @@ # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]' alembic==1.13.1 - mako==1.2.4 - markupsafe==2.1.2 + mako==1.3.6 + markupsafe==3.0.2 sqlalchemy==1.4.52 - greenlet==3.0.3 + greenlet==3.1.1 typing_extensions==4.12.2 async-timeout==4.0.3 babel==2.12.1 -beaker==1.12.1 -celery==5.3.6 - billiard==4.2.0 - click==8.1.3 - click-didyoumean==0.3.0 - click==8.1.3 +beaker==1.13.0 +celery==5.4.0 + billiard==4.2.1 + click==8.1.7 + click-didyoumean==0.3.1 + click==8.1.7 click-plugins==1.1.1 - click==8.1.3 - click-repl==0.2.0 - click==8.1.3 - prompt_toolkit==3.0.47 + click==8.1.7 + click-repl==0.3.0 + click==8.1.7 + prompt_toolkit==3.0.48 wcwidth==0.2.13 - six==1.16.0 - kombu==5.3.5 + kombu==5.4.2 amqp==5.2.0 vine==5.1.0 + tzdata==2024.2 vine==5.1.0 - python-dateutil==2.8.2 + python-dateutil==2.9.0.post0 six==1.16.0 - tzdata==2024.1 + tzdata==2024.2 vine==5.1.0 channelstream==0.7.1 - gevent==24.2.1 - greenlet==3.0.3 + gevent==24.11.1 + greenlet==3.1.1 zope.event==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 itsdangerous==1.1.0 marshmallow==2.18.0 pyramid==2.0.2 @@ -43,14 +43,14 @@ channelstream==0.7.1 pastedeploy==3.1.0 plaster==1.1.2 translationstring==1.4 - venusian==3.0.0 - webob==1.8.7 + venusian==3.1.0 + webob==1.8.9 zope.deprecation==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 pyramid-jinja2==2.10 - jinja2==3.1.2 - markupsafe==2.1.2 - markupsafe==2.1.2 + jinja2==3.1.4 + markupsafe==3.0.2 + markupsafe==3.0.2 pyramid==2.0.2 hupper==1.12 plaster==1.1.2 @@ -58,18 +58,18 @@ channelstream==0.7.1 pastedeploy==3.1.0 plaster==1.1.2 translationstring==1.4 - venusian==3.0.0 - webob==1.8.7 + venusian==3.1.0 + webob==1.8.9 zope.deprecation==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 zope.deprecation==5.0.0 - python-dateutil==2.8.2 + python-dateutil==2.9.0.post0 six==1.16.0 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.1.0 - idna==3.4 - urllib3==1.26.14 + requests==2.32.3 + certifi==2024.8.30 + charset-normalizer==3.4.0 + idna==3.10 + urllib3==1.26.20 ws4py==0.5.1 deform==2.0.15 chameleon==3.10.2 @@ -87,13 +87,13 @@ dogpile.cache==1.3.3 pbr==5.11.1 formencode==2.1.0 six==1.16.0 -fsspec==2024.9.0 +fsspec==2024.10.0 gunicorn==23.0.0 packaging==24.1 -gevent==24.2.1 - greenlet==3.0.3 +gevent==24.11.1 + greenlet==3.1.1 zope.event==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 ipython==8.26.0 decorator==5.1.1 jedi==0.19.1 @@ -102,7 +102,7 @@ ipython==8.26.0 traitlets==5.14.3 pexpect==4.9.0 ptyprocess==0.7.0 - prompt_toolkit==3.0.47 + prompt_toolkit==3.0.48 wcwidth==0.2.13 pygments==2.18.0 stack-data==0.6.3 @@ -113,7 +113,7 @@ ipython==8.26.0 traitlets==5.14.3 typing_extensions==4.12.2 markdown==3.4.3 -msgpack==1.0.8 +msgpack==1.1.0 mysqlclient==2.1.1 nbconvert==7.7.3 beautifulsoup4==4.12.3 @@ -122,23 +122,23 @@ nbconvert==7.7.3 six==1.16.0 webencodings==0.5.1 defusedxml==0.7.1 - jinja2==3.1.2 - markupsafe==2.1.2 + jinja2==3.1.4 + markupsafe==3.0.2 jupyter_core==5.3.1 platformdirs==3.10.0 traitlets==5.14.3 jupyterlab-pygments==0.2.2 - markupsafe==2.1.2 + markupsafe==3.0.2 mistune==2.0.5 nbclient==0.8.0 jupyter_client==8.3.0 jupyter_core==5.3.1 platformdirs==3.10.0 traitlets==5.14.3 - python-dateutil==2.8.2 + python-dateutil==2.9.0.post0 six==1.16.0 - pyzmq==25.0.0 - tornado==6.2 + pyzmq==26.2.0 + tornado==6.4.2 traitlets==5.14.3 jupyter_core==5.3.1 platformdirs==3.10.0 @@ -146,7 +146,7 @@ nbconvert==7.7.3 nbformat==5.9.2 fastjsonschema==2.18.0 jsonschema==4.18.6 - attrs==22.2.0 + attrs==24.2.0 pyrsistent==0.19.3 jupyter_core==5.3.1 platformdirs==3.10.0 @@ -156,7 +156,7 @@ nbconvert==7.7.3 nbformat==5.9.2 fastjsonschema==2.18.0 jsonschema==4.18.6 - attrs==22.2.0 + attrs==24.2.0 pyrsistent==0.19.3 jupyter_core==5.3.1 platformdirs==3.10.0 @@ -174,20 +174,20 @@ premailer==3.10.0 cssselect==1.2.0 cssutils==2.6.0 lxml==5.3.0 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.1.0 - idna==3.4 - urllib3==1.26.14 + requests==2.32.3 + certifi==2024.8.30 + charset-normalizer==3.4.0 + idna==3.10 + urllib3==1.26.20 psutil==5.9.8 -psycopg2==2.9.9 +psycopg2==2.9.10 py-bcrypt==0.4 pycmarkgfm==1.2.0 - cffi==1.16.0 - pycparser==2.21 -pycryptodome==3.17 + cffi==1.17.1 + pycparser==2.22 +pycryptodome==3.21.0 pycurl==7.45.3 -pymysql==1.0.3 +pymysql==1.1.1 pyotp==2.8.0 pyparsing==3.1.1 pyramid-mailer==0.15.1 @@ -198,19 +198,19 @@ pyramid-mailer==0.15.1 pastedeploy==3.1.0 plaster==1.1.2 translationstring==1.4 - venusian==3.0.0 - webob==1.8.7 + venusian==3.1.0 + webob==1.8.9 zope.deprecation==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 repoze.sendmail==4.4.1 transaction==5.0.0 - zope.interface==7.0.3 - zope.interface==7.0.3 + zope.interface==7.1.1 + zope.interface==7.1.1 transaction==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 pyramid-mako==1.1.0 - mako==1.2.4 - markupsafe==2.1.2 + mako==1.3.6 + markupsafe==3.0.2 pyramid==2.0.2 hupper==1.12 plaster==1.1.2 @@ -218,16 +218,15 @@ pyramid-mako==1.1.0 pastedeploy==3.1.0 plaster==1.1.2 translationstring==1.4 - venusian==3.0.0 - webob==1.8.7 + venusian==3.1.0 + webob==1.8.9 zope.deprecation==5.0.0 - zope.interface==7.0.3 + zope.interface==7.1.1 python-ldap==3.4.3 pyasn1==0.4.8 pyasn1-modules==0.2.8 pyasn1==0.4.8 -python-memcached==1.59 - six==1.16.0 +python-memcached==1.62 python-pam==2.0.2 python3-saml==1.16.0 isodate==0.6.1 @@ -236,60 +235,66 @@ python3-saml==1.16.0 xmlsec==1.3.14 lxml==5.3.0 pyyaml==6.0.1 -redis==5.1.0 +redis==5.2.0 async-timeout==4.0.3 regex==2022.10.31 routes==2.5.1 repoze.lru==0.7 six==1.16.0 -s3fs==2024.9.0 - aiobotocore==2.13.0 - aiohttp==3.9.5 +s3fs==2024.10.0 + aiobotocore==2.15.2 + aiohttp==3.11.4 + aiohappyeyeballs==2.4.3 aiosignal==1.3.1 - frozenlist==1.4.1 - attrs==22.2.0 - frozenlist==1.4.1 - multidict==6.0.5 - yarl==1.9.4 - idna==3.4 - multidict==6.0.5 - aioitertools==0.11.0 - botocore==1.34.106 + frozenlist==1.5.0 + attrs==24.2.0 + frozenlist==1.5.0 + multidict==6.1.0 + propcache==0.2.0 + yarl==1.17.2 + idna==3.10 + multidict==6.1.0 + propcache==0.2.0 + aioitertools==0.12.0 + botocore==1.35.36 jmespath==1.0.1 - python-dateutil==2.8.2 + python-dateutil==2.9.0.post0 six==1.16.0 - urllib3==1.26.14 + urllib3==1.26.20 wrapt==1.16.0 - aiohttp==3.9.5 + aiohttp==3.11.4 + aiohappyeyeballs==2.4.3 aiosignal==1.3.1 - frozenlist==1.4.1 - attrs==22.2.0 - frozenlist==1.4.1 - multidict==6.0.5 - yarl==1.9.4 - idna==3.4 - multidict==6.0.5 - fsspec==2024.9.0 + frozenlist==1.5.0 + attrs==24.2.0 + frozenlist==1.5.0 + multidict==6.1.0 + propcache==0.2.0 + yarl==1.17.2 + idna==3.10 + multidict==6.1.0 + propcache==0.2.0 + fsspec==2024.10.0 simplejson==3.19.2 sshpubkeys==3.3.1 - cryptography==40.0.2 - cffi==1.16.0 - pycparser==2.21 - ecdsa==0.18.0 + cryptography==43.0.3 + cffi==1.17.1 + pycparser==2.22 + ecdsa==0.19.0 six==1.16.0 sqlalchemy==1.4.52 - greenlet==3.0.3 + greenlet==3.1.1 typing_extensions==4.12.2 supervisor==4.2.5 tzlocal==4.3 pytz-deprecation-shim==0.1.0.post0 - tzdata==2024.1 + tzdata==2024.2 tempita==0.5.2 -unidecode==1.3.6 +unidecode==1.3.8 urlobject==2.4.3 -waitress==3.0.0 +waitress==3.0.1 webhelpers2==2.1 - markupsafe==2.1.2 + markupsafe==3.0.2 six==1.16.0 whoosh==2.7.4 zope.cachedescriptors==5.0.0 diff --git a/requirements_debug.txt b/requirements_debug.txt --- a/requirements_debug.txt +++ b/requirements_debug.txt @@ -15,10 +15,8 @@ pyramid-debugtoolbar flake8 ruff -pipdeptree==2.7.1 +pipdeptree invoke==2.0.0 -bumpversion==0.6.0 -bump2version==1.0.1 docutils-stubs types-redis diff --git a/requirements_rc_tools.txt b/requirements_rc_tools.txt --- a/requirements_rc_tools.txt +++ b/requirements_rc_tools.txt @@ -1,3 +1,3 @@ ## rhodecode-tools, special case, use file://PATH.tar.gz#egg=rhodecode-tools==X.Y.Z, to test local version -rhodecode-tools @ https://code.rhodecode.com/_file_store/download/0-36d03a63-36f7-47af-a33c-d81c39f88251.0.0.tar.gz#egg=rhodecode-tools -rhodecode-tools==3.0.0 +rhodecode-tools @ https://code.rhodecode.com/_file_store/download/0-51fb13c9-6175-480c-b9dc-630c7601d47c.1.0.tar.gz#egg=rhodecode-tools +rhodecode-tools==4.1.0 diff --git a/requirements_test.txt b/requirements_test.txt --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,47 +1,54 @@ # test related requirements + mock==5.1.0 -pytest-cov==4.1.0 - coverage==7.4.3 - pytest==8.1.1 - iniconfig==2.0.0 - packaging==24.1 - pluggy==1.4.0 -pytest-env==1.1.3 - pytest==8.1.1 + +pytest==8.3.3 + iniconfig==2.0.0 + packaging==24.1 + pluggy==1.5.0 +pytest-cov==5.0.0 + coverage==7.6.4 + pytest==8.3.3 iniconfig==2.0.0 packaging==24.1 - pluggy==1.4.0 -pytest-profiling==1.7.0 - gprof2dot==2022.7.29 - pytest==8.1.1 + pluggy==1.5.0 +pytest-env==1.1.5 + pytest==8.3.3 iniconfig==2.0.0 packaging==24.1 - pluggy==1.4.0 - six==1.16.0 -pytest-rerunfailures==13.0 - packaging==24.1 - pytest==8.1.1 + pluggy==1.5.0 +pytest-profiling==1.7.0 + gprof2dot==2024.6.6 + pytest==8.3.3 iniconfig==2.0.0 packaging==24.1 - pluggy==1.4.0 + pluggy==1.5.0 + six==1.16.0 +pytest-rerunfailures==14.0 + packaging==24.1 + pytest==8.3.3 + iniconfig==2.0.0 + packaging==24.1 + pluggy==1.5.0 pytest-runner==6.0.1 pytest-sugar==1.0.0 packaging==24.1 - pytest==8.1.1 + pytest==8.3.3 iniconfig==2.0.0 packaging==24.1 - pluggy==1.4.0 - termcolor==2.4.0 + pluggy==1.5.0 + termcolor==2.5.0 pytest-timeout==2.3.1 - pytest==8.1.1 + pytest==8.3.3 iniconfig==2.0.0 packaging==24.1 - pluggy==1.4.0 -webtest==3.0.0 + pluggy==1.5.0 + +webtest==3.0.1 beautifulsoup4==4.12.3 soupsieve==2.5 - waitress==3.0.0 - webob==1.8.7 + waitress==3.0.1 + webob==1.8.9 # RhodeCode test-data rc_testdata @ https://code.rhodecode.com/upstream/rc-testdata-dist/raw/77378e9097f700b4c1b9391b56199fe63566b5c9/rc_testdata-0.11.0.tar.gz#egg=rc_testdata diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -45,13 +45,20 @@ CELERY_EAGER = False # link to config for pyramid CONFIG = {} +class NotGivenMeta: + + def __repr__(self): + return 'NotGivenObject()' + __str__ = __repr__ + +NotGiven = NotGivenMeta() class ConfigGet: - NotGiven = object() - def _get_val_or_missing(self, key, missing): + @classmethod + def _get_val_or_missing(cls, key, missing): if key not in CONFIG: - if missing == self.NotGiven: + if missing != NotGiven: return missing # we don't get key, we don't get missing value, return nothing similar as config.get(key) return None @@ -74,6 +81,12 @@ class ConfigGet: val = self._get_val_or_missing(key, missing) return str2bool(val) + def get_list(self, key, missing=NotGiven): + from rhodecode.lib.type_utils import aslist + val = self._get_val_or_missing(key, missing) + return aslist(val, sep=',') + + # Populated with the settings dictionary from application init in # rhodecode.conf.environment.load_pyramid_environment PYRAMID_SETTINGS = {} diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/exc.py b/rhodecode/api/exc.py --- a/rhodecode/api/exc.py +++ b/rhodecode/api/exc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/__init__.py b/rhodecode/api/tests/__init__.py --- a/rhodecode/api/tests/__init__.py +++ b/rhodecode/api/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/conftest.py b/rhodecode/api/tests/conftest.py --- a/rhodecode/api/tests/conftest.py +++ b/rhodecode/api/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,12 +21,12 @@ import pytest from rhodecode.model.meta import Session from rhodecode.model.user import UserModel from rhodecode.model.auth_token import AuthTokenModel -from rhodecode.tests import TEST_USER_ADMIN_LOGIN @pytest.fixture(scope="class") def testuser_api(request, baseapp): cls = request.cls + from rhodecode.tests import TEST_USER_ADMIN_LOGIN # ADMIN USER cls.usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) diff --git a/rhodecode/api/tests/test_add_field_to_repo.py b/rhodecode/api/tests/test_add_field_to_repo.py --- a/rhodecode/api/tests/test_add_field_to_repo.py +++ b/rhodecode/api/tests/test_add_field_to_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_add_user_to_user_group.py b/rhodecode/api/tests/test_add_user_to_user_group.py --- a/rhodecode/api/tests/test_add_user_to_user_group.py +++ b/rhodecode/api/tests/test_add_user_to_user_group.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_api.py b/rhodecode/api/tests/test_api.py --- a/rhodecode/api/tests/test_api.py +++ b/rhodecode/api/tests/test_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_cleanup_repos.py b/rhodecode/api/tests/test_cleanup_repos.py new file mode 100644 --- /dev/null +++ b/rhodecode/api/tests/test_cleanup_repos.py @@ -0,0 +1,44 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import mock +import pytest + +from rhodecode.model.scm import ScmModel +from rhodecode.api.tests.utils import ( + build_data, api_call, assert_ok, assert_error, crash) + + +@pytest.mark.usefixtures("testuser_api", "app") +class TestCleanupRepos(object): + def test_api_cleanup_repos(self): + id_, params = build_data(self.apikey, 'cleanup_repos') + response = api_call(self.app, params) + + expected = {'removed': [], 'errors': []} + assert_ok(id_, expected, given=response.body) + + def test_api_cleanup_repos_error(self): + + id_, params = build_data(self.apikey, 'cleanup_repos', ) + + with mock.patch('rhodecode.lib.utils.repo2db_cleanup', side_effect=crash): + response = api_call(self.app, params) + + expected = 'Error occurred during repo storage cleanup action' + assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/tests/test_cleanup_sessions.py b/rhodecode/api/tests/test_cleanup_sessions.py --- a/rhodecode/api/tests/test_cleanup_sessions.py +++ b/rhodecode/api/tests/test_cleanup_sessions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_close_pull_request.py b/rhodecode/api/tests/test_close_pull_request.py --- a/rhodecode/api/tests/test_close_pull_request.py +++ b/rhodecode/api/tests/test_close_pull_request.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_comment_commit.py b/rhodecode/api/tests/test_comment_commit.py --- a/rhodecode/api/tests/test_comment_commit.py +++ b/rhodecode/api/tests/test_comment_commit.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_comment_pull_request.py b/rhodecode/api/tests/test_comment_pull_request.py --- a/rhodecode/api/tests/test_comment_pull_request.py +++ b/rhodecode/api/tests/test_comment_pull_request.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_create_gist.py b/rhodecode/api/tests/test_create_gist.py --- a/rhodecode/api/tests/test_create_gist.py +++ b/rhodecode/api/tests/test_create_gist.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,7 +23,7 @@ from rhodecode.model.db import Gist from rhodecode.model.gist import GistModel from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture @pytest.mark.usefixtures("testuser_api", "app") diff --git a/rhodecode/api/tests/test_create_pull_request.py b/rhodecode/api/tests/test_create_pull_request.py --- a/rhodecode/api/tests/test_create_pull_request.py +++ b/rhodecode/api/tests/test_create_pull_request.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_create_repo.py b/rhodecode/api/tests/test_create_repo.py --- a/rhodecode/api/tests/test_create_repo.py +++ b/rhodecode/api/tests/test_create_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,7 +26,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.api.tests.utils import ( build_data, api_call, assert_ok, assert_error, crash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.lib.ext_json import json from rhodecode.lib.str_utils import safe_str diff --git a/rhodecode/api/tests/test_create_repo_group.py b/rhodecode/api/tests/test_create_repo_group.py --- a/rhodecode/api/tests/test_create_repo_group.py +++ b/rhodecode/api/tests/test_create_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,7 +25,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.api.tests.utils import ( build_data, api_call, assert_ok, assert_error, crash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() diff --git a/rhodecode/api/tests/test_create_user.py b/rhodecode/api/tests/test_create_user.py --- a/rhodecode/api/tests/test_create_user.py +++ b/rhodecode/api/tests/test_create_user.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,7 +25,7 @@ from rhodecode.tests import ( TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL) from rhodecode.api.tests.utils import ( build_data, api_call, assert_ok, assert_error, jsonify, crash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.model.db import RepoGroup diff --git a/rhodecode/api/tests/test_create_user_group.py b/rhodecode/api/tests/test_create_user_group.py --- a/rhodecode/api/tests/test_create_user_group.py +++ b/rhodecode/api/tests/test_create_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,7 +24,7 @@ from rhodecode.model.user import UserMod from rhodecode.model.user_group import UserGroupModel from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash, jsonify) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture @pytest.mark.usefixtures("testuser_api", "app") diff --git a/rhodecode/api/tests/test_delete_gist.py b/rhodecode/api/tests/test_delete_gist.py --- a/rhodecode/api/tests/test_delete_gist.py +++ b/rhodecode/api/tests/test_delete_gist.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_delete_repo.py b/rhodecode/api/tests/test_delete_repo.py --- a/rhodecode/api/tests/test_delete_repo.py +++ b/rhodecode/api/tests/test_delete_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_delete_repo_group.py b/rhodecode/api/tests/test_delete_repo_group.py --- a/rhodecode/api/tests/test_delete_repo_group.py +++ b/rhodecode/api/tests/test_delete_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_delete_user.py b/rhodecode/api/tests/test_delete_user.py --- a/rhodecode/api/tests/test_delete_user.py +++ b/rhodecode/api/tests/test_delete_user.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_delete_user_group.py b/rhodecode/api/tests/test_delete_user_group.py --- a/rhodecode/api/tests/test_delete_user_group.py +++ b/rhodecode/api/tests/test_delete_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_deprecated_api.py b/rhodecode/api/tests/test_deprecated_api.py --- a/rhodecode/api/tests/test_deprecated_api.py +++ b/rhodecode/api/tests/test_deprecated_api.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_fork_repo.py b/rhodecode/api/tests/test_fork_repo.py --- a/rhodecode/api/tests/test_fork_repo.py +++ b/rhodecode/api/tests/test_fork_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,7 +27,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() diff --git a/rhodecode/api/tests/test_fts_search.py b/rhodecode/api/tests/test_fts_search.py --- a/rhodecode/api/tests/test_fts_search.py +++ b/rhodecode/api/tests/test_fts_search.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_gist.py b/rhodecode/api/tests/test_get_gist.py --- a/rhodecode/api/tests/test_get_gist.py +++ b/rhodecode/api/tests/test_get_gist.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_gists.py b/rhodecode/api/tests/test_get_gists.py --- a/rhodecode/api/tests/test_get_gists.py +++ b/rhodecode/api/tests/test_get_gists.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_ip.py b/rhodecode/api/tests/test_get_ip.py --- a/rhodecode/api/tests/test_get_ip.py +++ b/rhodecode/api/tests/test_get_ip.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_locks.py b/rhodecode/api/tests/test_get_locks.py --- a/rhodecode/api/tests/test_get_locks.py +++ b/rhodecode/api/tests/test_get_locks.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_method.py b/rhodecode/api/tests/test_get_method.py --- a/rhodecode/api/tests/test_get_method.py +++ b/rhodecode/api/tests/test_get_method.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_pull_request.py b/rhodecode/api/tests/test_get_pull_request.py --- a/rhodecode/api/tests/test_get_pull_request.py +++ b/rhodecode/api/tests/test_get_pull_request.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_pull_request_comments.py b/rhodecode/api/tests/test_get_pull_request_comments.py --- a/rhodecode/api/tests/test_get_pull_request_comments.py +++ b/rhodecode/api/tests/test_get_pull_request_comments.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_pull_requests.py b/rhodecode/api/tests/test_get_pull_requests.py --- a/rhodecode/api/tests/test_get_pull_requests.py +++ b/rhodecode/api/tests/test_get_pull_requests.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo.py b/rhodecode/api/tests/test_get_repo.py --- a/rhodecode/api/tests/test_get_repo.py +++ b/rhodecode/api/tests/test_get_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo_changeset.py b/rhodecode/api/tests/test_get_repo_changeset.py --- a/rhodecode/api/tests/test_get_repo_changeset.py +++ b/rhodecode/api/tests/test_get_repo_changeset.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo_comments.py b/rhodecode/api/tests/test_get_repo_comments.py --- a/rhodecode/api/tests/test_get_repo_comments.py +++ b/rhodecode/api/tests/test_get_repo_comments.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,6 +19,7 @@ import pytest +from rhodecode.lib.str_utils import safe_str from rhodecode.model.db import User, ChangesetComment from rhodecode.model.meta import Session from rhodecode.model.comment import CommentsModel @@ -37,7 +37,7 @@ def make_repo_comments_factory(request): commit = repo.scm_instance()[0] commit_id = commit.raw_id - file_0 = commit.affected_files[0] + file_0 = safe_str(commit.affected_files[0]) comments = [] # general diff --git a/rhodecode/api/tests/test_get_repo_group.py b/rhodecode/api/tests/test_get_repo_group.py --- a/rhodecode/api/tests/test_get_repo_group.py +++ b/rhodecode/api/tests/test_get_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo_groups.py b/rhodecode/api/tests/test_get_repo_groups.py --- a/rhodecode/api/tests/test_get_repo_groups.py +++ b/rhodecode/api/tests/test_get_repo_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo_nodes.py b/rhodecode/api/tests/test_get_repo_nodes.py --- a/rhodecode/api/tests/test_get_repo_nodes.py +++ b/rhodecode/api/tests/test_get_repo_nodes.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repo_refs.py b/rhodecode/api/tests/test_get_repo_refs.py --- a/rhodecode/api/tests/test_get_repo_refs.py +++ b/rhodecode/api/tests/test_get_repo_refs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_repos.py b/rhodecode/api/tests/test_get_repos.py --- a/rhodecode/api/tests/test_get_repos.py +++ b/rhodecode/api/tests/test_get_repos.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_server_info.py b/rhodecode/api/tests/test_get_server_info.py --- a/rhodecode/api/tests/test_get_server_info.py +++ b/rhodecode/api/tests/test_get_server_info.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_user.py b/rhodecode/api/tests/test_get_user.py --- a/rhodecode/api/tests/test_get_user.py +++ b/rhodecode/api/tests/test_get_user.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_user_group.py b/rhodecode/api/tests/test_get_user_group.py --- a/rhodecode/api/tests/test_get_user_group.py +++ b/rhodecode/api/tests/test_get_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_user_groups.py b/rhodecode/api/tests/test_get_user_groups.py --- a/rhodecode/api/tests/test_get_user_groups.py +++ b/rhodecode/api/tests/test_get_user_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_get_users.py b/rhodecode/api/tests/test_get_users.py --- a/rhodecode/api/tests/test_get_users.py +++ b/rhodecode/api/tests/test_get_users.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_group_permission.py b/rhodecode/api/tests/test_grant_user_group_permission.py --- a/rhodecode/api/tests/test_grant_user_group_permission.py +++ b/rhodecode/api/tests/test_grant_user_group_permission.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_group_permission_to_repo_group.py b/rhodecode/api/tests/test_grant_user_group_permission_to_repo_group.py --- a/rhodecode/api/tests/test_grant_user_group_permission_to_repo_group.py +++ b/rhodecode/api/tests/test_grant_user_group_permission_to_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_group_permission_to_user_group.py b/rhodecode/api/tests/test_grant_user_group_permission_to_user_group.py --- a/rhodecode/api/tests/test_grant_user_group_permission_to_user_group.py +++ b/rhodecode/api/tests/test_grant_user_group_permission_to_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_permission.py b/rhodecode/api/tests/test_grant_user_permission.py --- a/rhodecode/api/tests/test_grant_user_permission.py +++ b/rhodecode/api/tests/test_grant_user_permission.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_permission_to_repo_group.py b/rhodecode/api/tests/test_grant_user_permission_to_repo_group.py --- a/rhodecode/api/tests/test_grant_user_permission_to_repo_group.py +++ b/rhodecode/api/tests/test_grant_user_permission_to_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_grant_user_permission_to_user_group.py b/rhodecode/api/tests/test_grant_user_permission_to_user_group.py --- a/rhodecode/api/tests/test_grant_user_permission_to_user_group.py +++ b/rhodecode/api/tests/test_grant_user_permission_to_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_invalidate_cache.py b/rhodecode/api/tests/test_invalidate_cache.py --- a/rhodecode/api/tests/test_invalidate_cache.py +++ b/rhodecode/api/tests/test_invalidate_cache.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_merge_pull_request.py b/rhodecode/api/tests/test_merge_pull_request.py --- a/rhodecode/api/tests/test_merge_pull_request.py +++ b/rhodecode/api/tests/test_merge_pull_request.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_pull.py b/rhodecode/api/tests/test_pull.py --- a/rhodecode/api/tests/test_pull.py +++ b/rhodecode/api/tests/test_pull.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_remove_field_from_repo.py b/rhodecode/api/tests/test_remove_field_from_repo.py --- a/rhodecode/api/tests/test_remove_field_from_repo.py +++ b/rhodecode/api/tests/test_remove_field_from_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_remove_user_from_user_group.py b/rhodecode/api/tests/test_remove_user_from_user_group.py --- a/rhodecode/api/tests/test_remove_user_from_user_group.py +++ b/rhodecode/api/tests/test_remove_user_from_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_repo_locking.py b/rhodecode/api/tests/test_repo_locking.py --- a/rhodecode/api/tests/test_repo_locking.py +++ b/rhodecode/api/tests/test_repo_locking.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_rescan_repos.py b/rhodecode/api/tests/test_rescan_repos.py --- a/rhodecode/api/tests/test_rescan_repos.py +++ b/rhodecode/api/tests/test_rescan_repos.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,7 +19,6 @@ import mock import pytest -from rhodecode.model.scm import ScmModel from rhodecode.api.tests.utils import ( build_data, api_call, assert_ok, assert_error, crash) @@ -31,13 +29,14 @@ class TestRescanRepos(object): id_, params = build_data(self.apikey, 'rescan_repos') response = api_call(self.app, params) - expected = {'added': [], 'removed': []} + expected = {'added': [], 'errors': []} assert_ok(id_, expected, given=response.body) - @mock.patch.object(ScmModel, 'repo_scan', crash) - def test_api_rescann_error(self): + def test_api_rescan_repos_error(self): id_, params = build_data(self.apikey, 'rescan_repos', ) - response = api_call(self.app, params) + + with mock.patch('rhodecode.lib.utils.repo2db_mapper', side_effect=crash): + response = api_call(self.app, params) expected = 'Error occurred during rescan repositories action' assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/tests/test_revoke_user_group_permission.py b/rhodecode/api/tests/test_revoke_user_group_permission.py --- a/rhodecode/api/tests/test_revoke_user_group_permission.py +++ b/rhodecode/api/tests/test_revoke_user_group_permission.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_revoke_user_group_permission_from_repo_group.py b/rhodecode/api/tests/test_revoke_user_group_permission_from_repo_group.py --- a/rhodecode/api/tests/test_revoke_user_group_permission_from_repo_group.py +++ b/rhodecode/api/tests/test_revoke_user_group_permission_from_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_revoke_user_group_permission_from_user_group.py b/rhodecode/api/tests/test_revoke_user_group_permission_from_user_group.py --- a/rhodecode/api/tests/test_revoke_user_group_permission_from_user_group.py +++ b/rhodecode/api/tests/test_revoke_user_group_permission_from_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_revoke_user_permission.py b/rhodecode/api/tests/test_revoke_user_permission.py --- a/rhodecode/api/tests/test_revoke_user_permission.py +++ b/rhodecode/api/tests/test_revoke_user_permission.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_revoke_user_permission_from_repo_group.py b/rhodecode/api/tests/test_revoke_user_permission_from_repo_group.py --- a/rhodecode/api/tests/test_revoke_user_permission_from_repo_group.py +++ b/rhodecode/api/tests/test_revoke_user_permission_from_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_revoke_user_permission_from_user_group.py b/rhodecode/api/tests/test_revoke_user_permission_from_user_group.py --- a/rhodecode/api/tests/test_revoke_user_permission_from_user_group.py +++ b/rhodecode/api/tests/test_revoke_user_permission_from_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_service_api.py b/rhodecode/api/tests/test_service_api.py --- a/rhodecode/api/tests/test_service_api.py +++ b/rhodecode/api/tests/test_service_api.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_store_exception.py b/rhodecode/api/tests/test_store_exception.py --- a/rhodecode/api/tests/test_store_exception.py +++ b/rhodecode/api/tests/test_store_exception.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_update_pull_request.py b/rhodecode/api/tests/test_update_pull_request.py --- a/rhodecode/api/tests/test_update_pull_request.py +++ b/rhodecode/api/tests/test_update_pull_request.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_update_repo.py b/rhodecode/api/tests/test_update_repo.py --- a/rhodecode/api/tests/test_update_repo.py +++ b/rhodecode/api/tests/test_update_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,8 +24,8 @@ from rhodecode.model.scm import ScmModel from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash, jsonify) -from rhodecode.tests.fixture import Fixture -from rhodecode.tests.fixture_mods.fixture_utils import plain_http_host_only_stub +from rhodecode.tests.fixtures.rc_fixture import Fixture +from rhodecode.tests.fixtures.fixture_utils import plain_http_host_only_stub fixture = Fixture() diff --git a/rhodecode/api/tests/test_update_repo_group.py b/rhodecode/api/tests/test_update_repo_group.py --- a/rhodecode/api/tests/test_update_repo_group.py +++ b/rhodecode/api/tests/test_update_repo_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_update_user.py b/rhodecode/api/tests/test_update_user.py --- a/rhodecode/api/tests/test_update_user.py +++ b/rhodecode/api/tests/test_update_user.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_update_user_group.py b/rhodecode/api/tests/test_update_user_group.py --- a/rhodecode/api/tests/test_update_user_group.py +++ b/rhodecode/api/tests/test_update_user_group.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/test_utils.py b/rhodecode/api/tests/test_utils.py --- a/rhodecode/api/tests/test_utils.py +++ b/rhodecode/api/tests/test_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/tests/utils.py b/rhodecode/api/tests/utils.py --- a/rhodecode/api/tests/utils.py +++ b/rhodecode/api/tests/utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/utils.py b/rhodecode/api/utils.py --- a/rhodecode/api/utils.py +++ b/rhodecode/api/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/__init__.py b/rhodecode/api/views/__init__.py --- a/rhodecode/api/views/__init__.py +++ b/rhodecode/api/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2023 RhodeCode GmbH +# Copyright (C) 2015-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/deprecated_api.py b/rhodecode/api/views/deprecated_api.py --- a/rhodecode/api/views/deprecated_api.py +++ b/rhodecode/api/views/deprecated_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/gist_api.py b/rhodecode/api/views/gist_api.py --- a/rhodecode/api/views/gist_api.py +++ b/rhodecode/api/views/gist_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/pull_request_api.py b/rhodecode/api/views/pull_request_api.py --- a/rhodecode/api/views/pull_request_api.py +++ b/rhodecode/api/views/pull_request_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -317,8 +317,7 @@ def get_repo_changeset(request, apiuser, ','.join(_changes_details_types))) vcs_repo = repo.scm_instance() - pre_load = ['author', 'branch', 'date', 'message', 'parents', - 'status', '_commit', '_file_paths'] + pre_load = ['author', 'branch', 'date', 'message', 'parents', 'status', '_commit'] try: commit = repo.get_commit(commit_id=revision, pre_load=pre_load) @@ -376,8 +375,7 @@ def get_repo_changesets(request, apiuser ','.join(_changes_details_types))) limit = int(limit) - pre_load = ['author', 'branch', 'date', 'message', 'parents', - 'status', '_commit', '_file_paths'] + pre_load = ['author', 'branch', 'date', 'message', 'parents', 'status', '_commit'] vcs_repo = repo.scm_instance() # SVN needs a special case to distinguish its index and commit id diff --git a/rhodecode/api/views/repo_group_api.py b/rhodecode/api/views/repo_group_api.py --- a/rhodecode/api/views/repo_group_api.py +++ b/rhodecode/api/views/repo_group_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/search_api.py b/rhodecode/api/views/search_api.py --- a/rhodecode/api/views/search_api.py +++ b/rhodecode/api/views/search_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py --- a/rhodecode/api/views/server_api.py +++ b/rhodecode/api/views/server_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -18,14 +18,13 @@ import logging import itertools -import base64 from rhodecode.api import ( jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods) from rhodecode.api.utils import ( Optional, OAttr, has_superadmin_permission, get_user_or_error) -from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path +from rhodecode.lib.utils import get_rhodecode_repo_store_path from rhodecode.lib import system_info from rhodecode.lib import user_sessions from rhodecode.lib import exc_tracking @@ -33,9 +32,7 @@ from rhodecode.lib.ext_json import json from rhodecode.lib.utils2 import safe_int from rhodecode.model.db import UserIpMap from rhodecode.model.scm import ScmModel -from rhodecode.apps.file_store import utils as store_utils -from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \ - FileOverSizeException + log = logging.getLogger(__name__) @@ -158,13 +155,10 @@ def get_ip(request, apiuser, userid=Opti @jsonrpc_method() -def rescan_repos(request, apiuser, remove_obsolete=Optional(False)): +def rescan_repos(request, apiuser): """ Triggers a rescan of the specified repositories. - - * If the ``remove_obsolete`` option is set, it also deletes repositories - that are found in the database but not on the file system, so called - "clean zombies". + It returns list of added repositories, and errors during scan. This command can only be run using an |authtoken| with admin rights to the specified repository. @@ -173,9 +167,6 @@ def rescan_repos(request, apiuser, remov :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser - :param remove_obsolete: Deletes repositories from the database that - are not found on the filesystem. - :type remove_obsolete: Optional(``True`` | ``False``) Example output: @@ -184,7 +175,7 @@ def rescan_repos(request, apiuser, remov id : result : { 'added': [,...] - 'removed': [,...] + 'errors': [,...] } error : null @@ -199,20 +190,69 @@ def rescan_repos(request, apiuser, remov } """ + from rhodecode.lib.utils import repo2db_mapper # re-import for testing patches + if not has_superadmin_permission(apiuser): raise JSONRPCForbidden() try: - rm_obsolete = Optional.extract(remove_obsolete) - added, removed = repo2db_mapper(ScmModel().repo_scan(), - remove_obsolete=rm_obsolete, force_hooks_rebuild=True) - return {'added': added, 'removed': removed} + added, errors = repo2db_mapper(ScmModel().repo_scan(), force_hooks_rebuild=True) + return {'added': added, 'errors': errors} except Exception: - log.exception('Failed to run repo rescann') + log.exception('Failed to run repo rescan') raise JSONRPCError( 'Error occurred during rescan repositories action' ) +@jsonrpc_method() +def cleanup_repos(request, apiuser): + """ + Triggers a cleanup of non-existing repositories or repository groups in filesystem. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : + result : { + 'removed': [,...] + 'errors': [,...] + } + error : null + + Example error output: + + .. code-block:: bash + + id : + result : null + error : { + 'Error occurred during repo storage cleanup action' + } + + """ + from rhodecode.lib.utils import repo2db_cleanup # re-import for testing patches + + if not has_superadmin_permission(apiuser): + raise JSONRPCForbidden() + + try: + removed, errors = repo2db_cleanup() + return {'removed': removed, 'errors': errors} + except Exception: + log.exception('Failed to run repo storage cleanup') + raise JSONRPCError( + 'Error occurred during repo storage cleanup action' + ) + @jsonrpc_method() def cleanup_sessions(request, apiuser, older_then=Optional(60)): diff --git a/rhodecode/api/views/service_api.py b/rhodecode/api/views/service_api.py --- a/rhodecode/api/views/service_api.py +++ b/rhodecode/api/views/service_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/testing_api.py b/rhodecode/api/views/testing_api.py --- a/rhodecode/api/views/testing_api.py +++ b/rhodecode/api/views/testing_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/user_api.py b/rhodecode/api/views/user_api.py --- a/rhodecode/api/views/user_api.py +++ b/rhodecode/api/views/user_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/api/views/user_group_api.py b/rhodecode/api/views/user_group_api.py --- a/rhodecode/api/views/user_group_api.py +++ b/rhodecode/api/views/user_group_api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/__init__.py b/rhodecode/apps/__init__.py --- a/rhodecode/apps/__init__.py +++ b/rhodecode/apps/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -395,7 +395,7 @@ class RepoAppView(BaseAppView): settings = settings_model.get_repo_settings_inherited() return settings.get(settings_key, default) - def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"): + def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/", nodes=None): log.debug("Looking for README file at path %s", path) if commit_id: landing_commit_id = commit_id @@ -413,16 +413,14 @@ class RepoAppView(BaseAppView): @region.conditional_cache_on_arguments(namespace=cache_namespace_uid) def generate_repo_readme( - repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type + _repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type ): - readme_data = None - readme_filename = None + _readme_data = None + _readme_filename = None commit = db_repo.get_commit(_commit_id) log.debug("Searching for a README file at commit %s.", _commit_id) - readme_node = ReadmeFinder(_renderer_type).search( - commit, path=_readme_search_path - ) + readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path, nodes=nodes) if readme_node: log.debug("Found README node: %s", readme_node) @@ -442,19 +440,19 @@ class RepoAppView(BaseAppView): ), } - readme_data = self._render_readme_or_none( + _readme_data = self._render_readme_or_none( commit, readme_node, relative_urls ) - readme_filename = readme_node.str_path + _readme_filename = readme_node.str_path - return readme_data, readme_filename + return _readme_data, _readme_filename readme_data, readme_filename = generate_repo_readme( db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, - renderer_type, + renderer_type ) compute_time = time.time() - start diff --git a/rhodecode/apps/_base/interfaces.py b/rhodecode/apps/_base/interfaces.py --- a/rhodecode/apps/_base/interfaces.py +++ b/rhodecode/apps/_base/interfaces.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/_base/navigation.py b/rhodecode/apps/_base/navigation.py --- a/rhodecode/apps/_base/navigation.py +++ b/rhodecode/apps/_base/navigation.py @@ -1,6 +1,5 @@ -import dataclasses -# Copyright (C) 2016-2023 RhodeCode GmbH +import dataclasses# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/_base/subscribers.py b/rhodecode/apps/_base/subscribers.py --- a/rhodecode/apps/_base/subscribers.py +++ b/rhodecode/apps/_base/subscribers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -361,12 +361,21 @@ def admin_routes(config): renderer='rhodecode:templates/admin/settings/settings.mako') config.add_route( - name='admin_settings_mapping_update', - pattern='/settings/mapping/update') + name='admin_settings_mapping_create', + pattern='/settings/mapping/create') config.add_view( AdminSettingsView, - attr='settings_mapping_update', - route_name='admin_settings_mapping_update', request_method='POST', + attr='settings_mapping_create', + route_name='admin_settings_mapping_create', request_method='POST', + renderer='rhodecode:templates/admin/settings/settings.mako') + + config.add_route( + name='admin_settings_mapping_cleanup', + pattern='/settings/mapping/cleanup') + config.add_view( + AdminSettingsView, + attr='settings_mapping_cleanup', + route_name='admin_settings_mapping_cleanup', request_method='POST', renderer='rhodecode:templates/admin/settings/settings.mako') config.add_route( diff --git a/rhodecode/apps/admin/tests/test_admin_audit_logs.py b/rhodecode/apps/admin/tests/test_admin_audit_logs.py --- a/rhodecode/apps/admin/tests/test_admin_audit_logs.py +++ b/rhodecode/apps/admin/tests/test_admin_audit_logs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,7 +25,7 @@ import pytest from rhodecode.lib.str_utils import safe_str from rhodecode.tests import * from rhodecode.tests.routes import route_path -from rhodecode.tests.fixture import FIXTURES +from rhodecode.tests.fixtures.rc_fixture import FIXTURES from rhodecode.model.db import UserLog from rhodecode.model.meta import Session diff --git a/rhodecode/apps/admin/tests/test_admin_auth_settings.py b/rhodecode/apps/admin/tests/test_admin_auth_settings.py --- a/rhodecode/apps/admin/tests/test_admin_auth_settings.py +++ b/rhodecode/apps/admin/tests/test_admin_auth_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/tests/test_admin_defaults.py b/rhodecode/apps/admin/tests/test_admin_defaults.py --- a/rhodecode/apps/admin/tests/test_admin_defaults.py +++ b/rhodecode/apps/admin/tests/test_admin_defaults.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/tests/test_admin_main_views.py b/rhodecode/apps/admin/tests/test_admin_main_views.py --- a/rhodecode/apps/admin/tests/test_admin_main_views.py +++ b/rhodecode/apps/admin/tests/test_admin_main_views.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,7 +19,7 @@ import pytest from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/admin/tests/test_admin_permissions.py b/rhodecode/apps/admin/tests/test_admin_permissions.py --- a/rhodecode/apps/admin/tests/test_admin_permissions.py +++ b/rhodecode/apps/admin/tests/test_admin_permissions.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/tests/test_admin_repos.py b/rhodecode/apps/admin/tests/test_admin_repos.py --- a/rhodecode/apps/admin/tests/test_admin_repos.py +++ b/rhodecode/apps/admin/tests/test_admin_repos.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -37,7 +36,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import ( login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture, error_function +from rhodecode.tests.fixtures.rc_fixture import Fixture, error_function from rhodecode.tests.utils import repo_on_filesystem from rhodecode.tests.routes import route_path @@ -111,9 +110,11 @@ class TestAdminRepos(object): repo_type=backend.alias, repo_description=description, csrf_token=csrf_token)) - - self.assert_repository_is_created_correctly( - repo_name, description, backend) + try: + self.assert_repository_is_created_correctly(repo_name, description, backend) + finally: + RepoModel().delete(numeric_repo) + Session().commit() @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii']) def test_create_in_group( diff --git a/rhodecode/apps/admin/tests/test_admin_repository_groups.py b/rhodecode/apps/admin/tests/test_admin_repository_groups.py --- a/rhodecode/apps/admin/tests/test_admin_repository_groups.py +++ b/rhodecode/apps/admin/tests/test_admin_repository_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,7 +26,7 @@ from rhodecode.model.meta import Session from rhodecode.model.repo_group import RepoGroupModel from rhodecode.tests import ( assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/admin/tests/test_admin_settings.py b/rhodecode/apps/admin/tests/test_admin_settings.py --- a/rhodecode/apps/admin/tests/test_admin_settings.py +++ b/rhodecode/apps/admin/tests/test_admin_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/tests/test_admin_user_groups.py b/rhodecode/apps/admin/tests/test_admin_user_groups.py --- a/rhodecode/apps/admin/tests/test_admin_user_groups.py +++ b/rhodecode/apps/admin/tests/test_admin_user_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,7 +23,7 @@ from rhodecode.model.meta import Session from rhodecode.tests import ( TestController, assert_session_flash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,7 +27,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import ( TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py --- a/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py +++ b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +21,7 @@ import pytest from rhodecode.model.db import User, UserSshKeys from rhodecode.tests import TestController, assert_session_flash -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/admin/views/__init__.py b/rhodecode/apps/admin/views/__init__.py --- a/rhodecode/apps/admin/views/__init__.py +++ b/rhodecode/apps/admin/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/artifacts.py b/rhodecode/apps/admin/views/artifacts.py --- a/rhodecode/apps/admin/views/artifacts.py +++ b/rhodecode/apps/admin/views/artifacts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/audit_logs.py b/rhodecode/apps/admin/views/audit_logs.py --- a/rhodecode/apps/admin/views/audit_logs.py +++ b/rhodecode/apps/admin/views/audit_logs.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/automation.py b/rhodecode/apps/admin/views/automation.py --- a/rhodecode/apps/admin/views/automation.py +++ b/rhodecode/apps/admin/views/automation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/defaults.py b/rhodecode/apps/admin/views/defaults.py --- a/rhodecode/apps/admin/views/defaults.py +++ b/rhodecode/apps/admin/views/defaults.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/exception_tracker.py b/rhodecode/apps/admin/views/exception_tracker.py --- a/rhodecode/apps/admin/views/exception_tracker.py +++ b/rhodecode/apps/admin/views/exception_tracker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2023 RhodeCode GmbH +# Copyright (C) 2018-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/main_views.py b/rhodecode/apps/admin/views/main_views.py --- a/rhodecode/apps/admin/views/main_views.py +++ b/rhodecode/apps/admin/views/main_views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/open_source_licenses.py b/rhodecode/apps/admin/views/open_source_licenses.py --- a/rhodecode/apps/admin/views/open_source_licenses.py +++ b/rhodecode/apps/admin/views/open_source_licenses.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/permissions.py b/rhodecode/apps/admin/views/permissions.py --- a/rhodecode/apps/admin/views/permissions.py +++ b/rhodecode/apps/admin/views/permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/process_management.py b/rhodecode/apps/admin/views/process_management.py --- a/rhodecode/apps/admin/views/process_management.py +++ b/rhodecode/apps/admin/views/process_management.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/repo_groups.py b/rhodecode/apps/admin/views/repo_groups.py --- a/rhodecode/apps/admin/views/repo_groups.py +++ b/rhodecode/apps/admin/views/repo_groups.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/repositories.py b/rhodecode/apps/admin/views/repositories.py --- a/rhodecode/apps/admin/views/repositories.py +++ b/rhodecode/apps/admin/views/repositories.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/scheduler.py b/rhodecode/apps/admin/views/scheduler.py --- a/rhodecode/apps/admin/views/scheduler.py +++ b/rhodecode/apps/admin/views/scheduler.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/sessions.py b/rhodecode/apps/admin/views/sessions.py --- a/rhodecode/apps/admin/views/sessions.py +++ b/rhodecode/apps/admin/views/sessions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/settings.py b/rhodecode/apps/admin/views/settings.py --- a/rhodecode/apps/admin/views/settings.py +++ b/rhodecode/apps/admin/views/settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -38,7 +38,7 @@ from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, CSRFRequired) from rhodecode.lib.celerylib import tasks, run_task from rhodecode.lib.str_utils import safe_str -from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path +from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path, repo2db_cleanup from rhodecode.lib.utils2 import str2bool, AttributeDict from rhodecode.lib.index import searcher_from_config @@ -77,13 +77,16 @@ class AdminSettingsView(BaseAppView): raise Exception('Could not get application ui settings !') settings = { # legacy param that needs to be kept - 'web_push_ssl': False + 'web_push_ssl': False, + 'extensions_hgsubversion': False } for each in ret: k = each.ui_key v = each.ui_value + section = each.ui_section + # skip some options if they are defined - if k in ['push_ssl']: + if f"{section}_{k}" in ['web_push_ssl', 'extensions_hgsubversion']: continue if k == '/': @@ -98,7 +101,7 @@ class AdminSettingsView(BaseAppView): if each.ui_section in ['hooks', 'extensions']: v = each.ui_active - settings[each.ui_section + '_' + k] = v + settings[section + '_' + k] = v return settings @@ -125,8 +128,6 @@ class AdminSettingsView(BaseAppView): c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path) defaults = self._form_defaults() - model.create_largeobjects_dirs_if_needed(defaults['paths_root_path']) - data = render('rhodecode:templates/admin/settings/settings.mako', self._get_template_context(c), self.request) html = formencode.htmlfill.render( @@ -232,13 +233,12 @@ class AdminSettingsView(BaseAppView): @LoginRequired() @HasPermissionAllDecorator('hg.admin') @CSRFRequired() - def settings_mapping_update(self): + def settings_mapping_create(self): _ = self.request.translate c = self.load_default_context() c.active = 'mapping' - rm_obsolete = self.request.POST.get('destroy', False) invalidate_cache = self.request.POST.get('invalidate', False) - log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete) + log.debug('rescanning repo location') if invalidate_cache: log.debug('invalidating all repositories cache') @@ -246,16 +246,34 @@ class AdminSettingsView(BaseAppView): ScmModel().mark_for_invalidation(repo.repo_name, delete=True) filesystem_repos = ScmModel().repo_scan() - added, removed = repo2db_mapper(filesystem_repos, rm_obsolete, force_hooks_rebuild=True) + added, errors = repo2db_mapper(filesystem_repos, force_hooks_rebuild=True) PermissionModel().trigger_permission_flush() def _repr(rm_repo): return ', '.join(map(safe_str, rm_repo)) or '-' - h.flash(_('Repositories successfully ' - 'rescanned added: %s ; removed: %s') % - (_repr(added), _repr(removed)), - category='success') + if errors: + h.flash(_('Errors during scan: {}').format(_repr(errors), ), category='error') + + h.flash(_('Repositories successfully scanned: Added: {}').format(_repr(added)), category='success') + raise HTTPFound(h.route_path('admin_settings_mapping')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + def settings_mapping_cleanup(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'mapping' + log.debug('rescanning repo location') + + removed, errors = repo2db_cleanup() + PermissionModel().trigger_permission_flush() + + def _repr(rm_repo): + return ', '.join(map(safe_str, rm_repo)) or '-' + + h.flash(_('Repositories successfully scanned: Errors: {}, Added: {}').format(errors, _repr(removed)), category='success') raise HTTPFound(h.route_path('admin_settings_mapping')) @LoginRequired() diff --git a/rhodecode/apps/admin/views/svn_config.py b/rhodecode/apps/admin/views/svn_config.py --- a/rhodecode/apps/admin/views/svn_config.py +++ b/rhodecode/apps/admin/views/svn_config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/system_info.py b/rhodecode/apps/admin/views/system_info.py --- a/rhodecode/apps/admin/views/system_info.py +++ b/rhodecode/apps/admin/views/system_info.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py --- a/rhodecode/apps/admin/views/user_groups.py +++ b/rhodecode/apps/admin/views/user_groups.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/channelstream/__init__.py b/rhodecode/apps/channelstream/__init__.py --- a/rhodecode/apps/channelstream/__init__.py +++ b/rhodecode/apps/channelstream/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/channelstream/views.py b/rhodecode/apps/channelstream/views.py --- a/rhodecode/apps/channelstream/views.py +++ b/rhodecode/apps/channelstream/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/debug_style/__init__.py b/rhodecode/apps/debug_style/__init__.py --- a/rhodecode/apps/debug_style/__init__.py +++ b/rhodecode/apps/debug_style/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/debug_style/views.py b/rhodecode/apps/debug_style/views.py --- a/rhodecode/apps/debug_style/views.py +++ b/rhodecode/apps/debug_style/views.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/__init__.py b/rhodecode/apps/file_store/__init__.py --- a/rhodecode/apps/file_store/__init__.py +++ b/rhodecode/apps/file_store/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/backends/__init__.py b/rhodecode/apps/file_store/backends/__init__.py --- a/rhodecode/apps/file_store/backends/__init__.py +++ b/rhodecode/apps/file_store/backends/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/backends/base.py b/rhodecode/apps/file_store/backends/base.py --- a/rhodecode/apps/file_store/backends/base.py +++ b/rhodecode/apps/file_store/backends/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/backends/filesystem.py b/rhodecode/apps/file_store/backends/filesystem.py --- a/rhodecode/apps/file_store/backends/filesystem.py +++ b/rhodecode/apps/file_store/backends/filesystem.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/backends/filesystem_legacy.py b/rhodecode/apps/file_store/backends/filesystem_legacy.py --- a/rhodecode/apps/file_store/backends/filesystem_legacy.py +++ b/rhodecode/apps/file_store/backends/filesystem_legacy.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/backends/objectstore.py b/rhodecode/apps/file_store/backends/objectstore.py --- a/rhodecode/apps/file_store/backends/objectstore.py +++ b/rhodecode/apps/file_store/backends/objectstore.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/config_keys.py b/rhodecode/apps/file_store/config_keys.py --- a/rhodecode/apps/file_store/config_keys.py +++ b/rhodecode/apps/file_store/config_keys.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/exceptions.py b/rhodecode/apps/file_store/exceptions.py --- a/rhodecode/apps/file_store/exceptions.py +++ b/rhodecode/apps/file_store/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/extensions.py b/rhodecode/apps/file_store/extensions.py --- a/rhodecode/apps/file_store/extensions.py +++ b/rhodecode/apps/file_store/extensions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/__init__.py b/rhodecode/apps/file_store/tests/__init__.py --- a/rhodecode/apps/file_store/tests/__init__.py +++ b/rhodecode/apps/file_store/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_filestore_backends.py b/rhodecode/apps/file_store/tests/test_filestore_backends.py --- a/rhodecode/apps/file_store/tests/test_filestore_backends.py +++ b/rhodecode/apps/file_store/tests/test_filestore_backends.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_filestore_filesystem_backend.py b/rhodecode/apps/file_store/tests/test_filestore_filesystem_backend.py --- a/rhodecode/apps/file_store/tests/test_filestore_filesystem_backend.py +++ b/rhodecode/apps/file_store/tests/test_filestore_filesystem_backend.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_filestore_legacy_and_v2_compatability.py b/rhodecode/apps/file_store/tests/test_filestore_legacy_and_v2_compatability.py --- a/rhodecode/apps/file_store/tests/test_filestore_legacy_and_v2_compatability.py +++ b/rhodecode/apps/file_store/tests/test_filestore_legacy_and_v2_compatability.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_filestore_legacy_backend.py b/rhodecode/apps/file_store/tests/test_filestore_legacy_backend.py --- a/rhodecode/apps/file_store/tests/test_filestore_legacy_backend.py +++ b/rhodecode/apps/file_store/tests/test_filestore_legacy_backend.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_filestore_objectstore_backend.py b/rhodecode/apps/file_store/tests/test_filestore_objectstore_backend.py --- a/rhodecode/apps/file_store/tests/test_filestore_objectstore_backend.py +++ b/rhodecode/apps/file_store/tests/test_filestore_objectstore_backend.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/tests/test_upload_file.py b/rhodecode/apps/file_store/tests/test_upload_file.py --- a/rhodecode/apps/file_store/tests/test_upload_file.py +++ b/rhodecode/apps/file_store/tests/test_upload_file.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/utils.py b/rhodecode/apps/file_store/utils.py --- a/rhodecode/apps/file_store/utils.py +++ b/rhodecode/apps/file_store/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/file_store/views.py b/rhodecode/apps/file_store/views.py --- a/rhodecode/apps/file_store/views.py +++ b/rhodecode/apps/file_store/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/gist/__init__.py b/rhodecode/apps/gist/__init__.py --- a/rhodecode/apps/gist/__init__.py +++ b/rhodecode/apps/gist/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/gist/tests/__init__.py b/rhodecode/apps/gist/tests/__init__.py --- a/rhodecode/apps/gist/tests/__init__.py +++ b/rhodecode/apps/gist/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/gist/tests/test_admin_gists.py b/rhodecode/apps/gist/tests/test_admin_gists.py --- a/rhodecode/apps/gist/tests/test_admin_gists.py +++ b/rhodecode/apps/gist/tests/test_admin_gists.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/gist/views.py b/rhodecode/apps/gist/views.py --- a/rhodecode/apps/gist/views.py +++ b/rhodecode/apps/gist/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py --- a/rhodecode/apps/home/__init__.py +++ b/rhodecode/apps/home/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/home/tests/__init__.py b/rhodecode/apps/home/tests/__init__.py --- a/rhodecode/apps/home/tests/__init__.py +++ b/rhodecode/apps/home/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/home/tests/test_get_goto_switched_data.py b/rhodecode/apps/home/tests/test_get_goto_switched_data.py --- a/rhodecode/apps/home/tests/test_get_goto_switched_data.py +++ b/rhodecode/apps/home/tests/test_get_goto_switched_data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,7 +27,7 @@ from rhodecode.model.repo_group import R from rhodecode.model.db import Session, Repository, RepoGroup from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/home/tests/test_get_repo_list_data.py b/rhodecode/apps/home/tests/test_get_repo_list_data.py --- a/rhodecode/apps/home/tests/test_get_repo_list_data.py +++ b/rhodecode/apps/home/tests/test_get_repo_list_data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +22,7 @@ from rhodecode.model.db import Repositor from rhodecode.lib.ext_json import json from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/home/tests/test_get_user_data.py b/rhodecode/apps/home/tests/test_get_user_data.py --- a/rhodecode/apps/home/tests/test_get_user_data.py +++ b/rhodecode/apps/home/tests/test_get_user_data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,7 +20,7 @@ import pytest from rhodecode.lib.ext_json import json from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/home/tests/test_get_user_group_data.py b/rhodecode/apps/home/tests/test_get_user_group_data.py --- a/rhodecode/apps/home/tests/test_get_user_group_data.py +++ b/rhodecode/apps/home/tests/test_get_user_group_data.py @@ -1,23 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -40,7 +21,7 @@ import pytest from rhodecode.lib.ext_json import json from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/home/tests/test_home.py b/rhodecode/apps/home/tests/test_home.py --- a/rhodecode/apps/home/tests/test_home.py +++ b/rhodecode/apps/home/tests/test_home.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,7 +24,7 @@ from rhodecode.model.db import Repositor from rhodecode.model.meta import Session from rhodecode.model.settings import SettingsModel from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/hovercards/__init__.py b/rhodecode/apps/hovercards/__init__.py --- a/rhodecode/apps/hovercards/__init__.py +++ b/rhodecode/apps/hovercards/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2023 RhodeCode GmbH +# Copyright (C) 2018-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/hovercards/views.py b/rhodecode/apps/hovercards/views.py --- a/rhodecode/apps/hovercards/views.py +++ b/rhodecode/apps/hovercards/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/journal/__init__.py b/rhodecode/apps/journal/__init__.py --- a/rhodecode/apps/journal/__init__.py +++ b/rhodecode/apps/journal/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/journal/tests/__init__.py b/rhodecode/apps/journal/tests/__init__.py --- a/rhodecode/apps/journal/tests/__init__.py +++ b/rhodecode/apps/journal/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/journal/tests/test_journal.py b/rhodecode/apps/journal/tests/test_journal.py --- a/rhodecode/apps/journal/tests/test_journal.py +++ b/rhodecode/apps/journal/tests/test_journal.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/journal/views.py b/rhodecode/apps/journal/views.py --- a/rhodecode/apps/journal/views.py +++ b/rhodecode/apps/journal/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/login/__init__.py b/rhodecode/apps/login/__init__.py --- a/rhodecode/apps/login/__init__.py +++ b/rhodecode/apps/login/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/login/tests/test_2fa.py b/rhodecode/apps/login/tests/test_2fa.py --- a/rhodecode/apps/login/tests/test_2fa.py +++ b/rhodecode/apps/login/tests/test_2fa.py @@ -3,7 +3,7 @@ import mock from rhodecode.lib.type_utils import AttributeDict from rhodecode.model.meta import Session -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path from rhodecode.model.settings import SettingsModel diff --git a/rhodecode/apps/login/tests/test_login.py b/rhodecode/apps/login/tests/test_login.py --- a/rhodecode/apps/login/tests/test_login.py +++ b/rhodecode/apps/login/tests/test_login.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -31,7 +31,7 @@ from rhodecode.model.meta import Session from rhodecode.tests import ( assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN, no_newline_id_generator) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/login/tests/test_password_reset.py b/rhodecode/apps/login/tests/test_password_reset.py --- a/rhodecode/apps/login/tests/test_password_reset.py +++ b/rhodecode/apps/login/tests/test_password_reset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +22,7 @@ from rhodecode.lib import helpers as h from rhodecode.tests import ( TestController, clear_cache_regions, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import AssertResponse from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/login/tests/test_register_captcha.py b/rhodecode/apps/login/tests/test_register_captcha.py --- a/rhodecode/apps/login/tests/test_register_captcha.py +++ b/rhodecode/apps/login/tests/test_register_captcha.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/tests/__init__.py b/rhodecode/apps/my_account/tests/__init__.py --- a/rhodecode/apps/my_account/tests/__init__.py +++ b/rhodecode/apps/my_account/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py --- a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py +++ b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +22,7 @@ from rhodecode.apps._base import ADMIN_P from rhodecode.model.db import User from rhodecode.tests import ( TestController, assert_session_flash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/my_account/tests/test_my_account_edit.py b/rhodecode/apps/my_account/tests/test_my_account_edit.py --- a/rhodecode/apps/my_account/tests/test_my_account_edit.py +++ b/rhodecode/apps/my_account/tests/test_my_account_edit.py @@ -1,23 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/tests/test_my_account_emails.py b/rhodecode/apps/my_account/tests/test_my_account_emails.py --- a/rhodecode/apps/my_account/tests/test_my_account_emails.py +++ b/rhodecode/apps/my_account/tests/test_my_account_emails.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -23,7 +23,7 @@ from rhodecode.model.db import User, Use from rhodecode.tests import ( TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, assert_session_flash, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/my_account/tests/test_my_account_notifications.py b/rhodecode/apps/my_account/tests/test_my_account_notifications.py --- a/rhodecode/apps/my_account/tests/test_my_account_notifications.py +++ b/rhodecode/apps/my_account/tests/test_my_account_notifications.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,7 +21,7 @@ import pytest from rhodecode.tests import ( TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path from rhodecode.model.db import Notification, User diff --git a/rhodecode/apps/my_account/tests/test_my_account_password.py b/rhodecode/apps/my_account/tests/test_my_account_password.py --- a/rhodecode/apps/my_account/tests/test_my_account_password.py +++ b/rhodecode/apps/my_account/tests/test_my_account_password.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,7 +23,7 @@ from rhodecode.lib.auth import check_pas from rhodecode.model.meta import Session from rhodecode.model.user import UserModel from rhodecode.tests import assert_session_flash, TestController -from rhodecode.tests.fixture import Fixture, error_function +from rhodecode.tests.fixtures.rc_fixture import Fixture, error_function from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/my_account/tests/test_my_account_profile.py b/rhodecode/apps/my_account/tests/test_my_account_profile.py --- a/rhodecode/apps/my_account/tests/test_my_account_profile.py +++ b/rhodecode/apps/my_account/tests/test_my_account_profile.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,7 +19,7 @@ from rhodecode.tests import ( TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/my_account/tests/test_my_account_simple_views.py b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py --- a/rhodecode/apps/my_account/tests/test_my_account_simple_views.py +++ b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,7 +18,7 @@ from rhodecode.model.db import User, Repository, UserFollowing from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/my_account/tests/test_my_account_ssh_keys.py b/rhodecode/apps/my_account/tests/test_my_account_ssh_keys.py --- a/rhodecode/apps/my_account/tests/test_my_account_ssh_keys.py +++ b/rhodecode/apps/my_account/tests/test_my_account_ssh_keys.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,7 +20,7 @@ from rhodecode.model.db import User, UserSshKeys from rhodecode.tests import TestController, assert_session_flash -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/my_account/views/__init__.py b/rhodecode/apps/my_account/views/__init__.py --- a/rhodecode/apps/my_account/views/__init__.py +++ b/rhodecode/apps/my_account/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/views/my_account_notifications.py b/rhodecode/apps/my_account/views/my_account_notifications.py --- a/rhodecode/apps/my_account/views/my_account_notifications.py +++ b/rhodecode/apps/my_account/views/my_account_notifications.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/my_account/views/my_account_ssh_keys.py b/rhodecode/apps/my_account/views/my_account_ssh_keys.py --- a/rhodecode/apps/my_account/views/my_account_ssh_keys.py +++ b/rhodecode/apps/my_account/views/my_account_ssh_keys.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ops/__init__.py b/rhodecode/apps/ops/__init__.py --- a/rhodecode/apps/ops/__init__.py +++ b/rhodecode/apps/ops/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ops/views.py b/rhodecode/apps/ops/views.py --- a/rhodecode/apps/ops/views.py +++ b/rhodecode/apps/ops/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/__init__.py b/rhodecode/apps/repo_group/__init__.py --- a/rhodecode/apps/repo_group/__init__.py +++ b/rhodecode/apps/repo_group/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/tests/__init__.py b/rhodecode/apps/repo_group/tests/__init__.py --- a/rhodecode/apps/repo_group/tests/__init__.py +++ b/rhodecode/apps/repo_group/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py b/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py --- a/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py +++ b/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py b/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py --- a/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py +++ b/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py b/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py --- a/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py +++ b/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/views/__init__.py b/rhodecode/apps/repo_group/views/__init__.py --- a/rhodecode/apps/repo_group/views/__init__.py +++ b/rhodecode/apps/repo_group/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/views/repo_group_advanced.py b/rhodecode/apps/repo_group/views/repo_group_advanced.py --- a/rhodecode/apps/repo_group/views/repo_group_advanced.py +++ b/rhodecode/apps/repo_group/views/repo_group_advanced.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/views/repo_group_permissions.py b/rhodecode/apps/repo_group/views/repo_group_permissions.py --- a/rhodecode/apps/repo_group/views/repo_group_permissions.py +++ b/rhodecode/apps/repo_group/views/repo_group_permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repo_group/views/repo_group_settings.py b/rhodecode/apps/repo_group/views/repo_group_settings.py --- a/rhodecode/apps/repo_group/views/repo_group_settings.py +++ b/rhodecode/apps/repo_group/views/repo_group_settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/__init__.py b/rhodecode/apps/repository/tests/__init__.py --- a/rhodecode/apps/repository/tests/__init__.py +++ b/rhodecode/apps/repository/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_pull_requests_list.py b/rhodecode/apps/repository/tests/test_pull_requests_list.py --- a/rhodecode/apps/repository/tests/test_pull_requests_list.py +++ b/rhodecode/apps/repository/tests/test_pull_requests_list.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_bookmarks.py b/rhodecode/apps/repository/tests/test_repo_bookmarks.py --- a/rhodecode/apps/repository/tests/test_repo_bookmarks.py +++ b/rhodecode/apps/repository/tests/test_repo_bookmarks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_branches.py b/rhodecode/apps/repository/tests/test_repo_branches.py --- a/rhodecode/apps/repository/tests/test_repo_branches.py +++ b/rhodecode/apps/repository/tests/test_repo_branches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_changelog.py b/rhodecode/apps/repository/tests/test_repo_changelog.py --- a/rhodecode/apps/repository/tests/test_repo_changelog.py +++ b/rhodecode/apps/repository/tests/test_repo_changelog.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_commit_comments.py b/rhodecode/apps/repository/tests/test_repo_commit_comments.py --- a/rhodecode/apps/repository/tests/test_repo_commit_comments.py +++ b/rhodecode/apps/repository/tests/test_repo_commit_comments.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_commits.py b/rhodecode/apps/repository/tests/test_repo_commits.py --- a/rhodecode/apps/repository/tests/test_repo_commits.py +++ b/rhodecode/apps/repository/tests/test_repo_commits.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_compare.py b/rhodecode/apps/repository/tests/test_repo_compare.py --- a/rhodecode/apps/repository/tests/test_repo_compare.py +++ b/rhodecode/apps/repository/tests/test_repo_compare.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_compare_local.py b/rhodecode/apps/repository/tests/test_repo_compare_local.py --- a/rhodecode/apps/repository/tests/test_repo_compare_local.py +++ b/rhodecode/apps/repository/tests/test_repo_compare_local.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py b/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py --- a/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py +++ b/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +21,7 @@ import pytest from rhodecode.apps.repository.tests.test_repo_compare import ComparePage from rhodecode.lib.vcs import nodes from rhodecode.lib.vcs.backends.base import EmptyCommit -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import commit_change from rhodecode.tests.routes import route_path @@ -166,14 +165,15 @@ class TestSideBySideDiff(object): response.mustcontain('Collapse 2 commits') response.mustcontain('123 file changed') - response.mustcontain( - 'r%s:%s...r%s:%s' % ( - commit1.idx, commit1.short_id, commit2.idx, commit2.short_id)) + response.mustcontain(f'r{commit1.idx}:{commit1.short_id}...r{commit2.idx}:{commit2.short_id}') response.mustcontain(f_path) - @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)') + #@pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)') def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub): + if backend.alias == 'git': + pytest.skip('GIT does not handle empty commit compare correct (missing 1 commit)') + f_path = b'test_sidebyside_file.py' commit1_content = b'content-25d7e49c18b159446c\n' commit2_content = b'content-603d6c72c46d953420\n' @@ -200,9 +200,7 @@ class TestSideBySideDiff(object): response.mustcontain('Collapse 2 commits') response.mustcontain('1 file changed') - response.mustcontain( - 'r%s:%s...r%s:%s' % ( - commit1.idx, commit1.short_id, commit2.idx, commit2.short_id)) + response.mustcontain(f'r{commit1.idx}:{commit1.short_id}...r{commit2.idx}:{commit2.short_id}') response.mustcontain(f_path) diff --git a/rhodecode/apps/repository/tests/test_repo_feed.py b/rhodecode/apps/repository/tests/test_repo_feed.py --- a/rhodecode/apps/repository/tests/test_repo_feed.py +++ b/rhodecode/apps/repository/tests/test_repo_feed.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_files.py b/rhodecode/apps/repository/tests/test_repo_files.py --- a/rhodecode/apps/repository/tests/test_repo_files.py +++ b/rhodecode/apps/repository/tests/test_repo_files.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -33,7 +32,7 @@ from rhodecode.lib.vcs.conf import setti from rhodecode.model.db import Session, Repository from rhodecode.tests import assert_session_flash -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path @@ -42,207 +41,171 @@ fixture = Fixture() def get_node_history(backend_type): return { - 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')), - 'git': json.loads(fixture.load_resource('git_node_history_response.json')), - 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')), + "hg": json.loads(fixture.load_resource("hg_node_history_response.json")), + "git": json.loads(fixture.load_resource("git_node_history_response.json")), + "svn": json.loads(fixture.load_resource("svn_node_history_response.json")), }[backend_type] def assert_files_in_response(response, files, params): - template = ( - 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"') + template = 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"' _assert_items_in_response(response, files, template, params) def assert_dirs_in_response(response, dirs, params): - template = ( - 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"') + template = 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"' _assert_items_in_response(response, dirs, template, params) def _assert_items_in_response(response, items, template, params): for item in items: - item_params = {'name': item} + item_params = {"name": item} item_params.update(params) response.mustcontain(template % item_params) def assert_timeago_in_response(response, items, params): for item in items: - response.mustcontain(h.age_component(params['date'])) + response.mustcontain(h.age_component(params["date"])) @pytest.mark.usefixtures("app") class TestFilesViews(object): - def test_show_files(self, backend): - response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id='tip', f_path='/')) + response = self.app.get(route_path("repo_files", repo_name=backend.repo_name, commit_id="tip", f_path="")) commit = backend.repo.get_commit() - params = { - 'repo_name': backend.repo_name, - 'commit_id': commit.raw_id, - 'date': commit.date - } - assert_dirs_in_response(response, ['docs', 'vcs'], params) + params = {"repo_name": backend.repo_name, "commit_id": commit.raw_id, "date": commit.date} + assert_dirs_in_response(response, ["docs", "vcs"], params) files = [ - '.gitignore', - '.hgignore', - '.hgtags', + ".gitignore", + ".hgignore", + ".hgtags", # TODO: missing in Git # '.travis.yml', - 'MANIFEST.in', - 'README.rst', + "MANIFEST.in", + "README.rst", # TODO: File is missing in svn repository # 'run_test_and_report.sh', - 'setup.cfg', - 'setup.py', - 'test_and_report.sh', - 'tox.ini', + "setup.cfg", + "setup.py", + "test_and_report.sh", + "tox.ini", ] assert_files_in_response(response, files, params) assert_timeago_in_response(response, files, params) def test_show_files_links_submodules_with_absolute_url(self, backend_hg): - repo = backend_hg['subrepos'] - response = self.app.get( - route_path('repo_files', - repo_name=repo.repo_name, - commit_id='tip', f_path='/')) + repo = backend_hg["subrepos"] + response = self.app.get(route_path("repo_files", repo_name=repo.repo_name, commit_id="tip", f_path="")) assert_response = response.assert_response() - assert_response.contains_one_link( - 'absolute-path @ 000000000000', 'http://example.com/absolute-path') + assert_response.contains_one_link("absolute-path @ 000000000000", "http://example.com/absolute-path") - def test_show_files_links_submodules_with_absolute_url_subpaths( - self, backend_hg): - repo = backend_hg['subrepos'] - response = self.app.get( - route_path('repo_files', - repo_name=repo.repo_name, - commit_id='tip', f_path='/')) + def test_show_files_links_submodules_with_absolute_url_subpaths(self, backend_hg): + repo = backend_hg["subrepos"] + response = self.app.get(route_path("repo_files", repo_name=repo.repo_name, commit_id="tip", f_path="")) assert_response = response.assert_response() - assert_response.contains_one_link( - 'subpaths-path @ 000000000000', - 'http://sub-base.example.com/subpaths-path') + assert_response.contains_one_link("subpaths-path @ 000000000000", "http://sub-base.example.com/subpaths-path") @pytest.mark.xfail_backends("svn", reason="Depends on branch support") def test_files_menu(self, backend): new_branch = "temp_branch_name" - commits = [ - {'message': 'a'}, - {'message': 'b', 'branch': new_branch} - ] + commits = [{"message": "a"}, {"message": "b", "branch": new_branch}] backend.create_repo(commits) backend.repo.landing_rev = f"branch:{new_branch}" Session().commit() # get response based on tip and not new commit - response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id='tip', f_path='/')) + response = self.app.get(route_path("repo_files", repo_name=backend.repo_name, commit_id="tip", f_path="")) # make sure Files menu url is not tip but new commit landing_rev = backend.repo.landing_ref_name - files_url = route_path('repo_files:default_path', - repo_name=backend.repo_name, - commit_id=landing_rev, params={'at': landing_rev}) + files_url = route_path( + "repo_files:default_path", repo_name=backend.repo_name, commit_id=landing_rev, params={"at": landing_rev} + ) - assert landing_rev != 'tip' + assert landing_rev != "tip" response.mustcontain(f'
  • ') def test_show_files_commit(self, backend): commit = backend.repo.get_commit(commit_idx=32) response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/')) + route_path("repo_files", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="") + ) - dirs = ['docs', 'tests'] - files = ['README.rst'] + dirs = ["docs", "tests"] + files = ["README.rst"] params = { - 'repo_name': backend.repo_name, - 'commit_id': commit.raw_id, + "repo_name": backend.repo_name, + "commit_id": commit.raw_id, } assert_dirs_in_response(response, dirs, params) assert_files_in_response(response, files, params) def test_show_files_different_branch(self, backend): branches = dict( - hg=(150, ['git']), + hg=(150, ["git"]), # TODO: Git test repository does not contain other branches - git=(633, ['master']), + git=(633, ["master"]), # TODO: Branch support in Subversion - svn=(150, []) + svn=(150, []), ) idx, branches = branches[backend.alias] commit = backend.repo.get_commit(commit_idx=idx) response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/')) + route_path("repo_files", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="") + ) assert_response = response.assert_response() for branch in branches: - assert_response.element_contains('.tags .branchtag', branch) + assert_response.element_contains(".tags .branchtag", branch) def test_show_files_paging(self, backend): repo = backend.repo indexes = [73, 92, 109, 1, 0] - idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id) - for rev in indexes] + idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id) for rev in indexes] for idx in idx_map: - response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id=idx[1], f_path='/')) + response = self.app.get(route_path("repo_files", repo_name=backend.repo_name, commit_id=idx[1], f_path="")) response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8])) def test_file_source(self, backend): commit = backend.repo.get_commit(commit_idx=167) response = self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='vcs/nodes.py')) + route_path("repo_files", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="vcs/nodes.py") + ) msgbox = """
    %s
    """ - response.mustcontain(msgbox % (commit.message, )) + response.mustcontain(msgbox % (commit.message,)) assert_response = response.assert_response() if commit.branch: - assert_response.element_contains( - '.tags.tags-main .branchtag', commit.branch) + assert_response.element_contains(".tags.tags-main .branchtag", commit.branch) if commit.tags: for tag in commit.tags: - assert_response.element_contains('.tags.tags-main .tagtag', tag) + assert_response.element_contains(".tags.tags-main .tagtag", tag) def test_file_source_annotated(self, backend): response = self.app.get( - route_path('repo_files:annotated', - repo_name=backend.repo_name, - commit_id='tip', f_path='vcs/nodes.py')) + route_path("repo_files:annotated", repo_name=backend.repo_name, commit_id="tip", f_path="vcs/nodes.py") + ) expected_commits = { - 'hg': 'r356', - 'git': 'r345', - 'svn': 'r208', + "hg": "r356", + "git": "r345", + "svn": "r208", } response.mustcontain(expected_commits[backend.alias]) def test_file_source_authors(self, backend): response = self.app.get( - route_path('repo_file_authors', - repo_name=backend.repo_name, - commit_id='tip', f_path='vcs/nodes.py')) + route_path("repo_file_authors", repo_name=backend.repo_name, commit_id="tip", f_path="vcs/nodes.py") + ) expected_authors = { - 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'), - 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'), - 'svn': ('marcin', 'lukasz'), + "hg": ("Marcin Kuzminski", "Lukasz Balcerzak"), + "git": ("Marcin Kuzminski", "Lukasz Balcerzak"), + "svn": ("marcin", "lukasz"), } for author in expected_authors[backend.alias]: @@ -250,14 +213,18 @@ class TestFilesViews(object): def test_file_source_authors_with_annotation(self, backend): response = self.app.get( - route_path('repo_file_authors', - repo_name=backend.repo_name, - commit_id='tip', f_path='vcs/nodes.py', - params=dict(annotate=1))) + route_path( + "repo_file_authors", + repo_name=backend.repo_name, + commit_id="tip", + f_path="vcs/nodes.py", + params=dict(annotate=1), + ) + ) expected_authors = { - 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'), - 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'), - 'svn': ('marcin', 'lukasz'), + "hg": ("Marcin Kuzminski", "Lukasz Balcerzak"), + "git": ("Marcin Kuzminski", "Lukasz Balcerzak"), + "svn": ("marcin", "lukasz"), } for author in expected_authors[backend.alias]: @@ -265,94 +232,88 @@ class TestFilesViews(object): def test_file_source_history(self, backend, xhr_header): response = self.app.get( - route_path('repo_file_history', - repo_name=backend.repo_name, - commit_id='tip', f_path='vcs/nodes.py'), - extra_environ=xhr_header) + route_path("repo_file_history", repo_name=backend.repo_name, commit_id="tip", f_path="vcs/nodes.py"), + extra_environ=xhr_header, + ) assert get_node_history(backend.alias) == json.loads(response.body) def test_file_source_history_svn(self, backend_svn, xhr_header): - simple_repo = backend_svn['svn-simple-layout'] + simple_repo = backend_svn["svn-simple-layout"] response = self.app.get( - route_path('repo_file_history', - repo_name=simple_repo.repo_name, - commit_id='tip', f_path='trunk/example.py'), - extra_environ=xhr_header) + route_path( + "repo_file_history", repo_name=simple_repo.repo_name, commit_id="tip", f_path="trunk/example.py" + ), + extra_environ=xhr_header, + ) - expected_data = json.loads( - fixture.load_resource('svn_node_history_branches.json')) + expected_data = json.loads(fixture.load_resource("svn_node_history_branches.json")) assert expected_data == response.json def test_file_source_history_with_annotation(self, backend, xhr_header): response = self.app.get( - route_path('repo_file_history', - repo_name=backend.repo_name, - commit_id='tip', f_path='vcs/nodes.py', - params=dict(annotate=1)), - - extra_environ=xhr_header) + route_path( + "repo_file_history", + repo_name=backend.repo_name, + commit_id="tip", + f_path="vcs/nodes.py", + params=dict(annotate=1), + ), + extra_environ=xhr_header, + ) assert get_node_history(backend.alias) == json.loads(response.body) def test_tree_search_top_level(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_files_nodelist', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/'), - extra_environ=xhr_header) - assert 'nodes' in response.json - assert {'name': 'docs', 'type': 'dir'} in response.json['nodes'] + route_path("repo_files_nodelist", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path=""), + extra_environ=xhr_header, + ) + assert "nodes" in response.json + assert {"name": "docs", "type": "dir"} in response.json["nodes"] def test_tree_search_missing_xhr(self, backend): self.app.get( - route_path('repo_files_nodelist', - repo_name=backend.repo_name, - commit_id='tip', f_path='/'), - status=404) + route_path("repo_files_nodelist", repo_name=backend.repo_name, commit_id="tip", f_path=""), status=404 + ) def test_tree_search_at_path(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_files_nodelist', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/docs'), - extra_environ=xhr_header) - assert 'nodes' in response.json - nodes = response.json['nodes'] - assert {'name': 'docs/api', 'type': 'dir'} in nodes - assert {'name': 'docs/index.rst', 'type': 'file'} in nodes + route_path("repo_files_nodelist", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="/docs"), + extra_environ=xhr_header, + ) + assert "nodes" in response.json + nodes = response.json["nodes"] + assert {"name": "docs/api", "type": "dir"} in nodes + assert {"name": "docs/index.rst", "type": "file"} in nodes def test_tree_search_at_path_2nd_level(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_files_nodelist', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/docs/api'), - extra_environ=xhr_header) - assert 'nodes' in response.json - nodes = response.json['nodes'] - assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes + route_path("repo_files_nodelist", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="/docs/api"), + extra_environ=xhr_header, + ) + assert "nodes" in response.json + nodes = response.json["nodes"] + assert {"name": "docs/api/index.rst", "type": "file"} in nodes def test_tree_search_at_path_missing_xhr(self, backend): self.app.get( - route_path('repo_files_nodelist', - repo_name=backend.repo_name, - commit_id='tip', f_path='/docs'), - status=404) + route_path("repo_files_nodelist", repo_name=backend.repo_name, commit_id="tip", f_path="/docs"), status=404 + ) def test_nodetree(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_nodetree_full', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/'), - extra_environ=xhr_header) + route_path("repo_nodetree_full", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path=""), + extra_environ=xhr_header, + ) assert_response = response.assert_response() - for attr in ['data-commit-id', 'data-date', 'data-author']: - elements = assert_response.get_elements('[{}]'.format(attr)) + for attr in ["data-commit-id", "data-date", "data-author"]: + elements = assert_response.get_elements("[{}]".format(attr)) assert len(elements) > 1 for element in elements: @@ -361,124 +322,113 @@ class TestFilesViews(object): def test_nodetree_if_file(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_nodetree_full', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='README.rst'), - extra_environ=xhr_header) - assert response.text == '' + route_path("repo_nodetree_full", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="README.rst"), + extra_environ=xhr_header, + ) + assert response.text == "" def test_nodetree_wrong_path(self, backend, xhr_header): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_nodetree_full', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='/dont-exist'), - extra_environ=xhr_header) + route_path( + "repo_nodetree_full", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="/dont-exist" + ), + extra_environ=xhr_header, + ) - err = 'error: There is no file nor ' \ - 'directory at the given path' + err = "error: There is no file nor " "directory at the given path" assert err in response.text def test_nodetree_missing_xhr(self, backend): self.app.get( - route_path('repo_nodetree_full', - repo_name=backend.repo_name, - commit_id='tip', f_path='/'), - status=404) + route_path("repo_nodetree_full", repo_name=backend.repo_name, commit_id="tip", f_path=""), status=404 + ) @pytest.mark.usefixtures("app", "autologin_user") class TestRawFileHandling(object): - def test_download_file(self, backend): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_file_download', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='vcs/nodes.py'),) + route_path( + "repo_file_download", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="vcs/nodes.py" + ), + ) - assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py' + assert response.content_disposition == "attachment; filename=\"nodes.py\"; filename*=UTF-8''nodes.py" assert response.content_type == "text/x-python" def test_download_file_wrong_cs(self, backend): - raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc' + raw_id = "ERRORce30c96924232dffcd24178a07ffeb5dfc" response = self.app.get( - route_path('repo_file_download', - repo_name=backend.repo_name, - commit_id=raw_id, f_path='vcs/nodes.svg'), - status=404) + route_path("repo_file_download", repo_name=backend.repo_name, commit_id=raw_id, f_path="vcs/nodes.svg"), + status=404, + ) msg = """No such commit exists for this repository""" response.mustcontain(msg) def test_download_file_wrong_f_path(self, backend): commit = backend.repo.get_commit(commit_idx=173) - f_path = 'vcs/ERRORnodes.py' + f_path = "vcs/ERRORnodes.py" response = self.app.get( - route_path('repo_file_download', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path=f_path), - status=404) + route_path("repo_file_download", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path=f_path), + status=404, + ) - msg = ( - "There is no file nor directory at the given path: " - "`%s` at commit %s" % (f_path, commit.short_id)) + msg = "There is no file nor directory at the given path: " "`%s` at commit %s" % (f_path, commit.short_id) response.mustcontain(msg) def test_file_raw(self, backend): commit = backend.repo.get_commit(commit_idx=173) response = self.app.get( - route_path('repo_file_raw', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path='vcs/nodes.py'),) + route_path("repo_file_raw", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path="vcs/nodes.py"), + ) assert response.content_type == "text/plain" def test_file_raw_binary(self, backend): commit = backend.repo.get_commit() response = self.app.get( - route_path('repo_file_raw', - repo_name=backend.repo_name, - commit_id=commit.raw_id, - f_path='docs/theme/ADC/static/breadcrumb_background.png'),) + route_path( + "repo_file_raw", + repo_name=backend.repo_name, + commit_id=commit.raw_id, + f_path="docs/theme/ADC/static/breadcrumb_background.png", + ), + ) - assert response.content_disposition == 'inline' + assert response.content_disposition == "inline" def test_raw_file_wrong_cs(self, backend): - raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc' + raw_id = "ERRORcce30c96924232dffcd24178a07ffeb5dfc" response = self.app.get( - route_path('repo_file_raw', - repo_name=backend.repo_name, - commit_id=raw_id, f_path='vcs/nodes.svg'), - status=404) + route_path("repo_file_raw", repo_name=backend.repo_name, commit_id=raw_id, f_path="vcs/nodes.svg"), + status=404, + ) msg = """No such commit exists for this repository""" response.mustcontain(msg) def test_raw_wrong_f_path(self, backend): commit = backend.repo.get_commit(commit_idx=173) - f_path = 'vcs/ERRORnodes.py' + f_path = "vcs/ERRORnodes.py" response = self.app.get( - route_path('repo_file_raw', - repo_name=backend.repo_name, - commit_id=commit.raw_id, f_path=f_path), - status=404) + route_path("repo_file_raw", repo_name=backend.repo_name, commit_id=commit.raw_id, f_path=f_path), status=404 + ) - msg = ( - "There is no file nor directory at the given path: " - "`%s` at commit %s" % (f_path, commit.short_id)) + msg = "There is no file nor directory at the given path: " "`%s` at commit %s" % (f_path, commit.short_id) response.mustcontain(msg) def test_raw_svg_should_not_be_rendered(self, backend): backend.create_repo() backend.ensure_file(b"xss.svg") response = self.app.get( - route_path('repo_file_raw', - repo_name=backend.repo_name, - commit_id='tip', f_path='xss.svg'),) + route_path("repo_file_raw", repo_name=backend.repo_name, commit_id="tip", f_path="xss.svg"), + ) # If the content type is image/svg+xml then it allows to render HTML # and malicious SVG. assert response.content_type == "text/plain" @@ -486,25 +436,23 @@ class TestRawFileHandling(object): @pytest.mark.usefixtures("app") class TestRepositoryArchival(object): - def test_archival(self, backend): backend.enable_downloads() commit = backend.repo.get_commit(commit_idx=173) for a_type, content_type, extension in settings.ARCHIVE_SPECS: - path_sha = get_path_sha('/') - filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha) + path_sha = get_path_sha("/") + filename = get_archive_name( + backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha + ) fname = commit.raw_id + extension - response = self.app.get( - route_path('repo_archivefile', - repo_name=backend.repo_name, - fname=fname)) + response = self.app.get(route_path("repo_archivefile", repo_name=backend.repo_name, fname=fname)) - assert response.status == '200 OK' + assert response.status == "200 OK" headers = [ - ('Content-Disposition', f'attachment; filename={filename}'), - ('Content-Type', content_type), + ("Content-Disposition", f"attachment; filename={filename}"), + ("Content-Type", content_type), ] for header in headers: @@ -514,19 +462,25 @@ class TestRepositoryArchival(object): backend.enable_downloads() commit = backend.repo.get_commit(commit_idx=173) for a_type, content_type, extension in settings.ARCHIVE_SPECS: - path_sha = get_path_sha('/') - filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False) + path_sha = get_path_sha("/") + filename = get_archive_name( + backend.repo_id, + backend.repo_name, + commit_sha=commit.short_id, + ext=extension, + path_sha=path_sha, + with_hash=False, + ) fname = commit.raw_id + extension response = self.app.get( - route_path('repo_archivefile', - repo_name=backend.repo_name, - fname=fname, params={'with_hash': 0})) + route_path("repo_archivefile", repo_name=backend.repo_name, fname=fname, params={"with_hash": 0}) + ) - assert response.status == '200 OK' + assert response.status == "200 OK" headers = [ - ('Content-Disposition', f'attachment; filename={filename}'), - ('Content-Type', content_type), + ("Content-Disposition", f"attachment; filename={filename}"), + ("Content-Type", content_type), ] for header in headers: @@ -535,359 +489,332 @@ class TestRepositoryArchival(object): def test_archival_at_path(self, backend): backend.enable_downloads() commit = backend.repo.get_commit(commit_idx=190) - at_path = 'vcs' + at_path = "vcs" for a_type, content_type, extension in settings.ARCHIVE_SPECS: path_sha = get_path_sha(at_path) - filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha) + filename = get_archive_name( + backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha + ) fname = commit.raw_id + extension response = self.app.get( - route_path('repo_archivefile', - repo_name=backend.repo_name, - fname=fname, params={'at_path': at_path})) + route_path("repo_archivefile", repo_name=backend.repo_name, fname=fname, params={"at_path": at_path}) + ) - assert response.status == '200 OK' + assert response.status == "200 OK" headers = [ - ('Content-Disposition', f'attachment; filename={filename}'), - ('Content-Type', content_type), + ("Content-Disposition", f"attachment; filename={filename}"), + ("Content-Type", content_type), ] for header in headers: assert header in list(response.headers.items()) - @pytest.mark.parametrize('arch_ext',[ - 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar']) + @pytest.mark.parametrize("arch_ext", ["tar", "rar", "x", "..ax", ".zipz", "tar.gz.tar"]) def test_archival_wrong_ext(self, backend, arch_ext): backend.enable_downloads() commit = backend.repo.get_commit(commit_idx=173) - fname = commit.raw_id + '.' + arch_ext + fname = commit.raw_id + "." + arch_ext - response = self.app.get( - route_path('repo_archivefile', - repo_name=backend.repo_name, - fname=fname)) - response.mustcontain( - 'Unknown archive type for: `{}`'.format(fname)) + response = self.app.get(route_path("repo_archivefile", repo_name=backend.repo_name, fname=fname)) + response.mustcontain("Unknown archive type for: `{}`".format(fname)) - @pytest.mark.parametrize('commit_id', [ - '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd']) + @pytest.mark.parametrize("commit_id", ["00x000000", "tar", "wrong", "@$@$42413232", "232dffcd"]) def test_archival_wrong_commit_id(self, backend, commit_id): backend.enable_downloads() - fname = f'{commit_id}.zip' + fname = f"{commit_id}.zip" - response = self.app.get( - route_path('repo_archivefile', - repo_name=backend.repo_name, - fname=fname)) - response.mustcontain('Unknown commit_id') + response = self.app.get(route_path("repo_archivefile", repo_name=backend.repo_name, fname=fname)) + response.mustcontain("Unknown commit_id") @pytest.mark.usefixtures("app") class TestFilesDiff(object): - - @pytest.mark.parametrize("diff", ['diff', 'download', 'raw']) + @pytest.mark.parametrize("diff", ["diff", "download", "raw"]) def test_file_full_diff(self, backend, diff): commit1 = backend.repo.get_commit(commit_idx=-1) commit2 = backend.repo.get_commit(commit_idx=-2) response = self.app.get( - route_path('repo_files_diff', - repo_name=backend.repo_name, - f_path='README'), + route_path("repo_files_diff", repo_name=backend.repo_name, f_path="README"), params={ - 'diff1': commit2.raw_id, - 'diff2': commit1.raw_id, - 'fulldiff': '1', - 'diff': diff, - }) + "diff1": commit2.raw_id, + "diff2": commit1.raw_id, + "fulldiff": "1", + "diff": diff, + }, + ) - if diff == 'diff': + if diff == "diff": # use redirect since this is OLD view redirecting to compare page response = response.follow() # It's a symlink to README.rst - response.mustcontain('README.rst') - response.mustcontain('No newline at end of file') + response.mustcontain("README.rst") + response.mustcontain("No newline at end of file") def test_file_binary_diff(self, backend): commits = [ - {'message': 'First commit'}, - {'message': 'Commit with binary', - 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]}, + {"message": "First commit"}, + {"message": "Commit with binary", "added": [nodes.FileNode(b"file.bin", content="\0BINARY\0")]}, ] repo = backend.create_repo(commits=commits) response = self.app.get( - route_path('repo_files_diff', - repo_name=backend.repo_name, - f_path='file.bin'), + route_path("repo_files_diff", repo_name=backend.repo_name, f_path="file.bin"), params={ - 'diff1': repo.get_commit(commit_idx=0).raw_id, - 'diff2': repo.get_commit(commit_idx=1).raw_id, - 'fulldiff': '1', - 'diff': 'diff', - }) + "diff1": repo.get_commit(commit_idx=0).raw_id, + "diff2": repo.get_commit(commit_idx=1).raw_id, + "fulldiff": "1", + "diff": "diff", + }, + ) # use redirect since this is OLD view redirecting to compare page response = response.follow() - response.mustcontain('Collapse 1 commit') + response.mustcontain("Collapse 1 commit") file_changes = (1, 0, 0) compare_page = ComparePage(response) compare_page.contains_change_summary(*file_changes) - if backend.alias == 'svn': - response.mustcontain('new file 10644') + if backend.alias == "svn": + response.mustcontain("new file 10644") # TODO(marcink): SVN doesn't yet detect binary changes else: - response.mustcontain('new file 100644') - response.mustcontain('binary diff hidden') + response.mustcontain("new file 100644") + response.mustcontain("binary diff hidden") def test_diff_2way(self, backend): commit1 = backend.repo.get_commit(commit_idx=-1) commit2 = backend.repo.get_commit(commit_idx=-2) response = self.app.get( - route_path('repo_files_diff_2way_redirect', - repo_name=backend.repo_name, - f_path='README'), + route_path("repo_files_diff_2way_redirect", repo_name=backend.repo_name, f_path="README"), params={ - 'diff1': commit2.raw_id, - 'diff2': commit1.raw_id, - }) + "diff1": commit2.raw_id, + "diff2": commit1.raw_id, + }, + ) # use redirect since this is OLD view redirecting to compare page response = response.follow() # It's a symlink to README.rst - response.mustcontain('README.rst') - response.mustcontain('No newline at end of file') + response.mustcontain("README.rst") + response.mustcontain("No newline at end of file") def test_requires_one_commit_id(self, backend, autologin_user): response = self.app.get( - route_path('repo_files_diff', - repo_name=backend.repo_name, - f_path='README.rst'), - status=400) - response.mustcontain( - 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.') + route_path("repo_files_diff", repo_name=backend.repo_name, f_path="README.rst"), status=400 + ) + response.mustcontain("Need query parameter", "diff1", "diff2", "to generate a diff.") def test_returns_no_files_if_file_does_not_exist(self, vcsbackend): repo = vcsbackend.repo response = self.app.get( - route_path('repo_files_diff', - repo_name=repo.name, - f_path='does-not-exist-in-any-commit'), - params={ - 'diff1': repo[0].raw_id, - 'diff2': repo[1].raw_id - }) + route_path("repo_files_diff", repo_name=repo.name, f_path="does-not-exist-in-any-commit"), + params={"diff1": repo[0].raw_id, "diff2": repo[1].raw_id}, + ) response = response.follow() - response.mustcontain('No files') + response.mustcontain("No files") def test_returns_redirect_if_file_not_changed(self, backend): commit = backend.repo.get_commit(commit_idx=-1) response = self.app.get( - route_path('repo_files_diff_2way_redirect', - repo_name=backend.repo_name, - f_path='README'), + route_path("repo_files_diff_2way_redirect", repo_name=backend.repo_name, f_path="README"), params={ - 'diff1': commit.raw_id, - 'diff2': commit.raw_id, - }) + "diff1": commit.raw_id, + "diff2": commit.raw_id, + }, + ) response = response.follow() - response.mustcontain('No files') - response.mustcontain('No commits in this compare') + response.mustcontain("No files") + response.mustcontain("No commits in this compare") def test_supports_diff_to_different_path_svn(self, backend_svn): - #TODO: check this case + # TODO: check this case return - repo = backend_svn['svn-simple-layout'].scm_instance() - commit_id_1 = '24' - commit_id_2 = '26' + repo = backend_svn["svn-simple-layout"].scm_instance() + commit_id_1 = "24" + commit_id_2 = "26" response = self.app.get( - route_path('repo_files_diff', - repo_name=backend_svn.repo_name, - f_path='trunk/example.py'), + route_path("repo_files_diff", repo_name=backend_svn.repo_name, f_path="trunk/example.py"), params={ - 'diff1': 'tags/v0.2/example.py@' + commit_id_1, - 'diff2': commit_id_2, - }) + "diff1": "tags/v0.2/example.py@" + commit_id_1, + "diff2": commit_id_2, + }, + ) response = response.follow() response.mustcontain( # diff contains this - "Will print out a useful message on invocation.") + "Will print out a useful message on invocation." + ) # Note: Expecting that we indicate the user what's being compared response.mustcontain("trunk/example.py") response.mustcontain("tags/v0.2/example.py") def test_show_rev_redirects_to_svn_path(self, backend_svn): - #TODO: check this case + # TODO: check this case return - repo = backend_svn['svn-simple-layout'].scm_instance() + repo = backend_svn["svn-simple-layout"].scm_instance() commit_id = repo[-1].raw_id response = self.app.get( - route_path('repo_files_diff', - repo_name=backend_svn.repo_name, - f_path='trunk/example.py'), + route_path("repo_files_diff", repo_name=backend_svn.repo_name, f_path="trunk/example.py"), params={ - 'diff1': 'branches/argparse/example.py@' + commit_id, - 'diff2': commit_id, + "diff1": "branches/argparse/example.py@" + commit_id, + "diff2": commit_id, }, - status=302) + status=302, + ) response = response.follow() - assert response.headers['Location'].endswith( - 'svn-svn-simple-layout/files/26/branches/argparse/example.py') + assert response.headers["Location"].endswith("svn-svn-simple-layout/files/26/branches/argparse/example.py") def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn): - #TODO: check this case + # TODO: check this case return - repo = backend_svn['svn-simple-layout'].scm_instance() + repo = backend_svn["svn-simple-layout"].scm_instance() commit_id = repo[-1].raw_id response = self.app.get( - route_path('repo_files_diff', - repo_name=backend_svn.repo_name, - f_path='trunk/example.py'), + route_path("repo_files_diff", repo_name=backend_svn.repo_name, f_path="trunk/example.py"), params={ - 'diff1': 'branches/argparse/example.py@' + commit_id, - 'diff2': commit_id, - 'show_rev': 'Show at Revision', - 'annotate': 'true', + "diff1": "branches/argparse/example.py@" + commit_id, + "diff2": commit_id, + "show_rev": "Show at Revision", + "annotate": "true", }, - status=302) + status=302, + ) response = response.follow() - assert response.headers['Location'].endswith( - 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py') + assert response.headers["Location"].endswith("svn-svn-simple-layout/annotate/26/branches/argparse/example.py") @pytest.mark.usefixtures("app", "autologin_user") class TestModifyFilesWithWebInterface(object): - def test_add_file_view(self, backend): - self.app.get( - route_path('repo_files_add_file', - repo_name=backend.repo_name, - commit_id='tip', f_path='/') - ) + self.app.get(route_path("repo_files_add_file", repo_name=backend.repo_name, commit_id="tip", f_path="")) @pytest.mark.xfail_backends("svn", reason="Depends on online editing") def test_add_file_into_repo_missing_content(self, backend, csrf_token): backend.create_repo() - filename = 'init.py' + filename = "init.py" response = self.app.post( - route_path('repo_files_create_file', - repo_name=backend.repo_name, - commit_id='tip', f_path='/'), + route_path("repo_files_create_file", repo_name=backend.repo_name, commit_id="tip", f_path=""), params={ - 'content': "", - 'filename': filename, - 'csrf_token': csrf_token, + "content": "", + "filename": filename, + "csrf_token": csrf_token, }, - status=302) - expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename)) + status=302, + ) + expected_msg = "Successfully committed new file `{}`".format(os.path.join(filename)) assert_session_flash(response, expected_msg) def test_add_file_into_repo_missing_filename(self, backend, csrf_token): commit_id = backend.repo.get_commit().raw_id response = self.app.post( - route_path('repo_files_create_file', - repo_name=backend.repo_name, - commit_id=commit_id, f_path='/'), + route_path("repo_files_create_file", repo_name=backend.repo_name, commit_id=commit_id, f_path=""), params={ - 'content': "foo", - 'csrf_token': csrf_token, + "content": "foo", + "csrf_token": csrf_token, }, - status=302) + status=302, + ) - assert_session_flash(response, 'No filename specified') + assert_session_flash(response, "No filename specified") - def test_add_file_into_repo_errors_and_no_commits( - self, backend, csrf_token): + def test_add_file_into_repo_errors_and_no_commits(self, backend, csrf_token): repo = backend.create_repo() # Create a file with no filename, it will display an error but # the repo has no commits yet response = self.app.post( - route_path('repo_files_create_file', - repo_name=repo.repo_name, - commit_id='tip', f_path='/'), + route_path("repo_files_create_file", repo_name=repo.repo_name, commit_id="tip", f_path=""), params={ - 'content': "foo", - 'csrf_token': csrf_token, + "content": "foo", + "csrf_token": csrf_token, }, - status=302) + status=302, + ) - assert_session_flash(response, 'No filename specified') + assert_session_flash(response, "No filename specified") # Not allowed, redirect to the summary redirected = response.follow() - summary_url = h.route_path('repo_summary', repo_name=repo.repo_name) + summary_url = h.route_path("repo_summary", repo_name=repo.repo_name) # As there are no commits, displays the summary page with the error of # creating a file with no filename assert redirected.request.path == summary_url - @pytest.mark.parametrize("filename, clean_filename", [ - ('/abs/foo', 'abs/foo'), - ('../rel/foo', 'rel/foo'), - ('file/../foo/foo', 'file/foo/foo'), - ]) + @pytest.mark.parametrize( + "filename, clean_filename", + [ + ("/abs/foo", "abs/foo"), + ("../rel/foo", "rel/foo"), + ("file/../foo/foo", "file/foo/foo"), + ], + ) def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token): repo = backend.create_repo() commit_id = repo.get_commit().raw_id response = self.app.post( - route_path('repo_files_create_file', - repo_name=repo.repo_name, - commit_id=commit_id, f_path='/'), + route_path("repo_files_create_file", repo_name=repo.repo_name, commit_id=commit_id, f_path=""), params={ - 'content': "foo", - 'filename': filename, - 'csrf_token': csrf_token, + "content": "foo", + "filename": filename, + "csrf_token": csrf_token, }, - status=302) + status=302, + ) - expected_msg = 'Successfully committed new file `{}`'.format(clean_filename) + expected_msg = "Successfully committed new file `{}`".format(clean_filename) assert_session_flash(response, expected_msg) - @pytest.mark.parametrize("cnt, filename, content", [ - (1, 'foo.txt', "Content"), - (2, 'dir/foo.rst', "Content"), - (3, 'dir/foo-second.rst', "Content"), - (4, 'rel/dir/foo.bar', "Content"), - ]) + @pytest.mark.parametrize( + "cnt, filename, content", + [ + (1, "foo.txt", "Content"), + (2, "dir/foo.rst", "Content"), + (3, "dir/foo-second.rst", "Content"), + (4, "rel/dir/foo.bar", "Content"), + ], + ) def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token): repo = backend.create_repo() commit_id = repo.get_commit().raw_id response = self.app.post( - route_path('repo_files_create_file', - repo_name=repo.repo_name, - commit_id=commit_id, f_path='/'), + route_path("repo_files_create_file", repo_name=repo.repo_name, commit_id=commit_id, f_path=""), params={ - 'content': content, - 'filename': filename, - 'csrf_token': csrf_token, + "content": content, + "filename": filename, + "csrf_token": csrf_token, }, - status=302) + status=302, + ) - expected_msg = 'Successfully committed new file `{}`'.format(filename) + expected_msg = "Successfully committed new file `{}`".format(filename) assert_session_flash(response, expected_msg) def test_edit_file_view(self, backend): response = self.app.get( - route_path('repo_files_edit_file', - repo_name=backend.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/nodes.py'), - status=200) + route_path( + "repo_files_edit_file", + repo_name=backend.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/nodes.py", + ), + status=200, + ) response.mustcontain("Module holding everything related to vcs nodes.") def test_edit_file_view_not_on_branch(self, backend): @@ -895,229 +822,221 @@ class TestModifyFilesWithWebInterface(ob backend.ensure_file(b"vcs/nodes.py") response = self.app.get( - route_path('repo_files_edit_file', - repo_name=repo.repo_name, - commit_id='tip', - f_path='vcs/nodes.py'), - status=302) - assert_session_flash( - response, 'Cannot modify file. Given commit `tip` is not head of a branch.') + route_path("repo_files_edit_file", repo_name=repo.repo_name, commit_id="tip", f_path="vcs/nodes.py"), + status=302, + ) + assert_session_flash(response, "Cannot modify file. Given commit `tip` is not head of a branch.") def test_edit_file_view_commit_changes(self, backend, csrf_token): repo = backend.create_repo() backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'") response = self.app.post( - route_path('repo_files_update_file', - repo_name=repo.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/nodes.py'), + route_path( + "repo_files_update_file", + repo_name=repo.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/nodes.py", + ), params={ - 'content': "print 'hello world'", - 'message': 'I committed', - 'filename': "vcs/nodes.py", - 'csrf_token': csrf_token, + "content": "print 'hello world'", + "message": "I committed", + "filename": "vcs/nodes.py", + "csrf_token": csrf_token, }, - status=302) - assert_session_flash( - response, 'Successfully committed changes to file `vcs/nodes.py`') + status=302, + ) + assert_session_flash(response, "Successfully committed changes to file `vcs/nodes.py`") tip = repo.get_commit(commit_idx=-1) - assert tip.message == 'I committed' + assert tip.message == "I committed" def test_replace_binary_file_view_commit_changes(self, backend, csrf_token): repo = backend.create_repo() backend.ensure_file(b"vcs/nodes.docx", content=b"PREVIOUS CONTENT'") response = self.app.post( - route_path('repo_files_replace_binary', - repo_name=repo.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/nodes.docx'), + route_path( + "repo_files_replace_binary", + repo_name=repo.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/nodes.docx", + ), params={ - 'message': 'I committed', - 'csrf_token': csrf_token, + "message": "I committed", + "csrf_token": csrf_token, }, - upload_files=[('files_upload', 'vcs/nodes.docx', b'SOME CONTENT')], - status=200) - assert_session_flash( - response, 'Successfully committed 1 new file') + upload_files=[("files_upload", "vcs/nodes.docx", b"SOME CONTENT")], + status=200, + ) + assert_session_flash(response, "Successfully committed 1 new file") tip = repo.get_commit(commit_idx=-1) - assert tip.message == 'I committed' + assert tip.message == "I committed" - def test_edit_file_view_commit_changes_default_message(self, backend, - csrf_token): + def test_edit_file_view_commit_changes_default_message(self, backend, csrf_token): repo = backend.create_repo() backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'") - commit_id = ( - backend.default_branch_name or - backend.repo.scm_instance().commit_ids[-1]) + commit_id = backend.default_branch_name or backend.repo.scm_instance().commit_ids[-1] response = self.app.post( - route_path('repo_files_update_file', - repo_name=repo.repo_name, - commit_id=commit_id, - f_path='vcs/nodes.py'), + route_path("repo_files_update_file", repo_name=repo.repo_name, commit_id=commit_id, f_path="vcs/nodes.py"), params={ - 'content': "print 'hello world'", - 'message': '', - 'filename': "vcs/nodes.py", - 'csrf_token': csrf_token, + "content": "print 'hello world'", + "message": "", + "filename": "vcs/nodes.py", + "csrf_token": csrf_token, }, - status=302) - assert_session_flash( - response, 'Successfully committed changes to file `vcs/nodes.py`') + status=302, + ) + assert_session_flash(response, "Successfully committed changes to file `vcs/nodes.py`") tip = repo.get_commit(commit_idx=-1) - assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise' + assert tip.message == "Edited file vcs/nodes.py via RhodeCode Enterprise" def test_replace_binary_file_content_with_content_that_not_belong_to_original_type(self, backend, csrf_token): repo = backend.create_repo() backend.ensure_file(b"vcs/sheet.xlsx", content=b"PREVIOUS CONTENT'") response = self.app.post( - route_path('repo_files_replace_binary', - repo_name=repo.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/sheet.xlsx'), + route_path( + "repo_files_replace_binary", + repo_name=repo.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/sheet.xlsx", + ), params={ - 'message': 'I committed', - 'csrf_token': csrf_token, + "message": "I committed", + "csrf_token": csrf_token, }, - upload_files=[('files_upload', 'vcs/sheet.docx', b'SOME CONTENT')], - status=200) - assert response.json['error'] == "file extension of uploaded file doesn't match an original file's extension" + upload_files=[("files_upload", "vcs/sheet.docx", b"SOME CONTENT")], + status=200, + ) + assert response.json["error"] == "file extension of uploaded file doesn't match an original file's extension" - @pytest.mark.parametrize("replacement_files, expected_error", [ - ([], 'missing files'), - ( - [('files_upload', 'vcs/node1.docx', b'SOME CONTENT'), - ('files_upload', 'vcs/node2.docx', b'SOME CONTENT')], - 'too many files for replacement'), - ]) - def test_replace_binary_with_wrong_amount_of_content_sources(self, replacement_files, expected_error, backend, - csrf_token): + @pytest.mark.parametrize( + "replacement_files, expected_error", + [ + ([], "missing files"), + ( + [ + ("files_upload", "vcs/node1.docx", b"SOME CONTENT"), + ("files_upload", "vcs/node2.docx", b"SOME CONTENT"), + ], + "too many files for replacement", + ), + ], + ) + def test_replace_binary_with_wrong_amount_of_content_sources( + self, replacement_files, expected_error, backend, csrf_token + ): repo = backend.create_repo() backend.ensure_file(b"vcs/node.docx", content=b"PREVIOUS CONTENT'") response = self.app.post( - route_path('repo_files_replace_binary', - repo_name=repo.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/node.docx'), + route_path( + "repo_files_replace_binary", + repo_name=repo.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/node.docx", + ), params={ - 'message': 'I committed', - 'csrf_token': csrf_token, + "message": "I committed", + "csrf_token": csrf_token, }, upload_files=replacement_files, - status=200) - assert response.json['error'] == expected_error + status=200, + ) + assert response.json["error"] == expected_error def test_delete_file_view(self, backend): self.app.get( - route_path('repo_files_remove_file', - repo_name=backend.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/nodes.py'), - status=200) + route_path( + "repo_files_remove_file", + repo_name=backend.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/nodes.py", + ), + status=200, + ) def test_delete_file_view_not_on_branch(self, backend): repo = backend.create_repo() - backend.ensure_file(b'vcs/nodes.py') + backend.ensure_file(b"vcs/nodes.py") response = self.app.get( - route_path('repo_files_remove_file', - repo_name=repo.repo_name, - commit_id='tip', - f_path='vcs/nodes.py'), - status=302) - assert_session_flash( - response, 'Cannot modify file. Given commit `tip` is not head of a branch.') + route_path("repo_files_remove_file", repo_name=repo.repo_name, commit_id="tip", f_path="vcs/nodes.py"), + status=302, + ) + assert_session_flash(response, "Cannot modify file. Given commit `tip` is not head of a branch.") def test_delete_file_view_commit_changes(self, backend, csrf_token): repo = backend.create_repo() backend.ensure_file(b"vcs/nodes.py") response = self.app.post( - route_path('repo_files_delete_file', - repo_name=repo.repo_name, - commit_id=backend.default_head_id, - f_path='vcs/nodes.py'), + route_path( + "repo_files_delete_file", + repo_name=repo.repo_name, + commit_id=backend.default_head_id, + f_path="vcs/nodes.py", + ), params={ - 'message': 'i committed', - 'csrf_token': csrf_token, + "message": "i committed", + "csrf_token": csrf_token, }, - status=302) - assert_session_flash( - response, 'Successfully deleted file `vcs/nodes.py`') + status=302, + ) + assert_session_flash(response, "Successfully deleted file `vcs/nodes.py`") @pytest.mark.usefixtures("app") class TestFilesViewOtherCases(object): - def test_access_empty_repo_redirect_to_summary_with_alert_write_perms( - self, backend_stub, autologin_regular_user, user_regular, - user_util): - + self, backend_stub, autologin_regular_user, user_regular, user_util + ): repo = backend_stub.create_repo() - user_util.grant_user_permission_to_repo( - repo, user_regular, 'repository.write') - response = self.app.get( - route_path('repo_files', - repo_name=repo.repo_name, - commit_id='tip', f_path='/')) + user_util.grant_user_permission_to_repo(repo, user_regular, "repository.write") + response = self.app.get(route_path("repo_files", repo_name=repo.repo_name, commit_id="tip", f_path="")) - repo_file_add_url = route_path( - 'repo_files_add_file', - repo_name=repo.repo_name, - commit_id=0, f_path='') + repo_file_add_url = route_path("repo_files_add_file", repo_name=repo.repo_name, commit_id=0, f_path="") add_new = f'
    add a new file' - repo_file_upload_url = route_path( - 'repo_files_upload_file', - repo_name=repo.repo_name, - commit_id=0, f_path='') + repo_file_upload_url = route_path("repo_files_upload_file", repo_name=repo.repo_name, commit_id=0, f_path="") upload_new = f'upload a new file' - assert_session_flash( - response, - 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new) - ) + assert_session_flash(response, "There are no files yet. Click here to %s or %s." % (add_new, upload_new)) def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms( - self, backend_stub, autologin_regular_user): + self, backend_stub, autologin_regular_user + ): repo = backend_stub.create_repo() # init session for anon user - route_path('repo_summary', repo_name=repo.repo_name) + route_path("repo_summary", repo_name=repo.repo_name) - repo_file_add_url = route_path( - 'repo_files_add_file', - repo_name=repo.repo_name, - commit_id=0, f_path='') + repo_file_add_url = route_path("repo_files_add_file", repo_name=repo.repo_name, commit_id=0, f_path="") - response = self.app.get( - route_path('repo_files', - repo_name=repo.repo_name, - commit_id='tip', f_path='/')) + response = self.app.get(route_path("repo_files", repo_name=repo.repo_name, commit_id="tip", f_path="")) assert_session_flash(response, no_=repo_file_add_url) - @pytest.mark.parametrize('file_node', [ - b'archive/file.zip', - b'diff/my-file.txt', - b'render.py', - b'render', - b'remove_file', - b'remove_file/to-delete.txt', - ]) + @pytest.mark.parametrize( + "file_node", + [ + b"archive/file.zip", + b"diff/my-file.txt", + b"render.py", + b"render", + b"remove_file", + b"remove_file/to-delete.txt", + ], + ) def test_file_names_equal_to_routes_parts(self, backend, file_node): backend.create_repo() backend.ensure_file(file_node) self.app.get( - route_path('repo_files', - repo_name=backend.repo_name, - commit_id='tip', f_path=safe_str(file_node)), - status=200) + route_path("repo_files", repo_name=backend.repo_name, commit_id="tip", f_path=safe_str(file_node)), + status=200, + ) class TestAdjustFilePathForSvn(object): @@ -1126,20 +1045,20 @@ class TestAdjustFilePathForSvn(object): """ def test_returns_path_relative_to_matched_reference(self): - repo = self._repo(branches=['trunk']) - self.assert_file_adjustment('trunk/file', 'file', repo) + repo = self._repo(branches=["trunk"]) + self.assert_file_adjustment("trunk/file", "file", repo) def test_does_not_modify_file_if_no_reference_matches(self): - repo = self._repo(branches=['trunk']) - self.assert_file_adjustment('notes/file', 'notes/file', repo) + repo = self._repo(branches=["trunk"]) + self.assert_file_adjustment("notes/file", "notes/file", repo) def test_does_not_adjust_partial_directory_names(self): - repo = self._repo(branches=['trun']) - self.assert_file_adjustment('trunk/file', 'trunk/file', repo) + repo = self._repo(branches=["trun"]) + self.assert_file_adjustment("trunk/file", "trunk/file", repo) def test_is_robust_to_patterns_which_prefix_other_patterns(self): - repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old']) - self.assert_file_adjustment('trunk/new/file', 'file', repo) + repo = self._repo(branches=["trunk", "trunk/new", "trunk/old"]) + self.assert_file_adjustment("trunk/new/file", "file", repo) def assert_file_adjustment(self, f_path, expected, repo): result = RepoFilesView.adjust_file_path_for_svn(f_path, repo) @@ -1147,6 +1066,6 @@ class TestAdjustFilePathForSvn(object): def _repo(self, branches=None): repo = mock.Mock() - repo.branches = OrderedDict((name, '0') for name in branches or []) + repo.branches = OrderedDict((name, "0") for name in branches or []) repo.tags = {} return repo diff --git a/rhodecode/apps/repository/tests/test_repo_forks.py b/rhodecode/apps/repository/tests/test_repo_forks.py --- a/rhodecode/apps/repository/tests/test_repo_forks.py +++ b/rhodecode/apps/repository/tests/test_repo_forks.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,7 +20,7 @@ import pytest from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.lib import helpers as h from rhodecode.model.db import Repository diff --git a/rhodecode/apps/repository/tests/test_repo_issue_tracker.py b/rhodecode/apps/repository/tests/test_repo_issue_tracker.py --- a/rhodecode/apps/repository/tests/test_repo_issue_tracker.py +++ b/rhodecode/apps/repository/tests/test_repo_issue_tracker.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_maintainance.py b/rhodecode/apps/repository/tests/test_repo_maintainance.py --- a/rhodecode/apps/repository/tests/test_repo_maintainance.py +++ b/rhodecode/apps/repository/tests/test_repo_maintainance.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,7 +20,7 @@ import pytest from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/repository/tests/test_repo_permissions.py b/rhodecode/apps/repository/tests/test_repo_permissions.py --- a/rhodecode/apps/repository/tests/test_repo_permissions.py +++ b/rhodecode/apps/repository/tests/test_repo_permissions.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_pullrequests.py b/rhodecode/apps/repository/tests/test_repo_pullrequests.py --- a/rhodecode/apps/repository/tests/test_repo_pullrequests.py +++ b/rhodecode/apps/repository/tests/test_repo_pullrequests.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -15,6 +15,9 @@ # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import logging +import os + import mock import pytest @@ -41,7 +44,7 @@ from rhodecode.tests import ( TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, ) -from rhodecode.tests.fixture_mods.fixture_utils import PRTestUtility +from rhodecode.tests.fixtures.fixture_utils import PRTestUtility from rhodecode.tests.routes import route_path @@ -1050,7 +1053,6 @@ class TestPullrequestsView(object): ) assert len(notifications.all()) == 2 - @pytest.mark.xfail(reason="unable to fix this test after python3 migration") def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token): commits = [ { @@ -1125,20 +1127,38 @@ class TestPullrequestsView(object): response.mustcontain(no=["content_of_ancestor-child"]) response.mustcontain("content_of_change") - def test_merge_pull_request_enabled(self, pr_util, csrf_token): - # Clear any previous calls to rcextensions - rhodecode.EXTENSIONS.calls.clear() + def test_merge_pull_request_enabled(self, pr_util, csrf_token, rcextensions_modification): pull_request = pr_util.create_pull_request(approved=True, mergeable=True) pull_request_id = pull_request.pull_request_id - repo_name = (pull_request.target_repo.scm_instance().name,) + repo_name = pull_request.target_repo.scm_instance().name url = route_path( "pullrequest_merge", - repo_name=str(repo_name[0]), + repo_name=repo_name, pull_request_id=pull_request_id, ) - response = self.app.post(url, params={"csrf_token": csrf_token}).follow() + + rcstack_location = os.path.dirname(self.app._pyramid_registry.settings['__file__']) + rc_ext_location = os.path.join(rcstack_location, 'rcextension-output.txt') + + + mods = [ + ('_push_hook', + f""" + import os + action = kwargs['action'] + commit_ids = kwargs['commit_ids'] + with open('{rc_ext_location}', 'w') as f: + f.write('test-execution'+os.linesep) + f.write(f'{{action}}'+os.linesep) + f.write(f'{{commit_ids}}'+os.linesep) + return HookResponse(0, 'HOOK_TEST') + """) + ] + # Add the hook + with rcextensions_modification(rcstack_location, mods, create_if_missing=True, force_create=True): + response = self.app.post(url, params={"csrf_token": csrf_token}).follow() pull_request = PullRequest.get(pull_request_id) @@ -1162,12 +1182,39 @@ class TestPullrequestsView(object): assert actions[-1].action == "user.push" assert actions[-1].action_data["commit_ids"] == pr_commit_ids - # Check post_push rcextension was really executed - push_calls = rhodecode.EXTENSIONS.calls["_push_hook"] - assert len(push_calls) == 1 - unused_last_call_args, last_call_kwargs = push_calls[0] - assert last_call_kwargs["action"] == "push" - assert last_call_kwargs["commit_ids"] == pr_commit_ids + with open(rc_ext_location) as f: + f_data = f.read() + assert 'test-execution' in f_data + for commit_id in pr_commit_ids: + assert f'{commit_id}' in f_data + + def test_merge_pull_request_forbidden_by_pre_push_hook(self, pr_util, csrf_token, rcextensions_modification, caplog): + caplog.set_level(logging.WARNING, logger="rhodecode.model.pull_request") + + pull_request = pr_util.create_pull_request(approved=True, mergeable=True) + pull_request_id = pull_request.pull_request_id + repo_name = pull_request.target_repo.scm_instance().name + + url = route_path( + "pullrequest_merge", + repo_name=repo_name, + pull_request_id=pull_request_id, + ) + + rcstack_location = os.path.dirname(self.app._pyramid_registry.settings['__file__']) + + mods = [ + ('_pre_push_hook', + f""" + return HookResponse(1, 'HOOK_TEST_FORBIDDEN') + """) + ] + # Add the hook + with rcextensions_modification(rcstack_location, mods, create_if_missing=True, force_create=True): + self.app.post(url, params={"csrf_token": csrf_token}) + + assert 'Merge failed, not updating the pull request.' in [r[2] for r in caplog.record_tuples] + def test_merge_pull_request_disabled(self, pr_util, csrf_token): pull_request = pr_util.create_pull_request(mergeable=False) @@ -1523,7 +1570,6 @@ class TestPullrequestsView(object): assert pull_request.revisions == [commit_ids["change-rebased"]] - def test_remove_pull_request_branch(self, backend_git, csrf_token): branch_name = "development" commits = [ diff --git a/rhodecode/apps/repository/tests/test_repo_settings.py b/rhodecode/apps/repository/tests/test_repo_settings.py --- a/rhodecode/apps/repository/tests/test_repo_settings.py +++ b/rhodecode/apps/repository/tests/test_repo_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,7 +25,7 @@ from rhodecode.model.db import Repositor from rhodecode.model.meta import Session from rhodecode.tests import ( TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py --- a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py +++ b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,7 +23,7 @@ from rhodecode.model.db import Repositor from rhodecode.model.repo import RepoModel from rhodecode.tests import ( HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import repo_on_filesystem from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/repository/tests/test_repo_summary.py b/rhodecode/apps/repository/tests/test_repo_summary.py --- a/rhodecode/apps/repository/tests/test_repo_summary.py +++ b/rhodecode/apps/repository/tests/test_repo_summary.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -31,7 +31,7 @@ from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.scm import ScmModel from rhodecode.tests import assert_session_flash -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import AssertResponse, repo_on_filesystem from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/repository/tests/test_repo_tags.py b/rhodecode/apps/repository/tests/test_repo_tags.py --- a/rhodecode/apps/repository/tests/test_repo_tags.py +++ b/rhodecode/apps/repository/tests/test_repo_tags.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/tests/test_repo_vcs_settings.py b/rhodecode/apps/repository/tests/test_repo_vcs_settings.py --- a/rhodecode/apps/repository/tests/test_repo_vcs_settings.py +++ b/rhodecode/apps/repository/tests/test_repo_vcs_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,7 +29,7 @@ from rhodecode.model.user import UserMod from rhodecode.tests import ( login_user_session, logout_user_session, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import AssertResponse from rhodecode.tests.routes import route_path @@ -48,6 +47,7 @@ class TestVcsSettings(object): 'extensions_evolve': False, 'phases_publish': 'False', 'rhodecode_pr_merge_enabled': False, + 'rhodecode_auto_merge_enabled': False, 'rhodecode_use_outdated_comments': False, 'new_svn_branch': '', 'new_svn_tag': '' @@ -59,7 +59,7 @@ class TestVcsSettings(object): response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name)) expected_settings = ( - 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled', + 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled', 'rhodecode_auto_merge_enabled', 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger', 'hooks_outgoing_pull_logger' ) diff --git a/rhodecode/apps/repository/tests/test_vcs_settings.py b/rhodecode/apps/repository/tests/test_vcs_settings.py --- a/rhodecode/apps/repository/tests/test_vcs_settings.py +++ b/rhodecode/apps/repository/tests/test_vcs_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,16 +31,13 @@ class TestAdminRepoVcsSettings(object): @pytest.mark.parametrize('setting_name, setting_backends', [ ('hg_use_rebase_for_merging', ['hg']), ]) - def test_labs_settings_visible_if_enabled( - self, setting_name, setting_backends, backend): + def test_labs_settings_visible_if_enabled(self, setting_name, setting_backends, backend): if backend.alias not in setting_backends: pytest.skip('Setting not available for backend {}'.format(backend)) - vcs_settings_url = route_path( - 'edit_repo_vcs', repo_name=backend.repo.repo_name) + vcs_settings_url = route_path('edit_repo_vcs', repo_name=backend.repo.repo_name) - with mock.patch.dict( - rhodecode.CONFIG, {'labs_settings_active': 'true'}): + with mock.patch.dict(rhodecode.CONFIG, {'labs_settings_active': 'true'}): response = self.app.get(vcs_settings_url) assertr = response.assert_response() diff --git a/rhodecode/apps/repository/utils.py b/rhodecode/apps/repository/utils.py --- a/rhodecode/apps/repository/utils.py +++ b/rhodecode/apps/repository/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/__init__.py b/rhodecode/apps/repository/views/__init__.py --- a/rhodecode/apps/repository/views/__init__.py +++ b/rhodecode/apps/repository/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_artifacts.py b/rhodecode/apps/repository/views/repo_artifacts.py --- a/rhodecode/apps/repository/views/repo_artifacts.py +++ b/rhodecode/apps/repository/views/repo_artifacts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_audit_logs.py b/rhodecode/apps/repository/views/repo_audit_logs.py --- a/rhodecode/apps/repository/views/repo_audit_logs.py +++ b/rhodecode/apps/repository/views/repo_audit_logs.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_automation.py b/rhodecode/apps/repository/views/repo_automation.py --- a/rhodecode/apps/repository/views/repo_automation.py +++ b/rhodecode/apps/repository/views/repo_automation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_bookmarks.py b/rhodecode/apps/repository/views/repo_bookmarks.py --- a/rhodecode/apps/repository/views/repo_bookmarks.py +++ b/rhodecode/apps/repository/views/repo_bookmarks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_branch_permissions.py b/rhodecode/apps/repository/views/repo_branch_permissions.py --- a/rhodecode/apps/repository/views/repo_branch_permissions.py +++ b/rhodecode/apps/repository/views/repo_branch_permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_branches.py b/rhodecode/apps/repository/views/repo_branches.py --- a/rhodecode/apps/repository/views/repo_branches.py +++ b/rhodecode/apps/repository/views/repo_branches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_caches.py b/rhodecode/apps/repository/views/repo_caches.py --- a/rhodecode/apps/repository/views/repo_caches.py +++ b/rhodecode/apps/repository/views/repo_caches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_changelog.py b/rhodecode/apps/repository/views/repo_changelog.py --- a/rhodecode/apps/repository/views/repo_changelog.py +++ b/rhodecode/apps/repository/views/repo_changelog.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -33,7 +33,7 @@ from rhodecode.lib.auth import ( from rhodecode.lib.graphmod import _colored, _dagwalker from rhodecode.lib.helpers import RepoPage from rhodecode.lib.utils2 import str2bool -from rhodecode.lib.str_utils import safe_int, safe_str +from rhodecode.lib.str_utils import safe_int, safe_str, safe_bytes from rhodecode.lib.vcs.exceptions import ( RepositoryError, CommitDoesNotExistError, CommitError, NodeDoesNotExistError, EmptyRepositoryError) @@ -204,10 +204,9 @@ class RepoChangelogView(RepoAppView): log.debug('generating changelog for path %s', f_path) # get the history for the file ! base_commit = self.rhodecode_vcs_repo.get_commit(commit_id) - + bytes_path = safe_bytes(f_path) try: - collection = base_commit.get_path_history( - f_path, limit=hist_limit, pre_load=pre_load) + collection = base_commit.get_path_history(bytes_path, limit=hist_limit, pre_load=pre_load) if collection and partial_xhr: # for ajax call we remove first one since we're looking # at it right now in the context of a file commit @@ -216,7 +215,7 @@ class RepoChangelogView(RepoAppView): # this node is not present at tip! try: commit = self._get_commit_or_redirect(commit_id) - collection = commit.get_path_history(f_path) + collection = commit.get_path_history(bytes_path) except RepositoryError as e: h.flash(safe_str(e), category='warning') redirect_url = h.route_path( @@ -310,9 +309,8 @@ class RepoChangelogView(RepoAppView): log.exception(safe_str(e)) raise HTTPFound( h.route_path('repo_commits', repo_name=self.db_repo_name)) - - collection = base_commit.get_path_history( - f_path, limit=hist_limit, pre_load=pre_load) + bytes_path = safe_bytes(f_path) + collection = base_commit.get_path_history(bytes_path, limit=hist_limit, pre_load=pre_load) collection = list(reversed(collection)) else: collection = self.rhodecode_vcs_repo.get_commits( diff --git a/rhodecode/apps/repository/views/repo_checks.py b/rhodecode/apps/repository/views/repo_checks.py --- a/rhodecode/apps/repository/views/repo_checks.py +++ b/rhodecode/apps/repository/views/repo_checks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_commits.py b/rhodecode/apps/repository/views/repo_commits.py --- a/rhodecode/apps/repository/views/repo_commits.py +++ b/rhodecode/apps/repository/views/repo_commits.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -89,8 +89,7 @@ class RepoCommitsView(RepoAppView): commit_range = commit_id_range.split('...')[:2] try: - pre_load = ['affected_files', 'author', 'branch', 'date', - 'message', 'parents'] + pre_load = ['author', 'branch', 'date', 'message', 'parents'] if self.rhodecode_vcs_repo.alias == 'hg': pre_load += ['hidden', 'obsolete', 'phase'] @@ -100,8 +99,7 @@ class RepoCommitsView(RepoAppView): pre_load=pre_load, translate_tags=False) commits = list(commits) else: - commits = [self.rhodecode_vcs_repo.get_commit( - commit_id=commit_id_range, pre_load=pre_load)] + commits = [self.rhodecode_vcs_repo.get_commit(commit_id=commit_id_range, pre_load=pre_load)] c.commit_ranges = commits if not c.commit_ranges: diff --git a/rhodecode/apps/repository/views/repo_compare.py b/rhodecode/apps/repository/views/repo_compare.py --- a/rhodecode/apps/repository/views/repo_compare.py +++ b/rhodecode/apps/repository/views/repo_compare.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_feed.py b/rhodecode/apps/repository/views/repo_feed.py --- a/rhodecode/apps/repository/views/repo_feed.py +++ b/rhodecode/apps/repository/views/repo_feed.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_files.py b/rhodecode/apps/repository/views/repo_files.py --- a/rhodecode/apps/repository/views/repo_files.py +++ b/rhodecode/apps/repository/views/repo_files.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -40,25 +40,34 @@ from rhodecode.lib import diffs, helpers from rhodecode.lib import audit_logger from rhodecode.lib.hash_utils import sha1_safe from rhodecode.lib.archive_cache import ( - get_archival_cache_store, get_archival_config, ArchiveCacheGenerationLock, archive_iterator) + get_archival_cache_store, + get_archival_config, + ArchiveCacheGenerationLock, + archive_iterator, +) from rhodecode.lib.str_utils import safe_bytes, convert_special_chars from rhodecode.lib.view_utils import parse_path_ref from rhodecode.lib.exceptions import NonRelativePathError -from rhodecode.lib.codeblocks import ( - filenode_as_lines_tokens, filenode_as_annotated_lines_tokens) +from rhodecode.lib.codeblocks import filenode_as_lines_tokens, filenode_as_annotated_lines_tokens from rhodecode.lib.utils2 import convert_line_endings, detect_mode from rhodecode.lib.type_utils import str2bool from rhodecode.lib.str_utils import safe_str, safe_int, header_safe_str -from rhodecode.lib.auth import ( - LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) +from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired from rhodecode.lib.vcs import path as vcspath from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.conf import settings from rhodecode.lib.vcs.nodes import FileNode from rhodecode.lib.vcs.exceptions import ( - RepositoryError, CommitDoesNotExistError, EmptyRepositoryError, - ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, - NodeDoesNotExistError, CommitError, NodeError) + RepositoryError, + CommitDoesNotExistError, + EmptyRepositoryError, + ImproperArchiveTypeError, + VCSError, + NodeAlreadyExistsError, + NodeDoesNotExistError, + CommitError, + NodeError, +) from rhodecode.model.scm import ScmModel from rhodecode.model.db import Repository @@ -66,17 +75,17 @@ from rhodecode.model.db import Repositor log = logging.getLogger(__name__) -def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True): +def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha="", with_hash=True): # original backward compat name of archive - clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_')) + clean_name = safe_str(convert_special_chars(db_repo_name).replace("/", "_")) # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip id_sha = sha1_safe(str(db_repo_id))[:4] - sub_repo = 'sub-1' if subrepos else 'sub-0' - commit = commit_sha if with_hash else 'archive' - path_marker = (path_sha if with_hash else '') or 'all' - archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}' + sub_repo = "sub-1" if subrepos else "sub-0" + commit = commit_sha if with_hash else "archive" + path_marker = (path_sha if with_hash else "") or "all" + archive_name = f"{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}" return archive_name @@ -86,16 +95,15 @@ def get_path_sha(at_path): def _get_archive_spec(fname): - log.debug('Detecting archive spec for: `%s`', fname) + log.debug("Detecting archive spec for: `%s`", fname) fileformat = None ext = None content_type = None for a_type, content_type, extension in settings.ARCHIVE_SPECS: - if fname.endswith(extension): fileformat = a_type - log.debug('archive is of type: %s', fileformat) + log.debug("archive is of type: %s", fileformat) ext = extension break @@ -103,13 +111,12 @@ def _get_archive_spec(fname): raise ValueError() # left over part of whole fname is the commit - commit_id = fname[:-len(ext)] + commit_id = fname[: -len(ext)] return commit_id, ext, fileformat, content_type class RepoFilesView(RepoAppView): - @staticmethod def adjust_file_path_for_svn(f_path, repo): """ @@ -118,13 +125,11 @@ class RepoFilesView(RepoAppView): This is mainly based on prefix matching of the recognized tags and branches in the underlying repository. """ - tags_and_branches = itertools.chain( - repo.branches.keys(), - repo.tags.keys()) + tags_and_branches = itertools.chain(repo.branches.keys(), repo.tags.keys()) tags_and_branches = sorted(tags_and_branches, key=len, reverse=True) for name in tags_and_branches: - if f_path.startswith(f'{name}/'): + if f_path.startswith(f"{name}/"): f_path = vcspath.relpath(f_path, name) break return f_path @@ -135,66 +140,61 @@ class RepoFilesView(RepoAppView): c.enable_downloads = self.db_repo.enable_downloads return c - def _ensure_not_locked(self, commit_id='tip'): + def _ensure_not_locked(self, commit_id="tip"): _ = self.request.translate repo = self.db_repo if repo.enable_locking and repo.locked[0]: - h.flash(_('This repository has been locked by %s on %s') - % (h.person_by_id(repo.locked[0]), - h.format_date(h.time_to_datetime(repo.locked[1]))), - 'warning') - files_url = h.route_path( - 'repo_files:default_path', - repo_name=self.db_repo_name, commit_id=commit_id) + h.flash( + _("This repository has been locked by %s on %s") + % (h.person_by_id(repo.locked[0]), h.format_date(h.time_to_datetime(repo.locked[1]))), + "warning", + ) + files_url = h.route_path("repo_files:default_path", repo_name=self.db_repo_name, commit_id=commit_id) raise HTTPFound(files_url) - def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False): + def forbid_non_head(self, is_head, f_path, commit_id="tip", json_mode=False): _ = self.request.translate if not is_head: - message = _('Cannot modify file. ' - 'Given commit `{}` is not head of a branch.').format(commit_id) - h.flash(message, category='warning') + message = _("Cannot modify file. " "Given commit `{}` is not head of a branch.").format(commit_id) + h.flash(message, category="warning") if json_mode: return message - files_url = h.route_path( - 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id, - f_path=f_path) + files_url = h.route_path("repo_files", repo_name=self.db_repo_name, commit_id=commit_id, f_path=f_path) raise HTTPFound(files_url) - def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False): + def check_branch_permission(self, branch_name, commit_id="tip", json_mode=False): _ = self.request.translate - rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission( - self.db_repo_name, branch_name) - if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']: - message = _('Branch `{}` changes forbidden by rule {}.').format( - h.escape(branch_name), h.escape(rule)) - h.flash(message, 'warning') + rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(self.db_repo_name, branch_name) + if branch_perm and branch_perm not in ["branch.push", "branch.push_force"]: + message = _("Branch `{}` changes forbidden by rule {}.").format(h.escape(branch_name), h.escape(rule)) + h.flash(message, "warning") if json_mode: return message - files_url = h.route_path( - 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id) + files_url = h.route_path("repo_files:default_path", repo_name=self.db_repo_name, commit_id=commit_id) raise HTTPFound(files_url) def _get_commit_and_path(self): default_commit_id = self.db_repo.landing_ref_name - default_f_path = '/' + default_f_path = "/" - commit_id = self.request.matchdict.get( - 'commit_id', default_commit_id) + commit_id = self.request.matchdict.get("commit_id", default_commit_id) f_path = self._get_f_path(self.request.matchdict, default_f_path) - return commit_id, f_path + + bytes_path = safe_bytes(f_path) + return commit_id, f_path, bytes_path - def _get_default_encoding(self, c): - enc_list = getattr(c, 'default_encodings', []) - return enc_list[0] if enc_list else 'UTF-8' + @classmethod + def _get_default_encoding(cls, c): + enc_list = getattr(c, "default_encodings", []) + return enc_list[0] if enc_list else "UTF-8" def _get_commit_or_redirect(self, commit_id, redirect_after=True): """ @@ -213,31 +213,25 @@ class RepoFilesView(RepoAppView): return None add_new = upload_new = "" - if h.HasRepoPermissionAny( - 'repository.write', 'repository.admin')(self.db_repo_name): - _url = h.route_path( - 'repo_files_add_file', - repo_name=self.db_repo_name, commit_id=0, f_path='') - add_new = h.link_to( - _('add a new file'), _url, class_="alert-link") + if h.HasRepoPermissionAny("repository.write", "repository.admin")(self.db_repo_name): + _url = h.route_path("repo_files_add_file", repo_name=self.db_repo_name, commit_id=0, f_path="") + add_new = h.link_to(_("add a new file"), _url, class_="alert-link") - _url_upld = h.route_path( - 'repo_files_upload_file', - repo_name=self.db_repo_name, commit_id=0, f_path='') - upload_new = h.link_to( - _('upload a new file'), _url_upld, class_="alert-link") + _url_upld = h.route_path("repo_files_upload_file", repo_name=self.db_repo_name, commit_id=0, f_path="") + upload_new = h.link_to(_("upload a new file"), _url_upld, class_="alert-link") - h.flash(h.literal( - _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning') - raise HTTPFound( - h.route_path('repo_summary', repo_name=self.db_repo_name)) + h.flash( + h.literal(_("There are no files yet. Click here to %s or %s.") % (add_new, upload_new)), + category="warning", + ) + raise HTTPFound(h.route_path("repo_summary", repo_name=self.db_repo_name)) except (CommitDoesNotExistError, LookupError) as e: - msg = _('No such commit exists for this repository. Commit: {}').format(commit_id) - h.flash(msg, category='error') + msg = _("No such commit exists for this repository. Commit: {}").format(commit_id) + h.flash(msg, category="error") raise HTTPNotFound() except RepositoryError as e: - h.flash(h.escape(safe_str(e)), category='error') + h.flash(h.escape(safe_str(e)), category="error") raise HTTPNotFound() def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None): @@ -250,22 +244,22 @@ class RepoFilesView(RepoAppView): try: file_node = commit_obj.get_node(path, pre_load=pre_load) if file_node.is_dir(): - raise RepositoryError('The given path is a directory') + raise RepositoryError("The given path is a directory") except CommitDoesNotExistError: - log.exception('No such commit exists for this repository') - h.flash(_('No such commit exists for this repository'), category='error') + log.exception("No such commit exists for this repository") + h.flash(_("No such commit exists for this repository"), category="error") raise HTTPNotFound() except RepositoryError as e: - log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e) - h.flash(h.escape(safe_str(e)), category='error') + log.warning("Repository error while fetching filenode `%s`. Err:%s", path, e) + h.flash(h.escape(safe_str(e)), category="error") raise HTTPNotFound() return file_node def _is_valid_head(self, commit_id, repo, landing_ref): - branch_name = sha_commit_id = '' + branch_name = sha_commit_id = "" is_head = False - log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo) + log.debug("Checking if commit_id `%s` is a head for %s.", commit_id, repo) for _branch_name, branch_commit_id in repo.branches.items(): # simple case we pass in branch name, it's a HEAD @@ -301,39 +295,39 @@ class RepoFilesView(RepoAppView): return branch_name, sha_commit_id, is_head def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None): - repo_id = self.db_repo.repo_id force_recache = self.get_recache_flag() - cache_seconds = safe_int( - rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time')) + cache_seconds = rhodecode.ConfigGet().get_int("rc_cache.cache_repo.expiration_time") cache_on = not force_recache and cache_seconds > 0 + log.debug( - 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`' - 'with caching: %s[TTL: %ss]' % ( - repo_id, commit_id, f_path, cache_on, cache_seconds or 0)) + "Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`" + "with caching: %s[TTL: %ss]" % (repo_id, commit_id, f_path, cache_on, cache_seconds or 0) + ) - cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}' - region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid) + cache_namespace_uid = f"repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}" + region = rc_cache.get_or_create_region("cache_repo", cache_namespace_uid) @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on) def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev): - log.debug('Generating cached file tree at for repo_id: %s, %s, %s', - _repo_id, _commit_id, _f_path) + log.debug("Generating cached file tree at for repo_id: %s, %s, %s", _repo_id, _commit_id, _f_path) c.full_load = _full_load return render( - 'rhodecode:templates/files/files_browser_tree.mako', - self._get_template_context(c), self.request, _at_rev) + "rhodecode:templates/files/files_browser_tree.mako", + self._get_template_context(c), + self.request, + _at_rev, + ) return compute_file_tree( - self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev) + self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev + ) def create_pure_path(self, *parts): # Split paths and sanitize them, removing any ../ etc - sanitized_path = [ - x for x in pathlib.PurePath(*parts).parts - if x not in ['.', '..']] + sanitized_path = [x for x in pathlib.PurePath(*parts).parts if x not in [".", ".."]] pure_path = pathlib.PurePath(*sanitized_path) return pure_path @@ -341,10 +335,7 @@ class RepoFilesView(RepoAppView): def _is_lf_enabled(self, target_repo): lf_enabled = False - lf_key_for_vcs_map = { - 'hg': 'extensions_largefiles', - 'git': 'vcs_git_lfs_enabled' - } + lf_key_for_vcs_map = {"hg": "extensions_largefiles", "git": "vcs_git_lfs_enabled"} lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type) @@ -354,66 +345,77 @@ class RepoFilesView(RepoAppView): return lf_enabled @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_archivefile(self): # archive cache config from rhodecode import CONFIG + _ = self.request.translate self.load_default_context() - default_at_path = '/' - fname = self.request.matchdict['fname'] - subrepos = self.request.GET.get('subrepos') == 'true' - with_hash = str2bool(self.request.GET.get('with_hash', '1')) - at_path = self.request.GET.get('at_path') or default_at_path + + subrepos = self.request.GET.get("subrepos") == "true" + with_hash = str2bool(self.request.GET.get("with_hash", "1")) + + default_at_path = "/" + fname = self.request.matchdict["fname"] + at_path = self.request.GET.get("at_path") or default_at_path if not self.db_repo.enable_downloads: - return Response(_('Downloads disabled')) + return Response(_("Downloads disabled")) try: - commit_id, ext, fileformat, content_type = \ - _get_archive_spec(fname) + commit_id, ext, file_format, content_type = _get_archive_spec(fname) except ValueError: - return Response(_('Unknown archive type for: `{}`').format( - h.escape(fname))) + return Response(_("Unknown archive type for: `{}`").format(h.escape(fname))) try: commit = self.rhodecode_vcs_repo.get_commit(commit_id) except CommitDoesNotExistError: - return Response(_('Unknown commit_id {}').format( - h.escape(commit_id))) + return Response(_("Unknown commit_id {}").format(h.escape(commit_id))) except EmptyRepositoryError: - return Response(_('Empty repository')) + return Response(_("Empty repository")) # we used a ref, or a shorter version, lets redirect client ot use explicit hash if commit_id != commit.raw_id: - fname=f'{commit.raw_id}{ext}' + fname = f"{commit.raw_id}{ext}" raise HTTPFound(self.request.current_route_path(fname=fname)) try: - at_path = commit.get_node(at_path).path or default_at_path + at_path = commit.get_node(safe_bytes(at_path)).path or default_at_path except Exception: - return Response(_('No node at path {} for this repository').format(h.escape(at_path))) + return Response(_("No node at path {} for this repository").format(h.escape(at_path))) path_sha = get_path_sha(at_path) # used for cache etc, consistent unique archive name archive_name_key = get_archive_name( - self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos, - path_sha=path_sha, with_hash=True) + self.db_repo.repo_id, + self.db_repo_name, + commit_sha=commit.short_id, + ext=ext, + subrepos=subrepos, + path_sha=path_sha, + with_hash=True, + ) if not with_hash: - path_sha = '' + path_sha = "" # what end client gets served response_archive_name = get_archive_name( - self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos, - path_sha=path_sha, with_hash=with_hash) + self.db_repo.repo_id, + self.db_repo_name, + commit_sha=commit.short_id, + ext=ext, + subrepos=subrepos, + path_sha=path_sha, + with_hash=with_hash, + ) # remove extension from our archive directory name - archive_dir_name = response_archive_name[:-len(ext)] + archive_dir_name = response_archive_name[: -len(ext)] - archive_cache_disable = self.request.GET.get('no_cache') + archive_cache_disable = self.request.GET.get("no_cache") d_cache = get_archival_cache_store(config=CONFIG) @@ -421,29 +423,38 @@ class RepoFilesView(RepoAppView): d_cache_conf = get_archival_config(config=CONFIG) # This is also a cache key, and lock key - reentrant_lock_key = archive_name_key + '.lock' + reentrant_lock_key = archive_name_key + ".lock" use_cached_archive = False if not archive_cache_disable and archive_name_key in d_cache: reader, metadata = d_cache.fetch(archive_name_key) use_cached_archive = True - log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s', - archive_name_key, metadata, reader.name) + log.debug( + "Found cached archive as key=%s tag=%s, serving archive from cache reader=%s", + archive_name_key, + metadata, + reader.name, + ) else: reader = None - log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key) + log.debug("Archive with key=%s is not yet cached, creating one now...", archive_name_key) if not reader: # generate new archive, as previous was not found in the cache try: with d_cache.get_lock(reentrant_lock_key): try: - commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name, - kind=fileformat, subrepos=subrepos, - archive_at_path=at_path, cache_config=d_cache_conf) + commit.archive_repo( + archive_name_key, + archive_dir_name=archive_dir_name, + kind=file_format, + subrepos=subrepos, + archive_at_path=at_path, + cache_config=d_cache_conf, + ) except ImproperArchiveTypeError: - return _('Unknown archive type') + return _("Unknown archive type") except ArchiveCacheGenerationLock: retry_after = round(random.uniform(0.3, 3.0), 1) @@ -462,7 +473,7 @@ class RepoFilesView(RepoAppView): reader, metadata = d_cache.fetch(archive_name_key, retry=True, retry_attempts=30) response = Response(app_iter=archive_iterator(reader)) - response.content_disposition = f'attachment; filename={response_archive_name}' + response.content_disposition = f"attachment; filename={response_archive_name}" response.content_type = str(content_type) try: @@ -470,23 +481,25 @@ class RepoFilesView(RepoAppView): finally: # store download action audit_logger.store_web( - 'repo.archive.download', action_data={ - 'user_agent': self.request.user_agent, - 'archive_name': archive_name_key, - 'archive_spec': fname, - 'archive_cached': use_cached_archive}, + "repo.archive.download", + action_data={ + "user_agent": self.request.user_agent, + "archive_name": archive_name_key, + "archive_spec": fname, + "archive_cached": use_cached_archive, + }, user=self._rhodecode_user, repo=self.db_repo, - commit=True + commit=True, ) def _get_file_node(self, commit_id, f_path): - if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]: + if commit_id not in ["", None, "None", "0" * 12, "0" * 40]: commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id) try: - node = commit.get_node(f_path) + node = commit.get_node(safe_bytes(f_path)) if node.is_dir(): - raise NodeError(f'{node} path is a {type(node)} not a file') + raise NodeError(f"{node} path is a {type(node)} not a file") except NodeDoesNotExistError: commit = EmptyCommit( commit_id=commit_id, @@ -495,46 +508,43 @@ class RepoFilesView(RepoAppView): alias=commit.repository.alias, message=commit.message, author=commit.author, - date=commit.date) - node = FileNode(safe_bytes(f_path), b'', commit=commit) + date=commit.date, + ) + node = FileNode(safe_bytes(f_path), b"", commit=commit) else: - commit = EmptyCommit( - repo=self.rhodecode_vcs_repo, - alias=self.rhodecode_vcs_repo.alias) - node = FileNode(safe_bytes(f_path), b'', commit=commit) + commit = EmptyCommit(repo=self.rhodecode_vcs_repo, alias=self.rhodecode_vcs_repo.alias) + node = FileNode(safe_bytes(f_path), b"", commit=commit) return node @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_files_diff(self): c = self.load_default_context() f_path = self._get_f_path(self.request.matchdict) - diff1 = self.request.GET.get('diff1', '') - diff2 = self.request.GET.get('diff2', '') + diff1 = self.request.GET.get("diff1", "") + diff2 = self.request.GET.get("diff2", "") path1, diff1 = parse_path_ref(diff1, default_path=f_path) - ignore_whitespace = str2bool(self.request.GET.get('ignorews')) - line_context = self.request.GET.get('context', 3) + ignore_whitespace = str2bool(self.request.GET.get("ignorews")) + line_context = self.request.GET.get("context", 3) if not any((diff1, diff2)): - h.flash( - 'Need query parameter "diff1" or "diff2" to generate a diff.', - category='error') + h.flash('Need query parameter "diff1" or "diff2" to generate a diff.', category="error") raise HTTPBadRequest() - c.action = self.request.GET.get('diff') - if c.action not in ['download', 'raw']: + c.action = self.request.GET.get("diff") + if c.action not in ["download", "raw"]: compare_url = h.route_path( - 'repo_compare', + "repo_compare", repo_name=self.db_repo_name, - source_ref_type='rev', + source_ref_type="rev", source_ref=diff1, target_repo=self.db_repo_name, - target_ref_type='rev', + target_ref_type="rev", target_ref=diff2, - _query=dict(f_path=f_path)) + _query=dict(f_path=f_path), + ) # redirect to new view if we render diff raise HTTPFound(compare_url) @@ -543,43 +553,34 @@ class RepoFilesView(RepoAppView): node2 = self._get_file_node(diff2, f_path) except (RepositoryError, NodeError): log.exception("Exception while trying to get node from repository") - raise HTTPFound( - h.route_path('repo_files', repo_name=self.db_repo_name, - commit_id='tip', f_path=f_path)) + raise HTTPFound(h.route_path("repo_files", repo_name=self.db_repo_name, commit_id="tip", f_path=f_path)) - if all(isinstance(node.commit, EmptyCommit) - for node in (node1, node2)): + if all(isinstance(node.commit, EmptyCommit) for node in (node1, node2)): raise HTTPNotFound() c.commit_1 = node1.commit c.commit_2 = node2.commit - if c.action == 'download': - _diff = diffs.get_gitdiff(node1, node2, - ignore_whitespace=ignore_whitespace, - context=line_context) + if c.action == "download": + _diff = diffs.get_gitdiff(node1, node2, ignore_whitespace=ignore_whitespace, context=line_context) # NOTE: this was using diff_format='gitdiff' - diff = diffs.DiffProcessor(_diff, diff_format='newdiff') + diff = diffs.DiffProcessor(_diff, diff_format="newdiff") response = Response(self.path_filter.get_raw_patch(diff)) - response.content_type = 'text/plain' - response.content_disposition = ( - f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff' - ) + response.content_type = "text/plain" + response.content_disposition = f"attachment; filename={f_path}_{diff1}_vs_{diff2}.diff" charset = self._get_default_encoding(c) if charset: response.charset = charset return response - elif c.action == 'raw': - _diff = diffs.get_gitdiff(node1, node2, - ignore_whitespace=ignore_whitespace, - context=line_context) + elif c.action == "raw": + _diff = diffs.get_gitdiff(node1, node2, ignore_whitespace=ignore_whitespace, context=line_context) # NOTE: this was using diff_format='gitdiff' - diff = diffs.DiffProcessor(_diff, diff_format='newdiff') + diff = diffs.DiffProcessor(_diff, diff_format="newdiff") response = Response(self.path_filter.get_raw_patch(diff)) - response.content_type = 'text/plain' + response.content_type = "text/plain" charset = self._get_default_encoding(c) if charset: response.charset = charset @@ -589,31 +590,32 @@ class RepoFilesView(RepoAppView): raise HTTPNotFound() @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_files_diff_2way_redirect(self): """ Kept only to make OLD links work """ f_path = self._get_f_path_unchecked(self.request.matchdict) - diff1 = self.request.GET.get('diff1', '') - diff2 = self.request.GET.get('diff2', '') + diff1 = self.request.GET.get("diff1", "") + diff2 = self.request.GET.get("diff2", "") if not any((diff1, diff2)): - h.flash( - 'Need query parameter "diff1" or "diff2" to generate a diff.', - category='error') + h.flash('Need query parameter "diff1" or "diff2" to generate a diff.', category="error") raise HTTPBadRequest() compare_url = h.route_path( - 'repo_compare', + "repo_compare", repo_name=self.db_repo_name, - source_ref_type='rev', + source_ref_type="rev", source_ref=diff1, - target_ref_type='rev', + target_ref_type="rev", target_ref=diff2, - _query=dict(f_path=f_path, diffmode='sideside', - target_repo=self.db_repo_name,)) + _query=dict( + f_path=f_path, + diffmode="sideside", + target_repo=self.db_repo_name, + ), + ) raise HTTPFound(compare_url) @LoginRequired() @@ -627,52 +629,51 @@ class RepoFilesView(RepoAppView): landing_url = h.repo_files_by_ref_url( c.rhodecode_db_repo.repo_name, c.rhodecode_db_repo.repo_type, - f_path='', + f_path="", ref_name=ref_name, - commit_id='tip', - query=dict(at=ref_name) + commit_id="tip", + query=dict(at=ref_name), ) raise HTTPFound(landing_url) @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_files(self): c = self.load_default_context() - view_name = getattr(self.request.matched_route, 'name', None) + view_name = getattr(self.request.matched_route, "name", None) - c.annotate = view_name == 'repo_files:annotated' + c.annotate = view_name == "repo_files:annotated" # default is false, but .rst/.md files later are auto rendered, we can # overwrite auto rendering by setting this GET flag - c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False) + c.renderer = view_name == "repo_files:rendered" or not self.request.GET.get("no-render", False) - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() c.commit = self._get_commit_or_redirect(commit_id) - c.branch = self.request.GET.get('branch', None) + c.branch = self.request.GET.get("branch", None) c.f_path = f_path - at_rev = self.request.GET.get('at') + at_rev = self.request.GET.get("at") # files or dirs try: - c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data']) + c.file = c.commit.get_node(bytes_path, pre_load=["is_binary", "size", "data"]) c.file_author = True - c.file_tree = '' + c.file_tree = "" # prev link try: prev_commit = c.commit.prev(c.branch) c.prev_commit = prev_commit c.url_prev = h.route_path( - 'repo_files', repo_name=self.db_repo_name, - commit_id=prev_commit.raw_id, f_path=f_path) + "repo_files", repo_name=self.db_repo_name, commit_id=prev_commit.raw_id, f_path=f_path + ) if c.branch: - c.url_prev += '?branch=%s' % c.branch + c.url_prev += f"?branch={c.branch}" except (CommitDoesNotExistError, VCSError): - c.url_prev = '#' + c.url_prev = "#" c.prev_commit = EmptyCommit() # next link @@ -680,110 +681,101 @@ class RepoFilesView(RepoAppView): next_commit = c.commit.next(c.branch) c.next_commit = next_commit c.url_next = h.route_path( - 'repo_files', repo_name=self.db_repo_name, - commit_id=next_commit.raw_id, f_path=f_path) + "repo_files", repo_name=self.db_repo_name, commit_id=next_commit.raw_id, f_path=f_path + ) if c.branch: - c.url_next += '?branch=%s' % c.branch + c.url_next += f"?branch={c.branch}" except (CommitDoesNotExistError, VCSError): - c.url_next = '#' + c.url_next = "#" c.next_commit = EmptyCommit() # load file content if c.file.is_file(): - c.lf_node = {} has_lf_enabled = self._is_lf_enabled(self.db_repo) if has_lf_enabled: c.lf_node = c.file.get_largefile_node() - c.file_source_page = 'true' + c.file_source_page = "true" c.file_last_commit = c.file.last_commit c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file if not (c.file_size_too_big or c.file.is_binary): if c.annotate: # annotation has precedence over renderer - c.annotated_lines = filenode_as_annotated_lines_tokens( - c.file - ) + c.annotated_lines = filenode_as_annotated_lines_tokens(c.file) else: - c.renderer = ( - c.renderer and h.renderer_from_filename(c.file.path) - ) + c.renderer = c.renderer and h.renderer_from_filename(c.file.path) if not c.renderer: c.lines = filenode_as_lines_tokens(c.file) - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) c.on_branch_head = is_head - branch = c.commit.branch if ( - c.commit.branch and '/' not in c.commit.branch) else None + branch = c.commit.branch if (c.commit.branch and "/" not in c.commit.branch) else None c.branch_or_raw_id = branch or c.commit.raw_id c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id) author = c.file_last_commit.author - c.authors = [[ - h.email(author), - h.person(author, 'username_or_name_or_email'), - 1 - ]] + c.authors = [[h.email(author), h.person(author, "username_or_name_or_email"), 1]] - else: # load tree content at path - c.file_source_page = 'false' + else: # load tree content (dir content) at path + c.file_source_page = "false" c.authors = [] + + dir_node = c.file + c.file_nodes = dir_node.commit.get_nodes(dir_node.bytes_path, pre_load=dir_node.default_pre_load) # this loads a simple tree without metadata to speed things up # later via ajax we call repo_nodetree_full and fetch whole c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev) - c.readme_data, c.readme_file = \ - self._get_readme_data(self.db_repo, c.visual.default_renderer, - c.commit.raw_id, f_path) + c.readme_data, c.readme_file = self._get_readme_data( + self.db_repo, c.visual.default_renderer, c.commit.raw_id, bytes_path, nodes=c.file_nodes + ) except RepositoryError as e: - h.flash(h.escape(safe_str(e)), category='error') + h.flash(h.escape(safe_str(e)), category="error") raise HTTPNotFound() - if self.request.environ.get('HTTP_X_PJAX'): - html = render('rhodecode:templates/files/files_pjax.mako', - self._get_template_context(c), self.request) + if self.request.environ.get("HTTP_X_PJAX"): + html = render("rhodecode:templates/files/files_pjax.mako", self._get_template_context(c), self.request) else: - html = render('rhodecode:templates/files/files.mako', - self._get_template_context(c), self.request) + html = render("rhodecode:templates/files/files.mako", self._get_template_context(c), self.request) return Response(html) - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_files_annotated_previous(self): self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, bytes_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) prev_commit_id = commit.raw_id - line_anchor = self.request.GET.get('line_anchor') + line_anchor = self.request.GET.get("line_anchor") is_file = False try: - _file = commit.get_node(f_path) + _file = commit.get_node(bytes_path) is_file = _file.is_file() except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError): pass if is_file: - history = commit.get_path_history(f_path) - prev_commit_id = history[1].raw_id \ - if len(history) > 1 else prev_commit_id + history = commit.get_path_history(bytes_path) + prev_commit_id = history[1].raw_id if len(history) > 1 else prev_commit_id prev_url = h.route_path( - 'repo_files:annotated', repo_name=self.db_repo_name, - commit_id=prev_commit_id, f_path=f_path, - _anchor=f'L{line_anchor}') + "repo_files:annotated", + repo_name=self.db_repo_name, + commit_id=prev_commit_id, + f_path=bytes_path, + _anchor=f"L{line_anchor}", + ) raise HTTPFound(prev_url) @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_nodetree_full(self): """ Returns rendered html of file tree that contains commit date, @@ -792,22 +784,22 @@ class RepoFilesView(RepoAppView): """ c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) try: - dir_node = commit.get_node(f_path) + dir_node = commit.get_node(bytes_path) except RepositoryError as e: - return Response(f'error: {h.escape(safe_str(e))}') + return Response(f"error: {h.escape(safe_str(e))}") if dir_node.is_file(): - return Response('') + return Response("") c.file = dir_node + c.file_nodes = dir_node.commit.get_nodes(dir_node.bytes_path, pre_load=dir_node.default_pre_load) c.commit = commit - at_rev = self.request.GET.get('at') + at_rev = self.request.GET.get("at") - html = self._get_tree_at_commit( - c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev) + html = self._get_tree_at_commit(c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev) return Response(html) @@ -816,15 +808,12 @@ class RepoFilesView(RepoAppView): safe_path = f_name.replace('"', '\\"') encoded_path = urllib.parse.quote(f_name) - headers = "attachment; " \ - "filename=\"{}\"; " \ - "filename*=UTF-8\'\'{}".format(safe_path, encoded_path) + headers = f"attachment; " f'filename="{safe_path}"; ' f"filename*=UTF-8''{encoded_path}" return header_safe_str(headers) @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_file_raw(self): """ Action for show as raw, some mimetypes are "rendered", @@ -832,25 +821,24 @@ class RepoFilesView(RepoAppView): """ c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) - file_node = self._get_filenode_or_redirect(commit, f_path) + file_node = self._get_filenode_or_redirect(commit, bytes_path) raw_mimetype_mapping = { # map original mimetype to a mimetype used for "show as raw" # you can also provide a content-disposition to override the # default "attachment" disposition. # orig_type: (new_type, new_dispo) - # show images inline: # Do not re-add SVG: it is unsafe and permits XSS attacks. One can # for example render an SVG with javascript inside or even render # HTML. - 'image/x-icon': ('image/x-icon', 'inline'), - 'image/png': ('image/png', 'inline'), - 'image/gif': ('image/gif', 'inline'), - 'image/jpeg': ('image/jpeg', 'inline'), - 'application/pdf': ('application/pdf', 'inline'), + "image/x-icon": ("image/x-icon", "inline"), + "image/png": ("image/png", "inline"), + "image/gif": ("image/gif", "inline"), + "image/jpeg": ("image/jpeg", "inline"), + "application/pdf": ("application/pdf", "inline"), } mimetype = file_node.mimetype @@ -860,7 +848,7 @@ class RepoFilesView(RepoAppView): # we don't know anything special about this, handle it safely if file_node.is_binary: # do same as download raw for binary files - mimetype, disposition = 'application/octet-stream', 'attachment' + mimetype, disposition = "application/octet-stream", "attachment" else: # do not just use the original mimetype, but force text/plain, # otherwise it would serve text/html and that might be unsafe. @@ -869,9 +857,9 @@ class RepoFilesView(RepoAppView): # binary.This might lead to erroneous text display in some # cases, but helps in other cases, like with text files # without extension. - mimetype, disposition = 'text/plain', 'inline' + mimetype, disposition = "text/plain", "inline" - if disposition == 'attachment': + if disposition == "attachment": disposition = self._get_attachement_headers(f_path) stream_content = file_node.stream_bytes() @@ -887,16 +875,15 @@ class RepoFilesView(RepoAppView): return response @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_file_download(self): c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) - file_node = self._get_filenode_or_redirect(commit, f_path) + file_node = self._get_filenode_or_redirect(commit, bytes_path) - if self.request.GET.get('lf'): + if self.request.GET.get("lf"): # only if lf get flag is passed, we download this file # as LFS/Largefile lf_node = file_node.get_largefile_node() @@ -919,49 +906,41 @@ class RepoFilesView(RepoAppView): return response def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path): - - cache_seconds = safe_int( - rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time')) + cache_seconds = rhodecode.ConfigGet().get_int("rc_cache.cache_repo.expiration_time") cache_on = cache_seconds > 0 log.debug( - 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`' - 'with caching: %s[TTL: %ss]' % ( - repo_id, commit_id, f_path, cache_on, cache_seconds or 0)) + "Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`" + "with caching: %s[TTL: %ss]" % (repo_id, commit_id, f_path, cache_on, cache_seconds or 0) + ) - cache_namespace_uid = f'repo.{repo_id}' - region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid) + cache_namespace_uid = f"repo.{repo_id}" + region = rc_cache.get_or_create_region("cache_repo", cache_namespace_uid) @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on) def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path): - log.debug('Generating cached nodelist for repo_id:%s, %s, %s', - _repo_id, commit_id, f_path) + log.debug("Generating cached nodelist for repo_id:%s, %s, %s", _repo_id, commit_id, f_path) try: _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path) except (RepositoryError, CommitDoesNotExistError, Exception) as e: log.exception(safe_str(e)) - h.flash(h.escape(safe_str(e)), category='error') - raise HTTPFound(h.route_path( - 'repo_files', repo_name=self.db_repo_name, - commit_id='tip', f_path='/')) + h.flash(h.escape(safe_str(e)), category="error") + raise HTTPFound(h.route_path("repo_files", repo_name=self.db_repo_name, commit_id="tip", f_path="/")) return _d + _f - result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id, - commit_id, f_path) - return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result) + result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path) + return filter(lambda n: self.path_filter.path_access_allowed(n["name"]), result) @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_nodelist(self): self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) - metadata = self._get_nodelist_at_commit( - self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path) - return {'nodes': [x for x in metadata]} + metadata = self._get_nodelist_at_commit(self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path) + return {"nodes": [x for x in metadata]} def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type): items = [] @@ -978,7 +957,7 @@ class RepoFilesView(RepoAppView): # NOTE(dan): old code we used in "diff" mode compare new_f_path = vcspath.join(name, f_path) - return f'{new_f_path}@{commit_id}' + return f"{new_f_path}@{commit_id}" def _get_node_history(self, commit_obj, f_path, commits=None): """ @@ -996,37 +975,34 @@ class RepoFilesView(RepoAppView): if commits is None: pre_load = ["author", "branch"] try: - commits = tip.get_path_history(f_path, pre_load=pre_load) + commits = tip.get_path_history(safe_bytes(f_path), pre_load=pre_load) except (NodeDoesNotExistError, CommitError): # this node is not present at tip! - commits = commit_obj.get_path_history(f_path, pre_load=pre_load) + commits = commit_obj.get_path_history(safe_bytes(f_path), pre_load=pre_load) history = [] commits_group = ([], _("Changesets")) for commit in commits: - branch = ' (%s)' % commit.branch if commit.branch else '' - n_desc = f'r{commit.idx}:{commit.short_id}{branch}' - commits_group[0].append((commit.raw_id, n_desc, 'sha')) + branch = " (%s)" % commit.branch if commit.branch else "" + n_desc = f"r{commit.idx}:{commit.short_id}{branch}" + commits_group[0].append((commit.raw_id, n_desc, "sha")) history.append(commits_group) symbolic_reference = self._symbolic_reference - if self.rhodecode_vcs_repo.alias == 'svn': - adjusted_f_path = RepoFilesView.adjust_file_path_for_svn( - f_path, self.rhodecode_vcs_repo) + if self.rhodecode_vcs_repo.alias == "svn": + adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(f_path, self.rhodecode_vcs_repo) if adjusted_f_path != f_path: log.debug( - 'Recognized svn tag or branch in file "%s", using svn ' - 'specific symbolic references', f_path) + 'Recognized svn tag or branch in file "%s", using svn ' "specific symbolic references", f_path + ) f_path = adjusted_f_path symbolic_reference = self._symbolic_reference_svn - branches = self._create_references( - self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch') + branches = self._create_references(self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, "branch") branches_group = (branches, _("Branches")) - tags = self._create_references( - self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag') + tags = self._create_references(self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, "tag") tags_group = (tags, _("Tags")) history.append(branches_group) @@ -1035,14 +1011,13 @@ class RepoFilesView(RepoAppView): return history, commits @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_file_history(self): self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) - file_node = self._get_filenode_or_redirect(commit, f_path) + file_node = self._get_filenode_or_redirect(commit, bytes_path) if file_node.is_file(): file_history, _hist = self._get_node_history(commit, f_path) @@ -1051,52 +1026,38 @@ class RepoFilesView(RepoAppView): for section_items, section in file_history: items = [] for obj_id, obj_text, obj_type in section_items: - at_rev = '' - if obj_type in ['branch', 'bookmark', 'tag']: + at_rev = "" + if obj_type in ["branch", "bookmark", "tag"]: at_rev = obj_text - entry = { - 'id': obj_id, - 'text': obj_text, - 'type': obj_type, - 'at_rev': at_rev - } + entry = {"id": obj_id, "text": obj_text, "type": obj_type, "at_rev": at_rev} items.append(entry) - res.append({ - 'text': section, - 'children': items - }) + res.append({"text": section, "children": items}) - data = { - 'more': False, - 'results': res - } + data = {"more": False, "results": res} return data - log.warning('Cannot fetch history for directory') + log.warning("Cannot fetch history for directory") raise HTTPBadRequest() @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin") def repo_file_authors(self): c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() commit = self._get_commit_or_redirect(commit_id) - file_node = self._get_filenode_or_redirect(commit, f_path) + file_node = self._get_filenode_or_redirect(commit, bytes_path) if not file_node.is_file(): raise HTTPBadRequest() c.file_last_commit = file_node.last_commit - if self.request.GET.get('annotate') == '1': + if self.request.GET.get("annotate") == "1": # use _hist from annotation if annotation mode is on commit_ids = {x[1] for x in file_node.annotate} - _hist = ( - self.rhodecode_vcs_repo.get_commit(commit_id) - for commit_id in commit_ids) + _hist = (self.rhodecode_vcs_repo.get_commit(commit_id) for commit_id in commit_ids) else: _f_history, _hist = self._get_node_history(commit, f_path) c.file_author = False @@ -1107,8 +1068,8 @@ class RepoFilesView(RepoAppView): if author not in unique: unique[commit.author] = [ h.email(author), - h.person(author, 'username_or_name_or_email'), - 1 # counter + h.person(author, "username_or_name_or_email"), + 1, # counter ] else: @@ -1120,204 +1081,186 @@ class RepoFilesView(RepoAppView): return self._get_template_context(c) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") def repo_files_check_head(self): self.load_default_context() - commit_id, f_path = self._get_commit_and_path() - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + commit_id, f_path, bytes_path = self._get_commit_and_path() + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) - new_path = self.request.POST.get('path') - operation = self.request.POST.get('operation') - path_exist = '' + new_path = self.request.POST.get("path") + operation = self.request.POST.get("operation") + path_exist = "" - if new_path and operation in ['create', 'upload']: - new_f_path = os.path.join(f_path.lstrip('/'), new_path) + if new_path and operation in ["create", "upload"]: + new_f_path = os.path.join(f_path.lstrip("/"), new_path) try: commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id) # NOTE(dan): construct whole path without leading / - file_node = commit_obj.get_node(new_f_path) - if file_node is not None: + file_node = commit_obj.get_node(safe_bytes(new_f_path)) + if file_node: path_exist = new_f_path - except EmptyRepositoryError: - pass - except Exception: + except (EmptyRepositoryError, NodeDoesNotExistError): pass - return { - 'branch': _branch_name, - 'sha': _sha_commit_id, - 'is_head': is_head, - 'path_exists': path_exist - } + return {"branch": _branch_name, "sha": _sha_commit_id, "is_head": is_head, "path_exists": path_exist} @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") def repo_files_remove_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path) self.check_branch_permission(_branch_name) c.commit = self._get_commit_or_redirect(commit_id) - c.file = self._get_filenode_or_redirect(c.commit, f_path) + c.file = self._get_filenode_or_redirect(c.commit, bytes_path) - c.default_message = _( - 'Deleted file {} via RhodeCode Enterprise').format(f_path) + c.default_message = _("Deleted file {} via RhodeCode Enterprise").format(f_path) c.f_path = f_path return self._get_template_context(c) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") @CSRFRequired() def repo_files_delete_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path) self.check_branch_permission(_branch_name) c.commit = self._get_commit_or_redirect(commit_id) - c.file = self._get_filenode_or_redirect(c.commit, f_path) + c.file = self._get_filenode_or_redirect(c.commit, bytes_path) - c.default_message = _( - 'Deleted file {} via RhodeCode Enterprise').format(f_path) + c.default_message = _("Deleted file {} via RhodeCode Enterprise").format(f_path) c.f_path = f_path node_path = f_path author = self._rhodecode_db_user.full_contact - message = self.request.POST.get('message') or c.default_message + message = self.request.POST.get("message") or c.default_message try: - nodes = { - safe_bytes(node_path): { - 'content': b'' - } - } + nodes = {safe_bytes(node_path): {"content": b""}} ScmModel().delete_nodes( - user=self._rhodecode_db_user.user_id, repo=self.db_repo, + user=self._rhodecode_db_user.user_id, + repo=self.db_repo, message=message, nodes=nodes, parent_commit=c.commit, author=author, ) - h.flash( - _('Successfully deleted file `{}`').format( - h.escape(f_path)), category='success') + h.flash(_("Successfully deleted file `{}`").format(h.escape(f_path)), category="success") except Exception: - log.exception('Error during commit operation') - h.flash(_('Error occurred during commit'), category='error') - raise HTTPFound( - h.route_path('repo_commit', repo_name=self.db_repo_name, - commit_id='tip')) + log.exception("Error during commit operation") + h.flash(_("Error occurred during commit"), category="error") + raise HTTPFound(h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id="tip")) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") def repo_files_edit_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path, commit_id=commit_id) self.check_branch_permission(_branch_name, commit_id=commit_id) c.commit = self._get_commit_or_redirect(commit_id) - c.file = self._get_filenode_or_redirect(c.commit, f_path) + c.file = self._get_filenode_or_redirect(c.commit, bytes_path) if c.file.is_binary: files_url = h.route_path( - 'repo_files', - repo_name=self.db_repo_name, - commit_id=c.commit.raw_id, f_path=f_path) + "repo_files", repo_name=self.db_repo_name, commit_id=c.commit.raw_id, f_path=f_path + ) raise HTTPFound(files_url) - c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path) + c.default_message = _("Edited file {} via RhodeCode Enterprise").format(f_path) c.f_path = f_path return self._get_template_context(c) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") @CSRFRequired() def repo_files_update_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() c.commit = self._get_commit_or_redirect(commit_id) - c.file = self._get_filenode_or_redirect(c.commit, f_path) + c.file = self._get_filenode_or_redirect(c.commit, bytes_path) if c.file.is_binary: - raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name, - commit_id=c.commit.raw_id, f_path=f_path)) + raise HTTPFound( + h.route_path("repo_files", repo_name=self.db_repo_name, commit_id=c.commit.raw_id, f_path=f_path) + ) - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path, commit_id=commit_id) self.check_branch_permission(_branch_name, commit_id=commit_id) - c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path) + c.default_message = _("Edited file {} via RhodeCode Enterprise").format(f_path) c.f_path = f_path old_content = c.file.str_content sl = old_content.splitlines(1) - first_line = sl[0] if sl else '' + first_line = sl[0] if sl else "" r_post = self.request.POST # line endings: 0 - Unix, 1 - Mac, 2 - DOS line_ending_mode = detect_mode(first_line, 0) - content = convert_line_endings(r_post.get('content', ''), line_ending_mode) + content = convert_line_endings(r_post.get("content", ""), line_ending_mode) - message = r_post.get('message') or c.default_message + message = r_post.get("message") or c.default_message org_node_path = c.file.str_path - filename = r_post['filename'] + filename = r_post["filename"] root_path = c.file.dir_path pure_path = self.create_pure_path(root_path, filename) node_path = pure_path.as_posix() - default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name, - commit_id=commit_id) + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit_id) if content == old_content and node_path == org_node_path: - h.flash(_('No changes detected on {}').format(h.escape(org_node_path)), - category='warning') + h.flash(_("No changes detected on {}").format(h.escape(org_node_path)), category="warning") raise HTTPFound(default_redirect_url) try: mapping = { c.file.bytes_path: { - 'org_filename': org_node_path, - 'filename': safe_bytes(node_path), - 'content': safe_bytes(content), - 'lexer': '', - 'op': 'mod', - 'mode': c.file.mode + "org_filename": org_node_path, + "filename": safe_bytes(node_path), + "content": safe_bytes(content), + "lexer": "", + "op": "mod", + "mode": c.file.mode, } } @@ -1329,28 +1272,26 @@ class RepoFilesView(RepoAppView): parent_commit=c.commit, ) - h.flash(_('Successfully committed changes to file `{}`').format( - h.escape(f_path)), category='success') - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + h.flash(_("Successfully committed changes to file `{}`").format(h.escape(f_path)), category="success") + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit.raw_id) except Exception: - log.exception('Error occurred during commit') - h.flash(_('Error occurred during commit'), category='error') + log.exception("Error occurred during commit") + h.flash(_("Error occurred during commit"), category="error") raise HTTPFound(default_redirect_url) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") def repo_files_add_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() # Check if we need to use this page to upload binary - upload_binary = str2bool(self.request.params.get('upload_binary', False)) + upload_binary = str2bool(self.request.params.get("upload_binary", False)) c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False) if c.commit is None: @@ -1359,29 +1300,32 @@ class RepoFilesView(RepoAppView): if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead - _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True + _branch_name, _sha_commit_id, is_head = c.commit.branch, "", True else: - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path, commit_id=commit_id) self.check_branch_permission(_branch_name, commit_id=commit_id) - c.default_message = (_('Added file via RhodeCode Enterprise')) \ - if not upload_binary else (_('Edited file {} via RhodeCode Enterprise').format(f_path)) - c.f_path = f_path.lstrip('/') # ensure not relative path + c.default_message = ( + (_("Added file via RhodeCode Enterprise")) + if not upload_binary + else (_("Edited file {} via RhodeCode Enterprise").format(f_path)) + ) + c.f_path = f_path.lstrip("/") # ensure not relative path c.replace_binary = upload_binary return self._get_template_context(c) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") @CSRFRequired() def repo_files_create_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() @@ -1391,56 +1335,48 @@ class RepoFilesView(RepoAppView): # calculate redirect URL if self.rhodecode_vcs_repo.is_empty(): - default_redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) + default_redirect_url = h.route_path("repo_summary", repo_name=self.db_repo_name) else: - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id="tip") if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead - _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True + _branch_name, _sha_commit_id, is_head = c.commit.branch, "", True else: - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) self.forbid_non_head(is_head, f_path, commit_id=commit_id) self.check_branch_permission(_branch_name, commit_id=commit_id) - c.default_message = (_('Added file via RhodeCode Enterprise')) + c.default_message = _("Added file via RhodeCode Enterprise") c.f_path = f_path r_post = self.request.POST - message = r_post.get('message') or c.default_message - filename = r_post.get('filename') + message = r_post.get("message") or c.default_message + filename = r_post.get("filename") unix_mode = 0 if not filename: # If there's no commit, redirect to repo summary if type(c.commit) is EmptyCommit: - redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) + redirect_url = h.route_path("repo_summary", repo_name=self.db_repo_name) else: redirect_url = default_redirect_url - h.flash(_('No filename specified'), category='warning') + h.flash(_("No filename specified"), category="warning") raise HTTPFound(redirect_url) root_path = f_path pure_path = self.create_pure_path(root_path, filename) - node_path = pure_path.as_posix().lstrip('/') + node_path = pure_path.as_posix().lstrip("/") author = self._rhodecode_db_user.full_contact - content = convert_line_endings(r_post.get('content', ''), unix_mode) - nodes = { - safe_bytes(node_path): { - 'content': safe_bytes(content) - } - } + content = convert_line_endings(r_post.get("content", ""), unix_mode) + nodes = {safe_bytes(node_path): {"content": safe_bytes(content)}} try: - commit = ScmModel().create_nodes( user=self._rhodecode_db_user.user_id, repo=self.db_repo, @@ -1450,32 +1386,32 @@ class RepoFilesView(RepoAppView): author=author, ) - h.flash(_('Successfully committed new file `{}`').format( - h.escape(node_path)), category='success') + h.flash(_("Successfully committed new file `{}`").format(h.escape(node_path)), category="success") - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit.raw_id) except NonRelativePathError: - log.exception('Non Relative path found') - h.flash(_('The location specified must be a relative path and must not ' - 'contain .. in the path'), category='warning') + log.exception("Non Relative path found") + h.flash( + _("The location specified must be a relative path and must not " "contain .. in the path"), + category="warning", + ) raise HTTPFound(default_redirect_url) except (NodeError, NodeAlreadyExistsError) as e: - h.flash(h.escape(safe_str(e)), category='error') + h.flash(h.escape(safe_str(e)), category="error") except Exception: - log.exception('Error occurred during commit') - h.flash(_('Error occurred during commit'), category='error') + log.exception("Error occurred during commit") + h.flash(_("Error occurred during commit"), category="error") raise HTTPFound(default_redirect_url) @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") @CSRFRequired() def repo_files_upload_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() @@ -1485,65 +1421,52 @@ class RepoFilesView(RepoAppView): # calculate redirect URL if self.rhodecode_vcs_repo.is_empty(): - default_redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) + default_redirect_url = h.route_path("repo_summary", repo_name=self.db_repo_name) else: - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id="tip") if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead - _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True + _branch_name, _sha_commit_id, is_head = c.commit.branch, "", True else: - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) error = self.forbid_non_head(is_head, f_path, json_mode=True) if error: - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} error = self.check_branch_permission(_branch_name, json_mode=True) if error: - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} - c.default_message = (_('Added file via RhodeCode Enterprise')) + c.default_message = _("Added file via RhodeCode Enterprise") c.f_path = f_path r_post = self.request.POST message = c.default_message - user_message = r_post.getall('message') + user_message = r_post.getall("message") if isinstance(user_message, list) and user_message: # we take the first from duplicated results if it's not empty message = user_message[0] if user_message[0] else message nodes = {} - for file_obj in r_post.getall('files_upload') or []: + for file_obj in r_post.getall("files_upload") or []: content = file_obj.file filename = file_obj.filename root_path = f_path pure_path = self.create_pure_path(root_path, filename) - node_path = pure_path.as_posix().lstrip('/') + node_path = pure_path.as_posix().lstrip("/") - nodes[safe_bytes(node_path)] = { - 'content': content - } + nodes[safe_bytes(node_path)] = {"content": content} if not nodes: - error = 'missing files' - return { - 'error': error, - 'redirect_url': default_redirect_url - } + error = "missing files" + return {"error": error, "redirect_url": default_redirect_url} author = self._rhodecode_db_user.full_contact @@ -1557,54 +1480,40 @@ class RepoFilesView(RepoAppView): author=author, ) if len(nodes) == 1: - flash_message = _('Successfully committed {} new files').format(len(nodes)) + flash_message = _("Successfully committed {} new files").format(len(nodes)) else: - flash_message = _('Successfully committed 1 new file') + flash_message = _("Successfully committed 1 new file") - h.flash(flash_message, category='success') + h.flash(flash_message, category="success") - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit.raw_id) except NonRelativePathError: - log.exception('Non Relative path found') - error = _('The location specified must be a relative path and must not ' - 'contain .. in the path') - h.flash(error, category='warning') + log.exception("Non Relative path found") + error = _("The location specified must be a relative path and must not " "contain .. in the path") + h.flash(error, category="warning") - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} except (NodeError, NodeAlreadyExistsError) as e: error = h.escape(e) - h.flash(error, category='error') + h.flash(error, category="error") - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} except Exception: - log.exception('Error occurred during commit') - error = _('Error occurred during commit') - h.flash(error, category='error') - return { - 'error': error, - 'redirect_url': default_redirect_url - } + log.exception("Error occurred during commit") + error = _("Error occurred during commit") + h.flash(error, category="error") + return {"error": error, "redirect_url": default_redirect_url} - return { - 'error': None, - 'redirect_url': default_redirect_url - } + return {"error": None, "redirect_url": default_redirect_url} @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @HasRepoPermissionAnyDecorator("repository.write", "repository.admin") @CSRFRequired() def repo_files_replace_file(self): _ = self.request.translate c = self.load_default_context() - commit_id, f_path = self._get_commit_and_path() + commit_id, f_path, bytes_path = self._get_commit_and_path() self._ensure_not_locked() @@ -1613,64 +1522,50 @@ class RepoFilesView(RepoAppView): c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) if self.rhodecode_vcs_repo.is_empty(): - default_redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) + default_redirect_url = h.route_path("repo_summary", repo_name=self.db_repo_name) else: - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id="tip") if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead - _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True + _branch_name, _sha_commit_id, is_head = c.commit.branch, "", True else: - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo, - landing_ref=self.db_repo.landing_ref_name) + _branch_name, _sha_commit_id, is_head = self._is_valid_head( + commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name + ) error = self.forbid_non_head(is_head, f_path, json_mode=True) if error: - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} error = self.check_branch_permission(_branch_name, json_mode=True) if error: - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} - c.default_message = (_('Edited file {} via RhodeCode Enterprise').format(f_path)) + c.default_message = _("Edited file {} via RhodeCode Enterprise").format(f_path) c.f_path = f_path r_post = self.request.POST message = c.default_message - user_message = r_post.getall('message') + user_message = r_post.getall("message") if isinstance(user_message, list) and user_message: # we take the first from duplicated results if it's not empty message = user_message[0] if user_message[0] else message - data_for_replacement = r_post.getall('files_upload') or [] + data_for_replacement = r_post.getall("files_upload") or [] if (objects_count := len(data_for_replacement)) > 1: - return { - 'error': 'too many files for replacement', - 'redirect_url': default_redirect_url - } + return {"error": "too many files for replacement", "redirect_url": default_redirect_url} elif not objects_count: - return { - 'error': 'missing files', - 'redirect_url': default_redirect_url - } + return {"error": "missing files", "redirect_url": default_redirect_url} content = data_for_replacement[0].file retrieved_filename = data_for_replacement[0].filename - if retrieved_filename.split('.')[-1] != f_path.split('.')[-1]: + if retrieved_filename.split(".")[-1] != f_path.split(".")[-1]: return { - 'error': 'file extension of uploaded file doesn\'t match an original file\'s extension', - 'redirect_url': default_redirect_url + "error": "file extension of uploaded file doesn't match an original file's extension", + "redirect_url": default_redirect_url, } author = self._rhodecode_db_user.full_contact @@ -1681,36 +1576,26 @@ class RepoFilesView(RepoAppView): repo=self.db_repo, message=message, node={ - 'content': content, - 'file_path': f_path.encode(), + "content": content, + "file_path": f_path.encode(), }, parent_commit=c.commit, author=author, ) - h.flash(_('Successfully committed 1 new file'), category='success') + h.flash(_("Successfully committed 1 new file"), category="success") - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + default_redirect_url = h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit.raw_id) except (NodeError, NodeAlreadyExistsError) as e: error = h.escape(e) - h.flash(error, category='error') + h.flash(error, category="error") - return { - 'error': error, - 'redirect_url': default_redirect_url - } + return {"error": error, "redirect_url": default_redirect_url} except Exception: - log.exception('Error occurred during commit') - error = _('Error occurred during commit') - h.flash(error, category='error') - return { - 'error': error, - 'redirect_url': default_redirect_url - } + log.exception("Error occurred during commit") + error = _("Error occurred during commit") + h.flash(error, category="error") + return {"error": error, "redirect_url": default_redirect_url} - return { - 'error': None, - 'redirect_url': default_redirect_url - } + return {"error": None, "redirect_url": default_redirect_url} diff --git a/rhodecode/apps/repository/views/repo_forks.py b/rhodecode/apps/repository/views/repo_forks.py --- a/rhodecode/apps/repository/views/repo_forks.py +++ b/rhodecode/apps/repository/views/repo_forks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_maintainance.py b/rhodecode/apps/repository/views/repo_maintainance.py --- a/rhodecode/apps/repository/views/repo_maintainance.py +++ b/rhodecode/apps/repository/views/repo_maintainance.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_permissions.py b/rhodecode/apps/repository/views/repo_permissions.py --- a/rhodecode/apps/repository/views/repo_permissions.py +++ b/rhodecode/apps/repository/views/repo_permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py --- a/rhodecode/apps/repository/views/repo_pull_requests.py +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -487,7 +487,9 @@ class RepoPullRequestsView(RepoAppView, c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json) # observers + observer_ids = [] for observer_obj, member in pull_request_at_ver.observers(): + observer_ids.append(observer_obj.user_id) member_observer = h.reviewer_as_json( member, reasons=[], mandatory=False, role=observer_obj.role, @@ -497,6 +499,7 @@ class RepoPullRequestsView(RepoAppView, c.pull_request_set_observers_data_json['observers'].append(member_observer) c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json) + c.status_change_disabled = self._rhodecode_user.user_id in observer_ids general_comments, inline_comments = \ self.register_comments_vars(c, pull_request_latest, versions) diff --git a/rhodecode/apps/repository/views/repo_review_rules.py b/rhodecode/apps/repository/views/repo_review_rules.py --- a/rhodecode/apps/repository/views/repo_review_rules.py +++ b/rhodecode/apps/repository/views/repo_review_rules.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings.py b/rhodecode/apps/repository/views/repo_settings.py --- a/rhodecode/apps/repository/views/repo_settings.py +++ b/rhodecode/apps/repository/views/repo_settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py --- a/rhodecode/apps/repository/views/repo_settings_advanced.py +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings_fields.py b/rhodecode/apps/repository/views/repo_settings_fields.py --- a/rhodecode/apps/repository/views/repo_settings_fields.py +++ b/rhodecode/apps/repository/views/repo_settings_fields.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings_issue_trackers.py b/rhodecode/apps/repository/views/repo_settings_issue_trackers.py --- a/rhodecode/apps/repository/views/repo_settings_issue_trackers.py +++ b/rhodecode/apps/repository/views/repo_settings_issue_trackers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings_remote.py b/rhodecode/apps/repository/views/repo_settings_remote.py --- a/rhodecode/apps/repository/views/repo_settings_remote.py +++ b/rhodecode/apps/repository/views/repo_settings_remote.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_settings_vcs.py b/rhodecode/apps/repository/views/repo_settings_vcs.py --- a/rhodecode/apps/repository/views/repo_settings_vcs.py +++ b/rhodecode/apps/repository/views/repo_settings_vcs.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_strip.py b/rhodecode/apps/repository/views/repo_strip.py --- a/rhodecode/apps/repository/views/repo_strip.py +++ b/rhodecode/apps/repository/views/repo_strip.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_summary.py b/rhodecode/apps/repository/views/repo_summary.py --- a/rhodecode/apps/repository/views/repo_summary.py +++ b/rhodecode/apps/repository/views/repo_summary.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/repository/views/repo_tags.py b/rhodecode/apps/repository/views/repo_tags.py --- a/rhodecode/apps/repository/views/repo_tags.py +++ b/rhodecode/apps/repository/views/repo_tags.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/search/__init__.py b/rhodecode/apps/search/__init__.py --- a/rhodecode/apps/search/__init__.py +++ b/rhodecode/apps/search/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/search/tests/test_search.py b/rhodecode/apps/search/tests/test_search.py --- a/rhodecode/apps/search/tests/test_search.py +++ b/rhodecode/apps/search/tests/test_search.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/search/views.py b/rhodecode/apps/search/views.py --- a/rhodecode/apps/search/views.py +++ b/rhodecode/apps/search/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/__init__.py b/rhodecode/apps/ssh_support/__init__.py --- a/rhodecode/apps/ssh_support/__init__.py +++ b/rhodecode/apps/ssh_support/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/config_keys.py b/rhodecode/apps/ssh_support/config_keys.py --- a/rhodecode/apps/ssh_support/config_keys.py +++ b/rhodecode/apps/ssh_support/config_keys.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/events.py b/rhodecode/apps/ssh_support/events.py --- a/rhodecode/apps/ssh_support/events.py +++ b/rhodecode/apps/ssh_support/events.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/__init__.py b/rhodecode/apps/ssh_support/lib/__init__.py --- a/rhodecode/apps/ssh_support/lib/__init__.py +++ b/rhodecode/apps/ssh_support/lib/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/backends/__init__.py b/rhodecode/apps/ssh_support/lib/backends/__init__.py --- a/rhodecode/apps/ssh_support/lib/backends/__init__.py +++ b/rhodecode/apps/ssh_support/lib/backends/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/backends/base.py b/rhodecode/apps/ssh_support/lib/backends/base.py --- a/rhodecode/apps/ssh_support/lib/backends/base.py +++ b/rhodecode/apps/ssh_support/lib/backends/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,7 +20,7 @@ import os import sys import logging -from rhodecode.lib.hook_daemon.base import prepare_callback_daemon +from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.lib.api_utils import call_service_api @@ -162,9 +162,7 @@ class SshVcsServer(object): extras = {} extras.update(tunnel_extras) - callback_daemon, extras = prepare_callback_daemon( - extras, protocol=self.hooks_protocol, - host=vcs_settings.HOOKS_HOST) + callback_daemon, extras = prepare_callback_daemon(extras, protocol=self.hooks_protocol) with callback_daemon: try: diff --git a/rhodecode/apps/ssh_support/lib/backends/git.py b/rhodecode/apps/ssh_support/lib/backends/git.py --- a/rhodecode/apps/ssh_support/lib/backends/git.py +++ b/rhodecode/apps/ssh_support/lib/backends/git.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/backends/hg.py b/rhodecode/apps/ssh_support/lib/backends/hg.py --- a/rhodecode/apps/ssh_support/lib/backends/hg.py +++ b/rhodecode/apps/ssh_support/lib/backends/hg.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,7 +25,7 @@ import tempfile from subprocess import Popen, PIPE import urllib.parse -from rhodecode_tools.lib.utils import safe_str +from rhodecode.lib.str_utils import safe_str from .base import SshVcsServer log = logging.getLogger(__name__) diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/lib/utils.py b/rhodecode/apps/ssh_support/lib/utils.py --- a/rhodecode/apps/ssh_support/lib/utils.py +++ b/rhodecode/apps/ssh_support/lib/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/subscribers.py b/rhodecode/apps/ssh_support/subscribers.py --- a/rhodecode/apps/ssh_support/subscribers.py +++ b/rhodecode/apps/ssh_support/subscribers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/tests/__init__.py b/rhodecode/apps/ssh_support/tests/__init__.py --- a/rhodecode/apps/ssh_support/tests/__init__.py +++ b/rhodecode/apps/ssh_support/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/tests/conftest.py b/rhodecode/apps/ssh_support/tests/conftest.py --- a/rhodecode/apps/ssh_support/tests/conftest.py +++ b/rhodecode/apps/ssh_support/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/tests/test_server_git.py b/rhodecode/apps/ssh_support/tests/test_server_git.py --- a/rhodecode/apps/ssh_support/tests/test_server_git.py +++ b/rhodecode/apps/ssh_support/tests/test_server_git.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -33,19 +33,24 @@ class GitServerCreator(object): 'app:main': { 'ssh.executable.git': git_path, 'vcs.hooks.protocol.v2': 'celery', + 'app.service_api.host': 'http://localhost', + 'app.service_api.token': 'secret4', + 'rhodecode.api.url': '/_admin/api', } } repo_name = 'test_git' repo_mode = 'receive-pack' user = plain_dummy_user() - def __init__(self): - pass + def __init__(self, service_api_url, ini_file): + self.service_api_url = service_api_url + self.ini_file = ini_file def create(self, **kwargs): + self.config_data['app:main']['app.service_api.host'] = self.service_api_url parameters = { 'store': self.root, - 'ini_path': '', + 'ini_path': self.ini_file, 'user': self.user, 'repo_name': self.repo_name, 'repo_mode': self.repo_mode, @@ -60,12 +65,30 @@ class GitServerCreator(object): return server -@pytest.fixture() -def git_server(app): - return GitServerCreator() +@pytest.fixture(scope='module') +def git_server(request, module_app, rhodecode_factory, available_port_factory): + ini_file = module_app._pyramid_settings['__file__'] + vcsserver_host = module_app._pyramid_settings['vcs.server'] + + store_dir = os.path.dirname(ini_file) + + # start rhodecode for service API + rc = rhodecode_factory( + request, + store_dir=store_dir, + port=available_port_factory(), + overrides=( + {'handler_console': {'level': 'DEBUG'}}, + {'app:main': {'vcs.server': vcsserver_host}}, + {'app:main': {'repo_store.path': store_dir}} + )) + + service_api_url = f'http://{rc.bind_addr}' + + return GitServerCreator(service_api_url, ini_file) -class TestGitServer(object): +class TestGitServer: def test_command(self, git_server): server = git_server.create() @@ -102,14 +125,14 @@ class TestGitServer(object): assert result is value def test_run_returns_executes_command(self, git_server): + from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper server = git_server.create() - from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper os.environ['SSH_CLIENT'] = '127.0.0.1' with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch: _patch.return_value = 0 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'): - exit_code = server.run() + exit_code = server.run(tunnel_extras={'config': server.ini_path}) assert exit_code == (0, False) @@ -135,7 +158,7 @@ class TestGitServer(object): 'action': action, 'ip': '10.10.10.10', 'locked_by': [None, None], - 'config': '', + 'config': git_server.ini_file, 'repo_store': store, 'server_url': None, 'hooks': ['push', 'pull'], diff --git a/rhodecode/apps/ssh_support/tests/test_server_hg.py b/rhodecode/apps/ssh_support/tests/test_server_hg.py --- a/rhodecode/apps/ssh_support/tests/test_server_hg.py +++ b/rhodecode/apps/ssh_support/tests/test_server_hg.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -17,6 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import os + import mock import pytest @@ -32,22 +33,27 @@ class MercurialServerCreator(object): 'app:main': { 'ssh.executable.hg': hg_path, 'vcs.hooks.protocol.v2': 'celery', + 'app.service_api.host': 'http://localhost', + 'app.service_api.token': 'secret4', + 'rhodecode.api.url': '/_admin/api', } } repo_name = 'test_hg' user = plain_dummy_user() - def __init__(self): - pass + def __init__(self, service_api_url, ini_file): + self.service_api_url = service_api_url + self.ini_file = ini_file def create(self, **kwargs): + self.config_data['app:main']['app.service_api.host'] = self.service_api_url parameters = { 'store': self.root, - 'ini_path': '', + 'ini_path': self.ini_file, 'user': self.user, 'repo_name': self.repo_name, 'user_permissions': { - 'test_hg': 'repository.admin' + self.repo_name: 'repository.admin' }, 'settings': self.config_data['app:main'], 'env': plain_dummy_env() @@ -57,12 +63,30 @@ class MercurialServerCreator(object): return server -@pytest.fixture() -def hg_server(app): - return MercurialServerCreator() +@pytest.fixture(scope='module') +def hg_server(request, module_app, rhodecode_factory, available_port_factory): + ini_file = module_app._pyramid_settings['__file__'] + vcsserver_host = module_app._pyramid_settings['vcs.server'] + + store_dir = os.path.dirname(ini_file) + + # start rhodecode for service API + rc = rhodecode_factory( + request, + store_dir=store_dir, + port=available_port_factory(), + overrides=( + {'handler_console': {'level': 'DEBUG'}}, + {'app:main': {'vcs.server': vcsserver_host}}, + {'app:main': {'repo_store.path': store_dir}} + )) + + service_api_url = f'http://{rc.bind_addr}' + + return MercurialServerCreator(service_api_url, ini_file) -class TestMercurialServer(object): +class TestMercurialServer: def test_command(self, hg_server, tmpdir): server = hg_server.create() @@ -107,7 +131,7 @@ class TestMercurialServer(object): with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch: _patch.return_value = 0 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'): - exit_code = server.run() + exit_code = server.run(tunnel_extras={'config': server.ini_path}) assert exit_code == (0, False) diff --git a/rhodecode/apps/ssh_support/tests/test_server_svn.py b/rhodecode/apps/ssh_support/tests/test_server_svn.py --- a/rhodecode/apps/ssh_support/tests/test_server_svn.py +++ b/rhodecode/apps/ssh_support/tests/test_server_svn.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -15,7 +15,9 @@ # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ + import os + import mock import pytest @@ -26,39 +28,62 @@ from rhodecode.apps.ssh_support.tests.co class SubversionServerCreator(object): root = '/tmp/repo/path/' svn_path = '/usr/local/bin/svnserve' + config_data = { 'app:main': { 'ssh.executable.svn': svn_path, 'vcs.hooks.protocol.v2': 'celery', + 'app.service_api.host': 'http://localhost', + 'app.service_api.token': 'secret4', + 'rhodecode.api.url': '/_admin/api', } } repo_name = 'test-svn' user = plain_dummy_user() - def __init__(self): - pass + def __init__(self, service_api_url, ini_file): + self.service_api_url = service_api_url + self.ini_file = ini_file def create(self, **kwargs): + self.config_data['app:main']['app.service_api.host'] = self.service_api_url parameters = { 'store': self.root, + 'ini_path': self.ini_file, + 'user': self.user, 'repo_name': self.repo_name, - 'ini_path': '', - 'user': self.user, 'user_permissions': { self.repo_name: 'repository.admin' }, 'settings': self.config_data['app:main'], 'env': plain_dummy_env() } - parameters.update(kwargs) server = SubversionServer(**parameters) return server -@pytest.fixture() -def svn_server(app): - return SubversionServerCreator() +@pytest.fixture(scope='module') +def svn_server(request, module_app, rhodecode_factory, available_port_factory): + ini_file = module_app._pyramid_settings['__file__'] + vcsserver_host = module_app._pyramid_settings['vcs.server'] + + store_dir = os.path.dirname(ini_file) + + # start rhodecode for service API + rc = rhodecode_factory( + request, + store_dir=store_dir, + port=available_port_factory(), + overrides=( + {'handler_console': {'level': 'DEBUG'}}, + {'app:main': {'vcs.server': vcsserver_host}}, + {'app:main': {'repo_store.path': store_dir}} + )) + + service_api_url = f'http://{rc.bind_addr}' + + return SubversionServerCreator(service_api_url, ini_file) class TestSubversionServer(object): @@ -168,8 +193,9 @@ class TestSubversionServer(object): assert repo_name == expected_match def test_run_returns_executes_command(self, svn_server): + from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper + server = svn_server.create() - from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper os.environ['SSH_CLIENT'] = '127.0.0.1' with mock.patch.object( SubversionTunnelWrapper, 'get_first_client_response', @@ -184,20 +210,18 @@ class TestSubversionServer(object): SubversionTunnelWrapper, 'command', return_value=['date']): - exit_code = server.run() + exit_code = server.run(tunnel_extras={'config': server.ini_path}) # SVN has this differently configured, and we get in our mock env # None as return code assert exit_code == (None, False) def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server): + from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper + server = svn_server.create() - from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper - with mock.patch.object( - SubversionTunnelWrapper, 'command', - return_value=['date']): - with mock.patch.object( - SubversionTunnelWrapper, 'get_first_client_response', + with mock.patch.object(SubversionTunnelWrapper, 'command', return_value=['date']): + with mock.patch.object(SubversionTunnelWrapper, 'get_first_client_response', return_value=None): - exit_code = server.run() + exit_code = server.run(tunnel_extras={'config': server.ini_path}) assert exit_code == (1, False) diff --git a/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py b/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py --- a/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py +++ b/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py b/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py --- a/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py +++ b/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/ssh_support/utils.py b/rhodecode/apps/ssh_support/utils.py --- a/rhodecode/apps/ssh_support/utils.py +++ b/rhodecode/apps/ssh_support/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/__init__.py b/rhodecode/apps/svn_support/__init__.py --- a/rhodecode/apps/svn_support/__init__.py +++ b/rhodecode/apps/svn_support/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/config_keys.py b/rhodecode/apps/svn_support/config_keys.py --- a/rhodecode/apps/svn_support/config_keys.py +++ b/rhodecode/apps/svn_support/config_keys.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/events.py b/rhodecode/apps/svn_support/events.py --- a/rhodecode/apps/svn_support/events.py +++ b/rhodecode/apps/svn_support/events.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/subscribers.py b/rhodecode/apps/svn_support/subscribers.py --- a/rhodecode/apps/svn_support/subscribers.py +++ b/rhodecode/apps/svn_support/subscribers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/tests/test_mod_dav_svn_config.py b/rhodecode/apps/svn_support/tests/test_mod_dav_svn_config.py --- a/rhodecode/apps/svn_support/tests/test_mod_dav_svn_config.py +++ b/rhodecode/apps/svn_support/tests/test_mod_dav_svn_config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/svn_support/utils.py b/rhodecode/apps/svn_support/utils.py --- a/rhodecode/apps/svn_support/utils.py +++ b/rhodecode/apps/svn_support/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group/__init__.py b/rhodecode/apps/user_group/__init__.py --- a/rhodecode/apps/user_group/__init__.py +++ b/rhodecode/apps/user_group/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group/tests/__init__.py b/rhodecode/apps/user_group/tests/__init__.py --- a/rhodecode/apps/user_group/tests/__init__.py +++ b/rhodecode/apps/user_group/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group/tests/test_user_groups.py b/rhodecode/apps/user_group/tests/test_user_groups.py --- a/rhodecode/apps/user_group/tests/test_user_groups.py +++ b/rhodecode/apps/user_group/tests/test_user_groups.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +22,7 @@ from rhodecode.tests import ( TestController, assert_session_flash, TEST_USER_ADMIN_LOGIN) from rhodecode.model.db import UserGroup from rhodecode.model.meta import Session -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/user_group/tests/test_user_groups_permissions.py b/rhodecode/apps/user_group/tests/test_user_groups_permissions.py --- a/rhodecode/apps/user_group/tests/test_user_groups_permissions.py +++ b/rhodecode/apps/user_group/tests/test_user_groups_permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group/views/__init__.py b/rhodecode/apps/user_group/views/__init__.py --- a/rhodecode/apps/user_group/views/__init__.py +++ b/rhodecode/apps/user_group/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group_profile/__init__.py b/rhodecode/apps/user_group_profile/__init__.py --- a/rhodecode/apps/user_group_profile/__init__.py +++ b/rhodecode/apps/user_group_profile/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group_profile/tests/__init__.py b/rhodecode/apps/user_group_profile/tests/__init__.py --- a/rhodecode/apps/user_group_profile/tests/__init__.py +++ b/rhodecode/apps/user_group_profile/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_group_profile/tests/test_user_group.py b/rhodecode/apps/user_group_profile/tests/test_user_group.py --- a/rhodecode/apps/user_group_profile/tests/test_user_group.py +++ b/rhodecode/apps/user_group_profile/tests/test_user_group.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -18,7 +18,7 @@ from rhodecode.model.user_group import UserGroupModel from rhodecode.tests import ( TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path fixture = Fixture() diff --git a/rhodecode/apps/user_group_profile/views.py b/rhodecode/apps/user_group_profile/views.py --- a/rhodecode/apps/user_group_profile/views.py +++ b/rhodecode/apps/user_group_profile/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_profile/__init__.py b/rhodecode/apps/user_profile/__init__.py --- a/rhodecode/apps/user_profile/__init__.py +++ b/rhodecode/apps/user_profile/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_profile/tests/__init__.py b/rhodecode/apps/user_profile/tests/__init__.py --- a/rhodecode/apps/user_profile/tests/__init__.py +++ b/rhodecode/apps/user_profile/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/apps/user_profile/tests/test_users.py b/rhodecode/apps/user_profile/tests/test_users.py --- a/rhodecode/apps/user_profile/tests/test_users.py +++ b/rhodecode/apps/user_profile/tests/test_users.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +22,7 @@ from rhodecode.model.db import User from rhodecode.tests import ( TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.utils import AssertResponse from rhodecode.tests.routes import route_path diff --git a/rhodecode/apps/user_profile/views.py b/rhodecode/apps/user_profile/views.py --- a/rhodecode/apps/user_profile/views.py +++ b/rhodecode/apps/user_profile/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/__init__.py b/rhodecode/authentication/__init__.py --- a/rhodecode/authentication/__init__.py +++ b/rhodecode/authentication/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/interface.py b/rhodecode/authentication/interface.py --- a/rhodecode/authentication/interface.py +++ b/rhodecode/authentication/interface.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/__init__.py b/rhodecode/authentication/plugins/__init__.py --- a/rhodecode/authentication/plugins/__init__.py +++ b/rhodecode/authentication/plugins/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_crowd.py b/rhodecode/authentication/plugins/auth_crowd.py --- a/rhodecode/authentication/plugins/auth_crowd.py +++ b/rhodecode/authentication/plugins/auth_crowd.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_headers.py b/rhodecode/authentication/plugins/auth_headers.py --- a/rhodecode/authentication/plugins/auth_headers.py +++ b/rhodecode/authentication/plugins/auth_headers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_jasig_cas.py b/rhodecode/authentication/plugins/auth_jasig_cas.py --- a/rhodecode/authentication/plugins/auth_jasig_cas.py +++ b/rhodecode/authentication/plugins/auth_jasig_cas.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_ldap.py b/rhodecode/authentication/plugins/auth_ldap.py --- a/rhodecode/authentication/plugins/auth_ldap.py +++ b/rhodecode/authentication/plugins/auth_ldap.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_pam.py b/rhodecode/authentication/plugins/auth_pam.py --- a/rhodecode/authentication/plugins/auth_pam.py +++ b/rhodecode/authentication/plugins/auth_pam.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_rhodecode.py b/rhodecode/authentication/plugins/auth_rhodecode.py --- a/rhodecode/authentication/plugins/auth_rhodecode.py +++ b/rhodecode/authentication/plugins/auth_rhodecode.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/plugins/auth_token.py b/rhodecode/authentication/plugins/auth_token.py --- a/rhodecode/authentication/plugins/auth_token.py +++ b/rhodecode/authentication/plugins/auth_token.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/registry.py b/rhodecode/authentication/registry.py --- a/rhodecode/authentication/registry.py +++ b/rhodecode/authentication/registry.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/routes.py b/rhodecode/authentication/routes.py --- a/rhodecode/authentication/routes.py +++ b/rhodecode/authentication/routes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/schema.py b/rhodecode/authentication/schema.py --- a/rhodecode/authentication/schema.py +++ b/rhodecode/authentication/schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/tests/conftest.py b/rhodecode/authentication/tests/conftest.py --- a/rhodecode/authentication/tests/conftest.py +++ b/rhodecode/authentication/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/tests/functional/test_settings.py b/rhodecode/authentication/tests/functional/test_settings.py --- a/rhodecode/authentication/tests/functional/test_settings.py +++ b/rhodecode/authentication/tests/functional/test_settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/authentication/views.py b/rhodecode/authentication/views.py --- a/rhodecode/authentication/views.py +++ b/rhodecode/authentication/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/bootstrap.py b/rhodecode/bootstrap.py new file mode 100644 --- /dev/null +++ b/rhodecode/bootstrap.py @@ -0,0 +1,41 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +# bootstrap data available for tests and setup clean install + +TEST_USER_ADMIN_LOGIN = 'test_admin' +TEST_USER_ADMIN_PASS = 'test12' +TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com' + +TEST_USER_REGULAR_LOGIN = 'test_regular' +TEST_USER_REGULAR_PASS = 'test12' +TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com' + +TEST_USER_REGULAR2_LOGIN = 'test_regular2' +TEST_USER_REGULAR2_PASS = 'test12' +TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com' + +HG_REPO = 'vcs_test_hg' +GIT_REPO = 'vcs_test_git' +SVN_REPO = 'vcs_test_svn' + +NEW_HG_REPO = 'vcs_test_hg_new' +NEW_GIT_REPO = 'vcs_test_git_new' + +HG_FORK = 'vcs_test_hg_fork' +GIT_FORK = 'vcs_test_git_fork' diff --git a/rhodecode/config/__init__.py b/rhodecode/config/__init__.py --- a/rhodecode/config/__init__.py +++ b/rhodecode/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/conf.py b/rhodecode/config/conf.py --- a/rhodecode/config/conf.py +++ b/rhodecode/config/conf.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py --- a/rhodecode/config/config_maker.py +++ b/rhodecode/config/config_maker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -122,9 +122,12 @@ def sanitize_settings_and_apply_defaults settings_maker.make_setting('vcs.start_server', 'false', parser='bool') settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list') settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int') - settings_maker.make_setting('vcs.git.lfs.storage_location', '/var/opt/rhodecode_repo_store/.cache/git_lfs_store') + settings_maker.make_setting('vcs.git.lfs.storage_location', + '/var/opt/rhodecode_repo_store/.cache/git_lfs_store', + parser='dir:ensured', default_when_empty=True) settings_maker.make_setting('vcs.hg.largefiles.storage_location', - '/var/opt/rhodecode_repo_store/.cache/hg_largefiles_store') + '/var/opt/rhodecode_repo_store/.cache/hg_largefiles_store', + parser='dir:ensured', default_when_empty=True) settings_maker.make_setting('vcs.methods.cache', True, parser='bool') @@ -162,7 +165,7 @@ def sanitize_settings_and_apply_defaults ) # celery - broker_url = settings_maker.make_setting('celery.broker_url', 'redis://redis:6379/8') + broker_url = settings_maker.make_setting('celery.broker_url', 'redis://redis:6379/8', default_when_empty=True) settings_maker.make_setting('celery.result_backend', broker_url) settings_maker.make_setting('exception_tracker.send_email', False, parser='bool') diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,7 +30,7 @@ from rhodecode.lib.vcs import connect_vc log = logging.getLogger(__name__) -def propagate_rhodecode_config(global_config, settings, config): +def propagate_rhodecode_config(global_config, settings, config, full=True): # Store the settings to make them available to other modules. settings_merged = global_config.copy() settings_merged.update(settings) @@ -40,7 +40,7 @@ def propagate_rhodecode_config(global_co rhodecode.PYRAMID_SETTINGS = settings_merged rhodecode.CONFIG = settings_merged - if 'default_user_id' not in rhodecode.CONFIG: + if full and 'default_user_id' not in rhodecode.CONFIG: rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id() log.debug('set rhodecode.CONFIG data') @@ -93,6 +93,7 @@ def load_pyramid_environment(global_conf # first run, to store data... propagate_rhodecode_config(global_config, settings, {}) + if vcs_server_enabled: connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings)) else: diff --git a/rhodecode/config/jsroutes.py b/rhodecode/config/jsroutes.py --- a/rhodecode/config/jsroutes.py +++ b/rhodecode/config/jsroutes.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -49,7 +49,7 @@ from rhodecode.lib.plugins.utils import from rhodecode.lib.utils2 import AttributeDict from rhodecode.lib.exc_tracking import store_exception, format_exc from rhodecode.subscribers import ( - scan_repositories_if_enabled, write_js_routes_if_enabled, + auto_merge_pr_if_needed, scan_repositories_if_enabled, write_js_routes_if_enabled, write_metadata_if_needed, write_usage_data, import_license_if_present) from rhodecode.lib.statsd_client import StatsdClient @@ -101,6 +101,9 @@ def make_pyramid_app(global_config, **se patches.inspect_getargspec() patches.repoze_sendmail_lf_fix() + # first init, so load_pyramid_enviroment, can access some critical data, like __file__ + propagate_rhodecode_config(global_config, {}, {}, full=False) + load_pyramid_environment(global_config, settings) # Static file view comes first @@ -392,6 +395,7 @@ def includeme(config, auth_resources=Non # Add subscribers. if load_all: log.debug('Adding subscribers...') + config.add_subscriber(auto_merge_pr_if_needed, rhodecode.events.PullRequestReviewEvent) config.add_subscriber(scan_repositories_if_enabled, pyramid.events.ApplicationCreated) config.add_subscriber(write_metadata_if_needed, diff --git a/rhodecode/config/patches.py b/rhodecode/config/patches.py --- a/rhodecode/config/patches.py +++ b/rhodecode/config/patches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/rcextensions/__init__.py b/rhodecode/config/rcextensions/__init__.py --- a/rhodecode/config/rcextensions/__init__.py +++ b/rhodecode/config/rcextensions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -17,7 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ """ -rcextensions module, please edit `hooks.py` to over write hooks logic +rcextensions module, please edit `hooks.py` to over-write hooks logic """ from .hooks import ( diff --git a/rhodecode/config/rcextensions/examples/validate_pushed_files_name_and_size.py b/rhodecode/config/rcextensions/examples/validate_pushed_files_name_and_size.py --- a/rhodecode/config/rcextensions/examples/validate_pushed_files_name_and_size.py +++ b/rhodecode/config/rcextensions/examples/validate_pushed_files_name_and_size.py @@ -85,7 +85,7 @@ def _pre_push_hook(*args, **kwargs): # check files names if forbidden_files: - reason = 'File {} is forbidden to be pushed'.format(file_name) + reason = f'File {file_name} is forbidden to be pushed' for forbidden_pattern in forbid_files: # here we can also filter for operation, e.g if check for only ADDED files # if operation == 'A': diff --git a/rhodecode/config/rcextensions/helpers/__init__.py b/rhodecode/config/rcextensions/helpers/__init__.py --- a/rhodecode/config/rcextensions/helpers/__init__.py +++ b/rhodecode/config/rcextensions/helpers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/rcextensions/helpers/extra_fields.py b/rhodecode/config/rcextensions/helpers/extra_fields.py --- a/rhodecode/config/rcextensions/helpers/extra_fields.py +++ b/rhodecode/config/rcextensions/helpers/extra_fields.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -55,7 +54,7 @@ def run(*args, **kwargs): return fields -class _Undefined(object): +class _Undefined: pass @@ -67,7 +66,7 @@ def get_field(extra_fields_data, key, de if key not in extra_fields_data: if isinstance(default, _Undefined): - raise ValueError('key {} not present in extra_fields'.format(key)) + raise ValueError(f'key {key} not present in extra_fields') return default # NOTE(dan): from metadata we get field_label, field_value, field_desc, field_type diff --git a/rhodecode/config/rcextensions/helpers/extract_post_commits.py b/rhodecode/config/rcextensions/helpers/extract_post_commits.py --- a/rhodecode/config/rcextensions/helpers/extract_post_commits.py +++ b/rhodecode/config/rcextensions/helpers/extract_post_commits.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/rcextensions/helpers/extract_pre_commits.py b/rhodecode/config/rcextensions/helpers/extract_pre_commits.py --- a/rhodecode/config/rcextensions/helpers/extract_pre_commits.py +++ b/rhodecode/config/rcextensions/helpers/extract_pre_commits.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -52,7 +51,7 @@ def get_git_commits(repo, refs): cmd = [ 'log', '--pretty=format:{"commit_id": "%H", "author": "%aN <%aE>", "date": "%ad", "message": "%s"}', - '{}...{}'.format(old_rev, new_rev) + f'{old_rev}...{new_rev}' ] stdout, stderr = repo.run_git_command(cmd, extra_env=git_env) @@ -80,12 +79,12 @@ def run(*args, **kwargs): if vcs_type == 'git': for rev_data in kwargs['commit_ids']: - new_environ = dict((k, v) for k, v in rev_data['git_env']) + new_environ = {k: v for k, v in rev_data['git_env']} commits = get_git_commits(vcs_repo, kwargs['commit_ids']) if vcs_type == 'hg': for rev_data in kwargs['commit_ids']: - new_environ = dict((k, v) for k, v in rev_data['hg_env']) + new_environ = {k: v for k, v in rev_data['hg_env']} commits = get_hg_commits(vcs_repo, kwargs['commit_ids']) return commits diff --git a/rhodecode/config/rcextensions/helpers/extract_pre_files.py b/rhodecode/config/rcextensions/helpers/extract_pre_files.py --- a/rhodecode/config/rcextensions/helpers/extract_pre_files.py +++ b/rhodecode/config/rcextensions/helpers/extract_pre_files.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -133,12 +132,12 @@ def run(*args, **kwargs): if vcs_type == 'git': for rev_data in kwargs['commit_ids']: - new_environ = dict((k, v) for k, v in rev_data['git_env']) + new_environ = {k: v for k, v in rev_data['git_env']} files = get_git_files(repo, vcs_repo, kwargs['commit_ids']) if vcs_type == 'hg': for rev_data in kwargs['commit_ids']: - new_environ = dict((k, v) for k, v in rev_data['hg_env']) + new_environ = {k: v for k, v in rev_data['hg_env']} files = get_hg_files(repo, vcs_repo, kwargs['commit_ids']) if vcs_type == 'svn': diff --git a/rhodecode/config/rcextensions/helpers/http_call.py b/rhodecode/config/rcextensions/helpers/http_call.py --- a/rhodecode/config/rcextensions/helpers/http_call.py +++ b/rhodecode/config/rcextensions/helpers/http_call.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/rcextensions/hooks.py b/rhodecode/config/rcextensions/hooks.py --- a/rhodecode/config/rcextensions/hooks.py +++ b/rhodecode/config/rcextensions/hooks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/rcextensions/utils.py b/rhodecode/config/rcextensions/utils.py --- a/rhodecode/config/rcextensions/utils.py +++ b/rhodecode/config/rcextensions/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,7 +28,7 @@ import urllib.error log = logging.getLogger('rhodecode.' + __name__) -class HookResponse(object): +class HookResponse: def __init__(self, status, output): self.status = status self.output = output @@ -44,6 +44,11 @@ class HookResponse(object): def __bool__(self): return self.status == 0 + def to_json(self): + return {'status': self.status, 'output': self.output} + + def __repr__(self): + return self.to_json().__repr__() class DotDict(dict): @@ -91,8 +96,8 @@ class DotDict(dict): def __repr__(self): keys = list(self.keys()) keys.sort() - args = ', '.join(['%s=%r' % (key, self[key]) for key in keys]) - return '%s(%s)' % (self.__class__.__name__, args) + args = ', '.join(['{}={!r}'.format(key, self[key]) for key in keys]) + return '{}({})'.format(self.__class__.__name__, args) @staticmethod def fromDict(d): @@ -110,7 +115,7 @@ def serialize(x): def unserialize(x): if isinstance(x, dict): - return dict((k, unserialize(v)) for k, v in x.items()) + return {k: unserialize(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return type(x)(unserialize(v) for v in x) else: @@ -161,7 +166,8 @@ def str2bool(_str) -> bool: string into boolean :param _str: string value to translate into boolean - :returns: bool from given string + :rtype: boolean + :returns: boolean from given string """ if _str is None: return False diff --git a/rhodecode/config/routing_links.py b/rhodecode/config/routing_links.py --- a/rhodecode/config/routing_links.py +++ b/rhodecode/config/routing_links.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -49,22 +48,22 @@ link_config = [ { "name": "enterprise_docs", "target": "https://rhodecode.com/r1/enterprise/docs/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", + "external_target": "https://docs.rhodecode.com/4.x/rce/index.html", }, { "name": "enterprise_log_file_locations", "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", + "external_target": "https://docs.rhodecode.com/4.x/rce/admin/system-overview.html#log-files", }, { "name": "enterprise_issue_tracker_settings", "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", + "external_target": "https://docs.rhodecode.com/4.x/rce/issue-trackers/issue-trackers.html", }, { "name": "enterprise_svn_setup", "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", + "external_target": "https://docs.rhodecode.com/4.x/rce/admin/svn-http.html", }, { "name": "enterprise_license_convert_from_old", diff --git a/rhodecode/config/settings_maker.py b/rhodecode/config/settings_maker.py --- a/rhodecode/config/settings_maker.py +++ b/rhodecode/config/settings_maker.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/config/utils.py b/rhodecode/config/utils.py --- a/rhodecode/config/utils.py +++ b/rhodecode/config/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,6 +19,8 @@ import os import platform +from rhodecode.lib.type_utils import str2bool + DEFAULT_USER = 'default' @@ -37,7 +39,6 @@ def configure_vcs(config): conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol.v2'] conf.settings.HOOKS_HOST = config['vcs.hooks.host'] - conf.settings.DEFAULT_ENCODINGS = config['default_encoding'] conf.settings.ALIASES[:] = config['vcs.backends'] conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version'] @@ -48,28 +49,23 @@ def initialize_database(config): engine = engine_from_config(config, 'sqlalchemy.db1.') init_model(engine, encryption_key=get_encryption_key(config)) +def initialize_test_environment(settings): + skip_test_env = str2bool(os.environ.get('RC_NO_TEST_ENV')) + if skip_test_env: + return -def initialize_test_environment(settings, test_env=None): - if test_env is None: - test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0)) + repo_store_path = os.environ.get('RC_TEST_ENV_REPO_STORE') or settings['repo_store.path'] from rhodecode.lib.utils import ( create_test_directory, create_test_database, create_test_repositories, create_test_index) - from rhodecode.tests import TESTS_TMP_PATH - from rhodecode.lib.vcs.backends.hg import largefiles_store - from rhodecode.lib.vcs.backends.git import lfs_store + create_test_directory(repo_store_path) + + create_test_database(repo_store_path, settings) # test repos - if test_env: - create_test_directory(TESTS_TMP_PATH) - # large object stores - create_test_directory(largefiles_store(TESTS_TMP_PATH)) - create_test_directory(lfs_store(TESTS_TMP_PATH)) - - create_test_database(TESTS_TMP_PATH, settings) - create_test_repositories(TESTS_TMP_PATH, settings) - create_test_index(TESTS_TMP_PATH, settings) + create_test_repositories(repo_store_path, settings) + create_test_index(repo_store_path, settings) def get_vcs_server_protocol(config): diff --git a/rhodecode/events/__init__.py b/rhodecode/events/__init__.py --- a/rhodecode/events/__init__.py +++ b/rhodecode/events/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -18,13 +18,10 @@ import logging -from rhodecode.events.base import ( - RhodeCodeIntegrationEvent, - RhodecodeEvent -) +from rhodecode.events.base import RhodeCodeIntegrationEvent, RhodecodeEvent from rhodecode.events.base import ( # pragma: no cover - FtsBuild + FtsBuild, ) from rhodecode.events.user import ( # pragma: no cover @@ -37,11 +34,16 @@ from rhodecode.events.user import ( # p from rhodecode.events.repo import ( # pragma: no cover RepoEvent, - RepoCommitCommentEvent, RepoCommitCommentEditEvent, - RepoPreCreateEvent, RepoCreateEvent, - RepoPreDeleteEvent, RepoDeleteEvent, - RepoPrePushEvent, RepoPushEvent, - RepoPrePullEvent, RepoPullEvent, + RepoCommitCommentEvent, + RepoCommitCommentEditEvent, + RepoPreCreateEvent, + RepoCreateEvent, + RepoPreDeleteEvent, + RepoDeleteEvent, + RepoPrePushEvent, + RepoPushEvent, + RepoPrePullEvent, + RepoPullEvent, ) from rhodecode.events.repo_group import ( # pragma: no cover @@ -77,12 +79,13 @@ def trigger(event, registry=None): from pyramid.threadlocal import get_current_registry event_name = event.__class__ - log.debug('event %s sent for execution', event_name) + log.debug("event %s sent for execution", event_name) registry = registry or get_current_registry() registry.notify(event) - log.debug('event %s triggered using registry %s', event_name, registry) + log.debug("event %s triggered using registry %s", event_name, registry) # Send the events to integrations directly from rhodecode.integrations import integrations_event_handler + if isinstance(event, RhodeCodeIntegrationEvent): integrations_event_handler(event) diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,10 +26,7 @@ from rhodecode.lib.utils2 import Attribu # this is a user object to be used for events caused by the system (eg. shell) -SYSTEM_USER = AttributeDict(dict( - username='__SYSTEM__', - user_id='__SYSTEM_ID__' -)) +SYSTEM_USER = AttributeDict(dict(username="__SYSTEM__", user_id="__SYSTEM_ID__")) log = logging.getLogger(__name__) @@ -38,16 +35,18 @@ class RhodecodeEvent(object): """ Base event class for all RhodeCode events """ + name = "RhodeCodeEvent" - no_url_set = '' + no_url_set = "" - def __init__(self, request=None, actor=None): + def __init__(self, request=None, actor=None, context=None): self._request = request self._actor = actor + self._context = context self.utc_timestamp = datetime.datetime.utcnow() def __repr__(self): - return '<{}:({})>'.format(self.__class__.__name__, self.name) + return f"<{self.__class__.__name__}:(name={self.name}, context={self._context})>" def get_request(self): if self._request: @@ -63,11 +62,11 @@ class RhodecodeEvent(object): if not self.request: return - user = getattr(self.request, 'user', None) + user = getattr(self.request, "user", None) if user: return user - api_user = getattr(self.request, 'rpc_user', None) + api_user = getattr(self.request, "rpc_user", None) if api_user: return api_user @@ -80,15 +79,17 @@ class RhodecodeEvent(object): return self._actor auth_user = self.auth_user - log.debug('Got integration actor: %s', auth_user) + log.debug("Got integration actor: %s", auth_user) if isinstance(auth_user, AuthUser): instance = auth_user.get_instance() # we can't find this DB user... if not instance: - return AttributeDict(dict( - username=auth_user.username, - user_id=auth_user.user_id, - )) + return AttributeDict( + dict( + username=auth_user.username, + user_id=auth_user.user_id, + ) + ) elif auth_user: return auth_user return SYSTEM_USER @@ -98,29 +99,26 @@ class RhodecodeEvent(object): auth_user = self.auth_user if auth_user: return auth_user.ip_addr - return '' + return "" @property def server_url(self): if self.request: try: - return self.request.route_url('home') + return self.request.route_url("home") except Exception: - log.exception('Failed to fetch URL for server') + log.exception("Failed to fetch URL for server") return self.no_url_set return self.no_url_set def as_dict(self): data = { - 'name': self.name, - 'utc_timestamp': self.utc_timestamp, - 'actor_ip': self.actor_ip, - 'actor': { - 'username': self.actor.username, - 'user_id': self.actor.user_id - }, - 'server_url': self.server_url + "name": self.name, + "utc_timestamp": self.utc_timestamp, + "actor_ip": self.actor_ip, + "actor": {"username": self.actor.username, "user_id": self.actor.user_id}, + "server_url": self.server_url, } return data @@ -129,13 +127,14 @@ class RhodeCodeIntegrationEvent(Rhodecod """ Special subclass for Integration events """ - description = '' + + description = "" class FtsBuild(RhodecodeEvent): """ This event will be triggered when FTS Build is triggered """ - name = 'fts-build' - display_name = 'Start FTS Build' + name = "fts-build" + display_name = "Start FTS Build" diff --git a/rhodecode/events/interfaces.py b/rhodecode/events/interfaces.py --- a/rhodecode/events/interfaces.py +++ b/rhodecode/events/interfaces.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,20 +24,23 @@ class IUserRegistered(Interface): An event type that is emitted whenever a new user registers a user account. """ - user = Attribute('The user object.') - session = Attribute('The session while processing the register form post.') + + user = Attribute("The user object.") + session = Attribute("The session while processing the register form post.") class IUserPreCreate(Interface): """ An event type that is emitted before a new user object is created. """ - user_data = Attribute('Data used to create the new user') + + user_data = Attribute("Data used to create the new user") class IUserPreUpdate(Interface): """ An event type that is emitted before a user object is updated. """ - user = Attribute('The not yet updated user object') - user_data = Attribute('Data used to update the user') + + user = Attribute("The not yet updated user object") + user_data = Attribute("Data used to update the user") diff --git a/rhodecode/events/pullrequest.py b/rhodecode/events/pullrequest.py --- a/rhodecode/events/pullrequest.py +++ b/rhodecode/events/pullrequest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,7 +19,7 @@ import logging from rhodecode.translation import lazy_ugettext -from rhodecode.events.repo import (RepoEvent, _commits_as_dict, _issues_as_dict) +from rhodecode.events.repo import RepoEvent, _commits_as_dict, _issues_as_dict log = logging.getLogger(__name__) @@ -30,45 +30,44 @@ class PullRequestEvent(RepoEvent): :param pullrequest: a :class:`PullRequest` instance """ - name = 'pullrequest-event' - display_name = lazy_ugettext('pullrequest generic event') - description = lazy_ugettext('All events within a context of a pull request') + + name = "pullrequest-event" + display_name = lazy_ugettext("pullrequest generic event") + description = lazy_ugettext("All events within a context of a pull request") - def __init__(self, pullrequest): - super().__init__(pullrequest.target_repo) + def __init__(self, pullrequest, context=None): + super().__init__(pullrequest.target_repo, context=context) self.pullrequest = pullrequest + self.context = self._context def as_dict(self): from rhodecode.lib.utils2 import md5_safe from rhodecode.model.pull_request import PullRequestModel + data = super().as_dict() - commits = _commits_as_dict( - self, - commit_ids=self.pullrequest.revisions, - repos=[self.pullrequest.source_repo] - ) + commits = _commits_as_dict(self, commit_ids=self.pullrequest.revisions, repos=[self.pullrequest.source_repo]) issues = _issues_as_dict(commits) # calculate hashes of all commits for unique identifier of commits # inside that pull request - commits_hash = md5_safe(':'.join(x.get('raw_id', '') for x in commits)) + commits_hash = md5_safe(":".join(x.get("raw_id", "") for x in commits)) - data.update({ - 'pullrequest': { - 'title': self.pullrequest.title, - 'issues': issues, - 'pull_request_id': self.pullrequest.pull_request_id, - 'url': PullRequestModel().get_url( - self.pullrequest, request=self.request), - 'permalink_url': PullRequestModel().get_url( - self.pullrequest, request=self.request, permalink=True), - 'shadow_url': PullRequestModel().get_shadow_clone_url( - self.pullrequest, request=self.request), - 'status': self.pullrequest.calculated_review_status(), - 'commits_uid': commits_hash, - 'commits': commits, + data.update( + { + "pullrequest": { + "title": self.pullrequest.title, + "issues": issues, + "pull_request_id": self.pullrequest.pull_request_id, + "url": PullRequestModel().get_url(self.pullrequest, request=self.request), + "permalink_url": PullRequestModel().get_url(self.pullrequest, request=self.request, permalink=True), + "shadow_url": PullRequestModel().get_shadow_clone_url(self.pullrequest, request=self.request), + "status": self.pullrequest.calculated_review_status(), + "commits_uid": commits_hash, + "commits": commits, + }, + "context": self.context, } - }) + ) return data @@ -77,9 +76,10 @@ class PullRequestCreateEvent(PullRequest An instance of this class is emitted as an :term:`event` after a pull request is created. """ - name = 'pullrequest-create' - display_name = lazy_ugettext('pullrequest created') - description = lazy_ugettext('Event triggered after pull request was created') + + name = "pullrequest-create" + display_name = lazy_ugettext("pullrequest created") + description = lazy_ugettext("Event triggered after pull request was created") class PullRequestCloseEvent(PullRequestEvent): @@ -87,9 +87,10 @@ class PullRequestCloseEvent(PullRequestE An instance of this class is emitted as an :term:`event` after a pull request is closed. """ - name = 'pullrequest-close' - display_name = lazy_ugettext('pullrequest closed') - description = lazy_ugettext('Event triggered after pull request was closed') + + name = "pullrequest-close" + display_name = lazy_ugettext("pullrequest closed") + description = lazy_ugettext("Event triggered after pull request was closed") class PullRequestUpdateEvent(PullRequestEvent): @@ -97,9 +98,10 @@ class PullRequestUpdateEvent(PullRequest An instance of this class is emitted as an :term:`event` after a pull request's commits have been updated. """ - name = 'pullrequest-update' - display_name = lazy_ugettext('pullrequest commits updated') - description = lazy_ugettext('Event triggered after pull requests was updated') + + name = "pullrequest-update" + display_name = lazy_ugettext("pullrequest commits updated") + description = lazy_ugettext("Event triggered after pull requests was updated") class PullRequestReviewEvent(PullRequestEvent): @@ -107,13 +109,13 @@ class PullRequestReviewEvent(PullRequest An instance of this class is emitted as an :term:`event` after a pull request review has changed. A status defines new status of review. """ - name = 'pullrequest-review' - display_name = lazy_ugettext('pullrequest review changed') - description = lazy_ugettext('Event triggered after a review status of a ' - 'pull requests has changed to other.') - def __init__(self, pullrequest, status): - super().__init__(pullrequest) + name = "pullrequest-review" + display_name = lazy_ugettext("pullrequest review changed") + description = lazy_ugettext("Event triggered after a review status of a pull requests has changed to other.") + + def __init__(self, pullrequest, status, context=None): + super().__init__(pullrequest, context=context) self.status = status @@ -122,10 +124,10 @@ class PullRequestMergeEvent(PullRequestE An instance of this class is emitted as an :term:`event` after a pull request is merged. """ - name = 'pullrequest-merge' - display_name = lazy_ugettext('pullrequest merged') - description = lazy_ugettext('Event triggered after a successful merge operation ' - 'was executed on a pull request') + + name = "pullrequest-merge" + display_name = lazy_ugettext("pullrequest merged") + description = lazy_ugettext("Event triggered after a successful merge operation was executed on a pull request") class PullRequestCommentEvent(PullRequestEvent): @@ -133,37 +135,39 @@ class PullRequestCommentEvent(PullReques An instance of this class is emitted as an :term:`event` after a pull request comment is created. """ - name = 'pullrequest-comment' - display_name = lazy_ugettext('pullrequest commented') - description = lazy_ugettext('Event triggered after a comment was made on a code ' - 'in the pull request') - def __init__(self, pullrequest, comment): - super().__init__(pullrequest) + name = "pullrequest-comment" + display_name = lazy_ugettext("pullrequest commented") + description = lazy_ugettext("Event triggered after a comment was made on a code in the pull request") + + def __init__(self, pullrequest, comment, context=None): + super().__init__(pullrequest, context=context) self.comment = comment def as_dict(self): from rhodecode.model.comment import CommentsModel + data = super().as_dict() status = None if self.comment.status_change: status = self.comment.review_status - data.update({ - 'comment': { - 'status': status, - 'text': self.comment.text, - 'type': self.comment.comment_type, - 'file': self.comment.f_path, - 'line': self.comment.line_no, - 'version': self.comment.last_version, - 'url': CommentsModel().get_url( - self.comment, request=self.request), - 'permalink_url': CommentsModel().get_url( - self.comment, request=self.request, permalink=True), + data.update( + { + "comment": { + "status": status, + "text": self.comment.text, + "type": self.comment.comment_type, + "file": self.comment.f_path, + "line": self.comment.line_no, + "version": self.comment.last_version, + "url": CommentsModel().get_url(self.comment, request=self.request), + "permalink_url": CommentsModel().get_url(self.comment, request=self.request, permalink=True), + }, + "context": self.context, } - }) + ) return data @@ -172,10 +176,10 @@ class PullRequestCommentEditEvent(PullRe An instance of this class is emitted as an :term:`event` after a pull request comment is edited. """ - name = 'pullrequest-comment-edit' - display_name = lazy_ugettext('pullrequest comment edited') - description = lazy_ugettext('Event triggered after a comment was edited on a code ' - 'in the pull request') + + name = "pullrequest-comment-edit" + display_name = lazy_ugettext("pullrequest comment edited") + description = lazy_ugettext("Event triggered after a comment was edited on a code in the pull request") def __init__(self, pullrequest, comment): super().__init__(pullrequest) @@ -183,24 +187,26 @@ class PullRequestCommentEditEvent(PullRe def as_dict(self): from rhodecode.model.comment import CommentsModel + data = super().as_dict() status = None if self.comment.status_change: status = self.comment.review_status - data.update({ - 'comment': { - 'status': status, - 'text': self.comment.text, - 'type': self.comment.comment_type, - 'file': self.comment.f_path, - 'line': self.comment.line_no, - 'version': self.comment.last_version, - 'url': CommentsModel().get_url( - self.comment, request=self.request), - 'permalink_url': CommentsModel().get_url( - self.comment, request=self.request, permalink=True), + data.update( + { + "comment": { + "status": status, + "text": self.comment.text, + "type": self.comment.comment_type, + "file": self.comment.f_path, + "line": self.comment.line_no, + "version": self.comment.last_version, + "url": CommentsModel().get_url(self.comment, request=self.request), + "permalink_url": CommentsModel().get_url(self.comment, request=self.request, permalink=True), + }, + "context": self.context } - }) + ) return data diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py --- a/rhodecode/events/repo.py +++ b/rhodecode/events/repo.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -37,12 +37,11 @@ def _commits_as_dict(event, commit_ids, :param repos: a list of repos to check """ from rhodecode.lib.utils2 import extract_mentioned_users - from rhodecode.lib.helpers import ( - urlify_commit_message, process_patterns, chop_at_smart) + from rhodecode.lib.helpers import urlify_commit_message, process_patterns, chop_at_smart from rhodecode.model.repo import RepoModel if not repos: - raise Exception('no repo defined') + raise Exception("no repo defined") if not isinstance(repos, (tuple, list)): repos = [repos] @@ -63,37 +62,31 @@ def _commits_as_dict(event, commit_ids, try: # use copy of needed_commits since we modify it while iterating for commit_id in list(needed_commits): - if commit_id.startswith('tag=>'): + if commit_id.startswith("tag=>"): raw_id = commit_id[5:] cs_data = { - 'raw_id': commit_id, 'short_id': commit_id, - 'branch': None, - 'git_ref_change': 'tag_add', - 'message': f'Added new tag {raw_id}', - 'author': event.actor.full_contact, - 'date': datetime.datetime.now(), - 'refs': { - 'branches': [], - 'bookmarks': [], - 'tags': [] - } + "raw_id": commit_id, + "short_id": commit_id, + "branch": None, + "git_ref_change": "tag_add", + "message": f"Added new tag {raw_id}", + "author": event.actor.full_contact, + "date": datetime.datetime.now(), + "refs": {"branches": [], "bookmarks": [], "tags": []}, } commits.append(cs_data) - elif commit_id.startswith('delete_branch=>'): + elif commit_id.startswith("delete_branch=>"): raw_id = commit_id[15:] cs_data = { - 'raw_id': commit_id, 'short_id': commit_id, - 'branch': None, - 'git_ref_change': 'branch_delete', - 'message': f'Deleted branch {raw_id}', - 'author': event.actor.full_contact, - 'date': datetime.datetime.now(), - 'refs': { - 'branches': [], - 'bookmarks': [], - 'tags': [] - } + "raw_id": commit_id, + "short_id": commit_id, + "branch": None, + "git_ref_change": "branch_delete", + "message": f"Deleted branch {raw_id}", + "author": event.actor.full_contact, + "date": datetime.datetime.now(), + "refs": {"branches": [], "bookmarks": [], "tags": []}, } commits.append(cs_data) @@ -104,50 +97,45 @@ def _commits_as_dict(event, commit_ids, continue # maybe its in next repo cs_data = cs.__json__() - cs_data['refs'] = cs._get_refs() + cs_data["refs"] = cs._get_refs() - cs_data['mentions'] = extract_mentioned_users(cs_data['message']) - cs_data['reviewers'] = reviewers - cs_data['url'] = RepoModel().get_commit_url( - repo, cs_data['raw_id'], request=event.request) - cs_data['permalink_url'] = RepoModel().get_commit_url( - repo, cs_data['raw_id'], request=event.request, - permalink=True) - urlified_message, issues_data, errors = process_patterns( - cs_data['message'], repo.repo_name) - cs_data['issues'] = issues_data - cs_data['message_html'] = urlify_commit_message( - cs_data['message'], repo.repo_name) - cs_data['message_html_title'] = chop_at_smart( - cs_data['message'], '\n', suffix_if_chopped='...') + cs_data["mentions"] = extract_mentioned_users(cs_data["message"]) + cs_data["reviewers"] = reviewers + cs_data["url"] = RepoModel().get_commit_url(repo, cs_data["raw_id"], request=event.request) + cs_data["permalink_url"] = RepoModel().get_commit_url( + repo, cs_data["raw_id"], request=event.request, permalink=True + ) + urlified_message, issues_data, errors = process_patterns(cs_data["message"], repo.repo_name) + cs_data["issues"] = issues_data + cs_data["message_html"] = urlify_commit_message(cs_data["message"], repo.repo_name) + cs_data["message_html_title"] = chop_at_smart(cs_data["message"], "\n", suffix_if_chopped="...") commits.append(cs_data) needed_commits.remove(commit_id) except Exception: - log.exception('Failed to extract commits data') + log.exception("Failed to extract commits data") # we don't send any commits when crash happens, only full list # matters we short circuit then. return [] # we failed to remove all needed_commits from all repositories if needed_commits: - raise ValueError(f'Unexpectedly not found {needed_commits} in all available repos {repos}') + raise ValueError(f"Unexpectedly not found {needed_commits} in all available repos {repos}") - missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits) + missing_commits = set(commit_ids) - set(c["raw_id"] for c in commits) if missing_commits: - log.error('Inconsistent repository state. ' - 'Missing commits: %s', ', '.join(missing_commits)) + log.error("Inconsistent repository state. " "Missing commits: %s", ", ".join(missing_commits)) return commits def _issues_as_dict(commits): - """ Helper function to serialize issues from commits """ + """Helper function to serialize issues from commits""" issues = {} for commit in commits: - for issue in commit['issues']: - issues[issue['id']] = issue + for issue in commit["issues"]: + issues[issue["id"]] = issue return issues @@ -156,33 +144,36 @@ class RepoEvent(RhodeCodeIntegrationEven Base class for events acting on a repository. """ - def __init__(self, repo, actor=None): + def __init__(self, repo, actor=None, context=None): """ :param repo: a :class:`Repository` instance """ - super().__init__(actor=actor) + super().__init__(actor=actor, context=context) self.repo = repo + self.context = self._context def as_dict(self): from rhodecode.model.repo import RepoModel + data = super().as_dict() extra_fields = collections.OrderedDict() for field in self.repo.extra_fields: extra_fields[field.field_key] = field.field_value - data.update({ - 'repo': { - 'repo_id': self.repo.repo_id, - 'repo_name': self.repo.repo_name, - 'repo_type': self.repo.repo_type, - 'url': RepoModel().get_url( - self.repo, request=self.request), - 'permalink_url': RepoModel().get_url( - self.repo, request=self.request, permalink=True), - 'extra_fields': extra_fields + data.update( + { + "repo": { + "repo_id": self.repo.repo_id, + "repo_name": self.repo.repo_name, + "repo_type": self.repo.repo_type, + "url": RepoModel().get_url(self.repo, request=self.request), + "permalink_url": RepoModel().get_url(self.repo, request=self.request, permalink=True), + "extra_fields": extra_fields, + }, + "context": self.context, } - }) + ) return data @@ -192,32 +183,32 @@ class RepoCommitCommentEvent(RepoEvent): on repository commit. """ - name = 'repo-commit-comment' - display_name = lazy_ugettext('repository commit comment') - description = lazy_ugettext('Event triggered after a comment was made ' - 'on commit inside a repository') + name = "repo-commit-comment" + display_name = lazy_ugettext("repository commit comment") + description = lazy_ugettext("Event triggered after a comment was made on commit inside a repository") - def __init__(self, repo, commit, comment): - super().__init__(repo) + def __init__(self, repo, commit, comment, context=None): + super().__init__(repo, context=context) self.commit = commit self.comment = comment def as_dict(self): data = super().as_dict() - data['commit'] = { - 'commit_id': self.commit.raw_id, - 'commit_message': self.commit.message, - 'commit_branch': self.commit.branch, + data["commit"] = { + "commit_id": self.commit.raw_id, + "commit_message": self.commit.message, + "commit_branch": self.commit.branch, } - data['comment'] = { - 'comment_id': self.comment.comment_id, - 'comment_text': self.comment.text, - 'comment_type': self.comment.comment_type, - 'comment_f_path': self.comment.f_path, - 'comment_line_no': self.comment.line_no, - 'comment_version': self.comment.last_version, + data["comment"] = { + "comment_id": self.comment.comment_id, + "comment_text": self.comment.text, + "comment_type": self.comment.comment_type, + "comment_f_path": self.comment.f_path, + "comment_line_no": self.comment.line_no, + "comment_version": self.comment.last_version, } + data["contex"] = self.context return data @@ -227,32 +218,32 @@ class RepoCommitCommentEditEvent(RepoEve on repository commit. """ - name = 'repo-commit-edit-comment' - display_name = lazy_ugettext('repository commit edit comment') - description = lazy_ugettext('Event triggered after a comment was edited ' - 'on commit inside a repository') + name = "repo-commit-edit-comment" + display_name = lazy_ugettext("repository commit edit comment") + description = lazy_ugettext("Event triggered after a comment was edited on commit inside a repository") - def __init__(self, repo, commit, comment): - super().__init__(repo) + def __init__(self, repo, commit, comment, context=None): + super().__init__(repo, context=context) self.commit = commit self.comment = comment def as_dict(self): data = super().as_dict() - data['commit'] = { - 'commit_id': self.commit.raw_id, - 'commit_message': self.commit.message, - 'commit_branch': self.commit.branch, + data["commit"] = { + "commit_id": self.commit.raw_id, + "commit_message": self.commit.message, + "commit_branch": self.commit.branch, } - data['comment'] = { - 'comment_id': self.comment.comment_id, - 'comment_text': self.comment.text, - 'comment_type': self.comment.comment_type, - 'comment_f_path': self.comment.f_path, - 'comment_line_no': self.comment.line_no, - 'comment_version': self.comment.last_version, + data["comment"] = { + "comment_id": self.comment.comment_id, + "comment_text": self.comment.text, + "comment_type": self.comment.comment_type, + "comment_f_path": self.comment.f_path, + "comment_line_no": self.comment.line_no, + "comment_version": self.comment.last_version, } + data["context"] = "context" return data @@ -261,9 +252,10 @@ class RepoPreCreateEvent(RepoEvent): An instance of this class is emitted as an :term:`event` before a repo is created. """ - name = 'repo-pre-create' - display_name = lazy_ugettext('repository pre create') - description = lazy_ugettext('Event triggered before repository is created') + + name = "repo-pre-create" + display_name = lazy_ugettext("repository pre create") + description = lazy_ugettext("Event triggered before repository is created") class RepoCreateEvent(RepoEvent): @@ -271,9 +263,10 @@ class RepoCreateEvent(RepoEvent): An instance of this class is emitted as an :term:`event` whenever a repo is created. """ - name = 'repo-create' - display_name = lazy_ugettext('repository created') - description = lazy_ugettext('Event triggered after repository was created') + + name = "repo-create" + display_name = lazy_ugettext("repository created") + description = lazy_ugettext("Event triggered after repository was created") class RepoPreDeleteEvent(RepoEvent): @@ -281,9 +274,10 @@ class RepoPreDeleteEvent(RepoEvent): An instance of this class is emitted as an :term:`event` whenever a repo is created. """ - name = 'repo-pre-delete' - display_name = lazy_ugettext('repository pre delete') - description = lazy_ugettext('Event triggered before a repository is deleted') + + name = "repo-pre-delete" + display_name = lazy_ugettext("repository pre delete") + description = lazy_ugettext("Event triggered before a repository is deleted") class RepoDeleteEvent(RepoEvent): @@ -291,43 +285,45 @@ class RepoDeleteEvent(RepoEvent): An instance of this class is emitted as an :term:`event` whenever a repo is created. """ - name = 'repo-delete' - display_name = lazy_ugettext('repository deleted') - description = lazy_ugettext('Event triggered after repository was deleted') + + name = "repo-delete" + display_name = lazy_ugettext("repository deleted") + description = lazy_ugettext("Event triggered after repository was deleted") class RepoVCSEvent(RepoEvent): """ Base class for events triggered by the VCS """ - name = '' - display_name = 'generic_vcs_event' - def __init__(self, repo_name, extras): + name = "" + display_name = "generic_vcs_event" + + def __init__(self, repo_name, extras, context=None): self.repo = Repository.get_by_repo_name(repo_name) if not self.repo: - raise Exception(f'repo by this name {repo_name} does not exist') + raise Exception(f"repo by this name {repo_name} does not exist") self.extras = extras - super().__init__(self.repo) + super().__init__(self.repo, context=context) @property def actor(self): - if self.extras.get('username'): - return User.get_by_username(self.extras['username']) + if self.extras.get("username"): + return User.get_by_username(self.extras["username"]) @property def actor_ip(self): - if self.extras.get('ip'): - return self.extras['ip'] + if self.extras.get("ip"): + return self.extras["ip"] @property def server_url(self): - if self.extras.get('server_url'): - return self.extras['server_url'] + if self.extras.get("server_url"): + return self.extras["server_url"] @property def request(self): - return self.extras.get('request') or self.get_request() + return self.extras.get("request") or self.get_request() class RepoPrePullEvent(RepoVCSEvent): @@ -335,9 +331,10 @@ class RepoPrePullEvent(RepoVCSEvent): An instance of this class is emitted as an :term:`event` before commits are pulled from a repo. """ - name = 'repo-pre-pull' - display_name = lazy_ugettext('repository pre pull') - description = lazy_ugettext('Event triggered before repository code is pulled') + + name = "repo-pre-pull" + display_name = lazy_ugettext("repository pre pull") + description = lazy_ugettext("Event triggered before repository code is pulled") class RepoPullEvent(RepoVCSEvent): @@ -345,9 +342,10 @@ class RepoPullEvent(RepoVCSEvent): An instance of this class is emitted as an :term:`event` after commits are pulled from a repo. """ - name = 'repo-pull' - display_name = lazy_ugettext('repository pull') - description = lazy_ugettext('Event triggered after repository code was pulled') + + name = "repo-pull" + display_name = lazy_ugettext("repository pull") + description = lazy_ugettext("Event triggered after repository code was pulled") class RepoPrePushEvent(RepoVCSEvent): @@ -355,10 +353,10 @@ class RepoPrePushEvent(RepoVCSEvent): An instance of this class is emitted as an :term:`event` before commits are pushed to a repo. """ - name = 'repo-pre-push' - display_name = lazy_ugettext('repository pre push') - description = lazy_ugettext('Event triggered before the code is ' - 'pushed to a repository') + + name = "repo-pre-push" + display_name = lazy_ugettext("repository pre push") + description = lazy_ugettext("Event triggered before the code is pushed to a repository") class RepoPushEvent(RepoVCSEvent): @@ -368,13 +366,13 @@ class RepoPushEvent(RepoVCSEvent): :param extras: (optional) dict of data from proxied VCS actions """ - name = 'repo-push' - display_name = lazy_ugettext('repository push') - description = lazy_ugettext('Event triggered after the code was ' - 'pushed to a repository') - def __init__(self, repo_name, pushed_commit_ids, extras): - super().__init__(repo_name, extras) + name = "repo-push" + display_name = lazy_ugettext("repository push") + description = lazy_ugettext("Event triggered after the code was pushed to a repository") + + def __init__(self, repo_name, pushed_commit_ids, extras, context=None): + super().__init__(repo_name, extras, context=context) self.pushed_commit_ids = pushed_commit_ids self.new_refs = extras.new_refs @@ -382,63 +380,48 @@ class RepoPushEvent(RepoVCSEvent): data = super().as_dict() def branch_url(branch_name): - return '{}/changelog?branch={}'.format( - data['repo']['url'], branch_name) + return "{}/changelog?branch={}".format(data["repo"]["url"], branch_name) def tag_url(tag_name): - return '{}/files/{}/'.format( - data['repo']['url'], tag_name) + return "{}/files/{}/".format(data["repo"]["url"], tag_name) - commits = _commits_as_dict( - self, commit_ids=self.pushed_commit_ids, repos=[self.repo]) + commits = _commits_as_dict(self, commit_ids=self.pushed_commit_ids, repos=[self.repo]) last_branch = None for commit in reversed(commits): - commit['branch'] = commit['branch'] or last_branch - last_branch = commit['branch'] + commit["branch"] = commit["branch"] or last_branch + last_branch = commit["branch"] issues = _issues_as_dict(commits) branches = set() tags = set() for commit in commits: - if commit['refs']['tags']: - for tag in commit['refs']['tags']: + if commit["refs"]["tags"]: + for tag in commit["refs"]["tags"]: tags.add(tag) - if commit['branch']: - branches.add(commit['branch']) + if commit["branch"]: + branches.add(commit["branch"]) # maybe we have branches in new_refs ? try: - branches = branches.union(set(self.new_refs['branches'])) + branches = branches.union(set(self.new_refs["branches"])) except Exception: pass - branches = [ - { - 'name': branch, - 'url': branch_url(branch) - } - for branch in branches - ] + branches = [{"name": branch, "url": branch_url(branch)} for branch in branches] # maybe we have branches in new_refs ? try: - tags = tags.union(set(self.new_refs['tags'])) + tags = tags.union(set(self.new_refs["tags"])) except Exception: pass - tags = [ - { - 'name': tag, - 'url': tag_url(tag) - } - for tag in tags - ] + tags = [{"name": tag, "url": tag_url(tag)} for tag in tags] - data['push'] = { - 'commits': commits, - 'issues': issues, - 'branches': branches, - 'tags': tags, + data["push"] = { + "commits": commits, + "issues": issues, + "branches": branches, + "tags": tags, } return data diff --git a/rhodecode/events/repo_group.py b/rhodecode/events/repo_group.py --- a/rhodecode/events/repo_group.py +++ b/rhodecode/events/repo_group.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -29,27 +29,31 @@ class RepoGroupEvent(RhodeCodeIntegratio """ Base class for events acting on a repository group. - :param repo: a :class:`RepositoryGroup` instance + :param repo_group: a :class:`RepositoryGroup` instance """ - def __init__(self, repo_group): - super().__init__() + def __init__(self, repo_group, context=None): + super().__init__(context=context) self.repo_group = repo_group + self.context = self._context def as_dict(self): data = super().as_dict() - data.update({ - 'repo_group': { - 'group_id': self.repo_group.group_id, - 'group_name': self.repo_group.group_name, - 'group_parent_id': self.repo_group.group_parent_id, - 'group_description': self.repo_group.group_description, - 'user_id': self.repo_group.user_id, - 'created_by': self.repo_group.user.username, - 'created_on': self.repo_group.created_on, - 'enable_locking': self.repo_group.enable_locking, + data.update( + { + "repo_group": { + "group_id": self.repo_group.group_id, + "group_name": self.repo_group.group_name, + "group_parent_id": self.repo_group.group_parent_id, + "group_description": self.repo_group.group_description, + "user_id": self.repo_group.user_id, + "created_by": self.repo_group.user.username, + "created_on": self.repo_group.created_on, + "enable_locking": self.repo_group.enable_locking, + }, + "context": self.context } - }) + ) return data @@ -58,9 +62,10 @@ class RepoGroupCreateEvent(RepoGroupEven An instance of this class is emitted as an :term:`event` whenever a repository group is created. """ - name = 'repo-group-create' - display_name = lazy_ugettext('repository group created') - description = lazy_ugettext('Event triggered after a repository group was created') + + name = "repo-group-create" + display_name = lazy_ugettext("repository group created") + description = lazy_ugettext("Event triggered after a repository group was created") class RepoGroupDeleteEvent(RepoGroupEvent): @@ -68,9 +73,10 @@ class RepoGroupDeleteEvent(RepoGroupEven An instance of this class is emitted as an :term:`event` whenever a repository group is deleted. """ - name = 'repo-group-delete' - display_name = lazy_ugettext('repository group deleted') - description = lazy_ugettext('Event triggered after a repository group was deleted') + + name = "repo-group-delete" + display_name = lazy_ugettext("repository group deleted") + description = lazy_ugettext("Event triggered after a repository group was deleted") class RepoGroupUpdateEvent(RepoGroupEvent): @@ -78,6 +84,7 @@ class RepoGroupUpdateEvent(RepoGroupEven An instance of this class is emitted as an :term:`event` whenever a repository group is updated. """ - name = 'repo-group-update' - display_name = lazy_ugettext('repository group update') - description = lazy_ugettext('Event triggered after a repository group was updated') + + name = "repo-group-update" + display_name = lazy_ugettext("repository group update") + description = lazy_ugettext("Event triggered after a repository group was updated") diff --git a/rhodecode/events/user.py b/rhodecode/events/user.py --- a/rhodecode/events/user.py +++ b/rhodecode/events/user.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,8 +21,7 @@ from zope.interface import implementer from rhodecode.translation import lazy_ugettext from rhodecode.events.base import RhodecodeEvent, RhodeCodeIntegrationEvent -from rhodecode.events.interfaces import ( - IUserRegistered, IUserPreCreate, IUserPreUpdate) +from rhodecode.events.interfaces import IUserRegistered, IUserPreCreate, IUserPreUpdate log = logging.getLogger(__name__) @@ -33,8 +32,9 @@ class UserRegistered(RhodeCodeIntegratio An instance of this class is emitted as an :term:`event` whenever a user account is registered. """ - name = 'user-register' - display_name = lazy_ugettext('user registered') + + name = "user-register" + display_name = lazy_ugettext("user registered") def __init__(self, user, session): super().__init__() @@ -48,8 +48,9 @@ class UserPreCreate(RhodeCodeIntegration An instance of this class is emitted as an :term:`event` before a new user object is created. """ - name = 'user-pre-create' - display_name = lazy_ugettext('user pre create') + + name = "user-pre-create" + display_name = lazy_ugettext("user pre create") def __init__(self, user_data): super().__init__() @@ -62,8 +63,9 @@ class UserPostCreate(RhodeCodeIntegratio An instance of this class is emitted as an :term:`event` after a new user object is created. """ - name = 'user-post-create' - display_name = lazy_ugettext('user post create') + + name = "user-post-create" + display_name = lazy_ugettext("user post create") def __init__(self, user_data): super().__init__() @@ -76,8 +78,9 @@ class UserPreUpdate(RhodeCodeIntegration An instance of this class is emitted as an :term:`event` before a user object is updated. """ - name = 'user-pre-update' - display_name = lazy_ugettext('user pre update') + + name = "user-pre-update" + display_name = lazy_ugettext("user pre update") def __init__(self, user, user_data): super().__init__() @@ -87,8 +90,8 @@ class UserPreUpdate(RhodeCodeIntegration class UserPermissionsChange(RhodecodeEvent): """ - This event should be triggered on an event that permissions of user might changed. - Currently this should be triggered on: + This event should be triggered on an event that permissions of user might be changed. + Currently, this should be triggered on: - user added/removed from user group - repo permissions changed @@ -96,9 +99,11 @@ class UserPermissionsChange(RhodecodeEve - user group permissions changed """ - name = 'user-permissions-change' - display_name = lazy_ugettext('user permissions change') + + name = "user-permissions-change" + display_name = lazy_ugettext("user permissions change") - def __init__(self, user_ids): - super().__init__() + def __init__(self, user_ids, context=None): + super().__init__(context=context) self.user_ids = user_ids + self.context = self._context diff --git a/rhodecode/forms/__init__.py b/rhodecode/forms/__init__.py --- a/rhodecode/forms/__init__.py +++ b/rhodecode/forms/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/__init__.py b/rhodecode/integrations/__init__.py --- a/rhodecode/integrations/__init__.py +++ b/rhodecode/integrations/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/registry.py b/rhodecode/integrations/registry.py --- a/rhodecode/integrations/registry.py +++ b/rhodecode/integrations/registry.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/routes.py b/rhodecode/integrations/routes.py --- a/rhodecode/integrations/routes.py +++ b/rhodecode/integrations/routes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/schema.py b/rhodecode/integrations/schema.py --- a/rhodecode/integrations/schema.py +++ b/rhodecode/integrations/schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/tests/__init__.py b/rhodecode/integrations/tests/__init__.py --- a/rhodecode/integrations/tests/__init__.py +++ b/rhodecode/integrations/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/tests/test_integrations.py b/rhodecode/integrations/tests/test_integrations.py --- a/rhodecode/integrations/tests/test_integrations.py +++ b/rhodecode/integrations/tests/test_integrations.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/__init__.py b/rhodecode/integrations/types/__init__.py --- a/rhodecode/integrations/types/__init__.py +++ b/rhodecode/integrations/types/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/base.py b/rhodecode/integrations/types/base.py --- a/rhodecode/integrations/types/base.py +++ b/rhodecode/integrations/types/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/email.py b/rhodecode/integrations/types/email.py --- a/rhodecode/integrations/types/email.py +++ b/rhodecode/integrations/types/email.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/handlers/__init__.py b/rhodecode/integrations/types/handlers/__init__.py --- a/rhodecode/integrations/types/handlers/__init__.py +++ b/rhodecode/integrations/types/handlers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/handlers/slack.py b/rhodecode/integrations/types/handlers/slack.py --- a/rhodecode/integrations/types/handlers/slack.py +++ b/rhodecode/integrations/types/handlers/slack.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/handlers/webhook.py b/rhodecode/integrations/types/handlers/webhook.py --- a/rhodecode/integrations/types/handlers/webhook.py +++ b/rhodecode/integrations/types/handlers/webhook.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/slack.py b/rhodecode/integrations/types/slack.py --- a/rhodecode/integrations/types/slack.py +++ b/rhodecode/integrations/types/slack.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/types/webhook.py b/rhodecode/integrations/types/webhook.py --- a/rhodecode/integrations/types/webhook.py +++ b/rhodecode/integrations/types/webhook.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/integrations/views.py b/rhodecode/integrations/views.py --- a/rhodecode/integrations/views.py +++ b/rhodecode/integrations/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/__init__.py b/rhodecode/lib/__init__.py --- a/rhodecode/lib/__init__.py +++ b/rhodecode/lib/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/_vendor/__init__.py b/rhodecode/lib/_vendor/__init__.py --- a/rhodecode/lib/_vendor/__init__.py +++ b/rhodecode/lib/_vendor/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/_vendor/authomatic/six.py b/rhodecode/lib/_vendor/authomatic/six.py --- a/rhodecode/lib/_vendor/authomatic/six.py +++ b/rhodecode/lib/_vendor/authomatic/six.py @@ -1,7 +1,5 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson +"""Utilities for writing code that runs on Python 2 and 3"""# Copyright (C) 2010-2015 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/rhodecode/lib/action_parser.py b/rhodecode/lib/action_parser.py --- a/rhodecode/lib/action_parser.py +++ b/rhodecode/lib/action_parser.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/api_utils.py b/rhodecode/lib/api_utils.py --- a/rhodecode/lib/api_utils.py +++ b/rhodecode/lib/api_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/audit_logger.py b/rhodecode/lib/audit_logger.py --- a/rhodecode/lib/audit_logger.py +++ b/rhodecode/lib/audit_logger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/caching_query.py b/rhodecode/lib/caching_query.py --- a/rhodecode/lib/caching_query.py +++ b/rhodecode/lib/caching_query.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/celerylib/__init__.py b/rhodecode/lib/celerylib/__init__.py --- a/rhodecode/lib/celerylib/__init__.py +++ b/rhodecode/lib/celerylib/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/celerylib/loader.py b/rhodecode/lib/celerylib/loader.py --- a/rhodecode/lib/celerylib/loader.py +++ b/rhodecode/lib/celerylib/loader.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/celerylib/scheduler.py b/rhodecode/lib/celerylib/scheduler.py --- a/rhodecode/lib/celerylib/scheduler.py +++ b/rhodecode/lib/celerylib/scheduler.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/celerylib/utils.py b/rhodecode/lib/celerylib/utils.py --- a/rhodecode/lib/celerylib/utils.py +++ b/rhodecode/lib/celerylib/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/channelstream.py b/rhodecode/lib/channelstream.py --- a/rhodecode/lib/channelstream.py +++ b/rhodecode/lib/channelstream.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/codeblocks.py b/rhodecode/lib/codeblocks.py --- a/rhodecode/lib/codeblocks.py +++ b/rhodecode/lib/codeblocks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -478,7 +478,9 @@ class DiffSet(object): log.debug('rendering diff for %r', patch['filename']) source_filename = patch['original_filename'] + source_filename_bytes = patch['original_filename_bytes'] target_filename = patch['filename'] + target_filename_bytes = patch['filename_bytes'] source_lexer = plain_text_lexer target_lexer = plain_text_lexer @@ -491,12 +493,12 @@ class DiffSet(object): if (source_filename and patch['operation'] in ('D', 'M') and source_filename not in self.source_nodes): self.source_nodes[source_filename] = ( - self.source_node_getter(source_filename)) + self.source_node_getter(source_filename_bytes)) if (target_filename and patch['operation'] in ('A', 'M') and target_filename not in self.target_nodes): self.target_nodes[target_filename] = ( - self.target_node_getter(target_filename)) + self.target_node_getter(target_filename_bytes)) elif hl_mode == self.HL_FAST: source_lexer = self._get_lexer_for_filename(source_filename) @@ -558,6 +560,7 @@ class DiffSet(object): }) file_chunks = patch['chunks'][1:] + for i, hunk in enumerate(file_chunks, 1): hunkbit = self.parse_hunk(hunk, source_file, target_file) hunkbit.source_file_path = source_file_path @@ -593,12 +596,13 @@ class DiffSet(object): return filediff def parse_hunk(self, hunk, source_file, target_file): + result = AttributeDict(dict( source_start=hunk['source_start'], source_length=hunk['source_length'], target_start=hunk['target_start'], target_length=hunk['target_length'], - section_header=hunk['section_header'], + section_header=safe_str(hunk['section_header']), lines=[], )) before, after = [], [] diff --git a/rhodecode/lib/colander_utils.py b/rhodecode/lib/colander_utils.py --- a/rhodecode/lib/colander_utils.py +++ b/rhodecode/lib/colander_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/colored_formatter.py b/rhodecode/lib/colored_formatter.py --- a/rhodecode/lib/colored_formatter.py +++ b/rhodecode/lib/colored_formatter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/config_utils.py b/rhodecode/lib/config_utils.py --- a/rhodecode/lib/config_utils.py +++ b/rhodecode/lib/config_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/datelib.py b/rhodecode/lib/datelib.py --- a/rhodecode/lib/datelib.py +++ b/rhodecode/lib/datelib.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -307,7 +307,7 @@ class DbManage(object): def create_test_admin_and_users(self): log.info('creating admin and regular test users') - from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \ + from rhodecode.bootstrap import TEST_USER_ADMIN_LOGIN, \ TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \ TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \ TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \ @@ -328,8 +328,6 @@ class DbManage(object): and disables dotencode """ settings_model = SettingsModel(sa=self.sa) - from rhodecode.lib.vcs.backends.hg import largefiles_store - from rhodecode.lib.vcs.backends.git import lfs_store # Build HOOKS hooks = [ @@ -360,24 +358,6 @@ class DbManage(object): largefiles.ui_value = '' self.sa.add(largefiles) - # set default largefiles cache dir, defaults to - # /repo_store_location/.cache/largefiles - largefiles = RhodeCodeUi() - largefiles.ui_section = 'largefiles' - largefiles.ui_key = 'usercache' - largefiles.ui_value = largefiles_store(repo_store_path) - - self.sa.add(largefiles) - - # set default lfs cache dir, defaults to - # /repo_store_location/.cache/lfs_store - lfsstore = RhodeCodeUi() - lfsstore.ui_section = 'vcs_git_lfs' - lfsstore.ui_key = 'store_location' - lfsstore.ui_value = lfs_store(repo_store_path) - - self.sa.add(lfsstore) - # enable hgevolve disabled by default hgevolve = RhodeCodeUi() hgevolve.ui_section = 'extensions' diff --git a/rhodecode/lib/dbmigrate/__init__.py b/rhodecode/lib/dbmigrate/__init__.py --- a/rhodecode/lib/dbmigrate/__init__.py +++ b/rhodecode/lib/dbmigrate/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/__init__.py b/rhodecode/lib/dbmigrate/schema/__init__.py --- a/rhodecode/lib/dbmigrate/schema/__init__.py +++ b/rhodecode/lib/dbmigrate/schema/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_1_0.py b/rhodecode/lib/dbmigrate/schema/db_1_1_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_1_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_1_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_4_0.py b/rhodecode/lib/dbmigrate/schema/db_1_4_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_4_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_4_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_5_0.py b/rhodecode/lib/dbmigrate/schema/db_1_5_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_5_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_5_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_5_2.py b/rhodecode/lib/dbmigrate/schema/db_1_5_2.py --- a/rhodecode/lib/dbmigrate/schema/db_1_5_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_5_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_6_0.py b/rhodecode/lib/dbmigrate/schema/db_1_6_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_6_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_6_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_7_0.py b/rhodecode/lib/dbmigrate/schema/db_1_7_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_7_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_7_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_1_8_0.py b/rhodecode/lib/dbmigrate/schema/db_1_8_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_8_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_8_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_0_0.py b/rhodecode/lib/dbmigrate/schema/db_2_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_2_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_0_1.py b/rhodecode/lib/dbmigrate/schema/db_2_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_2_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_0_2.py b/rhodecode/lib/dbmigrate/schema/db_2_0_2.py --- a/rhodecode/lib/dbmigrate/schema/db_2_0_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_0_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_1_0.py b/rhodecode/lib/dbmigrate/schema/db_2_1_0.py --- a/rhodecode/lib/dbmigrate/schema/db_2_1_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_1_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_2_0.py b/rhodecode/lib/dbmigrate/schema/db_2_2_0.py --- a/rhodecode/lib/dbmigrate/schema/db_2_2_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_2_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_2_3.py b/rhodecode/lib/dbmigrate/schema/db_2_2_3.py --- a/rhodecode/lib/dbmigrate/schema/db_2_2_3.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_2_3.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_3_0_0.py b/rhodecode/lib/dbmigrate/schema/db_2_3_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_2_3_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_3_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_3_0_1.py b/rhodecode/lib/dbmigrate/schema/db_2_3_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_2_3_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_3_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_2_3_0_2.py b/rhodecode/lib/dbmigrate/schema/db_2_3_0_2.py --- a/rhodecode/lib/dbmigrate/schema/db_2_3_0_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_2_3_0_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_0_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_0_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_0_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_0_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_0_0_1.py b/rhodecode/lib/dbmigrate/schema/db_3_0_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_3_0_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_0_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_1_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_1_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_1_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_1_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_1_0_1.py b/rhodecode/lib/dbmigrate/schema/db_3_1_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_3_1_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_1_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py b/rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_11_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_11_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_11_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_11_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_13_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_13_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_13_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_13_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_16_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_16_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_16_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_16_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_16_0_1.py b/rhodecode/lib/dbmigrate/schema/db_4_16_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_4_16_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_16_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_16_0_2.py b/rhodecode/lib/dbmigrate/schema/db_4_16_0_2.py --- a/rhodecode/lib/dbmigrate/schema/db_4_16_0_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_16_0_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_18_0_1.py b/rhodecode/lib/dbmigrate/schema/db_4_18_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_4_18_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_18_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py b/rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py --- a/rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_20_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_20_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_20_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_20_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_4_0_1.py b/rhodecode/lib/dbmigrate/schema/db_4_4_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_4_4_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_4_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py b/rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py --- a/rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py b/rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py --- a/rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/schema/db_4_9_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_9_0_0.py --- a/rhodecode/lib/dbmigrate/schema/db_4_9_0_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_4_9_0_0.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/utils.py b/rhodecode/lib/dbmigrate/utils.py --- a/rhodecode/lib/dbmigrate/utils.py +++ b/rhodecode/lib/dbmigrate/utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/dbmigrate/versions/__init__.py b/rhodecode/lib/dbmigrate/versions/__init__.py --- a/rhodecode/lib/dbmigrate/versions/__init__.py +++ b/rhodecode/lib/dbmigrate/versions/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -455,7 +455,9 @@ class DiffProcessor(object): return arg for chunk in self._diff.chunks(): + bytes_head = chunk.header head = chunk.header_as_str + log.debug('parsing diff chunk %r', chunk) raw_diff = chunk.raw @@ -598,10 +600,17 @@ class DiffProcessor(object): chunks.insert(0, frag) - original_filename = safe_str(head['a_path']) + original_filename = head['a_path'] + original_filename_bytes = bytes_head['a_path'] + + filename = head['b_path'] + filename_bytes = bytes_head['b_path'] + _files.append({ 'original_filename': original_filename, - 'filename': safe_str(head['b_path']), + 'original_filename_bytes': original_filename_bytes, + 'filename': filename, + 'filename_bytes': filename_bytes, 'old_revision': head['a_blob_id'], 'new_revision': head['b_blob_id'], 'chunks': chunks, diff --git a/rhodecode/lib/enc_utils.py b/rhodecode/lib/enc_utils.py --- a/rhodecode/lib/enc_utils.py +++ b/rhodecode/lib/enc_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/encrypt.py b/rhodecode/lib/encrypt.py --- a/rhodecode/lib/encrypt.py +++ b/rhodecode/lib/encrypt.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/exc_tracking.py b/rhodecode/lib/exc_tracking.py --- a/rhodecode/lib/exc_tracking.py +++ b/rhodecode/lib/exc_tracking.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,8 +20,7 @@ Set of custom exceptions used in RhodeCode """ -from webob.exc import HTTPClientError -from pyramid.httpexceptions import HTTPBadGateway +from pyramid.httpexceptions import HTTPBadGateway, HTTPClientError class LdapUsernameError(Exception): @@ -102,12 +101,7 @@ class HTTPRequirementError(HTTPClientErr self.args = (message, ) -class ClientNotSupportedError(HTTPRequirementError): - title = explanation = 'Client Not Supported' - reason = None - - -class HTTPLockedRC(HTTPClientError): +class HTTPLockedRepo(HTTPClientError): """ Special Exception For locked Repos in RhodeCode, the return code can be overwritten by _code keyword argument passed into constructors @@ -131,14 +125,13 @@ class HTTPBranchProtected(HTTPClientErro Special Exception For Indicating that branch is protected in RhodeCode, the return code can be overwritten by _code keyword argument passed into constructors """ - code = 403 title = explanation = 'Branch Protected' reason = None - def __init__(self, message, *args, **kwargs): - self.title = self.explanation = message - super().__init__(*args, **kwargs) - self.args = (message, ) + +class ClientNotSupported(HTTPRequirementError): + title = explanation = 'Client Not Supported' + reason = None class IMCCommitError(Exception): diff --git a/rhodecode/lib/ext_json_renderer.py b/rhodecode/lib/ext_json_renderer.py --- a/rhodecode/lib/ext_json_renderer.py +++ b/rhodecode/lib/ext_json_renderer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/feedgenerator/__init__.py b/rhodecode/lib/feedgenerator/__init__.py --- a/rhodecode/lib/feedgenerator/__init__.py +++ b/rhodecode/lib/feedgenerator/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/graphmod.py b/rhodecode/lib/graphmod.py --- a/rhodecode/lib/graphmod.py +++ b/rhodecode/lib/graphmod.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/hash_utils.py b/rhodecode/lib/hash_utils.py --- a/rhodecode/lib/hash_utils.py +++ b/rhodecode/lib/hash_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -51,7 +51,6 @@ from pygments.formatters.html import Htm from pygments.lexers import ( get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype) -from pyramid.threadlocal import get_current_request from tempita import looper from webhelpers2.html import literal, HTML, escape from webhelpers2.html._autolink import _auto_link_urls @@ -89,6 +88,7 @@ from rhodecode.lib.utils2 import ( str2bool, get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, AttributeDict, safe_int, md5, md5_safe, get_host_info) +from rhodecode.lib.pyramid_utils import get_current_request from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit diff --git a/rhodecode/lib/hook_daemon/__init__.py b/rhodecode/lib/hook_daemon/__init__.py --- a/rhodecode/lib/hook_daemon/__init__.py +++ b/rhodecode/lib/hook_daemon/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/hook_daemon/base.py b/rhodecode/lib/hook_daemon/base.py --- a/rhodecode/lib/hook_daemon/base.py +++ b/rhodecode/lib/hook_daemon/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -16,13 +16,14 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -import os -import time import logging +import traceback -from rhodecode.lib.config_utils import get_config +from rhodecode.model import meta +from rhodecode.lib import hooks_base +from rhodecode.lib.utils2 import AttributeDict +from rhodecode.lib.exceptions import HTTPLockedRepo, HTTPBranchProtected -from rhodecode.lib.svn_txn_utils import get_txn_id_from_store log = logging.getLogger(__name__) @@ -42,48 +43,82 @@ class BaseHooksCallbackDaemon: log.debug('Exiting `%s` callback daemon', self.__class__.__name__) -class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon): +class Hooks(object): + """ + Exposes the hooks module for calling them using the local HooksModuleCallbackDaemon + """ + def __init__(self, request=None, log_prefix=''): + self.log_prefix = log_prefix + self.request = request - def __init__(self, module): - super().__init__() - self.hooks_module = module + def repo_size(self, extras): + log.debug("%sCalled repo_size of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.repo_size, extras) - def __repr__(self): - return f'HooksModuleCallbackDaemon(hooks_module={self.hooks_module})' - + def pre_pull(self, extras): + log.debug("%sCalled pre_pull of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.pre_pull, extras) -def prepare_callback_daemon(extras, protocol, host, txn_id=None): + def post_pull(self, extras): + log.debug("%sCalled post_pull of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.post_pull, extras) + + def pre_push(self, extras): + log.debug("%sCalled pre_push of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.pre_push, extras) - match protocol: - case 'http': - from rhodecode.lib.hook_daemon.http_hooks_deamon import HttpHooksCallbackDaemon - port = 0 - if txn_id: - # read txn-id to re-use the PORT for callback daemon - repo_path = os.path.join(extras['repo_store'], extras['repository']) - txn_details = get_txn_id_from_store(repo_path, txn_id) - port = txn_details.get('port', 0) + def post_push(self, extras): + log.debug("%sCalled post_push of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.post_push, extras) + + def _call_hook(self, hook, extras): + extras = AttributeDict(extras) + _server_url = extras['server_url'] - callback_daemon = HttpHooksCallbackDaemon( - txn_id=txn_id, host=host, port=port) - case 'celery': - from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon - callback_daemon = CeleryHooksCallbackDaemon(get_config(extras['config'])) - case 'local': - from rhodecode.lib.hook_daemon.hook_module import Hooks - callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__) - case _: - log.error('Unsupported callback daemon protocol "%s"', protocol) - raise Exception('Unsupported callback daemon protocol.') + extras.request = self.request + try: + result = hook(extras) + if result is None: + raise Exception(f'Failed to obtain hook result from func: {hook}') + except HTTPBranchProtected as error: + # Those special cases don't need error reporting. It's a case of + # locked repo or protected branch + result = AttributeDict({ + 'status': error.code, + 'output': error.explanation + }) + except HTTPLockedRepo as error: + # Those special cases don't need error reporting. It's a case of + # locked repo or protected branch + result = AttributeDict({ + 'status': error.code, + 'output': error.explanation + }) + except Exception as error: + # locked needs different handling since we need to also + # handle PULL operations + log.exception('%sException when handling hook %s', self.log_prefix, hook) + exc_tb = traceback.format_exc() + error_args = error.args + return { + 'status': 128, + 'output': '', + 'exception': type(error).__name__, + 'exception_traceback': exc_tb, + 'exception_args': error_args, + } + finally: + meta.Session.remove() - extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '') - extras['task_queue'] = getattr(callback_daemon, 'task_queue', '') - extras['task_backend'] = getattr(callback_daemon, 'task_backend', '') - extras['hooks_protocol'] = protocol - extras['time'] = time.time() + log.debug('%sGot hook call response %s', self.log_prefix, result) + return { + 'status': result.status, + 'output': result.output, + } - # register txn_id - extras['txn_id'] = txn_id - log.debug('Prepared a callback daemon: %s', - callback_daemon.__class__.__name__) - return callback_daemon, extras + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + diff --git a/rhodecode/lib/hook_daemon/celery_hooks_deamon.py b/rhodecode/lib/hook_daemon/celery_hooks_deamon.py --- a/rhodecode/lib/hook_daemon/celery_hooks_deamon.py +++ b/rhodecode/lib/hook_daemon/celery_hooks_deamon.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,12 +22,16 @@ from rhodecode.lib.hook_daemon.base impo class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon): """ Context manger for achieving a compatibility with celery backend + It is calling a call to vcsserver, where it uses HooksCeleryClient to actually call a task from + + f'rhodecode.lib.celerylib.tasks.{method}' + """ - def __init__(self, config): - # TODO: replace this with settings bootstrapped... - self.task_queue = config.get('app:main', 'celery.broker_url') - self.task_backend = config.get('app:main', 'celery.result_backend') + def __init__(self, broker_url, result_backend): + super().__init__() + self.broker_url = broker_url + self.result_backend = result_backend def __repr__(self): - return f'CeleryHooksCallbackDaemon(task_queue={self.task_queue}, task_backend={self.task_backend})' + return f'CeleryHooksCallbackDaemon(broker_url={self.broker_url}, result_backend={self.result_backend})' diff --git a/rhodecode/lib/hook_daemon/hook_module.py b/rhodecode/lib/hook_daemon/hook_module.py --- a/rhodecode/lib/hook_daemon/hook_module.py +++ b/rhodecode/lib/hook_daemon/hook_module.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -17,88 +17,18 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging -import traceback -from rhodecode.model import meta - -from rhodecode.lib import hooks_base -from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected -from rhodecode.lib.utils2 import AttributeDict +from rhodecode.lib.hook_daemon.base import BaseHooksCallbackDaemon log = logging.getLogger(__name__) -class Hooks(object): - """ - Exposes the hooks for remote callbacks - """ - def __init__(self, request=None, log_prefix=''): - self.log_prefix = log_prefix - self.request = request - - def repo_size(self, extras): - log.debug("%sCalled repo_size of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.repo_size, extras) - - def pre_pull(self, extras): - log.debug("%sCalled pre_pull of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.pre_pull, extras) - - def post_pull(self, extras): - log.debug("%sCalled post_pull of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.post_pull, extras) - - def pre_push(self, extras): - log.debug("%sCalled pre_push of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.pre_push, extras) - - def post_push(self, extras): - log.debug("%sCalled post_push of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.post_push, extras) - - def _call_hook(self, hook, extras): - extras = AttributeDict(extras) - _server_url = extras['server_url'] - - extras.request = self.request +class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon): - try: - result = hook(extras) - if result is None: - raise Exception(f'Failed to obtain hook result from func: {hook}') - except HTTPBranchProtected as error: - # Those special cases don't need error reporting. It's a case of - # locked repo or protected branch - result = AttributeDict({ - 'status': error.code, - 'output': error.explanation - }) - except (HTTPLockedRC, Exception) as error: - # locked needs different handling since we need to also - # handle PULL operations - exc_tb = '' - if not isinstance(error, HTTPLockedRC): - exc_tb = traceback.format_exc() - log.exception('%sException when handling hook %s', self.log_prefix, hook) - error_args = error.args - return { - 'status': 128, - 'output': '', - 'exception': type(error).__name__, - 'exception_traceback': exc_tb, - 'exception_args': error_args, - } - finally: - meta.Session.remove() + def __init__(self, module): + super().__init__() + self.hooks_module = module - log.debug('%sGot hook call response %s', self.log_prefix, result) - return { - 'status': result.status, - 'output': result.output, - } + def __repr__(self): + return f'HooksModuleCallbackDaemon(hooks_module={self.hooks_module})' - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - pass diff --git a/rhodecode/lib/hook_daemon/http_hooks_deamon.py b/rhodecode/lib/hook_daemon/http_hooks_deamon.py deleted file mode 100644 --- a/rhodecode/lib/hook_daemon/http_hooks_deamon.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -import os -import logging -import traceback -import threading -import socket -import msgpack -import gevent - -from http.server import BaseHTTPRequestHandler -from socketserver import TCPServer - -from rhodecode.model import meta -from rhodecode.lib.ext_json import json -from rhodecode.lib import rc_cache -from rhodecode.lib.svn_txn_utils import get_txn_id_data_key -from rhodecode.lib.hook_daemon.hook_module import Hooks - -log = logging.getLogger(__name__) - - -class HooksHttpHandler(BaseHTTPRequestHandler): - - JSON_HOOKS_PROTO = 'json.v1' - MSGPACK_HOOKS_PROTO = 'msgpack.v1' - # starting with RhodeCode 5.0.0 MsgPack is the default, prior it used json - DEFAULT_HOOKS_PROTO = MSGPACK_HOOKS_PROTO - - @classmethod - def serialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO): - if proto == cls.MSGPACK_HOOKS_PROTO: - return msgpack.packb(data) - return json.dumps(data) - - @classmethod - def deserialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO): - if proto == cls.MSGPACK_HOOKS_PROTO: - return msgpack.unpackb(data) - return json.loads(data) - - def do_POST(self): - hooks_proto, method, extras = self._read_request() - log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto) - - txn_id = getattr(self.server, 'txn_id', None) - if txn_id: - log.debug('Computing TXN_ID based on `%s`:`%s`', - extras['repository'], extras['txn_id']) - computed_txn_id = rc_cache.utils.compute_key_from_params( - extras['repository'], extras['txn_id']) - if txn_id != computed_txn_id: - raise Exception( - 'TXN ID fail: expected {} got {} instead'.format( - txn_id, computed_txn_id)) - - request = getattr(self.server, 'request', None) - try: - hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address)) - result = self._call_hook_method(hooks, method, extras) - - except Exception as e: - exc_tb = traceback.format_exc() - result = { - 'exception': e.__class__.__name__, - 'exception_traceback': exc_tb, - 'exception_args': e.args - } - self._write_response(hooks_proto, result) - - def _read_request(self): - length = int(self.headers['Content-Length']) - # respect sent headers, fallback to OLD proto for compatability - hooks_proto = self.headers.get('rc-hooks-protocol') or self.JSON_HOOKS_PROTO - if hooks_proto == self.MSGPACK_HOOKS_PROTO: - # support for new vcsserver msgpack based protocol hooks - body = self.rfile.read(length) - data = self.deserialize_data(body) - else: - body = self.rfile.read(length) - data = self.deserialize_data(body) - - return hooks_proto, data['method'], data['extras'] - - def _write_response(self, hooks_proto, result): - self.send_response(200) - if hooks_proto == self.MSGPACK_HOOKS_PROTO: - self.send_header("Content-type", "application/msgpack") - self.end_headers() - data = self.serialize_data(result) - self.wfile.write(data) - else: - self.send_header("Content-type", "text/json") - self.end_headers() - data = self.serialize_data(result) - self.wfile.write(data) - - def _call_hook_method(self, hooks, method, extras): - try: - result = getattr(hooks, method)(extras) - finally: - meta.Session.remove() - return result - - def log_message(self, format, *args): - """ - This is an overridden method of BaseHTTPRequestHandler which logs using - a logging library instead of writing directly to stderr. - """ - - message = format % args - - log.debug( - "HOOKS: client=%s - - [%s] %s", self.client_address, - self.log_date_time_string(), message) - - -class ThreadedHookCallbackDaemon(object): - - _callback_thread = None - _daemon = None - _done = False - use_gevent = False - - def __init__(self, txn_id=None, host=None, port=None): - self._prepare(txn_id=txn_id, host=host, port=port) - if self.use_gevent: - self._run_func = self._run_gevent - self._stop_func = self._stop_gevent - else: - self._run_func = self._run - self._stop_func = self._stop - - def __enter__(self): - log.debug('Running `%s` callback daemon', self.__class__.__name__) - self._run_func() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - log.debug('Exiting `%s` callback daemon', self.__class__.__name__) - self._stop_func() - - def _prepare(self, txn_id=None, host=None, port=None): - raise NotImplementedError() - - def _run(self): - raise NotImplementedError() - - def _stop(self): - raise NotImplementedError() - - def _run_gevent(self): - raise NotImplementedError() - - def _stop_gevent(self): - raise NotImplementedError() - - -class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon): - """ - Context manager which will run a callback daemon in a background thread. - """ - - hooks_uri = None - - # From Python docs: Polling reduces our responsiveness to a shutdown - # request and wastes cpu at all other times. - POLL_INTERVAL = 0.01 - - use_gevent = False - - def __repr__(self): - return f'HttpHooksCallbackDaemon(hooks_uri={self.hooks_uri})' - - @property - def _hook_prefix(self): - return f'HOOKS: {self.hooks_uri} ' - - def get_hostname(self): - return socket.gethostname() or '127.0.0.1' - - def get_available_port(self, min_port=20000, max_port=65535): - from rhodecode.lib.utils2 import get_available_port as _get_port - return _get_port(min_port, max_port) - - def _prepare(self, txn_id=None, host=None, port=None): - from pyramid.threadlocal import get_current_request - - if not host or host == "*": - host = self.get_hostname() - if not port: - port = self.get_available_port() - - server_address = (host, port) - self.hooks_uri = f'{host}:{port}' - self.txn_id = txn_id - self._done = False - - log.debug( - "%s Preparing HTTP callback daemon registering hook object: %s", - self._hook_prefix, HooksHttpHandler) - - self._daemon = TCPServer(server_address, HooksHttpHandler) - # inject transaction_id for later verification - self._daemon.txn_id = self.txn_id - - # pass the WEB app request into daemon - self._daemon.request = get_current_request() - - def _run(self): - log.debug("Running thread-based loop of callback daemon in background") - callback_thread = threading.Thread( - target=self._daemon.serve_forever, - kwargs={'poll_interval': self.POLL_INTERVAL}) - callback_thread.daemon = True - callback_thread.start() - self._callback_thread = callback_thread - - def _run_gevent(self): - log.debug("Running gevent-based loop of callback daemon in background") - # create a new greenlet for the daemon's serve_forever method - callback_greenlet = gevent.spawn( - self._daemon.serve_forever, - poll_interval=self.POLL_INTERVAL) - - # store reference to greenlet - self._callback_greenlet = callback_greenlet - - # switch to this greenlet - gevent.sleep(0.01) - - def _stop(self): - log.debug("Waiting for background thread to finish.") - self._daemon.shutdown() - self._callback_thread.join() - self._daemon = None - self._callback_thread = None - if self.txn_id: - #TODO: figure out the repo_path... - repo_path = '' - txn_id_file = get_txn_id_data_key(repo_path, self.txn_id) - log.debug('Cleaning up TXN ID %s', txn_id_file) - if os.path.isfile(txn_id_file): - os.remove(txn_id_file) - - log.debug("Background thread done.") - - def _stop_gevent(self): - log.debug("Waiting for background greenlet to finish.") - - # if greenlet exists and is running - if self._callback_greenlet and not self._callback_greenlet.dead: - # shutdown daemon if it exists - if self._daemon: - self._daemon.shutdown() - - # kill the greenlet - self._callback_greenlet.kill() - - self._daemon = None - self._callback_greenlet = None - - if self.txn_id: - #TODO: figure out the repo_path... - repo_path = '' - txn_id_file = get_txn_id_data_key(repo_path, self.txn_id) - log.debug('Cleaning up TXN ID %s', txn_id_file) - if os.path.isfile(txn_id_file): - os.remove(txn_id_file) - - log.debug("Background greenlet done.") diff --git a/rhodecode/lib/hook_daemon/utils.py b/rhodecode/lib/hook_daemon/utils.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/hook_daemon/utils.py @@ -0,0 +1,61 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import time +import logging + +from rhodecode.lib.config_utils import get_app_config_lightweight + +from rhodecode.lib.hook_daemon.base import Hooks +from rhodecode.lib.hook_daemon.hook_module import HooksModuleCallbackDaemon +from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon +from rhodecode.lib.type_utils import str2bool + +log = logging.getLogger(__name__) + + + +def prepare_callback_daemon(extras, protocol: str, txn_id=None): + hooks_config = {} + match protocol: + case 'celery': + config = get_app_config_lightweight(extras['config']) + + broker_url = config.get('celery.broker_url') + result_backend = config.get('celery.result_backend') + + hooks_config = { + 'broker_url': broker_url, + 'result_backend': result_backend, + } + + callback_daemon = CeleryHooksCallbackDaemon(broker_url, result_backend) + case 'local': + callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__) + case _: + log.error('Unsupported callback daemon protocol "%s"', protocol) + raise Exception('Unsupported callback daemon protocol.') + + extras['hooks_config'] = hooks_config + extras['hooks_protocol'] = protocol + extras['time'] = time.time() + + # register txn_id + extras['txn_id'] = txn_id + log.debug('Prepared a callback daemon: %s', callback_daemon.__class__.__name__) + return callback_daemon, extras diff --git a/rhodecode/lib/hooks_base.py b/rhodecode/lib/hooks_base.py --- a/rhodecode/lib/hooks_base.py +++ b/rhodecode/lib/hooks_base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,14 +30,14 @@ from rhodecode.lib import helpers as h from rhodecode.lib import audit_logger from rhodecode.lib.utils2 import safe_str, user_agent_normalizer from rhodecode.lib.exceptions import ( - HTTPLockedRC, HTTPBranchProtected, UserCreationError, ClientNotSupportedError) + HTTPLockedRepo, HTTPBranchProtected, UserCreationError, ClientNotSupported) from rhodecode.model.db import Repository, User from rhodecode.lib.statsd_client import StatsdClient log = logging.getLogger(__name__) -class HookResponse(object): +class HookResponse: def __init__(self, status, output): self.status = status self.output = output @@ -56,6 +56,8 @@ class HookResponse(object): def to_json(self): return {'status': self.status, 'output': self.output} + def __repr__(self): + return self.to_json().__repr__() def is_shadow_repo(extras): """ @@ -73,8 +75,68 @@ def check_vcs_client(extras): except ModuleNotFoundError: is_vcs_client_whitelisted = lambda *x: True backend = extras.get('scm') - if not is_vcs_client_whitelisted(extras.get('user_agent'), backend): - raise ClientNotSupportedError(f"Your {backend} client is forbidden") + user_agent = extras.get('user_agent') + if not is_vcs_client_whitelisted(user_agent, backend): + raise ClientNotSupported(f"Your {backend} client (version={user_agent}) is forbidden by security rules") + + +def check_locked_repo(extras, check_same_user=True): + user = User.get_by_username(extras.username) + output = '' + if extras.locked_by[0] and (not check_same_user or user.user_id != extras.locked_by[0]): + locked_by = User.get(extras.locked_by[0]).username + reason = extras.locked_by[2] + # this exception is interpreted in git/hg middlewares and based + # on that proper return code is server to client + _http_ret = HTTPLockedRepo(_locked_by_explanation(extras.repository, locked_by, reason)) + if str(_http_ret.code).startswith('2'): + # 2xx Codes don't raise exceptions + output = _http_ret.title + else: + raise _http_ret + + return output + + +def check_branch_protected(extras): + if extras.commit_ids and extras.check_branch_perms: + user = User.get_by_username(extras.username) + auth_user = user.AuthUser() + repo = Repository.get_by_repo_name(extras.repository) + if not repo: + raise ValueError(f'Repo for {extras.repository} not found') + affected_branches = [] + if repo.repo_type == 'hg': + for entry in extras.commit_ids: + if entry['type'] == 'branch': + is_forced = bool(entry['multiple_heads']) + affected_branches.append([entry['name'], is_forced]) + elif repo.repo_type == 'git': + for entry in extras.commit_ids: + if entry['type'] == 'heads': + is_forced = bool(entry['pruned_sha']) + affected_branches.append([entry['name'], is_forced]) + + for branch_name, is_forced in affected_branches: + + rule, branch_perm = auth_user.get_rule_and_branch_permission(extras.repository, branch_name) + if not branch_perm: + # no branch permission found for this branch, just keep checking + continue + + if branch_perm == 'branch.push_force': + continue + elif branch_perm == 'branch.push' and is_forced is False: + continue + elif branch_perm == 'branch.push' and is_forced is True: + halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \ + f'FORCE PUSH FORBIDDEN.' + else: + halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.' + + if halt_message: + _http_ret = HTTPBranchProtected(halt_message) + raise _http_ret def _get_scm_size(alias, root_path): @@ -109,116 +171,30 @@ def repo_size(extras): repo = Repository.get_by_repo_name(extras.repository) vcs_part = f'.{repo.repo_type}' size_vcs, size_root, size_total = _get_scm_size(vcs_part, repo.repo_full_path) - msg = (f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n') + msg = f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n' return HookResponse(0, msg) -def pre_push(extras): - """ - Hook executed before pushing code. - - It bans pushing when the repository is locked. - """ - - check_vcs_client(extras) - user = User.get_by_username(extras.username) - output = '' - if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]): - locked_by = User.get(extras.locked_by[0]).username - reason = extras.locked_by[2] - # this exception is interpreted in git/hg middlewares and based - # on that proper return code is server to client - _http_ret = HTTPLockedRC( - _locked_by_explanation(extras.repository, locked_by, reason)) - if str(_http_ret.code).startswith('2'): - # 2xx Codes don't raise exceptions - output = _http_ret.title - else: - raise _http_ret - - hook_response = '' - if not is_shadow_repo(extras): - - if extras.commit_ids and extras.check_branch_perms: - auth_user = user.AuthUser() - repo = Repository.get_by_repo_name(extras.repository) - if not repo: - raise ValueError(f'Repo for {extras.repository} not found') - affected_branches = [] - if repo.repo_type == 'hg': - for entry in extras.commit_ids: - if entry['type'] == 'branch': - is_forced = bool(entry['multiple_heads']) - affected_branches.append([entry['name'], is_forced]) - elif repo.repo_type == 'git': - for entry in extras.commit_ids: - if entry['type'] == 'heads': - is_forced = bool(entry['pruned_sha']) - affected_branches.append([entry['name'], is_forced]) - - for branch_name, is_forced in affected_branches: - - rule, branch_perm = auth_user.get_rule_and_branch_permission( - extras.repository, branch_name) - if not branch_perm: - # no branch permission found for this branch, just keep checking - continue - - if branch_perm == 'branch.push_force': - continue - elif branch_perm == 'branch.push' and is_forced is False: - continue - elif branch_perm == 'branch.push' and is_forced is True: - halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \ - f'FORCE PUSH FORBIDDEN.' - else: - halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.' - - if halt_message: - _http_ret = HTTPBranchProtected(halt_message) - raise _http_ret - - # Propagate to external components. This is done after checking the - # lock, for consistent behavior. - hook_response = pre_push_extension( - repo_store_path=Repository.base_path(), **extras) - events.trigger(events.RepoPrePushEvent( - repo_name=extras.repository, extras=extras)) - - return HookResponse(0, output) + hook_response - - def pre_pull(extras): """ Hook executed before pulling the code. It bans pulling when the repository is locked. + It bans pulling when incorrect client is used. """ - - check_vcs_client(extras) output = '' - if extras.locked_by[0]: - locked_by = User.get(extras.locked_by[0]).username - reason = extras.locked_by[2] - # this exception is interpreted in git/hg middlewares and based - # on that proper return code is server to client - _http_ret = HTTPLockedRC( - _locked_by_explanation(extras.repository, locked_by, reason)) - if str(_http_ret.code).startswith('2'): - # 2xx Codes don't raise exceptions - output = _http_ret.title - else: - raise _http_ret + check_vcs_client(extras) + + # locking repo can, but not have to stop the operation it can also just produce output + output += check_locked_repo(extras, check_same_user=False) # Propagate to external components. This is done after checking the # lock, for consistent behavior. hook_response = '' if not is_shadow_repo(extras): extras.hook_type = extras.hook_type or 'pre_pull' - hook_response = pre_pull_extension( - repo_store_path=Repository.base_path(), **extras) - events.trigger(events.RepoPrePullEvent( - repo_name=extras.repository, extras=extras)) + hook_response = pre_pull_extension(repo_store_path=Repository.base_path(), **extras) + events.trigger(events.RepoPrePullEvent(repo_name=extras.repository, extras=extras)) return HookResponse(0, output) + hook_response @@ -239,6 +215,7 @@ def post_pull(extras): statsd.incr('rhodecode_pull_total', tags=[ f'user-agent:{user_agent_normalizer(extras.user_agent)}', ]) + output = '' # make lock is a tri state False, True, None. We only make lock on True if extras.make_lock is True and not is_shadow_repo(extras): @@ -246,18 +223,9 @@ def post_pull(extras): Repository.lock(Repository.get_by_repo_name(extras.repository), user.user_id, lock_reason=Repository.LOCK_PULL) - msg = 'Made lock on repo `{}`'.format(extras.repository) + msg = f'Made lock on repo `{extras.repository}`' output += msg - if extras.locked_by[0]: - locked_by = User.get(extras.locked_by[0]).username - reason = extras.locked_by[2] - _http_ret = HTTPLockedRC( - _locked_by_explanation(extras.repository, locked_by, reason)) - if str(_http_ret.code).startswith('2'): - # 2xx Codes don't raise exceptions - output += _http_ret.title - # Propagate to external components. hook_response = '' if not is_shadow_repo(extras): @@ -270,6 +238,33 @@ def post_pull(extras): return HookResponse(0, output) + hook_response +def pre_push(extras): + """ + Hook executed before pushing code. + + It bans pushing when the repository is locked. + It banks pushing when incorrect client is used. + It also checks for Branch protection + """ + output = '' + check_vcs_client(extras) + + # locking repo can, but not have to stop the operation it can also just produce output + output += check_locked_repo(extras) + + hook_response = '' + if not is_shadow_repo(extras): + + check_branch_protected(extras) + + # Propagate to external components. This is done after checking the + # lock, for consistent behavior. + hook_response = pre_push_extension(repo_store_path=Repository.base_path(), **extras) + events.trigger(events.RepoPrePushEvent(repo_name=extras.repository, extras=extras)) + + return HookResponse(0, output) + hook_response + + def post_push(extras): """Hook executed after user pushes to the repository.""" commit_ids = extras.commit_ids @@ -292,22 +287,13 @@ def post_push(extras): # Propagate to external components. output = '' + # make lock is a tri state False, True, None. We only release lock on False if extras.make_lock is False and not is_shadow_repo(extras): Repository.unlock(Repository.get_by_repo_name(extras.repository)) msg = f'Released lock on repo `{extras.repository}`\n' output += msg - if extras.locked_by[0]: - locked_by = User.get(extras.locked_by[0]).username - reason = extras.locked_by[2] - _http_ret = HTTPLockedRC( - _locked_by_explanation(extras.repository, locked_by, reason)) - # TODO: johbo: if not? - if str(_http_ret.code).startswith('2'): - # 2xx Codes don't raise exceptions - output += _http_ret.title - if extras.new_refs: tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format( safe_str(extras.server_url), safe_str(extras.repository)) @@ -322,11 +308,8 @@ def post_push(extras): hook_response = '' if not is_shadow_repo(extras): - hook_response = post_push_extension( - repo_store_path=Repository.base_path(), - **extras) - events.trigger(events.RepoPushEvent( - repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras)) + hook_response = post_push_extension(repo_store_path=Repository.base_path(), **extras) + events.trigger(events.RepoPushEvent(repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras)) output += 'RhodeCode: push completed\n' return HookResponse(0, output) + hook_response @@ -380,12 +363,20 @@ class ExtensionCallback(object): # with older rcextensions that require api_key present if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']: kwargs_to_pass['api_key'] = '_DEPRECATED_' - return callback(**kwargs_to_pass) + result = callback(**kwargs_to_pass) + log.debug('got rcextensions result: %s', result) + return result def is_active(self): return hasattr(rhodecode.EXTENSIONS, self._hook_name) def _get_callback(self): + if rhodecode.is_test: + log.debug('In test mode, reloading rcextensions...') + # NOTE: for test re-load rcextensions always so we can dynamically change them for testing purposes + from rhodecode.lib.utils import load_rcextensions + load_rcextensions(root_path=os.path.dirname(rhodecode.CONFIG['__file__'])) + return getattr(rhodecode.EXTENSIONS, self._hook_name, None) return getattr(rhodecode.EXTENSIONS, self._hook_name, None) diff --git a/rhodecode/lib/hooks_utils.py b/rhodecode/lib/hooks_utils.py --- a/rhodecode/lib/hooks_utils.py +++ b/rhodecode/lib/hooks_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/html_filters.py b/rhodecode/lib/html_filters.py --- a/rhodecode/lib/html_filters.py +++ b/rhodecode/lib/html_filters.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2023 RhodeCode GmbH +# Copyright (C) 2020-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/index/__init__.py b/rhodecode/lib/index/__init__.py --- a/rhodecode/lib/index/__init__.py +++ b/rhodecode/lib/index/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/index/search_utils.py b/rhodecode/lib/index/search_utils.py --- a/rhodecode/lib/index/search_utils.py +++ b/rhodecode/lib/index/search_utils.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/index/whoosh.py b/rhodecode/lib/index/whoosh.py --- a/rhodecode/lib/index/whoosh.py +++ b/rhodecode/lib/index/whoosh.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/index/whoosh_fallback_schema.py b/rhodecode/lib/index/whoosh_fallback_schema.py --- a/rhodecode/lib/index/whoosh_fallback_schema.py +++ b/rhodecode/lib/index/whoosh_fallback_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/jsonalchemy.py b/rhodecode/lib/jsonalchemy.py --- a/rhodecode/lib/jsonalchemy.py +++ b/rhodecode/lib/jsonalchemy.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/logging_formatter.py b/rhodecode/lib/logging_formatter.py --- a/rhodecode/lib/logging_formatter.py +++ b/rhodecode/lib/logging_formatter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/markdown_ext.py b/rhodecode/lib/markdown_ext.py --- a/rhodecode/lib/markdown_ext.py +++ b/rhodecode/lib/markdown_ext.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py --- a/rhodecode/lib/markup_renderer.py +++ b/rhodecode/lib/markup_renderer.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/memory_lru_dict.py b/rhodecode/lib/memory_lru_dict.py --- a/rhodecode/lib/memory_lru_dict.py +++ b/rhodecode/lib/memory_lru_dict.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/__init__.py b/rhodecode/lib/middleware/__init__.py --- a/rhodecode/lib/middleware/__init__.py +++ b/rhodecode/lib/middleware/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/appenlight.py b/rhodecode/lib/middleware/appenlight.py --- a/rhodecode/lib/middleware/appenlight.py +++ b/rhodecode/lib/middleware/appenlight.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/csrf.py b/rhodecode/lib/middleware/csrf.py --- a/rhodecode/lib/middleware/csrf.py +++ b/rhodecode/lib/middleware/csrf.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/https_fixup.py b/rhodecode/lib/middleware/https_fixup.py --- a/rhodecode/lib/middleware/https_fixup.py +++ b/rhodecode/lib/middleware/https_fixup.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/request_wrapper.py b/rhodecode/lib/middleware/request_wrapper.py --- a/rhodecode/lib/middleware/request_wrapper.py +++ b/rhodecode/lib/middleware/request_wrapper.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -40,16 +39,6 @@ GIT_PROTO_PAT = re.compile( GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))') -def default_lfs_store(): - """ - Default lfs store location, it's consistent with Mercurials large file - store which is in .cache/largefiles - """ - from rhodecode.lib.vcs.backends.git import lfs_store - user_home = os.path.expanduser("~") - return lfs_store(user_home) - - class SimpleGit(simplevcs.SimpleVCS): SCM = 'git' @@ -151,6 +140,6 @@ class SimpleGit(simplevcs.SimpleVCS): extras['git_lfs_enabled'] = utils2.str2bool( config.get('vcs_git_lfs', 'enabled')) - extras['git_lfs_store_path'] = custom_store or default_lfs_store() + extras['git_lfs_store_path'] = custom_store extras['git_lfs_http_scheme'] = scheme return extras diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py +++ b/rhodecode/lib/middleware/simplehg.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,6 +21,7 @@ SimpleHG middleware for handling mercuri (push/clone etc.). It's implemented with basic auth function """ +import copy import logging import urllib.parse import urllib.request @@ -32,6 +32,7 @@ from rhodecode.lib import utils from rhodecode.lib.ext_json import json from rhodecode.lib.middleware import simplevcs from rhodecode.lib.middleware.utils import get_path_info +from rhodecode.lib.str_utils import safe_str log = logging.getLogger(__name__) @@ -95,7 +96,7 @@ class SimpleHg(simplevcs.SimpleVCS): i = 1 chunks = [] # gather chunks stored in multiple 'hgarg_N' while True: - head = environ.get('HTTP_X_HGARG_{}'.format(i)) + head = environ.get(f'HTTP_X_HGARG_{i}') if not head: break i += 1 @@ -118,8 +119,18 @@ class SimpleHg(simplevcs.SimpleVCS): """ default = 'push' batch_cmds = [] + try: - cmds = cls._get_xarg_headers(environ) + httppostargs_enabled = True + post_args_size = environ.get('HTTP_X_HGARGS_POST') + if post_args_size and httppostargs_enabled: + # a new proto when httppostargs is enabled + response_data = copy.copy(environ['wsgi.input']) + cmds = [safe_str(response_data.read(post_args_size))] + else: + # old way... from headers + cmds = cls._get_xarg_headers(environ) + for pair in cmds: parts = pair.split(' ', 1) if len(parts) != 2: diff --git a/rhodecode/lib/middleware/simplesvn.py b/rhodecode/lib/middleware/simplesvn.py --- a/rhodecode/lib/middleware/simplesvn.py +++ b/rhodecode/lib/middleware/simplesvn.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,8 +30,7 @@ from functools import wraps import time from paste.httpheaders import REMOTE_USER, AUTH_TYPE -from pyramid.httpexceptions import ( - HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError) +from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError from zope.cachedescriptors.property import Lazy as LazyProperty import rhodecode @@ -41,10 +38,9 @@ from rhodecode.authentication.base impor from rhodecode.lib import rc_cache from rhodecode.lib.svn_txn_utils import store_txn_id_data from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware -from rhodecode.lib.base import ( - BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) -from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError) -from rhodecode.lib.hook_daemon.base import prepare_callback_daemon +from rhodecode.lib.base import BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context +from rhodecode.lib.exceptions import UserCreationError, NotAllowedToCreateUserError +from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon from rhodecode.lib.middleware import appenlight from rhodecode.lib.middleware.utils import scm_app_http from rhodecode.lib.str_utils import safe_bytes, safe_int @@ -78,17 +74,18 @@ def initialize_generator(factory): try: init = next(gen) except StopIteration: - raise ValueError('Generator must yield at least one element.') + raise ValueError("Generator must yield at least one element.") if init != "__init__": raise ValueError('First yielded element must be "__init__".') return gen + return wrapper class SimpleVCS(object): """Common functionality for SCM HTTP handlers.""" - SCM = 'unknown' + SCM = "unknown" acl_repo_name = None url_repo_name = None @@ -100,11 +97,11 @@ class SimpleVCS(object): # we use this regex which will match only on URLs pointing to shadow # repositories. shadow_repo_re = re.compile( - '(?P(?:{slug_pat}/)*)' # repo groups - '(?P{slug_pat})/' # target repo - 'pull-request/(?P\\d+)/' # pull request - 'repository$' # shadow repo - .format(slug_pat=SLUG_RE.pattern)) + "(?P(?:{slug_pat}/)*)" # repo groups + "(?P{slug_pat})/" # target repo + "pull-request/(?P\\d+)/" # pull request + "repository$".format(slug_pat=SLUG_RE.pattern) # shadow repo + ) def __init__(self, config, registry): self.registry = registry @@ -113,15 +110,14 @@ class SimpleVCS(object): self.repo_vcs_config = base.Config() rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False) - realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH' + realm = rc_settings.get("rhodecode_realm") or "RhodeCode AUTH" # authenticate this VCS request using authfunc - auth_ret_code_detection = \ - str2bool(self.config.get('auth_ret_code_detection', False)) + auth_ret_code_detection = str2bool(self.config.get("auth_ret_code_detection", False)) self.authenticate = BasicAuth( - '', authenticate, registry, config.get('auth_ret_code'), - auth_ret_code_detection, rc_realm=realm) - self.ip_addr = '0.0.0.0' + "", authenticate, registry, config.get("auth_ret_code"), auth_ret_code_detection, rc_realm=realm + ) + self.ip_addr = "0.0.0.0" @LazyProperty def global_vcs_config(self): @@ -132,10 +128,10 @@ class SimpleVCS(object): @property def base_path(self): - settings_path = self.config.get('repo_store.path') + settings_path = self.config.get("repo_store.path") if not settings_path: - raise ValueError('FATAL: repo_store.path is empty') + raise ValueError("FATAL: repo_store.path is empty") return settings_path def set_repo_names(self, environ): @@ -164,17 +160,16 @@ class SimpleVCS(object): match_dict = match.groupdict() # Build acl repo name from regex match. - acl_repo_name = safe_str('{groups}{target}'.format( - groups=match_dict['groups'] or '', - target=match_dict['target'])) + acl_repo_name = safe_str( + "{groups}{target}".format(groups=match_dict["groups"] or "", target=match_dict["target"]) + ) # Retrieve pull request instance by ID from regex match. - pull_request = PullRequest.get(match_dict['pr_id']) + pull_request = PullRequest.get(match_dict["pr_id"]) # Only proceed if we got a pull request and if acl repo name from # URL equals the target repo name of the pull request. if pull_request and (acl_repo_name == pull_request.target_repo.repo_name): - # Get file system path to shadow repository. workspace_id = PullRequestModel()._workspace_id(pull_request) vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id) @@ -184,21 +179,23 @@ class SimpleVCS(object): self.acl_repo_name = acl_repo_name self.is_shadow_repo = True - log.debug('Setting all VCS repository names: %s', { - 'acl_repo_name': self.acl_repo_name, - 'url_repo_name': self.url_repo_name, - 'vcs_repo_name': self.vcs_repo_name, - }) + log.debug( + "Setting all VCS repository names: %s", + { + "acl_repo_name": self.acl_repo_name, + "url_repo_name": self.url_repo_name, + "vcs_repo_name": self.vcs_repo_name, + }, + ) @property def scm_app(self): - custom_implementation = self.config['vcs.scm_app_implementation'] - if custom_implementation == 'http': - log.debug('Using HTTP implementation of scm app.') + custom_implementation = self.config["vcs.scm_app_implementation"] + if custom_implementation == "http": + log.debug("Using HTTP implementation of scm app.") scm_app_impl = scm_app_http else: - log.debug('Using custom implementation of scm_app: "{}"'.format( - custom_implementation)) + log.debug('Using custom implementation of scm_app: "{}"'.format(custom_implementation)) scm_app_impl = importlib.import_module(custom_implementation) return scm_app_impl @@ -208,17 +205,18 @@ class SimpleVCS(object): with a repository_name for support of _ non changeable urls """ - data = repo_name.split('/') + data = repo_name.split("/") if len(data) >= 2: from rhodecode.model.repo import RepoModel + by_id_match = RepoModel().get_repo_by_id(repo_name) if by_id_match: data[1] = by_id_match.repo_name # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO # and we use this data - maybe_new_path = '/'.join(data) - return safe_bytes(maybe_new_path).decode('latin1') + maybe_new_path = "/".join(data) + return safe_bytes(maybe_new_path).decode("latin1") def _invalidate_cache(self, repo_name): """ @@ -231,21 +229,18 @@ class SimpleVCS(object): def is_valid_and_existing_repo(self, repo_name, base_path, scm_type): db_repo = Repository.get_by_repo_name(repo_name) if not db_repo: - log.debug('Repository `%s` not found inside the database.', - repo_name) + log.debug("Repository `%s` not found inside the database.", repo_name) return False if db_repo.repo_type != scm_type: log.warning( - 'Repository `%s` have incorrect scm_type, expected %s got %s', - repo_name, db_repo.repo_type, scm_type) + "Repository `%s` have incorrect scm_type, expected %s got %s", repo_name, db_repo.repo_type, scm_type + ) return False config = db_repo._config - config.set('extensions', 'largefiles', '') - return is_valid_repo( - repo_name, base_path, - explicit_scm=scm_type, expect_scm=scm_type, config=config) + config.set("extensions", "largefiles", "") + return is_valid_repo(repo_name, base_path, explicit_scm=scm_type, expect_scm=scm_type, config=config) def valid_and_active_user(self, user): """ @@ -267,8 +262,9 @@ class SimpleVCS(object): def is_shadow_repo_dir(self): return os.path.isdir(self.vcs_repo_name) - def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None, - plugin_id='', plugin_cache_active=False, cache_ttl=0): + def _check_permission( + self, action, user, auth_user, repo_name, ip_addr=None, plugin_id="", plugin_cache_active=False, cache_ttl=0 + ): """ Checks permissions using action (push/pull) user and repository name. If plugin_cache and ttl is set it will use the plugin which @@ -280,71 +276,67 @@ class SimpleVCS(object): :param repo_name: repository name """ - log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)', - plugin_id, plugin_cache_active, cache_ttl) + log.debug("AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)", plugin_id, plugin_cache_active, cache_ttl) user_id = user.user_id - cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}' - region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid) + cache_namespace_uid = f"cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}" + region = rc_cache.get_or_create_region("cache_perms", cache_namespace_uid) - @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, - expiration_time=cache_ttl, - condition=plugin_cache_active) - def compute_perm_vcs( - cache_name, plugin_id, action, user_id, repo_name, ip_addr): - - log.debug('auth: calculating permission access now for vcs operation: %s', action) + @region.conditional_cache_on_arguments( + namespace=cache_namespace_uid, expiration_time=cache_ttl, condition=plugin_cache_active + ) + def compute_perm_vcs(cache_name, plugin_id, action, user_id, repo_name, ip_addr): + log.debug("auth: calculating permission access now for vcs operation: %s", action) # check IP inherit = user.inherit_default_permissions - ip_allowed = AuthUser.check_ip_allowed( - user_id, ip_addr, inherit_from_default=inherit) + ip_allowed = AuthUser.check_ip_allowed(user_id, ip_addr, inherit_from_default=inherit) if ip_allowed: - log.info('Access for IP:%s allowed', ip_addr) + log.info("Access for IP:%s allowed", ip_addr) else: return False - if action == 'push': - perms = ('repository.write', 'repository.admin') + if action == "push": + perms = ("repository.write", "repository.admin") if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name): return False else: # any other action need at least read permission - perms = ( - 'repository.read', 'repository.write', 'repository.admin') + perms = ("repository.read", "repository.write", "repository.admin") if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name): return False return True start = time.time() - log.debug('Running plugin `%s` permissions check', plugin_id) + log.debug("Running plugin `%s` permissions check", plugin_id) # for environ based auth, password can be empty, but then the validation is # on the server that fills in the env data needed for authentication - perm_result = compute_perm_vcs( - 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr) + perm_result = compute_perm_vcs("vcs_permissions", plugin_id, action, user.user_id, repo_name, ip_addr) auth_time = time.time() - start - log.debug('Permissions for plugin `%s` completed in %.4fs, ' - 'expiration time of fetched cache %.1fs.', - plugin_id, auth_time, cache_ttl) + log.debug( + "Permissions for plugin `%s` completed in %.4fs, " "expiration time of fetched cache %.1fs.", + plugin_id, + auth_time, + cache_ttl, + ) return perm_result def _get_http_scheme(self, environ): try: - return environ['wsgi.url_scheme'] + return environ["wsgi.url_scheme"] except Exception: - log.exception('Failed to read http scheme') - return 'http' + log.exception("Failed to read http scheme") + return "http" def _get_default_cache_ttl(self): # take AUTH_CACHE_TTL from the `rhodecode` auth plugin - plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode') + plugin = loadplugin("egg:rhodecode-enterprise-ce#rhodecode") plugin_settings = plugin.get_settings() - plugin_cache_active, cache_ttl = plugin.get_ttl_cache( - plugin_settings) or (False, 0) + plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings) or (False, 0) return plugin_cache_active, cache_ttl def __call__(self, environ, start_response): @@ -359,17 +351,17 @@ class SimpleVCS(object): def _handle_request(self, environ, start_response): if not self.url_repo_name: - log.warning('Repository name is empty: %s', self.url_repo_name) + log.warning("Repository name is empty: %s", self.url_repo_name) # failed to get repo name, we fail now return HTTPNotFound()(environ, start_response) - log.debug('Extracted repo name is %s', self.url_repo_name) + log.debug("Extracted repo name is %s", self.url_repo_name) ip_addr = get_ip_addr(environ) user_agent = get_user_agent(environ) username = None # skip passing error to error controller - environ['pylons.status_code_redirect'] = True + environ["pylons.status_code_redirect"] = True # ====================================================================== # GET ACTION PULL or PUSH @@ -380,17 +372,15 @@ class SimpleVCS(object): # Check if this is a request to a shadow repository of a pull request. # In this case only pull action is allowed. # ====================================================================== - if self.is_shadow_repo and action != 'pull': - reason = 'Only pull action is allowed for shadow repositories.' - log.debug('User not allowed to proceed, %s', reason) + if self.is_shadow_repo and action != "pull": + reason = "Only pull action is allowed for shadow repositories." + log.debug("User not allowed to proceed, %s", reason) return HTTPNotAcceptable(reason)(environ, start_response) # Check if the shadow repo actually exists, in case someone refers # to it, and it has been deleted because of successful merge. if self.is_shadow_repo and not self.is_shadow_repo_dir: - log.debug( - 'Shadow repo detected, and shadow repo dir `%s` is missing', - self.is_shadow_repo_dir) + log.debug("Shadow repo detected, and shadow repo dir `%s` is missing", self.is_shadow_repo_dir) return HTTPNotFound()(environ, start_response) # ====================================================================== @@ -398,7 +388,7 @@ class SimpleVCS(object): # ====================================================================== detect_force_push = False check_branch_perms = False - if action in ['pull', 'push']: + if action in ["pull", "push"]: user_obj = anonymous_user = User.get_default_user() auth_user = user_obj.AuthUser() username = anonymous_user.username @@ -406,8 +396,12 @@ class SimpleVCS(object): plugin_cache_active, cache_ttl = self._get_default_cache_ttl() # ONLY check permissions if the user is activated anonymous_perm = self._check_permission( - action, anonymous_user, auth_user, self.acl_repo_name, ip_addr, - plugin_id='anonymous_access', + action, + anonymous_user, + auth_user, + self.acl_repo_name, + ip_addr, + plugin_id="anonymous_access", plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl, ) @@ -416,12 +410,13 @@ class SimpleVCS(object): if not anonymous_user.active or not anonymous_perm: if not anonymous_user.active: - log.debug('Anonymous access is disabled, running ' - 'authentication') + log.debug("Anonymous access is disabled, running " "authentication") if not anonymous_perm: - log.debug('Not enough credentials to access repo: `%s` ' - 'repository as anonymous user', self.acl_repo_name) + log.debug( + "Not enough credentials to access repo: `%s` " "repository as anonymous user", + self.acl_repo_name, + ) username = None # ============================================================== @@ -430,19 +425,18 @@ class SimpleVCS(object): # ============================================================== # try to auth based on environ, container auth methods - log.debug('Running PRE-AUTH for container|headers based authentication') + log.debug("Running PRE-AUTH for container|headers based authentication") # headers auth, by just reading special headers and bypass the auth with user/passwd pre_auth = authenticate( - '', '', environ, VCS_TYPE, registry=self.registry, - acl_repo_name=self.acl_repo_name) + "", "", environ, VCS_TYPE, registry=self.registry, acl_repo_name=self.acl_repo_name + ) - if pre_auth and pre_auth.get('username'): - username = pre_auth['username'] - log.debug('PRE-AUTH got `%s` as username', username) + if pre_auth and pre_auth.get("username"): + username = pre_auth["username"] + log.debug("PRE-AUTH got `%s` as username", username) if pre_auth: - log.debug('PRE-AUTH successful from %s', - pre_auth.get('auth_data', {}).get('_plugin')) + log.debug("PRE-AUTH successful from %s", pre_auth.get("auth_data", {}).get("_plugin")) # If not authenticated by the container, running basic auth # before inject the calling repo_name for special scope checks @@ -463,16 +457,16 @@ class SimpleVCS(object): return HTTPNotAcceptable(reason)(environ, start_response) if isinstance(auth_result, dict): - AUTH_TYPE.update(environ, 'basic') - REMOTE_USER.update(environ, auth_result['username']) - username = auth_result['username'] - plugin = auth_result.get('auth_data', {}).get('_plugin') - log.info( - 'MAIN-AUTH successful for user `%s` from %s plugin', - username, plugin) + AUTH_TYPE.update(environ, "basic") + REMOTE_USER.update(environ, auth_result["username"]) + username = auth_result["username"] + plugin = auth_result.get("auth_data", {}).get("_plugin") + log.info("MAIN-AUTH successful for user `%s` from %s plugin", username, plugin) - plugin_cache_active, cache_ttl = auth_result.get( - 'auth_data', {}).get('_ttl_cache') or (False, 0) + plugin_cache_active, cache_ttl = auth_result.get("auth_data", {}).get("_ttl_cache") or ( + False, + 0, + ) else: return auth_result.wsgi_application(environ, start_response) @@ -488,21 +482,24 @@ class SimpleVCS(object): # check user attributes for password change flag user_obj = user auth_user = user_obj.AuthUser() - if user_obj and user_obj.username != User.DEFAULT_USER and \ - user_obj.user_data.get('force_password_change'): - reason = 'password change required' - log.debug('User not allowed to authenticate, %s', reason) + if ( + user_obj + and user_obj.username != User.DEFAULT_USER + and user_obj.user_data.get("force_password_change") + ): + reason = "password change required" + log.debug("User not allowed to authenticate, %s", reason) return HTTPNotAcceptable(reason)(environ, start_response) # check permissions for this repository perm = self._check_permission( - action, user, auth_user, self.acl_repo_name, ip_addr, - plugin, plugin_cache_active, cache_ttl) + action, user, auth_user, self.acl_repo_name, ip_addr, plugin, plugin_cache_active, cache_ttl + ) if not perm: return HTTPForbidden()(environ, start_response) - environ['rc_auth_user_id'] = str(user_id) + environ["rc_auth_user_id"] = str(user_id) - if action == 'push': + if action == "push": perms = auth_user.get_branch_permissions(self.acl_repo_name) if perms: check_branch_perms = True @@ -510,41 +507,48 @@ class SimpleVCS(object): # extras are injected into UI object and later available # in hooks executed by RhodeCode - check_locking = _should_check_locking(environ.get('QUERY_STRING')) + check_locking = _should_check_locking(environ.get("QUERY_STRING")) extras = vcs_operation_context( - environ, repo_name=self.acl_repo_name, username=username, - action=action, scm=self.SCM, check_locking=check_locking, - is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms, - detect_force_push=detect_force_push + environ, + repo_name=self.acl_repo_name, + username=username, + action=action, + scm=self.SCM, + check_locking=check_locking, + is_shadow_repo=self.is_shadow_repo, + check_branch_perms=check_branch_perms, + detect_force_push=detect_force_push, ) # ====================================================================== # REQUEST HANDLING # ====================================================================== - repo_path = os.path.join( - safe_str(self.base_path), safe_str(self.vcs_repo_name)) - log.debug('Repository path is %s', repo_path) + repo_path = os.path.join(safe_str(self.base_path), safe_str(self.vcs_repo_name)) + log.debug("Repository path is %s", repo_path) fix_PATH() log.info( '%s action on %s repo "%s" by "%s" from %s %s', - action, self.SCM, safe_str(self.url_repo_name), - safe_str(username), ip_addr, user_agent) + action, + self.SCM, + safe_str(self.url_repo_name), + safe_str(username), + ip_addr, + user_agent, + ) - return self._generate_vcs_response( - environ, start_response, repo_path, extras, action) + return self._generate_vcs_response(environ, start_response, repo_path, extras, action) def _get_txn_id(self, environ): - - for k in ['RAW_URI', 'HTTP_DESTINATION']: + for k in ["RAW_URI", "HTTP_DESTINATION"]: url = environ.get(k) if not url: continue # regex to search for svn-txn-id - pattern = r'/!svn/txr/([^/]+)/' + pattern = r"/!svn/txr/([^/]+)/" # Search for the pattern in the URL match = re.search(pattern, url) @@ -555,8 +559,7 @@ class SimpleVCS(object): return txn_id @initialize_generator - def _generate_vcs_response( - self, environ, start_response, repo_path, extras, action): + def _generate_vcs_response(self, environ, start_response, repo_path, extras, action): """ Returns a generator for the response content. @@ -565,24 +568,20 @@ class SimpleVCS(object): also handles the locking exceptions which will be triggered when the first chunk is produced by the underlying WSGI application. """ - svn_txn_id = '' - if action == 'push': + svn_txn_id = "" + if action == "push": svn_txn_id = self._get_txn_id(environ) - callback_daemon, extras = self._prepare_callback_daemon( - extras, environ, action, txn_id=svn_txn_id) + callback_daemon, extras = self._prepare_callback_daemon(extras, environ, action, txn_id=svn_txn_id) if svn_txn_id: - - port = safe_int(extras['hooks_uri'].split(':')[-1]) txn_id_data = extras.copy() - txn_id_data.update({'port': port}) - txn_id_data.update({'req_method': environ['REQUEST_METHOD']}) + txn_id_data.update({"req_method": environ["REQUEST_METHOD"]}) full_repo_path = repo_path store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data) - log.debug('HOOKS extras is %s', extras) + log.debug("HOOKS extras is %s", extras) http_scheme = self._get_http_scheme(environ) @@ -609,7 +608,7 @@ class SimpleVCS(object): try: # invalidate cache on push - if action == 'push': + if action == "push": self._invalidate_cache(self.url_repo_name) finally: meta.Session.remove() @@ -632,12 +631,12 @@ class SimpleVCS(object): """Return the WSGI app that will finally handle the request.""" raise NotImplementedError() - def _create_config(self, extras, repo_name, scheme='http'): + def _create_config(self, extras, repo_name, scheme="http"): """Create a safe config representation.""" raise NotImplementedError() def _should_use_callback_daemon(self, extras, environ, action): - if extras.get('is_shadow_repo'): + if extras.get("is_shadow_repo"): # we don't want to execute hooks, and callback daemon for shadow repos return False return True @@ -647,11 +646,9 @@ class SimpleVCS(object): if not self._should_use_callback_daemon(extras, environ, action): # disable callback daemon for actions that don't require it - protocol = 'local' + protocol = "local" - return prepare_callback_daemon( - extras, protocol=protocol, - host=vcs_settings.HOOKS_HOST, txn_id=txn_id) + return prepare_callback_daemon(extras, protocol=protocol, txn_id=txn_id) def _should_check_locking(query_string): @@ -659,4 +656,4 @@ def _should_check_locking(query_string): # server see all operation on commit; bookmarks, phases and # obsolescence marker in different transaction, we don't want to check # locking on those - return query_string not in ['cmd=listkeys'] + return query_string not in ["cmd=listkeys"] diff --git a/rhodecode/lib/middleware/utils/__init__.py b/rhodecode/lib/middleware/utils/__init__.py --- a/rhodecode/lib/middleware/utils/__init__.py +++ b/rhodecode/lib/middleware/utils/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/utils/scm_app.py b/rhodecode/lib/middleware/utils/scm_app.py --- a/rhodecode/lib/middleware/utils/scm_app.py +++ b/rhodecode/lib/middleware/utils/scm_app.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/utils/scm_app_http.py b/rhodecode/lib/middleware/utils/scm_app_http.py --- a/rhodecode/lib/middleware/utils/scm_app_http.py +++ b/rhodecode/lib/middleware/utils/scm_app_http.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/utils/wsgi_app_caller_client.py b/rhodecode/lib/middleware/utils/wsgi_app_caller_client.py --- a/rhodecode/lib/middleware/utils/wsgi_app_caller_client.py +++ b/rhodecode/lib/middleware/utils/wsgi_app_caller_client.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/middleware/vcs.py b/rhodecode/lib/middleware/vcs.py --- a/rhodecode/lib/middleware/vcs.py +++ b/rhodecode/lib/middleware/vcs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/pagination.py b/rhodecode/lib/pagination.py --- a/rhodecode/lib/pagination.py +++ b/rhodecode/lib/pagination.py @@ -1,6 +1,4 @@ - - -# Copyright (c) 2007-2012 Christoph Haas +# Copyright (C) 2007-2012 Christoph Haas # NOTE: MIT license based code, backported and edited by RhodeCode GmbH """ @@ -869,8 +867,7 @@ class _Page(list): return make_html_tag("a", text=text, href=target_url, **item["attrs"]) -# Below is RhodeCode custom code -# Copyright (C) 2010-2023 RhodeCode GmbH +# Below is RhodeCode custom code# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/partial_renderer.py b/rhodecode/lib/partial_renderer.py --- a/rhodecode/lib/partial_renderer.py +++ b/rhodecode/lib/partial_renderer.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/pidlock.py b/rhodecode/lib/pidlock.py --- a/rhodecode/lib/pidlock.py +++ b/rhodecode/lib/pidlock.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/plugins/__init__.py b/rhodecode/lib/plugins/__init__.py --- a/rhodecode/lib/plugins/__init__.py +++ b/rhodecode/lib/plugins/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/plugins/utils.py b/rhodecode/lib/plugins/utils.py --- a/rhodecode/lib/plugins/utils.py +++ b/rhodecode/lib/plugins/utils.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/pyramid_shell/__init__.py b/rhodecode/lib/pyramid_shell/__init__.py --- a/rhodecode/lib/pyramid_shell/__init__.py +++ b/rhodecode/lib/pyramid_shell/__init__.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/pyramid_utils.py b/rhodecode/lib/pyramid_utils.py --- a/rhodecode/lib/pyramid_utils.py +++ b/rhodecode/lib/pyramid_utils.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_cache/__init__.py b/rhodecode/lib/rc_cache/__init__.py --- a/rhodecode/lib/rc_cache/__init__.py +++ b/rhodecode/lib/rc_cache/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2023 RhodeCode GmbH +# Copyright (C) 2015-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -55,7 +55,7 @@ register_backend( log = logging.getLogger(__name__) -FILE_TREE_CACHE_VER = 'v5' +FILE_TREE_CACHE_VER = 'v6' LICENSE_CACHE_VER = 'v3' PERMISSIONS_CACHE_VER = 'v2' diff --git a/rhodecode/lib/rc_cache/backends.py b/rhodecode/lib/rc_cache/backends.py --- a/rhodecode/lib/rc_cache/backends.py +++ b/rhodecode/lib/rc_cache/backends.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2023 RhodeCode GmbH +# Copyright (C) 2015-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -216,6 +216,9 @@ class BaseRedisBackend(redis_backend.Red self._lock_timeout = self.lock_timeout self._lock_auto_renewal = str2bool(arguments.pop("lock_auto_renewal", True)) + self._store_key_prefix = arguments.pop('key_prefix', '') + self.key_prefix = f'{self._store_key_prefix}{self.key_prefix}' + if self._lock_auto_renewal and not self._lock_timeout: # set default timeout for auto_renewal self._lock_timeout = 30 @@ -275,7 +278,7 @@ class BaseRedisBackend(redis_backend.Red def get_mutex(self, key): if self.distributed_lock: - lock_key = f'_lock_{safe_str(key)}' + lock_key = f'{self._store_key_prefix}_lock_{safe_str(key)}' return get_mutex_lock( self.writer_client, lock_key, self._lock_timeout, diff --git a/rhodecode/lib/rc_cache/region_meta.py b/rhodecode/lib/rc_cache/region_meta.py --- a/rhodecode/lib/rc_cache/region_meta.py +++ b/rhodecode/lib/rc_cache/region_meta.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2023 RhodeCode GmbH +# Copyright (C) 2015-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_cache/utils.py b/rhodecode/lib/rc_cache/utils.py --- a/rhodecode/lib/rc_cache/utils.py +++ b/rhodecode/lib/rc_cache/utils.py @@ -69,19 +69,17 @@ class RhodeCodeCacheRegion(CacheRegion): def get_or_create_for_user_func(func_key_generator, user_func, *arg, **kw): if not condition: - log.debug('Calling un-cached method:%s', user_func.__name__) + log.debug('Calling un-cached method:`%s`', user_func.__name__) start = time.time() result = user_func(*arg, **kw) total = time.time() - start - log.debug('un-cached method:%s took %.4fs', user_func.__name__, total) + log.debug('Call for un-cached method:`%s` took %.4fs', user_func.__name__, total) return result key = func_key_generator(*arg, **kw) + timeout = expiration_time() if expiration_time_is_callable else expiration_time + log.debug('Calling cached (timeout=%s) method:`%s`', timeout, user_func.__name__) - timeout = expiration_time() if expiration_time_is_callable \ - else expiration_time - - log.debug('Calling cached method:`%s`', user_func.__name__) return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw)) def cache_decorator(user_func): @@ -174,7 +172,7 @@ def backend_key_generator(backend): return wrapper -def get_or_create_region(region_name, region_namespace: str = None, use_async_runner=False): +def get_or_create_region(region_name, region_namespace: str = None, use_async_runner=False, force=False): from .backends import FileNamespaceBackend from . import async_creation_runner @@ -191,7 +189,7 @@ def get_or_create_region(region_name, re raise ValueError(f'{FileNamespaceBackend} used requires to specify region_namespace param') region_exist = region_meta.dogpile_cache_regions.get(region_namespace) - if region_exist: + if region_exist and not force: log.debug('Using already configured region: %s', region_namespace) return region_exist diff --git a/rhodecode/lib/rc_commands/add_artifact.py b/rhodecode/lib/rc_commands/add_artifact.py --- a/rhodecode/lib/rc_commands/add_artifact.py +++ b/rhodecode/lib/rc_commands/add_artifact.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_commands/ishell.py b/rhodecode/lib/rc_commands/ishell.py --- a/rhodecode/lib/rc_commands/ishell.py +++ b/rhodecode/lib/rc_commands/ishell.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_commands/migrate_artifact.py b/rhodecode/lib/rc_commands/migrate_artifact.py --- a/rhodecode/lib/rc_commands/migrate_artifact.py +++ b/rhodecode/lib/rc_commands/migrate_artifact.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_commands/setup_rc.py b/rhodecode/lib/rc_commands/setup_rc.py --- a/rhodecode/lib/rc_commands/setup_rc.py +++ b/rhodecode/lib/rc_commands/setup_rc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/rc_commands/upgrade_db.py b/rhodecode/lib/rc_commands/upgrade_db.py --- a/rhodecode/lib/rc_commands/upgrade_db.py +++ b/rhodecode/lib/rc_commands/upgrade_db.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/repo_maintenance.py b/rhodecode/lib/repo_maintenance.py --- a/rhodecode/lib/repo_maintenance.py +++ b/rhodecode/lib/repo_maintenance.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/request.py b/rhodecode/lib/request.py --- a/rhodecode/lib/request.py +++ b/rhodecode/lib/request.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/request_counter.py b/rhodecode/lib/request_counter.py --- a/rhodecode/lib/request_counter.py +++ b/rhodecode/lib/request_counter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/str_utils.py b/rhodecode/lib/str_utils.py --- a/rhodecode/lib/str_utils.py +++ b/rhodecode/lib/str_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -67,10 +67,7 @@ def base64_to_str(text: str | bytes) -> def get_default_encodings() -> list[str]: - return aslist(rhodecode.CONFIG.get('default_encoding', 'utf8'), sep=',') - - -DEFAULT_ENCODINGS = get_default_encodings() + return rhodecode.ConfigGet().get_list('default_encoding', missing='utf8') def safe_str(str_, to_encoding=None) -> str: @@ -87,7 +84,7 @@ def safe_str(str_, to_encoding=None) -> if not isinstance(str_, bytes): return str(str_) - to_encoding = to_encoding or DEFAULT_ENCODINGS + to_encoding = to_encoding or get_default_encodings() if not isinstance(to_encoding, (list, tuple)): to_encoding = [to_encoding] @@ -120,7 +117,7 @@ def safe_bytes(str_, from_encoding=None) for enc in from_encoding: try: return str_.encode(enc) - except UnicodeDecodeError: + except (UnicodeDecodeError, UnicodeEncodeError): pass return str_.encode(from_encoding[0], 'replace') diff --git a/rhodecode/lib/string_renderer.py b/rhodecode/lib/string_renderer.py --- a/rhodecode/lib/string_renderer.py +++ b/rhodecode/lib/string_renderer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/svn_txn_utils.py b/rhodecode/lib/svn_txn_utils.py --- a/rhodecode/lib/svn_txn_utils.py +++ b/rhodecode/lib/svn_txn_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/type_utils.py b/rhodecode/lib/type_utils.py --- a/rhodecode/lib/type_utils.py +++ b/rhodecode/lib/type_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/user_log_filter.py b/rhodecode/lib/user_log_filter.py --- a/rhodecode/lib/user_log_filter.py +++ b/rhodecode/lib/user_log_filter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/user_sessions.py b/rhodecode/lib/user_sessions.py --- a/rhodecode/lib/user_sessions.py +++ b/rhodecode/lib/user_sessions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,6 +21,7 @@ Utilities library for RhodeCode """ import datetime +import importlib import decorator import logging @@ -42,8 +43,9 @@ from webhelpers2.text import collapse, s from mako import exceptions +import rhodecode from rhodecode import ConfigGet -from rhodecode.lib.exceptions import HTTPBranchProtected, HTTPLockedRC +from rhodecode.lib.exceptions import HTTPBranchProtected, HTTPLockedRepo, ClientNotSupported from rhodecode.lib.hash_utils import sha256_safe, md5, sha1 from rhodecode.lib.type_utils import AttributeDict from rhodecode.lib.str_utils import safe_bytes, safe_str @@ -86,6 +88,7 @@ def adopt_for_celery(func): @wraps(func) def wrapper(extras): extras = AttributeDict(extras) + try: # HooksResponse implements to_json method which must be used there. return func(extras).to_json() @@ -100,7 +103,18 @@ def adopt_for_celery(func): 'exception_args': error_args, 'exception_traceback': '', } - except HTTPLockedRC as error: + except ClientNotSupported as error: + # Those special cases don't need error reporting. It's a case of + # locked repo or protected branch + error_args = error.args + return { + 'status': error.code, + 'output': error.explanation, + 'exception': type(error).__name__, + 'exception_args': error_args, + 'exception_traceback': '', + } + except HTTPLockedRepo as error: # Those special cases don't need error reporting. It's a case of # locked repo or protected branch error_args = error.args @@ -117,7 +131,7 @@ def adopt_for_celery(func): 'output': '', 'exception': type(e).__name__, 'exception_args': e.args, - 'exception_traceback': '', + 'exception_traceback': traceback.format_exc(), } return wrapper @@ -411,6 +425,10 @@ def prepare_config_data(clear_session=Tr ('web', 'push_ssl', 'false'), ] for setting in ui_settings: + # skip certain deprecated keys that might be still in DB + if f"{setting.section}_{setting.key}" in ['extensions_hgsubversion']: + continue + # Todo: remove this section once transition to *.ini files will be completed if setting.section in ('largefiles', 'vcs_git_lfs'): if setting.key != 'enabled': @@ -564,23 +582,19 @@ def map_groups(path): return group -def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False): +def repo2db_mapper(initial_repo_list, force_hooks_rebuild=False): """ - maps all repos given in initial_repo_list, non existing repositories - are created, if remove_obsolete is True it also checks for db entries - that are not in initial_repo_list and removes them. - - :param initial_repo_list: list of repositories found by scanning methods - :param remove_obsolete: check for obsolete entries in database + maps all repos given in initial_repo_list, non-existing repositories + are created """ from rhodecode.model.repo import RepoModel - from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.settings import SettingsModel sa = meta.Session() repo_model = RepoModel() user = User.get_first_super_admin() added = [] + errors = [] # creation defaults defs = SettingsModel().get_default_repo_settings(strip_prefix=True) @@ -598,9 +612,7 @@ def repo2db_mapper(initial_repo_list, re if not db_repo: log.info('repository `%s` not found in the database, creating now', name) added.append(name) - desc = (repo.description - if repo.description != 'unknown' - else '%s repository' % name) + desc = repo.description if repo.description != 'unknown' else f'{name} repository' db_repo = repo_model._create_repo( repo_name=name, @@ -615,93 +627,151 @@ def repo2db_mapper(initial_repo_list, re state=Repository.STATE_CREATED ) sa.commit() + + try: + config = db_repo._config + config.set('extensions', 'largefiles', '') + scm_repo = db_repo.scm_instance(config=config) + except Exception: + log.error(traceback.format_exc()) + errors.append(f'getting vcs instance for {name} failed') + continue + + try: + db_repo.update_commit_cache(recursive=False) + except Exception: + log.error(traceback.format_exc()) + errors.append(f'update_commit_cache for {name} failed') + continue + + try: + scm_repo.install_hooks(force=force_hooks_rebuild) + except Exception: + log.error(traceback.format_exc()) + errors.append(f'install_hooks for {name} failed') + continue + + try: # we added that repo just now, and make sure we updated server info if db_repo.repo_type == 'git': - git_repo = db_repo.scm_instance() # update repository server-info log.debug('Running update server info') - git_repo._update_server_info(force=True) - - db_repo.update_commit_cache(recursive=False) + scm_repo._update_server_info(force=True) + except Exception: + log.error(traceback.format_exc()) + errors.append(f'update_server_info for {name} failed') + continue - config = db_repo._config - config.set('extensions', 'largefiles', '') - repo = db_repo.scm_instance(config=config) - repo.install_hooks(force=force_hooks_rebuild) + return added, errors +def repo2db_cleanup(skip_repos=None, skip_groups=None): + from rhodecode.model.repo import RepoModel + from rhodecode.model.repo_group import RepoGroupModel + + sa = meta.Session() removed = [] - if remove_obsolete: - # remove from database those repositories that are not in the filesystem - for repo in sa.query(Repository).all(): - if repo.repo_name not in list(initial_repo_list.keys()): - log.debug("Removing non-existing repository found in db `%s`", - repo.repo_name) - try: - RepoModel(sa).delete(repo, forks='detach', fs_remove=False) - sa.commit() - removed.append(repo.repo_name) - except Exception: - # don't hold further removals on error - log.error(traceback.format_exc()) - sa.rollback() + errors = [] + + + all_repos = Repository.execute( + Repository.select(Repository)\ + .order_by(Repository.repo_name) + ).scalars() - def splitter(full_repo_name): - _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1) - gr_name = None - if len(_parts) == 2: - gr_name = _parts[0] - return gr_name + # remove from database those repositories that are not in the filesystem + for db_repo in all_repos: + db_repo_name = db_repo.repo_name + if skip_repos and db_repo_name in skip_repos: + log.debug('Skipping repo `%s`', db_repo_name) + continue + try: + instance = db_repo.scm_instance() + except Exception: + instance = None + + if not instance: + log.debug("Removing non-existing repository found in db `%s`", db_repo_name) + try: + RepoModel(sa).delete(db_repo, forks='detach', fs_remove=False, call_events=False) + sa.commit() + removed.append(db_repo_name) + except Exception: + # don't hold further removals on error + log.error(traceback.format_exc()) + sa.rollback() + errors.append(db_repo_name) - initial_repo_group_list = [splitter(x) for x in - list(initial_repo_list.keys()) if splitter(x)] + # remove from database those repository groups that are not in the + # filesystem due to parent child relationships we need to delete them + # in a specific order of most nested first + all_groups = RepoGroup.execute( + RepoGroup.select(RepoGroup.group_name)\ + .order_by(RepoGroup.group_name) + ).scalars().all() + + def nested_sort(gr): + return len(gr.split('/')) - # remove from database those repository groups that are not in the - # filesystem due to parent child relationships we need to delete them - # in a specific order of most nested first - all_groups = [x.group_name for x in sa.query(RepoGroup).all()] - def nested_sort(gr): - return len(gr.split('/')) - for group_name in sorted(all_groups, key=nested_sort, reverse=True): - if group_name not in initial_repo_group_list: - repo_group = RepoGroup.get_by_group_name(group_name) - if (repo_group.children.all() or - not RepoGroupModel().check_exist_filesystem( - group_name=group_name, exc_on_failure=False)): - continue + for group_name in sorted(all_groups, key=nested_sort, reverse=True): + if skip_groups and group_name in skip_groups: + log.debug('Skipping repo group `%s`', group_name) + continue + + repo_group = RepoGroup.get_by_group_name(group_name) + + if repo_group.children.all() or not RepoGroupModel().check_exist_filesystem(group_name=group_name, exc_on_failure=False): + continue + + log.info('Removing non-existing repository group found in db `%s`', group_name) - log.info( - 'Removing non-existing repository group found in db `%s`', - group_name) - try: - RepoGroupModel(sa).delete(group_name, fs_remove=False) - sa.commit() - removed.append(group_name) - except Exception: - # don't hold further removals on error - log.exception( - 'Unable to remove repository group `%s`', - group_name) - sa.rollback() - raise + try: + RepoGroupModel(sa).delete(group_name, fs_remove=False, call_events=False) + sa.commit() + removed.append(group_name) + except Exception: + # don't hold further removals on error + log.exception('Unable to remove repository group `%s`',group_name) + sa.rollback() + errors.append(group_name) + + return removed, errors + - return added, removed +def deep_reload_package(package_name): + """ + Deeply reload a package by removing it and its submodules from sys.modules, + then re-importing it. + """ + # Remove the package and its submodules from sys.modules + to_reload = [name for name in sys.modules if name == package_name or name.startswith(package_name + ".")] + for module_name in to_reload: + del sys.modules[module_name] + log.debug(f"Removed module from cache: {module_name}") + # Re-import the package + package = importlib.import_module(package_name) + log.debug(f"Re-imported package: {package_name}") + + return package def load_rcextensions(root_path): import rhodecode from rhodecode.config import conf path = os.path.join(root_path) - sys.path.append(path) + deep_reload = path in sys.path + sys.path.insert(0, path) try: - rcextensions = __import__('rcextensions') + rcextensions = __import__('rcextensions', fromlist=['']) except ImportError: if os.path.isdir(os.path.join(path, 'rcextensions')): log.warning('Unable to load rcextensions from %s', path) rcextensions = None if rcextensions: + if deep_reload: + rcextensions = deep_reload_package('rcextensions') log.info('Loaded rcextensions from %s...', rcextensions) rhodecode.EXTENSIONS = rcextensions @@ -741,6 +811,7 @@ def create_test_index(repo_location, con except ImportError: raise ImportError('Failed to import rc_testdata, ' 'please make sure this package is installed from requirements_test.txt') + rc_testdata.extract_search_index( 'vcs_search_index', os.path.dirname(config['search.location'])) @@ -785,22 +856,15 @@ def create_test_repositories(test_path, Creates test repositories in the temporary directory. Repositories are extracted from archives within the rc_testdata package. """ - import rc_testdata - from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO - - log.debug('making test vcs repositories') - - idx_path = config['search.location'] - data_path = config['cache_dir'] + try: + import rc_testdata + except ImportError: + raise ImportError('Failed to import rc_testdata, ' + 'please make sure this package is installed from requirements_test.txt') - # clean index and data - if idx_path and os.path.exists(idx_path): - log.debug('remove %s', idx_path) - shutil.rmtree(idx_path) + from rhodecode.bootstrap import HG_REPO, GIT_REPO, SVN_REPO - if data_path and os.path.exists(data_path): - log.debug('remove %s', data_path) - shutil.rmtree(data_path) + log.debug('making test vcs repositories at %s', test_path) rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO)) rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO)) diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -153,6 +153,7 @@ def remove_prefix(s, prefix): def find_calling_context(ignore_modules=None, depth=4, output_writer=None, indent=True): """ + How to find calling context: Look through the calling stack and return the frame which called this function and is part of core module ( ie. rhodecode.* ) diff --git a/rhodecode/lib/vcs/__init__.py b/rhodecode/lib/vcs/__init__.py --- a/rhodecode/lib/vcs/__init__.py +++ b/rhodecode/lib/vcs/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -139,8 +139,8 @@ class CurlSession(object): try: curl.perform() - except pycurl.error as exc: - log.error('Failed to call endpoint url: {} using pycurl'.format(url)) + except pycurl.error: + log.error('Failed to call endpoint url: %s using pycurl', url) raise status_code = curl.getinfo(pycurl.HTTP_CODE) diff --git a/rhodecode/lib/vcs/backends/__init__.py b/rhodecode/lib/vcs/backends/__init__.py --- a/rhodecode/lib/vcs/backends/__init__.py +++ b/rhodecode/lib/vcs/backends/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -58,9 +58,10 @@ def get_vcs_instance(repo_path, *args, * raise VCSError(f"Given path {repo_path} is not a directory") except VCSError: log.exception( - 'Perhaps this repository is in db and not in ' - 'filesystem run rescan repositories with ' - '"destroy old data" option from admin panel') + 'Perhaps this repository is in db and not in filesystem.' + 'Run cleanup filesystem option from admin settings under Remap and rescan' + ) + return None return backend(repo_path=repo_path, *args, **kwargs) diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,6 +19,7 @@ """ Base module for all VCS systems """ + import os import re import time @@ -39,19 +40,25 @@ from rhodecode.lib.utils2 import safe_st from rhodecode.lib.vcs.utils import author_name, author_email from rhodecode.lib.vcs.conf import settings from rhodecode.lib.vcs.exceptions import ( - CommitError, EmptyRepositoryError, NodeAlreadyAddedError, - NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError, - NodeDoesNotExistError, NodeNotChangedError, VCSError, - ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError, - RepositoryError) + CommitError, + EmptyRepositoryError, + NodeAlreadyAddedError, + NodeAlreadyChangedError, + NodeAlreadyExistsError, + NodeAlreadyRemovedError, + NodeDoesNotExistError, + NodeNotChangedError, + VCSError, + ImproperArchiveTypeError, + BranchDoesNotExistError, + CommitDoesNotExistError, + RepositoryError, +) log = logging.getLogger(__name__) - -FILEMODE_DEFAULT = 0o100644 -FILEMODE_EXECUTABLE = 0o100755 -EMPTY_COMMIT_ID = '0' * 40 +EMPTY_COMMIT_ID = "0" * 40 @dataclasses.dataclass @@ -67,12 +74,12 @@ class Reference: @property def branch(self): - if self.type == 'branch': + if self.type == "branch": return self.name @property def bookmark(self): - if self.type == 'book': + if self.type == "book": return self.name @property @@ -80,11 +87,7 @@ class Reference: return reference_to_unicode(self) def asdict(self): - return dict( - type=self.type, - name=self.name, - commit_id=self.commit_id - ) + return dict(type=self.type, name=self.name, commit_id=self.commit_id) def unicode_to_reference(raw: str): @@ -93,7 +96,7 @@ def unicode_to_reference(raw: str): If unicode evaluates to False it returns None. """ if raw: - refs = raw.split(':') + refs = raw.split(":") return Reference(*refs) else: return None @@ -105,7 +108,7 @@ def reference_to_unicode(ref: Reference) If reference is None it returns None. """ if ref: - return ':'.join(ref) + return ":".join(ref) else: return None @@ -194,47 +197,44 @@ class UpdateFailureReason(object): class MergeResponse(object): - # uses .format(**metadata) for variables MERGE_STATUS_MESSAGES = { - MergeFailureReason.NONE: lazy_ugettext( - 'This pull request can be automatically merged.'), + MergeFailureReason.NONE: lazy_ugettext("This pull request can be automatically merged."), MergeFailureReason.UNKNOWN: lazy_ugettext( - 'This pull request cannot be merged because of an unhandled exception. ' - '{exception}'), + "This pull request cannot be merged because of an unhandled exception. " "{exception}" + ), MergeFailureReason.MERGE_FAILED: lazy_ugettext( - 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'), + "This pull request cannot be merged because of merge conflicts. {unresolved_files}" + ), MergeFailureReason.PUSH_FAILED: lazy_ugettext( - 'This pull request could not be merged because push to ' - 'target:`{target}@{merge_commit}` failed.'), + "This pull request could not be merged because push to " "target:`{target}@{merge_commit}` failed." + ), MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext( - 'This pull request cannot be merged because the target ' - '`{target_ref.name}` is not a head.'), + "This pull request cannot be merged because the target " "`{target_ref.name}` is not a head." + ), MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext( - 'This pull request cannot be merged because the source contains ' - 'more branches than the target.'), + "This pull request cannot be merged because the source contains " "more branches than the target." + ), MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext( - 'This pull request cannot be merged because the target `{target_ref.name}` ' - 'has multiple heads: `{heads}`.'), + "This pull request cannot be merged because the target `{target_ref.name}` " + "has multiple heads: `{heads}`." + ), MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext( - 'This pull request cannot be merged because the target repository is ' - 'locked by {locked_by}.'), - + "This pull request cannot be merged because the target repository is " "locked by {locked_by}." + ), MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext( - 'This pull request cannot be merged because the target ' - 'reference `{target_ref.name}` is missing.'), + "This pull request cannot be merged because the target " "reference `{target_ref.name}` is missing." + ), MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext( - 'This pull request cannot be merged because the source ' - 'reference `{source_ref.name}` is missing.'), + "This pull request cannot be merged because the source " "reference `{source_ref.name}` is missing." + ), MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext( - 'This pull request cannot be merged because of conflicts related ' - 'to sub repositories.'), - + "This pull request cannot be merged because of conflicts related " "to sub repositories." + ), # Deprecations MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext( - 'This pull request cannot be merged because the target or the ' - 'source reference is missing.'), - + "This pull request cannot be merged because the target or the " "source reference is missing." + ), } def __init__(self, possible, executed, merge_ref: Reference, failure_reason, metadata=None): @@ -245,19 +245,20 @@ class MergeResponse(object): self.metadata = metadata or {} def __repr__(self): - return f'' + return f"" def __eq__(self, other): same_instance = isinstance(other, self.__class__) - return same_instance \ - and self.possible == other.possible \ - and self.executed == other.executed \ - and self.failure_reason == other.failure_reason + return ( + same_instance + and self.possible == other.possible + and self.executed == other.executed + and self.failure_reason == other.failure_reason + ) @property def label(self): - label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if - not k.startswith('_')) + label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if not k.startswith("_")) return label_dict.get(self.failure_reason) @property @@ -270,13 +271,12 @@ class MergeResponse(object): try: return msg.format(**self.metadata) except Exception: - log.exception('Failed to format %s message', self) + log.exception("Failed to format %s message", self) return msg def asdict(self): data = {} - for k in ['possible', 'executed', 'merge_ref', 'failure_reason', - 'merge_status_message']: + for k in ["possible", "executed", "merge_ref", "failure_reason", "merge_status_message"]: data[k] = getattr(self, k) return data @@ -289,607 +289,7 @@ class SourceRefMissing(ValueError): pass -class BaseRepository(object): - """ - Base Repository for final backends - - .. attribute:: DEFAULT_BRANCH_NAME - - name of default branch (i.e. "trunk" for svn, "master" for git etc. - - .. attribute:: commit_ids - - list of all available commit ids, in ascending order - - .. attribute:: path - - absolute path to the repository - - .. attribute:: bookmarks - - Mapping from name to :term:`Commit ID` of the bookmark. Empty in case - there are no bookmarks or the backend implementation does not support - bookmarks. - - .. attribute:: tags - - Mapping from name to :term:`Commit ID` of the tag. - - """ - - DEFAULT_BRANCH_NAME = None - DEFAULT_CONTACT = "Unknown" - DEFAULT_DESCRIPTION = "unknown" - EMPTY_COMMIT_ID = '0' * 40 - COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}') - - path = None - - _is_empty = None - _commit_ids = {} - - def __init__(self, repo_path, config=None, create=False, **kwargs): - """ - Initializes repository. Raises RepositoryError if repository could - not be find at the given ``repo_path`` or directory at ``repo_path`` - exists and ``create`` is set to True. - - :param repo_path: local path of the repository - :param config: repository configuration - :param create=False: if set to True, would try to create repository. - :param src_url=None: if set, should be proper url from which repository - would be cloned; requires ``create`` parameter to be set to True - - raises RepositoryError if src_url is set and create evaluates to - False - """ - raise NotImplementedError - - def __repr__(self): - return f'<{self.__class__.__name__} at {self.path}>' - - def __len__(self): - return self.count() - - def __eq__(self, other): - same_instance = isinstance(other, self.__class__) - return same_instance and other.path == self.path - - def __ne__(self, other): - return not self.__eq__(other) - - def get_create_shadow_cache_pr_path(self, db_repo): - path = db_repo.cached_diffs_dir - if not os.path.exists(path): - os.makedirs(path, 0o755) - return path - - @classmethod - def get_default_config(cls, default=None): - config = Config() - if default and isinstance(default, list): - for section, key, val in default: - config.set(section, key, val) - return config - - @LazyProperty - def _remote(self): - raise NotImplementedError - - def _heads(self, branch=None): - return [] - - @LazyProperty - def EMPTY_COMMIT(self): - return EmptyCommit(self.EMPTY_COMMIT_ID) - - @LazyProperty - def alias(self): - for k, v in settings.BACKENDS.items(): - if v.split('.')[-1] == str(self.__class__.__name__): - return k - - @LazyProperty - def name(self): - return safe_str(os.path.basename(self.path)) - - @LazyProperty - def description(self): - raise NotImplementedError - - def refs(self): - """ - returns a `dict` with branches, bookmarks, tags, and closed_branches - for this repository - """ - return dict( - branches=self.branches, - branches_closed=self.branches_closed, - tags=self.tags, - bookmarks=self.bookmarks - ) - - @LazyProperty - def branches(self): - """ - A `dict` which maps branch names to commit ids. - """ - raise NotImplementedError - - @LazyProperty - def branches_closed(self): - """ - A `dict` which maps tags names to commit ids. - """ - raise NotImplementedError - - @LazyProperty - def bookmarks(self): - """ - A `dict` which maps tags names to commit ids. - """ - raise NotImplementedError - - @LazyProperty - def tags(self): - """ - A `dict` which maps tags names to commit ids. - """ - raise NotImplementedError - - @LazyProperty - def size(self): - """ - Returns combined size in bytes for all repository files - """ - tip = self.get_commit() - return tip.size - - def size_at_commit(self, commit_id): - commit = self.get_commit(commit_id) - return commit.size - - def _check_for_empty(self): - no_commits = len(self._commit_ids) == 0 - if no_commits: - # check on remote to be sure - return self._remote.is_empty() - else: - return False - - def is_empty(self): - if rhodecode.is_test: - return self._check_for_empty() - - if self._is_empty is None: - # cache empty for production, but not tests - self._is_empty = self._check_for_empty() - - return self._is_empty - - @staticmethod - def check_url(url, config): - """ - Function will check given url and try to verify if it's a valid - link. - """ - raise NotImplementedError - - @staticmethod - def is_valid_repository(path): - """ - Check if given `path` contains a valid repository of this backend - """ - raise NotImplementedError - - # ========================================================================== - # COMMITS - # ========================================================================== - - @CachedProperty - def commit_ids(self): - raise NotImplementedError - - def append_commit_id(self, commit_id): - if commit_id not in self.commit_ids: - self._rebuild_cache(self.commit_ids + [commit_id]) - - # clear cache - self._invalidate_prop_cache('commit_ids') - self._is_empty = False - - def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, - translate_tag=None, maybe_unreachable=False, reference_obj=None): - """ - Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx` - are both None, most recent commit is returned. - - :param pre_load: Optional. List of commit attributes to load. - - :raises ``EmptyRepositoryError``: if there are no commits - """ - raise NotImplementedError - - def __iter__(self): - for commit_id in self.commit_ids: - yield self.get_commit(commit_id=commit_id) - - def get_commits( - self, start_id=None, end_id=None, start_date=None, end_date=None, - branch_name=None, show_hidden=False, pre_load=None, translate_tags=None): - """ - Returns iterator of `BaseCommit` objects from start to end - not inclusive. This should behave just like a list, ie. end is not - inclusive. - - :param start_id: None or str, must be a valid commit id - :param end_id: None or str, must be a valid commit id - :param start_date: - :param end_date: - :param branch_name: - :param show_hidden: - :param pre_load: - :param translate_tags: - """ - raise NotImplementedError - - def __getitem__(self, key): - """ - Allows index based access to the commit objects of this repository. - """ - pre_load = ["author", "branch", "date", "message", "parents"] - if isinstance(key, slice): - return self._get_range(key, pre_load) - return self.get_commit(commit_idx=key, pre_load=pre_load) - - def _get_range(self, slice_obj, pre_load): - for commit_id in self.commit_ids.__getitem__(slice_obj): - yield self.get_commit(commit_id=commit_id, pre_load=pre_load) - - def count(self): - return len(self.commit_ids) - - def tag(self, name, user, commit_id=None, message=None, date=None, **opts): - """ - Creates and returns a tag for the given ``commit_id``. - - :param name: name for new tag - :param user: full username, i.e.: "Joe Doe " - :param commit_id: commit id for which new tag would be created - :param message: message of the tag's commit - :param date: date of tag's commit - - :raises TagAlreadyExistError: if tag with same name already exists - """ - raise NotImplementedError - - def remove_tag(self, name, user, message=None, date=None): - """ - Removes tag with the given ``name``. - - :param name: name of the tag to be removed - :param user: full username, i.e.: "Joe Doe " - :param message: message of the tag's removal commit - :param date: date of tag's removal commit - - :raises TagDoesNotExistError: if tag with given name does not exists - """ - raise NotImplementedError - - def get_diff( - self, commit1, commit2, path=None, ignore_whitespace=False, - context=3, path1=None): - """ - Returns (git like) *diff*, as plain text. Shows changes introduced by - `commit2` since `commit1`. - - :param commit1: Entry point from which diff is shown. Can be - ``self.EMPTY_COMMIT`` - in this case, patch showing all - the changes since empty state of the repository until `commit2` - :param commit2: Until which commit changes should be shown. - :param path: Can be set to a path of a file to create a diff of that - file. If `path1` is also set, this value is only associated to - `commit2`. - :param ignore_whitespace: If set to ``True``, would not show whitespace - changes. Defaults to ``False``. - :param context: How many lines before/after changed lines should be - shown. Defaults to ``3``. - :param path1: Can be set to a path to associate with `commit1`. This - parameter works only for backends which support diff generation for - different paths. Other backends will raise a `ValueError` if `path1` - is set and has a different value than `path`. - :param file_path: filter this diff by given path pattern - """ - raise NotImplementedError - - def strip(self, commit_id, branch=None): - """ - Strip given commit_id from the repository - """ - raise NotImplementedError - - def get_common_ancestor(self, commit_id1, commit_id2, repo2): - """ - Return a latest common ancestor commit if one exists for this repo - `commit_id1` vs `commit_id2` from `repo2`. - - :param commit_id1: Commit it from this repository to use as a - target for the comparison. - :param commit_id2: Source commit id to use for comparison. - :param repo2: Source repository to use for comparison. - """ - raise NotImplementedError - - def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): - """ - Compare this repository's revision `commit_id1` with `commit_id2`. - - Returns a tuple(commits, ancestor) that would be merged from - `commit_id2`. Doing a normal compare (``merge=False``), ``None`` - will be returned as ancestor. - - :param commit_id1: Commit it from this repository to use as a - target for the comparison. - :param commit_id2: Source commit id to use for comparison. - :param repo2: Source repository to use for comparison. - :param merge: If set to ``True`` will do a merge compare which also - returns the common ancestor. - :param pre_load: Optional. List of commit attributes to load. - """ - raise NotImplementedError - - def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref, - user_name='', user_email='', message='', dry_run=False, - use_rebase=False, close_branch=False): - """ - Merge the revisions specified in `source_ref` from `source_repo` - onto the `target_ref` of this repository. - - `source_ref` and `target_ref` are named tupls with the following - fields `type`, `name` and `commit_id`. - - Returns a MergeResponse named tuple with the following fields - 'possible', 'executed', 'source_commit', 'target_commit', - 'merge_commit'. - - :param repo_id: `repo_id` target repo id. - :param workspace_id: `workspace_id` unique identifier. - :param target_ref: `target_ref` points to the commit on top of which - the `source_ref` should be merged. - :param source_repo: The repository that contains the commits to be - merged. - :param source_ref: `source_ref` points to the topmost commit from - the `source_repo` which should be merged. - :param user_name: Merge commit `user_name`. - :param user_email: Merge commit `user_email`. - :param message: Merge commit `message`. - :param dry_run: If `True` the merge will not take place. - :param use_rebase: If `True` commits from the source will be rebased - on top of the target instead of being merged. - :param close_branch: If `True` branch will be close before merging it - """ - if dry_run: - message = message or settings.MERGE_DRY_RUN_MESSAGE - user_email = user_email or settings.MERGE_DRY_RUN_EMAIL - user_name = user_name or settings.MERGE_DRY_RUN_USER - else: - if not user_name: - raise ValueError('user_name cannot be empty') - if not user_email: - raise ValueError('user_email cannot be empty') - if not message: - raise ValueError('message cannot be empty') - - try: - return self._merge_repo( - repo_id, workspace_id, target_ref, source_repo, - source_ref, message, user_name, user_email, dry_run=dry_run, - use_rebase=use_rebase, close_branch=close_branch) - except RepositoryError as exc: - log.exception('Unexpected failure when running merge, dry-run=%s', dry_run) - return MergeResponse( - False, False, None, MergeFailureReason.UNKNOWN, - metadata={'exception': str(exc)}) - - def _merge_repo(self, repo_id, workspace_id, target_ref, - source_repo, source_ref, merge_message, - merger_name, merger_email, dry_run=False, - use_rebase=False, close_branch=False): - """Internal implementation of merge.""" - raise NotImplementedError - - def _maybe_prepare_merge_workspace( - self, repo_id, workspace_id, target_ref, source_ref): - """ - Create the merge workspace. - - :param workspace_id: `workspace_id` unique identifier. - """ - raise NotImplementedError - - @classmethod - def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id): - """ - Legacy version that was used before. We still need it for - backward compat - """ - return os.path.join( - os.path.dirname(repo_path), - f'.__shadow_{os.path.basename(repo_path)}_{workspace_id}') - - @classmethod - def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id): - # The name of the shadow repository must start with '.', so it is - # skipped by 'rhodecode.lib.utils.get_filesystem_repos'. - legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id) - if os.path.exists(legacy_repository_path): - return legacy_repository_path - else: - return os.path.join( - os.path.dirname(repo_path), - f'.__shadow_repo_{repo_id}_{workspace_id}') - - def cleanup_merge_workspace(self, repo_id, workspace_id): - """ - Remove merge workspace. - - This function MUST not fail in case there is no workspace associated to - the given `workspace_id`. - - :param workspace_id: `workspace_id` unique identifier. - """ - shadow_repository_path = self._get_shadow_repository_path( - self.path, repo_id, workspace_id) - shadow_repository_path_del = '{}.{}.delete'.format( - shadow_repository_path, time.time()) - - # move the shadow repo, so it never conflicts with the one used. - # we use this method because shutil.rmtree had some edge case problems - # removing symlinked repositories - if not os.path.isdir(shadow_repository_path): - return - - shutil.move(shadow_repository_path, shadow_repository_path_del) - try: - shutil.rmtree(shadow_repository_path_del, ignore_errors=False) - except Exception: - log.exception('Failed to gracefully remove shadow repo under %s', - shadow_repository_path_del) - shutil.rmtree(shadow_repository_path_del, ignore_errors=True) - - # ========== # - # COMMIT API # - # ========== # - - @LazyProperty - def in_memory_commit(self): - """ - Returns :class:`InMemoryCommit` object for this repository. - """ - raise NotImplementedError - - # ======================== # - # UTILITIES FOR SUBCLASSES # - # ======================== # - - def _validate_diff_commits(self, commit1, commit2): - """ - Validates that the given commits are related to this repository. - - Intended as a utility for sub classes to have a consistent validation - of input parameters in methods like :meth:`get_diff`. - """ - self._validate_commit(commit1) - self._validate_commit(commit2) - if (isinstance(commit1, EmptyCommit) and - isinstance(commit2, EmptyCommit)): - raise ValueError("Cannot compare two empty commits") - - def _validate_commit(self, commit): - if not isinstance(commit, BaseCommit): - raise TypeError( - "%s is not of type BaseCommit" % repr(commit)) - if commit.repository != self and not isinstance(commit, EmptyCommit): - raise ValueError( - "Commit %s must be a valid commit from this repository %s, " - "related to this repository instead %s." % - (commit, self, commit.repository)) - - def _validate_commit_id(self, commit_id): - if not isinstance(commit_id, str): - raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead") - - def _validate_commit_idx(self, commit_idx): - if not isinstance(commit_idx, int): - raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}") - - def _validate_branch_name(self, branch_name): - if branch_name and branch_name not in self.branches_all: - msg = (f"Branch {branch_name} not found in {self}") - raise BranchDoesNotExistError(msg) - - # - # Supporting deprecated API parts - # TODO: johbo: consider to move this into a mixin - # - - @property - def EMPTY_CHANGESET(self): - warnings.warn( - "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning) - return self.EMPTY_COMMIT_ID - - @property - def revisions(self): - warnings.warn("Use commits attribute instead", DeprecationWarning) - return self.commit_ids - - @revisions.setter - def revisions(self, value): - warnings.warn("Use commits attribute instead", DeprecationWarning) - self.commit_ids = value - - def get_changeset(self, revision=None, pre_load=None): - warnings.warn("Use get_commit instead", DeprecationWarning) - commit_id = None - commit_idx = None - if isinstance(revision, str): - commit_id = revision - else: - commit_idx = revision - return self.get_commit( - commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load) - - def get_changesets( - self, start=None, end=None, start_date=None, end_date=None, - branch_name=None, pre_load=None): - warnings.warn("Use get_commits instead", DeprecationWarning) - start_id = self._revision_to_commit(start) - end_id = self._revision_to_commit(end) - return self.get_commits( - start_id=start_id, end_id=end_id, start_date=start_date, - end_date=end_date, branch_name=branch_name, pre_load=pre_load) - - def _revision_to_commit(self, revision): - """ - Translates a revision to a commit_id - - Helps to support the old changeset based API which allows to use - commit ids and commit indices interchangeable. - """ - if revision is None: - return revision - - if isinstance(revision, str): - commit_id = revision - else: - commit_id = self.commit_ids[revision] - return commit_id - - @property - def in_memory_changeset(self): - warnings.warn("Use in_memory_commit instead", DeprecationWarning) - return self.in_memory_commit - - def get_path_permissions(self, username): - """ - Returns a path permission checker or None if not supported - - :param username: session user name - :return: an instance of BasePathPermissionChecker or None - """ - return None - - def install_hooks(self, force=False): - return self._remote.install_hooks(force) - - def get_hooks_info(self): - return self._remote.get_hooks_info() - - def vcsserver_invalidate_cache(self, delete=False): - return self._remote.vcsserver_invalidate_cache(delete) - - -class BaseCommit(object): +class BaseCommit: """ Each backend should implement it's commit representation. @@ -933,6 +333,7 @@ class BaseCommit(object): list of parent commits """ + repository = None branch = None @@ -942,7 +343,7 @@ class BaseCommit(object): value as ``None``. """ - _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}' + _ARCHIVE_PREFIX_TEMPLATE = "{repo_name}-{short_id}" """ This template is used to generate a default prefix for repository archives if no prefix has been specified. @@ -952,7 +353,7 @@ class BaseCommit(object): return self.__str__() def __str__(self): - return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>' + return f"<{self.__class__.__name__} at {self.idx}:{self.short_id}>" def __eq__(self, other): same_instance = isinstance(other, self.__class__) @@ -962,26 +363,26 @@ class BaseCommit(object): parents = [] try: for parent in self.parents: - parents.append({'raw_id': parent.raw_id}) + parents.append({"raw_id": parent.raw_id}) except NotImplementedError: # empty commit doesn't have parents implemented pass return { - 'short_id': self.short_id, - 'raw_id': self.raw_id, - 'revision': self.idx, - 'message': self.message, - 'date': self.date, - 'author': self.author, - 'parents': parents, - 'branch': self.branch + "short_id": self.short_id, + "raw_id": self.raw_id, + "revision": self.idx, + "message": self.message, + "date": self.date, + "author": self.author, + "parents": parents, + "branch": self.branch, } def __getstate__(self): d = self.__dict__.copy() - d.pop('_remote', None) - d.pop('repository', None) + d.pop("_remote", None) + d.pop("repository", None) return d def get_remote(self): @@ -992,9 +393,9 @@ class BaseCommit(object): def _get_refs(self): return { - 'branches': [self.branch] if self.branch else [], - 'bookmarks': getattr(self, 'bookmarks', []), - 'tags': self.tags + "branches": [self.branch] if self.branch else [], + "bookmarks": getattr(self, "bookmarks", []), + "tags": self.tags, } @LazyProperty @@ -1118,7 +519,7 @@ class BaseCommit(object): """ raise NotImplementedError - def is_link(self, path): + def is_link(self, path: bytes): """ Returns ``True`` if given `path` is a symlink """ @@ -1154,25 +555,24 @@ class BaseCommit(object): """ raise NotImplementedError - def get_path_commit(self, path, pre_load=None): + def get_path_commit(self, path: bytes, pre_load=None): """ Returns last commit of the file at the given `path`. - :param pre_load: Optional. List of commit attributes to load. """ commits = self.get_path_history(path, limit=1, pre_load=pre_load) if not commits: raise RepositoryError( - 'Failed to fetch history for path {}. ' - 'Please check if such path exists in your repository'.format( - path)) + f"Failed to fetch history for path {path}. Please check if such path exists in your repository" + ) return commits[0] - def get_path_history(self, path, limit=None, pre_load=None): + def get_path_history(self, path: bytes, limit=None, pre_load=None): """ Returns history of file as reversed list of :class:`BaseCommit` objects for which file at given `path` has been modified. + :param path: file path or dir path :param limit: Optional. Allows to limit the size of the returned history. This is intended as a hint to the underlying backend, so that it can apply optimizations depending on the limit. @@ -1180,16 +580,17 @@ class BaseCommit(object): """ raise NotImplementedError - def get_file_annotate(self, path, pre_load=None): + def get_file_annotate(self, path: bytes, pre_load=None): """ Returns a generator of four element tuples with lineno, sha, commit lazy loader and line + :param path: file path :param pre_load: Optional. List of commit attributes to load. """ raise NotImplementedError - def get_nodes(self, path, pre_load=None): + def get_nodes(self, path: bytes, pre_load=None): """ Returns combined ``DirNode`` and ``FileNode`` objects list representing state of commit at the given ``path``. @@ -1199,7 +600,7 @@ class BaseCommit(object): """ raise NotImplementedError - def get_node(self, path): + def get_node(self, path: bytes, pre_load=None): """ Returns ``Node`` object from the given ``path``. @@ -1208,16 +609,24 @@ class BaseCommit(object): """ raise NotImplementedError - def get_largefile_node(self, path): + def get_largefile_node(self, path: bytes): """ Returns the path to largefile from Mercurial/Git-lfs storage. or None if it's not a largefile node """ return None - def archive_repo(self, archive_name_key, kind='tgz', subrepos=None, - archive_dir_name=None, write_metadata=False, mtime=None, - archive_at_path='/', cache_config=None): + def archive_repo( + self, + archive_name_key, + kind="tgz", + subrepos=None, + archive_dir_name=None, + write_metadata=False, + mtime=None, + archive_at_path="/", + cache_config=None, + ): """ Creates an archive containing the contents of the repository. @@ -1237,27 +646,26 @@ class BaseCommit(object): cache_config = cache_config or {} allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS] if kind not in allowed_kinds: - raise ImproperArchiveTypeError( - f'Archive kind ({kind}) not supported use one of {allowed_kinds}') + raise ImproperArchiveTypeError(f"Archive kind ({kind}) not supported use one of {allowed_kinds}") archive_dir_name = self._validate_archive_prefix(archive_dir_name) mtime = mtime is not None or time.mktime(self.date.timetuple()) commit_id = self.raw_id return self.repository._remote.archive_repo( - archive_name_key, kind, mtime, archive_at_path, - archive_dir_name, commit_id, cache_config) + archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config + ) def _validate_archive_prefix(self, archive_dir_name): if archive_dir_name is None: archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format( - repo_name=safe_str(self.repository.name), - short_id=self.short_id) + repo_name=safe_str(self.repository.name), short_id=self.short_id + ) elif not isinstance(archive_dir_name, str): raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}") - elif archive_dir_name.startswith('/'): + elif archive_dir_name.startswith("/"): raise VCSError("Prefix cannot start with leading slash") - elif archive_dir_name.strip() == '': + elif archive_dir_name.strip() == "": raise VCSError("Prefix cannot be empty") elif not archive_dir_name.isascii(): raise VCSError("Prefix cannot contain non ascii characters") @@ -1268,7 +676,7 @@ class BaseCommit(object): """ Returns ``RootNode`` object for this commit. """ - return self.get_node('') + return self.get_node(b"") def next(self, branch=None): """ @@ -1292,8 +700,7 @@ class BaseCommit(object): def _find_next(self, indexes, branch=None): if branch and self.branch != branch: - raise VCSError('Branch option used on commit not belonging ' - 'to that branch') + raise VCSError("Branch option used on commit not belonging " "to that branch") for next_idx in indexes: commit = self.repository.get_commit(commit_idx=next_idx) @@ -1307,41 +714,17 @@ class BaseCommit(object): Returns a `Diff` object representing the change made by this commit. """ parent = self.first_parent - diff = self.repository.get_diff( - parent, self, - ignore_whitespace=ignore_whitespace, - context=context) + diff = self.repository.get_diff(parent, self, ignore_whitespace=ignore_whitespace, context=context) return diff @LazyProperty - def added(self): - """ - Returns list of added ``FileNode`` objects. - """ - raise NotImplementedError - - @LazyProperty - def changed(self): - """ - Returns list of modified ``FileNode`` objects. - """ - raise NotImplementedError - - @LazyProperty - def removed(self): - """ - Returns list of removed ``FileNode`` objects. - """ - raise NotImplementedError - - @LazyProperty def size(self): """ Returns total number of bytes from contents of all filenodes. """ return sum(node.size for node in self.get_filenodes_generator()) - def walk(self, topurl=''): + def walk(self, top_url=b""): """ Similar to os.walk method. Insted of filesystem it walks through commit starting at given ``topurl``. Returns generator of tuples @@ -1349,10 +732,10 @@ class BaseCommit(object): """ from rhodecode.lib.vcs.nodes import DirNode - if isinstance(topurl, DirNode): - top_node = topurl + if isinstance(top_url, DirNode): + top_node = top_url else: - top_node = self.get_node(topurl) + top_node = self.get_node(top_url) has_default_pre_load = False if isinstance(top_node, DirNode): @@ -1381,15 +764,20 @@ class BaseCommit(object): def no_node_at_path(self, path): return NodeDoesNotExistError( - f"There is no file nor directory at the given path: " - f"`{safe_str(path)}` at commit {self.short_id}") + f"There is no file nor directory at the given path: " f"`{safe_str(path)}` at commit {self.short_id}" + ) - def _fix_path(self, path: str) -> str: + @classmethod + def _fix_path(cls, path: bytes) -> bytes: """ - Paths are stored without trailing slash so we need to get rid off it if - needed. + Paths are stored without trailing slash so we need to get rid off it if needed. + It also validates that path is a bytestring for support of mixed encodings... """ - return safe_str(path).rstrip('/') + + if not isinstance(path, bytes): + raise ValueError(f'path=`{safe_str(path)}` must be bytes') + + return path.rstrip(b"/") # # Deprecated API based on changesets @@ -1410,17 +798,644 @@ class BaseCommit(object): return self.get_path_commit(path) +class BaseRepository(object): + """ + Base Repository for final backends + + .. attribute:: DEFAULT_BRANCH_NAME + + name of default branch (i.e. "trunk" for svn, "master" for git etc. + + .. attribute:: commit_ids + + list of all available commit ids, in ascending order + + .. attribute:: path + + absolute path to the repository + + .. attribute:: bookmarks + + Mapping from name to :term:`Commit ID` of the bookmark. Empty in case + there are no bookmarks or the backend implementation does not support + bookmarks. + + .. attribute:: tags + + Mapping from name to :term:`Commit ID` of the tag. + + """ + + DEFAULT_BRANCH_NAME = None + DEFAULT_CONTACT = "Unknown" + DEFAULT_DESCRIPTION = "unknown" + EMPTY_COMMIT_ID = "0" * 40 + COMMIT_ID_PAT = re.compile(r"[0-9a-fA-F]{40}") + + path = None + + _is_empty = None + _commit_ids = {} + + def __init__(self, repo_path, config=None, create=False, **kwargs): + """ + Initializes repository. Raises RepositoryError if repository could + not be find at the given ``repo_path`` or directory at ``repo_path`` + exists and ``create`` is set to True. + + :param repo_path: local path of the repository + :param config: repository configuration + :param create=False: if set to True, would try to create repository. + :param src_url=None: if set, should be proper url from which repository + would be cloned; requires ``create`` parameter to be set to True - + raises RepositoryError if src_url is set and create evaluates to + False + """ + raise NotImplementedError + + def __repr__(self): + return f"<{self.__class__.__name__} at {self.path}>" + + def __len__(self): + return self.count() + + def __eq__(self, other): + same_instance = isinstance(other, self.__class__) + return same_instance and other.path == self.path + + def __ne__(self, other): + return not self.__eq__(other) + + @classmethod + def get_create_shadow_cache_pr_path(cls, db_repo): + path = db_repo.cached_diffs_dir + if not os.path.exists(path): + os.makedirs(path, 0o755) + return path + + @classmethod + def get_default_config(cls, default=None): + config = Config() + if default and isinstance(default, list): + for section, key, val in default: + config.set(section, key, val) + return config + + @LazyProperty + def _remote(self): + raise NotImplementedError + + def _heads(self, branch=None): + return [] + + @LazyProperty + def EMPTY_COMMIT(self): + return EmptyCommit(self.EMPTY_COMMIT_ID) + + @LazyProperty + def alias(self): + for k, v in settings.BACKENDS.items(): + if v.split(".")[-1] == str(self.__class__.__name__): + return k + + @LazyProperty + def name(self): + return safe_str(os.path.basename(self.path)) + + @LazyProperty + def description(self): + raise NotImplementedError + + def refs(self): + """ + returns a `dict` with branches, bookmarks, tags, and closed_branches + for this repository + """ + return dict( + branches=self.branches, branches_closed=self.branches_closed, tags=self.tags, bookmarks=self.bookmarks + ) + + @LazyProperty + def branches(self): + """ + A `dict` which maps branch names to commit ids. + """ + raise NotImplementedError + + @LazyProperty + def branches_closed(self): + """ + A `dict` which maps tags names to commit ids. + """ + raise NotImplementedError + + @LazyProperty + def bookmarks(self): + """ + A `dict` which maps tags names to commit ids. + """ + raise NotImplementedError + + @LazyProperty + def tags(self): + """ + A `dict` which maps tags names to commit ids. + """ + raise NotImplementedError + + @LazyProperty + def size(self): + """ + Returns combined size in bytes for all repository files + """ + tip = self.get_commit() + return tip.size + + def size_at_commit(self, commit_id): + commit = self.get_commit(commit_id) + return commit.size + + def _check_for_empty(self): + no_commits = len(self._commit_ids) == 0 + if no_commits: + # check on remote to be sure + return self._remote.is_empty() + else: + return False + + def is_empty(self): + if rhodecode.is_test: + return self._check_for_empty() + + if self._is_empty is None: + # cache empty for production, but not tests + self._is_empty = self._check_for_empty() + + return self._is_empty + + @staticmethod + def check_url(url, config): + """ + Function will check given url and try to verify if it's a valid + link. + """ + raise NotImplementedError + + @staticmethod + def is_valid_repository(path): + """ + Check if given `path` contains a valid repository of this backend + """ + raise NotImplementedError + + # ========================================================================== + # COMMITS + # ========================================================================== + + @CachedProperty + def commit_ids(self): + raise NotImplementedError + + def append_commit_id(self, commit_id): + if commit_id not in self.commit_ids: + self._rebuild_cache(self.commit_ids + [commit_id]) + + # clear cache + self._invalidate_prop_cache("commit_ids") + self._is_empty = False + + def get_commit( + self, + commit_id=None, + commit_idx=None, + pre_load=None, + translate_tag=None, + maybe_unreachable=False, + reference_obj=None, + ) -> BaseCommit: + """ + Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx` + are both None, most recent commit is returned. + + :param pre_load: Optional. List of commit attributes to load. + + :raises ``EmptyRepositoryError``: if there are no commits + """ + raise NotImplementedError + + def __iter__(self): + for commit_id in self.commit_ids: + yield self.get_commit(commit_id=commit_id) + + def get_commits( + self, + start_id=None, + end_id=None, + start_date=None, + end_date=None, + branch_name=None, + show_hidden=False, + pre_load=None, + translate_tags=None, + ): + """ + Returns iterator of `BaseCommit` objects from start to end + not inclusive. This should behave just like a list, ie. end is not + inclusive. + + :param start_id: None or str, must be a valid commit id + :param end_id: None or str, must be a valid commit id + :param start_date: + :param end_date: + :param branch_name: + :param show_hidden: + :param pre_load: + :param translate_tags: + """ + raise NotImplementedError + + def __getitem__(self, key): + """ + Allows index based access to the commit objects of this repository. + """ + pre_load = ["author", "branch", "date", "message", "parents"] + if isinstance(key, slice): + return self._get_range(key, pre_load) + return self.get_commit(commit_idx=key, pre_load=pre_load) + + def _get_range(self, slice_obj, pre_load): + for commit_id in self.commit_ids.__getitem__(slice_obj): + yield self.get_commit(commit_id=commit_id, pre_load=pre_load) + + def count(self): + return len(self.commit_ids) + + def tag(self, name, user, commit_id=None, message=None, date=None, **opts): + """ + Creates and returns a tag for the given ``commit_id``. + + :param name: name for new tag + :param user: full username, i.e.: "Joe Doe " + :param commit_id: commit id for which new tag would be created + :param message: message of the tag's commit + :param date: date of tag's commit + + :raises TagAlreadyExistError: if tag with same name already exists + """ + raise NotImplementedError + + def remove_tag(self, name, user, message=None, date=None): + """ + Removes tag with the given ``name``. + + :param name: name of the tag to be removed + :param user: full username, i.e.: "Joe Doe " + :param message: message of the tag's removal commit + :param date: date of tag's removal commit + + :raises TagDoesNotExistError: if tag with given name does not exists + """ + raise NotImplementedError + + def get_diff(self, commit1, commit2, path=None, ignore_whitespace=False, context=3, path1=None): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + `commit2` since `commit1`. + + :param commit1: Entry point from which diff is shown. Can be + ``self.EMPTY_COMMIT`` - in this case, patch showing all + the changes since empty state of the repository until `commit2` + :param commit2: Until which commit changes should be shown. + :param path: Can be set to a path of a file to create a diff of that + file. If `path1` is also set, this value is only associated to + `commit2`. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + :param path1: Can be set to a path to associate with `commit1`. This + parameter works only for backends which support diff generation for + different paths. Other backends will raise a `ValueError` if `path1` + is set and has a different value than `path`. + :param file_path: filter this diff by given path pattern + """ + raise NotImplementedError + + def strip(self, commit_id, branch=None): + """ + Strip given commit_id from the repository + """ + raise NotImplementedError + + def get_common_ancestor(self, commit_id1, commit_id2, repo2): + """ + Return a latest common ancestor commit if one exists for this repo + `commit_id1` vs `commit_id2` from `repo2`. + + :param commit_id1: Commit it from this repository to use as a + target for the comparison. + :param commit_id2: Source commit id to use for comparison. + :param repo2: Source repository to use for comparison. + """ + raise NotImplementedError + + def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): + """ + Compare this repository's revision `commit_id1` with `commit_id2`. + + Returns a tuple(commits, ancestor) that would be merged from + `commit_id2`. Doing a normal compare (``merge=False``), ``None`` + will be returned as ancestor. + + :param commit_id1: Commit it from this repository to use as a + target for the comparison. + :param commit_id2: Source commit id to use for comparison. + :param repo2: Source repository to use for comparison. + :param merge: If set to ``True`` will do a merge compare which also + returns the common ancestor. + :param pre_load: Optional. List of commit attributes to load. + """ + raise NotImplementedError + + def merge( + self, + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + user_name="", + user_email="", + message="", + dry_run=False, + use_rebase=False, + close_branch=False, + ): + """ + Merge the revisions specified in `source_ref` from `source_repo` + onto the `target_ref` of this repository. + + `source_ref` and `target_ref` are named tupls with the following + fields `type`, `name` and `commit_id`. + + Returns a MergeResponse named tuple with the following fields + 'possible', 'executed', 'source_commit', 'target_commit', + 'merge_commit'. + + :param repo_id: `repo_id` target repo id. + :param workspace_id: `workspace_id` unique identifier. + :param target_ref: `target_ref` points to the commit on top of which + the `source_ref` should be merged. + :param source_repo: The repository that contains the commits to be + merged. + :param source_ref: `source_ref` points to the topmost commit from + the `source_repo` which should be merged. + :param user_name: Merge commit `user_name`. + :param user_email: Merge commit `user_email`. + :param message: Merge commit `message`. + :param dry_run: If `True` the merge will not take place. + :param use_rebase: If `True` commits from the source will be rebased + on top of the target instead of being merged. + :param close_branch: If `True` branch will be close before merging it + """ + if dry_run: + message = message or settings.MERGE_DRY_RUN_MESSAGE + user_email = user_email or settings.MERGE_DRY_RUN_EMAIL + user_name = user_name or settings.MERGE_DRY_RUN_USER + else: + if not user_name: + raise ValueError("user_name cannot be empty") + if not user_email: + raise ValueError("user_email cannot be empty") + if not message: + raise ValueError("message cannot be empty") + + try: + return self._merge_repo( + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + message, + user_name, + user_email, + dry_run=dry_run, + use_rebase=use_rebase, + close_branch=close_branch, + ) + except RepositoryError as exc: + log.exception("Unexpected failure when running merge, dry-run=%s", dry_run) + return MergeResponse(False, False, None, MergeFailureReason.UNKNOWN, metadata={"exception": str(exc)}) + + def _merge_repo( + self, + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + merge_message, + merger_name, + merger_email, + dry_run=False, + use_rebase=False, + close_branch=False, + ): + """Internal implementation of merge.""" + raise NotImplementedError + + def _maybe_prepare_merge_workspace(self, repo_id, workspace_id, target_ref, source_ref): + """ + Create the merge workspace. + + :param workspace_id: `workspace_id` unique identifier. + """ + raise NotImplementedError + + @classmethod + def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id): + """ + Legacy version that was used before. We still need it for + backward compat + """ + return os.path.join(os.path.dirname(repo_path), f".__shadow_{os.path.basename(repo_path)}_{workspace_id}") + + @classmethod + def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id): + # The name of the shadow repository must start with '.', so it is + # skipped by 'rhodecode.lib.utils.get_filesystem_repos'. + legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id) + if os.path.exists(legacy_repository_path): + return legacy_repository_path + else: + return os.path.join(os.path.dirname(repo_path), f".__shadow_repo_{repo_id}_{workspace_id}") + + def cleanup_merge_workspace(self, repo_id, workspace_id): + """ + Remove merge workspace. + + This function MUST not fail in case there is no workspace associated to + the given `workspace_id`. + + :param workspace_id: `workspace_id` unique identifier. + """ + shadow_repository_path = self._get_shadow_repository_path(self.path, repo_id, workspace_id) + shadow_repository_path_del = "{}.{}.delete".format(shadow_repository_path, time.time()) + + # move the shadow repo, so it never conflicts with the one used. + # we use this method because shutil.rmtree had some edge case problems + # removing symlinked repositories + if not os.path.isdir(shadow_repository_path): + return + + shutil.move(shadow_repository_path, shadow_repository_path_del) + try: + shutil.rmtree(shadow_repository_path_del, ignore_errors=False) + except Exception: + log.exception("Failed to gracefully remove shadow repo under %s", shadow_repository_path_del) + shutil.rmtree(shadow_repository_path_del, ignore_errors=True) + + # ========== # + # COMMIT API # + # ========== # + + @LazyProperty + def in_memory_commit(self): + """ + Returns :class:`InMemoryCommit` object for this repository. + """ + raise NotImplementedError + + # ======================== # + # UTILITIES FOR SUBCLASSES # + # ======================== # + + def _validate_diff_commits(self, commit1, commit2): + """ + Validates that the given commits are related to this repository. + + Intended as a utility for sub classes to have a consistent validation + of input parameters in methods like :meth:`get_diff`. + """ + self._validate_commit(commit1) + self._validate_commit(commit2) + if isinstance(commit1, EmptyCommit) and isinstance(commit2, EmptyCommit): + raise ValueError("Cannot compare two empty commits") + + def _validate_commit(self, commit): + if not isinstance(commit, BaseCommit): + raise TypeError("%s is not of type BaseCommit" % repr(commit)) + if commit.repository != self and not isinstance(commit, EmptyCommit): + raise ValueError( + "Commit %s must be a valid commit from this repository %s, " + "related to this repository instead %s." % (commit, self, commit.repository) + ) + + def _validate_commit_id(self, commit_id): + if not isinstance(commit_id, str): + raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead") + + def _validate_commit_idx(self, commit_idx): + if not isinstance(commit_idx, int): + raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}") + + def _validate_branch_name(self, branch_name): + if branch_name and branch_name not in self.branches_all: + msg = f"Branch {branch_name} not found in {self}" + raise BranchDoesNotExistError(msg) + + # + # Supporting deprecated API parts + # TODO: johbo: consider to move this into a mixin + # + + @property + def EMPTY_CHANGESET(self): + warnings.warn("Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning) + return self.EMPTY_COMMIT_ID + + @property + def revisions(self): + warnings.warn("Use commits attribute instead", DeprecationWarning) + return self.commit_ids + + @revisions.setter + def revisions(self, value): + warnings.warn("Use commits attribute instead", DeprecationWarning) + self.commit_ids = value + + def get_changeset(self, revision=None, pre_load=None): + warnings.warn("Use get_commit instead", DeprecationWarning) + commit_id = None + commit_idx = None + if isinstance(revision, str): + commit_id = revision + else: + commit_idx = revision + return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load) + + def get_changesets(self, start=None, end=None, start_date=None, end_date=None, branch_name=None, pre_load=None): + warnings.warn("Use get_commits instead", DeprecationWarning) + start_id = self._revision_to_commit(start) + end_id = self._revision_to_commit(end) + return self.get_commits( + start_id=start_id, + end_id=end_id, + start_date=start_date, + end_date=end_date, + branch_name=branch_name, + pre_load=pre_load, + ) + + def _revision_to_commit(self, revision): + """ + Translates a revision to a commit_id + + Helps to support the old changeset based API which allows to use + commit ids and commit indices interchangeable. + """ + if revision is None: + return revision + + if isinstance(revision, str): + commit_id = revision + else: + commit_id = self.commit_ids[revision] + return commit_id + + @property + def in_memory_changeset(self): + warnings.warn("Use in_memory_commit instead", DeprecationWarning) + return self.in_memory_commit + + def get_path_permissions(self, username): + """ + Returns a path permission checker or None if not supported + + :param username: session user name + :return: an instance of BasePathPermissionChecker or None + """ + return None + + def install_hooks(self, force=False): + return self._remote.install_hooks(force) + + def get_hooks_info(self): + return self._remote.get_hooks_info() + + def vcsserver_invalidate_cache(self, delete=False): + return self._remote.vcsserver_invalidate_cache(delete) + + class BaseChangesetClass(type): - def __instancecheck__(self, instance): return isinstance(instance, BaseCommit) class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass): - def __new__(cls, *args, **kwargs): - warnings.warn( - "Use BaseCommit instead of BaseChangeset", DeprecationWarning) + warnings.warn("Use BaseCommit instead of BaseChangeset", DeprecationWarning) return super().__new__(cls, *args, **kwargs) @@ -1441,8 +1456,7 @@ class BaseInMemoryCommit(object): list of ``FileNode`` objects marked as *changed* ``removed`` - list of ``FileNode`` or ``RemovedFileNode`` objects marked to be - *removed* + list of ``FileNode`` objects marked to be *removed* ``parents`` list of :class:`BaseCommit` instances representing parents of @@ -1469,9 +1483,7 @@ class BaseInMemoryCommit(object): # Check if not already marked as *added* first for node in filenodes: if node.path in (n.path for n in self.added): - raise NodeAlreadyAddedError( - "Such FileNode %s is already marked for addition" - % node.path) + raise NodeAlreadyAddedError(f"Such FileNode {node.path} is already marked for addition") for node in filenodes: self.added.append(node) @@ -1490,24 +1502,19 @@ class BaseInMemoryCommit(object): """ for node in filenodes: if node.path in (n.path for n in self.removed): - raise NodeAlreadyRemovedError( - "Node at %s is already marked as removed" % node.path) + raise NodeAlreadyRemovedError("Node at %s is already marked as removed" % node.path) try: self.repository.get_commit() except EmptyRepositoryError: - raise EmptyRepositoryError( - "Nothing to change - try to *add* new nodes rather than " - "changing them") + raise EmptyRepositoryError("Nothing to change - try to *add* new nodes rather than " "changing them") for node in filenodes: if node.path in (n.path for n in self.changed): - raise NodeAlreadyChangedError( - "Node at '%s' is already marked as changed" % node.path) + raise NodeAlreadyChangedError("Node at '%s' is already marked as changed" % node.path) self.changed.append(node) def remove(self, *filenodes): """ - Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be - *removed* in next commit. + Marks given ``FileNode`` objects to be *removed* in next commit. :raises ``NodeAlreadyRemovedError``: if node has been already marked to be *removed* @@ -1516,11 +1523,9 @@ class BaseInMemoryCommit(object): """ for node in filenodes: if node.path in (n.path for n in self.removed): - raise NodeAlreadyRemovedError( - "Node is already marked to for removal at %s" % node.path) + raise NodeAlreadyRemovedError("Node is already marked to for removal at %s" % node.path) if node.path in (n.path for n in self.changed): - raise NodeAlreadyChangedError( - "Node is already marked to be changed at %s" % node.path) + raise NodeAlreadyChangedError("Node is already marked to be changed at %s" % node.path) # We only mark node as *removed* - real removal is done by # commit method self.removed.append(node) @@ -1575,12 +1580,11 @@ class BaseInMemoryCommit(object): for p in parents: for node in self.added: try: - p.get_node(node.path) + p.get_node(node.bytes_path) except NodeDoesNotExistError: pass else: - raise NodeAlreadyExistsError( - f"Node `{node.path}` already exists at {p}") + raise NodeAlreadyExistsError(f"Node `{node.path}` already exists at {p}") # Check nodes marked as changed missing = set(self.changed) @@ -1590,7 +1594,7 @@ class BaseInMemoryCommit(object): for p in parents: for node in self.changed: try: - old = p.get_node(node.path) + old = p.get_node(node.bytes_path) missing.remove(node) # if content actually changed, remove node from not_changed if old.content != node.content: @@ -1598,24 +1602,23 @@ class BaseInMemoryCommit(object): except NodeDoesNotExistError: pass if self.changed and missing: - raise NodeDoesNotExistError( - f"Node `{node.path}` marked as modified but missing in parents: {parents}") + raise NodeDoesNotExistError(f"Node `{node.path}` marked as modified but missing in parents: {parents}") if self.changed and not_changed: raise NodeNotChangedError( - "Node `%s` wasn't actually changed (parents: %s)" - % (not_changed.pop().path, parents)) + "Node `%s` wasn't actually changed (parents: %s)" % (not_changed.pop().path, parents) + ) # Check nodes marked as removed if self.removed and not parents: raise NodeDoesNotExistError( - "Cannot remove node at %s as there " - "were no parents specified" % self.removed[0].path) + "Cannot remove node at %s as there " "were no parents specified" % self.removed[0].path + ) really_removed = set() for p in parents: for node in self.removed: try: - p.get_node(node.path) + p.get_node(node.bytes_path) really_removed.add(node) except CommitError: pass @@ -1623,8 +1626,8 @@ class BaseInMemoryCommit(object): if not_removed: # TODO: johbo: This code branch does not seem to be covered raise NodeDoesNotExistError( - "Cannot remove node at %s from " - "following parents: %s" % (not_removed, parents)) + "Cannot remove node at %s from " "following parents: %s" % (not_removed, parents) + ) def commit(self, message, author, parents=None, branch=None, date=None, **kwargs): """ @@ -1652,16 +1655,13 @@ class BaseInMemoryCommit(object): class BaseInMemoryChangesetClass(type): - def __instancecheck__(self, instance): return isinstance(instance, BaseInMemoryCommit) class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass): - def __new__(cls, *args, **kwargs): - warnings.warn( - "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning) + warnings.warn("Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning) return super().__new__(cls, *args, **kwargs) @@ -1671,9 +1671,7 @@ class EmptyCommit(BaseCommit): an EmptyCommit """ - def __init__( - self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1, - message='', author='', date=None): + def __init__(self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1, message="", author="", date=None): self._empty_commit_id = commit_id # TODO: johbo: Solve idx parameter, default value does not make # too much sense @@ -1697,6 +1695,7 @@ class EmptyCommit(BaseCommit): def branch(self): if self.alias: from rhodecode.lib.vcs.backends import get_backend + return get_backend(self.alias).DEFAULT_BRANCH_NAME @LazyProperty @@ -1711,7 +1710,7 @@ class EmptyCommit(BaseCommit): return self def get_file_content(self, path) -> bytes: - return b'' + return b"" def get_file_content_streamed(self, path): yield self.get_file_content(path) @@ -1721,27 +1720,29 @@ class EmptyCommit(BaseCommit): class EmptyChangesetClass(type): - def __instancecheck__(self, instance): return isinstance(instance, EmptyCommit) class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass): - def __new__(cls, *args, **kwargs): - warnings.warn( - "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning) + warnings.warn("Use EmptyCommit instead of EmptyChangeset", DeprecationWarning) return super(EmptyCommit, cls).__new__(cls, *args, **kwargs) - def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None, - alias=None, revision=-1, message='', author='', date=None): + def __init__( + self, + cs=EMPTY_COMMIT_ID, + repo=None, + requested_revision=None, + alias=None, + revision=-1, + message="", + author="", + date=None, + ): if requested_revision is not None: - warnings.warn( - "Parameter requested_revision not supported anymore", - DeprecationWarning) - super().__init__( - commit_id=cs, repo=repo, alias=alias, idx=revision, - message=message, author=author, date=date) + warnings.warn("Parameter requested_revision not supported anymore", DeprecationWarning) + super().__init__(commit_id=cs, repo=repo, alias=alias, idx=revision, message=message, author=author, date=date) @property def revision(self): @@ -1760,11 +1761,11 @@ class EmptyRepository(BaseRepository): def get_diff(self, *args, **kwargs): from rhodecode.lib.vcs.backends.git.diff import GitDiff - return GitDiff(b'') + + return GitDiff(b"") class CollectionGenerator(object): - def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None): self.repo = repo self.commit_ids = commit_ids @@ -1786,26 +1787,22 @@ class CollectionGenerator(object): """ Allows backends to override the way commits are generated. """ - return self.repo.get_commit( - commit_id=commit_id, pre_load=self.pre_load, - translate_tag=self.translate_tag) + return self.repo.get_commit(commit_id=commit_id, pre_load=self.pre_load, translate_tag=self.translate_tag) def __getitem__(self, key): """Return either a single element by index, or a sliced collection.""" if isinstance(key, slice): - commit_ids = self.commit_ids[key.start:key.stop] + commit_ids = self.commit_ids[key.start : key.stop] else: # single item commit_ids = self.commit_ids[key] - return self.__class__( - self.repo, commit_ids, pre_load=self.pre_load, - translate_tag=self.translate_tag) + return self.__class__(self.repo, commit_ids, pre_load=self.pre_load, translate_tag=self.translate_tag) def __repr__(self): - return '' % (self.__len__()) + return "" % (self.__len__()) class Config(object): @@ -1826,8 +1823,7 @@ class Config(object): return clone def __repr__(self): - return ''.format( - len(self._values), hex(id(self))) + return "".format(len(self._values), hex(id(self))) def items(self, section): return self._values.get(section, {}).items() @@ -1844,7 +1840,7 @@ class Config(object): def drop_option(self, section, option): if section not in self._values: - raise ValueError(f'Section {section} does not exist') + raise ValueError(f"Section {section} does not exist") del self._values[section][option] def serialize(self): @@ -1855,8 +1851,7 @@ class Config(object): items = [] for section in self._values: for option, value in self._values[section].items(): - items.append( - (safe_str(section), safe_str(option), safe_str(value))) + items.append((safe_str(section), safe_str(option), safe_str(value))) return items @@ -1867,12 +1862,13 @@ class Diff(object): Subclasses have to provide a backend specific value for :attr:`_header_re` and :attr:`_meta_re`. """ + _meta_re = None - _header_re: bytes = re.compile(br"") + _header_re: bytes = re.compile(rb"") def __init__(self, raw_diff: bytes): if not isinstance(raw_diff, bytes): - raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}') + raise Exception(f"raw_diff must be bytes - got {type(raw_diff)}") self.raw = memoryview(raw_diff) @@ -1886,7 +1882,7 @@ class Diff(object): we can detect last chunk as this was also has special rule """ - diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git') + diff_parts = (b"\n" + bytes(self.raw)).split(b"\ndiff --git") chunks = diff_parts[1:] total_chunks = len(chunks) @@ -1894,44 +1890,46 @@ class Diff(object): def diff_iter(_chunks): for cur_chunk, chunk in enumerate(_chunks, start=1): yield DiffChunk(chunk, self, cur_chunk == total_chunks) + return diff_iter(chunks) class DiffChunk(object): - def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool): self.diff_obj = diff_obj # since we split by \ndiff --git that part is lost from original diff # we need to re-apply it at the end, EXCEPT ! if it's last chunk if not is_last_chunk: - chunk += b'\n' + chunk += b"\n" header_re = self.diff_obj.get_header_re() + match = header_re.match(chunk) self.header = match.groupdict() - self.diff = chunk[match.end():] + self.diff = chunk[match.end() :] self.raw = chunk @property def header_as_str(self): if self.header: + def safe_str_on_bytes(val): if isinstance(val, bytes): return safe_str(val) return val + return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()} def __repr__(self): - return f'DiffChunk({self.header_as_str})' + return f"DiffChunk({self.header_as_str})" class BasePathPermissionChecker(object): - @staticmethod def create_from_patterns(includes, excludes): - if includes and '*' in includes and not excludes: + if includes and "*" in includes and not excludes: return AllPathPermissionChecker() - elif excludes and '*' in excludes: + elif excludes and "*" in excludes: return NonePathPermissionChecker() else: return PatternPathPermissionChecker(includes, excludes) @@ -1945,7 +1943,6 @@ class BasePathPermissionChecker(object): class AllPathPermissionChecker(BasePathPermissionChecker): - @property def has_full_access(self): return True @@ -1955,7 +1952,6 @@ class AllPathPermissionChecker(BasePathP class NonePathPermissionChecker(BasePathPermissionChecker): - @property def has_full_access(self): return False @@ -1965,18 +1961,15 @@ class NonePathPermissionChecker(BasePath class PatternPathPermissionChecker(BasePathPermissionChecker): - def __init__(self, includes, excludes): self.includes = includes self.excludes = excludes - self.includes_re = [] if not includes else [ - re.compile(fnmatch.translate(pattern)) for pattern in includes] - self.excludes_re = [] if not excludes else [ - re.compile(fnmatch.translate(pattern)) for pattern in excludes] + self.includes_re = [] if not includes else [re.compile(fnmatch.translate(pattern)) for pattern in includes] + self.excludes_re = [] if not excludes else [re.compile(fnmatch.translate(pattern)) for pattern in excludes] @property def has_full_access(self): - return '*' in self.includes and not self.excludes + return "*" in self.includes and not self.excludes def has_access(self, path): for regex in self.excludes_re: diff --git a/rhodecode/lib/vcs/backends/git/__init__.py b/rhodecode/lib/vcs/backends/git/__init__.py --- a/rhodecode/lib/vcs/backends/git/__init__.py +++ b/rhodecode/lib/vcs/backends/git/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -45,10 +45,3 @@ def discover_git_version(raise_on_exc=Fa if raise_on_exc: raise return '' - - -def lfs_store(base_location): - """ - Return a lfs store relative to base_location - """ - return os.path.join(base_location, '.cache', 'lfs_store') diff --git a/rhodecode/lib/vcs/backends/git/commit.py b/rhodecode/lib/vcs/backends/git/commit.py --- a/rhodecode/lib/vcs/backends/git/commit.py +++ b/rhodecode/lib/vcs/backends/git/commit.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,8 +21,8 @@ GIT commit module """ import io -import stat import configparser +import logging from itertools import chain from zope.cachedescriptors.property import Lazy as LazyProperty @@ -32,9 +32,16 @@ from rhodecode.lib.str_utils import safe from rhodecode.lib.vcs.backends import base from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError from rhodecode.lib.vcs.nodes import ( - FileNode, DirNode, NodeKind, RootNode, SubModuleNode, - ChangedFileNodesGenerator, AddedFileNodesGenerator, - RemovedFileNodesGenerator, LargeFileNode) + FileNode, + DirNode, + NodeKind, + RootNode, + SubModuleNode, + LargeFileNode, +) +from rhodecode.lib.vcs_common import FILEMODE_LINK + +log = logging.getLogger(__name__) class GitCommit(base.BaseCommit): @@ -50,13 +57,11 @@ class GitCommit(base.BaseCommit): # done through a more complex tree walk on parents "status", # mercurial specific property not supported here - "_file_paths", - # mercurial specific property not supported here - 'obsolete', + "obsolete", # mercurial specific property not supported here - 'phase', + "phase", # mercurial specific property not supported here - 'hidden' + "hidden", ] def __init__(self, repository, raw_id, idx, pre_load=None): @@ -69,17 +74,16 @@ class GitCommit(base.BaseCommit): self._set_bulk_properties(pre_load) # caches - self._stat_modes = {} # stat info for paths - self._paths = {} # path processed with parse_tree self.nodes = {} + self._path_mode_cache = {} # path stats cache, e.g filemode etc + self._path_type_cache = {} # path type dir/file/link etc cache + self._submodules = None def _set_bulk_properties(self, pre_load): - if not pre_load: return - pre_load = [entry for entry in pre_load - if entry not in self._filter_pre_load] + pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load] if not pre_load: return @@ -102,7 +106,7 @@ class GitCommit(base.BaseCommit): @LazyProperty def _tree_id(self): - return self._remote[self._commit['tree']]['id'] + return self._remote[self._commit["tree"]]["id"] @LazyProperty def id(self): @@ -134,13 +138,12 @@ class GitCommit(base.BaseCommit): """ Returns modified, added, removed, deleted files for current commit """ - return self.changed, self.added, self.removed + added, modified, deleted = self._changes_cache + return list(modified), list(modified), list(deleted) @LazyProperty def tags(self): - tags = [safe_str(name) for name, - commit_id in self.repository.tags.items() - if commit_id == self.raw_id] + tags = [safe_str(name) for name, commit_id in self.repository.tags.items() if commit_id == self.raw_id] return tags @LazyProperty @@ -161,47 +164,33 @@ class GitCommit(base.BaseCommit): branches = self._remote.branch(self.raw_id) return self._set_branch(branches) - def _get_tree_id_for_path(self, path): + def _get_path_tree_id_and_type(self, path: bytes): - path = safe_str(path) - if path in self._paths: - return self._paths[path] - - tree_id = self._tree_id + if path in self._path_type_cache: + return self._path_type_cache[path] - path = path.strip('/') - if path == '': - data = [tree_id, "tree"] - self._paths[''] = data - return data + if path == b"": + self._path_type_cache[b""] = [self._tree_id, NodeKind.DIR] + return self._path_type_cache[path] - tree_id, tree_type, tree_mode = \ - self._remote.tree_and_type_for_path(self.raw_id, path) + tree_id, tree_type, tree_mode = self._remote.tree_and_type_for_path(self.raw_id, path) if tree_id is None: raise self.no_node_at_path(path) - self._paths[path] = [tree_id, tree_type] - self._stat_modes[path] = tree_mode + self._path_type_cache[path] = [tree_id, tree_type] + self._path_mode_cache[path] = tree_mode - if path not in self._paths: - raise self.no_node_at_path(path) - - return self._paths[path] + return self._path_type_cache[path] def _get_kind(self, path): - tree_id, type_ = self._get_tree_id_for_path(path) - if type_ == 'blob': - return NodeKind.FILE - elif type_ == 'tree': - return NodeKind.DIR - elif type_ == 'link': - return NodeKind.SUBMODULE - return None + path = self._fix_path(path) + _, path_type = self._get_path_tree_id_and_type(path) + return path_type def _assert_is_path(self, path): path = self._fix_path(path) if self._get_kind(path) != NodeKind.FILE: - raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'") + raise CommitError(f"File at path={path} does not exist for commit {self.raw_id}") return path def _get_file_nodes(self): @@ -237,15 +226,19 @@ class GitCommit(base.BaseCommit): path = self._assert_is_path(path) # ensure path is traversed - self._get_tree_id_for_path(path) + self._get_path_tree_id_and_type(path) + + return self._path_mode_cache[path] - return self._stat_modes[path] + def is_link(self, path: bytes): + path = self._assert_is_path(path) + if path not in self._path_mode_cache: + self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path) - def is_link(self, path): - return stat.S_ISLNK(self.get_file_mode(path)) + return self._path_mode_cache[path] == FILEMODE_LINK def is_node_binary(self, path): - tree_id, _ = self._get_tree_id_for_path(path) + tree_id, _ = self._get_path_tree_id_and_type(path) return self._remote.is_binary(tree_id) def node_md5_hash(self, path): @@ -256,19 +249,19 @@ class GitCommit(base.BaseCommit): """ Returns content of the file at given `path`. """ - tree_id, _ = self._get_tree_id_for_path(path) + tree_id, _ = self._get_path_tree_id_and_type(path) return self._remote.blob_as_pretty_string(tree_id) def get_file_content_streamed(self, path): - tree_id, _ = self._get_tree_id_for_path(path) - stream_method = getattr(self._remote, 'stream:blob_as_pretty_string') + tree_id, _ = self._get_path_tree_id_and_type(path) + stream_method = getattr(self._remote, "stream:blob_as_pretty_string") return stream_method(tree_id) def get_file_size(self, path): """ Returns size of the file at given `path`. """ - tree_id, _ = self._get_tree_id_for_path(path) + tree_id, _ = self._get_path_tree_id_and_type(path) return self._remote.blob_raw_length(tree_id) def get_path_history(self, path, limit=None, pre_load=None): @@ -276,12 +269,9 @@ class GitCommit(base.BaseCommit): Returns history of file as reversed list of `GitCommit` objects for which file at given `path` has been modified. """ - path = self._assert_is_path(path) - hist = self._remote.node_history(self.raw_id, path, limit) - return [ - self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) - for commit_id in hist] + history = self._remote.node_history(self.raw_id, path, limit) + return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in history] def get_file_annotate(self, path, pre_load=None): """ @@ -293,95 +283,102 @@ class GitCommit(base.BaseCommit): for ln_no, commit_id, content in result: yield ( - ln_no, commit_id, + ln_no, + commit_id, lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load), - content) + content, + ) - def get_nodes(self, path, pre_load=None): + def get_nodes(self, path: bytes, pre_load=None): if self._get_kind(path) != NodeKind.DIR: - raise CommitError( - f"Directory does not exist for commit {self.raw_id} at '{path}'") + raise CommitError(f"Directory does not exist for commit {self.raw_id} at '{path}'") path = self._fix_path(path) - tree_id, _ = self._get_tree_id_for_path(path) + path_nodes = [] - dirnodes = [] - filenodes = [] + for obj_name, stat_, tree_item_id, node_kind, pre_load_data in self._remote.get_nodes(self.raw_id, path, pre_load): + if node_kind is None: + raise CommitError(f"Requested object type={node_kind} cannot be determined") - # extracted tree ID gives us our files... - str_path = safe_str(path) # libgit operates on bytes - for name, stat_, id_, type_ in self._remote.tree_items(tree_id): - if type_ == 'link': - url = self._get_submodule_url('/'.join((str_path, name))) - dirnodes.append(SubModuleNode( - name, url=url, commit=id_, alias=self.repository.alias)) - continue + if path == b"": + obj_path = obj_name + else: + obj_path = b"/".join((path, obj_name)) + + # cache file mode for git, since we have it already + if obj_path not in self._path_mode_cache: + self._path_mode_cache[obj_path] = stat_ - if str_path != '': - obj_path = '/'.join((str_path, name)) - else: - obj_path = name - if obj_path not in self._stat_modes: - self._stat_modes[obj_path] = stat_ + # cache type + if node_kind not in self._path_type_cache: + self._path_type_cache[obj_path] = [tree_item_id, node_kind] - if type_ == 'tree': - dirnodes.append(DirNode(safe_bytes(obj_path), commit=self)) - elif type_ == 'blob': - filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load)) + entry = None + if obj_path in self.nodes: + entry = self.nodes[obj_path] else: - raise CommitError(f"Requested object should be Tree or Blob, is {type_}") + if node_kind == NodeKind.SUBMODULE: + url = self._get_submodule_url(obj_path) + entry= SubModuleNode(obj_name, url=url, commit=tree_item_id, alias=self.repository.alias) + elif node_kind == NodeKind.DIR: + entry = DirNode(safe_bytes(obj_path), commit=self) + elif node_kind == NodeKind.FILE: + entry = FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load_data=pre_load_data) - nodes = dirnodes + filenodes - for node in nodes: - if node.path not in self.nodes: - self.nodes[node.path] = node - nodes.sort() - return nodes + if entry: + self.nodes[obj_path] = entry + path_nodes.append(entry) - def get_node(self, path, pre_load=None): + path_nodes.sort() + return path_nodes + + def get_node(self, path: bytes, pre_load=None): path = self._fix_path(path) - if path not in self.nodes: - try: - tree_id, type_ = self._get_tree_id_for_path(path) - except CommitError: - raise NodeDoesNotExistError( - f"Cannot find one of parents' directories for a given " - f"path: {path}") + + # use cached, if we have one + if path in self.nodes: + return self.nodes[path] - if type_ in ['link', 'commit']: + try: + tree_id, path_type = self._get_path_tree_id_and_type(path) + except CommitError: + raise NodeDoesNotExistError(f"Cannot find one of parents' directories for a given path: {path}") + + if path == b"": + node = RootNode(commit=self) + else: + if path_type == NodeKind.SUBMODULE: url = self._get_submodule_url(path) - node = SubModuleNode(path, url=url, commit=tree_id, - alias=self.repository.alias) - elif type_ == 'tree': - if path == '': - node = RootNode(commit=self) - else: - node = DirNode(safe_bytes(path), commit=self) - elif type_ == 'blob': + node = SubModuleNode(path, url=url, commit=tree_id, alias=self.repository.alias) + elif path_type == NodeKind.DIR: + node = DirNode(safe_bytes(path), commit=self) + elif path_type == NodeKind.FILE: node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load) - self._stat_modes[path] = node.mode + self._path_mode_cache[path] = node.mode else: raise self.no_node_at_path(path) - # cache node - self.nodes[path] = node - + # cache node + self.nodes[path] = node return self.nodes[path] - def get_largefile_node(self, path): - tree_id, _ = self._get_tree_id_for_path(path) + def get_largefile_node(self, path: bytes): + tree_id, _ = self._get_path_tree_id_and_type(path) pointer_spec = self._remote.is_large_file(tree_id) if pointer_spec: # content of that file regular FileNode is the hash of largefile - file_id = pointer_spec.get('oid_hash') - if self._remote.in_largefiles_store(file_id): - lf_path = self._remote.store_path(file_id) - return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path) + file_id = pointer_spec.get("oid_hash") + if not self._remote.in_largefiles_store(file_id): + log.warning(f'Largefile oid={file_id} not found in store') + return None + + lf_path = self._remote.store_path(file_id) + return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path) @LazyProperty - def affected_files(self): + def affected_files(self) -> list[bytes]: """ Gets a fast accessible file changes for given commit """ @@ -389,7 +386,7 @@ class GitCommit(base.BaseCommit): return list(added.union(modified).union(deleted)) @LazyProperty - def _changes_cache(self): + def _changes_cache(self) -> tuple[set, set, set]: added = set() modified = set() deleted = set() @@ -416,53 +413,22 @@ class GitCommit(base.BaseCommit): :param status: one of: *added*, *modified* or *deleted* """ added, modified, deleted = self._changes_cache - return sorted({ - 'added': list(added), - 'modified': list(modified), - 'deleted': list(deleted)}[status] - ) - - @LazyProperty - def added(self): - """ - Returns list of added ``FileNode`` objects. - """ - if not self.parents: - return list(self._get_file_nodes()) - return AddedFileNodesGenerator(self.added_paths, self) + return sorted({"added": list(added), "modified": list(modified), "deleted": list(deleted)}[status]) @LazyProperty def added_paths(self): - return [n for n in self._get_paths_for_status('added')] - - @LazyProperty - def changed(self): - """ - Returns list of modified ``FileNode`` objects. - """ - if not self.parents: - return [] - return ChangedFileNodesGenerator(self.changed_paths, self) + return [n for n in self._get_paths_for_status("added")] @LazyProperty def changed_paths(self): - return [n for n in self._get_paths_for_status('modified')] - - @LazyProperty - def removed(self): - """ - Returns list of removed ``FileNode`` objects. - """ - if not self.parents: - return [] - return RemovedFileNodesGenerator(self.removed_paths, self) + return [n for n in self._get_paths_for_status("modified")] @LazyProperty def removed_paths(self): - return [n for n in self._get_paths_for_status('deleted')] + return [n for n in self._get_paths_for_status("deleted")] - def _get_submodule_url(self, submodule_path): - git_modules_path = '.gitmodules' + def _get_submodule_url(self, submodule_path: bytes): + git_modules_path = b".gitmodules" if self._submodules is None: self._submodules = {} @@ -476,9 +442,9 @@ class GitCommit(base.BaseCommit): parser.read_file(io.StringIO(submodules_node.str_content)) for section in parser.sections(): - path = parser.get(section, 'path') - url = parser.get(section, 'url') + path = parser.get(section, "path") + url = parser.get(section, "url") if path and url: - self._submodules[path.strip('/')] = url + self._submodules[safe_bytes(path).strip(b"/")] = url - return self._submodules.get(submodule_path.strip('/')) + return self._submodules.get(submodule_path.strip(b"/")) diff --git a/rhodecode/lib/vcs/backends/git/diff.py b/rhodecode/lib/vcs/backends/git/diff.py --- a/rhodecode/lib/vcs/backends/git/diff.py +++ b/rhodecode/lib/vcs/backends/git/diff.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/git/inmemory.py b/rhodecode/lib/vcs/backends/git/inmemory.py --- a/rhodecode/lib/vcs/backends/git/inmemory.py +++ b/rhodecode/lib/vcs/backends/git/inmemory.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -425,7 +425,7 @@ class GitRepository(BaseRepository): return def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, - translate_tag=True, maybe_unreachable=False, reference_obj=None): + translate_tag=True, maybe_unreachable=False, reference_obj=None) -> GitCommit: """ Returns `GitCommit` object representing commit from git repository at the given `commit_id` or head (most recent commit) if None given. @@ -608,8 +608,7 @@ class GitRepository(BaseRepository): return commit_id1 if self != repo2: - commits = self._remote.get_missing_revs( - commit_id1, commit_id2, repo2.path) + commits = self._remote.get_missing_revs(commit_id1, commit_id2, repo2.path) if commits: commit = repo2.get_commit(commits[-1]) if commit.parents: @@ -620,9 +619,7 @@ class GitRepository(BaseRepository): # no commits from other repo, ancestor_id is the commit_id2 ancestor_id = commit_id2 else: - output, __ = self.run_git_command( - ['merge-base', commit_id1, commit_id2]) - ancestor_id = self.COMMIT_ID_PAT.findall(output)[0] + ancestor_id = self._remote.get_common_ancestor(commit_id1, commit_id2) log.debug('Found common ancestor with sha: %s', ancestor_id) @@ -630,23 +627,22 @@ class GitRepository(BaseRepository): def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): repo1 = self - ancestor_id = None if commit_id1 == commit_id2: commits = [] elif repo1 != repo2: - missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2, - repo2.path) + missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2, repo2.path) commits = [ repo2.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in reversed(missing_ids)] else: - output, __ = repo1.run_git_command( - ['log', '--reverse', '--pretty=format: %H', '-s', - f'{commit_id1}..{commit_id2}']) + compare_commits = self._remote.compare_commits( + commit_id1, commit_id2 + ) commits = [ repo1.get_commit(commit_id=commit_id, pre_load=pre_load) - for commit_id in self.COMMIT_ID_PAT.findall(output)] + for commit_id in compare_commits] + return commits diff --git a/rhodecode/lib/vcs/backends/hg/__init__.py b/rhodecode/lib/vcs/backends/hg/__init__.py --- a/rhodecode/lib/vcs/backends/hg/__init__.py +++ b/rhodecode/lib/vcs/backends/hg/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -45,10 +45,3 @@ def discover_hg_version(raise_on_exc=Fal if raise_on_exc: raise return '' - - -def largefiles_store(base_location): - """ - Return a largefile store relative to base_location - """ - return os.path.join(base_location, '.cache', 'largefiles') diff --git a/rhodecode/lib/vcs/backends/hg/commit.py b/rhodecode/lib/vcs/backends/hg/commit.py --- a/rhodecode/lib/vcs/backends/hg/commit.py +++ b/rhodecode/lib/vcs/backends/hg/commit.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,21 +19,26 @@ """ HG commit module """ - import os +import logging from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.lib.datelib import utcdate_fromtimestamp from rhodecode.lib.str_utils import safe_bytes, safe_str -from rhodecode.lib.vcs import path as vcspath from rhodecode.lib.vcs.backends import base from rhodecode.lib.vcs.exceptions import CommitError from rhodecode.lib.vcs.nodes import ( - AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, - NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode, - LargeFileNode) -from rhodecode.lib.vcs.utils.paths import get_dirs_for_path + DirNode, + FileNode, + NodeKind, + RootNode, + SubModuleNode, + LargeFileNode, +) +from rhodecode.lib.vcs_common import FILEMODE_LINK + +log = logging.getLogger(__name__) class MercurialCommit(base.BaseCommit): @@ -59,13 +64,13 @@ class MercurialCommit(base.BaseCommit): # caches self.nodes = {} - self._stat_modes = {} # stat info for paths + self._path_mode_cache = {} # path stats cache, e.g filemode etc + self._path_type_cache = {} # path type dir/file/link etc cache def _set_bulk_properties(self, pre_load): if not pre_load: return - pre_load = [entry for entry in pre_load - if entry not in self._filter_pre_load] + pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load] if not pre_load: return @@ -86,8 +91,7 @@ class MercurialCommit(base.BaseCommit): @LazyProperty def tags(self): - tags = [name for name, commit_id in self.repository.tags.items() - if commit_id == self.raw_id] + tags = [name for name, commit_id in self.repository.tags.items() if commit_id == self.raw_id] return tags @LazyProperty @@ -96,9 +100,7 @@ class MercurialCommit(base.BaseCommit): @LazyProperty def bookmarks(self): - bookmarks = [ - name for name, commit_id in self.repository.bookmarks.items() - if commit_id == self.raw_id] + bookmarks = [name for name, commit_id in self.repository.bookmarks.items() if commit_id == self.raw_id] return bookmarks @LazyProperty @@ -122,27 +124,13 @@ class MercurialCommit(base.BaseCommit): """ Returns modified, added, removed, deleted files for current commit """ - return self._remote.ctx_status(self.raw_id) - - @LazyProperty - def _file_paths(self): - return self._remote.ctx_list(self.raw_id) - - @LazyProperty - def _dir_paths(self): - dir_paths = [''] - dir_paths.extend(list(set(get_dirs_for_path(*self._file_paths)))) - - return dir_paths - - @LazyProperty - def _paths(self): - return self._dir_paths + self._file_paths + modified, added, deleted, *_ = self._remote.ctx_status(self.raw_id) + return modified, added, deleted @LazyProperty def id(self): if self.last: - return 'tip' + return "tip" return self.short_id @LazyProperty @@ -150,8 +138,7 @@ class MercurialCommit(base.BaseCommit): return self.raw_id[:12] def _make_commits(self, commit_ids, pre_load=None): - return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) - for commit_id in commit_ids] + return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in commit_ids] @LazyProperty def parents(self): @@ -163,10 +150,10 @@ class MercurialCommit(base.BaseCommit): def _get_phase_text(self, phase_id): return { - 0: 'public', - 1: 'draft', - 2: 'secret', - }.get(phase_id) or '' + 0: "public", + 1: "draft", + 2: "secret", + }.get(phase_id) or "" @LazyProperty def phase(self): @@ -195,17 +182,14 @@ class MercurialCommit(base.BaseCommit): def _get_kind(self, path): path = self._fix_path(path) - if path in self._file_paths: - return NodeKind.FILE - elif path in self._dir_paths: - return NodeKind.DIR - else: - raise CommitError(f"Node does not exist at the given path '{path}'") + path_type = self._get_path_type(path) + return path_type - def _assert_is_path(self, path) -> str: + def _assert_is_path(self, path) -> str | bytes: path = self._fix_path(path) + if self._get_kind(path) != NodeKind.FILE: - raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'") + raise CommitError(f"File at path={path} does not exist for commit {self.raw_id}") return path @@ -214,20 +198,17 @@ class MercurialCommit(base.BaseCommit): Returns stat mode of the file at the given ``path``. """ path = self._assert_is_path(path) + if path not in self._path_mode_cache: + self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path) - if path not in self._stat_modes: - self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path) + return self._path_mode_cache[path] - if 'x' in self._stat_modes[path]: - return base.FILEMODE_EXECUTABLE - return base.FILEMODE_DEFAULT + def is_link(self, path: bytes): + path = self._assert_is_path(path) + if path not in self._path_mode_cache: + self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path) - def is_link(self, path): - path = self._assert_is_path(path) - if path not in self._stat_modes: - self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path) - - return 'l' in self._stat_modes[path] + return self._path_mode_cache[path] == FILEMODE_LINK def is_node_binary(self, path): path = self._assert_is_path(path) @@ -246,7 +227,7 @@ class MercurialCommit(base.BaseCommit): def get_file_content_streamed(self, path): path = self._assert_is_path(path) - stream_method = getattr(self._remote, 'stream:fctx_node_data') + stream_method = getattr(self._remote, "stream:fctx_node_data") return stream_method(self.raw_id, path) def get_file_size(self, path): @@ -262,10 +243,8 @@ class MercurialCommit(base.BaseCommit): for which file at given ``path`` has been modified. """ path = self._assert_is_path(path) - hist = self._remote.node_history(self.raw_id, path, limit) - return [ - self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) - for commit_id in hist] + history = self._remote.node_history(self.raw_id, path, limit) + return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in history] def get_file_annotate(self, path, pre_load=None): """ @@ -276,11 +255,13 @@ class MercurialCommit(base.BaseCommit): for ln_no, commit_id, content in result: yield ( - ln_no, commit_id, + ln_no, + commit_id, lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load), - content) + content, + ) - def get_nodes(self, path, pre_load=None): + def get_nodes(self, path: bytes, pre_load=None): """ Returns combined ``DirNode`` and ``FileNode`` objects list representing state of commit at the given ``path``. If node at the given ``path`` @@ -288,59 +269,92 @@ class MercurialCommit(base.BaseCommit): """ if self._get_kind(path) != NodeKind.DIR: - raise CommitError( - f"Directory does not exist for idx {self.raw_id} at '{path}'") + raise CommitError(f"Directory does not exist for idx {self.raw_id} at '{path}'") path = self._fix_path(path) - filenodes = [ - FileNode(safe_bytes(f), commit=self, pre_load=pre_load) for f in self._file_paths - if os.path.dirname(f) == path] - # TODO: johbo: Check if this can be done in a more obvious way - dirs = path == '' and '' or [ - d for d in self._dir_paths - if d and vcspath.dirname(d) == path] - dirnodes = [ - DirNode(safe_bytes(d), commit=self) for d in dirs - if os.path.dirname(d) == path] + path_nodes = [] + + for obj_path, node_kind, flags, pre_load_data in self._remote.get_nodes(self.raw_id, path, pre_load): + + if node_kind is None: + raise CommitError(f"Requested object type={node_kind} cannot be mapped to a proper type") + + stat_ = flags + # cache file mode + if obj_path not in self._path_mode_cache: + self._path_mode_cache[obj_path] = stat_ + + # cache type + if node_kind not in self._path_type_cache: + self._path_type_cache[obj_path] = node_kind - alias = self.repository.alias - for k, vals in self._submodules.items(): - if vcspath.dirname(k) == path: - loc = vals[0] - commit = vals[1] - dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias)) + entry = None + if obj_path in self.nodes: + entry = self.nodes[obj_path] + else: + if node_kind == NodeKind.DIR: + entry = DirNode(safe_bytes(obj_path), commit=self) + elif node_kind == NodeKind.FILE: + entry = FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load, pre_load_data=pre_load_data) + if entry: + self.nodes[obj_path] = entry + path_nodes.append(entry) - nodes = dirnodes + filenodes - for node in nodes: - if node.path not in self.nodes: - self.nodes[node.path] = node - nodes.sort() + for obj_path, (location, commit, scm_type) in self._submodules.items(): - return nodes + if os.path.dirname(obj_path) == path: + entry = SubModuleNode(obj_path, url=location, commit=commit, alias=scm_type) + self.nodes[obj_path] = entry + path_nodes.append(entry) - def get_node(self, path, pre_load=None): + path_nodes.sort() + return path_nodes + + def get_node(self, path: bytes, pre_load=None): """ Returns `Node` object from the given `path`. If there is no node at the given `path`, `NodeDoesNotExistError` would be raised. """ path = self._fix_path(path) - if path not in self.nodes: - if path in self._file_paths: + # use cached, if we have one + if path in self.nodes: + return self.nodes[path] + + path_type = self._get_path_type(path) + if path == b"": + node = RootNode(commit=self) + else: + if path_type == NodeKind.DIR: + node = DirNode(safe_bytes(path), commit=self) + elif path_type == NodeKind.FILE: node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load) - elif path in self._dir_paths: - if path == '': - node = RootNode(commit=self) - else: - node = DirNode(safe_bytes(path), commit=self) + self._path_mode_cache[path] = node.mode else: raise self.no_node_at_path(path) - - # cache node - self.nodes[path] = node + # cache node + self.nodes[path] = node return self.nodes[path] - def get_largefile_node(self, path): + def _get_path_type(self, path: bytes): + if path in self._path_type_cache: + return self._path_type_cache[path] + + if path == b"": + self._path_type_cache[b""] = NodeKind.DIR + return NodeKind.DIR + + path_type, flags = self._remote.get_path_type(self.raw_id, path) + + if not path_type: + raise self.no_node_at_path(path) + + self._path_type_cache[path] = path_type + self._path_mode_cache[path] = flags + + return self._path_type_cache[path] + + def get_largefile_node(self, path: bytes): pointer_spec = self._remote.is_large_file(self.raw_id, path) if pointer_spec: # content of that file regular FileNode is the hash of largefile @@ -363,40 +377,20 @@ class MercurialCommit(base.BaseCommit): return self._remote.ctx_substate(self.raw_id) @LazyProperty - def affected_files(self): + def affected_files(self) -> list[bytes]: """ Gets a fast accessible file changes for given commit """ return self._remote.ctx_files(self.raw_id) - @property - def added(self): - """ - Returns list of added ``FileNode`` objects. - """ - return AddedFileNodesGenerator(self.added_paths, self) - @LazyProperty def added_paths(self): return [n for n in self.status[1]] - @property - def changed(self): - """ - Returns list of modified ``FileNode`` objects. - """ - return ChangedFileNodesGenerator(self.changed_paths, self) - @LazyProperty def changed_paths(self): return [n for n in self.status[0]] - @property - def removed(self): - """ - Returns list of removed ``FileNode`` objects. - """ - return RemovedFileNodesGenerator(self.removed_paths, self) @LazyProperty def removed_paths(self): diff --git a/rhodecode/lib/vcs/backends/hg/diff.py b/rhodecode/lib/vcs/backends/hg/diff.py --- a/rhodecode/lib/vcs/backends/hg/diff.py +++ b/rhodecode/lib/vcs/backends/hg/diff.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/hg/inmemory.py b/rhodecode/lib/vcs/backends/hg/inmemory.py --- a/rhodecode/lib/vcs/backends/hg/inmemory.py +++ b/rhodecode/lib/vcs/backends/hg/inmemory.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -368,26 +368,32 @@ class MercurialRepository(BaseRepository be created. If `src_url` is given, would try to clone repository from the - location at given clone_point. Additionally it'll make update to + location at given clone_point. Additionally, it'll make update to working copy accordingly to `do_workspace_checkout` flag. """ if create and os.path.exists(self.path): raise RepositoryError( f"Cannot create repository at {self.path}, location already exist") - if src_url: - url = str(self._get_url(src_url)) - MercurialRepository.check_url(url, self.config) + if create: + if src_url: + url = str(self._get_url(src_url)) + MercurialRepository.check_url(url, self.config) - self._remote.clone(url, self.path, do_workspace_checkout) + self._remote.clone(url, self.path, do_workspace_checkout) - # Don't try to create if we've already cloned repo - create = False + # Don't try to create if we've already cloned repo + create = False + self._remote.localrepository(create) + else: + os.makedirs(self.path, mode=0o755) + create = True + self._remote.localrepository(create) - if create: - os.makedirs(self.path, mode=0o755) - - self._remote.localrepository(create) + else: + if not self._remote.assert_correct_path(): + raise RepositoryError( + f'Path "{self.path}" does not contain a Mercurial repository') @LazyProperty def in_memory_commit(self): @@ -444,7 +450,7 @@ class MercurialRepository(BaseRepository return os.path.join(self.path, '.hg', '.hgrc') def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, - translate_tag=None, maybe_unreachable=False, reference_obj=None): + translate_tag=None, maybe_unreachable=False, reference_obj=None) -> MercurialCommit: """ Returns ``MercurialCommit`` object representing repository's commit at the given `commit_id` or `commit_idx`. @@ -592,8 +598,7 @@ class MercurialRepository(BaseRepository """ Create a local clone of the current repo. """ - self._remote.clone(self.path, clone_path, update_after_clone=True, - hooks=False) + self._remote.clone(self.path, clone_path, update_after_clone=True, hooks=False) def _update(self, revision, clean=False): """ diff --git a/rhodecode/lib/vcs/backends/svn/__init__.py b/rhodecode/lib/vcs/backends/svn/__init__.py --- a/rhodecode/lib/vcs/backends/svn/__init__.py +++ b/rhodecode/lib/vcs/backends/svn/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/svn/commit.py b/rhodecode/lib/vcs/backends/svn/commit.py --- a/rhodecode/lib/vcs/backends/svn/commit.py +++ b/rhodecode/lib/vcs/backends/svn/commit.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,8 +19,7 @@ """ SVN commit module """ - - +import logging import dateutil.parser from zope.cachedescriptors.property import Lazy as LazyProperty @@ -28,9 +27,10 @@ from rhodecode.lib.str_utils import safe from rhodecode.lib.vcs import nodes, path as vcspath from rhodecode.lib.vcs.backends import base from rhodecode.lib.vcs.exceptions import CommitError - +from vcsserver.lib.vcs_common import NodeKind, FILEMODE_EXECUTABLE, FILEMODE_DEFAULT, FILEMODE_LINK +_SVN_PROP_TRUE = "*" -_SVN_PROP_TRUE = '*' +log = logging.getLogger(__name__) class SubversionCommit(base.BaseCommit): @@ -53,15 +53,16 @@ class SubversionCommit(base.BaseCommit): # which knows how to translate commit index and commit id self.raw_id = commit_id self.short_id = commit_id - self.id = f'r{commit_id}' + self.id = f"r{commit_id}" - # TODO: Implement the following placeholder attributes self.nodes = {} + self._path_mode_cache = {} # path stats cache, e.g filemode etc + self._path_type_cache = {} # path type dir/file/link etc cache self.tags = [] @property def author(self): - return safe_str(self._properties.get('svn:author')) + return safe_str(self._properties.get("svn:author")) @property def date(self): @@ -69,7 +70,7 @@ class SubversionCommit(base.BaseCommit): @property def message(self): - return safe_str(self._properties.get('svn:log')) + return safe_str(self._properties.get("svn:log")) @LazyProperty def _properties(self): @@ -91,19 +92,46 @@ class SubversionCommit(base.BaseCommit): return [child] return [] - def get_file_mode(self, path: bytes): + def _calculate_file_mode(self, path: bytes): # Note: Subversion flags files which are executable with a special # property `svn:executable` which is set to the value ``"*"``. - if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE: - return base.FILEMODE_EXECUTABLE + if self._get_file_property(path, "svn:executable") == _SVN_PROP_TRUE: + return FILEMODE_EXECUTABLE else: - return base.FILEMODE_DEFAULT + return FILEMODE_DEFAULT + + def get_file_mode(self, path: bytes): + path = self._fix_path(path) + + if path not in self._path_mode_cache: + self._path_mode_cache[path] = self._calculate_file_mode(path) + + return self._path_mode_cache[path] + + def _get_path_type(self, path: bytes): + if path in self._path_type_cache: + return self._path_type_cache[path] - def is_link(self, path): + if path == b"": + self._path_type_cache[b""] = NodeKind.DIR + return NodeKind.DIR + + path_type = self._remote.get_node_type(self._svn_rev, path) + + if not path_type: + raise self.no_node_at_path(path) + + #flags = None + self._path_type_cache[path] = path_type + #self._path_mode_cache[path] = flags + + return self._path_type_cache[path] + + def is_link(self, path: bytes): # Note: Subversion has a flag for special files, the content of the # file contains the type of that file. - if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE: - return self.get_file_content(path).startswith(b'link') + if self._get_file_property(path, "svn:special") == _SVN_PROP_TRUE: + return self.get_file_content(path).startswith(b"link") return False def is_node_binary(self, path): @@ -115,8 +143,7 @@ class SubversionCommit(base.BaseCommit): return self._remote.md5_hash(self._svn_rev, safe_str(path)) def _get_file_property(self, path, name): - file_properties = self._remote.node_properties( - safe_str(path), self._svn_rev) + file_properties = self._remote.node_properties(safe_str(path), self._svn_rev) return file_properties.get(name) def get_file_content(self, path): @@ -126,7 +153,7 @@ class SubversionCommit(base.BaseCommit): def get_file_content_streamed(self, path): path = self._fix_path(path) - stream_method = getattr(self._remote, 'stream:get_file_content') + stream_method = getattr(self._remote, "stream:get_file_content") return stream_method(self._svn_rev, safe_str(path)) def get_file_size(self, path): @@ -134,11 +161,9 @@ class SubversionCommit(base.BaseCommit): return self._remote.get_file_size(self._svn_rev, safe_str(path)) def get_path_history(self, path, limit=None, pre_load=None): - path = safe_str(self._fix_path(path)) - history = self._remote.node_history(path, self._svn_rev, limit) - return [ - self.repository.get_commit(commit_id=str(svn_rev)) - for svn_rev in history] + path = self._fix_path(path) + history = self._remote.node_history(self._svn_rev, safe_str(path), limit) + return [self.repository.get_commit(commit_id=str(svn_rev)) for svn_rev in history] def get_file_annotate(self, path, pre_load=None): result = self._remote.file_annotate(safe_str(path), self._svn_rev) @@ -146,67 +171,78 @@ class SubversionCommit(base.BaseCommit): for zero_based_line_no, svn_rev, content in result: commit_id = str(svn_rev) line_no = zero_based_line_no + 1 - yield ( - line_no, - commit_id, - lambda: self.repository.get_commit(commit_id=commit_id), - content) + yield line_no, commit_id, lambda: self.repository.get_commit(commit_id=commit_id), content - def get_node(self, path, pre_load=None): + def get_node(self, path: bytes, pre_load=None): path = self._fix_path(path) - if path not in self.nodes: + + # use cached, if we have one + if path in self.nodes: + return self.nodes[path] - if path == '': - node = nodes.RootNode(commit=self) + path_type = self._get_path_type(path) + if path == b"": + node = nodes.RootNode(commit=self) + else: + if path_type == NodeKind.DIR: + node = nodes.DirNode(safe_bytes(path), commit=self) + elif path_type == NodeKind.FILE: + node = nodes.FileNode(safe_bytes(path), commit=self, pre_load=pre_load) + self._path_mode_cache[path] = node.mode else: - node_type = self._remote.get_node_type(self._svn_rev, safe_str(path)) - if node_type == 'dir': - node = nodes.DirNode(safe_bytes(path), commit=self) - elif node_type == 'file': - node = nodes.FileNode(safe_bytes(path), commit=self, pre_load=pre_load) - else: - raise self.no_node_at_path(path) + raise self.no_node_at_path(path) - self.nodes[path] = node + self.nodes[path] = node return self.nodes[path] - def get_nodes(self, path, pre_load=None): + def get_nodes(self, path: bytes, pre_load=None): if self._get_kind(path) != nodes.NodeKind.DIR: - raise CommitError( - f"Directory does not exist for commit {self.raw_id} at '{path}'") - path = safe_str(self._fix_path(path)) + raise CommitError(f"Directory does not exist for commit {self.raw_id} at '{path}'") + path = self._fix_path(path) path_nodes = [] - for name, kind in self._remote.get_nodes(self._svn_rev, path): - node_path = vcspath.join(path, name) - if kind == 'dir': - node = nodes.DirNode(safe_bytes(node_path), commit=self) - elif kind == 'file': - node = nodes.FileNode(safe_bytes(node_path), commit=self, pre_load=pre_load) + + for obj_path, node_kind, pre_load_data in self._remote.get_nodes(self._svn_rev, path, pre_load): + + if node_kind is None: + raise CommitError(f"Requested object type={node_kind} cannot be determined") + + # TODO: implement it ?? + stat_ = None + # # cache file mode + # if obj_path not in self._path_mode_cache: + # self._path_mode_cache[obj_path] = stat_ + + # cache type + if node_kind not in self._path_type_cache: + self._path_type_cache[obj_path] = node_kind + + entry = None + if obj_path in self.nodes: + entry = self.nodes[obj_path] else: - raise ValueError(f"Node kind {kind} not supported.") - self.nodes[node_path] = node - path_nodes.append(node) + if node_kind == NodeKind.DIR: + entry = nodes.DirNode(safe_bytes(obj_path), commit=self) + elif node_kind == NodeKind.FILE: + entry = nodes.FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load, pre_load_data=pre_load_data) + if entry: + self.nodes[obj_path] = entry + path_nodes.append(entry) + path_nodes.sort() return path_nodes def _get_kind(self, path): path = self._fix_path(path) - kind = self._remote.get_node_type(self._svn_rev, path) - if kind == 'file': - return nodes.NodeKind.FILE - elif kind == 'dir': - return nodes.NodeKind.DIR - else: - raise CommitError( - f"Node does not exist at the given path '{path}'") + path_type = self._get_path_type(path) + return path_type @LazyProperty def _changes_cache(self): return self._remote.revision_changes(self._svn_rev) @LazyProperty - def affected_files(self): + def affected_files(self) -> list[bytes]: changed_files = set() for files in self._changes_cache.values(): changed_files.update(files) @@ -216,29 +252,17 @@ class SubversionCommit(base.BaseCommit): def id(self): return self.raw_id - @property - def added(self): - return nodes.AddedFileNodesGenerator(self.added_paths, self) - @LazyProperty def added_paths(self): - return [n for n in self._changes_cache['added']] - - @property - def changed(self): - return nodes.ChangedFileNodesGenerator(self.changed_paths, self) + return [n for n in self._changes_cache["added"]] @LazyProperty def changed_paths(self): - return [n for n in self._changes_cache['changed']] - - @property - def removed(self): - return nodes.RemovedFileNodesGenerator(self.removed_paths, self) + return [n for n in self._changes_cache["changed"]] @LazyProperty def removed_paths(self): - return [n for n in self._changes_cache['removed']] + return [n for n in self._changes_cache["removed"]] def _date_from_svn_properties(properties): @@ -248,7 +272,7 @@ def _date_from_svn_properties(properties :return: :class:`datetime.datetime` instance. The object is naive. """ - aware_date = dateutil.parser.parse(properties.get('svn:date')) + aware_date = dateutil.parser.parse(properties.get("svn:date")) # final_date = aware_date.astimezone(dateutil.tz.tzlocal()) final_date = aware_date return final_date.replace(tzinfo=None) diff --git a/rhodecode/lib/vcs/backends/svn/diff.py b/rhodecode/lib/vcs/backends/svn/diff.py --- a/rhodecode/lib/vcs/backends/svn/diff.py +++ b/rhodecode/lib/vcs/backends/svn/diff.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/svn/inmemory.py b/rhodecode/lib/vcs/backends/svn/inmemory.py --- a/rhodecode/lib/vcs/backends/svn/inmemory.py +++ b/rhodecode/lib/vcs/backends/svn/inmemory.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/backends/svn/repository.py b/rhodecode/lib/vcs/backends/svn/repository.py --- a/rhodecode/lib/vcs/backends/svn/repository.py +++ b/rhodecode/lib/vcs/backends/svn/repository.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,7 +30,7 @@ from zope.cachedescriptors.property impo from collections import OrderedDict from rhodecode.lib.datelib import date_astimestamp -from rhodecode.lib.str_utils import safe_str +from rhodecode.lib.str_utils import safe_str, safe_bytes from rhodecode.lib.utils2 import CachedProperty from rhodecode.lib.vcs import connection, path as vcspath from rhodecode.lib.vcs.backends import base @@ -157,16 +157,18 @@ class SubversionRepository(base.BaseRepo for pattern in self._patterns_from_section(config_section): pattern = vcspath.sanitize(pattern) + bytes_pattern = safe_bytes(pattern) + tip = self.get_commit() try: - if pattern.endswith('*'): - basedir = tip.get_node(vcspath.dirname(pattern)) + if bytes_pattern.endswith(b'*'): + basedir = tip.get_node(vcspath.dirname(bytes_pattern)) directories = basedir.dirs else: - directories = (tip.get_node(pattern), ) + directories = (tip.get_node(bytes_pattern), ) except NodeDoesNotExistError: continue - found_items.update((safe_str(n.path), self.commit_ids[-1]) for n in directories) + found_items.update((dir_node.str_path, self.commit_ids[-1]) for dir_node in directories) def get_name(item): return item[0] @@ -216,7 +218,7 @@ class SubversionRepository(base.BaseRepo def _get_commit_idx(self, commit_id): try: svn_rev = int(commit_id) - except: + except Exception: # TODO: johbo: this might be only one case, HEAD, check this svn_rev = self._remote.lookup(commit_id) commit_idx = svn_rev - 1 @@ -321,8 +323,7 @@ class SubversionRepository(base.BaseRepo # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here if branch_name not in [None, self.DEFAULT_BRANCH_NAME]: svn_rev = int(self.commit_ids[-1]) - commit_ids = self._remote.node_history( - path=branch_name, revision=svn_rev, limit=None) + commit_ids = self._remote.node_history(svn_rev, branch_name, None) commit_ids = [str(i) for i in reversed(commit_ids)] if start_pos or end_pos: diff --git a/rhodecode/lib/vcs/client_http.py b/rhodecode/lib/vcs/client_http.py --- a/rhodecode/lib/vcs/client_http.py +++ b/rhodecode/lib/vcs/client_http.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,14 +27,13 @@ import time import urllib.request import urllib.error import urllib.parse -import urllib.parse import uuid import traceback import pycurl import msgpack import requests -from requests.packages.urllib3.util.retry import Retry +from urllib3.util.retry import Retry import rhodecode from rhodecode.lib import rc_cache @@ -216,7 +215,7 @@ class RemoteRepo(object): self._cache_region, self._cache_namespace = \ remote_maker.init_cache_region(cache_repo_id) - with_wire = with_wire or {} + with_wire = with_wire or {"cache": False} repo_state_uid = with_wire.get('repo_state_uid') or 'state' @@ -287,13 +286,15 @@ class RemoteRepo(object): 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length', 'node_history', 'revision', 'tree_items', - 'ctx_list', 'ctx_branch', 'ctx_description', + 'ctx_branch', 'ctx_description', 'bulk_request', 'assert_correct_path', 'is_path_valid_repository', ] - if local_cache_on and name in cache_methods: + wire_cache = self._wire['cache'] + + if local_cache_on and wire_cache and name in cache_methods: cache_on = True repo_state_uid = self._wire['repo_state_uid'] call_args = [a for a in args] @@ -303,6 +304,7 @@ class RemoteRepo(object): @exceptions.map_vcs_exceptions def _call(self, name, *args, **kwargs): + context_uid, payload = self._base_call(name, *args, **kwargs) url = self.url diff --git a/rhodecode/lib/vcs/compat.py b/rhodecode/lib/vcs/compat.py --- a/rhodecode/lib/vcs/compat.py +++ b/rhodecode/lib/vcs/compat.py @@ -1,1 +1,1 @@ -from pyramid.compat import configparser \ No newline at end of file +from pyramid.compat import configparser diff --git a/rhodecode/lib/vcs/conf/__init__.py b/rhodecode/lib/vcs/conf/__init__.py --- a/rhodecode/lib/vcs/conf/__init__.py +++ b/rhodecode/lib/vcs/conf/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/conf/mtypes.py b/rhodecode/lib/vcs/conf/mtypes.py --- a/rhodecode/lib/vcs/conf/mtypes.py +++ b/rhodecode/lib/vcs/conf/mtypes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/conf/settings.py b/rhodecode/lib/vcs/conf/settings.py --- a/rhodecode/lib/vcs/conf/settings.py +++ b/rhodecode/lib/vcs/conf/settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,9 +20,6 @@ Internal settings for vcs-lib """ -# list of default encoding used in safe_str methods -DEFAULT_ENCODINGS = ['utf8'] - # Compatibility version when creating SVN repositories. None means newest. # Other available options are: pre-1.4-compatible, pre-1.5-compatible, diff --git a/rhodecode/lib/vcs/connection.py b/rhodecode/lib/vcs/connection.py --- a/rhodecode/lib/vcs/connection.py +++ b/rhodecode/lib/vcs/connection.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/exceptions.py b/rhodecode/lib/vcs/exceptions.py --- a/rhodecode/lib/vcs/exceptions.py +++ b/rhodecode/lib/vcs/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -102,10 +102,6 @@ class NodeError(VCSError): pass -class RemovedFileNodeError(NodeError): - pass - - class NodeAlreadyExistsError(CommittingError): pass diff --git a/rhodecode/lib/vcs/geventcurl.py b/rhodecode/lib/vcs/geventcurl.py --- a/rhodecode/lib/vcs/geventcurl.py +++ b/rhodecode/lib/vcs/geventcurl.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/nodes.py b/rhodecode/lib/vcs/nodes.py --- a/rhodecode/lib/vcs/nodes.py +++ b/rhodecode/lib/vcs/nodes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,6 +19,7 @@ """ Module holding everything related to vcs nodes, with vcs2 architecture. """ + import functools import os import stat @@ -29,83 +30,25 @@ from rhodecode.config.conf import LANGUA from rhodecode.lib.str_utils import safe_str, safe_bytes from rhodecode.lib.hash_utils import md5 from rhodecode.lib.vcs import path as vcspath -from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT +from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db -from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError - -LARGEFILE_PREFIX = '.hglf' +from rhodecode.lib.vcs.exceptions import NodeError +from rhodecode.lib.vcs_common import NodeKind, FILEMODE_DEFAULT - -class NodeKind: - SUBMODULE = -1 - DIR = 1 - FILE = 2 - LARGEFILE = 3 +LARGEFILE_PREFIX = ".hglf" class NodeState: - ADDED = 'added' - CHANGED = 'changed' - NOT_CHANGED = 'not changed' - REMOVED = 'removed' - -#TODO: not sure if that should be bytes or str ? -# most probably bytes because content should be bytes and we check it -BIN_BYTE_MARKER = b'\0' + ADDED = "added" + CHANGED = "changed" + NOT_CHANGED = "not changed" + REMOVED = "removed" -class NodeGeneratorBase(object): - """ - Base class for removed added and changed filenodes, it's a lazy generator - class that will create filenodes only on iteration or call - - The len method doesn't need to create filenodes at all - """ - - def __init__(self, current_paths, cs): - self.cs = cs - self.current_paths = current_paths - - def __call__(self): - return [n for n in self] - - def __getitem__(self, key): - if isinstance(key, slice): - for p in self.current_paths[key.start:key.stop]: - yield self.cs.get_node(p) - - def __len__(self): - return len(self.current_paths) +# TODO: not sure if that should be bytes or str ? +# most probably bytes because content should be bytes and we check it +BIN_BYTE_MARKER = b"\0" - def __iter__(self): - for p in self.current_paths: - yield self.cs.get_node(p) - - -class AddedFileNodesGenerator(NodeGeneratorBase): - """ - Class holding added files for current commit - """ - - -class ChangedFileNodesGenerator(NodeGeneratorBase): - """ - Class holding changed files for current commit - """ - - -class RemovedFileNodesGenerator(NodeGeneratorBase): - """ - Class holding removed files for current commit - """ - def __iter__(self): - for p in self.current_paths: - yield RemovedFileNode(path=safe_bytes(p)) - - def __getitem__(self, key): - if isinstance(key, slice): - for p in self.current_paths[key.start:key.stop]: - yield RemovedFileNode(path=safe_bytes(p)) @functools.total_ordering @@ -119,21 +62,22 @@ class Node(object): only. Moreover, every single node is identified by the ``path`` attribute, so it cannot end with slash, too. Otherwise, path could lead to mistakes. """ + # RTLO marker allows swapping text, and certain # security attacks could be used with this - RTLO_MARKER = "\u202E" + RTLO_MARKER = "\u202e" commit = None def __init__(self, path: bytes, kind): self._validate_path(path) # can throw exception if path is invalid - self.bytes_path = path.rstrip(b'/') # store for __repr__ - self.path = safe_str(self.bytes_path) # we store paths as str + self.bytes_path: bytes = path.rstrip(b"/") # store for mixed encoding, and raw version + self.str_path: str = safe_str(self.bytes_path) # we store paths as str + self.path: str = self.str_path - if self.bytes_path == b'' and kind != NodeKind.DIR: - raise NodeError("Only DirNode and its subclasses may be " - "initialized with empty path") + if self.bytes_path == b"" and kind != NodeKind.DIR: + raise NodeError("Only DirNode and its subclasses may be initialized with empty path") self.kind = kind if self.is_root() and not self.is_dir(): @@ -142,7 +86,7 @@ class Node(object): def __eq__(self, other): if type(self) is not type(other): return False - for attr in ['name', 'path', 'kind']: + for attr in ["name", "path", "kind"]: if getattr(self, attr) != getattr(other, attr): return False if self.is_file(): @@ -166,22 +110,9 @@ class Node(object): if self.path > other.path: return False - # def __cmp__(self, other): - # """ - # Comparator using name of the node, needed for quick list sorting. - # """ - # - # kind_cmp = cmp(self.kind, other.kind) - # if kind_cmp: - # if isinstance(self, SubModuleNode): - # # we make submodules equal to dirnode for "sorting" purposes - # return NodeKind.DIR - # return kind_cmp - # return cmp(self.name, other.name) - def __repr__(self): - maybe_path = getattr(self, 'path', 'UNKNOWN_PATH') - return f'<{self.__class__.__name__} {maybe_path!r}>' + maybe_path = getattr(self, "path", "UNKNOWN_PATH") + return f"<{self.__class__.__name__} {maybe_path!r}>" def __str__(self): return self.name @@ -189,19 +120,21 @@ class Node(object): def _validate_path(self, path: bytes): self._assert_bytes(path) - if path.startswith(b'/'): + if path.startswith(b"/"): raise NodeError( f"Cannot initialize Node objects with slash at " f"the beginning as only relative paths are supported. " - f"Got {path}") + f"Got {path}" + ) - def _assert_bytes(self, value): + @classmethod + def _assert_bytes(cls, value): if not isinstance(value, bytes): raise TypeError(f"Bytes required as input, got {type(value)} of {value}.") @LazyProperty def parent(self): - parent_path = self.get_parent_path() + parent_path: bytes = self.get_parent_path() if parent_path: if self.commit: return self.commit.get_node(parent_path) @@ -209,10 +142,6 @@ class Node(object): return None @LazyProperty - def str_path(self) -> str: - return safe_str(self.path) - - @LazyProperty def has_rtlo(self): """Detects if a path has right-to-left-override marker""" return self.RTLO_MARKER in self.str_path @@ -223,10 +152,10 @@ class Node(object): Returns name of the directory from full path of this vcs node. Empty string is returned if there's no directory in the path """ - _parts = self.path.rstrip('/').rsplit('/', 1) + _parts = self.path.rstrip("/").rsplit("/", 1) if len(_parts) == 2: return _parts[0] - return '' + return "" @LazyProperty def name(self): @@ -234,7 +163,7 @@ class Node(object): Returns name of the node so if its path then only last part is returned. """ - return self.path.rstrip('/').split('/')[-1] + return self.str_path.rstrip("/").split("/")[-1] @property def kind(self): @@ -242,12 +171,12 @@ class Node(object): @kind.setter def kind(self, kind): - if hasattr(self, '_kind'): + if hasattr(self, "_kind"): raise NodeError("Cannot change node's kind") else: self._kind = kind # Post setter check (path's trailing slash) - if self.path.endswith('/'): + if self.str_path.endswith("/"): raise NodeError("Node's path cannot end with slash") def get_parent_path(self) -> bytes: @@ -255,8 +184,8 @@ class Node(object): Returns node's parent path or empty string if node is root. """ if self.is_root(): - return b'' - str_path = vcspath.dirname(self.path.rstrip('/')) + '/' + return b"" + str_path = vcspath.dirname(self.bytes_path.rstrip(b"/")) + b"/" return safe_bytes(str_path) @@ -278,7 +207,7 @@ class Node(object): """ Returns ``True`` if node is a root node and ``False`` otherwise. """ - return self.kind == NodeKind.DIR and self.path == '' + return self.kind == NodeKind.DIR and self.path == "" def is_submodule(self): """ @@ -292,29 +221,13 @@ class Node(object): Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False`` otherwise """ - return self.kind == NodeKind.LARGEFILE + return self.kind == NodeKind.LARGE_FILE def is_link(self): if self.commit: - return self.commit.is_link(self.path) + return self.commit.is_link(self.bytes_path) return False - @LazyProperty - def added(self): - return self.state is NodeState.ADDED - - @LazyProperty - def changed(self): - return self.state is NodeState.CHANGED - - @LazyProperty - def not_changed(self): - return self.state is NodeState.NOT_CHANGED - - @LazyProperty - def removed(self): - return self.state is NodeState.REMOVED - class FileNode(Node): """ @@ -325,9 +238,10 @@ class FileNode(Node): :attribute: commit: if given, first time content is accessed, callback :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`. """ + _filter_pre_load = [] - def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None): + def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None, pre_load_data=None): """ Only one of ``content`` and ``commit`` may be given. Passing both would raise ``NodeError`` exception. @@ -349,8 +263,10 @@ class FileNode(Node): content = safe_bytes(content) self._content = content self._mode = mode or FILEMODE_DEFAULT - - self._set_bulk_properties(pre_load) + if pre_load_data: + self._store_pre_load(pre_load_data) + else: + self._set_bulk_properties(pre_load) def __eq__(self, other): eq = super().__eq__(other) @@ -359,7 +275,7 @@ class FileNode(Node): return self.content == other.content def __hash__(self): - raw_id = getattr(self.commit, 'raw_id', '') + raw_id = getattr(self.commit, "raw_id", "") return hash((self.path, raw_id)) def __lt__(self, other): @@ -369,33 +285,35 @@ class FileNode(Node): return self.content < other.content def __repr__(self): - short_id = getattr(self.commit, 'short_id', '') - return f'<{self.__class__.__name__} path={self.path!r}, short_id={short_id}>' + short_id = getattr(self.commit, "short_id", "") + return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>" def _set_bulk_properties(self, pre_load): if not pre_load: return - pre_load = [entry for entry in pre_load - if entry not in self._filter_pre_load] + pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load] if not pre_load: return remote = self.commit.get_remote() - result = remote.bulk_file_request(self.commit.raw_id, self.path, pre_load) + result = remote.bulk_file_request(self.commit.raw_id, self.bytes_path, pre_load) - for attr, value in result.items(): + self._store_pre_load(result.items()) + + def _store_pre_load(self, pre_load_data): + for attr, value in pre_load_data: if attr == "flags": - self.__dict__['mode'] = safe_str(value) + self.__dict__["mode"] = safe_str(value) elif attr == "size": - self.__dict__['size'] = value + self.__dict__["size"] = value elif attr == "data": - self.__dict__['_content'] = value + self.__dict__["_content"] = value elif attr == "is_binary": - self.__dict__['is_binary'] = value + self.__dict__["is_binary"] = value elif attr == "md5": - self.__dict__['md5'] = value + self.__dict__["md5"] = value else: - raise ValueError(f'Unsupported attr in bulk_property: {attr}') + raise ValueError(f"Unsupported attr in bulk_property: {attr}") @LazyProperty def mode(self): @@ -404,7 +322,7 @@ class FileNode(Node): use value given at initialization or `FILEMODE_DEFAULT` (default). """ if self.commit: - mode = self.commit.get_file_mode(self.path) + mode = self.commit.get_file_mode(self.bytes_path) else: mode = self._mode return mode @@ -416,7 +334,7 @@ class FileNode(Node): """ if self.commit: if self._content is None: - self._content = self.commit.get_file_content(self.path) + self._content = self.commit.get_file_content(self.bytes_path) content = self._content else: content = self._content @@ -427,7 +345,7 @@ class FileNode(Node): Returns lazily content of the FileNode. """ if self.commit: - content = self.commit.get_file_content(self.path) + content = self.commit.get_file_content(self.bytes_path) else: content = self._content return content @@ -438,7 +356,7 @@ class FileNode(Node): vcsserver without loading it to memory. """ if self.commit: - return self.commit.get_file_content_streamed(self.path) + return self.commit.get_file_content_streamed(self.bytes_path) raise NodeError("Cannot retrieve stream_bytes without related commit attribute") def metadata_uncached(self): @@ -462,7 +380,7 @@ class FileNode(Node): """ content = self.raw_bytes if content and not isinstance(content, bytes): - raise ValueError(f'Content is of type {type(content)} instead of bytes') + raise ValueError(f"Content is of type {type(content)} instead of bytes") return content @LazyProperty @@ -472,27 +390,21 @@ class FileNode(Node): @LazyProperty def size(self): if self.commit: - return self.commit.get_file_size(self.path) - raise NodeError( - "Cannot retrieve size of the file without related " - "commit attribute") + return self.commit.get_file_size(self.bytes_path) + raise NodeError("Cannot retrieve size of the file without related commit attribute") @LazyProperty def message(self): if self.commit: return self.last_commit.message - raise NodeError( - "Cannot retrieve message of the file without related " - "commit attribute") + raise NodeError("Cannot retrieve message of the file without related " "commit attribute") @LazyProperty def last_commit(self): if self.commit: pre_load = ["author", "date", "message", "parents"] - return self.commit.get_path_commit(self.path, pre_load=pre_load) - raise NodeError( - "Cannot retrieve last commit of the file without " - "related commit attribute") + return self.commit.get_path_commit(self.bytes_path, pre_load=pre_load) + raise NodeError("Cannot retrieve last commit of the file without related commit attribute") def get_mimetype(self): """ @@ -502,28 +414,27 @@ class FileNode(Node): attribute to indicate that type should *NOT* be calculated). """ - if hasattr(self, '_mimetype'): - if (isinstance(self._mimetype, (tuple, list)) and - len(self._mimetype) == 2): + if hasattr(self, "_mimetype"): + if isinstance(self._mimetype, (tuple, list)) and len(self._mimetype) == 2: return self._mimetype else: - raise NodeError('given _mimetype attribute must be an 2 ' - 'element list or tuple') + raise NodeError("given _mimetype attribute must be an 2 element list or tuple") db = get_mimetypes_db() mtype, encoding = db.guess_type(self.name) if mtype is None: if not self.is_largefile() and self.is_binary: - mtype = 'application/octet-stream' + mtype = "application/octet-stream" encoding = None else: - mtype = 'text/plain' + mtype = "text/plain" encoding = None # try with pygments try: from pygments.lexers import get_lexer_for_filename + mt = get_lexer_for_filename(self.name).mimetypes except Exception: mt = None @@ -544,18 +455,17 @@ class FileNode(Node): @LazyProperty def mimetype_main(self): - return self.mimetype.split('/')[0] + return self.mimetype.split("/")[0] @classmethod def get_lexer(cls, filename, content=None): from pygments import lexers - extension = filename.split('.')[-1] + extension = filename.split(".")[-1] lexer = None try: - lexer = lexers.guess_lexer_for_filename( - filename, content, stripnl=False) + lexer = lexers.guess_lexer_for_filename(filename, content, stripnl=False) except lexers.ClassNotFound: pass @@ -580,7 +490,7 @@ class FileNode(Node): content, name and mimetype. """ # TODO: this is more proper, but super heavy on investigating the type based on the content - #self.get_lexer(self.name, self.content) + # self.get_lexer(self.name, self.content) return self.get_lexer(self.name) @@ -597,8 +507,8 @@ class FileNode(Node): Returns a list of commit for this file in which the file was changed """ if self.commit is None: - raise NodeError('Unable to get commit for this FileNode') - return self.commit.get_path_history(self.path) + raise NodeError("Unable to get commit for this FileNode") + return self.commit.get_path_history(self.bytes_path) @LazyProperty def annotate(self): @@ -606,22 +516,9 @@ class FileNode(Node): Returns a list of three element tuples with lineno, commit and line """ if self.commit is None: - raise NodeError('Unable to get commit for this FileNode') + raise NodeError("Unable to get commit for this FileNode") pre_load = ["author", "date", "message", "parents"] - return self.commit.get_file_annotate(self.path, pre_load=pre_load) - - @LazyProperty - def state(self): - if not self.commit: - raise NodeError( - "Cannot check state of the node if it's not " - "linked with commit") - elif self.path in (node.path for node in self.commit.added): - return NodeState.ADDED - elif self.path in (node.path for node in self.commit.changed): - return NodeState.CHANGED - else: - return NodeState.NOT_CHANGED + return self.commit.get_file_annotate(self.bytes_path, pre_load=pre_load) @LazyProperty def is_binary(self): @@ -629,7 +526,7 @@ class FileNode(Node): Returns True if file has binary content. """ if self.commit: - return self.commit.is_node_binary(self.path) + return self.commit.is_node_binary(self.bytes_path) else: raw_bytes = self._content return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes) @@ -641,7 +538,7 @@ class FileNode(Node): """ if self.commit: - return self.commit.node_md5_hash(self.path) + return self.commit.node_md5_hash(self.bytes_path) else: raw_bytes = self._content # TODO: this sucks, we're computing md5 on potentially super big stream data... @@ -650,7 +547,7 @@ class FileNode(Node): @LazyProperty def extension(self): """Returns filenode extension""" - return self.name.split('.')[-1] + return self.name.split(".")[-1] @property def is_executable(self): @@ -667,15 +564,15 @@ class FileNode(Node): LF store. """ if self.commit: - return self.commit.get_largefile_node(self.path) + return self.commit.get_largefile_node(self.bytes_path) def count_lines(self, content: str | bytes, count_empty=False): if isinstance(content, str): - newline_marker = '\n' + newline_marker = "\n" elif isinstance(content, bytes): - newline_marker = b'\n' + newline_marker = b"\n" else: - raise ValueError('content must be bytes or str got {type(content)} instead') + raise ValueError("content must be bytes or str got {type(content)} instead") if count_empty: all_lines = 0 @@ -704,33 +601,6 @@ class FileNode(Node): return all_lines, empty_lines -class RemovedFileNode(FileNode): - """ - Dummy FileNode class - trying to access any public attribute except path, - name, kind or state (or methods/attributes checking those two) would raise - RemovedFileNodeError. - """ - ALLOWED_ATTRIBUTES = [ - 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind', - 'added', 'changed', 'not_changed', 'removed', 'bytes_path' - ] - - def __init__(self, path): - """ - :param path: relative path to the node - """ - super().__init__(path=path) - - def __getattribute__(self, attr): - if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES: - return super().__getattribute__(attr) - raise RemovedFileNodeError(f"Cannot access attribute {attr} on RemovedFileNode. Not in allowed attributes") - - @LazyProperty - def state(self): - return NodeState.REMOVED - - class DirNode(Node): """ DirNode stores list of files and directories within this node. @@ -752,7 +622,7 @@ class DirNode(Node): super().__init__(path, NodeKind.DIR) self.commit = commit self._nodes = nodes - self.default_pre_load = default_pre_load or ['is_binary', 'size'] + self.default_pre_load = default_pre_load or ["is_binary", "size"] def __iter__(self): yield from self.nodes @@ -782,10 +652,9 @@ class DirNode(Node): @LazyProperty def nodes(self): if self.commit: - nodes = self.commit.get_nodes(self.path, pre_load=self.default_pre_load) + nodes = self.commit.get_nodes(self.bytes_path, pre_load=self.default_pre_load) else: nodes = self._nodes - self._nodes_dict = {node.path: node for node in nodes} return sorted(nodes) @LazyProperty @@ -796,47 +665,6 @@ class DirNode(Node): def dirs(self): return sorted(node for node in self.nodes if node.is_dir()) - def get_node(self, path): - """ - Returns node from within this particular ``DirNode``, so it is now - allowed to fetch, i.e. node located at 'docs/api/index.rst' from node - 'docs'. In order to access deeper nodes one must fetch nodes between - them first - this would work:: - - docs = root.get_node('docs') - docs.get_node('api').get_node('index.rst') - - :param: path - relative to the current node - - .. note:: - To access lazily (as in example above) node have to be initialized - with related commit object - without it node is out of - context and may know nothing about anything else than nearest - (located at same level) nodes. - """ - try: - path = path.rstrip('/') - if path == '': - raise NodeError("Cannot retrieve node without path") - self.nodes # access nodes first in order to set _nodes_dict - paths = path.split('/') - if len(paths) == 1: - if not self.is_root(): - path = '/'.join((self.path, paths[0])) - else: - path = paths[0] - return self._nodes_dict[path] - elif len(paths) > 1: - if self.commit is None: - raise NodeError("Cannot access deeper nodes without commit") - else: - path1, path2 = paths[0], '/'.join(paths[1:]) - return self.get_node(path1).get_node(path2) - else: - raise KeyError - except KeyError: - raise NodeError(f"Node does not exist at {path}") - @LazyProperty def state(self): raise NodeError("Cannot access state of DirNode") @@ -844,7 +672,7 @@ class DirNode(Node): @LazyProperty def size(self): size = 0 - for root, dirs, files in self.commit.walk(self.path): + for root, dirs, files in self.commit.walk(self.bytes_path): for f in files: size += f.size @@ -854,14 +682,12 @@ class DirNode(Node): def last_commit(self): if self.commit: pre_load = ["author", "date", "message", "parents"] - return self.commit.get_path_commit(self.path, pre_load=pre_load) - raise NodeError( - "Cannot retrieve last commit of the file without " - "related commit attribute") + return self.commit.get_path_commit(self.bytes_path, pre_load=pre_load) + raise NodeError("Cannot retrieve last commit of the file without related commit attribute") def __repr__(self): - short_id = getattr(self.commit, 'short_id', '') - return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>' + short_id = getattr(self.commit, "short_id", "") + return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>" class RootNode(DirNode): @@ -870,37 +696,39 @@ class RootNode(DirNode): """ def __init__(self, nodes=(), commit=None): - super().__init__(path=b'', nodes=nodes, commit=commit) + super().__init__(path=b"", nodes=nodes, commit=commit) def __repr__(self): - return f'<{self.__class__.__name__}>' + short_id = getattr(self.commit, "short_id", "") + return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>" class SubModuleNode(Node): """ represents a SubModule of Git or SubRepo of Mercurial """ + is_binary = False size = 0 def __init__(self, name, url=None, commit=None, alias=None): - self.path = name + self.path: bytes = name + self.str_path: str = safe_str(self.path) # we store paths as str self.kind = NodeKind.SUBMODULE self.alias = alias # we have to use EmptyCommit here since this can point to svn/git/hg # submodules we cannot get from repository - self.commit = EmptyCommit(str(commit), alias=alias) - self.url = url or self._extract_submodule_url() + self.commit = EmptyCommit(safe_str(commit), alias=alias) + self.url = safe_str(url) or self._extract_submodule_url() def __repr__(self): - short_id = getattr(self.commit, 'short_id', '') - return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>' + short_id = getattr(self.commit, "short_id", "") + return f"<{self.__class__.__name__} {self.str_path!r} @ {short_id}>" def _extract_submodule_url(self): - # TODO: find a way to parse gits submodule file and extract the - # linking URL - return self.path + # TODO: find a way to parse gits submodule file and extract the linking URL + return safe_str(self.path) @LazyProperty def name(self): @@ -908,22 +736,22 @@ class SubModuleNode(Node): Returns name of the node so if its path then only last part is returned. """ - org = safe_str(self.path.rstrip('/').split('/')[-1]) - return f'{org} @ {self.commit.short_id}' + org = self.str_path.rstrip("/").split("/")[-1] + return f"{org} @ {self.commit.short_id}" class LargeFileNode(FileNode): - def __init__(self, path, url=None, commit=None, alias=None, org_path=None): self._validate_path(path) # can throw exception if path is invalid self.org_path = org_path # as stored in VCS as LF pointer - self.bytes_path = path.rstrip(b'/') # store for __repr__ - self.path = safe_str(self.bytes_path) # we store paths as str + self.bytes_path = path.rstrip(b"/") # store for __repr__ + self.str_path = safe_str(self.bytes_path) + self.path = self.str_path - self.kind = NodeKind.LARGEFILE + self.kind = NodeKind.LARGE_FILE self.alias = alias - self._content = b'' + self._content = b"" def _validate_path(self, path: bytes): """ @@ -932,7 +760,7 @@ class LargeFileNode(FileNode): self._assert_bytes(path) def __repr__(self): - return f'<{self.__class__.__name__} {self.org_path} -> {self.path!r}>' + return f"<{self.__class__.__name__} {self.org_path} -> {self.str_path!r}>" @LazyProperty def size(self): @@ -940,7 +768,7 @@ class LargeFileNode(FileNode): @LazyProperty def raw_bytes(self): - with open(self.path, 'rb') as f: + with open(self.path, "rb") as f: content = f.read() return content @@ -952,7 +780,7 @@ class LargeFileNode(FileNode): return self.org_path def stream_bytes(self): - with open(self.path, 'rb') as stream: + with open(self.path, "rb") as stream: while True: data = stream.read(16 * 1024) if not data: diff --git a/rhodecode/lib/vcs/path.py b/rhodecode/lib/vcs/path.py --- a/rhodecode/lib/vcs/path.py +++ b/rhodecode/lib/vcs/path.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/utils/__init__.py b/rhodecode/lib/vcs/utils/__init__.py --- a/rhodecode/lib/vcs/utils/__init__.py +++ b/rhodecode/lib/vcs/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/utils/helpers.py b/rhodecode/lib/vcs/utils/helpers.py --- a/rhodecode/lib/vcs/utils/helpers.py +++ b/rhodecode/lib/vcs/utils/helpers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2023 RhodeCode GmbH +# Copyright (C) 2014-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/utils/imports.py b/rhodecode/lib/vcs/utils/imports.py --- a/rhodecode/lib/vcs/utils/imports.py +++ b/rhodecode/lib/vcs/utils/imports.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs/utils/paths.py b/rhodecode/lib/vcs/utils/paths.py --- a/rhodecode/lib/vcs/utils/paths.py +++ b/rhodecode/lib/vcs/utils/paths.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/lib/vcs_common.py b/rhodecode/lib/vcs_common.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs_common.py @@ -0,0 +1,46 @@ +# Copyright (C) 2014-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +""" +Common VCS module for rhodecode and vcsserver +""" + + +import enum + +FILEMODE_DEFAULT = 0o100644 +FILEMODE_EXECUTABLE = 0o100755 +FILEMODE_LINK = 0o120000 + + +class NodeKind(int, enum.Enum): + SUBMODULE = -1 + DIR = 1 + FILE = 2 + LARGE_FILE = 3 + + +def map_git_obj_type(obj_type): + if obj_type == "blob": + return NodeKind.FILE + elif obj_type == "tree": + return NodeKind.DIR + elif obj_type == "link": + return NodeKind.SUBMODULE + return None diff --git a/rhodecode/lib/view_utils.py b/rhodecode/lib/view_utils.py --- a/rhodecode/lib/view_utils.py +++ b/rhodecode/lib/view_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/auth_token.py b/rhodecode/model/auth_token.py --- a/rhodecode/model/auth_token.py +++ b/rhodecode/model/auth_token.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/changeset_status.py b/rhodecode/model/changeset_status.py --- a/rhodecode/model/changeset_status.py +++ b/rhodecode/model/changeset_status.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,12 +25,13 @@ import logging import traceback import collections -from pyramid.threadlocal import get_current_registry, get_current_request +from pyramid.threadlocal import get_current_registry from sqlalchemy.sql.expression import null from sqlalchemy.sql.functions import coalesce from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils from rhodecode.lib import audit_logger +from rhodecode.lib.pyramid_utils import get_current_request from rhodecode.lib.exceptions import CommentVersionMismatch from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int from rhodecode.model import BaseModel @@ -373,6 +374,7 @@ class CommentsModel(BaseModel): Session().add(comment) Session().flush() + kwargs = { 'user': user, 'renderer_type': renderer, @@ -387,8 +389,7 @@ class CommentsModel(BaseModel): } if commit_obj: - recipients = ChangesetComment.get_users( - revision=commit_obj.raw_id) + recipients = ChangesetComment.get_users(revision=commit_obj.raw_id) # add commit author if it's in RhodeCode system cs_author = User.get_from_cs_author(commit_obj.author) if not cs_author: @@ -397,16 +398,13 @@ class CommentsModel(BaseModel): recipients += [cs_author] commit_comment_url = self.get_url(comment, request=request) - commit_comment_reply_url = self.get_url( - comment, request=request, - anchor=f'comment-{comment.comment_id}/?/ReplyToComment') + commit_comment_reply_url = self.get_url(comment, request=request, anchor=f'comment-{comment.comment_id}/?/ReplyToComment') target_repo_url = h.link_to( repo.repo_name, h.route_url('repo_summary', repo_name=repo.repo_name)) - commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, - commit_id=commit_id) + commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, commit_id=commit_id) # commit specifics kwargs.update({ @@ -489,7 +487,6 @@ class CommentsModel(BaseModel): if not is_draft: comment_data = comment.get_api_data() - self._log_audit_action( action, {'data': comment_data}, auth_user, comment) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -35,7 +35,7 @@ import collections import pyotp from sqlalchemy import ( - or_, and_, not_, func, cast, TypeDecorator, event, select, + or_, and_, not_, func, cast, TypeDecorator, event, select, delete, true, false, null, union_all, Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, @@ -274,6 +274,22 @@ class BaseModel(object): return stmt @classmethod + def delete(cls, custom_cls=None): + """ + stmt = cls.delete().where(cls.user_id==1) + # optionally + stmt = cls.delete(User).where(cls.user_id==1) + result = cls.execute(stmt) + """ + + if custom_cls: + stmt = delete(custom_cls) + else: + stmt = delete(cls) + return stmt + + + @classmethod def execute(cls, stmt): return Session().execute(stmt) @@ -284,7 +300,7 @@ class BaseModel(object): @classmethod def get(cls, id_): if id_: - return cls.query().get(id_) + return Session().get(cls, id_) @classmethod def get_or_404(cls, id_): @@ -1075,28 +1091,26 @@ class User(Base, BaseModel): @classmethod def get(cls, user_id, cache=False): if not user_id: - return - - user = cls.query() + return None + + q = cls.select().where(cls.user_id == user_id) if cache: - user = user.options( - FromCache("sql_cache_short", f"get_users_{user_id}")) - return user.get(user_id) + q = q.options(FromCache("sql_cache_short", f"get_users_{user_id}")) + return cls.execute(q).scalar_one_or_none() @classmethod - def get_by_username(cls, username, case_insensitive=False, - cache=False): + def get_by_username(cls, username, case_insensitive=False, cache=False): + if not username: + return None if case_insensitive: - q = cls.select().where( - func.lower(cls.username) == func.lower(username)) + q = cls.select().where(func.lower(cls.username) == func.lower(username)) else: q = cls.select().where(cls.username == username) if cache: hash_key = _hash_key(username) - q = q.options( - FromCache("sql_cache_short", f"get_user_by_name_{hash_key}")) + q = q.options(FromCache("sql_cache_short", f"get_user_by_name_{hash_key}")) return cls.execute(q).scalar_one_or_none() @@ -1125,6 +1139,8 @@ class User(Base, BaseModel): @classmethod def get_by_email(cls, email, case_insensitive=False, cache=False): + if not email: + return None if case_insensitive: q = cls.select().where(func.lower(cls.email) == func.lower(email)) @@ -1133,8 +1149,7 @@ class User(Base, BaseModel): if cache: email_key = _hash_key(email) - q = q.options( - FromCache("sql_cache_short", f"get_email_key_{email_key}")) + q = q.options(FromCache("sql_cache_short", f"get_email_key_{email_key}")) ret = cls.execute(q).scalar_one_or_none() @@ -1147,8 +1162,8 @@ class User(Base, BaseModel): q = q.where(UserEmailMap.email == email) q = q.options(joinedload(UserEmailMap.user)) if cache: - q = q.options( - FromCache("sql_cache_short", f"get_email_map_key_{email_key}")) + email_key = _hash_key(email) + q = q.options(FromCache("sql_cache_short", f"get_email_map_key_{email_key}")) result = cls.execute(q).scalar_one_or_none() ret = getattr(result, 'user', None) @@ -1642,11 +1657,12 @@ class UserGroup(Base, BaseModel): return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>" @classmethod - def get_by_group_name(cls, group_name, cache=False, - case_insensitive=False): + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if not group_name: + return None + if case_insensitive: - q = cls.query().filter(func.lower(cls.users_group_name) == - func.lower(group_name)) + q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name)) else: q = cls.query().filter(cls.users_group_name == group_name) @@ -2968,9 +2984,11 @@ class RepoGroup(Base, BaseModel): @classmethod def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if not group_name: + return None + if case_insensitive: - gr = cls.query().filter(func.lower(cls.group_name) - == func.lower(group_name)) + gr = cls.query().filter(func.lower(cls.group_name) == func.lower(group_name)) else: gr = cls.query().filter(cls.group_name == group_name) if cache: diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -445,6 +445,7 @@ class _BaseVcsSettingsForm(formencode.Sc # PR/Code-review rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False) + rhodecode_auto_merge_enabled = v.StringBoolean(if_missing=False) rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False) # hg diff --git a/rhodecode/model/gist.py b/rhodecode/model/gist.py --- a/rhodecode/model/gist.py +++ b/rhodecode/model/gist.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -103,7 +103,7 @@ class GistModel(BaseModel): raise VCSError(f'Failed to load gist repository for {repo}') commit = vcs_repo.get_commit(commit_id=revision) - return commit, [n for n in commit.get_node('/')] + return commit, [n for n in commit.get_node(b'/')] def create(self, description, owner, gist_mapping, gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None, diff --git a/rhodecode/model/integration.py b/rhodecode/model/integration.py --- a/rhodecode/model/integration.py +++ b/rhodecode/model/integration.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/meta.py b/rhodecode/model/meta.py --- a/rhodecode/model/meta.py +++ b/rhodecode/model/meta.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,11 +25,11 @@ import logging import traceback import premailer -from pyramid.threadlocal import get_current_request from sqlalchemy.sql.expression import false, true import rhodecode from rhodecode.lib import helpers as h +from rhodecode.lib.pyramid_utils import get_current_request from rhodecode.model import BaseModel from rhodecode.model.db import Notification, User, UserNotification from rhodecode.model.meta import Session diff --git a/rhodecode/model/permission.py b/rhodecode/model/permission.py --- a/rhodecode/model/permission.py +++ b/rhodecode/model/permission.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -38,7 +38,7 @@ from rhodecode.translation import lazy_u from rhodecode.lib import helpers as h, hooks_utils, diffs from rhodecode.lib import audit_logger from collections import OrderedDict -from rhodecode.lib.hook_daemon.base import prepare_callback_daemon +from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.markup_renderer import ( DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer) @@ -178,6 +178,7 @@ def get_diff_info( log.debug('Calculating authors of changed files') target_commit = source_repo.get_commit(ancestor_id) + # TODO: change to operate in bytes.. for fname, lines in changed_lines.items(): try: @@ -980,9 +981,7 @@ class PullRequestModel(BaseModel): target_ref = self._refresh_reference( pull_request.target_ref_parts, target_vcs) - callback_daemon, extras = prepare_callback_daemon( - extras, protocol=vcs_settings.HOOKS_PROTOCOL, - host=vcs_settings.HOOKS_HOST) + callback_daemon, extras = prepare_callback_daemon(extras, protocol=vcs_settings.HOOKS_PROTOCOL) with callback_daemon: # TODO: johbo: Implement a clean way to run a config_override @@ -2125,6 +2124,10 @@ class PullRequestModel(BaseModel): return self._get_general_setting( pull_request, 'rhodecode_pr_merge_enabled') + def is_automatic_merge_enabled(self, pull_request): + return self._get_general_setting( + pull_request, 'rhodecode_auto_merge_enabled') + def _use_rebase_for_merging(self, pull_request): repo_type = pull_request.target_repo.repo_type if repo_type == 'hg': @@ -2225,8 +2228,7 @@ class MergeCheck(object): ) @classmethod - def validate(cls, pull_request, auth_user, translator, fail_early=False, - force_shadow_repo_refresh=False): + def validate(cls, pull_request, auth_user, translator, fail_early=False, force_shadow_repo_refresh=False): _ = translator merge_check = cls() @@ -2287,12 +2289,10 @@ class MergeCheck(object): # left over TODOs todos = CommentsModel().get_pull_request_unresolved_todos(pull_request) if todos: - log.debug("MergeCheck: cannot merge, {} " - "unresolved TODOs left.".format(len(todos))) + log.debug("MergeCheck: cannot merge, %s unresolved TODOs left.", len(todos)) if len(todos) == 1: - msg = _('Cannot merge, {} TODO still not resolved.').format( - len(todos)) + msg = _('Cannot merge, {} TODO still not resolved.').format(len(todos)) else: msg = _('Cannot merge, {} TODOs still not resolved.').format( len(todos)) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -33,6 +33,7 @@ from rhodecode.lib.auth import HasUserGr from rhodecode.lib.caching_query import FromCache from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError from rhodecode.lib import hooks_base +from rhodecode.lib.str_utils import safe_bytes from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.utils import make_db_config from rhodecode.lib.utils2 import ( @@ -745,7 +746,7 @@ class RepoModel(BaseModel): log.error(traceback.format_exc()) raise - def delete(self, repo, forks=None, pull_requests=None, artifacts=None, fs_remove=True, cur_user=None): + def delete(self, repo, forks=None, pull_requests=None, artifacts=None, fs_remove=True, cur_user=None, call_events=True): """ Delete given repository, forks parameter defines what do do with attached forks. Throws AttachedForksError if deleted repo has attached @@ -760,47 +761,54 @@ class RepoModel(BaseModel): if not cur_user: cur_user = getattr(get_current_rhodecode_user(), 'username', None) repo = self._get_repo(repo) - if repo: - if forks == 'detach': - for r in repo.forks: - r.fork = None - self.sa.add(r) - elif forks == 'delete': - for r in repo.forks: - self.delete(r, forks='delete') - elif [f for f in repo.forks]: - raise AttachedForksError() + if not repo: + return False - # check for pull requests - pr_sources = repo.pull_requests_source - pr_targets = repo.pull_requests_target - if pull_requests != 'delete' and (pr_sources or pr_targets): - raise AttachedPullRequestsError() + if forks == 'detach': + for r in repo.forks: + r.fork = None + self.sa.add(r) + elif forks == 'delete': + for r in repo.forks: + self.delete(r, forks='delete') + elif [f for f in repo.forks]: + raise AttachedForksError() + + # check for pull requests + pr_sources = repo.pull_requests_source + pr_targets = repo.pull_requests_target + if pull_requests != 'delete' and (pr_sources or pr_targets): + raise AttachedPullRequestsError() - artifacts_objs = repo.artifacts - if artifacts == 'delete': - for a in artifacts_objs: - self.sa.delete(a) - elif [a for a in artifacts_objs]: - raise AttachedArtifactsError() + artifacts_objs = repo.artifacts + if artifacts == 'delete': + for a in artifacts_objs: + self.sa.delete(a) + elif [a for a in artifacts_objs]: + raise AttachedArtifactsError() - old_repo_dict = repo.get_dict() + old_repo_dict = repo.get_dict() + if call_events: events.trigger(events.RepoPreDeleteEvent(repo)) - try: - self.sa.delete(repo) - if fs_remove: - self._delete_filesystem_repo(repo) - else: - log.debug('skipping removal from filesystem') - old_repo_dict.update({ - 'deleted_by': cur_user, - 'deleted_on': time.time(), - }) + + try: + self.sa.delete(repo) + if fs_remove: + self._delete_filesystem_repo(repo) + else: + log.debug('skipping removal from filesystem') + old_repo_dict.update({ + 'deleted_by': cur_user, + 'deleted_on': time.time(), + }) + if call_events: hooks_base.delete_repository(**old_repo_dict) events.trigger(events.RepoDeleteEvent(repo)) - except Exception: - log.error(traceback.format_exc()) - raise + except Exception: + log.error(traceback.format_exc()) + raise + + return True def grant_user_permission(self, repo, user, perm): """ @@ -1102,47 +1110,49 @@ class ReadmeFinder: different. """ - readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE) - path_re = re.compile(r'^docs?', re.IGNORECASE) + readme_re = re.compile(br'^readme(\.[^.]+)?$', re.IGNORECASE) + path_re = re.compile(br'^docs?', re.IGNORECASE) default_priorities = { None: 0, - '.rst': 1, - '.md': 1, - '.rest': 2, - '.mkdn': 2, - '.text': 2, - '.txt': 3, - '.mdown': 3, - '.markdown': 4, + b'.rst': 1, + b'.md': 1, + b'.rest': 2, + b'.mkdn': 2, + b'.text': 2, + b'.txt': 3, + b'.mdown': 3, + b'.markdown': 4, } path_priority = { - 'doc': 0, - 'docs': 1, + b'doc': 0, + b'docs': 1, } FALLBACK_PRIORITY = 99 RENDERER_TO_EXTENSION = { - 'rst': ['.rst', '.rest'], - 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'], + 'rst': [b'.rst', b'.rest'], + 'markdown': [b'.md', b'mkdn', b'.mdown', b'.markdown'], } def __init__(self, default_renderer=None): self._default_renderer = default_renderer - self._renderer_extensions = self.RENDERER_TO_EXTENSION.get( - default_renderer, []) + self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(default_renderer, []) - def search(self, commit, path='/'): + def search(self, commit, path=b'/', nodes=None): """ Find a readme in the given `commit`. """ # firstly, check the PATH type if it is actually a DIR - if commit.get_node(path).kind != NodeKind.DIR: + bytes_path = safe_bytes(path) + if commit.get_node(bytes_path).kind != NodeKind.DIR: return None - nodes = commit.get_nodes(path) + if not nodes: + nodes = commit.get_nodes(bytes_path) + matches = self._match_readmes(nodes) matches = self._sort_according_to_priority(matches) if matches: @@ -1150,8 +1160,8 @@ class ReadmeFinder: paths = self._match_paths(nodes) paths = self._sort_paths_according_to_priority(paths) - for path in paths: - match = self.search(commit, path=path) + for bytes_path in paths: + match = self.search(commit, path=bytes_path) if match: return match @@ -1161,7 +1171,7 @@ class ReadmeFinder: for node in nodes: if not node.is_file(): continue - path = node.path.rsplit('/', 1)[-1] + path = node.bytes_path.rsplit(b'/', 1)[-1] match = self.readme_re.match(path) if match: extension = match.group(1) @@ -1171,28 +1181,26 @@ class ReadmeFinder: for node in nodes: if not node.is_dir(): continue - match = self.path_re.match(node.path) + match = self.path_re.match(node.bytes_path) if match: - yield node.path + yield node.bytes_path def _priority(self, extension): - renderer_priority = ( - 0 if extension in self._renderer_extensions else 1) - extension_priority = self.default_priorities.get( - extension, self.FALLBACK_PRIORITY) - return (renderer_priority, extension_priority) + renderer_priority = 0 if extension in self._renderer_extensions else 1 + extension_priority = self.default_priorities.get(extension, self.FALLBACK_PRIORITY) + return renderer_priority, extension_priority def _sort_according_to_priority(self, matches): def priority_and_path(match): - return (match.priority, match.path) + return match.priority, match.path return sorted(matches, key=priority_and_path) def _sort_paths_according_to_priority(self, paths): def priority_and_path(path): - return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path) + return self.path_priority.get(path, self.FALLBACK_PRIORITY), path return sorted(paths, key=priority_and_path) diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -156,7 +156,7 @@ class RepoGroupModel(BaseModel): def check_exist_filesystem(self, group_name, exc_on_failure=True): create_path = os.path.join(self.repos_path, group_name) - log.debug('creating new group in %s', create_path) + log.debug('checking FS presence for repo group in %s', create_path) if os.path.isdir(create_path): if exc_on_failure: @@ -573,10 +573,11 @@ class RepoGroupModel(BaseModel): log.error(traceback.format_exc()) raise - def delete(self, repo_group, force_delete=False, fs_remove=True): + def delete(self, repo_group, force_delete=False, fs_remove=True, call_events=True): repo_group = self._get_repo_group(repo_group) if not repo_group: return False + repo_group_name = repo_group.group_name try: self.sa.delete(repo_group) if fs_remove: @@ -585,13 +586,15 @@ class RepoGroupModel(BaseModel): log.debug('skipping removal from filesystem') # Trigger delete event. - events.trigger(events.RepoGroupDeleteEvent(repo_group)) - return True + if call_events: + events.trigger(events.RepoGroupDeleteEvent(repo_group)) except Exception: - log.error('Error removing repo_group %s', repo_group) + log.error('Error removing repo_group %s', repo_group_name) raise + return True + def grant_user_permission(self, repo_group, user, perm): """ Grant permission for user on given repository group, or update diff --git a/rhodecode/model/repo_permission.py b/rhodecode/model/repo_permission.py --- a/rhodecode/model/repo_permission.py +++ b/rhodecode/model/repo_permission.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -213,12 +213,10 @@ class ScmModel(BaseModel): try: if name in repos: - raise RepositoryError('Duplicate repository name %s ' - 'found in %s' % (name, path)) + raise RepositoryError(f'Duplicate repository name {name} found in {path}') elif path[0] in rhodecode.BACKENDS: backend = get_backend(path[0]) - repos[name] = backend(path[1], config=config, - with_wire={"cache": False}) + repos[name] = backend(path[1], config=config, with_wire={"cache": False}) except OSError: continue except RepositoryError: @@ -545,7 +543,7 @@ class ScmModel(BaseModel): root_path = root_path.lstrip('/') # get RootNode, inject pre-load options before walking - top_node = commit.get_node(root_path) + top_node = commit.get_node(safe_bytes(root_path)) extended_info_pre_load = [] if extended_info: extended_info_pre_load += ['md5'] @@ -616,12 +614,13 @@ class ScmModel(BaseModel): _files = list() _dirs = list() + bytes_path = safe_bytes(root_path) try: _repo = self._get_repo(repo_name) commit = _repo.scm_instance().get_commit(commit_id=commit_id) - root_path = root_path.lstrip('/') + root_path = bytes_path.lstrip(b'/') - top_node = commit.get_node(root_path) + top_node = commit.get_node(safe_bytes(root_path)) top_node.default_pre_load = [] for __, dirs, files in commit.walk(top_node): @@ -738,7 +737,7 @@ class ScmModel(BaseModel): _repo = self._get_repo(repo_name) commit = _repo.scm_instance().get_commit(commit_id=commit_id) root_path = root_path.lstrip('/') - top_node = commit.get_node(root_path) + top_node = commit.get_node(safe_bytes(root_path)) top_node.default_pre_load = [] for __, dirs, files in commit.walk(top_node): @@ -776,7 +775,7 @@ class ScmModel(BaseModel): only for git :param trigger_push_hook: trigger push hooks - :returns: new committed commit + :returns: new commit """ user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars( user, repo, message, author) diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -461,6 +461,7 @@ class VcsSettingsModel(object): GENERAL_SETTINGS = ( 'use_outdated_comments', 'pr_merge_enabled', + 'auto_merge_enabled', 'hg_use_rebase_for_merging', 'hg_close_branch_before_merging', 'git_use_rebase_for_merging', @@ -862,27 +863,3 @@ class VcsSettingsModel(object): raise ValueError( f'The given data does not contain {data_key} key') return data_keys - - def create_largeobjects_dirs_if_needed(self, repo_store_path): - """ - This is subscribed to the `pyramid.events.ApplicationCreated` event. It - does a repository scan if enabled in the settings. - """ - - from rhodecode.lib.vcs.backends.hg import largefiles_store - from rhodecode.lib.vcs.backends.git import lfs_store - - paths = [ - largefiles_store(repo_store_path), - lfs_store(repo_store_path)] - - for path in paths: - if os.path.isdir(path): - continue - if os.path.isfile(path): - continue - # not a file nor dir, we try to create it - try: - os.makedirs(path) - except Exception: - log.warning('Failed to create largefiles dir:%s', path) diff --git a/rhodecode/model/ssh_key.py b/rhodecode/model/ssh_key.py --- a/rhodecode/model/ssh_key.py +++ b/rhodecode/model/ssh_key.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/update.py b/rhodecode/model/update.py --- a/rhodecode/model/update.py +++ b/rhodecode/model/update.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2023 RhodeCode GmbH +# Copyright (C) 2013-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -54,11 +54,12 @@ class UserModel(BaseModel): cls = User def get(self, user_id, cache=False): - user = self.sa.query(User) + cls = self.cls + q = cls.select().where(cls.user_id == user_id) if cache: - user = user.options( - FromCache("sql_cache_short", f"get_user_{user_id}")) - return user.get(user_id) + q = q.options( + FromCache("sql_cache_short", f"get_users_{user_id}")) + return cls.execute(q).scalar_one_or_none() def get_user(self, user): return self._get_user(user) diff --git a/rhodecode/model/user_group.py b/rhodecode/model/user_group.py --- a/rhodecode/model/user_group.py +++ b/rhodecode/model/user_group.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/__init__.py b/rhodecode/model/validation_schema/__init__.py --- a/rhodecode/model/validation_schema/__init__.py +++ b/rhodecode/model/validation_schema/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/preparers.py b/rhodecode/model/validation_schema/preparers.py --- a/rhodecode/model/validation_schema/preparers.py +++ b/rhodecode/model/validation_schema/preparers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/__init__.py b/rhodecode/model/validation_schema/schemas/__init__.py --- a/rhodecode/model/validation_schema/schemas/__init__.py +++ b/rhodecode/model/validation_schema/schemas/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/comment_schema.py b/rhodecode/model/validation_schema/schemas/comment_schema.py --- a/rhodecode/model/validation_schema/schemas/comment_schema.py +++ b/rhodecode/model/validation_schema/schemas/comment_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2023 RhodeCode GmbH +# Copyright (C) 2017-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/gist_schema.py b/rhodecode/model/validation_schema/schemas/gist_schema.py --- a/rhodecode/model/validation_schema/schemas/gist_schema.py +++ b/rhodecode/model/validation_schema/schemas/gist_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/integration_schema.py b/rhodecode/model/validation_schema/schemas/integration_schema.py --- a/rhodecode/model/validation_schema/schemas/integration_schema.py +++ b/rhodecode/model/validation_schema/schemas/integration_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/repo_group_schema.py b/rhodecode/model/validation_schema/schemas/repo_group_schema.py --- a/rhodecode/model/validation_schema/schemas/repo_group_schema.py +++ b/rhodecode/model/validation_schema/schemas/repo_group_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/repo_schema.py b/rhodecode/model/validation_schema/schemas/repo_schema.py --- a/rhodecode/model/validation_schema/schemas/repo_schema.py +++ b/rhodecode/model/validation_schema/schemas/repo_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/reviewer_schema.py b/rhodecode/model/validation_schema/schemas/reviewer_schema.py --- a/rhodecode/model/validation_schema/schemas/reviewer_schema.py +++ b/rhodecode/model/validation_schema/schemas/reviewer_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/search_schema.py b/rhodecode/model/validation_schema/schemas/search_schema.py --- a/rhodecode/model/validation_schema/schemas/search_schema.py +++ b/rhodecode/model/validation_schema/schemas/search_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/user_group_schema.py b/rhodecode/model/validation_schema/schemas/user_group_schema.py --- a/rhodecode/model/validation_schema/schemas/user_group_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_group_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py --- a/rhodecode/model/validation_schema/schemas/user_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/types.py b/rhodecode/model/validation_schema/types.py --- a/rhodecode/model/validation_schema/types.py +++ b/rhodecode/model/validation_schema/types.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/utils.py b/rhodecode/model/validation_schema/utils.py --- a/rhodecode/model/validation_schema/utils.py +++ b/rhodecode/model/validation_schema/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py --- a/rhodecode/model/validation_schema/validators.py +++ b/rhodecode/model/validation_schema/validators.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validation_schema/widgets.py b/rhodecode/model/validation_schema/widgets.py --- a/rhodecode/model/validation_schema/widgets.py +++ b/rhodecode/model/validation_schema/widgets.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/public/css/legacy_code_styles.less b/rhodecode/public/css/legacy_code_styles.less --- a/rhodecode/public/css/legacy_code_styles.less +++ b/rhodecode/public/css/legacy_code_styles.less @@ -287,10 +287,18 @@ div.markdown-block #message { font-family: @text-monospace; font-size: 11px; .border-radius(@border-radius); - background-color: white; color: @grey3; } +div.markdown-block code, +div.markdown-block pre { + background-color: transparent; +} + +div.markdown-block #ws, +div.markdown-block #message { + background-color: white; +} div.markdown-block code { border: @border-thickness solid @grey6; diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -41,6 +41,9 @@ function registerRCRoutes() { pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []); pyroutes.register('admin_scheduler', '/_admin/scheduler', []); pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []); + pyroutes.register('admin_security', '/_admin/security', []); + pyroutes.register('admin_security_modify_allowed_vcs_client_versions', '/_admin/security/modify/allowed_vcs_client_versions', []); + pyroutes.register('admin_security_update', '/_admin/security/audit/update', []); pyroutes.register('admin_settings', '/_admin/settings', []); pyroutes.register('admin_settings_email', '/_admin/settings/email', []); pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []); @@ -62,7 +65,8 @@ function registerRCRoutes() { pyroutes.register('admin_settings_license', '/_admin/settings/license', []); pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []); pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []); - pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []); + pyroutes.register('admin_settings_mapping_cleanup', '/_admin/settings/mapping/cleanup', []); + pyroutes.register('admin_settings_mapping_create', '/_admin/settings/mapping/create', []); pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []); pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []); pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []); @@ -86,7 +90,6 @@ function registerRCRoutes() { pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []); pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []); pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []); - pyroutes.register('admin_security_modify_allowed_vcs_client_versions', '/_admin/security/modify/allowed_vcs_client_versions', []); pyroutes.register('apiv2', '/_admin/api', []); pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']); pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']); diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -57,6 +57,36 @@ def add_renderer_globals(event): event['h'] = helpers +def auto_merge_pr_if_needed(event): + #TODO To be re-enabled later + pass + # from rhodecode.model.db import PullRequest + # from rhodecode.model.pull_request import ( + # PullRequestModel, ChangesetStatus, MergeCheck + # ) + # + # pr_event_data = event.as_dict()['pullrequest'] + # pull_request = PullRequest.get(pr_event_data['pull_request_id']) + # calculated_status = pr_event_data['status'] + # if (calculated_status == ChangesetStatus.STATUS_APPROVED + # and PullRequestModel().is_automatic_merge_enabled(pull_request)): + # user = pull_request.author.AuthUser() + # + # merge_check = MergeCheck.validate( + # pull_request, user, translator=lambda x: x, fail_early=True + # ) + # if merge_check.merge_possible: + # from rhodecode.lib.base import vcs_operation_context + # extras = vcs_operation_context( + # event.request.environ, repo_name=pull_request.target_repo.repo_name, + # username=user.username, action='push', + # scm=pull_request.target_repo.repo_type) + # from rc_ee.lib.celerylib.tasks import auto_merge_repo + # auto_merge_repo.apply_async( + # args=(pull_request.pull_request_id, extras), countdown=3 + # ) + + def set_user_lang(event): request = event.request cur_user = getattr(request, 'user', None) @@ -121,7 +151,7 @@ def scan_repositories_if_enabled(event): from rhodecode.lib.utils import repo2db_mapper scm = ScmModel() repositories = scm.repo_scan(scm.repos_path) - repo2db_mapper(repositories, remove_obsolete=False) + repo2db_mapper(repositories) def write_metadata_if_needed(event): diff --git a/rhodecode/templates/admin/auth/plugin_settings.mako b/rhodecode/templates/admin/auth/plugin_settings.mako --- a/rhodecode/templates/admin/auth/plugin_settings.mako +++ b/rhodecode/templates/admin/auth/plugin_settings.mako @@ -143,4 +143,16 @@ }); + diff --git a/rhodecode/templates/admin/repos/repo_edit_caches.mako b/rhodecode/templates/admin/repos/repo_edit_caches.mako --- a/rhodecode/templates/admin/repos/repo_edit_caches.mako +++ b/rhodecode/templates/admin/repos/repo_edit_caches.mako @@ -10,7 +10,7 @@ ${_('Cache purge can be automated by such api call. Can be called periodically in crontab etc.')}
    - ${h.api_call_example(method='invalidate_cache', args={"repoid": c.rhodecode_db_repo.repo_name})} + ${h.api_call_example(method='invalidate_cache', args={"repoid": c.rhodecode_db_repo.repo_id})}

    diff --git a/rhodecode/templates/admin/settings/settings_global.mako b/rhodecode/templates/admin/settings/settings_global.mako --- a/rhodecode/templates/admin/settings/settings_global.mako +++ b/rhodecode/templates/admin/settings/settings_global.mako @@ -68,7 +68,7 @@
    - ${h.text('rhodecode_captcha_public_key',size=60)} + ${h.text('rhodecode_captcha_public_key',size=60, type="password")}
    @@ -80,7 +80,7 @@
    - ${h.text('rhodecode_captcha_private_key',size=60)} + ${h.text('rhodecode_captcha_private_key',size=60, type="password")}
    diff --git a/rhodecode/templates/admin/settings/settings_mapping.mako b/rhodecode/templates/admin/settings/settings_mapping.mako --- a/rhodecode/templates/admin/settings/settings_mapping.mako +++ b/rhodecode/templates/admin/settings/settings_mapping.mako @@ -1,33 +1,45 @@ -${h.secure_form(h.route_path('admin_settings_mapping_update'), request=request)} +
    -

    ${_('Import New Groups or Repositories')}

    +

    ${_('Import new repository groups and repositories')}

    - + ${h.secure_form(h.route_path('admin_settings_mapping_create'), request=request)}

    - ${_('This function will scann all data under the current storage path location at')} ${c.storage_path} + ${_('This function will scan all data under the current storage path location at')} ${c.storage_path}
    + ${_('Each folder will be imported as a new repository group, and each repository found will be also imported to root level or corresponding repository group')}

    - ${h.checkbox('destroy',True)} - -
    - ${_('In case a repository or a group was deleted from the filesystem and it still exists in the database, check this option to remove obsolete data from the database.')} - -
    ${h.checkbox('invalidate',True)}
    ${_('Each cache data for repositories will be cleaned with this option selected. Use this to reload data and clear cache keys.')}
    - ${h.submit('rescan',_('Rescan Filesystem'),class_="btn")} + ${h.submit('rescan',_('Scan filesystem'),class_="btn")}
    - + ${h.end_form()}
    -${h.end_form()} +
    +
    +

    ${_('Cleanup removed Repository Groups or Repositories')}

    +
    +
    + ${h.secure_form(h.route_path('admin_settings_mapping_cleanup'), request=request)} +

    + ${_('This function will scan all data under the current storage path location at')} ${c.storage_path} + ${_('Then it will remove all repository groups and repositories that are no longer present in the filesystem.')} +

    + +
    + ${h.submit('rescan',_('Cleanup filesystem'),class_="btn btn-danger")} +
    + ${h.end_form()} +
    +
    + diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -210,6 +210,23 @@
    ${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}
    + %if c.rhodecode_edition_id != 'EE': +
    + + +
    +
    + ${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='sales@rhodecode.com')|n} +
    + %else: +
    + ${h.checkbox('rhodecode_auto_merge_enabled' + suffix, 'True', **kwargs)} + +
    +
    + ${_('When this is enabled, the pull request will be merged once it has at least one reviewer and is approved.')} +
    + %endif
    ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)} diff --git a/rhodecode/templates/changeset/changeset.mako b/rhodecode/templates/changeset/changeset.mako --- a/rhodecode/templates/changeset/changeset.mako +++ b/rhodecode/templates/changeset/changeset.mako @@ -128,7 +128,7 @@
    -
    +
    ${_('Show More')}
    @@ -416,6 +416,51 @@ var channel = '${c.commit_broadcast_channel}'; new ReviewerPresenceController(channel) + function breakLongCommitMessage() { + const commitElements = document.querySelectorAll(".left-content-message .commit"); + const maxAllowedWidth = window.innerWidth * 0.9; + + commitElements.forEach(commitElement => { + const originalText = commitElement.textContent; + const lines = originalText.split("\n"); + const brokenLines = []; + + commitElement.style.whiteSpace = "nowrap"; + + for (let line of lines) { + let brokenLine = ""; + let words = line.split(" "); + let currentLine = ""; + + words.forEach(word => { + const testLine = currentLine.length > 0 ? currentLine + " " + word : word; + + commitElement.textContent = testLine; + const testLineWidth = commitElement.offsetWidth; + + if (testLineWidth > maxAllowedWidth) { + brokenLine += currentLine + "\n"; + currentLine = word; + } else { + currentLine = testLine; + } + }); + + brokenLine += currentLine; + brokenLines.push(brokenLine.trim()); + } + + commitElement.textContent = brokenLines.join("\n"); + + commitElement.style.whiteSpace = "pre-wrap"; + }); + } + + window.addEventListener("load", function () { + const button = document.getElementById("break-button"); + button.addEventListener("click", breakLongCommitMessage); + + }); }) diff --git a/rhodecode/templates/email_templates/base.mako b/rhodecode/templates/email_templates/base.mako --- a/rhodecode/templates/email_templates/base.mako +++ b/rhodecode/templates/email_templates/base.mako @@ -409,8 +409,17 @@ text_monospace = "'Menlo', 'Liberation M -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; + color: #7E7F7F + } + + div.markdown-block code, + div.markdown-block pre { + background-color: transparent; + } + + div.markdown-block #ws, + div.markdown-block #message { background-color: #FFFFFF; - color: #7E7F7F } div.markdown-block code { diff --git a/rhodecode/templates/files/files_browser_tree.mako b/rhodecode/templates/files/files_browser_tree.mako --- a/rhodecode/templates/files/files_browser_tree.mako +++ b/rhodecode/templates/files/files_browser_tree.mako @@ -38,7 +38,7 @@ http://docker-dev:10020/ipython/files/ma <% has_files = False %> % if not c.file.is_submodule(): - % for cnt, node in enumerate(c.file): + % for cnt, node in enumerate(c.file_nodes): <% has_files = True %> @@ -105,6 +105,8 @@ http://docker-dev:10020/ipython/files/ma ${h.escape(c.file.name)}
    ${c.file.url}
    + % else: +
    ${_('Empty directory')}
    %endif diff --git a/rhodecode/templates/pullrequests/pullrequest_show.mako b/rhodecode/templates/pullrequests/pullrequest_show.mako --- a/rhodecode/templates/pullrequests/pullrequest_show.mako +++ b/rhodecode/templates/pullrequests/pullrequest_show.mako @@ -1054,6 +1054,16 @@ window.setObserversData = ${c.pull_reque new ReviewerPresenceController(channel) // register globally so inject comment logic can re-use it. window.commentsController = commentsController; + + // hiding status change dropdown for observers + const isHidden = ${'true' if c.status_change_disabled else 'false'}; + + if (isHidden) { + const element = document.getElementById("s2id_change_status_general"); + if (element) { + element.style.display = "none"; + } + } }) diff --git a/rhodecode/tests/__init__.py b/rhodecode/tests/__init__.py --- a/rhodecode/tests/__init__.py +++ b/rhodecode/tests/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -34,49 +33,20 @@ from rhodecode.lib import helpers as h from rhodecode.lib.helpers import flash from rhodecode.lib.str_utils import safe_str from rhodecode.lib.hash_utils import sha1_safe +from rhodecode.bootstrap import \ + TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \ + TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, TEST_USER_REGULAR_EMAIL, \ + TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL,\ + HG_REPO, GIT_REPO, SVN_REPO,\ + NEW_HG_REPO, NEW_GIT_REPO,\ + HG_FORK, GIT_FORK log = logging.getLogger(__name__) -__all__ = [ - 'get_new_dir', 'TestController', - 'clear_cache_regions', - 'assert_session_flash', 'login_user', 'no_newline_id_generator', - 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO', - 'NEW_HG_REPO', 'NEW_GIT_REPO', - 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS', - 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS', - 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN', - 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', - 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', - 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'SCM_TESTS', -] - # SOME GLOBALS FOR TESTS TEST_DIR = tempfile.gettempdir() -TEST_USER_ADMIN_LOGIN = 'test_admin' -TEST_USER_ADMIN_PASS = 'test12' -TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com' - -TEST_USER_REGULAR_LOGIN = 'test_regular' -TEST_USER_REGULAR_PASS = 'test12' -TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com' - -TEST_USER_REGULAR2_LOGIN = 'test_regular2' -TEST_USER_REGULAR2_PASS = 'test12' -TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com' - -HG_REPO = 'vcs_test_hg' -GIT_REPO = 'vcs_test_git' -SVN_REPO = 'vcs_test_svn' - -NEW_HG_REPO = 'vcs_test_hg_new' -NEW_GIT_REPO = 'vcs_test_git_new' - -HG_FORK = 'vcs_test_hg_fork' -GIT_FORK = 'vcs_test_git_fork' - ## VCS SCM_TESTS = ['hg', 'git'] uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple()))) @@ -244,3 +214,11 @@ def no_newline_id_generator(test_name): return test_name or 'test-with-empty-name' +def console_printer(*msg): + print_func = print + try: + from rich import print as print_func + except ImportError: + pass + + print_func(*msg) diff --git a/rhodecode/tests/auth_external_test.py b/rhodecode/tests/auth_external_test.py --- a/rhodecode/tests/auth_external_test.py +++ b/rhodecode/tests/auth_external_test.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -90,7 +89,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExter 'firstname': firstname, 'lastname': lastname, 'groups': [], - 'email': '%s@rhodecode.com' % username, + 'email': f'{username}@rhodecode.com', 'admin': admin, 'active': active, "active_from_extern": None, diff --git a/rhodecode/tests/config/__init__.py b/rhodecode/tests/config/__init__.py --- a/rhodecode/tests/config/__init__.py +++ b/rhodecode/tests/config/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/config/test_environment.py b/rhodecode/tests/config/test_environment.py --- a/rhodecode/tests/config/test_environment.py +++ b/rhodecode/tests/config/test_environment.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/config/test_routing_links.py b/rhodecode/tests/config/test_routing_links.py --- a/rhodecode/tests/config/test_routing_links.py +++ b/rhodecode/tests/config/test_routing_links.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,14 +19,14 @@ import pytest import requests from rhodecode.config import routing_links - +from rhodecode.tests import console_printer def check_connection(): try: response = requests.get('https://rhodecode.com') return response.status_code == 200 except Exception as e: - print(e) + console_printer(e) return False @@ -37,8 +36,8 @@ connection_available = pytest.mark.skipi import requests +from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry def requests_retry_session( diff --git a/rhodecode/tests/config/test_sanitize_settings.py b/rhodecode/tests/config/test_sanitize_settings.py --- a/rhodecode/tests/config/test_sanitize_settings.py +++ b/rhodecode/tests/config/test_sanitize_settings.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/config/test_utils.py b/rhodecode/tests/config/test_utils.py --- a/rhodecode/tests/config/test_utils.py +++ b/rhodecode/tests/config/test_utils.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2012-2023 RhodeCode GmbH +# Copyright (C) 2012-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/conftest.py b/rhodecode/tests/conftest.py --- a/rhodecode/tests/conftest.py +++ b/rhodecode/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -16,23 +16,10 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -""" -py.test config for test suite for making push/pull operations. - -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. -""" - -import pytest +import pytest # noqa import logging - -from rhodecode.authentication import AuthenticationPluginRegistry -from rhodecode.model.db import Permission, User -from rhodecode.model.meta import Session -from rhodecode.model.settings import SettingsModel -from rhodecode.model.user import UserModel +import collections +import rhodecode log = logging.getLogger(__name__) @@ -40,99 +27,3 @@ log = logging.getLogger(__name__) # Docker image running httpbin... HTTPBIN_DOMAIN = 'http://httpbin' HTTPBIN_POST = HTTPBIN_DOMAIN + '/post' - - -@pytest.fixture() -def enable_auth_plugins(request, baseapp, csrf_token): - """ - Return a factory object that when called, allows to control which - authentication plugins are enabled. - """ - - class AuthPluginManager(object): - - def cleanup(self): - self._enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode']) - - def enable(self, plugins_list, override=None): - return self._enable_plugins(plugins_list, override) - - def _enable_plugins(self, plugins_list, override=None): - override = override or {} - params = { - 'auth_plugins': ','.join(plugins_list), - } - - # helper translate some names to others, to fix settings code - name_map = { - 'token': 'authtoken' - } - log.debug('enable_auth_plugins: enabling following auth-plugins: %s', plugins_list) - - for module in plugins_list: - plugin_name = module.partition('#')[-1] - if plugin_name in name_map: - plugin_name = name_map[plugin_name] - enabled_plugin = f'auth_{plugin_name}_enabled' - cache_ttl = f'auth_{plugin_name}_cache_ttl' - - # default params that are needed for each plugin, - # `enabled` and `cache_ttl` - params.update({ - enabled_plugin: True, - cache_ttl: 0 - }) - if override.get: - params.update(override.get(module, {})) - - validated_params = params - - for k, v in validated_params.items(): - setting = SettingsModel().create_or_update_setting(k, v) - Session().add(setting) - Session().commit() - - AuthenticationPluginRegistry.invalidate_auth_plugins_cache(hard=True) - - enabled_plugins = SettingsModel().get_auth_plugins() - assert plugins_list == enabled_plugins - - enabler = AuthPluginManager() - request.addfinalizer(enabler.cleanup) - - return enabler - - -@pytest.fixture() -def test_user_factory(request, baseapp): - - def user_factory(username='test_user', password='qweqwe', first_name='John', last_name='Testing', **kwargs): - usr = UserModel().create_or_update( - username=username, - password=password, - email=f'{username}@rhodecode.org', - firstname=first_name, lastname=last_name) - Session().commit() - - for k, v in kwargs.items(): - setattr(usr, k, v) - Session().add(usr) - - new_usr = User.get_by_username(username) - new_usr_id = new_usr.user_id - assert new_usr == usr - - @request.addfinalizer - def cleanup(): - if User.get(new_usr_id) is None: - return - - perm = Permission.query().all() - for p in perm: - UserModel().revoke_perm(usr, p) - - UserModel().delete(new_usr_id) - Session().commit() - return usr - - return user_factory diff --git a/rhodecode/tests/conftest_common.py b/rhodecode/tests/conftest_common.py --- a/rhodecode/tests/conftest_common.py +++ b/rhodecode/tests/conftest_common.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -98,16 +98,16 @@ def pytest_addoption(parser): 'pyramid_config', "Set up a Pyramid environment with the specified config file.") + parser.addini('rhodecode_config', 'rhodecode config ini for tests') + parser.addini('celery_config', 'celery config ini for tests') + parser.addini('vcsserver_config', 'vcsserver config ini for tests') + vcsgroup = parser.getgroup('vcs') + vcsgroup.addoption( '--without-vcsserver', dest='with_vcsserver', action='store_false', help="Do not start the VCSServer in a background process.") - vcsgroup.addoption( - '--with-vcsserver-http', dest='vcsserver_config_http', - help="Start the HTTP VCSServer with the specified config file.") - vcsgroup.addoption( - '--vcsserver-protocol', dest='vcsserver_protocol', - help="Start the VCSServer with HTTP protocol support.") + vcsgroup.addoption( '--vcsserver-config-override', action='store', type=_parse_json, default=None, dest='vcsserver_config_override', help=( @@ -122,12 +122,6 @@ def pytest_addoption(parser): "Allows to set the port of the vcsserver. Useful when testing " "against an already running server and random ports cause " "trouble.")) - parser.addini( - 'vcsserver_config_http', - "Start the HTTP VCSServer with the specified config file.") - parser.addini( - 'vcsserver_protocol', - "Start the VCSServer with HTTP protocol support.") @pytest.hookimpl(tryfirst=True, hookwrapper=True) diff --git a/rhodecode/tests/database/__init__.py b/rhodecode/tests/database/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/database/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/tests/database/conftest.py b/rhodecode/tests/database/conftest.py --- a/rhodecode/tests/database/conftest.py +++ b/rhodecode/tests/database/conftest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -17,7 +16,7 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -from subprocess import Popen, PIPE +import subprocess import os import sys import tempfile @@ -26,87 +25,71 @@ import pytest from sqlalchemy.engine import url from rhodecode.lib.str_utils import safe_str, safe_bytes -from rhodecode.tests.fixture import TestINI +from rhodecode.tests.fixtures.rc_fixture import TestINI def _get_dbs_from_metafunc(metafunc): - dbs_mark = metafunc.definition.get_closest_marker('dbs') + dbs_mark = metafunc.definition.get_closest_marker("dbs") if dbs_mark: # Supported backends by this test function, created from pytest.mark.dbs backends = dbs_mark.args else: - backends = metafunc.config.getoption('--dbs') + backends = metafunc.config.getoption("--dbs") return backends def pytest_generate_tests(metafunc): # Support test generation based on --dbs parameter - if 'db_backend' in metafunc.fixturenames: - requested_backends = set(metafunc.config.getoption('--dbs')) + if "db_backend" in metafunc.fixturenames: + requested_backends = set(metafunc.config.getoption("--dbs")) backends = _get_dbs_from_metafunc(metafunc) backends = requested_backends.intersection(backends) # TODO: johbo: Disabling a backend did not work out with # parametrization, find better way to achieve this. if not backends: metafunc.function._skip = True - metafunc.parametrize('db_backend_name', backends) + metafunc.parametrize("db_backend_name", backends) def pytest_collection_modifyitems(session, config, items): - remaining = [ - i for i in items if not getattr(i.obj, '_skip', False)] + remaining = [i for i in items if not getattr(i.obj, "_skip", False)] items[:] = remaining @pytest.fixture() -def db_backend( - request, db_backend_name, ini_config, tmpdir_factory): +def db_backend(request, db_backend_name, ini_config, tmpdir_factory): basetemp = tmpdir_factory.getbasetemp().strpath klass = _get_backend(db_backend_name) - option_name = '--{}-connection-string'.format(db_backend_name) + option_name = "--{}-connection-string".format(db_backend_name) connection_string = request.config.getoption(option_name) or None - return klass( - config_file=ini_config, basetemp=basetemp, - connection_string=connection_string) + return klass(config_file=ini_config, basetemp=basetemp, connection_string=connection_string) def _get_backend(backend_type): - return { - 'sqlite': SQLiteDBBackend, - 'postgres': PostgresDBBackend, - 'mysql': MySQLDBBackend, - '': EmptyDBBackend - }[backend_type] + return {"sqlite": SQLiteDBBackend, "postgres": PostgresDBBackend, "mysql": MySQLDBBackend, "": EmptyDBBackend}[ + backend_type + ] class DBBackend(object): _store = os.path.dirname(os.path.abspath(__file__)) _type = None - _base_ini_config = [{'app:main': {'vcs.start_server': 'false', - 'startup.import_repos': 'false'}}] - _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}] - _base_db_name = 'rhodecode_test_db_backend' - std_env = {'RC_TEST': '0'} + _base_ini_config = [{"app:main": {"vcs.start_server": "false", "startup.import_repos": "false"}}] + _db_url = [{"app:main": {"sqlalchemy.db1.url": ""}}] + _base_db_name = "rhodecode_test_db_backend" + std_env = {"RC_TEST": "0"} - def __init__( - self, config_file, db_name=None, basetemp=None, - connection_string=None): - - from rhodecode.lib.vcs.backends.hg import largefiles_store - from rhodecode.lib.vcs.backends.git import lfs_store - + def __init__(self, config_file, db_name=None, basetemp=None, connection_string=None): self.fixture_store = os.path.join(self._store, self._type) self.db_name = db_name or self._base_db_name self._base_ini_file = config_file - self.stderr = '' - self.stdout = '' + self.stderr = "" + self.stdout = "" self._basetemp = basetemp or tempfile.gettempdir() - self._repos_location = os.path.join(self._basetemp, 'rc_test_repos') - self._repos_hg_largefiles_store = largefiles_store(self._basetemp) - self._repos_git_lfs_store = lfs_store(self._basetemp) + self._repos_location = os.path.join(self._basetemp, "rc_test_repos") self.connection_string = connection_string @property @@ -118,8 +101,7 @@ class DBBackend(object): if not new_connection_string: new_connection_string = self.get_default_connection_string() else: - new_connection_string = new_connection_string.format( - db_name=self.db_name) + new_connection_string = new_connection_string.format(db_name=self.db_name) url_parts = url.make_url(new_connection_string) self._connection_string = new_connection_string self.user = url_parts.username @@ -127,73 +109,67 @@ class DBBackend(object): self.host = url_parts.host def get_default_connection_string(self): - raise NotImplementedError('default connection_string is required.') + raise NotImplementedError("default connection_string is required.") def execute(self, cmd, env=None, *args): """ Runs command on the system with given ``args``. """ - command = cmd + ' ' + ' '.join(args) - sys.stdout.write(f'CMD: {command}') + command = cmd + " " + " ".join(args) + sys.stdout.write(f"CMD: {command}") # Tell Python to use UTF-8 encoding out stdout _env = os.environ.copy() - _env['PYTHONIOENCODING'] = 'UTF-8' + _env["PYTHONIOENCODING"] = "UTF-8" _env.update(self.std_env) if env: _env.update(env) - self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env) + self.p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=_env) self.stdout, self.stderr = self.p.communicate() stdout_str = safe_str(self.stdout) - sys.stdout.write(f'COMMAND:{command}\n') + sys.stdout.write(f"COMMAND:{command}\n") sys.stdout.write(stdout_str) return self.stdout, self.stderr def assert_returncode_success(self): from rich import print as pprint + if not self.p.returncode == 0: pprint(safe_str(self.stderr)) - raise AssertionError(f'non 0 retcode:{self.p.returncode}') + raise AssertionError(f"non 0 retcode:{self.p.returncode}") def assert_correct_output(self, stdout, version): - assert b'UPGRADE FOR STEP %b COMPLETED' % safe_bytes(version) in stdout + assert b"UPGRADE FOR STEP %b COMPLETED" % safe_bytes(version) in stdout def setup_rhodecode_db(self, ini_params=None, env=None): if not ini_params: ini_params = self._base_ini_config ini_params.extend(self._db_url) - with TestINI(self._base_ini_file, ini_params, - self._type, destroy=True) as _ini_file: - + with TestINI(self._base_ini_file, ini_params, self._type, destroy=True) as _ini_file: if not os.path.isdir(self._repos_location): os.makedirs(self._repos_location) - if not os.path.isdir(self._repos_hg_largefiles_store): - os.makedirs(self._repos_hg_largefiles_store) - if not os.path.isdir(self._repos_git_lfs_store): - os.makedirs(self._repos_git_lfs_store) return self.execute( "rc-setup-app {0} --user=marcink " "--email=marcin@rhodeocode.com --password={1} " - "--repos={2} --force-yes".format( - _ini_file, 'qweqwe', self._repos_location), env=env) + "--repos={2} --force-yes".format(_ini_file, "qweqwe", self._repos_location), + env=env, + ) def upgrade_database(self, ini_params=None): if not ini_params: ini_params = self._base_ini_config ini_params.extend(self._db_url) - test_ini = TestINI( - self._base_ini_file, ini_params, self._type, destroy=True) + test_ini = TestINI(self._base_ini_file, ini_params, self._type, destroy=True) with test_ini as ini_file: if not os.path.isdir(self._repos_location): os.makedirs(self._repos_location) - return self.execute( - "rc-upgrade-db {0} --force-yes".format(ini_file)) + return self.execute("rc-upgrade-db {0} --force-yes".format(ini_file)) def setup_db(self): raise NotImplementedError @@ -206,7 +182,7 @@ class DBBackend(object): class EmptyDBBackend(DBBackend): - _type = '' + _type = "" def setup_db(self): pass @@ -222,21 +198,20 @@ class EmptyDBBackend(DBBackend): class SQLiteDBBackend(DBBackend): - _type = 'sqlite' + _type = "sqlite" def get_default_connection_string(self): - return 'sqlite:///{}/{}.sqlite'.format(self._basetemp, self.db_name) + return "sqlite:///{}/{}.sqlite".format(self._basetemp, self.db_name) def setup_db(self): # dump schema for tests # cp -v $TEST_DB_NAME - self._db_url = [{'app:main': { - 'sqlalchemy.db1.url': self.connection_string}}] + self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}] def import_dump(self, dumpname): dump = os.path.join(self.fixture_store, dumpname) - target = os.path.join(self._basetemp, '{0.db_name}.sqlite'.format(self)) - return self.execute(f'cp -v {dump} {target}') + target = os.path.join(self._basetemp, "{0.db_name}.sqlite".format(self)) + return self.execute(f"cp -v {dump} {target}") def teardown_db(self): target_db = os.path.join(self._basetemp, self.db_name) @@ -244,39 +219,39 @@ class SQLiteDBBackend(DBBackend): class MySQLDBBackend(DBBackend): - _type = 'mysql' + _type = "mysql" def get_default_connection_string(self): - return 'mysql://root:qweqwe@127.0.0.1/{}'.format(self.db_name) + return "mysql://root:qweqwe@127.0.0.1/{}".format(self.db_name) def setup_db(self): # dump schema for tests # mysqldump -uroot -pqweqwe $TEST_DB_NAME - self._db_url = [{'app:main': { - 'sqlalchemy.db1.url': self.connection_string}}] - return self.execute("mysql -v -u{} -p{} -e 'create database '{}';'".format( - self.user, self.password, self.db_name)) + self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}] + return self.execute( + "mysql -v -u{} -p{} -e 'create database '{}';'".format(self.user, self.password, self.db_name) + ) def import_dump(self, dumpname): dump = os.path.join(self.fixture_store, dumpname) - return self.execute("mysql -u{} -p{} {} < {}".format( - self.user, self.password, self.db_name, dump)) + return self.execute("mysql -u{} -p{} {} < {}".format(self.user, self.password, self.db_name, dump)) def teardown_db(self): - return self.execute("mysql -v -u{} -p{} -e 'drop database '{}';'".format( - self.user, self.password, self.db_name)) + return self.execute( + "mysql -v -u{} -p{} -e 'drop database '{}';'".format(self.user, self.password, self.db_name) + ) class PostgresDBBackend(DBBackend): - _type = 'postgres' + _type = "postgres" def get_default_connection_string(self): - return 'postgresql://postgres:qweqwe@localhost/{}'.format(self.db_name) + return "postgresql://postgres:qweqwe@localhost/{}".format(self.db_name) def setup_db(self): # dump schema for tests # pg_dump -U postgres -h localhost $TEST_DB_NAME - self._db_url = [{'app:main': {'sqlalchemy.db1.url': self.connection_string}}] + self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}] cmd = f"PGPASSWORD={self.password} psql -U {self.user} -h localhost -c 'create database '{self.db_name}';'" return self.execute(cmd) diff --git a/rhodecode/tests/database/test_creation.py b/rhodecode/tests/database/test_creation.py --- a/rhodecode/tests/database/test_creation.py +++ b/rhodecode/tests/database/test_creation.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/database/test_migration.py b/rhodecode/tests/database/test_migration.py --- a/rhodecode/tests/database/test_migration.py +++ b/rhodecode/tests/database/test_migration.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,33 +20,42 @@ import pytest @pytest.mark.dbs("postgres") -@pytest.mark.parametrize("dumpname", [ - '1.4.4.sql', - '1.5.0.sql', - '1.6.0.sql', - '1.6.0_no_repo_name_index.sql', -]) +@pytest.mark.parametrize( + "dumpname", + [ + "1.4.4.sql", + "1.5.0.sql", + "1.6.0.sql", + "1.6.0_no_repo_name_index.sql", + ], +) def test_migrate_postgres_db(db_backend, dumpname): _run_migration_test(db_backend, dumpname) @pytest.mark.dbs("sqlite") -@pytest.mark.parametrize("dumpname", [ - 'rhodecode.1.4.4.sqlite', - 'rhodecode.1.4.4_with_groups.sqlite', - 'rhodecode.1.4.4_with_ldap_active.sqlite', -]) +@pytest.mark.parametrize( + "dumpname", + [ + "rhodecode.1.4.4.sqlite", + "rhodecode.1.4.4_with_groups.sqlite", + "rhodecode.1.4.4_with_ldap_active.sqlite", + ], +) def test_migrate_sqlite_db(db_backend, dumpname): _run_migration_test(db_backend, dumpname) @pytest.mark.dbs("mysql") -@pytest.mark.parametrize("dumpname", [ - '1.4.4.sql', - '1.5.0.sql', - '1.6.0.sql', - '1.6.0_no_repo_name_index.sql', -]) +@pytest.mark.parametrize( + "dumpname", + [ + "1.4.4.sql", + "1.5.0.sql", + "1.6.0.sql", + "1.6.0_no_repo_name_index.sql", + ], +) def test_migrate_mysql_db(db_backend, dumpname): _run_migration_test(db_backend, dumpname) @@ -60,5 +68,5 @@ def _run_migration_test(db_backend, dump db_backend.import_dump(dumpname) stdout, stderr = db_backend.upgrade_database() - db_backend.assert_correct_output(stdout+stderr, version='16') + db_backend.assert_correct_output(stdout + stderr, version="16") db_backend.assert_returncode_success() diff --git a/rhodecode/tests/events/__init__.py b/rhodecode/tests/events/__init__.py --- a/rhodecode/tests/events/__init__.py +++ b/rhodecode/tests/events/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/events/conftest.py b/rhodecode/tests/events/conftest.py --- a/rhodecode/tests/events/conftest.py +++ b/rhodecode/tests/events/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/events/test_pullrequest.py b/rhodecode/tests/events/test_pullrequest.py --- a/rhodecode/tests/events/test_pullrequest.py +++ b/rhodecode/tests/events/test_pullrequest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/events/test_repo.py b/rhodecode/tests/events/test_repo.py --- a/rhodecode/tests/events/test_repo.py +++ b/rhodecode/tests/events/test_repo.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/fixture_mods/__init__.py b/rhodecode/tests/fixtures/__init__.py rename from rhodecode/tests/fixture_mods/__init__.py rename to rhodecode/tests/fixtures/__init__.py diff --git a/rhodecode/tests/fixtures/diff_with_diff_data.diff b/rhodecode/tests/fixtures/diff_fixtures/diff_with_diff_data.diff rename from rhodecode/tests/fixtures/diff_with_diff_data.diff rename to rhodecode/tests/fixtures/diff_fixtures/diff_with_diff_data.diff diff --git a/rhodecode/tests/fixtures/git_diff_binary_and_normal.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_and_normal.diff rename from rhodecode/tests/fixtures/git_diff_binary_and_normal.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_and_normal.diff diff --git a/rhodecode/tests/fixtures/git_diff_binary_special_files.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_special_files.diff rename from rhodecode/tests/fixtures/git_diff_binary_special_files.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_special_files.diff diff --git a/rhodecode/tests/fixtures/git_diff_binary_special_files_2.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_special_files_2.diff rename from rhodecode/tests/fixtures/git_diff_binary_special_files_2.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_binary_special_files_2.diff diff --git a/rhodecode/tests/fixtures/git_diff_chmod.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_chmod.diff rename from rhodecode/tests/fixtures/git_diff_chmod.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_chmod.diff diff --git a/rhodecode/tests/fixtures/git_diff_js_chars.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_js_chars.diff rename from rhodecode/tests/fixtures/git_diff_js_chars.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_js_chars.diff diff --git a/rhodecode/tests/fixtures/git_diff_mod_single_binary_file.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_mod_single_binary_file.diff rename from rhodecode/tests/fixtures/git_diff_mod_single_binary_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_mod_single_binary_file.diff diff --git a/rhodecode/tests/fixtures/git_diff_rename_file.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_rename_file.diff rename from rhodecode/tests/fixtures/git_diff_rename_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_rename_file.diff diff --git a/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff b/rhodecode/tests/fixtures/diff_fixtures/git_diff_rename_file_with_spaces.diff rename from rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff rename to rhodecode/tests/fixtures/diff_fixtures/git_diff_rename_file_with_spaces.diff diff --git a/rhodecode/tests/fixtures/git_node_history_response.json b/rhodecode/tests/fixtures/diff_fixtures/git_node_history_response.json rename from rhodecode/tests/fixtures/git_node_history_response.json rename to rhodecode/tests/fixtures/diff_fixtures/git_node_history_response.json diff --git a/rhodecode/tests/fixtures/hg_diff_add_single_binary_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_add_single_binary_file.diff rename from rhodecode/tests/fixtures/hg_diff_add_single_binary_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_add_single_binary_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_binary_and_normal.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_binary_and_normal.diff rename from rhodecode/tests/fixtures/hg_diff_binary_and_normal.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_binary_and_normal.diff diff --git a/rhodecode/tests/fixtures/hg_diff_chmod.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_chmod.diff rename from rhodecode/tests/fixtures/hg_diff_chmod.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_chmod.diff diff --git a/rhodecode/tests/fixtures/hg_diff_chmod_and_mod_single_binary_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_chmod_and_mod_single_binary_file.diff rename from rhodecode/tests/fixtures/hg_diff_chmod_and_mod_single_binary_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_chmod_and_mod_single_binary_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_copy_and_chmod_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_and_chmod_file.diff rename from rhodecode/tests/fixtures/hg_diff_copy_and_chmod_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_and_chmod_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_copy_and_modify_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_and_modify_file.diff rename from rhodecode/tests/fixtures/hg_diff_copy_and_modify_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_and_modify_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_copy_chmod_and_edit_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_chmod_and_edit_file.diff rename from rhodecode/tests/fixtures/hg_diff_copy_chmod_and_edit_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_chmod_and_edit_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_copy_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_file.diff rename from rhodecode/tests/fixtures/hg_diff_copy_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_file_with_spaces.diff rename from rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_copy_file_with_spaces.diff diff --git a/rhodecode/tests/fixtures/hg_diff_del_single_binary_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_del_single_binary_file.diff rename from rhodecode/tests/fixtures/hg_diff_del_single_binary_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_del_single_binary_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_double_file_change_double_newline.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_double_file_change_double_newline.diff rename from rhodecode/tests/fixtures/hg_diff_double_file_change_double_newline.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_double_file_change_double_newline.diff diff --git a/rhodecode/tests/fixtures/hg_diff_double_file_change_newline.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_double_file_change_newline.diff rename from rhodecode/tests/fixtures/hg_diff_double_file_change_newline.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_double_file_change_newline.diff diff --git a/rhodecode/tests/fixtures/hg_diff_four_file_change_newline.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_four_file_change_newline.diff rename from rhodecode/tests/fixtures/hg_diff_four_file_change_newline.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_four_file_change_newline.diff diff --git a/rhodecode/tests/fixtures/hg_diff_mixed_filename_encodings.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_mixed_filename_encodings.diff rename from rhodecode/tests/fixtures/hg_diff_mixed_filename_encodings.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_mixed_filename_encodings.diff diff --git a/rhodecode/tests/fixtures/hg_diff_mod_file_and_rename.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_file_and_rename.diff rename from rhodecode/tests/fixtures/hg_diff_mod_file_and_rename.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_file_and_rename.diff diff --git a/rhodecode/tests/fixtures/git_diff_mod_single_binary_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_single_binary_file.diff copy from rhodecode/tests/fixtures/git_diff_mod_single_binary_file.diff copy to rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_single_binary_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_mod_single_file_and_rename_and_chmod.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_single_file_and_rename_and_chmod.diff rename from rhodecode/tests/fixtures/hg_diff_mod_single_file_and_rename_and_chmod.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_mod_single_file_and_rename_and_chmod.diff diff --git a/rhodecode/tests/fixtures/hg_diff_no_newline.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_no_newline.diff rename from rhodecode/tests/fixtures/hg_diff_no_newline.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_no_newline.diff diff --git a/rhodecode/tests/fixtures/hg_diff_rename_and_chmod_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_and_chmod_file.diff rename from rhodecode/tests/fixtures/hg_diff_rename_and_chmod_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_and_chmod_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_rename_file.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_file.diff rename from rhodecode/tests/fixtures/hg_diff_rename_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_file.diff diff --git a/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_file_with_spaces.diff rename from rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_rename_file_with_spaces.diff diff --git a/rhodecode/tests/fixtures/hg_diff_single_file_change_newline.diff b/rhodecode/tests/fixtures/diff_fixtures/hg_diff_single_file_change_newline.diff rename from rhodecode/tests/fixtures/hg_diff_single_file_change_newline.diff rename to rhodecode/tests/fixtures/diff_fixtures/hg_diff_single_file_change_newline.diff diff --git a/rhodecode/tests/fixtures/hg_node_history_response.json b/rhodecode/tests/fixtures/diff_fixtures/hg_node_history_response.json rename from rhodecode/tests/fixtures/hg_node_history_response.json rename to rhodecode/tests/fixtures/diff_fixtures/hg_node_history_response.json diff --git a/rhodecode/tests/fixtures/journal_dump.csv b/rhodecode/tests/fixtures/diff_fixtures/journal_dump.csv rename from rhodecode/tests/fixtures/journal_dump.csv rename to rhodecode/tests/fixtures/diff_fixtures/journal_dump.csv diff --git a/rhodecode/tests/fixtures/large_diff.diff b/rhodecode/tests/fixtures/diff_fixtures/large_diff.diff rename from rhodecode/tests/fixtures/large_diff.diff rename to rhodecode/tests/fixtures/diff_fixtures/large_diff.diff diff --git a/rhodecode/tests/fixtures/svn_diff_binary_add_file.diff b/rhodecode/tests/fixtures/diff_fixtures/svn_diff_binary_add_file.diff rename from rhodecode/tests/fixtures/svn_diff_binary_add_file.diff rename to rhodecode/tests/fixtures/diff_fixtures/svn_diff_binary_add_file.diff diff --git a/rhodecode/tests/fixtures/svn_diff_multiple_changes.diff b/rhodecode/tests/fixtures/diff_fixtures/svn_diff_multiple_changes.diff rename from rhodecode/tests/fixtures/svn_diff_multiple_changes.diff rename to rhodecode/tests/fixtures/diff_fixtures/svn_diff_multiple_changes.diff diff --git a/rhodecode/tests/fixtures/svn_node_history_branches.json b/rhodecode/tests/fixtures/diff_fixtures/svn_node_history_branches.json rename from rhodecode/tests/fixtures/svn_node_history_branches.json rename to rhodecode/tests/fixtures/diff_fixtures/svn_node_history_branches.json diff --git a/rhodecode/tests/fixtures/svn_node_history_response.json b/rhodecode/tests/fixtures/diff_fixtures/svn_node_history_response.json rename from rhodecode/tests/fixtures/svn_node_history_response.json rename to rhodecode/tests/fixtures/diff_fixtures/svn_node_history_response.json diff --git a/rhodecode/tests/fixture_mods/fixture_pyramid.py b/rhodecode/tests/fixtures/fixture_pyramid.py rename from rhodecode/tests/fixture_mods/fixture_pyramid.py rename to rhodecode/tests/fixtures/fixture_pyramid.py --- a/rhodecode/tests/fixture_mods/fixture_pyramid.py +++ b/rhodecode/tests/fixtures/fixture_pyramid.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,61 +19,128 @@ import pytest from rhodecode.lib.config_utils import get_app_config -from rhodecode.tests.fixture import TestINI +from rhodecode.tests.fixtures.rc_fixture import TestINI from rhodecode.tests import TESTS_TMP_PATH from rhodecode.tests.server_utils import RcVCSServer +from rhodecode.tests.server_utils import RcWebServer +from rhodecode.tests.server_utils import CeleryServer -@pytest.fixture(scope='session') -def vcsserver(request, vcsserver_port, vcsserver_factory): - """ - Session scope VCSServer. - - Tests which need the VCSServer have to rely on this fixture in order - to ensure it will be running. - - For specific needs, the fixture vcsserver_factory can be used. It allows to - adjust the configuration file for the test run. - - Command line args: - - --without-vcsserver: Allows to switch this fixture off. You have to - manually start the server. - - --vcsserver-port: Will expect the VCSServer to listen on this port. - """ - - if not request.config.getoption('with_vcsserver'): - return None - - return vcsserver_factory( - request, vcsserver_port=vcsserver_port) - - -@pytest.fixture(scope='session') -def vcsserver_factory(tmpdir_factory): +@pytest.fixture(scope="session") +def vcsserver_factory(): """ Use this if you need a running vcsserver with a special configuration. """ - def factory(request, overrides=(), vcsserver_port=None, - log_file=None, workers='3'): - - if vcsserver_port is None: + def factory(request, store_dir, overrides=(), config_file=None, port=None, log_file=None, workers="3", env=None, info_prefix=""): + env = env or {"RC_NO_TEST_ENV": "1"} + vcsserver_port = port + if port is None: vcsserver_port = get_available_port() overrides = list(overrides) - overrides.append({'server:main': {'port': vcsserver_port}}) + overrides.append({"server:main": {"port": vcsserver_port}}) + + if getattr(request, 'param', None): + config_overrides = [request.param] + overrides.extend(config_overrides) + + option_name = "vcsserver_config" + override_option_name = None + if not config_file: + config_file = get_config( + request.config, + option_name=option_name, + override_option_name=override_option_name, + overrides=overrides, + basetemp=store_dir, + prefix=f"{info_prefix}test_vcsserver_ini_", + ) + server = RcVCSServer(config_file, log_file, workers, env=env, info_prefix=info_prefix) + server.start() + + @request.addfinalizer + def cleanup(): + server.shutdown() + + server.wait_until_ready() + return server + + return factory + + +@pytest.fixture(scope="session") +def rhodecode_factory(): + def factory(request, store_dir, overrides=(), config_file=None, port=None, log_file=None, workers="3", env=None, info_prefix=""): + env = env or {"RC_NO_TEST_ENV": "1"} + rhodecode_port = port + if port is None: + rhodecode_port = get_available_port() + + overrides = list(overrides) + overrides.append({"server:main": {"port": rhodecode_port}}) + overrides.append({"app:main": {"use_celery": "true"}}) + overrides.append({"app:main": {"celery.task_always_eager": "false"}}) + + if getattr(request, 'param', None): + config_overrides = [request.param] + overrides.extend(config_overrides) + - option_name = 'vcsserver_config_http' - override_option_name = 'vcsserver_config_override' - config_file = get_config( - request.config, option_name=option_name, - override_option_name=override_option_name, overrides=overrides, - basetemp=tmpdir_factory.getbasetemp().strpath, - prefix='test_vcs_') + option_name = "rhodecode_config" + override_option_name = None + if not config_file: + config_file = get_config( + request.config, + option_name=option_name, + override_option_name=override_option_name, + overrides=overrides, + basetemp=store_dir, + prefix=f"{info_prefix}test_rhodecode_ini", + ) + + server = RcWebServer(config_file, log_file, workers, env, info_prefix=info_prefix) + server.start() + + @request.addfinalizer + def cleanup(): + server.shutdown() + + server.wait_until_ready() + return server + + return factory + - server = RcVCSServer(config_file, log_file, workers) +@pytest.fixture(scope="session") +def celery_factory(): + def factory(request, store_dir, overrides=(), config_file=None, port=None, log_file=None, workers="3", env=None, info_prefix=""): + env = env or {"RC_NO_TEST_ENV": "1"} + rhodecode_port = port + + overrides = list(overrides) + overrides.append({"app:main": {"use_celery": "true"}}) + overrides.append({"app:main": {"celery.task_always_eager": "false"}}) + config_overrides = None + + if getattr(request, 'param', None): + config_overrides = [request.param] + overrides.extend(config_overrides) + + option_name = "celery_config" + override_option_name = None + + if not config_file: + config_file = get_config( + request.config, + option_name=option_name, + override_option_name=override_option_name, + overrides=overrides, + basetemp=store_dir, + prefix=f"{info_prefix}test_celery_ini_", + ) + + server = CeleryServer(config_file, log_file, workers, env, info_prefix=info_prefix) server.start() @request.addfinalizer @@ -88,52 +154,68 @@ def vcsserver_factory(tmpdir_factory): def _use_log_level(config): - level = config.getoption('test_loglevel') or 'critical' + level = config.getoption("test_loglevel") or "critical" return level.upper() -@pytest.fixture(scope='session') -def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port): - option_name = 'pyramid_config' +def _ini_config_factory(request, base_dir, rcserver_port, vcsserver_port): + option_name = "pyramid_config" log_level = _use_log_level(request.config) overrides = [ - {'server:main': {'port': rcserver_port}}, - {'app:main': { - 'cache_dir': '%(here)s/rc-tests/rc_data', - 'vcs.server': f'localhost:{vcsserver_port}', - # johbo: We will always start the VCSServer on our own based on the - # fixtures of the test cases. For the test run it must always be - # off in the INI file. - 'vcs.start_server': 'false', - - 'vcs.server.protocol': 'http', - 'vcs.scm_app_implementation': 'http', - 'vcs.svn.proxy.enabled': 'true', - 'vcs.hooks.protocol.v2': 'celery', - 'vcs.hooks.host': '*', - 'repo_store.path': TESTS_TMP_PATH, - 'app.service_api.token': 'service_secret_token', - }}, - - {'handler_console': { - 'class': 'StreamHandler', - 'args': '(sys.stderr,)', - 'level': log_level, - }}, - + {"server:main": {"port": rcserver_port}}, + { + "app:main": { + #'cache_dir': '%(here)s/rc-tests/rc_data', + "vcs.server": f"localhost:{vcsserver_port}", + # johbo: We will always start the VCSServer on our own based on the + # fixtures of the test cases. For the test run it must always be + # off in the INI file. + "vcs.start_server": "false", + "vcs.server.protocol": "http", + "vcs.scm_app_implementation": "http", + "vcs.svn.proxy.enabled": "true", + "vcs.hooks.protocol.v2": "celery", + "vcs.hooks.host": "*", + "repo_store.path": TESTS_TMP_PATH, + "app.service_api.token": "service_secret_token", + } + }, + { + "handler_console": { + "class": "StreamHandler", + "args": "(sys.stderr,)", + "level": log_level, + } + }, ] filename = get_config( - request.config, option_name=option_name, - override_option_name='{}_override'.format(option_name), + request.config, + option_name=option_name, + override_option_name=f"{option_name}_override", overrides=overrides, - basetemp=tmpdir_factory.getbasetemp().strpath, - prefix='test_rce_') + basetemp=base_dir, + prefix="test_rce_", + ) return filename -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") +def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port): + base_dir = tmpdir_factory.getbasetemp().strpath + return _ini_config_factory(request, base_dir, rcserver_port, vcsserver_port) + + +@pytest.fixture(scope="session") +def ini_config_factory(request, tmpdir_factory, rcserver_port, vcsserver_port): + def _factory(ini_config_basedir, overrides=()): + return _ini_config_factory(request, ini_config_basedir, rcserver_port, vcsserver_port) + + return _factory + + +@pytest.fixture(scope="session") def ini_settings(ini_config): ini_path = ini_config return get_app_config(ini_path) @@ -141,26 +223,25 @@ def ini_settings(ini_config): def get_available_port(min_port=40000, max_port=55555): from rhodecode.lib.utils2 import get_available_port as _get_port + return _get_port(min_port, max_port) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def rcserver_port(request): port = get_available_port() - print(f'Using rhodecode port {port}') return port -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def vcsserver_port(request): - port = request.config.getoption('--vcsserver-port') + port = request.config.getoption("--vcsserver-port") if port is None: port = get_available_port() - print(f'Using vcsserver port {port}') return port -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def available_port_factory() -> get_available_port: """ Returns a callable which returns free port numbers. @@ -178,7 +259,7 @@ def available_port(available_port_factor return available_port_factory() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def testini_factory(tmpdir_factory, ini_config): """ Factory to create an INI file based on TestINI. @@ -190,37 +271,38 @@ def testini_factory(tmpdir_factory, ini_ class TestIniFactory(object): - - def __init__(self, basetemp, template_ini): - self._basetemp = basetemp + def __init__(self, ini_store_dir, template_ini): + self._ini_store_dir = ini_store_dir self._template_ini = template_ini - def __call__(self, ini_params, new_file_prefix='test'): + def __call__(self, ini_params, new_file_prefix="test"): ini_file = TestINI( - self._template_ini, ini_params=ini_params, - new_file_prefix=new_file_prefix, dir=self._basetemp) + self._template_ini, ini_params=ini_params, new_file_prefix=new_file_prefix, dir=self._ini_store_dir + ) result = ini_file.create() return result -def get_config( - config, option_name, override_option_name, overrides=None, - basetemp=None, prefix='test'): +def get_config(config, option_name, override_option_name, overrides=None, basetemp=None, prefix="test"): """ Find a configuration file and apply overrides for the given `prefix`. """ - config_file = ( - config.getoption(option_name) or config.getini(option_name)) + try: + config_file = config.getoption(option_name) + except ValueError: + config_file = None + if not config_file: - pytest.exit( - "Configuration error, could not extract {}.".format(option_name)) + config_file = config.getini(option_name) + + if not config_file: + pytest.exit(f"Configuration error, could not extract {option_name}.") overrides = overrides or [] - config_override = config.getoption(override_option_name) - if config_override: - overrides.append(config_override) - temp_ini_file = TestINI( - config_file, ini_params=overrides, new_file_prefix=prefix, - dir=basetemp) + if override_option_name: + config_override = config.getoption(override_option_name) + if config_override: + overrides.append(config_override) + temp_ini_file = TestINI(config_file, ini_params=overrides, new_file_prefix=prefix, dir=basetemp) return temp_ini_file.create() diff --git a/rhodecode/tests/fixture_mods/fixture_utils.py b/rhodecode/tests/fixtures/fixture_utils.py rename from rhodecode/tests/fixture_mods/fixture_utils.py rename to rhodecode/tests/fixtures/fixture_utils.py --- a/rhodecode/tests/fixture_mods/fixture_utils.py +++ b/rhodecode/tests/fixtures/fixture_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,6 +29,7 @@ import uuid import dateutil.tz import logging import functools +import textwrap import mock import pyramid.testing @@ -43,8 +43,17 @@ import rhodecode.lib from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import CommentsModel from rhodecode.model.db import ( - PullRequest, PullRequestReviewers, Repository, RhodeCodeSetting, ChangesetStatus, - RepoGroup, UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi) + PullRequest, + PullRequestReviewers, + Repository, + RhodeCodeSetting, + ChangesetStatus, + RepoGroup, + UserGroup, + RepoRhodeCodeUi, + RepoRhodeCodeSetting, + RhodeCodeUi, +) from rhodecode.model.meta import Session from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.repo import RepoModel @@ -60,12 +69,20 @@ from rhodecode.lib.str_utils import safe from rhodecode.lib.hash_utils import sha1_safe from rhodecode.lib.vcs.backends import get_backend from rhodecode.lib.vcs.nodes import FileNode +from rhodecode.lib.base import bootstrap_config from rhodecode.tests import ( - login_user_session, get_new_dir, utils, TESTS_TMP_PATH, - TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN, - TEST_USER_REGULAR_PASS) -from rhodecode.tests.utils import CustomTestApp, set_anonymous_access -from rhodecode.tests.fixture import Fixture + login_user_session, + get_new_dir, + utils, + TESTS_TMP_PATH, + TEST_USER_ADMIN_LOGIN, + TEST_USER_REGULAR_LOGIN, + TEST_USER_REGULAR2_LOGIN, + TEST_USER_REGULAR_PASS, + console_printer, +) +from rhodecode.tests.utils import set_anonymous_access +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.config import utils as config_utils log = logging.getLogger(__name__) @@ -76,36 +93,7 @@ def cmp(a, b): return (a > b) - (a < b) -@pytest.fixture(scope='session', autouse=True) -def activate_example_rcextensions(request): - """ - Patch in an example rcextensions module which verifies passed in kwargs. - """ - from rhodecode.config import rcextensions - - old_extensions = rhodecode.EXTENSIONS - rhodecode.EXTENSIONS = rcextensions - rhodecode.EXTENSIONS.calls = collections.defaultdict(list) - - @request.addfinalizer - def cleanup(): - rhodecode.EXTENSIONS = old_extensions - - -@pytest.fixture() -def capture_rcextensions(): - """ - Returns the recorded calls to entry points in rcextensions. - """ - calls = rhodecode.EXTENSIONS.calls - calls.clear() - # Note: At this moment, it is still the empty dict, but that will - # be filled during the test run and since it is a reference this - # is enough to make it work. - return calls - - -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def http_environ_session(): """ Allow to use "http_environ" in session scope. @@ -117,7 +105,31 @@ def plain_http_host_stub(): """ Value of HTTP_HOST in the test run. """ - return 'example.com:80' + return "example.com:80" + + +def plain_config_stub(request, request_stub): + """ + Set up pyramid.testing and return the Configurator. + """ + + config = bootstrap_config(request=request_stub) + + @request.addfinalizer + def cleanup(): + pyramid.testing.tearDown() + + return config + + +def plain_request_stub(): + """ + Stub request object. + """ + from rhodecode.lib.base import bootstrap_request + + _request = bootstrap_request(scheme="https") + return _request @pytest.fixture() @@ -132,7 +144,7 @@ def plain_http_host_only_stub(): """ Value of HTTP_HOST in the test run. """ - return plain_http_host_stub().split(':')[0] + return plain_http_host_stub().split(":")[0] @pytest.fixture() @@ -147,56 +159,58 @@ def plain_http_environ(): """ HTTP extra environ keys. - User by the test application and as well for setting up the pylons + Used by the test application and as well for setting up the pylons environment. In the case of the fixture "app" it should be possible to override this for a specific test case. """ return { - 'SERVER_NAME': plain_http_host_only_stub(), - 'SERVER_PORT': plain_http_host_stub().split(':')[1], - 'HTTP_HOST': plain_http_host_stub(), - 'HTTP_USER_AGENT': 'rc-test-agent', - 'REQUEST_METHOD': 'GET' + "SERVER_NAME": plain_http_host_only_stub(), + "SERVER_PORT": plain_http_host_stub().split(":")[1], + "HTTP_HOST": plain_http_host_stub(), + "HTTP_USER_AGENT": "rc-test-agent", + "REQUEST_METHOD": "GET", } -@pytest.fixture() -def http_environ(): - """ - HTTP extra environ keys. - - User by the test application and as well for setting up the pylons - environment. In the case of the fixture "app" it should be possible - to override this for a specific test case. - """ - return plain_http_environ() - - -@pytest.fixture(scope='session') -def baseapp(ini_config, vcsserver, http_environ_session): +@pytest.fixture(scope="session") +def baseapp(request, ini_config, http_environ_session, available_port_factory, vcsserver_factory, celery_factory): from rhodecode.lib.config_utils import get_app_config from rhodecode.config.middleware import make_pyramid_app - log.info("Using the RhodeCode configuration:{}".format(ini_config)) + log.info("Using the RhodeCode configuration:%s", ini_config) pyramid.paster.setup_logging(ini_config) settings = get_app_config(ini_config) - app = make_pyramid_app({'__file__': ini_config}, **settings) + store_dir = os.path.dirname(ini_config) + + # start vcsserver + _vcsserver_port = available_port_factory() + vcsserver_instance = vcsserver_factory(request, store_dir=store_dir, port=_vcsserver_port, info_prefix="base-app-") + + settings["vcs.server"] = vcsserver_instance.bind_addr + + # we skip setting store_dir for baseapp, it's internally set via testing rhodecode.ini + # settings['repo_store.path'] = str(store_dir) + console_printer(f" :warning: [green]pytest-setup[/green] Starting base pyramid-app: {ini_config}") + pyramid_baseapp = make_pyramid_app({"__file__": ini_config}, **settings) - return app + # start celery + celery_factory( + request, + store_dir=store_dir, + port=None, + info_prefix="base-app-", + overrides=( + {"handler_console": {"level": "DEBUG"}}, + {"app:main": {"vcs.server": vcsserver_instance.bind_addr}}, + {"app:main": {"repo_store.path": store_dir}}, + ), + ) + + return pyramid_baseapp -@pytest.fixture(scope='function') -def app(request, config_stub, baseapp, http_environ): - app = CustomTestApp( - baseapp, - extra_environ=http_environ) - if request.cls: - request.cls.app = app - return app - - -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def app_settings(baseapp, ini_config): """ Settings dictionary used to create the app. @@ -207,19 +221,19 @@ def app_settings(baseapp, ini_config): return baseapp.config.get_settings() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def db_connection(ini_settings): # Initialize the database connection. config_utils.initialize_database(ini_settings) -LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user')) +LoginData = collections.namedtuple("LoginData", ("csrf_token", "user")) def _autologin_user(app, *args): session = login_user_session(app, *args) csrf_token = rhodecode.lib.auth.get_csrf_token(session) - return LoginData(csrf_token, session['rhodecode_user']) + return LoginData(csrf_token, session["rhodecode_user"]) @pytest.fixture() @@ -235,18 +249,17 @@ def autologin_regular_user(app): """ Utility fixture which makes sure that the regular user is logged in """ - return _autologin_user( - app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) + return _autologin_user(app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def csrf_token(request, autologin_user): return autologin_user.csrf_token -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def xhr_header(request): - return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} + return {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"} @pytest.fixture() @@ -257,18 +270,18 @@ def real_crypto_backend(monkeypatch): During the test run the crypto backend is replaced with a faster implementation based on the MD5 algorithm. """ - monkeypatch.setattr(rhodecode, 'is_test', False) + monkeypatch.setattr(rhodecode, "is_test", False) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def index_location(request, baseapp): - index_location = baseapp.config.get_settings()['search.location'] + index_location = baseapp.config.get_settings()["search.location"] if request.cls: request.cls.index_location = index_location return index_location -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def tests_tmp_path(request): """ Create temporary directory to be used during the test session. @@ -276,7 +289,8 @@ def tests_tmp_path(request): if not os.path.exists(TESTS_TMP_PATH): os.makedirs(TESTS_TMP_PATH) - if not request.config.getoption('--keep-tmp-path'): + if not request.config.getoption("--keep-tmp-path"): + @request.addfinalizer def remove_tmp_path(): shutil.rmtree(TESTS_TMP_PATH) @@ -291,7 +305,7 @@ def test_repo_group(request): usage automatically """ fixture = Fixture() - repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '') + repogroupid = "test_repo_group_%s" % str(time.time()).replace(".", "") repo_group = fixture.create_repo_group(repogroupid) def _cleanup(): @@ -308,7 +322,7 @@ def test_user_group(request): usage automatically """ fixture = Fixture() - usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '') + usergroupid = "test_user_group_%s" % str(time.time()).replace(".", "") user_group = fixture.create_user_group(usergroupid) def _cleanup(): @@ -318,7 +332,7 @@ def test_user_group(request): return user_group -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def test_repo(request): container = TestRepoContainer() request.addfinalizer(container._cleanup) @@ -340,9 +354,9 @@ class TestRepoContainer(object): """ dump_extractors = { - 'git': utils.extract_git_repo_from_dump, - 'hg': utils.extract_hg_repo_from_dump, - 'svn': utils.extract_svn_repo_from_dump, + "git": utils.extract_git_repo_from_dump, + "hg": utils.extract_hg_repo_from_dump, + "svn": utils.extract_svn_repo_from_dump, } def __init__(self): @@ -358,7 +372,7 @@ class TestRepoContainer(object): return Repository.get(self._repos[key]) def _create_repo(self, dump_name, backend_alias, config): - repo_name = f'{backend_alias}-{dump_name}' + repo_name = f"{backend_alias}-{dump_name}" backend = get_backend(backend_alias) dump_extractor = self.dump_extractors[backend_alias] repo_path = dump_extractor(dump_name, repo_name) @@ -375,19 +389,17 @@ class TestRepoContainer(object): self._fixture.destroy_repo(repo_name) -def backend_base(request, backend_alias, baseapp, test_repo): - if backend_alias not in request.config.getoption('--backends'): - pytest.skip("Backend %s not selected." % (backend_alias, )) +def backend_base(request, backend_alias, test_repo): + if backend_alias not in request.config.getoption("--backends"): + pytest.skip(f"Backend {backend_alias} not selected.") utils.check_xfail_backends(request.node, backend_alias) utils.check_skip_backends(request.node, backend_alias) - repo_name = 'vcs_test_%s' % (backend_alias, ) + repo_name = f"vcs_test_{backend_alias}" backend = Backend( - alias=backend_alias, - repo_name=repo_name, - test_name=request.node.name, - test_repo_container=test_repo) + alias=backend_alias, repo_name=repo_name, test_name=request.node.name, test_repo_container=test_repo + ) request.addfinalizer(backend.cleanup) return backend @@ -404,22 +416,22 @@ def backend(request, backend_alias, base for specific backends. This is intended as a utility for incremental development of a new backend implementation. """ - return backend_base(request, backend_alias, baseapp, test_repo) + return backend_base(request, backend_alias, test_repo) @pytest.fixture() def backend_git(request, baseapp, test_repo): - return backend_base(request, 'git', baseapp, test_repo) + return backend_base(request, "git", test_repo) @pytest.fixture() def backend_hg(request, baseapp, test_repo): - return backend_base(request, 'hg', baseapp, test_repo) + return backend_base(request, "hg", test_repo) @pytest.fixture() def backend_svn(request, baseapp, test_repo): - return backend_base(request, 'svn', baseapp, test_repo) + return backend_base(request, "svn", test_repo) @pytest.fixture() @@ -467,9 +479,9 @@ class Backend(object): session. """ - invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+') + invalid_repo_name = re.compile(r"[^0-9a-zA-Z]+") _master_repo = None - _master_repo_path = '' + _master_repo_path = "" _commit_ids = {} def __init__(self, alias, repo_name, test_name, test_repo_container): @@ -500,6 +512,7 @@ class Backend(object): last repo which has been created with `create_repo`. """ from rhodecode.model.db import Repository + return Repository.get_by_repo_name(self.repo_name) @property @@ -517,9 +530,7 @@ class Backend(object): which can serve as the base to create a new commit on top of it. """ vcsrepo = self.repo.scm_instance() - head_id = ( - vcsrepo.DEFAULT_BRANCH_NAME or - vcsrepo.commit_ids[-1]) + head_id = vcsrepo.DEFAULT_BRANCH_NAME or vcsrepo.commit_ids[-1] return head_id @property @@ -543,9 +554,7 @@ class Backend(object): return self._commit_ids - def create_repo( - self, commits=None, number_of_commits=0, heads=None, - name_suffix='', bare=False, **kwargs): + def create_repo(self, commits=None, number_of_commits=0, heads=None, name_suffix="", bare=False, **kwargs): """ Create a repository and record it for later cleanup. @@ -559,13 +568,10 @@ class Backend(object): :param bare: set a repo as bare (no checkout) """ self.repo_name = self._next_repo_name() + name_suffix - repo = self._fixture.create_repo( - self.repo_name, repo_type=self.alias, bare=bare, **kwargs) + repo = self._fixture.create_repo(self.repo_name, repo_type=self.alias, bare=bare, **kwargs) self._cleanup_repos.append(repo.repo_name) - commits = commits or [ - {'message': f'Commit {x} of {self.repo_name}'} - for x in range(number_of_commits)] + commits = commits or [{"message": f"Commit {x} of {self.repo_name}"} for x in range(number_of_commits)] vcs_repo = repo.scm_instance() vcs_repo.count() self._add_commits_to_repo(vcs_repo, commits) @@ -579,7 +585,7 @@ class Backend(object): Make sure that repo contains all commits mentioned in `heads` """ vcsrepo = repo.scm_instance() - vcsrepo.config.clear_section('hooks') + vcsrepo.config.clear_section("hooks") commit_ids = [self._commit_ids[h] for h in heads] if do_fetch: vcsrepo.fetch(self._master_repo_path, commit_ids=commit_ids) @@ -592,21 +598,22 @@ class Backend(object): self._cleanup_repos.append(self.repo_name) return repo - def new_repo_name(self, suffix=''): + def new_repo_name(self, suffix=""): self.repo_name = self._next_repo_name() + suffix self._cleanup_repos.append(self.repo_name) return self.repo_name def _next_repo_name(self): - return "%s_%s" % ( - self.invalid_repo_name.sub('_', self._test_name), len(self._cleanup_repos)) + return "%s_%s" % (self.invalid_repo_name.sub("_", self._test_name), len(self._cleanup_repos)) - def ensure_file(self, filename, content=b'Test content\n'): + def ensure_file(self, filename, content=b"Test content\n"): assert self._cleanup_repos, "Avoid writing into vcs_test repos" commits = [ - {'added': [ - FileNode(filename, content=content), - ]}, + { + "added": [ + FileNode(filename, content=content), + ] + }, ] self._add_commits_to_repo(self.repo.scm_instance(), commits) @@ -627,11 +634,11 @@ class Backend(object): self._commit_ids = commit_ids # Creating refs for Git to allow fetching them from remote repository - if self.alias == 'git': + if self.alias == "git": refs = {} for message in self._commit_ids: - cleanup_message = message.replace(' ', '') - ref_name = f'refs/test-refs/{cleanup_message}' + cleanup_message = message.replace(" ", "") + ref_name = f"refs/test-refs/{cleanup_message}" refs[ref_name] = self._commit_ids[message] self._create_refs(repo, refs) @@ -645,7 +652,7 @@ class VcsBackend(object): Represents the test configuration for one supported vcs backend. """ - invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+') + invalid_repo_name = re.compile(r"[^0-9a-zA-Z]+") def __init__(self, alias, repo_path, test_name, test_repo_container): self.alias = alias @@ -658,7 +665,7 @@ class VcsBackend(object): return self._test_repo_container(key, self.alias).scm_instance() def __repr__(self): - return f'{self.__class__.__name__}(alias={self.alias}, repo={self._repo_path})' + return f"{self.__class__.__name__}(alias={self.alias}, repo={self._repo_path})" @property def repo(self): @@ -676,8 +683,7 @@ class VcsBackend(object): """ return get_backend(self.alias) - def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None, - bare=False): + def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None, bare=False): repo_name = self._next_repo_name() self._repo_path = get_new_dir(repo_name) repo_class = get_backend(self.alias) @@ -687,9 +693,7 @@ class VcsBackend(object): repo = repo_class(self._repo_path, create=True, src_url=src_url, bare=bare) self._cleanup_repos.append(repo) - commits = commits or [ - {'message': 'Commit %s of %s' % (x, repo_name)} - for x in range(number_of_commits)] + commits = commits or [{"message": f"Commit {x} of {repo_name}"} for x in range(number_of_commits)] _add_commits_to_repo(repo, commits) return repo @@ -706,38 +710,30 @@ class VcsBackend(object): return self._repo_path def _next_repo_name(self): + return "{}_{}".format(self.invalid_repo_name.sub("_", self._test_name), len(self._cleanup_repos)) - return "{}_{}".format( - self.invalid_repo_name.sub('_', self._test_name), - len(self._cleanup_repos) - ) - - def add_file(self, repo, filename, content='Test content\n'): + def add_file(self, repo, filename, content="Test content\n"): imc = repo.in_memory_commit imc.add(FileNode(safe_bytes(filename), content=safe_bytes(content))) - imc.commit( - message='Automatic commit from vcsbackend fixture', - author='Automatic ') + imc.commit(message="Automatic commit from vcsbackend fixture", author="Automatic ") - def ensure_file(self, filename, content='Test content\n'): + def ensure_file(self, filename, content="Test content\n"): assert self._cleanup_repos, "Avoid writing into vcs_test repos" self.add_file(self.repo, filename, content) def vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo) -> VcsBackend: - if backend_alias not in request.config.getoption('--backends'): - pytest.skip("Backend %s not selected." % (backend_alias, )) + if backend_alias not in request.config.getoption("--backends"): + pytest.skip(f"Backend {backend_alias} not selected.") utils.check_xfail_backends(request.node, backend_alias) utils.check_skip_backends(request.node, backend_alias) - repo_name = f'vcs_test_{backend_alias}' + repo_name = f"vcs_test_{backend_alias}" repo_path = os.path.join(tests_tmp_path, repo_name) backend = VcsBackend( - alias=backend_alias, - repo_path=repo_path, - test_name=request.node.name, - test_repo_container=test_repo) + alias=backend_alias, repo_path=repo_path, test_name=request.node.name, test_repo_container=test_repo + ) request.addfinalizer(backend.cleanup) return backend @@ -758,17 +754,17 @@ def vcsbackend(request, backend_alias, t @pytest.fixture() def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo): - return vcsbackend_base(request, 'git', tests_tmp_path, baseapp, test_repo) + return vcsbackend_base(request, "git", tests_tmp_path, baseapp, test_repo) @pytest.fixture() def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo): - return vcsbackend_base(request, 'hg', tests_tmp_path, baseapp, test_repo) + return vcsbackend_base(request, "hg", tests_tmp_path, baseapp, test_repo) @pytest.fixture() def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo): - return vcsbackend_base(request, 'svn', tests_tmp_path, baseapp, test_repo) + return vcsbackend_base(request, "svn", tests_tmp_path, baseapp, test_repo) @pytest.fixture() @@ -789,29 +785,28 @@ def _add_commits_to_repo(vcs_repo, commi imc = vcs_repo.in_memory_commit for idx, commit in enumerate(commits): - message = str(commit.get('message', f'Commit {idx}')) + message = str(commit.get("message", f"Commit {idx}")) - for node in commit.get('added', []): + for node in commit.get("added", []): imc.add(FileNode(safe_bytes(node.path), content=node.content)) - for node in commit.get('changed', []): + for node in commit.get("changed", []): imc.change(FileNode(safe_bytes(node.path), content=node.content)) - for node in commit.get('removed', []): + for node in commit.get("removed", []): imc.remove(FileNode(safe_bytes(node.path))) - parents = [ - vcs_repo.get_commit(commit_id=commit_ids[p]) - for p in commit.get('parents', [])] + parents = [vcs_repo.get_commit(commit_id=commit_ids[p]) for p in commit.get("parents", [])] - operations = ('added', 'changed', 'removed') + operations = ("added", "changed", "removed") if not any((commit.get(o) for o in operations)): - imc.add(FileNode(b'file_%b' % safe_bytes(str(idx)), content=safe_bytes(message))) + imc.add(FileNode(b"file_%b" % safe_bytes(str(idx)), content=safe_bytes(message))) commit = imc.commit( message=message, - author=str(commit.get('author', 'Automatic ')), - date=commit.get('date'), - branch=commit.get('branch'), - parents=parents) + author=str(commit.get("author", "Automatic ")), + date=commit.get("date"), + branch=commit.get("branch"), + parents=parents, + ) commit_ids[commit.message] = commit.raw_id @@ -842,14 +837,14 @@ class RepoServer(object): self._cleanup_servers = [] def serve(self, vcsrepo): - if vcsrepo.alias != 'svn': - raise TypeError("Backend %s not supported" % vcsrepo.alias) + if vcsrepo.alias != "svn": + raise TypeError(f"Backend {vcsrepo.alias} not supported") proc = subprocess.Popen( - ['svnserve', '-d', '--foreground', '--listen-host', 'localhost', - '--root', vcsrepo.path]) + ["svnserve", "-d", "--foreground", "--listen-host", "localhost", "--root", vcsrepo.path] + ) self._cleanup_servers.append(proc) - self.url = 'svn://localhost' + self.url = "svn://localhost" def cleanup(self): for proc in self._cleanup_servers: @@ -874,7 +869,6 @@ def pr_util(backend, request, config_stu class PRTestUtility(object): - pull_request = None pull_request_id = None mergeable_patcher = None @@ -886,48 +880,55 @@ class PRTestUtility(object): self.backend = backend def create_pull_request( - self, commits=None, target_head=None, source_head=None, - revisions=None, approved=False, author=None, mergeable=False, - enable_notifications=True, name_suffix='', reviewers=None, observers=None, - title="Test", description="Description"): + self, + commits=None, + target_head=None, + source_head=None, + revisions=None, + approved=False, + author=None, + mergeable=False, + enable_notifications=True, + name_suffix="", + reviewers=None, + observers=None, + title="Test", + description="Description", + ): self.set_mergeable(mergeable) if not enable_notifications: # mock notification side effect - self.notification_patcher = mock.patch( - 'rhodecode.model.notification.NotificationModel.create') + self.notification_patcher = mock.patch("rhodecode.model.notification.NotificationModel.create") self.notification_patcher.start() if not self.pull_request: if not commits: commits = [ - {'message': 'c1'}, - {'message': 'c2'}, - {'message': 'c3'}, + {"message": "c1"}, + {"message": "c2"}, + {"message": "c3"}, ] - target_head = 'c1' - source_head = 'c2' - revisions = ['c2'] + target_head = "c1" + source_head = "c2" + revisions = ["c2"] self.commit_ids = self.backend.create_master_repo(commits) - self.target_repository = self.backend.create_repo( - heads=[target_head], name_suffix=name_suffix) - self.source_repository = self.backend.create_repo( - heads=[source_head], name_suffix=name_suffix) - self.author = author or UserModel().get_by_username( - TEST_USER_ADMIN_LOGIN) + self.target_repository = self.backend.create_repo(heads=[target_head], name_suffix=name_suffix) + self.source_repository = self.backend.create_repo(heads=[source_head], name_suffix=name_suffix) + self.author = author or UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) model = PullRequestModel() self.create_parameters = { - 'created_by': self.author, - 'source_repo': self.source_repository.repo_name, - 'source_ref': self._default_branch_reference(source_head), - 'target_repo': self.target_repository.repo_name, - 'target_ref': self._default_branch_reference(target_head), - 'revisions': [self.commit_ids[r] for r in revisions], - 'reviewers': reviewers or self._get_reviewers(), - 'observers': observers or self._get_observers(), - 'title': title, - 'description': description, + "created_by": self.author, + "source_repo": self.source_repository.repo_name, + "source_ref": self._default_branch_reference(source_head), + "target_repo": self.target_repository.repo_name, + "target_ref": self._default_branch_reference(target_head), + "revisions": [self.commit_ids[r] for r in revisions], + "reviewers": reviewers or self._get_reviewers(), + "observers": observers or self._get_observers(), + "title": title, + "description": description, } self.pull_request = model.create(**self.create_parameters) assert model.get_versions(self.pull_request) == [] @@ -943,9 +944,7 @@ class PRTestUtility(object): return self.pull_request def approve(self): - self.create_status_votes( - ChangesetStatus.STATUS_APPROVED, - *self.pull_request.reviewers) + self.create_status_votes(ChangesetStatus.STATUS_APPROVED, *self.pull_request.reviewers) def close(self): PullRequestModel().close_pull_request(self.pull_request, self.author) @@ -953,28 +952,26 @@ class PRTestUtility(object): def _default_branch_reference(self, commit_message, branch: str = None) -> str: default_branch = branch or self.backend.default_branch_name message = self.commit_ids[commit_message] - reference = f'branch:{default_branch}:{message}' + reference = f"branch:{default_branch}:{message}" return reference def _get_reviewers(self): role = PullRequestReviewers.ROLE_REVIEWER return [ - (TEST_USER_REGULAR_LOGIN, ['default1'], False, role, []), - (TEST_USER_REGULAR2_LOGIN, ['default2'], False, role, []), + (TEST_USER_REGULAR_LOGIN, ["default1"], False, role, []), + (TEST_USER_REGULAR2_LOGIN, ["default2"], False, role, []), ] def _get_observers(self): - return [ - - ] + return [] def update_source_repository(self, head=None, do_fetch=False): - heads = [head or 'c3'] + heads = [head or "c3"] self.backend.pull_heads(self.source_repository, heads=heads, do_fetch=do_fetch) def update_target_repository(self, head=None, do_fetch=False): - heads = [head or 'c3'] + heads = [head or "c3"] self.backend.pull_heads(self.target_repository, heads=heads, do_fetch=do_fetch) def set_pr_target_ref(self, ref_type: str = "branch", ref_name: str = "branch", ref_commit_id: str = "") -> str: @@ -1004,7 +1001,7 @@ class PRTestUtility(object): # TODO: johbo: Git and Mercurial have an inconsistent vcs api here, # remove the if once that's sorted out. if self.backend.alias == "git": - kwargs = {'branch_name': self.backend.default_branch_name} + kwargs = {"branch_name": self.backend.default_branch_name} else: kwargs = {} source_vcs.strip(removed_commit_id, **kwargs) @@ -1015,10 +1012,8 @@ class PRTestUtility(object): def create_comment(self, linked_to=None): comment = CommentsModel().create( - text="Test comment", - repo=self.target_repository.repo_name, - user=self.author, - pull_request=self.pull_request) + text="Test comment", repo=self.target_repository.repo_name, user=self.author, pull_request=self.pull_request + ) assert comment.pull_request_version_id is None if linked_to: @@ -1026,15 +1021,15 @@ class PRTestUtility(object): return comment - def create_inline_comment( - self, linked_to=None, line_no='n1', file_path='file_1'): + def create_inline_comment(self, linked_to=None, line_no="n1", file_path="file_1"): comment = CommentsModel().create( text="Test comment", repo=self.target_repository.repo_name, user=self.author, line_no=line_no, f_path=file_path, - pull_request=self.pull_request) + pull_request=self.pull_request, + ) assert comment.pull_request_version_id is None if linked_to: @@ -1044,25 +1039,20 @@ class PRTestUtility(object): def create_version_of_pull_request(self): pull_request = self.create_pull_request() - version = PullRequestModel()._create_version_from_snapshot( - pull_request) + version = PullRequestModel()._create_version_from_snapshot(pull_request) return version def create_status_votes(self, status, *reviewers): for reviewer in reviewers: ChangesetStatusModel().set_status( - repo=self.pull_request.target_repo, - status=status, - user=reviewer.user_id, - pull_request=self.pull_request) + repo=self.pull_request.target_repo, status=status, user=reviewer.user_id, pull_request=self.pull_request + ) def set_mergeable(self, value): if not self.mergeable_patcher: - self.mergeable_patcher = mock.patch.object( - VcsSettingsModel, 'get_general_settings') + self.mergeable_patcher = mock.patch.object(VcsSettingsModel, "get_general_settings") self.mergeable_mock = self.mergeable_patcher.start() - self.mergeable_mock.return_value = { - 'rhodecode_pr_merge_enabled': value} + self.mergeable_mock.return_value = {"rhodecode_pr_merge_enabled": value} def cleanup(self): # In case the source repository is already cleaned up, the pull @@ -1109,7 +1099,6 @@ def user_util(request, db_connection): # TODO: johbo: Split this up into utilities per domain or something similar class UserUtility(object): - def __init__(self, test_name="test"): self._test_name = self._sanitize_name(test_name) self.fixture = Fixture() @@ -1126,37 +1115,29 @@ class UserUtility(object): self.user_permissions = [] def _sanitize_name(self, name): - for char in ['[', ']']: - name = name.replace(char, '_') + for char in ["[", "]"]: + name = name.replace(char, "_") return name - def create_repo_group( - self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True): - group_name = "{prefix}_repogroup_{count}".format( - prefix=self._test_name, - count=len(self.repo_group_ids)) - repo_group = self.fixture.create_repo_group( - group_name, cur_user=owner) + def create_repo_group(self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True): + group_name = f"{self._test_name}_repogroup_{len(self.repo_group_ids)}" + repo_group = self.fixture.create_repo_group(group_name, cur_user=owner) if auto_cleanup: self.repo_group_ids.append(repo_group.group_id) return repo_group - def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None, - auto_cleanup=True, repo_type='hg', bare=False): - repo_name = "{prefix}_repository_{count}".format( - prefix=self._test_name, - count=len(self.repos_ids)) + def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None, auto_cleanup=True, repo_type="hg", bare=False): + repo_name = f"{self._test_name}_repository_{len(self.repos_ids)}" repository = self.fixture.create_repo( - repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type, bare=bare) + repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type, bare=bare + ) if auto_cleanup: self.repos_ids.append(repository.repo_id) return repository def create_user(self, auto_cleanup=True, **kwargs): - user_name = "{prefix}_user_{count}".format( - prefix=self._test_name, - count=len(self.user_ids)) + user_name = f"{self._test_name}_user_{len(self.user_ids)}" user = self.fixture.create_user(user_name, **kwargs) if auto_cleanup: self.user_ids.append(user.user_id) @@ -1171,13 +1152,9 @@ class UserUtility(object): user_group = self.create_user_group(members=[user]) return user, user_group - def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None, - auto_cleanup=True, **kwargs): - group_name = "{prefix}_usergroup_{count}".format( - prefix=self._test_name, - count=len(self.user_group_ids)) - user_group = self.fixture.create_user_group( - group_name, cur_user=owner, **kwargs) + def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None, auto_cleanup=True, **kwargs): + group_name = f"{self._test_name}_usergroup_{len(self.user_group_ids)}" + user_group = self.fixture.create_user_group(group_name, cur_user=owner, **kwargs) if auto_cleanup: self.user_group_ids.append(user_group.users_group_id) @@ -1190,52 +1167,34 @@ class UserUtility(object): self.inherit_default_user_permissions(user_name, False) self.user_permissions.append((user_name, permission_name)) - def grant_user_permission_to_repo_group( - self, repo_group, user, permission_name): - permission = RepoGroupModel().grant_user_permission( - repo_group, user, permission_name) - self.user_repo_group_permission_ids.append( - (repo_group.group_id, user.user_id)) + def grant_user_permission_to_repo_group(self, repo_group, user, permission_name): + permission = RepoGroupModel().grant_user_permission(repo_group, user, permission_name) + self.user_repo_group_permission_ids.append((repo_group.group_id, user.user_id)) return permission - def grant_user_group_permission_to_repo_group( - self, repo_group, user_group, permission_name): - permission = RepoGroupModel().grant_user_group_permission( - repo_group, user_group, permission_name) - self.user_group_repo_group_permission_ids.append( - (repo_group.group_id, user_group.users_group_id)) + def grant_user_group_permission_to_repo_group(self, repo_group, user_group, permission_name): + permission = RepoGroupModel().grant_user_group_permission(repo_group, user_group, permission_name) + self.user_group_repo_group_permission_ids.append((repo_group.group_id, user_group.users_group_id)) return permission - def grant_user_permission_to_repo( - self, repo, user, permission_name): - permission = RepoModel().grant_user_permission( - repo, user, permission_name) - self.user_repo_permission_ids.append( - (repo.repo_id, user.user_id)) + def grant_user_permission_to_repo(self, repo, user, permission_name): + permission = RepoModel().grant_user_permission(repo, user, permission_name) + self.user_repo_permission_ids.append((repo.repo_id, user.user_id)) return permission - def grant_user_group_permission_to_repo( - self, repo, user_group, permission_name): - permission = RepoModel().grant_user_group_permission( - repo, user_group, permission_name) - self.user_group_repo_permission_ids.append( - (repo.repo_id, user_group.users_group_id)) + def grant_user_group_permission_to_repo(self, repo, user_group, permission_name): + permission = RepoModel().grant_user_group_permission(repo, user_group, permission_name) + self.user_group_repo_permission_ids.append((repo.repo_id, user_group.users_group_id)) return permission - def grant_user_permission_to_user_group( - self, target_user_group, user, permission_name): - permission = UserGroupModel().grant_user_permission( - target_user_group, user, permission_name) - self.user_user_group_permission_ids.append( - (target_user_group.users_group_id, user.user_id)) + def grant_user_permission_to_user_group(self, target_user_group, user, permission_name): + permission = UserGroupModel().grant_user_permission(target_user_group, user, permission_name) + self.user_user_group_permission_ids.append((target_user_group.users_group_id, user.user_id)) return permission - def grant_user_group_permission_to_user_group( - self, target_user_group, user_group, permission_name): - permission = UserGroupModel().grant_user_group_permission( - target_user_group, user_group, permission_name) - self.user_group_user_group_permission_ids.append( - (target_user_group.users_group_id, user_group.users_group_id)) + def grant_user_group_permission_to_user_group(self, target_user_group, user_group, permission_name): + permission = UserGroupModel().grant_user_group_permission(target_user_group, user_group, permission_name) + self.user_group_user_group_permission_ids.append((target_user_group.users_group_id, user_group.users_group_id)) return permission def revoke_user_permission(self, user_name, permission_name): @@ -1285,14 +1244,11 @@ class UserUtility(object): """ first_group = RepoGroup.get(first_group_id) second_group = RepoGroup.get(second_group_id) - first_group_parts = ( - len(first_group.group_name.split('/')) if first_group else 0) - second_group_parts = ( - len(second_group.group_name.split('/')) if second_group else 0) + first_group_parts = len(first_group.group_name.split("/")) if first_group else 0 + second_group_parts = len(second_group.group_name.split("/")) if second_group else 0 return cmp(second_group_parts, first_group_parts) - sorted_repo_group_ids = sorted( - self.repo_group_ids, key=functools.cmp_to_key(_repo_group_compare)) + sorted_repo_group_ids = sorted(self.repo_group_ids, key=functools.cmp_to_key(_repo_group_compare)) for repo_group_id in sorted_repo_group_ids: self.fixture.destroy_repo_group(repo_group_id) @@ -1308,16 +1264,11 @@ class UserUtility(object): """ first_group = UserGroup.get(first_group_id) second_group = UserGroup.get(second_group_id) - first_group_parts = ( - len(first_group.users_group_name.split('/')) - if first_group else 0) - second_group_parts = ( - len(second_group.users_group_name.split('/')) - if second_group else 0) + first_group_parts = len(first_group.users_group_name.split("/")) if first_group else 0 + second_group_parts = len(second_group.users_group_name.split("/")) if second_group else 0 return cmp(second_group_parts, first_group_parts) - sorted_user_group_ids = sorted( - self.user_group_ids, key=functools.cmp_to_key(_user_group_compare)) + sorted_user_group_ids = sorted(self.user_group_ids, key=functools.cmp_to_key(_user_group_compare)) for user_group_id in sorted_user_group_ids: self.fixture.destroy_user_group(user_group_id) @@ -1326,22 +1277,19 @@ class UserUtility(object): self.fixture.destroy_user(user_id) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def testrun(): return { - 'uuid': uuid.uuid4(), - 'start': datetime.datetime.utcnow().isoformat(), - 'timestamp': int(time.time()), + "uuid": uuid.uuid4(), + "start": datetime.datetime.utcnow().isoformat(), + "timestamp": int(time.time()), } class AppenlightClient(object): - - url_template = '{url}?protocol_version=0.5' + url_template = "{url}?protocol_version=0.5" - def __init__( - self, url, api_key, add_server=True, add_timestamp=True, - namespace=None, request=None, testrun=None): + def __init__(self, url, api_key, add_server=True, add_timestamp=True, namespace=None, request=None, testrun=None): self.url = self.url_template.format(url=url) self.api_key = api_key self.add_server = add_server @@ -1362,40 +1310,41 @@ class AppenlightClient(object): def collect(self, data): if self.add_server: - data.setdefault('server', self.server) + data.setdefault("server", self.server) if self.add_timestamp: - data.setdefault('date', datetime.datetime.utcnow().isoformat()) + data.setdefault("date", datetime.datetime.utcnow().isoformat()) if self.namespace: - data.setdefault('namespace', self.namespace) + data.setdefault("namespace", self.namespace) if self.request: - data.setdefault('request', self.request) + data.setdefault("request", self.request) self.stats.append(data) def send_stats(self): tags = [ - ('testrun', self.request), - ('testrun.start', self.testrun['start']), - ('testrun.timestamp', self.testrun['timestamp']), - ('test', self.namespace), + ("testrun", self.request), + ("testrun.start", self.testrun["start"]), + ("testrun.timestamp", self.testrun["timestamp"]), + ("test", self.namespace), ] for key, value in self.tags_before.items(): - tags.append((key + '.before', value)) + tags.append((key + ".before", value)) try: delta = self.tags_after[key] - value - tags.append((key + '.delta', delta)) + tags.append((key + ".delta", delta)) except Exception: pass for key, value in self.tags_after.items(): - tags.append((key + '.after', value)) - self.collect({ - 'message': "Collected tags", - 'tags': tags, - }) + tags.append((key + ".after", value)) + self.collect( + { + "message": "Collected tags", + "tags": tags, + } + ) response = requests.post( self.url, - headers={ - 'X-appenlight-api-key': self.api_key}, + headers={"X-appenlight-api-key": self.api_key}, json=self.stats, ) @@ -1403,7 +1352,7 @@ class AppenlightClient(object): pprint.pprint(self.stats) print(response.headers) print(response.text) - raise Exception('Sending to appenlight failed') + raise Exception("Sending to appenlight failed") @pytest.fixture() @@ -1454,9 +1403,8 @@ class SettingsUtility(object): self.repo_rhodecode_ui_ids = [] self.repo_rhodecode_setting_ids = [] - def create_repo_rhodecode_ui( - self, repo, section, value, key=None, active=True, cleanup=True): - key = key or sha1_safe(f'{section}{value}{repo.repo_id}') + def create_repo_rhodecode_ui(self, repo, section, value, key=None, active=True, cleanup=True): + key = key or sha1_safe(f"{section}{value}{repo.repo_id}") setting = RepoRhodeCodeUi() setting.repository_id = repo.repo_id @@ -1471,9 +1419,8 @@ class SettingsUtility(object): self.repo_rhodecode_ui_ids.append(setting.ui_id) return setting - def create_rhodecode_ui( - self, section, value, key=None, active=True, cleanup=True): - key = key or sha1_safe(f'{section}{value}') + def create_rhodecode_ui(self, section, value, key=None, active=True, cleanup=True): + key = key or sha1_safe(f"{section}{value}") setting = RhodeCodeUi() setting.ui_section = section @@ -1487,10 +1434,8 @@ class SettingsUtility(object): self.rhodecode_ui_ids.append(setting.ui_id) return setting - def create_repo_rhodecode_setting( - self, repo, name, value, type_, cleanup=True): - setting = RepoRhodeCodeSetting( - repo.repo_id, key=name, val=value, type=type_) + def create_repo_rhodecode_setting(self, repo, name, value, type_, cleanup=True): + setting = RepoRhodeCodeSetting(repo.repo_id, key=name, val=value, type=type_) Session().add(setting) Session().commit() @@ -1530,13 +1475,12 @@ class SettingsUtility(object): @pytest.fixture() def no_notifications(request): - notification_patcher = mock.patch( - 'rhodecode.model.notification.NotificationModel.create') + notification_patcher = mock.patch("rhodecode.model.notification.NotificationModel.create") notification_patcher.start() request.addfinalizer(notification_patcher.stop) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def repeat(request): """ The number of repetitions is based on this fixture. @@ -1544,7 +1488,7 @@ def repeat(request): Slower calls may divide it by 10 or 100. It is chosen in a way so that the tests are not too slow in our default test suite. """ - return request.config.getoption('--repeat') + return request.config.getoption("--repeat") @pytest.fixture() @@ -1562,42 +1506,17 @@ def context_stub(): @pytest.fixture() -def request_stub(): - """ - Stub request object. - """ - from rhodecode.lib.base import bootstrap_request - request = bootstrap_request(scheme='https') - return request - - -@pytest.fixture() -def config_stub(request, request_stub): - """ - Set up pyramid.testing and return the Configurator. - """ - from rhodecode.lib.base import bootstrap_config - config = bootstrap_config(request=request_stub) - - @request.addfinalizer - def cleanup(): - pyramid.testing.tearDown() - - return config - - -@pytest.fixture() def StubIntegrationType(): class _StubIntegrationType(IntegrationTypeBase): - """ Test integration type class """ + """Test integration type class""" - key = 'test' - display_name = 'Test integration type' - description = 'A test integration type for testing' + key = "test" + display_name = "Test integration type" + description = "A test integration type for testing" @classmethod def icon(cls): - return 'test_icon_html_image' + return "test_icon_html_image" def __init__(self, settings): super(_StubIntegrationType, self).__init__(settings) @@ -1611,15 +1530,15 @@ def StubIntegrationType(): test_string_field = colander.SchemaNode( colander.String(), missing=colander.required, - title='test string field', + title="test string field", ) test_int_field = colander.SchemaNode( colander.Int(), - title='some integer setting', + title="some integer setting", ) + return SettingsSchema() - integration_type_registry.register_integration_type(_StubIntegrationType) return _StubIntegrationType @@ -1627,18 +1546,43 @@ def StubIntegrationType(): @pytest.fixture() def stub_integration_settings(): return { - 'test_string_field': 'some data', - 'test_int_field': 100, + "test_string_field": "some data", + "test_int_field": 100, } @pytest.fixture() -def repo_integration_stub(request, repo_stub, StubIntegrationType, - stub_integration_settings): +def repo_integration_stub(request, repo_stub, StubIntegrationType, stub_integration_settings): + repo_id = repo_stub.repo_id integration = IntegrationModel().create( - StubIntegrationType, settings=stub_integration_settings, enabled=True, - name='test repo integration', - repo=repo_stub, repo_group=None, child_repos_only=None) + StubIntegrationType, + settings=stub_integration_settings, + enabled=True, + name="test repo integration", + repo=repo_stub, + repo_group=None, + child_repos_only=None, + ) + + @request.addfinalizer + def cleanup(): + IntegrationModel().delete(integration) + RepoModel().delete(repo_id) + + return integration + + +@pytest.fixture() +def repogroup_integration_stub(request, test_repo_group, StubIntegrationType, stub_integration_settings): + integration = IntegrationModel().create( + StubIntegrationType, + settings=stub_integration_settings, + enabled=True, + name="test repogroup integration", + repo=None, + repo_group=test_repo_group, + child_repos_only=True, + ) @request.addfinalizer def cleanup(): @@ -1648,12 +1592,16 @@ def repo_integration_stub(request, repo_ @pytest.fixture() -def repogroup_integration_stub(request, test_repo_group, StubIntegrationType, - stub_integration_settings): +def repogroup_recursive_integration_stub(request, test_repo_group, StubIntegrationType, stub_integration_settings): integration = IntegrationModel().create( - StubIntegrationType, settings=stub_integration_settings, enabled=True, - name='test repogroup integration', - repo=None, repo_group=test_repo_group, child_repos_only=True) + StubIntegrationType, + settings=stub_integration_settings, + enabled=True, + name="test recursive repogroup integration", + repo=None, + repo_group=test_repo_group, + child_repos_only=False, + ) @request.addfinalizer def cleanup(): @@ -1663,12 +1611,16 @@ def repogroup_integration_stub(request, @pytest.fixture() -def repogroup_recursive_integration_stub(request, test_repo_group, - StubIntegrationType, stub_integration_settings): +def global_integration_stub(request, StubIntegrationType, stub_integration_settings): integration = IntegrationModel().create( - StubIntegrationType, settings=stub_integration_settings, enabled=True, - name='test recursive repogroup integration', - repo=None, repo_group=test_repo_group, child_repos_only=False) + StubIntegrationType, + settings=stub_integration_settings, + enabled=True, + name="test global integration", + repo=None, + repo_group=None, + child_repos_only=None, + ) @request.addfinalizer def cleanup(): @@ -1678,27 +1630,16 @@ def repogroup_recursive_integration_stub @pytest.fixture() -def global_integration_stub(request, StubIntegrationType, - stub_integration_settings): +def root_repos_integration_stub(request, StubIntegrationType, stub_integration_settings): integration = IntegrationModel().create( - StubIntegrationType, settings=stub_integration_settings, enabled=True, - name='test global integration', - repo=None, repo_group=None, child_repos_only=None) - - @request.addfinalizer - def cleanup(): - IntegrationModel().delete(integration) - - return integration - - -@pytest.fixture() -def root_repos_integration_stub(request, StubIntegrationType, - stub_integration_settings): - integration = IntegrationModel().create( - StubIntegrationType, settings=stub_integration_settings, enabled=True, - name='test global integration', - repo=None, repo_group=None, child_repos_only=True) + StubIntegrationType, + settings=stub_integration_settings, + enabled=True, + name="test global integration", + repo=None, + repo_group=None, + child_repos_only=True, + ) @request.addfinalizer def cleanup(): @@ -1710,8 +1651,8 @@ def root_repos_integration_stub(request, @pytest.fixture() def local_dt_to_utc(): def _factory(dt): - return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone( - dateutil.tz.tzutc()).replace(tzinfo=None) + return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(dateutil.tz.tzutc()).replace(tzinfo=None) + return _factory @@ -1724,7 +1665,7 @@ def disable_anonymous_user(request, base set_anonymous_access(True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rc_fixture(request): return Fixture() @@ -1734,9 +1675,9 @@ def repo_groups(request): fixture = Fixture() session = Session() - zombie_group = fixture.create_repo_group('zombie') - parent_group = fixture.create_repo_group('parent') - child_group = fixture.create_repo_group('parent/child') + zombie_group = fixture.create_repo_group("zombie") + parent_group = fixture.create_repo_group("parent") + child_group = fixture.create_repo_group("parent/child") groups_in_db = session.query(RepoGroup).all() assert len(groups_in_db) == 3 assert child_group.group_parent_id == parent_group.group_id diff --git a/rhodecode/tests/fixtures/function_scoped_baseapp.py b/rhodecode/tests/fixtures/function_scoped_baseapp.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/fixtures/function_scoped_baseapp.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest +from rhodecode.tests.utils import CustomTestApp +from rhodecode.tests.fixtures.fixture_utils import plain_http_environ, plain_config_stub, plain_request_stub + + +@pytest.fixture(scope='function') +def request_stub(): + return plain_request_stub() + + +@pytest.fixture(scope='function') +def config_stub(request, request_stub): + return plain_config_stub(request, request_stub) + + +@pytest.fixture(scope='function') +def http_environ(): + """ + HTTP extra environ keys. + + Used by the test application and as well for setting up the pylons + environment. In the case of the fixture "app" it should be possible + to override this for a specific test case. + """ + return plain_http_environ() + + +@pytest.fixture(scope='function') +def app(request, config_stub, http_environ, baseapp): + app = CustomTestApp(baseapp, extra_environ=http_environ) + if request.cls: + # inject app into a class that uses this fixtures + request.cls.app = app + return app diff --git a/rhodecode/tests/fixtures/hg_diff_mod_single_binary_file.diff b/rhodecode/tests/fixtures/hg_diff_mod_single_binary_file.diff deleted file mode 100644 --- a/rhodecode/tests/fixtures/hg_diff_mod_single_binary_file.diff +++ /dev/null @@ -1,1094 +0,0 @@ -diff --git a/US Warszawa.jpg b/US Warszawa.jpg -index 5a4b530a9322aad2b13d39a423a6a93146a09a29..b3b889566812a77f52376379e96d32d3db117150 -GIT binary patch -literal 85172 -zc$~C)2S5|s+ThHjCnN+2EtG^_0tAwf03w7A0)m1FNTf(87CIILK}Aupi-NuPMpaNy -zR4!NS9eeKuyCO>1Gn3%OckkPM|J&UpXXZO~&iVTHO)SEF{q3C`L{y8D^G?n@S$eXH -zPIVG+9l4oF`ouhW4wvsgF)>f>D<7AcEay@^Jh-`e -zIr79w)TwJ5V}ml2ota0UkM1FteYo-TOVF0Uy9a(yOdPg!(p|XtI8FaRvz{MQcYXZy -z)veR6d~f&Hk^8^oHXC**5=X{#uOb&GG|3;Ft+ZnSif~Me8^> -zZohqef5Sbf{!UGq=c*8%MTCFO)0xv=HY0x+ub(l)s&vUFuQXec(Q3-MM>}Ze?$=8m -z7#<3a*3z}xw9b3T;}gye!D(F=#pd`v(^ZmXIfC#^{*M&`wZNi&-t?Xi8K&hAh@x`S -z{Ad2F*Uvp?cbdESbN2F!D}N5{EbKJxw7y=`Q`pc9b+=u7mi@TtPiF_pq>7yU25um& -z$#p00aK@!VxFXLhb;k157f$hZ#XU^bE1UT)_*nBP!&ye=zSsG<`GTyr-BaRbnT@Wy -z+P&&hcI&E7>idNSYjr2pKJs|eo2A=ld?=W4z+H0LzxdeCC&gN=N6My!Pd`#QmzP>C -zoJ*5$`1JM3%NKWajP4|lxYvdhF1>D1e9gL}yma`HA6p4kGmdNT(t1kw+;V60hBr2o -zUkyLS+Pc?$)Y*gZR_T5(_g5`)Q)uU-z0RG5`$Y6RymMH)-8BA)QdX!ZXYumDgX0{07THaWT6ptt{y5@8n?=o?Ggc<-G5Dl^czbw| -zrt6t1*=8Xe74UKa6xB9?Q*p!MfGi$wJSQ}OdHVJ7dz&j>8_pnhCz?Qe(s-qNv@W}+ -z_NMPY-*CC7fLnX*PMVD1!F6sjrDon;bZW%akVhkp!^QAMn*4?!llstk#`e46&)SRe -zmm04GRTicHsPuJvmXK;B-S=kgPeGdZ2&wOVmxWQmD^2#v0&fQ?h#3TR^Bct>v}XzP -zFP>PhXZO&ZeD~_7Qyxz!i}*9Q6nJ;f*SZ&wleqHQtTCgOZ(9Fn%^J$|qBLpC>@D1g -zQ`uLm9$wnzUB2&x@s*8pPOkE}Fz?lZJokrZdevgJqdax@Gj%dgYQ4SuaJli@vG2XZ -zviwt9!fg-Y3S%wD5o^zdzMtQl+H^hXUR<`3bdtWP)DRAM`7=m7!YG_FbgtArzw&0- -z!_Rj`&ws*W%*yr3Q+?ISYZ$?14qIKFyeFOh)^Un8uVRAEp$Cti&ReHb;9=JEW9p0~ -z%jCd~g<21b!n^7ZJH9iUzM*$_v{flBggp9@)l}!EDQOejA02MXzPTy*-ouNzhWiDh -z8t%D&+V*HHi}YEm|N1KU>Ez<5yDhVRuItrMdkW318y&u$r{nN?)yr>tr1e*lU5d7D -zU#mF$Jmq0Q!~~(?{9v!pwdvtoq^pf5W_4J*eOo!7G4?&{(<|2EskzOQR+$bP$8LN> -zn&8&Eaa8_+yVp&udNWqUg%&qozEl>nF?m?t$egVkx<*}|bD%DA#oHx&8xx6RHiS;y -zJ&HxX%iKyx-|#X1RMV22tU0^xcb&PF>9_Jj+i|b?c4wnrwma$t*%n{eFzz$?!kQaF -z$NoI!^iVN=;=1ba3wt*2XiRpwsKd=|I*{nHDx7|_m0m90r*Zh|=r3KB^j1IPcAeO9 -z>MQ0u7vSDpiI;1BG@UTb<#wm})I@&F3a#(^8%e9*fOQz`zwtvkF3i5>AQJ|YhCT;WR!u5M* -z7aZ@(t$m)KeI<6j=;sOY{I<$F-Jfu{P5bqB=H8K6H1dZ>UY@WiHfv+mz20HDlU`m# -zPQNR|`>&ncwtvZ{ksVLBDJ*rzA}#y9T->c|Q=Dv*t$jomU-&|I=)p-;YWWuAP+ -zjKyk>_d6apsGqpeJK0yVzBD{~G*QyI)X8ng200{Rfx7$9BKGI_8>V1C4kw -z|Gac)^vR1KSmW2OD=?0Kad_d^E9)9ud89y3u`4Lce}HEk1h8K1t-z366Kwi`^BsTdBT#Zr8MR -zT7Q~y3_rT>=)SVsGYVYVx7i$UH(AoQF=$xO{E*RWa_5HF#GD-Vto+Lw=^b@ -zG@yu~%$meWjtk)#p3g0ZFPt5o6iB|f;y6v+Amz5H?x@P~g+zZ@$8Pj#HO*b8J>^vK}B{ucMfqgO8mVOi-*&KP~N$;DZGdyh;-BlZ!UO9_s+j!z$neMp}BXY}X -zu0<_@KWtwEA8mh1dOm-;e5Peam}`dZ+ij^M?&M6>CY$JokD3;=O=D=pgIJsL{hKRZ -ztiL_PY5wBU5i`1eDxSF0hOBPbG$UNwb-0H00b#^VgF>y!&R5r5#v~=ZpEcd5E=(#ALfQH -z7n=3$Y5(YKtutYjEVOglh1m^70o&@6>OQwC>_jaO`b{rS^YV(=fpd!a&RKoHQlBc% -zNLIN407BgdC&xj4gtvF*RPGqGMc@cN%Bu`@DKc{=MNZ3-bDjR3f5FGP`O#rX -zxW9Z7Cl>%m*RaS*u7iAUr -zFkF6kPUg4>c^-ERA9zJvex!VA9+w|9DKS;lbN4|E$6m?O^^H%&DyG%+tH9pJ->i(&-3%6@mGu3R|lTx7@f$@zoXY>5O)6=Z==(bQP<7M`QPJE=r|aM*x5jXcY0oKxID)vb5d4jhCCyW -zi(=X*b7E#rL{{QBFwj>%IenZQ*u`m}BT$~6nwG~E34TAp+v6|{LC~y?{d*?NYlBoD -z?z^I~=BNIC8rT2PX(B=sb0-YY?(c^S$^i4TGSTVWyqtXbU*{DF#e)Cl#Q#9re}C|} -zBa7nvm9;a6vcO5f(bOgq)h6&cR}O)J52SRghwO!Iuk}>Jr-v#XZ^tnc9&z -zBRjv;KbM}0e^>mu=iH$~ytMxN0bz4xlXpGoy;lWwZt1#rWB(iSoo!}qL}r~00)I@b -zlHHoMaB2h_f1>uT`jI!6R}#3DYYx3Tab&IU_L3#qdT|Mv@|_jchu(!e(%4Nu@9ma>J-PCTu2*`djqx$=YBbnXtW2a90_*~oFZDkZq9ygm`CY!v< -z^8L+{)(gixNl)HODnIc(9>=pHahAC>={qpF&R?2+;&$c7_ltMb&S@}vXYj$H{>iI% -zyzo1|_&U6YS_qkaXYupNt1M{JdA;4Hhr`Ij>>gLiCch-7O@hg=tdVP3=xmm>H{eFi -z=auoHGi@H!pL5$3-By|y(D97+#%Z~*_R8?4eXetodKMh}>3CMR*r-@nZ-+(NE5+STCeeJWdzU#h^*BRPs -zq}*jfwIF>Cf7`ywZzfGe@7?CUG`RAX_SR5MaOV7(>a3ePDQdfKA==zm;}Va1J|9Wn -z?ko{>xopc?HT8^j+sQkVHxes1MfLDrmB?LoY^sRz3jI8zta7Jo{=WB5KkHooQEES8 -zCscR4*y6Yr>gM*R~po33lm%-Lf*WX{u~M}~p7 -zZnezXx%i2`Wl`UwSi9Qw<6Qx=+ppvoo8v+n*=fYKHQA; -zzMRo`AV1)J<)RHIn+#<;kT;LRHc<{6bVnxOH|%{`@u)P!T4J|6*LtV$^ftJ3CeJc< -zTm-Xs(T3Y3^9JU|X|7{8HRKgI-S8N8Y5SVA3a9hSpc60o`xh^=c(7sq`m-#{tLD4U -z&99J&^ekUIIig*(NeB9wPAfeb%Dr=WlFq23YTU{rjm}z|?;LA675e?_RenTu_>!*+ -z^UM1$e3>`pZtH!dH0gvOgUXG&KK=d~_O{bwR*p$Ik(K6sT;e)0?rbSyqLp1xyP+g0 -zt#jIxKaZZ&R5NtzZ7{7+Oukdh&gMFiCC%YXg -zJe0Y!t+aX8vbLE&tDF)m)QY|zNcNueXPTCLbLy$p7vX0g=e!IF+#@()abkpF%COdg -zITJ5tW)h#`LB9eyyzj2bjz`|Q{;`(7_^ -zp0%`c+1jy*`dZYom{m&*VlI?kygs{ZBx`WoC<@K%LC=gti5+Z#IlL)zpid1FQphuDo5ou_&@tk>+_ -z_=GdeqHC4(4>Lg)w&A}{=EI?!@ce%Wsk?X?m3bCLH#wUYL}s6 -z=ji)zVg==CM=UbN?c5qpHd>%bp$Tu-jVaWt4j<(PxZbR%R>?n;?!&@je&p -zIo5-swer%iw!|LyDoWYoN#`XOk1x}9yGwgnA?JRcm^;I5^9qw@cZ2J*!+#dcBz8$j(umopjhU-tFG{IW&*6VDPJ*%Xe0- -zT}g>)xpFx;zN6H~)Mr0`hgYJFFtPi@+n85ZHZ1)z?Zl-w-8--7<ae`-WOcIt#UL!-s^H_9_7qQ`J!2WW>t8(82&Jx -z<)!z)T8qe;?m#ZPE%&~)iDPq$Jmd7L=dD`HNS1LmMiX30r(|u2O%$vi>WW;jx_`7g -z@a4kRbr50QX+mQ;CWS#SwHLK -z+v@j`+=)gJxCY#%CBHOToYHV-{odu0?Ip>^K`*Mv -zGh$>%on@8j6FSnmO=eoU%RhrXNnD+DNgC*U0D-QW4CuTm$%KbL`Nb;(m -zpQlmQ>{=e?>K{sf8+8w7^X2UAJnl)yo$u#KsHa&Q)MM(kc7OPCJ?jp4i|5a2*^57X -zM8e$@V>(JhY>nTv$M>H-vob9Bn@a=Nn0(%ZA}m>WTrj%m;>?mMQ55aMEuHgizB({d -zzOcL}zj+XJ?0R;!Lr3z|GZ`6LBPZ-yHYTL&WE`h;%=p)A+oop|SMDh9UDY3Sx+XC1 -zapjytL!v+OyT2Euc~vF%93FP_RKl_7t9Qq_jxOZA-uTYvv9+s4dk00&!|z9dg=q7h -zGkceBc(vk^*^bw1Hf+y{W4rK&Pw$G$-lt*ISH+hcOQ6Soz`b*OwLz3}aryPhef=b| -zqWtFHokH;-r!X9Kp!2wr0iW-8M}Ls3hE{#$DT(O -z|CPZ2R(Ik$i3Zn!V=h+J1LYGZbDf<3+gRa0k3;yrAufsF!A@9}pO*?cMO-YBQa$+} -z;*Nn`2PvIhez5BP&#{NnSq@kFy`l2t^u%AGnX{|2gFxuQbrHD8F#i|$(HVKUTr4Au -zVjZlYB8}wlq!}2*4^B_c<&ME{C;RPzx!9ulV64;NwFc*#m2)Nk@m!}t8yVE<^!F6D -zlat{8G28q*Mi>YVl`*fqGT^lz6!#8_e*ZDl`In$}aBwkd*b(Y%Df7)c-ZN5n-Jya* -z)Q8mBIxkD#wny<+l>YRL3DurEO)zy+#GSe0`ltHiWK!LGyhqxmuf%h=&#hWw?sMqn -za2C(>)x4g>^$Sv;BNt4)G~b+fdF)wItbEJbu`_O~6}fF3U3RGmF3L5U^mmaI3=Zmj -zRa6FOh>nTniX6Eju@g5Vf8s>hZ!{Vl+zn5h1du<*dO~JKx_q1sSKv4hPYs$dIJWhb -z=b|j=`ZpFFOyj6@`4o9hc#b?pj`BKqg3~We5tg4fF+Bq`34f0leG>B$CuXK{2lnLj -zE1pF8EfggMe^jn~I0y(;ZModP3xY_si+_p`RYRQr;eCqO5#`3=U{C+STNRi?eeh-K -z+I1^aBO|+X{=CpSrC`%J$2`4)caxx(tw9Gi3(l%JR>Eca%s=MNqx!Nm(#yGCFwWm+ -zX|%>4-r_N?f6ksHl{L>d`LD&aNx#)?4Ht{oA|DAkAZ$>>v0mFCV-M -zTD_uE7lH^CHBes#OLdcN)lO(<*YR=&$anod>}tv#qYHTl`c=AcgGuyOE)jEvMjH~z@Vx$J_> -z%ywM9;`EdHm)-g=SA~pqfHveR%=NP;z1(biyZZLc8+v1^tBXhM>~HSTwdj7ETA=@N -zS1NPTgDJN+e3y%$wrZ_wDbL>>EVYFWAB4EY@20G|4nyXCMXy`F`gw>xw3yTItG;gE -z?&|7l8OJ!|8+Y;k=VBwptfM{a=TALy|FqAy2j+q~>B!-xjJ*pQ)J!(4U(uPI{i37T -z@KLd0&-3Sw)Tib-J%;P8O-G94abuV5yz4F=-FEK5rRJ9Y>+QvQUr9ylCoHvI$bs{> -zQI;I`C!D%~gZxYo^NeF@gx-Cba8Drj=(G{zmv4BkSiZ6Uj<$ogXI)d!hDUdNPQJ7= -z@%-^=bx$lJ;zB`E0#&M=h73(mvn#RBf*jhkdS-H&jHEVd8`8-&dhf+Jg%E~8h$#vTv{8xC;asYi=E@I -zCPrHAR?pXRp1azw$andr(`h~pLvQICEgbIsd?W3W_6y5AQZG`VH+1HVg~t=p^kfU{ -zJ4jlygO2b0qcY)Z;w`6CC!gxMzvnimQf}4?NNz~6D}lVCRo*EX8EpV -zpGTh0srchV*7qiA9Iac_P0Y|dR8nr6JPqq(lMZDz+>E#X0c}wyR=grTn_$+^EqrZ2-q6ItD -z#%z~9+jw@`s%)zFrL9d7(Us>a{ZA84(dU?a -zc|Iq9Q$cI*#@<qs4hNbQqUUj~5f6M*@-R!$jcS!}wLsMHv70?R~eRk?q>x=9w>g}Q@;3mvY -zp3R;;VQ$T)g(h8D8y+svxOM9Myws}+dAF;R-;7^7>CSlf6mKELu~YI=FfC)ZoIT<2 -znTGqHx2$T6SskKzwuIH!1PsX3(?bYm}b)0n$*>$cQyVAh!h<$vter)#MuVe1k -zZjUC#W;S*=DiX#`IC&_&e$~Nibqf#eYivEYQ7}v2VnOiav>}os^vPCl@7=n6gZ5_e -z&HhITZ5&icc`E6vBh*H&z&-QLzI -zzdY)`^B-U4z0Q>gB}d1U-EKM3`ory8z45%VLjgdaUj4O!bxH -z`A_ZNQW3ACUPopd6JHY#P!%3{EltCbS|81?x{LS?o(nV&3ievO$!{7nHNB^g+)7UL -z&D@%t9G9H?q_8p|GUjSpL;B~#FZMWP2<*N|zN+svCx+yWMncHk~&2_4$3zjs|`R+7-Mij6Q5=<>6|b71!4<3SJ!M -zd3^c9Z6A9Dk;14{H90#)CuPz8y7&BgpdmCG)y_&Y>W5@O8{d?lo_k~5@KjM|^ -zbA0anh4&29GfgKByZhPaXv~Zl#h!B`)=Z~HQpGw8GC$nA9BVz|6u+hQTH=Je;;Z8B -zgU8>lyuIgOv0aj`%tp-HZ#^Uagl+T372ij%h_~+!yyFx-KH_k(Rf<*S^HUDbWo2i! -zp1EGR@EzmM-I(EgzHM=1>eKaSw4=21r&FfSnI6Ey*&f-`uzPY;>w)3>jrV8oNk1@Z -z|J|`ejxBC|*D<52Z&JQ({vY}87+LCfc75CbZsNuCoi6iStBc+TjY}Ta-?iYtnl-F7 -zy&M|>vY%0>gVv!UtK=u6=r_y+qC?Z -z)r8@l>uraIy2!E$KAc@G%uN+uIKMX7O>*x@-8aSe7oQIGdiN6fJ@HL>mtH^mKBG$^ -ztTN=UYnfR5y&acy=dM&+cl2T5<2#{? -z*XU+`zjxTJC2wk)dD4^fZ<}s!nQ~*w_U`R>i?aDY?w#mcsD4qs+<4f~y;HaT-1#t| -zk8?XHucI@47FO21G;<>9ja?Ql`b=7XJZ=H+!Ue@i&tzc+xq-|-=mLb+9rN}(@s-V{1G2K*0#m{^XXTk-o?GYv!(0h0o`X6_pDo7 -zf3Ey+uwQodNavij-)?+c!TId@x$tv;Pl8iX_J{ayrJsJBNuqfD-CY;{Q*gT$WO3_m0lL@FC>3 -z-8_#s(+_wK>59p}VO%KL{btOIHCqg>HGi6vbx2{%-Z8xU^0dZ_n{Vyc*XkIZyN}dZ -z{dn=>+b{N$io35RHIBUbExkuPx2)h+Ltkgn$eUdk4`2LVQP3M>WaV+M?BSs!Zl1fh -z#U2>-`h4#03t!%EJL;=VzYx3c -z>y`Q_+Vk7}snHdut~7riRkoPi0)nBad78BseQ~08!yfMjjo@HA$HQhT+eE!cq1jQ> -z$v$@y^1=#n+eeM6j?+A(S7gzha`jAP+TszzufKP;+xn0fdGWoy&CZ8op858yjGH_6 -zK159S#>nV4 -z3At;;UwGq4Zr9{{t?_m~p6xD*pYWpfr04$-<_(U?N2A#R@Dqbr%~>GAf;?wu$zMev -z=89ZhxFV5=>nsw0HUU=x?gD`eHXf}DorPSXivWxfbA>_?S15Mmx;i>xZD>C>M*`M& -zktpYZb*jce@%&?dLx^22hHLsWIdoSyye1;R6J*wP^ -zv#Tq*V+q$uD0Bg92t~go3Iw8m2(29jE{?z6_&N>L|I%U*`dXR-IYKxnVN&AMEJ6|b -zVj8}RzMW2!CnnpsYClAZXlf(bA3Ftt&4AD=8CJwV -z5RLmo)LTGpRJH&0k&rk9_%m;fxN+1Yt -z4(Xxo_-T+kek>FL$|U3k6btT;K=A>k5I+_^j`m}xl|Tz1CX@`30aloh6dDCk8Afym -zbVh0;Aumn12n=I_s}z(apx_`+I3J3~%4ATJ!kP8f_F`!hoLQ%9AZ~)Q>v3)-OT5@{ -zHkj!MuHm3$fwBRVW1!pzg$SvGt0yS&pcI3G?{9-6>I{v<%>Wozr>TVj!|Q}16<}@& -z79U>kuESApg~veeKtaIMW9#+V=+k5CwY9N(9g~U8h^ePg(S|J5_Oj6Jp&hb1Z6%OY -z$I?~-s^^Xz08;DtVimx!#DJ7K3LOIy2LO4UP^<#1ZB>8~7i>~q$JXa4hlY(%&GGP5 -z0s6`ZB-U$T+fA&e(b0Q+owWe#2&;>VRRZ;{L&T0=I&c^`Wf;I>7{GQGxR!u|0Pal& -zbV&x#MR0w9LU3|jRxF(&E;gs+8hj1hI9+g7 -z7ks+YmUFt()#WryJLP){Y3gq~c!#1P4!aBkP-v|xOLLH@VczJlDds`HFX#3 -z9@J6W>fYC(qoux$;6NN-SUrjpg@lcYABDIav*lzSgEt&Ia14<&YjgNV;FhDhoYtc{ -zoTIJUoWso{IR~3fTMR4(g4GWNxjWCoHp466vlO|>*BFr(|F-ykan&3R?A(nuX -z#tP)rgJRFp;MlP&I6Rgs$CjnVxo`c>8o6MN-f-3|CTBg@hr?kRb4~Zgo>d9&@t!&^bYEWs8ouCqmHQKkPFBKTtCu}!>iF<33yi=;tE5q;RLt| -z%&yT4hJqu#YqVf+kMORg2zbFY$s@hzEL><*vod&ObWMp-OcNQ5iLQZb;6*Vt@J?obSF5n>8 -zX!HS)bZF!dH&+*l*jXeLI63ki?Cp5AHr7^_7UpKACR~oOk)Z)wUr$#@o28|x!Bl6^ -z)o4@-nM5SuaR?0A!8Lk5(Y5*{o`H#pNsPU!&i=h#gR@|MG^v3!e;Z`*FOv-arEc^u -zb>m<4;WZGe#^21x7hSj(@_$wXY1P228VG#~tQ88Lq?+WHJl@PNy++?BIUxbe@HJy{ -zYy98yRa?QN&_GbKwxZEE$r_BV*ujs=T?hAWV*dwjV7-KvG7C7 -z_!?!AhV<>#rUlYzqc=L15Ydq@{%58lq1P341O#y+)c?166Qq?b^y0G%=y1 -z1fE*5S#n|=z+58OV=aWU@JowC@#u$^gfwmq9xRSs3_uIlFO6GnhN8nSEdgB3e8H@L -zYzNI0pXf4^HiH^XaQCax;MD|x5dj5H3~=Rsdg)xWT3%kpt=TvtdT_4^dX0&R(E}SU -z_cH?vg2nvCdxN9s@$K!DXHp>`Ibl5d%<+lnKK;gX%PZvAUKe0{z=ryz0m3BypGK9J -z`z4$CB_}3(D_8fakw#)SC^9x0J0L)vZ;Yx*H30PBv7Q9q7!xJ3fx!Eq8#7DvHBgfM -zS4)Db1vK~#baK%h1c3!=xZ}7r(8y>rFw_OT$X%fFaV{A1n8d(fR`_4DY6uogGj918 -zs0L0jd;8}1mPAzx(SrE}LP38s|Ag{#e>1Lsc|v(&Q-4X48JB5RUK<=-p5>PSo*E9+ -z5okYLVNm10Af|?ykOsQ}+@l=eKQh|Wz(gZvpc+00kpNcSQAPrc>jG%3LK})Bxyvu~_^3b=6O=nEnxs$?sMM56~^;2i+X!)hN)xbLcn{K(X -zzedX^8fSpSC?UiEheA{ykaYJNT^@j~dF6lz7tLyzyc$Aun}K@_m#G0X07|_Gv)~c2 -z(OkdsUtFMUQJoa!1HkHLiD~63Q9_yipIcG$p~N)Gu|T9N0LDv_#sj7Szr+Qo`Y|bI -z*7$!lF)%6DFw^9^^3km#K9SLbpE{tJfWq;vHG}7ksFlL=B4VSPnILeQ7a3iLz=%(R -zcTBB0=!vyt6kd_I(qTZ?l7eJ#*XC<&ep -zlfi6P=peivdIZISZx(PiAHgR9_Q6bi3zP=9tp$IJq(^!K#X}=N*$p4UA0%9ZWC#H7=~RZUHe2(IV?bUKp&0hM1* -z(U5@qDJhLcBMyNe6MYpAP6GAzgXlOyGMD)obu9Y?8bUsg -zr|Ig-#~e-BTgcPh^Q|VRf{T+QT*fTu(`$oyGDv1(0-I@=X_yFfbajAFNhE2BEUhdp -z&CE1}0^p71obB;F6Mk!-tRAzBt9f(KaihegCL=jyxMAysw6E$xtX*pTG5v -zGrZkP&NbBSJ8#Ul%|kKi*8dP!kDm&uL1K7$EfLkM&5&CEs|Ix6;DE2B*s&p)PSxTJ -z#iA)A6dOz!20S~_Z;ee1lEGZL~SkI_)!T-p~)IUHQ9zDzC&4X -z-WErm3EA9xgpRJdmBlf2b927Kvh+SYn$;W9{~WgnKLxUa)s%cA74w;%OoXYf>#2q} -zHR75$njj`do)AUd)YQhBP1Hn5Pt-w4Pc%eHPsB)11RR2qo=CK@{+;wSv|yASP5WQD -z#Iyl4os((0dcJi%Cv$Q%kTQnoI~j!POATU>QBTwx-8?nr#tS3xL=`LP#7TA2lfrZ -zeI>FW9hj@40iDJqdu!ntFifVCfduq4qL_w&;67+T@ehC=Ott~=xdH71z!NDIB>)WT -z=`z&@=BQz6kcN_rPQ+ASQ~zmIujrK&hjJ)7H_Rtc_#R -zRokJH5mc7wC^``~q{C!l5~^rGag8Y&8YZ+vbO2V@K=BV{>M?0#w2j6F8~7^KM<>z| -zOwzHjbOh7c8M>H2p%SGe5)dO)r+Q-gR2E}mrtwQufHoCsVC2!peQip^%Hn($8z-eWfw^c0B9ht~l=jZpRn?1Gf|Z -zJ-7G%&$*raf6MIz!0qraZioNBa=YL^;&$!-fZOr1LX6pgS1_di86EYr`6_kGoXbJU8X!c|#ptp4K_K6-}Lq{1K92X=`WCvzrj0lq$R> -zupuT|+zlZ2#941~k -zHrUyFsTN8`0w!JrJW4u3h8iYMfN&Z@8D9qI=IV+G7I4)ux4f;b;ENu<4M2V5Hb@41 -zG@dLQ$Q!J*DzFg7TYgi-4{K?G55H14BL;yAdm`&+Hhe>24>sWhbXW?oBNS-7R6m^r -z<49z31A=3LGazISm`uWX5C{~M@8~GnD0m7EPsSl6(9(!DkVvTWN?}PzG+jeCg=dT -zEafV>LXU{aHj#z-dzfq!^)RpRFMm&8@0UDlsP4>jsH+bgeUu|a`cll5mZGE?!Vob< -ztRXtV3}2=%D_95H8Q39J%>W`}`qfmWTptV=vREmItOU%+!$?jX(0HE3rEaL%xoC%qkDn%B;iT-ly@EpbaM=up$V6BIbVCISS%a(80 -zuzcwT#6j^)aSk2|GhsbgtT>~1e)Gzeo0o51LvcP2;M@sFo*HBX4^`n@lcW3s8m*2WwQZA>n;GmKF>pn;Og`R4t$ -zL!d+fRYXclr*tb&5?NZ22An)iTxLO8=sIaYhWC=KClp^#jhX4GR?`x&J-zi7A~zKnoEjS-jKn4qh-3;4LI5&|1Ok~t1!zGNCQ>FvTLM80bq0sw -z$w*+}iF_=I0y_nJ*3eaI*FNxrWJ*bc&Cs7v3#e2r!ezzN{fc*r5Bn64+W&wlYhdPK -zyzIvoob2ZoyzJ)&Tq0V4OcevNjUwDe^GYyJB~uiFFA;|TM;PGyr_>TfCmtdoSbm0z -z*(zn0hKNcGY6rj-LMfoqa({W1pQXB(TS>+odTbV~rgAHlbb?_D`7c@G@j8@Rtpjrx -zF{L(mH@ywO91lSOsU^z#_zeUEZ6RR(H6a1Q1Oj4*!8LFjd=bXOSn5e#0`5OC7X^(f -zP-hB2n7F{0mk49V4Bbl@1qRlnt&XNH;mLk&j3;}``;3axz_w6_Q7AEU7Q%roE%fWx -z@v{S9={gL4h@4YHbV}7r#gv#c8U+ba+&O24{8KuGfu)SFNvdR#rYh{ZgC*7Uvw{aS^`OLx -zySso7>JYzAz{m8(pahFD!}4FIR)~WGc`yMT{Xa{$fEB%?O}w-y<|9m}Z>LfIr`Z>_ -z*kDAs7HvJ1R#|Q0&O!ZkMGWoQOyMg2jgxo*ID!zcenpa&!HMwiDk5) -zT<+nF83_-fi0rQBZV*EBBL}G!!9`9KsSxJtG1MhNM({uFP+b6nP5ug=*lasC%I9|I -zUpri|Y#zqpc9^xc%XAh9NCsTYq;oL~!^KQG7c=QxY!Ej~G!P>R`M*L`ciHcomhn`< -zDy%UmTLbG1#HwPkGZw@m8oyFI5}^hYL0QHbXYD^_$l|R<3xd}4b!ZLai=suNrX)rQ=Ui#S_I>woW^s?kAwr9sw(a=E}{bbT|qj5TgiW5@b&V;{OQq0bxGRz0ZBX -zQ$bB1DDb0q;DeLGMnAT|MnFAJR3sxEz~j({f~3wQDV-^S(F=(Qq#8yq);McIATE$F -zg+4`t2wZ42vLlp*{trr>!^Cs(2%axPr3gI8Q|S=#c(9ozqQTTuMJ$LmOA}yB7wLGq -z|Ia^ao{m`V=^h;9;l4Ulk$eF@4-bJw@U;unpku|;6*so-Q9LP`{-+<$i&3L!w8SZ(>x#wSA&6arG^t4W1_t3ge6%}es#inoKS}X<7g)* -z7YWXqjmNkh%Qv$z<-x}EHG9Acv)P05&Fz1M_w57h!Pij5Ck4txvkS)9R6Qb}t*6bm -zB-xmWNv>wY5I;@~5~CN%3FoIHa*o`79Df=znN!LsHPg{&>Fe6CY;=dPhUgMmy1ETK -z2NsX#kk6~&RXE^y4!SHnWVFNt9(+_B7j4IJiEIr6Crs<>NSu^;4Fo;rXDpTAgo*|B -zlEMjlbpcLoZNSE|+6-+8TN53_#ZcyM%7UV -zvrbp<;bH!MIy7wtXGvk;Ob3Ao_0hEZUm?2*>mdWENorvv#f%~s*bXw8j`yIF2zq)f -z5ZZeh!FmvlN#oLRw1Hd@?N|PdMl+()iKsTjUTzTSnBAqbN$NxnkqbN+oFx|U4`gs~ -z18;f&@KMQsILvWFJ20b!t2R_A9iBku{uV@MOm__3GF0)PHVKHUkdMB?fO!D3&Kkh@ -z0;x68Hu)<*hKdw?YxUmZ;ubhov7N}$2=Q`=(xN#}(mBwIOjrwhDUPk}dpj!5+RV&= -zvP)e9QpWSh4WKPFq4iP&h|Z*Q>5g=~8buv~it&qq2>Vji2^2E+5{s%S!F)4yWz5D> -zBVh*zF=p>U6cHpg1r3umu(TrPUr;d5f}(Ukh$x^?o}Yrr6{w%a&QHnEQW<4b+wzoW -z2U^3uPbL5og(j(BvwzJ`=^{6%+I*9cl|C&ksjC8wj5Kk}i8ybspU?5K?PG#*IP`pM -z2qGB~E&-pMb`Zx@dE@T~yz%$a^(aKZY2r+A65JTv1Y9AAvGF9DKB9x?A?A2XstY2f -z1|oqJf2tZCrXhGdLco*NXl%SS&I)fsv81{qBD{neh!3C!(V~$wB$YfBUyjVh-@tbf -zx~R_y&&l85Z!}{X8BI!1X^5H@*cX`sp27lNuMK1cTN5ma!=PZ;m*7VvlW{a00iqCb -zFablmltR&vY=mlnUTx~XI)Hr4FOFy6(D)cr=|nyc^N?j&X3yOnQ$w%}c*t#JEdPMs -zc(QB=Rl@_`GK~I6MvC(Lic5+cw-mF_z`(WdgzbT`z0}LcU;SZ6xb(vofBlCs{t?*q -zFklh3ViqB;8PWnQ0n7mo-6IbGL>y5|8>g*h30vYUwJfy*-~jEZl&N$zO&G7H>4`4} -z+=Qobz$wlvfbZRq@xq4VQtUJK;xYhU(QC903r0>BX8?E -zQ7}Jzh0m~b-({PT5XDPHqouXNj_^I-bClwu;`OmB0WN)I=IraryzxWa6TnUSkR@!b -zq7aL1j*xYAS$dGMHwzB~f0m3U2EIx$*AmiY!Ig=1om9g)Znq!2@v^FtdV8ml!0~l;&iWu!=WLc?5@Y~B? -z<@;U@RrW&pcI5Zq15<_?Lfl|i6!)aXPRg^6avifqEO8I<8M-e%vd|CEDAQ}{))^JU -zR_0hZvb3~Af`;8(xV$SoD8j-De$x9BDK%x^Jom>XA5=Ec|4l5%bf=+IH;M|oAT9(k -zH517sBv6ZyVnP;`f?708{oqm4=88h2C{ff{8g4QLCQuO^1;K0LZSc-`UwkBf0-l7& -z@;7*(VJSG61Ok4b><|{3C6sDlxWf1nLR4)1Vn8@w4G3qi^8W&mno35WNewX3lrz*B -z;S7X;VH~{zwXQ`Pc;7%8Fu|zsN;S~6NCZrVap*dr^%Q1^NXhg8LjXj#;`tO3!BqiVONSXEeH;x_|f8E -zIjs<$PEkh?Trv#9^;im-Atffp45nbH{)@?8h8f+-6u;oZkmX1ojfWO~5n -z$g*=xOa|q+W!>qO?U%h?hvUzh^>K7?~1t6#fHvSARM3$Q}Z(nGWWHLMWU!N>alvG49A+w -ziQ}~?PO+{=Zn*gKR -zl9~XwmLOHWP9cjRDmKeO*VtI~!dTk@`xgX-C2_!_0Z_e)v4X>ZEp$NN8K4LSz%z`8 -z0B}+1f;c!ht2>7|1CwrnQo<4Kwb0(ETceBUVi|N@H5664YUql(40JhNj5xa3({+I= -z#uSGx_PRzxqRSr;HGn@MUp5eJpn|B3s<4OybYnpW4~seg&ke*GKG4@?4W1_dXvOH=L%jS^DiamSxUayUCPAm9HabLC -zVjOWI5wDN4gb^Gb&2l0nsyc8vT}d&dbP|q)ra_4W)WEUPW-%d(kV3!_*mxXX57Nck -zK$duvF<(h34B%=pzy*)ibpID~Zvq}wkv)o6)!lmU+nwIK(@Q$tSvr`Ig%F6OSpo=z -zAP^v!NRWLe1Q8Gzj=PS43nHTuLckCiiHbNnGT<^Uj0!4lGlMcCKgNZ0zf*N@ -zCqbS0&wKCt{@+WdZk>B?-L2}>+3M6O0FZ+o5)nsuffzV~IbP}W_EIJ(x|Oc3HEXe1 -z8_?!!r)igHg>QKbYI(gEY*7-GRx+md5gKl -zESSv&`N4v91%z2R>(8l*SD~!*H%nPr_uxr+c?DBGdr2TY&H~KD`D?xd&K?t}u*Q&P -z8&xr2!wAeBWZ*L`TW< -z^A%yLo=FH&NhwGPrNsBd!N(pu_yjlk`mPwM+PL+4F0ZR@BT6O}%b$2F>4&U<3Qkm# -zT!RqO@5iWre^1{Cj+LmAE8ht+mKK-B$Lg8Wjq?Fh#vNdgf*a~^BNJ;0rv}S -zsx9OkRnkC`OhdtLeNq)-&#N-7qNF)g88?yAG81h-mudB8czHtD_zvB|Q`7j=4f&Ml -z#&^rKJfms6L2ZB|Om5?6>V93O?V=Q}m;6Rqz_Lc5_|VHkszhrk;yXX?T6_Q`(#Zz| -z{>KZ#n?KFsAA&lr2aMXvg~+h3M~l!EWt33$??qGU}VGvZns3cPD{Fb#p$lo_4KB!OIWCvPsoD5OBncP69ysF -z((>a;=nF%Vp9ph=v&e~Z&@1tfPAk|wTDyI!w#F{{i|r(Y4ZVHnqQhsB{HxPRtw*LzV!SzqGil2g(;}JnYs}cph@zRbx0zW;%xpl{ -z%oGT-jmdZpRmMBT+S)ujw}+%bTi2&39Mi*~nSp^~ht+N)DXR%7DYhzBleFR;sXXgId|dqK?5t!A3OFxLiDP{#~u3; -zAI_LNWA>~WIODm4*f)E|ocUc>-Fwx8rj-9W>gKcqA9c -zNC_$=MrDt)q>Nz8p$2087+MP -z-%KD2JsuA!2E_i>e%_JR;odn!a1?7eXR)L)+vlQdnBn+TiZq?;neCwBVE62e_f_Na -zoPaqH4-luVx7DQEj-na|B;(HvC;rs1X;1y5?p}tK+7BrN%K9?`CG>gXmd3C5C7!|) -z-uiapmPh`yY-x4d#{2HO@yPf&bIF%)B--mn6bNrtluk>$^4`4x00;a{_{ -z>}kyR6i7XKGj1Jjo`R?Jm2=pH&WG!CvXc{o=u-&DpKIA6*) -zRT<)08c$hI%2hL85mPvrHVZZ7RNXz#Q1V32Q{^`LAN@V0U-2%$7x{ozoKO7o*Ihte -zrFLHI@=dg;-)+m#u14F -z$(;jwkN(%U@C7iTByMwZ)Pd-Cb|J%jht~485hH(Acj#aX68WdtS>?E -z4_B1K*NuRPa58M{2ec$~fYX^nGFqpM38i}U*&gvjShfi2nFlTvMXTdQo{WeSxJsy8=;tg8 -zj}eADFAGm}P7Pg=G0WNDUGBWbdu?!CMw-oL>TnYh>A)H~6FVNz%caq%CRE|8_7NYm -z#rqr)hmXrAxRYSQMm>$CBtKT)=%n7V?6au=L5)!Gy&zWh>lxfud!6b4q}ZD1e~9NV -z%$w0Nao3uxjBs6P$)dvCu`d0fk7gYIdroF#?tm-5tRx@4dF9jf&wsLV(3Jtd-)plJ -zSl$T?{OI?S@2(i!JZScZblUJ#(wE#$z5q^=9@pX_WE~wpBdYxj-jQ+*>fBDfcggKt -zZ;)crm&Q-}6d|r%co!Maweg6sUlg^Ro@>XNe1vzyX<$5BRc#I8AlJ-M;r*KwBTp!z -z1!qt>{J9ikKNn{{6!ZiY<9j(A#_EgC#L5cu;e8sRuF5Nl=vzTmQHg^RD@%xL@5;rd -z#6KUnQ~MCs+|PY+)8th@z69tzH2E2?h5lee1JIFpobRja@96944@8?>swvS|k^X_rOhW(L|9#t);2q9WLI#Z{E~?*WOjoFWOKVzq=H3UEJuw -zgZ9?VdGgY#nhBRr`p2H^saZPr{L;gx%;WB?nQ&9(xbfVg`wtZtWVSqcN=OrSQIIV7d*{JM7uGeS|B -zHf4U|zN;Km@%-`y$HthPlZppkb!qvsoJdwIC$_fg^2hsfeY>_~4o_e9$YrCd@ri}) -zLx+vckG^e*rP-aag1i;us%M9y0k@tccPEzfe6+OxPK5D`f`gkQ-U_`i7`KkVlhACm -z0`VNzfayA6B%gW_))#e)Zrq&2xA*K7Z%u5*E1;Sfcc7LDv?880fGg3A;wm*$xyhRO -z+#HRD`uC@${QFC&e}9R|~^BHd- -z6#gqhnTHXI?m{T*QG{|$2*rjVl&6DKfKZ>i5xV3mgz{G-RIm-9zKanmJcv*c+`HI= -zP|34O>IdihJq_suLj7$Bm5Gqx8_MAR17{*sek-KcX)L>6S>dmD_`S--GnUMxkq^5g -z9QGOcAA>7i6xC1(PdIb>;^p)}Q}Bwy!tQm}x+5OaD++!YH*w;Un#J>2tkS~1zg#7t -z1rRdmd~5!|G$d#Uw`+D -zi$5)pH-1~b;f{}n+RKNI+#*{yWHi3NsAj)ew!S=k>Ro@hky~=;=H$sm&y~q*Z@u(S -zrRV#9@b~9_+VM@*KmYodf1X<1wCkbQpI>n8cTW_JxaL`J(VnYzb^JZNaq_C10Y&aF -z1K+&&asBzDPnG=U^S3gW4E_GZPtUKKGWpD#T?Mb*visrP5A9vG@}y53^kK(0yPt30 -zQ95Do)CXSw?U9KUw|q0Xu`#Os&cA8T9dGCFIr*;v9M?s=en$B%tIE!|#3J>5Ut9A9?X?jc)4q^XoJ( -zv_@W<{AGOo?k|Sjvis5NJFj_lL+1AaXTmJkm}%SdE5^4UditK@ -zr8_=Nj30OEp;sne;j=H^+&Q^6wtmU$J71do)$iV)HtVY3RqqWU4p@o7!1b>OKG^&LY8`;OI=TMnrJze -zr^=P|`$Z}$D*EMdDBB;7)9ZU5FYWcr-q+FNseO74h0BV2_G$XJVOIw)n>T%7)x`f_ -z -z{fdKqiVBO_sx&lCYf1gD2XF25h5dge?rLe@g8seZt}6TgKJKcxi?U063aN2dpMRh6 -z&+fRZbGAcCl(rOjXaSA8+AyEpcyMyk|rRD!l6r@eS_-}%InD8nR -z0j_{F4&X=N*l1FQGV!aRE58b5;5ejgIJX_He-zH&0C}A-DcJ?(IskqI%5m%-O=viz -zA&@3Ps-cPOLW3c%f;0gVqR*uFgL>Zt*B^lWlaMyjcY{jwOW=D``96MYLQ -z35i1i*ycm3hBOV*9gwy{5|LRwvINpPNQWSuWoP1?v-Pf`cspdbux#)Ag@tT)y0TkW -z&vy4tu2u4?%awfCWlFIDO0mAh%Hd0fD0x<%l3OE%%{12;3p)opI0rfoiJ+yh#pEzD -zgRmMv+c-OFfkZe_Jrd`vdo!bjTMt2VV9*3Hnu!9*P7b#kEro-1BuTzSRumxLkTc2| -za%Qi|Qn+>SDDo-V4e1ahj(iHgPsk@|9XUl~)xx#{(pE@^ARUABEhLegg5OE_eM~+^ -zX7Ulrhg1P+8lEs(ZBYJmiDCv0U%LGn7JHz2)>3LwQHjf12i$6Mhs?c`W% -zbVy*ZgZzoSikwiXN68;p{suY1@;{PSSpFL1e#l=ZM_T;>G+0mQEZkS&Ax9v+3bpnM -z63B~tGp&K-U<)|})f0eiKBNjr)sUt^x&x9(4v~!3nE@-@9?QuMG* -zWk92s;U+J`4a~5ufHVzKC!}v7B_WAWIs;z(4z@MJHXl+2q-l`WLHZVw$X@4LNCYiW -zUvW2kv3&KVtLYwj8Gae?3z1MfUG~W_c_ep-51akC+Mo23Qsi&|`LkLymUe91|1abB -z|1u)&U@f_w+=0@eA-1Ub9j!m42io!dtA=^%b^_p6zUKq)1>1St8u&9D`Q84&JHDF( -zujRLE;K;#fJBF+y$ci5D^$$GrIxD&!PCVEexQ=G~18aRF0tp8Eq8zAFReN~sch&Hg|Eyh2O?bKzxkeC(lNKf7T~X*-@9&y((z -zrbyLNKdDg4lR{EJN|(HnO=H!_8k5GL(P=aqQ3GnZ25D?mRgY1AY7-e-6an%f2@Vv)Q-`_DeX8(4{cy^NDm;4EgNq0(mZ>^ZMC(?`Jd~DWpI$y>@iVZzcrmrX0Zk#Ahpq9l-yfS?!bq -z+#vkV%JK9#H)vRG?dWzqiIqb^{0}Gx81oNSPUA;GS`Gy@ex=-lO1TJJ59Kmx2IaI` -z6k+8eS}iNbV_I%cQ)cC`J(-!Tj5COuSeYhguy>i)BTyz1VPzc6=yg`+bw@KT(=vz^ -z@%f;L-^Yqz5Av}hKI~ydCiN7_SBu=-UF2r=Bo6nK@hN4Dr&48%r=U#izx|svBo@Pa -z2iDH0tDF_CYzSA*g4ED*%Zj4{(q7j3v|#$1|DpVd+uo;9pxMtE3o -z&%nB0IbBCj4-606gX$_LPT5lzpEay?V0<7A-CDbM#JJ+pi=K6J_p^$}{mRY8(VG?1 -zXN@TRmD8p4^a%Q_Qu?e?`m7Q05$sur;rO^Ido*ZB?a(?Ur@qX9uMOT%?GT5&bPz+{ -zfg#s*-VP8*b|QVOw#5)0(qe=}PxTqxXD~eleTtqk!C|v{%5~kqkhcTxR8Prp*b*Ls -zV#}{?x*EAE=M7W-Y1n8uq{MetqdYhLyFWO`0^&9;M`(0Q?u5}T6_-z)vPY7XV7PE> -zK`FWWyAb9yN41N -zkpL-}I2FFNZt9c{5SJ)>XsU$|Y{D_z9le^pwi2K^hUoXE((mIarR$X;|NIu;u8maIhUuy?hG5w+#!|23yWB -zxQfAB7;FTXhUyIlD_wJk1zT`Ku(D<7EgFs -zWs94JiDrR?E2qzF8FzVigyR-GbVm597DTZ)#)_b!>@oivv8-9dX=iv?i*xm9SI;hu -zteuKSt)5NAqjp{#k2-ZWM7Jy)H4UO08Q!pYI6V5+ku(x75V#hyp>|3O-jIsoOTS9_ -zK7|ZK8s-OEwBaG)xtr%VK+||Ox1jN>LaiQ8{6O*)@>B*lPn;4CwN!Y+wbO_B_Sn$o -z@vHW_<3abuXZqyrku6Ha?lGBFz+mhR%<4YJAXa>q{{J}QbjwU=k$ElJp$(hm0o@VO -z|F>dKQ#|QLH8`JwTVk=6+}z$_qzAG6l0JDW+DSOPR0i3GR-Y1_x^_Um>;JWI(7zJW -zdE&nh@C@fv=-0wXkDjPduT&A(nPO0aJc|seXSPe4%hj;YI5@mTZWyU-YjvT}e|b@R -z@+`fIWj&Xvuh}vncJcnei}o*iJ;P=WUYL&(pt_qk>n=L?pQC{T?y0fAS^V!_vB3fV -zltb$7MFoEuF#O-dG(SLIA}{^NsOJCg2fO~afx3kbxUqbqd=81X9 -zujqN2lfNPwjaf+u?L^PudFUB*2)%^Q!d1J`0n~=5f;|j9jMkvL(I!xuroxe%(Rla? -zaQtrUPPU9APh92bVd7PY+{1M(Dgsy>i;87JiiaSXR?mi2BiCuUNHx%yjSh6K~7)tq2 -zJ(`OiL>;&Uk02qTE?Jd4iX8Bam2lSwP%GLGzjpL{bOIZMv&qMkXOSD_q08WV+R&eH -zCzt42pQwNu6QGuIP#K(Af?hzcqT@J>UnENegHR~Mh1JP-kPY=kHSo4i!j)g(f066p -zw~jl)4^Iw3Ca9M?X>Frd&?ne~^KmtvL~_Uy@(8yKY2b-{;Wrb_gZjQ7?*1_dDf@|m -z9OEA2f5-nMrYBA%P0%7y^bq@WMOh6x*YIfJ-q!r=n?p`1Lzp~0DdRYr&z#xY=U19hj0zP7Jk>^ -z+wh}!Cw>aI!BdXoPx05F@ca+{i2z~~k$6G1rV4hLEF;&DyUD|J85MGz{GI&3Ik^lj -z#+7j8TrIZ*-f9!K1%CUuPk0Z1j88&67Yg?ZTZNs%?}V3xv!X$|9yG`|E8GMwF@gw+Y{MGy;{FnTfLY?qO;d43R*t>-Gaedu;l#JkC#$e#e8 -zr=gi#6#+~oJMm3qEp8*3!b))<8HmTCvp|;bCP&Cta-Iz2s_Hp4#Zk -z`Ipfd{vdqYpWx0bMFYN$d@CAID`xgz{0djV$GAVD6WmEG@!Qc~cpY})GvrBb9ALrk -z`9Z=I6yhF6&vK1;E!sya5z_snxfSquEZzllVInTX-*ZXCk+Fb9rQB!e2DE^@kIq0p -zz6srjXYzB+|%&($H=o>6@OM3kLN-USc^8I#^idmN|?gGh36m+PeKv?6i~M{Tp=HV -zd>v4gI-qI$p?7xx9T?12!4Vf=&KSUp8lVsl!tZ{d6FgwhJm}+-fr|YJwTTl+JDMYy -zFi;(Y_&+Abqp8Uq=z-)Mv^aSe>I3v(Q*sU5aVPp5-GO%E4T)>fQsjr8^D!PH3@68g -z;mJN^Gx>l_AopI}zEIl{>_UHq-?P-$KzJT)=HEjTP(||A8Q_V=-`0Pi%M| -zT?lfW7veNNe30*?=kJ9t`x-ci1*M~0oOS^5$#d}$dc9b~^TWfF!n4Co+8edvJkQm_ -zQf-s|2H^(1nB~xNuB=?YBVDUav-)#$b8?U`-4C2J;P+dQ#uXI}vP^13I+LTwYzBx1 -zRjCo_l!Y2mL+{S~>uN;m&|D)Ww` -zB9Q{nvUEl?b!c+K9=oNok=;z(?IfFP72TZ)@*2FVHN9}=GSMDgvI@mmCPeWgByu~rQ>ary{ -zKW&o?{TB=$zp5`C%o>&aiudt@01rw@pLm|usLgd7J-Io?+}tu_KYOWnK<;I^^~U<# -z`Nny<4F#Kx8*?6XJmh)GXwRmzS83&C(RqY)5@gK|_pa>y?&q@)yN_kRW&bE!Gt7bg -zY+eejFe?oZRwJ+mVd2GUC=_r8T(P{|;xfK0?=pU5-Xu+JY_?`zY=vQy;Wfh##vfvq -z(qa?l<^0TIXJN?ZnwGO9hvfM3O%igl?PS+}*F83$PhwLAJghk@vvh?%PM!62gy{Zh>UKo|HnoicAF|03HJR)|nd%f10LT?_Q&t>BNd!>faWHK>@q|AzWuwM_UjwZoI4Yf>l9-_p~uiZflOduCWS -zo@t~J{jFtXSc$fTl+aR*kfFv%$`iU@Df^WaXEDF9tU;uxka~>SsUKcA%88PRsP5DK -zfF;j7Fn{;K5ltgY7Mz%aiz;tgw<^8GwfOkWH|-iHYn>Sfea@>6FR3eBG;i*s(exW? -zhW~EE*!5#=CZi`aqFdbO(%ME><1M4((??yh^6XC=F71y$%J#|GRrwXgOy-&PH6S(ZM4Z?{|7di -zwnk55cuo@QY?2p -zu5UzuJVkVls)`A0@+x!9v}}6x>W><>kCSz6xeG=%J;_J!+g-V|s&H*r6WO?U(crt@ -z=sKv*bLPGWexWDR<6h<}T_cH;#Hm`&Z2V3*FLK%x>1UI{YIG?;n*umWmZyzYl_HulvA?#$SZ%E3YsD#2t#&QHN_aqfM0|^XS3E6#E&WUUL1VY-bOOimL=+{h -zRs%a)jV2R1X`J)p?jgAR3Q$G|4Wm-*odO0-M$F1pQkiF9yWZ9lj8GD7=dem5t0d*8B -zteEU<4VBTLN39N;ecURSiAu)y3@qEDS1yj#V#RzLw|>N9jm-g1v)Rho2Ch2a>Y^9_ -zeUDcuj_Yd`S_V*KtVoHNfoZhpo7(WMuM_j}p^p>W*9jdL4&s)?imsU?usU%C<(I>N -zUuFSyF(Nl!u;1>Y6`DplAqC77<(equW+$wY&TSYWj?_#NYc+Gkc^XZzJit1@QR1qU -zM_WfbDqVF#op!ujZ>@KXcP$bYX=lodtcx5oUDsf{Ruqg^a1(`zx+@F|xmm(2-9m%T -z>Ek7fPp`LSdgHXEy^K6bEL1(^36hH*m1OnkS;m?qH8P0;Q2HP#ZrTj!jHhKriVGx6 -zCte06PU?Hoi@o&tWt1BMY|2Ci6BUM-bjAFRYmkp2mx&=4V>pN|B2$AL4AJ6n%K)l? -z3hV1BE~Tw6CL#By!~9H5P;oghrmUL{e!(07)74_|32U1v{?&+bt&(XAZP(+Ss~x!=_dc^RsSWk@%$R=-;l#er$Z>k8iyB -z${$~+!-62QKF3Fql~3P8lr#CxJqipg}nS+*@WRI^+2i -zvc%cG&cb125G4_dW*yQQ4NMRLo7boy0X8weH4zI27U#h(jg7W1M(n>OwAnYDsi>6TysF|Fz>hikNA@|TW`rm*~TnWwmHk1Z|jdrt=DKaYVPOmGdziR -zlAZb;hJ9$i*kSquP{j|lU-4g=zqFncf7JTuVih2Av#l&Dq8aNwY-?4CiC1Sb^HyZh -zNI*8t5tB87kV)c<*brgysY@y58G#DsQUDvVElt$vEm2*}GLavztFtV$tg&pi=qx&( -zLzp&)(l9+$U5_nnrKfZ7Pxs~1^ruKX@b8V=I6)wyBneubPNO#%bh5<)U3~Oj0a=6X -z$;;xib!JoWWs9T`*~l5&@sn{~uv&+g=+^0ouAS7xwbd59 -z#InvpEObvV3%Eg8Dl`k60EPGA^J(XSypNU7xvS3A*Sosv8{yyM?gIFWZn`e3KlpT3 -z6Yy_g+f7x>i+vMN`d?&#m77dJ{Y`Q?O%$eSbW32uls03~5F`harx1o@N*-@R1?Hd? -za1{4f|7u6K6iMu+Yo -z0UxTb4dJ3RXTQ=k*un;fxhxKkPCVc7R0Uu3)Pb!fm+s%4XnX#tocH)>*F&c*uam`H -z_y6%Inf=oVvS#0fW631w*k5sbgqH}jrD6&7M^A&o9Em&dvEq}(B)@oCF{v)b#db6@ -zhH>!$l7Vul3{_vR%y1hA*Pmh6o7C%HhwBUWVvPN%!b)%1hx~rMelQRWBl2(alXZUV -z*TY>Y3oZd7{0PvzsUS_6LHqtZo*xY0p&CV#w#a@n(m10*VBL(PXqf`6)lqJuW!yx| -z6e&v9wVpt_{9mid|4OOJ->1}M|IgLrUNcx#^_a<*{W!0r5jBE_*YKj-<#7>FuLGSO -zIv3^iHS -z4}>3W=RJI1>i~CO-(YK%XRz;bYn^+%Z@P7nXS#2txYB-}oOj8fyqJwn=Qu}$W2u94 -z_{>}6Z8DK%-s{szsDtbRyzgZC09*TwhT*G`0WI!H^YMCT+(?5y(dIC!(`ajq>O5o$ -zbjGz=xy3CYMtW$7@V${}F$)zwn6mo-4mgVBOevn3TO0tflSX0)GHHV8nUasONyhdi -zACn*)OtNtMDVragiUv?0tLi!p!elIVo~aX6l*M-e%RjBeQz-9hELVw+YW|>l|I&N!D5p_Y}yeHMVV^JUD=WM&4I5I-(uT`f5s+!;VWJ1h8eeZogkMR`cJxf%~N=i -z^RYG@z#KNQ0JOWv1o75t8O2;KpqV-Ih -zLsCNkQ{bOW#g|eHUs9tnP=G~%V^SEK&ixLd~@?aZ|$tO*M`S1(GRBbAbi7s6k`-5#xSVb<1K(C~g+|lu>&4qmus(Z9>P7 -z!`aFo@a^U7+a+`~ex;UuezkUscAK_E+o?UNJ*$7a7W?^ -z2kP8)G|rgF>qIFcAbu;qjc?&Q`BS{u$)Dvx(F^j&VUOoiU&?nY^gR1go_#6LzLZx3 -zYS-|okLA0+ksqtM_#5G)ln*SgIK#S@MF}=jzpOEqR#If=;9GBMYir~GcI?n6zYPv`t*_u=n6L3B@IJqW_XgA9-yikN_g&f -zjxa%9K3rc&6Ah>_LTkVQ-_W$7&IxG~oDFFW&WFfD+=Ir)<;Oh+jnS=}Pv&v+g{uuz -z)B%xEyI;4@U~m{V8a^YODPx*>iFuuwGvju$E551#(U)p~*P;PAP66s@k=d+AJ@4p) -zSI;ytHP@8krF~u>3n+_X#+jTLr`3r^(i+4bS`1(2vu7TYaDeI93SrZ+-pN#Esjs*A -zuv*RajY`DOSp70}`1Am!!~JW|ECYRc8T)oCHSFfg^{3&V8q692*=kc{t=P(@OG>@t -zhnDY2|MuAviGMBo`sQao4D5EVn|jl($8Vf}JKo@Y?ifzTx~DN&zk9oP!NQl{e)py8 -zX`5vu#JvL=j0x|K8?Ei+H4U-iLaUQnH~tjY0)PkknXTic_$UB!NVYa#F2iNI%kXeA -zTyvSWTCT$r$wbXm?KpWMoiQ*hhOk1XTLh~M$ -zWazce$@XFbqLY@!y$f5*Fd3lL5sgk4!30EGf6my|RU*aiS&QqL45>+e+-jzkpk`k^=PlD}9NtT*;??S;hd}0W -zlo1;wkE1NrMQe*nuoz~uLZyC1A$vA?ylG0Jnw#16;*wSK@!#&^G~!*?bX|F^_904- -zwu9964A3JNu%HX^kX3KO)_%UJf!Ufx0ba{YTN<{N*fx{urL<2PQzkG&3ecwj5S>5W -zYxNXcso5(dtJp&KL1UEF+^pts{{6k_QRO_8m(`q}k6#8r#5Bq`DmX!3=Ue1krd?@T -zW!|8>$$X#jDRaB|E7O-|*<>&TEoK{NOct|2YxR2dp=Yz0u- -zo=ztUWl(t(xm=(DY5Y;s!=e%%lNtFG=~86jhnc;b>55`~Fmq{Ub0(LW;rjm+OW8c= -z8XEUj{B`LxJaA_(2?pfTD&VyBti!ucyL#+lYz&7+7b!2xXS$QKj7H@b*2;%dt1oo_ -zQN*cd7BEfRTxOOBSgZq>wW?87Mm~;v++~&wcbOHEDefziGi;CoknCmMXwx$^1Tt+} -zn7f2z(WowPn8~!^&~~!<@EfaNf4eHXW=!(jOErrp_X&;u1aIGP@7VhuOB4tl)qhy^ -z@Vn`e%&}J|8gbtnZ|$#_x~}GmN>`1TyOGkAM}de^4-Y-MCvF!7Kh;MO$I~X!X+f9J -zFiTOoY%P{1a-)K}ppoc2Mqc}WF=%EOctvlCkyiIu`FwrVxzn+qkCycuDkAo~hV~{9f -z0V5$6`VLz5WGxNrMUS43)7UpPT4v#=(f?sP)_svEP~R9k^Gj2HdDm$c=DM5>=O~7J -z4=jm3+oA_|%mqLImCp%NxmW+@AWD!~ByQJAY(^XDrctCsa` -zdduR=FTZ_Y+rw=m7gd)uk-NI~-qv@-iIjn;fl*!ZOa5=d+o*up<5@Gf8GIAB -zoaZB1C0v)*H>y*L@ -z5J>^i6p%&hZ#adrjnPb!$z?_QnTx~2B9-}5gOkEFk%jvC#s#L?wpp%K`qjqO=C$(G -znN5+6+-CjF#?9v2d{7lhai;WvGRp|qi7b8Z)2ZT -z-kZI|8*v!>__HE7A~@I*qLhuUMUER?mjp^3NzgZ@f<=(`3{O -zA<)>p8cF0iBH~D91{{Nq>h0s9vEpj(@LHx5U%> -z&_~io!yAv%Mo|vVp&}+lvm7;|B0!!(_$C~+(!o!9%9`rWEPR}q@sWwN+q?VDP`Yq^ -z)oEtfJ;TPm>G*+S(xvfjPty%EumPcDgNXpvGTEpHIYCNFEA^A2LRCl4RAR0x<3}lZ -z$?4#g(87R>nbErEjMM(GcFC>@`K33LWNWo@&`%)*pW`rFr$n -zpMLvl;=2d%d-CGjCJ$*ERyil^oE|HEY}S$&XU=sQ_E%JAU?~LD>pj-1s-o)v3S^*?~DmY1!0bq@2kU(SVu4)vN -z3^q7LIJ%%GHN=n_F31~faFy0C2pYf0iS1-*T#Gb@uj4?lNe9-0#H$B12%=dMH+~#{ -zj6cb9TX@X3Q$cu~p;J0(;I6k>;fnH0b-G0uAf|3J69NnTmvoVgFc;#|LR>UUlsHoKCYkcl@46n^{sI2S1H&_XMN~_}2NT1{ -zRDAD&Yi_%RQv43sY(hstDvZQkq#V@D@@XI@u0y*KzYWfB1|L}PB;X>T2X!1h -zdK4)(F}@30*$DDgYdmk3Wr2;1mPgyJkgu@udV`~%boaK7sa_`g{ -zZIPV2sutc=`%U7t#7+3xgOAjY>3d`1W}(Amowa|_^NFsmr#XDl3 -z@4p-$8DIwIPUCT7(kLRV;q@G^Cz41cL8rwUK_`K#@fz0Ybr|y+jgCi>rc%%mtc&v} -zLB$$t?5B=}D@;t^t;MTD9NQHRMD#TI~{6gLs159*KWIsI4zV`^PJ0@WAw -z#yItY13XZXz?S9$;VwCO}}mG%7HIt -zR$0(^3pi)|T0X;;F+e*?J1lcj#;lAr+S|1^X6{J)UEWKaQS0=$oCTxv-gOFI==MY| -z#5z}WuS2X_~K_ub*$2Z)}UUWtl12%FOAPIaODypBbH*y*#`egwx;X -z9yZ*SeP7-^1&`~VGCY>`c=q1tD^W+b5+YEn$w&d=6p)z$veht9N*M}-Q$S`4NT;i& -zSp8*FHCYjZj`swkc3yu;x`!&v8SXq<;Q@DryV^a?z1w}vEt=f{_Y(I>Hy?1{;U@0i -z14`Q2oNUl!Y2--CH{zJc_&6XwmN8+A8`>PjilxzHDaQDcy7YzVB;9AH!HQYGTJNLE@ewQhMPuxs&NPDm2Mixx5mwex!IC} -zHSTtDMXQvVOBdJml^xH;x%4UYirkcLkgKk8OM$=crx)dV*rP&Oxy22IorR>Lu(^;F -zQtN0Yaw((ZtiJ`-7>zYSHRvJ(;%@p9!Avu=Yp?*BHDrpQ&8MPMGr+d+Y;zsc*ruS;H<3yCQ{` -ztsgsLRdygY^4dAB@pa=%!@l%1U1rhXHFZ@Tk%Vy;D=_L!& -z*+D~a6=4*ym?N@$+=69G+|poaw(u+%XpJ&xVzE+kXHi2i10+(SXJP1OVX=oP$+OT9 -zt8||&_P~aM<3rd$!E$TnA>hF$rKH5CtnU6^R(HQ*aAUR-#TSP8tWm4`xZXNcW7V0i -zSg+=!#IdWK$^GRtF+&Iyz1T>*D%F>$W-00g(Q90t>p$!8x<6k%|D78e?#Y -z6;JHEcIEbskKFpxV_Pw|`SQWU^y6@1edF~PkDPepFm1CTiOada0&Vr9T)ZUSpw|Ps -z>La!>`bwLqO?Ri~>7%y1aGAcJZIphvZIU!aKUe>w?tkp2OTu|sgTjNd#$;{D+m$k<*aZoOdGYtI#*$Z?h~;hiGpnd)l&nX%e#z%0X1X -zY=h0H6CDTPt(~lm7YaU~Sy!3iGw2-lqDYZ0;&L5#V%ZsYHaMG|d>(LRQj^CT(a9Ro -z+1-dv)`(6AJI6vHGsVxUDt<~MI;lZ+6w?5gn{fnX1TqhqkC{)JlV(0(t}s`dITl~H -z#_VCFB!iI>AH9=GKv+wf-LbsoA=;STJ~^~8&rze+MeW&j`aCuLpQfWJr)kc_fkqIf -zowQdnC%Y`VAg0o)&JxhqSTF7EGBJC%zHsRBwKus;cty)!&MtoQwu7s8%=*i=7ykOd -zj-|#>g6E-bz6! -z%1AjBkLJgY(tr9x)-q)s2pZ<^hV7Lmrj~_9@D89MqC1oY3E%Xrp>T0Q6jN_b5~VTs=$Yu2KCX7$zt#OV?!f&) -zyzPwZptvP3;@!&%4#u7W(V6D_>tQd*=095?{Uh -zViMordSFw_^=r1<9w95&&ba=@8-x2^ozpsV+QXOle}8*t;(xw?m#zVA-3={Ed-Ai2 -zs{5Qi;G%@krHHp;jVmRmxTqAHP4U=eVMuLZOx)tKX*SHJ<3PlIVykj3N*HYV7*@AuS@`-Ks=YBoP~`@ -z{F|p<{H&inD!=!G;>80zV;u4D6^_Z_+1x_MBF~)gYR_8#t)5%_4?3Rm9Q6Fv@kQ`_ -zFzr&uBaUYr+<=^!BFUo82w}i7S12e3v;EbkX(nPa`RF6@+v8~9Ml!dJzC@q{mmxjw -zAGf#|F2U!xWEXL5$pam|e;e8tX|m8NGEoEqWBt>tu3i0)N1r}H -z{`kc5m90;$S-TxikXJ7rv+n(+2G^toSo8i#EI*j|Eb-68mx;a49^#50+JE@rTX*k9 -zD$ZB{XXr|RH7Y&OdHlR!1ODaIrbIR^^A~5H1s247wvj_ojhJ*sTm5LhvgAEmn_D%s -zTy&>%3<0(3a!C~UMlmGAEfw*OVIX4qTK6q`?%5^=-Lj8b>q$`m0yLcik`0bFY*9r2j_^$phW>5F)HfdQqQ!AgWCE6_kOB3pDV1+{XyBk<(-jqKaCG3cKwqWMO -zk|r}&?XcQeV`fFuWZI4vnbHQaX2ILrRz*B41JoVIdYevSAVar-d$u;5hd#NYyMrZiJC-#Rn6TywQ=gJz@eUClcVi^QDfvQoZsVa5z76bharhQz<#+I;3ZJQ}aTPrbPFr3)X`@2YKoOiLp*mt0~E`9&eyZ#DY0hLj`re%8xlX2tK6=}=!Wb&&$> -zq}jzO>#Q(`ZKV{~jS^g@olR`8vpd=uO4xgP^TnisDo|(Gf}bkC@+*cp^+iPGMNTuy -zK>>D5eLg+LQBf{~9;A<@SK1jfv$HSl`M%V{;8{3deSuV>SXo-)z&Vb~9G6AEFnnDg -zXbbRKv=*=7munjJ%M4c=S37S(oAIsuM$LNtjfRcJ+njG$j-**Ls8^dWNY}t_4F>aR -z)+ZRHDCy4$8j#YW{&m5kO}8b^fAB%#{GC^ATrhXT&9mp+G~lu=6V~s1=K6I{a^9T# -z=Wjc4a@*|ta`Fz}bTEl9?!4ngJaO)gH%yzc>BbAmsx8$!ny=rrQ;nI&$C&kQZ`@8z -zRYs0CUSz6z9-EK`5o~J3dR;p<#WM{?jcT_7qL1|x&64ILX1$ul<9Z{T7-m!!KvV(M -zh3dyPoieI=KIKBCNPR(|X6>YPuZbD%QiiH8QbsD7E&e=8s8qe^Ugb8 -z!ebI`SYH)9Eoqwpg%*XZkWT1XT8C@lgE)&P5YFHGvU3DbSWU!@0Z3c@MTa9*g -zD$aVqdriin5CC*q&i29`^HD*aL?mi_mS3>E5|)}XV(Xm=N|y<+Q>(l*Hr&_kQb0BX^B|QbLCj% -zb)-C>&I6#v@$DLBjR#so?b+}NDYs+j5Y#~l(t)E#k8-s~k6w84=ux=hBs}0J;5<6C -zJ09fXMoaMmejT}kJfPv9=CKxu0^zg*HW2)}vZ#fQ@?^wJ@sz8p)_8St1oE*4GpQE& -zvx==RB`LBN@)(4;(X9BiQDRvj;Ghr}h~UnaK&KL?? -zfKk&$3yR23ZG+#Qc;Bb_%lT^utqD9k;`M2?-Znz74gdwC(sFbBAuJfonL>$BDF_vT -zmH-I^GJHk8AwFug5(lJF6V@2V7*D;X-Z;fv@3_)4U$f9S*Sy%V*wY#Kz;MEO!u@I5 -z-<^MVf5z-y?x2uw&bJi^73R1w#yn1#Eu2XIAO1(#Alps6NRZbD-BV}xne?vA<9aOX -zzM&duMj>4C=*KbDFIxsE`6a)f6Hfv{N>nEq&n>>^UsR72qB#q -zZ^7Ge3qFha0RCU?y?J~TMfxya)yEt?_mxa0$MobTfy~u&K_Cq|AY2JI0Rx%L!KEZX -zKtWva0tI$;b-i!b)vqTpD1vz4vAVjt%6hH3qPy$Y6O>(D*RL?0-&5T)NjUa+_x-)^ -z=lx?RsqX6T>guYep0lo}0k?rdsCbN&=dhko2n)*+9?OJMif*^Qh5Lz -zJLNfC^1@zRP%?Ss?9(BdGH2k<$HRRnGUh3+o`-tEAR$^qT*z90I!Y)9al&0i+w(`r -zuQB&Lvi9Nb-D^bYFHb(PiW27ibi)I`Sij)`_VLVLuV}mC<#p26(%ZM-YoDIC^VMI! -z{=%!!y4$4{j0d==6*R$a@h5uo81s1ZG&9q{_w$sSAEhrV4mbkElZt!!D|yY>{IP{o -z^QRVeXcp_w%0H`crDm0Wh54NPRfPxnH|!rcKXAQK^0EEnl8^X7KCg_aHP<>q%vf^^ -zGu6Dv{NK7SizTzpVx;qmNTaIqpiz;rp!{_Xn>mr|;TACuH6!#GKjd -zA99Q6zLFc5%QY}Zm~k0_G}ni1K`Ka=VVt=wb4_R-(S+ucn((_*n^0D=fMLl^=!Pc5 -zC$(X&2~TZ6xdkn;k?m*8u{cmiWHYzQLXS{Kw%;}Os^yoxzHSe-`8%QIy~1AOdX8&hQd?wlrWT*t^UMKT4}+_^>uF_~AB^GS%w -z$Sot!k5hE#{fHj?=rO%T(C~V)l&`kK?<3Re8_!z!h(>IY*X8YPiD|@u5)H)Er3Eo+ -z-{9fB1rQA=QF6CTj*7annA&KEU?bPx?zKU*Scw)x)Ip-(KRh($2`eXgXrcq;7w{BI -z8MfT~B27L1;t7d;{P=~;mLFP}t;e^L5&Ju(S$M2GbJvRA5}O&ef*r>M*=;PFuVGm= -z!%z%sL)f6BXuF=VusSuFrwa;7k;QbS9os=K(d!Kr9CxJ-yLAn^HXSXGT}I@wOBthe -zGDhoUX|F4hxuC3=5RWb-F7v(uoBhF4rMo;ik&NZaG8{F`kg=Bt?x9(_mSAwZS);6d -zFlx-EN{yK-#9E_TnTA6IIT4Up>HQ;9aMXa~+a8gYmxkSu@FT&A*R?XAA3F3u=igv# -zy^1;O`0dZnND>V8NVDj#KsLMZ-?G+tF=sc?I=ZO9WL4=@HqmP0b)ud($-0xNz+3D3 -zz~y`uls0lAYo(6L-I0ZROhwp48uFZ?Shan=X*UPrSOi0Zul5OMa!~8F);xpLTBWPf -zR~f?kupwl;!J@0SR@&6}ApvPjH$_ViHSB93>9B4Uv|PwEC!aB$)q=1 -ztX2-u1$LXQ!phm&HAAD*<=giBF#Ax9p`&=PsQPv54e+x3HbN)IXacufEss^O7BeskJc -z3&6{enG-IjJaNKl{`TVnqB9(IIn77q7;=)q7-A)mX%w1~%W4ssscJOW*K3}i)^D6M -zt=~Co(ZT~rKlmBa4St44u4J+Ug+*dqX(VO@X7s2dX30(&>>&CHTGL6Ex!S6fF#-6) -zL0g`@pb0CUUAEZAjjo@PZ>eN;(mBt5P+RJ*{nI1T*%Qlso97GCvU|V2-axH7{;S=SI_40(x)g}+1(0qQ^pZHvYr|#+moWm<$jo8VOdq4tS~OaX)?l+} -z4T#oYoh+6Rr>(wHgEghR4cn+vi?Sdf!su7Eu -zXIdvefAX|=GV{&c`R&?|6?BoOsmKU}%r9*>5^pW2b1hjhq==Awwc=mOVY&3q2`d^F -zFPSiD(zqq|5~lKwHB-jkQ#HAvYi;IDVqr~{dYEsS31DGSvV|3+wxa@M6+s=1R-q|X -zaN=Zx3~Cn#do_=}2kg~e(fGUpGw@Go7%go;`=|q=kHU;rZ&*ju6zNuKqn1)V6ivBI -znswSQQ5#;0mr`^CoOxZ^q5_DYxwCVPj12M;BxSg)7Ssk{q7+kBT29IQRPL>?FkR0krQ>h@M*5-m*~t%H@b+Hz -z@e>b!Af5R6Pq5*0y6wck(~ovPi|w)(CutVd#l8XB`*@M7GGS!4sx@ZwJ{;VGZZm4& -zELzmJ8J8g1O!G8NKVZ3Km)zfy`R=IsyQ8w;m0cHAQWgP-GYA+5IA_NA!)tysW6={A -zZmb$#2B;&=dIEosjbFW&IsSUbj%yD*DY+#c@XZDIWQI24kO5bUVe3Nua{Z0^d-X5r -z+3ECj!_OGniYX1!t7tXL>1Z_q`DS>TX6!W00M1c*1EZ!7PzU5LlG{ZNF$^R@FLTU3 -zD*YJCa$>Pt;Bqz;r*yPuHAH0zGLDD_wOCpvsJD7T>MKpeK^io4w|E@kqp4*ij2b8HjGRHFv9v-m93}o?v -zE*PUr$HZu+xVT>7c}SjTZP$ysn0{+}OfOc(^rb~`9+TZ)fOtj2L3vFSZNb#FnMLdJz -zC-z^vW6^JqXWsit`irC?g8>MsH;|PH_DyVe`te|gVoAlEhbJ@B2#f~JK)UR^%v3>gU9VXVHrxCVl -zkj6x5$o#ugmTVV_B{U;1&DLlDx%GOj!3dJy4#M4$m+y2s_6^qWVUd$3XT8-z&f+2m -zDAr^ZHm|J0cCs2xiNk4kIGk3!R$Jn*Ley#jyO%dx>}F6Sw0e!x!I~^);BZhRmUfy= -zCaqSZ0Sus=R;$H=G%nB_%@ehF7Dz8W932ou#Nt_dc^+e2P_Pg0d{|jl(CI3ek#RXQ -z8COBZDR=0caV2(6CCpBz{O3!t$@=U1=X0@<2RyCc1R8_|G>8E!M}}?5l*?)T6>zgg -zUcH3K>LsMi&@{rGJ$jK9vpUAwPES^2fRj~G9Lg}6oQd(R()llbSni5)IRA^^wv`o) -z`Q$li^#SSERqA}Z^b&B*hU>2RvYbAgaY;wM+4%_l5UA>%yZF@P<3G>t&_mW5*l~P6 -z${Rem$B{3{BlL1FSp^fKo9V|5jC^Zseu0p$vFI&!n#IUe#H#H&PG6ywH__4JgIb&? -zTef-f+-)1w{Qrzbmu-bK>MGw9`s99nq>zjkN_zn04mDSo$C9yORL3%q=i4+5f)g@^?1r(VVhLPu_@4dMHJ0ah$rEFKesSj_A`+ruHLw -zE{`QPydRHIWmH!fRr*3~(3o9$CMt{m1p}W7pA`qHm*j(-ge{$JBL=tILLM@_g*P2EMSlPAj{Lk%zacF -z`z+*>B8%#NwFlZ=cDV^>VquDv5ZQvaQXgWvhu(s=&~y*#!BiV=r$GAxDNE7YK{MJ% -zbq!G4=zY|jM^ORu?maT;&B%P4K@FKsnd5cJJCE3GLHh8eM}E)Thn>=Aqy&#i^Qm)? -z6FBcYQTr~1jl97o$o^Ohir`G$CK~l7#A$e~PfKa%S>;k0;Tipa2|s|MrrRuc?8n;> -zX+=_P!f=U9J6cQj^uV1U;^k5INX~`uN1Ll=PnfY-*fb__p0i_G)7-jq3ro*S&ChP! -zR|AE579jaEgj9?h%AcB@mWC_%DRa#}KkUgplpG2-$ywkmFg!A7BAIFMJuH -zqMsmCG8*vnZG=j}P%eD}p)yEQ4*67{1)ne@1dq{}_Ym?K5%R-30k{|ZMDg<%NkagY -zF!ClsV;Klt2#u>pXgp{i6AqItY!RAxJ3@`$Bh(}z)C^^AhHtg}fY4-ke+uN=IvWB! -zpB_eN#(PSbITgYy#P8A>d;|vn4IhESPOJWjlfdx5{rsUu$19$jZ+!ce2d;no-WM+U^SoPr -zefX8HzWLi@zjprSz<0m9;oY?>25y`4eZUy`=8=aEG=ASb@O5MHw!6pQaLoxkPfKO|I3Eax=c)?WI_;yYW;irz`r -zT~>1O(#vD7+wz^GpU-c4WA^UL9=d#0(}$P$Tz2<0x~2~=E4;npsYLJW-<2*xV>ahM -z@ba^fdoH?kcjYC!KWX~D#@6wDjbrrYYc`JAe9cpNPyI0IlHE0yr*6FOqTSQh{oza7 -zv+w-knPcnkt@(cJ>RS%Jvu5`tnbeXCcPF;6PyM8YTKd}En$5)r_(Sj9{RCV*c=czi -z-0;%uCEx$>v&)`3^3#Lw{9^yHzmD==u>Wd!{6YKo++Q6}t!}(zcX@Kl{()x~>??fv -z>l;F~n?HS^;}5kDbR7CHbYSWylu;qKREwv=HE5Sn%_p9V`E2pN|Du3piAzN5<4jxq%!+W2jOR25s3Uvj#kzR*Z -z!#A3kThRt4iUg8BwRP|~wGbgTJ$Q`16Rn2O#w@|}WGIYByNDr6`WWCWqIr;iIlx+W -z?zePeJ|%la#2MnC363I!D(mBV-FbqLJhL0mhaH-z^?Wb{DAsBFaUQ6hV^ -z=CHUAR}Z*c@{Q^}1_QZKv8Sa)J`cEg`9Z}%VNo{X$je4dmTZL6%aPK7s;X?Hq(q7A -z;W&~_=^nkFoIRkGk~2M!pD!n%2MWlCpT&-W5_dMj+2x3B0N&j{_zcb)m@_XMnc0?& -zG&g4>jg8sJs8LE}PkANzLf$|@f&7KMfxNtI#A3-tv0Qo+mn4# -zT#}7s3u@g1*+3HOfK$oWIxur)Hqtyf8>y(wMzUYB<{9nT?pt;U}>Xv&e$n -z02g+TL2hYg4}6!L2eh1=lF0#z8K@thjZB@YMD{G`AZfh=TAd7B&4AV=N3;WCV>Tkc -zL?U&*Y^16t8!0W%MqIhK><&3%A1E)+Mk=e6$R52dY?^4qK|tXkpl^_Pt;80Du^F3? -z0CCd*)9xn4LZVye3=e+pc7O4u%kBFTe{OfVzdmAie+2=3Cw>R!&sNd-olYPA4!hh( -zkLcXyV^F83!Pms@-f3f%BJeCVA9_uKEd -z+*{wmZ^5bSo!)n-mtU%Jzx+}x`ZCu2s_9o$|L@6U?7hDS>goNhJTLpw-*UEa`L4OU -z`gUEi>#kk>yPn^r7N5hB{TBBM2+u%x8p2Z$o`moOgah*}?#K5Rx*vu3-u*83V-WU1 -z*bil?Z?L$>Lzn=e2|^=;NevG7LMFdc~G2DE`L>e@yYK#8nhpj^+RMhM-&)+UPXlQd_Tzry3r|D*(#g@GrDW^+;D23TKi%A~ -zYF-}iSCuubBNs+;VI&vE=6(~oFqJjoetYxsemmUl%XtPH_J_4@IJQo4velP$5k~$Y -z;d*jFBhsDzTel9w^L0qhp;kV~1I+S`+FXEd|D#O)jUOp%EodC#+$m!f7hd6i(}x%C -zcQ!73xIkSyNr?(id8z1>>*7S$&#i -zlFPbFE`yx3c{wyp{GX-Tv7o%_KS1n6Pmkdp|7#w?{*EW7ho?>Zhkl%7uW>E27d8}n -zEFHPa_T1<d{xak7}d`s+{Yo)16Hb7^Q~&TQz2*|8Kb?JCj?ney$cMy}m!c7UI>l -zJD?F>FY7ng_OlBQ7S?z0W(#ltd`4GhCpj;Z{o0iQ)-2zVB_)FNXTD2hjRI+cu|U#0 -z0peS`R|3+&e|#5VKb{@t{+91Ng`ORjGAkeRb|_eFe!c7^5K}f_Yvz7jFINl8JPp3o -z+E3?4;o-mig_EoFZ}|c1_&?zYjQIQl3%5jNXOFRmm~BjgehjQJH2BBC52W*?q|`y* -zByUhVA9vxEcmuv@$aTyTyiAVVh2wY?KKCT6b~<_#6pi=LpNP$!{S9IU^xOi7{WrFu -z3*@)oK!?$X=v#Cgv)F=NxD5TX-;J`X8uEJ^@1x{C0>q(R)NSZREXhuE8c`#Z<_l^w -zeJM?zZ$lS=ZY})NoX_y14b&C*Eb3fzJHCTzq!v;iQ1^{|jx}gH)O{_!>hyPU9(IE| -zHWs(w*|;0;z+Y2AJQ014zCjs)gblmV)(z&yk_=GdQkBdgMaxq3_nBXP~Y8SWkCT1N3xy6a5#KqX)na -zKZIN=GxaT%21RfSx*GmE5O7e`g)T-HqE`X_$M6X>N`~VtkYW}5eaIx3jr5Cn0Hx7< -zl!o&gXc4{=CD7$izZtlI`ZcnnJ=A}rJJ7rMEP5ikn%;=Pwgq*x2g>s^cKX?^+;o$cTdvj8uAGQ)tPH;vLK@?yPV(oxwHdKLTihPI-l{K@?W$W1urK+M3 -z$LR}023oIKfR-W3?vTeyEm}(ip(<0ASVa|bc73wa{9Pu{X$g93k9Lw#GP0n`@9V^N -zGC7URPb;$!uSa?Ld3h|^*@o3m6fX<^aaV9f%To2UGc>KU--9N3A+OV3E3 -zB`>Y6@f44$x@_69EtOT}<>j>-fulPHzo4hGccDVGP;}^U5vDX61Jl5%S*O$9fV56| -zax$+qM|8U4hT^tjO2rvmYLm*X;%Qn%Mn`wkH0|vSb_O#6uMG7dYJg}1TnD`XceXUU -zX|S@%-cV(N4wZ@$-hOV|HjoChq}`OsIJv24v9%<&HLw3yl;K-EQF{7YY3;a$Wo4tB -z+}}(VD6a{y%)}&sXFV>ukQQsSIz-VbR>iWKsi;{+X*p6XQfNIC8X+TZ6pfThDH)mP -z0T^L{Q+9WAn%CMRWUav-IBUqtI5-nqlPtHJro&pAVL1(}E5vqoG%jPSG!YzS$7({l -zCOnOus%g?KqUJI4xCOdZRFYZFE>mxzdYSc_joLnLlaANBpv-C)lng`MolgJcMpOQdmV>9r>uT*xK -z!p4bqm1r!?i*jjKj)j#Sj^ -zbf|QKin-m1iJPH4Msq}VH&me2MVwBHh_nR-ZWp%(h!R)mTy(EOl7Q+#FCcr6Y)6x$ -z%i2yhhx8VQCvOgn6Gg7l?t17>v|vkW;< -z;!(ZU3j8gHHcXS1C2n;_@Rxj3zB`}Nt6kdFc|-h#OdrbB2D|{`+~jus;K2c{4NwRW -zi0r}%2&IcaEjw_TQV*F7!r6^)^6B%6?bGWb8)Kv?ob$>mQ)4VO7TT#+^LZIFleiakaFj-I%s9ii;^~Iz#l1GRjI;_(4h~c}(ZXPjXOf1q=VKS@ -zsVFJb$(+WV$piw~H7aDveW%A0QutvhWy$TtlMiTN8L^2lbFp-1=>kv9?AXDNrZ1r`M_W>B>Xxi863uoyo`AWlf7jkRuc8VcK3+;y87%#`-u<(3fU*zZ -zssf7Dq6K0Nt=4EBp=dir(GAoJs!P3*>Q%FpW+K+8>4_|>Wq{x`G^LjJ1l0oM)w(S@ -zOcC;}C0n8fy##?`4Vf5lmh8T}P!CLiiam_#L=EMX5WOe!DOLU{wH~Ygl-ZSSYvxYo -zB7j{?ET`-?yTCPby$;rF&@|Z?Ba00h0xV~j6PtA|!|HqmVU_7@1Xba4KsQ5NIopb4 -z(HnE8bi2-EZHm<{43Jiw|MSEHd#N!^+xbclUv}bSCb7A7_Pf7ry`o1$0ad9nW;NwHLD!_4txM}R=u|qYi|gemPFZJX -zlM7=o8QccZKx=3hyIMa)a6xZpjIeNM6KiaU<}`%LvNR{w0B+%G$OPI`hcqrN)>doF -zBicu3iqml_on5DVe4b84l!EQ%gUmtpueEELgM>lXY7TQdQHT#p}JE(VD-25lDYRE@1>5xb~hCA+eqPqVgkeT|m5 -z;(HWm6W14!3P$vXNKuik+FkA~-%@@>Ia6NFH&ioYG+cv?D@J2nqkFt2FU~Uq<1kgY -zE2xSJwJWc;4v-P5U)xE5k)>m}R=7K1qgbJ4g>Lh+lwB$Bu~i<)ZtPNK0i5L-hs8tv -zxnp&xE;w&aT~}3k%;)WBzvh?8B}=j9`pYk$aCcjY@Yx1jinY>RxZ(?~!DgNqZW~u~ -zS&7e?@BG#E7u-0euAEbKP99T+O{P)LypUnYvO~b`H-N^>s2I%`tsxvLZo*B)^GpfT -z1?&X{nmi(A0S}uCi)~`mD;HBWjB*G?+J}uYWana|>y$8Mm)^?rrJD)GBZ1n4-qIt(&Qd(l^rH -z%4%seJGg$*Z0Tj`tGj+yQj+<#p1UWDsrX}Ax>oI_Fc@ber76q!SdndKbJA4a?B6pa -zLsjxbvkQPf-vHE|2hzDhrz%!mpr&m+)RS_sT9DC}CYH13xC{q^BwyD5pgHB%RO+a3 -zWzuk#5xSDa!`vX2Muw#h -z=3;0E6N;f;aaO%*no6`amiJN{i#L_@hBc#zpm6H*oe^gZs1PP{8Dp7=0%Mp|t2VoM -zm(SJfdfmmkT%L(}h6t*5nR~{G1-PIjM0fW}aI -z$#17lo-w*2K-y_N@T4ce#!#W8SV|*|Z&f4`D3beG!hSJIB1|m=9zs!sLmu0s1t=)W -zfex6H#-wp;D55RwA6ibJ3&sp<;6}Ra{J) -zY_|OU9VTp|qK}2?7Mh~D`3PIBl^(f-CanodSZ}I#*Hf;5hinDCM`w*_y|p=;2pSAE -zbRKQ!%*cw1w^NaB*p58VN|j`LFW?xmE|67Olora%!`Ur#<(u~5HfG+G-j+^W^vdLU -zi{>s}j4NMiy}Gc_b>58KPvx~ZB -z);b@XpL`zs*7sasQ%Ua|Jo_H-Mr%inJ1AXVK5zb%B}GLJyNRnS+j?^~UsOWc?>T6{ -zo51okp%$^0-K$dRX(PQ4YrZCvA+Q_s$ct&DH}aUL+iA*Vaa$;wwwUG0b_S2e0-atN -z2$=xUh!sM%ctVQSNt)`W6V-UE^g{DxqkJI-x8NXVXxledyK`3kaWAyln86dwSAanr -zGN27&SZ^>y2tZV7y-KY$?8gmE8?%(z!oZJKgLR~CP%l=mQlGD8ky>v6@y>(kBJZZG -z)dP1inM`gIWl)Khyod^foFeLiWF0;f)7wDG4b~Kyw4L>p2qL*GE21nxI5Jh*&?!ZfN9^G}~T6|n$q0BP@&t7J~mbY4&A{G?xA8Vaz-Dst)lxKfP -z)vVf}qE%d(C#w(uzM~3R`jmL|1cwaPh@KeKG72g9LrhjXm8I@1-YA%2Wy!WdDvP{@ -z5cBecrl$99z2&{;rU~OGxBUM4JKkw-8ZTYG`0Q0@owaJ!S=8sxNQdL`L?Y3R-3MR5 -z&Qv0vN_I;h9l-VvKLjQI*q{CcEqfib`UmV+h(|vW^U4fk3tNp-3)|Imjq{z0i&rq4 -z!1UiY_&Fg+)4stw#Y(VAkkwddwiXw93zrq1Td1~Lb&uy!UgG3=Kh|`)d)<^g9GkB* -zN36YoExg9#wo(NJrKVDMDMe-Jn^Nc-2%C@&To47cQ)YZQH9DO20HxULM9P*EDXVli -zc$g24Kfd#Wx#x6m?un&@;1p|d1J-ZGT5Q>T<)Zs4sdK+s{rtjp51qBVrzkJqr^n4D -z4M*O-B=ggboyBCr%HUzVw&QfLQUXhEHID6g6vM>1Rj74#I_#lWsxyL);&VX -zDaS$xOdP;Tgkd3(9}e4Ou2Pj#UPwR3yKU2_zyITF|2SoG(~q}ZynM=(rb8F)cy&_K -zl=HXzY?suVoIg7?UM!j+R+YOaq>Im~sTzOD*~Qa}`6_%{*Dvbpo0`XsyS-!UeKD1| -zw|DlHvGBxkKL5^HEAJnxs(+5Ep1yc?eSNFjSmF#W&0Nqr({HS?R;_DZzS(BapFnub -zGN6`y>@n1cc8i6h4du0!;{)}L^$W+%o|uXzCasHdqXna^%Dhdi-vFSV*alRi$p@fK -z7+<)rs|Xhr392?9_W4FVE>I$eInMOBit~z)Ue^MLGS9rRcx<89XZQFD$B$*S2vjGq -z^qNi0?q*6Svu@5zShvg(h$?;*7#A??+S;7p&1!(18AU-N0{A3>8yt2{Hr&k>8qv9= -zAnVAsff%!I*37oIZg}W_W|q(XU~za$ZRx17KHs*WIId}Jb@iAUcX?N7WNoFpDy0=bdgb0SzC2JK>a*``T^jpb!ekFgVT&Q1(<-niD|NT8t0AC80G@d -zGz9=)kEeM5s8Onl{iU=FR|~+ktioL?>Wq=nd{0@a3`(ACLG3g7+&)U1rDd%`%S1{a -z1q}A)zz84&X{e}}3j8WhRak~#RyJ(=5l|gyYn%DbEq8xCv$Uus7CNVK?6y%QrL|?j -zD}~uN#d!L?%=VHw`K$J}%v*xL?|Wg&Vd5*&3%95!QglYMuHkXNRl0N;O -zqHD)Q_LDwT(hoWjAmKpeD2SGeW9B3tk+q{pw?)Vl;zMGHE*WiCCxzjTQGzqpm_obC#%HnEF!koTM~A6(B>LD+ -zzGNrPojmuYn=%hj#=GvF+TM0f*N(d-PeoNlD(Uie%@{SNbV)Q?dTz4WepT<-pcntD -z=f5J8qU@o9>e?%poO55brg#s2rF^=@OiRCHY>ug!H=AeL4YahADaf0b>k~c>urMPx -zXh@Bq0ht7IlbPYL3wv=r9&c&|>+Br7(zXS26t+S_v)UyzP;C_aSf`+(5v#E{h)GEq -z3}!Z~$`SZ@3TQi<BrnMoYK`K#2!5`9@b(BtL;K4$sSgjo;hb-jIpsd*( -z9ooK8?sm=o-AI3(xs+MuFpIx-G_gT-^cSA9OT?NEjE6crETqF@ -zwH9lAV6L7&8?a1JKL5XbH1gow$E2yM3Ar!kdETDT!bgvEO|>PZ9dMg_8S@G -zDk&)yTvRWMSym$;uB621@XKrNv_%oWKg2NAjw|!~^QnAU?KMa8L1uuI$l6ZBf|sls -zUDMf_ZM|Byw46u`W~8`~bs*#kFhizPZZt>cAc#ueFJ*d4d$Dl~zk;s{FC0Jp!D$a( -z3s$Z6QmkI?ne+9J{4+t8%<7xAv+btqq@Nc~^5gEMX`{(9J`im&+sj?!e{<6zOo!{F -z2PStL?dFE6SYx4uFN!{S(_!f^gnKp){(=1jG#N4uwNmyfN32;kr{?QKIH<|Nvrl;> -zJLnz-5fi!VAcqa~MlEFxX{nMB&FLLz8JGrQ1?6<8(#jatcgh%6r=o+9-@h`L#eO3F -zNa~k%OIz_)JPWtui=-b#>UZQ{c_9s*A5v>H -zU^_d`B|4s4bho>ye2|PK%1B&iRv;>hESNToED9N7(VW8TU)j5}Px|^ssSf|<7HmE5 -z>ieXvsTI@j=~JtJ^x(|1x~NZImmXUb;ZS}OEnNBxpm>@k)(@0O#Fu)xi5IqEg=fZ*XnZKy?P*9o>=_n7ev{p;YUE~HZ -zAk}KZ$rwgMc?tJdjK^FzdA1WmcK+Q-h?SU(@-U)q7{09vH@UC1|P7FUIU}_jeQ$1XTA-{ -z#&sqj+txt~5t{%xdh3b9Z@*2KzWp}X(=TC0I&mTbUn3(gx5C#9s9W?_Fr&Bzs)1{x -z+PFDP2X_v24!4P3quIh4C>=|-wSYgYqV)g?Ev+n;m01`V(O9d|*;$%^ArnwIm^@rS -zy4Jj2xhI5JGfUFg;dG> -zgRBpgxhYwXog0A^wKg&W3AAS;BaqqQBSo+*1EH_mP17GzuQTs!-{crAWzr(9O)GL& -zP+OTRv|Bmy=tJ%6oI$Hm>0Fe9acLb~4OPihs%tgnx@!GcN?^umg8xzD!0V3wjtFY{h7-#`o)yral0Y^* -zk>!F0Q(@!*1k6YE=svt26^Lfs9>$mtw}&Yd4(0nR9(gz6&?XgDQC(HIs)`R0eBcb#O0SJ6QeEvec}4G1FYR*B={obbicwro -zx<-hAkdK{13^6yzDeI21G&mzi%#C@{Mbg942D}rub^K&v@Vtu3!r6jwLDQ`5I^LMgCra=J -z>TLFql9Ouib9xixdrW(jSQD-oFN$&Kf -zNsZH9xis<6q|!0nGZL%v@>%JA>eu+u`2F=wqREKstZo3(g>BS4Yy|Om6l6#*d|iX~ -zh+R+`#*EDjSn-UAMP>o+!f~b-uVD;G%>Wl;X -z^HB$T4(etvKx#)b69}4vGWjWr7-oAjlC&f9S9~8X#|1!aw`8uO7SkItM<{z% -zhrC$k@bzK^9r9^JO_!!u!;oz+WOY#s$x@&ef`u_cDO3h4WOY#1(>=q+g@?UZ+9A=h -z>X(7${8CNQyHJuVpbh?vFDK4k_fpRRj`qt=h_t#0R`Smd>-<@|6)5U*1yZV=Ln4 -z#1il- -zl~?fm>_AONAx+mmB|X1p-T78Et}yadHKQ*{1VW=~>z}-~@0YDpX17k8Cw;qZ<1M54 -zqC!t`Z{w8CqQbmv|w1VbB -zO%)kOG!nbZ1Q9bqTeCS|)HCJs&P{MxUS2FBtx37o@^C%cRjBH%1-Y4>GDc -zVow41u*4kdBF@Q5>cO3#&YQa=yj&4~@yFJd##*oRrrl?E6d$p~M#a|5frwksK -zenQpBoU@K>+j2x!qNrce&yf+*4@Es(5hCByK#}P>M>}f~v@Pk$!MMyu)k$~0^-Fwt -zhNX@}UdKTCY>-D_ZxP2Mma3sD0G?ppRO2eD5l^QW3ey~7X%}+PpjFf3kU)bxrO!fh -z=(ACR-i+4K=B$RDo#>}AX_jgzTCslsbn-lrhRzWkH|$7>>HuNQiYyHxR7Dz5>C!q) -zntEhexaS~;lBu`PUx%I7{Q~QIHomjGxOM;LbEU&~-7kHydBZc4 -z3H`P~IfHUJTOM&>wO*c!q1Z4ViP?glXT-(_RgOHRw6n9Ny;6NV9a>0H20<<|lknIY_j)M>C~yCJ*)`YD;k4sn$e!mXf{mIK=4DeL|@ -z5EtA*gmiG$?LbI>A3}yGSZ*sJ90Bw4c91Du2w8v~T7QF(y$N756QR6C2<3|qo^sH`*_oevirm+E^Kj8QK0=`%%77C1lyxQ0H -zB-bZWYx(Nd?$mjFds7Q9ztppq7wUp_k-CsK6bwfB#dR~A=kkF%U#E9Ad2dEf^6zFk -zT$uCLrqY~`4+@-b=*NdcpYr -zYh~)0vtm;UaF^@`L$x-}LlTkeqiG{*kMMZO43uoX) -zZZ9)`Me5wt+IC2kTANy(NUh`jkz96d>-)}Lu^Mg#Pu@7d%nvb){DMbkvS+B+q6_6B%3Hw}kn{3vpJHm|%(xS1T2(*z^gJQl^9y7^!<_vK-R -zDW?T4)6|m~x)%)R(vhE1NCPLAM~*Fqizk(iMU}T=QfPs{lmo``c(8^`4oZ8NuOF2?@c#Bj@|LTb*Fr7MBcHJ -z8*MI68j2+6t0A&F-nV{j%0Fu8{fVnr_w>Kgl>7CCDzEf*BQV6!k*OHBkhR}F+YF*FzwFI?g(bIEw&)PY?@kEMOwpU8W*Pq=t -zd-5b+P+lNgKjrX;%WdZ7&ys+0?dSc%$ZH?(4`;7sbn}NsWCfBeF((D}Am|0xb5rN_ -z!Kw8epo_*_WKu5D3X;~lu&sL~sX8gZigSdhKfq@Ns&X0RvlQoTnjVeA*A<%g_AP^l -z0`KaLB0`G(Z-M$ovwtzqG3lSOT>po*nemSR5FdkISVfifh -zQ*rWAKo&j$zb_>CqbcCv@klx@_(87v)7|MrEbJ#=i$zKP$ur~+FDhS;`V)Z>r3;2a -zJ^`c^d?5rn3do*F(B}^gJ&D7U6zJ!n277M4qs!qWhX?Pi2MtrBH1rTa$km0!uv@`5di7v2OvhF+%foK6uz8*bA0H_ -za3dHzz3f6ToPe(eNm+scxK2XiZXuS1f&gC&Ls}sc3j4ze(s&RHKna2*pKyTWM{*I! -z*OL%~i~wdSKeP>e65_#-Tpr)>Unrh^8kC>5xAW -zi-*#X1z?XqJKV -za3cvUJqn);BvR=}G8Kg{#^Q-^6j)+11WZZ@b_;1=JQ4{(GJvZu4V(`cXAH^&aw(mN -z0|ElFr9RHatPgi_-4o#O9S<$Lx}*CI3@Vg5uux8>yL-Sz=1;k7!(Vb -zBf$fZWIygQjr_>-Y@Bn}z_;gG;=5|AntCLj%Tr()?yB9TY|oBff1nZ2%Yx}>*q;uhLa~TH9tHXX3=E~f -zHjPITX((nO5rfiocSmBNhy@c+mRL8C3kV}2C8U7=#}i2)6<`U#heD||Xk5THBB|~O -zkVYWjPsF>S0P%DJbm&Af0J1(Bi*%=wP>pb$P!!-%3~~=dL%{YxuL}bcAWC5@5l#T} -z2e|n|QK%=86+r>04mdGb1kfSNMMJ=&K)nnQ-A5oF -z0ShG@iUmNz5ZyI;#+-r#OU`WF{1d%GkU5br;g6?4EXO0sDDeFR@Ej;oBo>S&fsFHqNw0L(4co^`T6bSbSGyurRXb^N5A&~?H0W^DmpxaOC -zD#U@=LbZ~D5Qzc<6o8|FFefAnN<|cyTLM@!acP_e;R`hbHV^6)Fwz*vC18!9Ndb}r -ztARH`lS+e-f=ttqbaynF^aIz5kT1rfiFh28cz|azL0C#63{Rt=i~xNAH;P3A!4z;F -zP{$&mdH|ONwJiYpSFjtHbrLvx$_ENqifBOHK_N+07(mQ4ksgqBDggRg3ivK4R7oKk -z0)-)%i~|uRp)q68RCh8eBmhppqtX#TEKs&X2_X$)1(>i;0EH|NOecK*`TghjpWpxH -zZvyFR-Po6!9bdgHMf%esK$~Gc3c^p|!B*|$|Nbx6Wm`RC{n>phy5p-?k^a>gCk=W5 -zb%%nBMvd$wB|`xQ&c_D~XBhU%-it&d!$TINU)7&`5EF*SX$+*VJ~H$Qi0t7f+0?$_ -zk*MK&5&voT{Qkgc_X5MCFhlo7jJOQli-t#(C_6%87#<=K{Qr1}#M{0$wSkw%MhKV; -T!viJ;E(+{1YEXtu4BY<*Qm7~* - diff --git a/rhodecode/tests/fixtures/module_scoped_baseapp.py b/rhodecode/tests/fixtures/module_scoped_baseapp.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/fixtures/module_scoped_baseapp.py @@ -0,0 +1,49 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest +from rhodecode.tests.utils import CustomTestApp +from rhodecode.tests.fixtures.fixture_utils import plain_http_environ, plain_config_stub, plain_request_stub + + +@pytest.fixture(scope='module') +def module_request_stub(): + return plain_request_stub() + + +@pytest.fixture(scope='module') +def module_config_stub(request, module_request_stub): + return plain_config_stub(request, module_request_stub) + + +@pytest.fixture(scope='module') +def module_http_environ(): + """ + HTTP extra environ keys. + + Used by the test application and as well for setting up the pylons + environment. In the case of the fixture "app" it should be possible + to override this for a specific test case. + """ + return plain_http_environ() + + +@pytest.fixture(scope='module') +def module_app(request, module_config_stub, module_http_environ, baseapp): + app = CustomTestApp(baseapp, extra_environ=module_http_environ) + return app diff --git a/rhodecode/tests/fixture.py b/rhodecode/tests/fixtures/rc_fixture.py rename from rhodecode/tests/fixture.py rename to rhodecode/tests/fixtures/rc_fixture.py --- a/rhodecode/tests/fixture.py +++ b/rhodecode/tests/fixtures/rc_fixture.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -37,17 +36,16 @@ from rhodecode.model.user_group import U from rhodecode.model.gist import GistModel from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.scm import ScmModel -from rhodecode.authentication.plugins.auth_rhodecode import \ - RhodeCodeAuthPlugin +from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin from rhodecode.tests import TEST_USER_ADMIN_LOGIN dn = os.path.dirname -FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures') +FIXTURES = os.path.join(dn(os.path.abspath(__file__)), "diff_fixtures") def error_function(*args, **kwargs): - raise Exception('Total Crash !') + raise Exception("Total Crash !") class TestINI(object): @@ -59,8 +57,7 @@ class TestINI(object): print('paster server %s' % new_test_ini) """ - def __init__(self, ini_file_path, ini_params, new_file_prefix='DEFAULT', - destroy=True, dir=None): + def __init__(self, ini_file_path, ini_params, new_file_prefix="DEFAULT", destroy=True, dir=None): self.ini_file_path = ini_file_path self.ini_params = ini_params self.new_path = None @@ -85,9 +82,8 @@ class TestINI(object): parser[section][key] = str(val) with tempfile.NamedTemporaryFile( - mode='w', - prefix=self.new_path_prefix, suffix='.ini', dir=self._dir, - delete=False) as new_ini_file: + mode="w", prefix=self.new_path_prefix, suffix=".ini", dir=self._dir, delete=False + ) as new_ini_file: parser.write(new_ini_file) self.new_path = new_ini_file.name @@ -99,7 +95,6 @@ class TestINI(object): class Fixture(object): - def anon_access(self, status): """ Context process for disabling anonymous access. use like: @@ -139,22 +134,19 @@ class Fixture(object): class context(object): def _get_plugin(self): - plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid) + plugin_id = "egg:rhodecode-enterprise-ce#{}".format(RhodeCodeAuthPlugin.uid) plugin = RhodeCodeAuthPlugin(plugin_id) return plugin def __enter__(self): - plugin = self._get_plugin() - plugin.create_or_update_setting('auth_restriction', auth_restriction) + plugin.create_or_update_setting("auth_restriction", auth_restriction) Session().commit() SettingsModel().invalidate_settings_cache(hard=True) def __exit__(self, exc_type, exc_val, exc_tb): - plugin = self._get_plugin() - plugin.create_or_update_setting( - 'auth_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE) + plugin.create_or_update_setting("auth_restriction", RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE) Session().commit() SettingsModel().invalidate_settings_cache(hard=True) @@ -173,62 +165,61 @@ class Fixture(object): class context(object): def _get_plugin(self): - plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid) + plugin_id = "egg:rhodecode-enterprise-ce#{}".format(RhodeCodeAuthPlugin.uid) plugin = RhodeCodeAuthPlugin(plugin_id) return plugin def __enter__(self): plugin = self._get_plugin() - plugin.create_or_update_setting('scope_restriction', scope_restriction) + plugin.create_or_update_setting("scope_restriction", scope_restriction) Session().commit() SettingsModel().invalidate_settings_cache(hard=True) def __exit__(self, exc_type, exc_val, exc_tb): plugin = self._get_plugin() - plugin.create_or_update_setting( - 'scope_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL) + plugin.create_or_update_setting("scope_restriction", RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL) Session().commit() SettingsModel().invalidate_settings_cache(hard=True) return context() def _get_repo_create_params(self, **custom): - repo_type = custom.get('repo_type') or 'hg' + repo_type = custom.get("repo_type") or "hg" default_landing_ref, landing_ref_lbl = ScmModel.backend_landing_ref(repo_type) defs = { - 'repo_name': None, - 'repo_type': repo_type, - 'clone_uri': '', - 'push_uri': '', - 'repo_group': '-1', - 'repo_description': 'DESC', - 'repo_private': False, - 'repo_landing_commit_ref': default_landing_ref, - 'repo_copy_permissions': False, - 'repo_state': Repository.STATE_CREATED, + "repo_name": None, + "repo_type": repo_type, + "clone_uri": "", + "push_uri": "", + "repo_group": "-1", + "repo_description": "DESC", + "repo_private": False, + "repo_landing_commit_ref": default_landing_ref, + "repo_copy_permissions": False, + "repo_state": Repository.STATE_CREATED, } defs.update(custom) - if 'repo_name_full' not in custom: - defs.update({'repo_name_full': defs['repo_name']}) + if "repo_name_full" not in custom: + defs.update({"repo_name_full": defs["repo_name"]}) # fix the repo name if passed as repo_name_full - if defs['repo_name']: - defs['repo_name'] = defs['repo_name'].split('/')[-1] + if defs["repo_name"]: + defs["repo_name"] = defs["repo_name"].split("/")[-1] return defs def _get_group_create_params(self, **custom): defs = { - 'group_name': None, - 'group_description': 'DESC', - 'perm_updates': [], - 'perm_additions': [], - 'perm_deletions': [], - 'group_parent_id': -1, - 'enable_locking': False, - 'recursive': False, + "group_name": None, + "group_description": "DESC", + "perm_updates": [], + "perm_additions": [], + "perm_deletions": [], + "group_parent_id": -1, + "enable_locking": False, + "recursive": False, } defs.update(custom) @@ -236,16 +227,16 @@ class Fixture(object): def _get_user_create_params(self, name, **custom): defs = { - 'username': name, - 'password': 'qweqwe', - 'email': '%s+test@rhodecode.org' % name, - 'firstname': 'TestUser', - 'lastname': 'Test', - 'description': 'test description', - 'active': True, - 'admin': False, - 'extern_type': 'rhodecode', - 'extern_name': None, + "username": name, + "password": "qweqwe", + "email": "%s+test@rhodecode.org" % name, + "firstname": "TestUser", + "lastname": "Test", + "description": "test description", + "active": True, + "admin": False, + "extern_type": "rhodecode", + "extern_name": None, } defs.update(custom) @@ -253,30 +244,30 @@ class Fixture(object): def _get_user_group_create_params(self, name, **custom): defs = { - 'users_group_name': name, - 'user_group_description': 'DESC', - 'users_group_active': True, - 'user_group_data': {}, + "users_group_name": name, + "user_group_description": "DESC", + "users_group_active": True, + "user_group_data": {}, } defs.update(custom) return defs def create_repo(self, name, **kwargs): - repo_group = kwargs.get('repo_group') + repo_group = kwargs.get("repo_group") if isinstance(repo_group, RepoGroup): - kwargs['repo_group'] = repo_group.group_id + kwargs["repo_group"] = repo_group.group_id name = name.split(Repository.NAME_SEP)[-1] name = Repository.NAME_SEP.join((repo_group.group_name, name)) - if 'skip_if_exists' in kwargs: - del kwargs['skip_if_exists'] + if "skip_if_exists" in kwargs: + del kwargs["skip_if_exists"] r = Repository.get_by_repo_name(name) if r: return r form_data = self._get_repo_create_params(repo_name=name, **kwargs) - cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN) + cur_user = kwargs.get("cur_user", TEST_USER_ADMIN_LOGIN) RepoModel().create(form_data, cur_user) Session().commit() repo = Repository.get_by_repo_name(name) @@ -287,17 +278,15 @@ class Fixture(object): repo_to_fork = Repository.get_by_repo_name(repo_to_fork) form_data = self._get_repo_create_params( - repo_name=fork_name, - fork_parent_id=repo_to_fork.repo_id, - repo_type=repo_to_fork.repo_type, - **kwargs) + repo_name=fork_name, fork_parent_id=repo_to_fork.repo_id, repo_type=repo_to_fork.repo_type, **kwargs + ) # TODO: fix it !! - form_data['description'] = form_data['repo_description'] - form_data['private'] = form_data['repo_private'] - form_data['landing_rev'] = form_data['repo_landing_commit_ref'] + form_data["description"] = form_data["repo_description"] + form_data["private"] = form_data["repo_private"] + form_data["landing_rev"] = form_data["repo_landing_commit_ref"] - owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN) + owner = kwargs.get("cur_user", TEST_USER_ADMIN_LOGIN) RepoModel().create_fork(form_data, cur_user=owner) Session().commit() r = Repository.get_by_repo_name(fork_name) @@ -305,7 +294,7 @@ class Fixture(object): return r def destroy_repo(self, repo_name, **kwargs): - RepoModel().delete(repo_name, pull_requests='delete', artifacts='delete', **kwargs) + RepoModel().delete(repo_name, pull_requests="delete", artifacts="delete", **kwargs) Session().commit() def destroy_repo_on_filesystem(self, repo_name): @@ -314,17 +303,16 @@ class Fixture(object): shutil.rmtree(rm_path) def create_repo_group(self, name, **kwargs): - if 'skip_if_exists' in kwargs: - del kwargs['skip_if_exists'] + if "skip_if_exists" in kwargs: + del kwargs["skip_if_exists"] gr = RepoGroup.get_by_group_name(group_name=name) if gr: return gr form_data = self._get_group_create_params(group_name=name, **kwargs) - owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN) + owner = kwargs.get("cur_user", TEST_USER_ADMIN_LOGIN) gr = RepoGroupModel().create( - group_name=form_data['group_name'], - group_description=form_data['group_name'], - owner=owner) + group_name=form_data["group_name"], group_description=form_data["group_name"], owner=owner + ) Session().commit() gr = RepoGroup.get_by_group_name(gr.group_name) return gr @@ -334,8 +322,8 @@ class Fixture(object): Session().commit() def create_user(self, name, **kwargs): - if 'skip_if_exists' in kwargs: - del kwargs['skip_if_exists'] + if "skip_if_exists" in kwargs: + del kwargs["skip_if_exists"] user = User.get_by_username(name) if user: return user @@ -343,8 +331,7 @@ class Fixture(object): user = UserModel().create(form_data) # create token for user - AuthTokenModel().create( - user=user, description=u'TEST_USER_TOKEN') + AuthTokenModel().create(user=user, description="TEST_USER_TOKEN") Session().commit() user = User.get_by_username(user.username) @@ -368,22 +355,24 @@ class Fixture(object): Session().commit() def create_user_group(self, name, **kwargs): - if 'skip_if_exists' in kwargs: - del kwargs['skip_if_exists'] + if "skip_if_exists" in kwargs: + del kwargs["skip_if_exists"] gr = UserGroup.get_by_group_name(group_name=name) if gr: return gr # map active flag to the real attribute. For API consistency of fixtures - if 'active' in kwargs: - kwargs['users_group_active'] = kwargs['active'] - del kwargs['active'] + if "active" in kwargs: + kwargs["users_group_active"] = kwargs["active"] + del kwargs["active"] form_data = self._get_user_group_create_params(name, **kwargs) - owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN) + owner = kwargs.get("cur_user", TEST_USER_ADMIN_LOGIN) user_group = UserGroupModel().create( - name=form_data['users_group_name'], - description=form_data['user_group_description'], - owner=owner, active=form_data['users_group_active'], - group_data=form_data['user_group_data']) + name=form_data["users_group_name"], + description=form_data["user_group_description"], + owner=owner, + active=form_data["users_group_active"], + group_data=form_data["user_group_data"], + ) Session().commit() user_group = UserGroup.get_by_group_name(user_group.users_group_name) return user_group @@ -394,18 +383,23 @@ class Fixture(object): def create_gist(self, **kwargs): form_data = { - 'description': 'new-gist', - 'owner': TEST_USER_ADMIN_LOGIN, - 'gist_type': GistModel.cls.GIST_PUBLIC, - 'lifetime': -1, - 'acl_level': Gist.ACL_LEVEL_PUBLIC, - 'gist_mapping': {b'filename1.txt': {'content': b'hello world'},} + "description": "new-gist", + "owner": TEST_USER_ADMIN_LOGIN, + "gist_type": GistModel.cls.GIST_PUBLIC, + "lifetime": -1, + "acl_level": Gist.ACL_LEVEL_PUBLIC, + "gist_mapping": { + b"filename1.txt": {"content": b"hello world"}, + }, } form_data.update(kwargs) gist = GistModel().create( - description=form_data['description'], owner=form_data['owner'], - gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'], - lifetime=form_data['lifetime'], gist_acl_level=form_data['acl_level'] + description=form_data["description"], + owner=form_data["owner"], + gist_mapping=form_data["gist_mapping"], + gist_type=form_data["gist_type"], + lifetime=form_data["lifetime"], + gist_acl_level=form_data["acl_level"], ) Session().commit() return gist @@ -420,7 +414,7 @@ class Fixture(object): Session().commit() def load_resource(self, resource_name, strip=False): - with open(os.path.join(FIXTURES, resource_name), 'rb') as f: + with open(os.path.join(FIXTURES, resource_name), "rb") as f: source = f.read() if strip: source = source.strip() diff --git a/rhodecode/tests/fixtures/rcextensions_fixtures.py b/rhodecode/tests/fixtures/rcextensions_fixtures.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/fixtures/rcextensions_fixtures.py @@ -0,0 +1,157 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import shutil +import logging +import textwrap + +import pytest + +import rhodecode +import rhodecode.lib + +from rhodecode.tests import console_printer + +log = logging.getLogger(__name__) + + +def store_rcextensions(destination, force=False): + from rhodecode.config import rcextensions + package_path = rcextensions.__path__[0] + + # Note: rcextensions are looked up based on the path of the ini file + rcextensions_path = os.path.join(destination, 'rcextensions') + + if force: + shutil.rmtree(rcextensions_path, ignore_errors=True) + shutil.copytree(package_path, rcextensions_path) + + +@pytest.fixture(scope="module") +def rcextensions(request, tmp_storage_location): + """ + Installs a testing rcextensions pack to ensure they work as expected. + """ + + # Note: rcextensions are looked up based on the path of the ini file + rcextensions_path = os.path.join(tmp_storage_location, 'rcextensions') + + if os.path.exists(rcextensions_path): + pytest.fail( + f"Path for rcextensions already exists, please clean up before " + f"test run this path: {rcextensions_path}") + else: + store_rcextensions(tmp_storage_location) + + +@pytest.fixture(scope='function') +def rcextensions_present(request): + + class RcExtensionsPresent: + def __init__(self, rcextensions_location): + self.rcextensions_location = rcextensions_location + + def __enter__(self): + self.store() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.cleanup() + + def store(self): + store_rcextensions(self.rcextensions_location) + + def cleanup(self): + shutil.rmtree(os.path.join(self.rcextensions_location, 'rcextensions')) + + return RcExtensionsPresent + + +@pytest.fixture(scope='function') +def rcextensions_modification(request): + """ + example usage:: + + hook_name = '_pre_push_hook' + code = ''' + raise OSError('failed') + return HookResponse(1, 'FAILED') + ''' + mods = [ + (hook_name, code), + ] + # rhodecode.ini file location, where rcextensions needs to live + rcstack_location = os.path.dirname(rcstack.config_file) + with rcextensions_modification(rcstack_location, mods): + # do some stuff + """ + + class RcextensionsModification: + def __init__(self, rcextensions_location, mods, create_if_missing=False, force_create=False): + self.force_create = force_create + self.create_if_missing = create_if_missing + self.rcextensions_location = rcextensions_location + self.mods = mods + if not isinstance(mods, list): + raise ValueError('mods must be a list of modifications') + + def __enter__(self): + if self.create_if_missing: + store_rcextensions(self.rcextensions_location, force=self.force_create) + + for hook_name, method_body in self.mods: + self.modification(hook_name, method_body) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.cleanup() + + def cleanup(self): + # reset rcextensions to "bare" state from the package + store_rcextensions(self.rcextensions_location, force=True) + + def modification(self, hook_name, method_body): + import ast + + rcextensions_path = os.path.join(self.rcextensions_location, 'rcextensions') + + # Load the code from hooks.py + hooks_filename = os.path.join(rcextensions_path, 'hooks.py') + with open(hooks_filename, "r") as file: + tree = ast.parse(file.read()) + + # Define new content for the function as a string + new_code = textwrap.dedent(method_body) + + # Parse the new code to add it to the function + new_body = ast.parse(new_code).body + + # Walk through the AST to find and modify the function + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == hook_name: + node.body = new_body # Replace the function body with the new body + + # Compile the modified AST back to code + compile(tree, hooks_filename, "exec") + + # Write the updated code back to hooks.py + with open(hooks_filename, "w") as file: + file.write(ast.unparse(tree)) # Requires Python 3.9+ + + console_printer(f" [green]rcextensions[/green] Updated the body of '{hooks_filename}' function '{hook_name}'") + + return RcextensionsModification diff --git a/rhodecode/tests/functional/__init__.py b/rhodecode/tests/functional/__init__.py --- a/rhodecode/tests/functional/__init__.py +++ b/rhodecode/tests/functional/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/functional/test_bad_request_data.py b/rhodecode/tests/functional/test_bad_request_data.py --- a/rhodecode/tests/functional/test_bad_request_data.py +++ b/rhodecode/tests/functional/test_bad_request_data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/functional/test_delegated_admin.py b/rhodecode/tests/functional/test_delegated_admin.py --- a/rhodecode/tests/functional/test_delegated_admin.py +++ b/rhodecode/tests/functional/test_delegated_admin.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,7 +19,7 @@ import pytest from rhodecode.tests import TestController -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests.routes import route_path diff --git a/rhodecode/tests/functional/test_sessions.py b/rhodecode/tests/functional/test_sessions.py --- a/rhodecode/tests/functional/test_sessions.py +++ b/rhodecode/tests/functional/test_sessions.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/integrations/__init__.py b/rhodecode/tests/integrations/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/integrations/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/tests/integrations/conftest.py b/rhodecode/tests/integrations/conftest.py --- a/rhodecode/tests/integrations/conftest.py +++ b/rhodecode/tests/integrations/conftest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/integrations/test_integration.py b/rhodecode/tests/integrations/test_integration.py --- a/rhodecode/tests/integrations/test_integration.py +++ b/rhodecode/tests/integrations/test_integration.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,23 +20,21 @@ import time import pytest from rhodecode import events -from rhodecode.tests.fixture import Fixture +from rhodecode.model.repo import RepoModel +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.model.db import Session, Integration from rhodecode.model.integration import IntegrationModel class TestDeleteScopesDeletesIntegrations(object): - def test_delete_repo_with_integration_deletes_integration( - self, repo_integration_stub): - - Session().delete(repo_integration_stub.repo) + def test_delete_repo_with_integration_deletes_integration(self, repo_integration_stub): + RepoModel().delete(repo_integration_stub.repo) Session().commit() Session().expire_all() integration = Integration.get(repo_integration_stub.integration_id) assert integration is None - def test_delete_repo_group_with_integration_deletes_integration( - self, repogroup_integration_stub): + def test_delete_repo_group_with_integration_deletes_integration(self, repogroup_integration_stub): Session().delete(repogroup_integration_stub.repo_group) Session().commit() @@ -52,7 +50,7 @@ def counter(): global count val = count count += 1 - return '{}_{}'.format(val, time.time()) + return f'{val}_{time.time()}' @pytest.fixture() diff --git a/rhodecode/tests/integrations/test_slack.py b/rhodecode/tests/integrations/test_slack.py --- a/rhodecode/tests/integrations/test_slack.py +++ b/rhodecode/tests/integrations/test_slack.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/integrations/test_webhook.py b/rhodecode/tests/integrations/test_webhook.py --- a/rhodecode/tests/integrations/test_webhook.py +++ b/rhodecode/tests/integrations/test_webhook.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/__init__.py b/rhodecode/tests/lib/__init__.py --- a/rhodecode/tests/lib/__init__.py +++ b/rhodecode/tests/lib/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/auth_modules/__init__.py b/rhodecode/tests/lib/auth_modules/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/lib/auth_modules/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/tests/lib/auth_modules/test_auth_modules.py b/rhodecode/tests/lib/auth_modules/test_auth_modules.py --- a/rhodecode/tests/lib/auth_modules/test_auth_modules.py +++ b/rhodecode/tests/lib/auth_modules/test_auth_modules.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/__init__.py b/rhodecode/tests/lib/middleware/__init__.py --- a/rhodecode/tests/lib/middleware/__init__.py +++ b/rhodecode/tests/lib/middleware/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/mock_scm_app.py b/rhodecode/tests/lib/middleware/mock_scm_app.py --- a/rhodecode/tests/lib/middleware/mock_scm_app.py +++ b/rhodecode/tests/lib/middleware/mock_scm_app.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/test_appenlight.py b/rhodecode/tests/lib/middleware/test_appenlight.py --- a/rhodecode/tests/lib/middleware/test_appenlight.py +++ b/rhodecode/tests/lib/middleware/test_appenlight.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/test_simplegit.py b/rhodecode/tests/lib/middleware/test_simplegit.py --- a/rhodecode/tests/lib/middleware/test_simplegit.py +++ b/rhodecode/tests/lib/middleware/test_simplegit.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/test_simplehg.py b/rhodecode/tests/lib/middleware/test_simplehg.py --- a/rhodecode/tests/lib/middleware/test_simplehg.py +++ b/rhodecode/tests/lib/middleware/test_simplehg.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -123,10 +122,14 @@ def test_get_config(user_util, baseapp, ('web', 'allow_push', '*'), ('web', 'allow_archive', 'gz zip bz2'), ('web', 'baseurl', '/'), + + # largefiles data... ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')), + ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')), + ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'), ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'), - ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')), + ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'), ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'), ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'), diff --git a/rhodecode/tests/lib/middleware/test_simplesvn.py b/rhodecode/tests/lib/middleware/test_simplesvn.py --- a/rhodecode/tests/lib/middleware/test_simplesvn.py +++ b/rhodecode/tests/lib/middleware/test_simplesvn.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/test_simplevcs.py b/rhodecode/tests/lib/middleware/test_simplevcs.py --- a/rhodecode/tests/lib/middleware/test_simplevcs.py +++ b/rhodecode/tests/lib/middleware/test_simplevcs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +21,8 @@ import pytest from rhodecode.lib.str_utils import base64_to_str from rhodecode.lib.utils2 import AttributeDict -from rhodecode.tests.utils import CustomTestApp +from rhodecode.tests.fixtures.fixture_pyramid import ini_config +from rhodecode.tests.utils import CustomTestApp, AuthPluginManager from rhodecode.lib.caching_query import FromCache from rhodecode.lib.middleware import simplevcs @@ -34,6 +34,57 @@ from rhodecode.tests import ( HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) from rhodecode.tests.lib.middleware import mock_scm_app +from rhodecode.model.db import Permission, User +from rhodecode.model.meta import Session +from rhodecode.model.user import UserModel + + +@pytest.fixture() +def enable_auth_plugins(request, app): + """ + Return a factory object that when called, allows to control which + authentication plugins are enabled. + """ + + enabler = AuthPluginManager() + request.addfinalizer(enabler.cleanup) + + return enabler + + +@pytest.fixture() +def test_user_factory(request, baseapp): + + def user_factory(username='test_user', password='qweqwe', first_name='John', last_name='Testing', **kwargs): + usr = UserModel().create_or_update( + username=username, + password=password, + email=f'{username}@rhodecode.org', + firstname=first_name, lastname=last_name) + Session().commit() + + for k, v in kwargs.items(): + setattr(usr, k, v) + Session().add(usr) + + new_usr = User.get_by_username(username) + new_usr_id = new_usr.user_id + assert new_usr == usr + + @request.addfinalizer + def cleanup(): + if User.get(new_usr_id) is None: + return + + perm = Permission.query().all() + for p in perm: + UserModel().revoke_perm(usr, p) + + UserModel().delete(new_usr_id) + Session().commit() + return usr + + return user_factory class StubVCSController(simplevcs.SimpleVCS): @@ -107,8 +158,7 @@ def _remove_default_user_from_query_cach Session().expire(user) -def test_handles_exceptions_during_permissions_checks( - vcscontroller, disable_anonymous_user, enable_auth_plugins, test_user_factory): +def test_handles_exceptions_during_permissions_checks(vcscontroller, disable_anonymous_user, enable_auth_plugins, test_user_factory): test_password = 'qweqwe' test_user = test_user_factory(password=test_password, extern_type='headers', extern_name='headers') @@ -373,29 +423,30 @@ class TestShadowRepoExposure(object): controller.vcs_repo_name) -@pytest.mark.usefixtures('baseapp') class TestGenerateVcsResponse(object): - def test_ensures_that_start_response_is_called_early_enough(self): - self.call_controller_with_response_body(iter(['a', 'b'])) + def test_ensures_that_start_response_is_called_early_enough(self, baseapp): + app_ini_config = baseapp.config.registry.settings['__file__'] + self.call_controller_with_response_body(app_ini_config, iter(['a', 'b'])) assert self.start_response.called - def test_invalidates_cache_after_body_is_consumed(self): - result = self.call_controller_with_response_body(iter(['a', 'b'])) + def test_invalidates_cache_after_body_is_consumed(self, baseapp): + app_ini_config = baseapp.config.registry.settings['__file__'] + result = self.call_controller_with_response_body(app_ini_config, iter(['a', 'b'])) assert not self.was_cache_invalidated() # Consume the result list(result) assert self.was_cache_invalidated() - def test_raises_unknown_exceptions(self): - result = self.call_controller_with_response_body( - self.raise_result_iter(vcs_kind='unknown')) + def test_raises_unknown_exceptions(self, baseapp): + app_ini_config = baseapp.config.registry.settings['__file__'] + result = self.call_controller_with_response_body(app_ini_config, self.raise_result_iter(vcs_kind='unknown')) with pytest.raises(Exception): list(result) - def call_controller_with_response_body(self, response_body): + def call_controller_with_response_body(self, ini_config, response_body): + settings = { - 'base_path': 'fake_base_path', 'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.direct_calls': False, } @@ -407,7 +458,7 @@ class TestGenerateVcsResponse(object): result = controller._generate_vcs_response( environ={}, start_response=self.start_response, repo_path='fake_repo_path', - extras={}, action='push') + extras={'config': ini_config}, action='push') self.controller = controller return result diff --git a/rhodecode/tests/lib/middleware/test_vcs.py b/rhodecode/tests/lib/middleware/test_vcs.py --- a/rhodecode/tests/lib/middleware/test_vcs.py +++ b/rhodecode/tests/lib/middleware/test_vcs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/test_vcs_unavailable.py b/rhodecode/tests/lib/middleware/test_vcs_unavailable.py --- a/rhodecode/tests/lib/middleware/test_vcs_unavailable.py +++ b/rhodecode/tests/lib/middleware/test_vcs_unavailable.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/utils/__init__.py b/rhodecode/tests/lib/middleware/utils/__init__.py --- a/rhodecode/tests/lib/middleware/utils/__init__.py +++ b/rhodecode/tests/lib/middleware/utils/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/utils/test_scm_app_http.py b/rhodecode/tests/lib/middleware/utils/test_scm_app_http.py --- a/rhodecode/tests/lib/middleware/utils/test_scm_app_http.py +++ b/rhodecode/tests/lib/middleware/utils/test_scm_app_http.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,6 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import pytest +import tempfile from rhodecode.tests.utils import CustomTestApp from rhodecode.lib.middleware.utils import scm_app_http, scm_app @@ -41,10 +40,13 @@ def vcsserver_http_echo_app(request, vcs """ A running VCSServer with the EchoApp activated via HTTP. """ - vcsserver = vcsserver_factory( + store_dir = tempfile.gettempdir() + + vcsserver_instance = vcsserver_factory( request=request, + store_dir=store_dir, overrides=[{'app:main': {'dev.use_echo_app': 'true'}}]) - return vcsserver + return vcsserver_instance @pytest.fixture(scope='session') diff --git a/rhodecode/tests/lib/middleware/utils/test_scm_app_http_chunking.py b/rhodecode/tests/lib/middleware/utils/test_scm_app_http_chunking.py --- a/rhodecode/tests/lib/middleware/utils/test_scm_app_http_chunking.py +++ b/rhodecode/tests/lib/middleware/utils/test_scm_app_http_chunking.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/middleware/utils/test_wsgi_app_caller_client.py b/rhodecode/tests/lib/middleware/utils/test_wsgi_app_caller_client.py --- a/rhodecode/tests/lib/middleware/utils/test_wsgi_app_caller_client.py +++ b/rhodecode/tests/lib/middleware/utils/test_wsgi_app_caller_client.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_action_parser.py b/rhodecode/tests/lib/test_action_parser.py --- a/rhodecode/tests/lib/test_action_parser.py +++ b/rhodecode/tests/lib/test_action_parser.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_archive_caches.py b/rhodecode/tests/lib/test_archive_caches.py --- a/rhodecode/tests/lib/test_archive_caches.py +++ b/rhodecode/tests/lib/test_archive_caches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_audit_logger.py b/rhodecode/tests/lib/test_audit_logger.py --- a/rhodecode/tests/lib/test_audit_logger.py +++ b/rhodecode/tests/lib/test_audit_logger.py @@ -1,6 +1,4 @@ -import collections - -# Copyright (C) 2010-2023 RhodeCode GmbH +import collections# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_auth.py b/rhodecode/tests/lib/test_auth.py --- a/rhodecode/tests/lib/test_auth.py +++ b/rhodecode/tests/lib/test_auth.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_auth_crypto_backend.py b/rhodecode/tests/lib/test_auth_crypto_backend.py --- a/rhodecode/tests/lib/test_auth_crypto_backend.py +++ b/rhodecode/tests/lib/test_auth_crypto_backend.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_base.py b/rhodecode/tests/lib/test_base.py --- a/rhodecode/tests/lib/test_base.py +++ b/rhodecode/tests/lib/test_base.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_caches.py b/rhodecode/tests/lib/test_caches.py --- a/rhodecode/tests/lib/test_caches.py +++ b/rhodecode/tests/lib/test_caches.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_codeblocks.py b/rhodecode/tests/lib/test_codeblocks.py --- a/rhodecode/tests/lib/test_codeblocks.py +++ b/rhodecode/tests/lib/test_codeblocks.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_colored_formatter.py b/rhodecode/tests/lib/test_colored_formatter.py --- a/rhodecode/tests/lib/test_colored_formatter.py +++ b/rhodecode/tests/lib/test_colored_formatter.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_datelib.py b/rhodecode/tests/lib/test_datelib.py --- a/rhodecode/tests/lib/test_datelib.py +++ b/rhodecode/tests/lib/test_datelib.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_db_manage.py b/rhodecode/tests/lib/test_db_manage.py --- a/rhodecode/tests/lib/test_db_manage.py +++ b/rhodecode/tests/lib/test_db_manage.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_dbmigrate.py b/rhodecode/tests/lib/test_dbmigrate.py --- a/rhodecode/tests/lib/test_dbmigrate.py +++ b/rhodecode/tests/lib/test_dbmigrate.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_diffs.py b/rhodecode/tests/lib/test_diffs.py --- a/rhodecode/tests/lib/test_diffs.py +++ b/rhodecode/tests/lib/test_diffs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -30,7 +29,7 @@ from rhodecode.lib.diffs import ( from rhodecode.lib.utils2 import AttributeDict from rhodecode.lib.vcs.backends.git import GitCommit -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.tests import no_newline_id_generator from rhodecode.lib.vcs.backends.git.repository import GitDiff from rhodecode.lib.vcs.backends.hg.repository import MercurialDiff diff --git a/rhodecode/tests/lib/test_diffs_context.py b/rhodecode/tests/lib/test_diffs_context.py --- a/rhodecode/tests/lib/test_diffs_context.py +++ b/rhodecode/tests/lib/test_diffs_context.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_encrypt.py b/rhodecode/tests/lib/test_encrypt.py --- a/rhodecode/tests/lib/test_encrypt.py +++ b/rhodecode/tests/lib/test_encrypt.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_ext_json.py b/rhodecode/tests/lib/test_ext_json.py --- a/rhodecode/tests/lib/test_ext_json.py +++ b/rhodecode/tests/lib/test_ext_json.py @@ -1,5 +1,4 @@ -import dataclasses -# Copyright (C) 2010-2023 RhodeCode GmbH +import dataclasses# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_graphmod.py b/rhodecode/tests/lib/test_graphmod.py --- a/rhodecode/tests/lib/test_graphmod.py +++ b/rhodecode/tests/lib/test_graphmod.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_helpers.py b/rhodecode/tests/lib/test_helpers.py --- a/rhodecode/tests/lib/test_helpers.py +++ b/rhodecode/tests/lib/test_helpers.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_hooks_base.py b/rhodecode/tests/lib/test_hooks_base.py --- a/rhodecode/tests/lib/test_hooks_base.py +++ b/rhodecode/tests/lib/test_hooks_base.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_hooks_daemon.py b/rhodecode/tests/lib/test_hooks_daemon.py --- a/rhodecode/tests/lib/test_hooks_daemon.py +++ b/rhodecode/tests/lib/test_hooks_daemon.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -18,310 +17,77 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging -import io import mock -import msgpack import pytest import tempfile -from rhodecode.lib.hook_daemon import http_hooks_deamon from rhodecode.lib.hook_daemon import celery_hooks_deamon -from rhodecode.lib.hook_daemon import hook_module +from rhodecode.lib.hook_daemon import utils as hooks_utils from rhodecode.lib.hook_daemon import base as hook_base -from rhodecode.lib.str_utils import safe_bytes + from rhodecode.tests.utils import assert_message_in_log -from rhodecode.lib.ext_json import json - -test_proto = http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO class TestHooks(object): def test_hooks_can_be_used_as_a_context_processor(self): - hooks = hook_module.Hooks() + hooks = hook_base.Hooks() with hooks as return_value: pass assert hooks == return_value - -class TestHooksHttpHandler(object): - def test_read_request_parses_method_name_and_arguments(self): - data = { - 'method': 'test', - 'extras': { - 'param1': 1, - 'param2': 'a' - } - } - request = self._generate_post_request(data) - hooks_patcher = mock.patch.object( - hook_module.Hooks, data['method'], create=True, return_value=1) - - with hooks_patcher as hooks_mock: - handler = http_hooks_deamon.HooksHttpHandler - handler.DEFAULT_HOOKS_PROTO = test_proto - handler.wbufsize = 10240 - MockServer(handler, request) - - hooks_mock.assert_called_once_with(data['extras']) - - def test_hooks_serialized_result_is_returned(self): - request = self._generate_post_request({}) - rpc_method = 'test' - hook_result = { - 'first': 'one', - 'second': 2 - } - extras = {} - - # patching our _read to return test method and proto used - read_patcher = mock.patch.object( - http_hooks_deamon.HooksHttpHandler, '_read_request', - return_value=(test_proto, rpc_method, extras)) - - # patch Hooks instance to return hook_result data on 'test' call - hooks_patcher = mock.patch.object( - hook_module.Hooks, rpc_method, create=True, - return_value=hook_result) - - with read_patcher, hooks_patcher: - handler = http_hooks_deamon.HooksHttpHandler - handler.DEFAULT_HOOKS_PROTO = test_proto - handler.wbufsize = 10240 - server = MockServer(handler, request) - - expected_result = http_hooks_deamon.HooksHttpHandler.serialize_data(hook_result) - - server.request.output_stream.seek(0) - assert server.request.output_stream.readlines()[-1] == expected_result - - def test_exception_is_returned_in_response(self): - request = self._generate_post_request({}) - rpc_method = 'test' - - read_patcher = mock.patch.object( - http_hooks_deamon.HooksHttpHandler, '_read_request', - return_value=(test_proto, rpc_method, {})) - - hooks_patcher = mock.patch.object( - hook_module.Hooks, rpc_method, create=True, - side_effect=Exception('Test exception')) - - with read_patcher, hooks_patcher: - handler = http_hooks_deamon.HooksHttpHandler - handler.DEFAULT_HOOKS_PROTO = test_proto - handler.wbufsize = 10240 - server = MockServer(handler, request) - - server.request.output_stream.seek(0) - data = server.request.output_stream.readlines() - msgpack_data = b''.join(data[5:]) - org_exc = http_hooks_deamon.HooksHttpHandler.deserialize_data(msgpack_data) - expected_result = { - 'exception': 'Exception', - 'exception_traceback': org_exc['exception_traceback'], - 'exception_args': ['Test exception'] - } - assert org_exc == expected_result - - def test_log_message_writes_to_debug_log(self, caplog): - ip_port = ('0.0.0.0', 8888) - handler = http_hooks_deamon.HooksHttpHandler(MockRequest('POST /'), ip_port, mock.Mock()) - fake_date = '1/Nov/2015 00:00:00' - date_patcher = mock.patch.object( - handler, 'log_date_time_string', return_value=fake_date) - - with date_patcher, caplog.at_level(logging.DEBUG): - handler.log_message('Some message %d, %s', 123, 'string') - - expected_message = f"HOOKS: client={ip_port} - - [{fake_date}] Some message 123, string" - - assert_message_in_log( - caplog.records, expected_message, - levelno=logging.DEBUG, module='http_hooks_deamon') - - def _generate_post_request(self, data, proto=test_proto): - if proto == http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO: - payload = msgpack.packb(data) - else: - payload = json.dumps(data) - - return b'POST / HTTP/1.0\nContent-Length: %d\n\n%b' % ( - len(payload), payload) - - -class ThreadedHookCallbackDaemon(object): - def test_constructor_calls_prepare(self): - prepare_daemon_patcher = mock.patch.object( - http_hooks_deamon.ThreadedHookCallbackDaemon, '_prepare') - with prepare_daemon_patcher as prepare_daemon_mock: - http_hooks_deamon.ThreadedHookCallbackDaemon() - prepare_daemon_mock.assert_called_once_with() - - def test_run_is_called_on_context_start(self): - patchers = mock.patch.multiple( - http_hooks_deamon.ThreadedHookCallbackDaemon, - _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT) - - with patchers as mocks: - daemon = http_hooks_deamon.ThreadedHookCallbackDaemon() - with daemon as daemon_context: - pass - mocks['_run'].assert_called_once_with() - assert daemon_context == daemon - - def test_stop_is_called_on_context_exit(self): - patchers = mock.patch.multiple( - http_hooks_deamon.ThreadedHookCallbackDaemon, - _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT) - - with patchers as mocks: - daemon = http_hooks_deamon.ThreadedHookCallbackDaemon() - with daemon as daemon_context: - assert mocks['_stop'].call_count == 0 - - mocks['_stop'].assert_called_once_with() - assert daemon_context == daemon - - -class TestHttpHooksCallbackDaemon(object): - def test_hooks_callback_generates_new_port(self, caplog): - with caplog.at_level(logging.DEBUG): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) - assert daemon._daemon.server_address == ('127.0.0.1', 8881) - - with caplog.at_level(logging.DEBUG): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host=None, port=None) - assert daemon._daemon.server_address[1] in range(0, 66000) - assert daemon._daemon.server_address[0] != '127.0.0.1' - - def test_prepare_inits_daemon_variable(self, tcp_server, caplog): - with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) - assert daemon._daemon == tcp_server - - _, port = tcp_server.server_address - - msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \ - f"hook object: " - assert_message_in_log( - caplog.records, msg, levelno=logging.DEBUG, module='http_hooks_deamon') - - def test_prepare_inits_hooks_uri_and_logs_it( - self, tcp_server, caplog): - with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) - - _, port = tcp_server.server_address - expected_uri = '{}:{}'.format('127.0.0.1', port) - assert daemon.hooks_uri == expected_uri - - msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \ - f"hook object: " - - assert_message_in_log( - caplog.records, msg, - levelno=logging.DEBUG, module='http_hooks_deamon') - - def test_run_creates_a_thread(self, tcp_server): - thread = mock.Mock() - - with self._tcp_patcher(tcp_server): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon() - - with self._thread_patcher(thread) as thread_mock: - daemon._run() - - thread_mock.assert_called_once_with( - target=tcp_server.serve_forever, - kwargs={'poll_interval': daemon.POLL_INTERVAL}) - assert thread.daemon is True - thread.start.assert_called_once_with() - - def test_run_logs(self, tcp_server, caplog): - - with self._tcp_patcher(tcp_server): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon() - - with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG): - daemon._run() - - assert_message_in_log( - caplog.records, - 'Running thread-based loop of callback daemon in background', - levelno=logging.DEBUG, module='http_hooks_deamon') - - def test_stop_cleans_up_the_connection(self, tcp_server, caplog): - thread = mock.Mock() - - with self._tcp_patcher(tcp_server): - daemon = http_hooks_deamon.HttpHooksCallbackDaemon() - - with self._thread_patcher(thread), caplog.at_level(logging.DEBUG): - with daemon: - assert daemon._daemon == tcp_server - assert daemon._callback_thread == thread - - assert daemon._daemon is None - assert daemon._callback_thread is None - tcp_server.shutdown.assert_called_with() - thread.join.assert_called_once_with() - - assert_message_in_log( - caplog.records, 'Waiting for background thread to finish.', - levelno=logging.DEBUG, module='http_hooks_deamon') - - def _tcp_patcher(self, tcp_server): - return mock.patch.object( - http_hooks_deamon, 'TCPServer', return_value=tcp_server) - - def _thread_patcher(self, thread): - return mock.patch.object( - http_hooks_deamon.threading, 'Thread', return_value=thread) - - class TestPrepareHooksDaemon(object): @pytest.mark.parametrize('protocol', ('celery',)) - def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified( - self, protocol): + def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified(self, protocol): with tempfile.NamedTemporaryFile(mode='w') as temp_file: - temp_file.write("[app:main]\ncelery.broker_url = redis://redis/0\n" - "celery.result_backend = redis://redis/0") + temp_file.write( + "[app:main]\n" + "celery.broker_url = redis://redis/0\n" + "celery.result_backend = redis://redis/0\n" + ) temp_file.flush() expected_extras = {'config': temp_file.name} - callback, extras = hook_base.prepare_callback_daemon( - expected_extras, protocol=protocol, host='') + callback, extras = hooks_utils.prepare_callback_daemon(expected_extras, protocol=protocol) assert isinstance(callback, celery_hooks_deamon.CeleryHooksCallbackDaemon) @pytest.mark.parametrize('protocol, expected_class', ( - ('http', http_hooks_deamon.HttpHooksCallbackDaemon), + ('celery', celery_hooks_deamon.CeleryHooksCallbackDaemon), )) - def test_returns_real_hooks_callback_daemon_when_protocol_is_specified( - self, protocol, expected_class): - expected_extras = { - 'extra1': 'value1', - 'txn_id': 'txnid2', - 'hooks_protocol': protocol.lower(), - 'task_backend': '', - 'task_queue': '', - 'repo_store': '/var/opt/rhodecode_repo_store', - 'repository': 'rhodecode', - } - from rhodecode import CONFIG - CONFIG['vcs.svn.redis_conn'] = 'redis://redis:6379/0' - callback, extras = hook_base.prepare_callback_daemon( - expected_extras.copy(), protocol=protocol, host='127.0.0.1', - txn_id='txnid2') - assert isinstance(callback, expected_class) - extras.pop('hooks_uri') - expected_extras['time'] = extras['time'] - assert extras == expected_extras + def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(self, protocol, expected_class): + + with tempfile.NamedTemporaryFile(mode='w') as temp_file: + temp_file.write( + "[app:main]\n" + "celery.broker_url = redis://redis:6379/0\n" + "celery.result_backend = redis://redis:6379/0\n" + ) + temp_file.flush() + + expected_extras = { + 'extra1': 'value1', + 'txn_id': 'txnid2', + 'hooks_protocol': protocol.lower(), + 'hooks_config': { + 'broker_url': 'redis://redis:6379/0', + 'result_backend': 'redis://redis:6379/0', + }, + 'repo_store': '/var/opt/rhodecode_repo_store', + 'repository': 'rhodecode', + 'config': temp_file.name + } + from rhodecode import CONFIG + CONFIG['vcs.svn.redis_conn'] = 'redis://redis:6379/0' + callback, extras = hooks_utils.prepare_callback_daemon(expected_extras.copy(), protocol=protocol,txn_id='txnid2') + assert isinstance(callback, expected_class) + expected_extras['time'] = extras['time'] + assert extras == expected_extras @pytest.mark.parametrize('protocol', ( 'invalid', 'Http', 'HTTP', + 'celerY' )) def test_raises_on_invalid_protocol(self, protocol): expected_extras = { @@ -329,35 +95,4 @@ class TestPrepareHooksDaemon(object): 'hooks_protocol': protocol.lower() } with pytest.raises(Exception): - callback, extras = hook_base.prepare_callback_daemon( - expected_extras.copy(), - protocol=protocol, host='127.0.0.1') - - -class MockRequest(object): - - def __init__(self, request): - self.request = request - self.input_stream = io.BytesIO(safe_bytes(self.request)) - self.output_stream = io.BytesIO() # make it un-closable for testing invesitagion - self.output_stream.close = lambda: None - - def makefile(self, mode, *args, **kwargs): - return self.output_stream if mode == 'wb' else self.input_stream - - -class MockServer(object): - - def __init__(self, handler_cls, request): - ip_port = ('0.0.0.0', 8888) - self.request = MockRequest(request) - self.server_address = ip_port - self.handler = handler_cls(self.request, ip_port, self) - - -@pytest.fixture() -def tcp_server(): - server = mock.Mock() - server.server_address = ('127.0.0.1', 8881) - server.wbufsize = 1024 - return server + callback, extras = hooks_utils.prepare_callback_daemon(expected_extras.copy(), protocol=protocol) diff --git a/rhodecode/tests/lib/test_html_sanitizer.py b/rhodecode/tests/lib/test_html_sanitizer.py --- a/rhodecode/tests/lib/test_html_sanitizer.py +++ b/rhodecode/tests/lib/test_html_sanitizer.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_jsonalchemy.py b/rhodecode/tests/lib/test_jsonalchemy.py --- a/rhodecode/tests/lib/test_jsonalchemy.py +++ b/rhodecode/tests/lib/test_jsonalchemy.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_mako_emails.py b/rhodecode/tests/lib/test_mako_emails.py --- a/rhodecode/tests/lib/test_mako_emails.py +++ b/rhodecode/tests/lib/test_mako_emails.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_markup_renderer.py b/rhodecode/tests/lib/test_markup_renderer.py --- a/rhodecode/tests/lib/test_markup_renderer.py +++ b/rhodecode/tests/lib/test_markup_renderer.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_search.py b/rhodecode/tests/lib/test_search.py --- a/rhodecode/tests/lib/test_search.py +++ b/rhodecode/tests/lib/test_search.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_search_utils.py b/rhodecode/tests/lib/test_search_utils.py --- a/rhodecode/tests/lib/test_search_utils.py +++ b/rhodecode/tests/lib/test_search_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/lib/test_utils.py b/rhodecode/tests/lib/test_utils.py --- a/rhodecode/tests/lib/test_utils.py +++ b/rhodecode/tests/lib/test_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,21 +18,26 @@ import multiprocessing import os +import shutil import mock import py import pytest +import rhodecode from rhodecode.lib import caching_query from rhodecode.lib import utils from rhodecode.lib.str_utils import safe_bytes from rhodecode.model import settings from rhodecode.model import db from rhodecode.model import meta +from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.scm import ScmModel from rhodecode.model.settings import UiSetting, SettingsModel -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.fixture_pyramid import rhodecode_factory +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode_tools.lib.hash_utils import md5_safe from rhodecode.lib.ext_json import json @@ -231,7 +235,7 @@ def platform_encodes_filenames(): return path_with_latin1 != read_path -def test_repo2db_mapper_groups(repo_groups): +def test_repo2db_cleaner_removes_zombie_groups(repo_groups): session = meta.Session() zombie_group, parent_group, child_group = repo_groups zombie_path = os.path.join( @@ -239,10 +243,9 @@ def test_repo2db_mapper_groups(repo_grou os.rmdir(zombie_path) # Avoid removing test repos when calling repo2db_mapper - repo_list = { - repo.repo_name: 'test' for repo in session.query(db.Repository).all() - } - utils.repo2db_mapper(repo_list, remove_obsolete=True) + repo_list = [repo.repo_name for repo in session.query(db.Repository).all()] + + utils.repo2db_cleanup(skip_repos=repo_list) groups_in_db = session.query(db.RepoGroup).all() assert child_group in groups_in_db @@ -250,20 +253,68 @@ def test_repo2db_mapper_groups(repo_grou assert zombie_path not in groups_in_db -def test_repo2db_mapper_enables_largefiles(backend): + +@pytest.mark.backends("hg", "git", "svn") +def test_repo2db_cleaner_removes_zombie_repos(backend): repo = backend.create_repo() - repo_list = {repo.repo_name: 'test'} - with mock.patch('rhodecode.model.db.Repository.scm_instance') as scm_mock: - utils.repo2db_mapper(repo_list, remove_obsolete=False) - _, kwargs = scm_mock.call_args - assert kwargs['config'].get('extensions', 'largefiles') == '' + zombie_path = repo.repo_full_path + shutil.rmtree(zombie_path) + + removed, errors = utils.repo2db_cleanup() + assert len(removed) == 1 + assert not errors -@pytest.mark.backends("git", "svn") +def test_repo2db_mapper_adds_new_repos(request, backend): + repo = backend.create_repo() + cleanup_repos = [] + cleanup_groups = [] + for num in range(5): + copy_repo_name = f'{repo.repo_name}-{num}' + copy_repo_path = f'{repo.repo_full_path}-{num}' + + shutil.copytree(repo.repo_full_path, copy_repo_path) + cleanup_repos.append(copy_repo_name) + + for gr_num in range(5): + gr_name = f'my_gr_{gr_num}' + dest_gr = os.path.join(os.path.dirname(repo.repo_full_path), gr_name) + os.makedirs(dest_gr, exist_ok=True) + + copy_repo_name = f'{gr_name}/{repo.repo_name}-{gr_num}' + copy_repo_path = f'{dest_gr}/{repo.repo_name}-{gr_num}' + + shutil.copytree(repo.repo_full_path, copy_repo_path) + cleanup_repos.append(copy_repo_name) + cleanup_groups.append(gr_name) + + repo_list = ScmModel().repo_scan() + + added, errors = utils.repo2db_mapper(repo_list) + Session().commit() + assert not errors + + assert len(added) == 10 + + @request.addfinalizer + def cleanup(): + for _repo in cleanup_repos: + del_result = RepoModel().delete(_repo, call_events=False) + Session().commit() + assert del_result is True + + for _repo_group in cleanup_groups: + del_result = RepoGroupModel().delete(_repo_group, force_delete=True, call_events=False) + Session().commit() + assert del_result is True + + def test_repo2db_mapper_installs_hooks_for_repos_in_db(backend): repo = backend.create_repo() repo_list = {repo.repo_name: 'test'} - utils.repo2db_mapper(repo_list, remove_obsolete=False) + added, errors = utils.repo2db_mapper(repo_list) + assert not errors + assert repo.scm_instance().get_hooks_info() == {'pre_version': rhodecode.__version__, 'post_version': rhodecode.__version__} @pytest.mark.backends("git", "svn") @@ -272,7 +323,9 @@ def test_repo2db_mapper_installs_hooks_f RepoModel().delete(repo, fs_remove=False) meta.Session().commit() repo_list = {repo.repo_name: repo.scm_instance()} - utils.repo2db_mapper(repo_list, remove_obsolete=False) + added, errors = utils.repo2db_mapper(repo_list) + assert not errors + assert len(added) == 1 class TestPasswordChanged(object): @@ -403,12 +456,9 @@ class TestPrepareConfigData(object): self._assert_repo_name_passed(model_mock, repo_name) - expected_result = [ - ('section1', 'option1', 'value1'), - ('section2', 'option2', 'value2'), - ] - # We have extra config items returned, so we're ignoring two last items - assert result[:2] == expected_result + assert ('section1', 'option1', 'value1') in result + assert ('section2', 'option2', 'value2') in result + assert ('section3', 'option3', 'value3') not in result def _assert_repo_name_passed(self, model_mock, repo_name): assert model_mock.call_count == 1 @@ -457,7 +507,7 @@ class TestGetEnabledHooks(object): def test_obfuscate_url_pw(): from rhodecode.lib.utils2 import obfuscate_url_pw - engine = u'/home/repos/malmö' + engine = '/home/repos/malmö' assert obfuscate_url_pw(engine) diff --git a/rhodecode/tests/load/api.py b/rhodecode/tests/load/api.py --- a/rhodecode/tests/load/api.py +++ b/rhodecode/tests/load/api.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/load/http_performance.py b/rhodecode/tests/load/http_performance.py --- a/rhodecode/tests/load/http_performance.py +++ b/rhodecode/tests/load/http_performance.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,7 +24,7 @@ It works by replaying a group of commits import argparse import collections -import ConfigParser +import configparser import functools import itertools import os @@ -294,7 +293,7 @@ class HgMixin(object): def add_remote(self, repo, remote_url, remote_name='upstream'): self.remove_remote(repo, remote_name) os.chdir(repo) - hgrc = ConfigParser.RawConfigParser() + hgrc = configparser.RawConfigParser() hgrc.read('.hg/hgrc') hgrc.set('paths', remote_name, remote_url) with open('.hg/hgrc', 'w') as f: @@ -303,7 +302,7 @@ class HgMixin(object): @keep_cwd def remove_remote(self, repo, remote_name='upstream'): os.chdir(repo) - hgrc = ConfigParser.RawConfigParser() + hgrc = configparser.RawConfigParser() hgrc.read('.hg/hgrc') hgrc.remove_option('paths', remote_name) with open('.hg/hgrc', 'w') as f: diff --git a/rhodecode/tests/load/profile-mem.py b/rhodecode/tests/load/profile-mem.py --- a/rhodecode/tests/load/profile-mem.py +++ b/rhodecode/tests/load/profile-mem.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -59,16 +58,6 @@ def parse_options(): parser.add_argument( '--interval', '-i', type=float, default=5, help="Interval in secods.") - parser.add_argument( - '--appenlight', '--ae', action='store_true') - parser.add_argument( - '--appenlight-url', '--ae-url', - default='https://ae.rhodecode.com/api/logs', - help='URL of the Appenlight API endpoint, defaults to "%(default)s".') - parser.add_argument( - '--appenlight-api-key', '--ae-key', - help='API key to use when sending data to appenlight. This has to be ' - 'set if Appenlight is enabled.') return parser.parse_args() diff --git a/rhodecode/tests/load/profile.py b/rhodecode/tests/load/profile.py --- a/rhodecode/tests/load/profile.py +++ b/rhodecode/tests/load/profile.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/load/time_urls.py b/rhodecode/tests/load/time_urls.py --- a/rhodecode/tests/load/time_urls.py +++ b/rhodecode/tests/load/time_urls.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/load/vcs_performance.py b/rhodecode/tests/load/vcs_performance.py --- a/rhodecode/tests/load/vcs_performance.py +++ b/rhodecode/tests/load/vcs_performance.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/__init__.py b/rhodecode/tests/models/__init__.py --- a/rhodecode/tests/models/__init__.py +++ b/rhodecode/tests/models/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/common.py b/rhodecode/tests/models/common.py --- a/rhodecode/tests/models/common.py +++ b/rhodecode/tests/models/common.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -23,7 +22,7 @@ from rhodecode.model.meta import Session from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.repo import RepoModel from rhodecode.model.user import UserModel -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() @@ -69,9 +68,9 @@ def _create_project_tree(): """ test_u1 = UserModel().create_or_update( - username=u'test_u1', password=u'qweqwe', - email=u'test_u1@rhodecode.org', firstname=u'test_u1', - lastname=u'test_u1') + username='test_u1', password='qweqwe', + email='test_u1@rhodecode.org', firstname='test_u1', + lastname='test_u1') fixture.create_repo_group('g0') g0_1 = fixture.create_repo_group('g0/g0_1') @@ -84,8 +83,7 @@ def _create_project_tree(): fixture.create_repo('g0/g0_2/g0_2_r2', repo_group=g0_2) g0_3 = fixture.create_repo_group('g0/g0_3') fixture.create_repo('g0/g0_3/g0_3_r1', repo_group=g0_3) - fixture.create_repo( - 'g0/g0_3/g0_3_r1_private', repo_group=g0_3, repo_private=True) + fixture.create_repo('g0/g0_3/g0_3_r1_private', repo_group=g0_3, repo_private=True) return test_u1 @@ -100,14 +98,14 @@ def expected_count(group_name, objects=F def _check_expected_count(items, repo_items, expected): should_be = len(items + repo_items) there_are = len(expected) - assert should_be == there_are, ( - '%s != %s' % ((items + repo_items), expected)) + assert should_be == there_are, f'{(items + repo_items)} != {expected}' -def check_tree_perms(obj_name, repo_perm, prefix, expected_perm): - assert repo_perm == expected_perm, ( - 'obj:`%s` got perm:`%s` should:`%s`' % ( - obj_name, repo_perm, expected_perm)) +def check_tree_perms(obj_name, repo_perm, prefix, expected_perm, default_user=False): + if 'r1_private' in obj_name and default_user: + # private repo must be private even if "recursive" permissions apply + expected_perm = 'repository.none' + assert repo_perm == expected_perm, f'obj:`{obj_name}` got perm:`{repo_perm}` should:`{expected_perm}`' def _get_perms(filter_='', recursive=None, key=None, test_u1_id=None): diff --git a/rhodecode/tests/models/schemas/test_gist_schema.py b/rhodecode/tests/models/schemas/test_gist_schema.py --- a/rhodecode/tests/models/schemas/test_gist_schema.py +++ b/rhodecode/tests/models/schemas/test_gist_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_integration_schema.py b/rhodecode/tests/models/schemas/test_integration_schema.py --- a/rhodecode/tests/models/schemas/test_integration_schema.py +++ b/rhodecode/tests/models/schemas/test_integration_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_repo_group_schema.py b/rhodecode/tests/models/schemas/test_repo_group_schema.py --- a/rhodecode/tests/models/schemas/test_repo_group_schema.py +++ b/rhodecode/tests/models/schemas/test_repo_group_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_repo_schema.py b/rhodecode/tests/models/schemas/test_repo_schema.py --- a/rhodecode/tests/models/schemas/test_repo_schema.py +++ b/rhodecode/tests/models/schemas/test_repo_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_schema_types.py b/rhodecode/tests/models/schemas/test_schema_types.py --- a/rhodecode/tests/models/schemas/test_schema_types.py +++ b/rhodecode/tests/models/schemas/test_schema_types.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_user_schema.py b/rhodecode/tests/models/schemas/test_user_schema.py --- a/rhodecode/tests/models/schemas/test_user_schema.py +++ b/rhodecode/tests/models/schemas/test_user_schema.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/schemas/test_user_usergroup_types.py b/rhodecode/tests/models/schemas/test_user_usergroup_types.py --- a/rhodecode/tests/models/schemas/test_user_usergroup_types.py +++ b/rhodecode/tests/models/schemas/test_user_usergroup_types.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/settings/__init__.py b/rhodecode/tests/models/settings/__init__.py --- a/rhodecode/tests/models/settings/__init__.py +++ b/rhodecode/tests/models/settings/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/settings/test_issue_tracker.py b/rhodecode/tests/models/settings/test_issue_tracker.py --- a/rhodecode/tests/models/settings/test_issue_tracker.py +++ b/rhodecode/tests/models/settings/test_issue_tracker.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/settings/test_settings.py b/rhodecode/tests/models/settings/test_settings.py --- a/rhodecode/tests/models/settings/test_settings.py +++ b/rhodecode/tests/models/settings/test_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/settings/test_vcs_settings.py b/rhodecode/tests/models/settings/test_vcs_settings.py --- a/rhodecode/tests/models/settings/test_vcs_settings.py +++ b/rhodecode/tests/models/settings/test_vcs_settings.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -38,6 +37,7 @@ SVN_FORM_DATA = { GENERAL_FORM_DATA = { 'rhodecode_pr_merge_enabled': True, + 'rhodecode_auto_merge_enabled': True, 'rhodecode_use_outdated_comments': True, 'rhodecode_hg_use_rebase_for_merging': True, 'rhodecode_hg_close_branch_before_merging': True, @@ -989,6 +989,7 @@ class TestCreateOrUpdateRepoSettings(obj 'vcs_git_lfs_enabled': False, 'phases_publish': 'False', 'rhodecode_pr_merge_enabled': False, + 'rhodecode_auto_merge_enabled': False, 'rhodecode_use_outdated_comments': False, 'new_svn_branch': '', 'new_svn_tag': '' diff --git a/rhodecode/tests/models/test_changeset_status.py b/rhodecode/tests/models/test_changeset_status.py --- a/rhodecode/tests/models/test_changeset_status.py +++ b/rhodecode/tests/models/test_changeset_status.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_changeset_status_group_voting.py b/rhodecode/tests/models/test_changeset_status_group_voting.py --- a/rhodecode/tests/models/test_changeset_status_group_voting.py +++ b/rhodecode/tests/models/test_changeset_status_group_voting.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_comment.py b/rhodecode/tests/models/test_comment.py --- a/rhodecode/tests/models/test_comment.py +++ b/rhodecode/tests/models/test_comment.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_db.py b/rhodecode/tests/models/test_db.py --- a/rhodecode/tests/models/test_db.py +++ b/rhodecode/tests/models/test_db.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_gist.py b/rhodecode/tests/models/test_gist.py --- a/rhodecode/tests/models/test_gist.py +++ b/rhodecode/tests/models/test_gist.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_notifications.py b/rhodecode/tests/models/test_notifications.py --- a/rhodecode/tests/models/test_notifications.py +++ b/rhodecode/tests/models/test_notifications.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,7 +18,7 @@ import pytest -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.model.db import User, Notification, UserNotification from rhodecode.model.meta import Session diff --git a/rhodecode/tests/models/test_permissions.py b/rhodecode/tests/models/test_permissions.py --- a/rhodecode/tests/models/test_permissions.py +++ b/rhodecode/tests/models/test_permissions.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -29,7 +28,7 @@ from rhodecode.model.repo import RepoMod from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() @@ -54,21 +53,21 @@ class TestPermissions(object): # TODO: User creation is a duplicate of test_nofitications, check # if that can be unified self.u1 = UserModel().create_or_update( - username=u'u1', password=u'qweqwe', - email=u'u1@rhodecode.org', firstname=u'u1', lastname=u'u1' + username='u1', password='qweqwe', + email='u1@rhodecode.org', firstname='u1', lastname='u1' ) self.u2 = UserModel().create_or_update( - username=u'u2', password=u'qweqwe', - email=u'u2@rhodecode.org', firstname=u'u2', lastname=u'u2' + username='u2', password='qweqwe', + email='u2@rhodecode.org', firstname='u2', lastname='u2' ) self.u3 = UserModel().create_or_update( - username=u'u3', password=u'qweqwe', - email=u'u3@rhodecode.org', firstname=u'u3', lastname=u'u3' + username='u3', password='qweqwe', + email='u3@rhodecode.org', firstname='u3', lastname='u3' ) self.anon = User.get_default_user() self.a1 = UserModel().create_or_update( - username=u'a1', password=u'qweqwe', - email=u'a1@rhodecode.org', firstname=u'a1', lastname=u'a1', + username='a1', password='qweqwe', + email='a1@rhodecode.org', firstname='a1', lastname='a1', admin=True ) Session().commit() @@ -180,12 +179,11 @@ class TestPermissions(object): repo_type='hg', repo_group=self.g1, cur_user=self.u1,) - assert repo_perms(self.anon) == { - repo_name_in_group: 'repository.read', - 'vcs_test_git': 'repository.read', - 'vcs_test_hg': 'repository.read', - 'vcs_test_svn': 'repository.read', - } + assert repo_perms(self.anon)[repo_name_in_group] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_git'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_hg'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_svn'] == 'repository.read' + # Now change default user permissions new_perm = 'repository.write' perm_updates = [ @@ -195,12 +193,10 @@ class TestPermissions(object): repo_group=self.g1, perm_updates=perm_updates, recursive='all') Session().commit() - assert repo_perms(self.anon) == { - repo_name_in_group: new_perm, - 'vcs_test_git': 'repository.read', - 'vcs_test_hg': 'repository.read', - 'vcs_test_svn': 'repository.read', - } + assert repo_perms(self.anon)[repo_name_in_group] == new_perm + assert repo_perms(self.anon)['vcs_test_git'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_hg'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_svn'] == 'repository.read' # NOW MARK repo as private changes = { @@ -211,12 +207,10 @@ class TestPermissions(object): Session().commit() # Private repo sets 'none' permission for default user - assert repo_perms(self.anon) == { - repo_name_in_group: 'repository.none', - 'vcs_test_git': 'repository.read', - 'vcs_test_hg': 'repository.read', - 'vcs_test_svn': 'repository.read', - } + assert repo_perms(self.anon)[repo_name_in_group] == 'repository.none' + assert repo_perms(self.anon)['vcs_test_git'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_hg'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_svn'] == 'repository.read' # apply same logic of "updated" recursive, but now the anon permissions should be not be impacted new_perm = 'repository.write' @@ -227,12 +221,10 @@ class TestPermissions(object): repo_group=self.g1, perm_updates=perm_updates, recursive='all') Session().commit() - assert repo_perms(self.anon) == { - repo_name_in_group: 'repository.none', - 'vcs_test_git': 'repository.read', - 'vcs_test_hg': 'repository.read', - 'vcs_test_svn': 'repository.read', - } + assert repo_perms(self.anon)[repo_name_in_group] == 'repository.none' + assert repo_perms(self.anon)['vcs_test_git'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_hg'] == 'repository.read' + assert repo_perms(self.anon)['vcs_test_svn'] == 'repository.read' def test_propagated_permission_from_users_group_by_explicit_perms_exist( self, repo_name): @@ -301,10 +293,10 @@ class TestPermissions(object): self.g2 = fixture.create_repo_group('group2', skip_if_exists=True) # both perms should be read ! assert group_perms(self.u1) == \ - {u'group1': u'group.read', u'group2': u'group.read'} + {'group1': 'group.read', 'group2': 'group.read'} assert group_perms(self.anon) == \ - {u'group1': u'group.read', u'group2': u'group.read'} + {'group1': 'group.read', 'group2': 'group.read'} # Change perms to none for both groups RepoGroupModel().grant_user_permission( @@ -313,9 +305,9 @@ class TestPermissions(object): repo_group=self.g2, user=self.anon, perm='group.none') assert group_perms(self.u1) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} assert group_perms(self.anon) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} # add repo to group name = RepoGroup.url_sep().join([self.g1.group_name, 'test_perm']) @@ -325,9 +317,9 @@ class TestPermissions(object): cur_user=self.u1,) assert group_perms(self.u1) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} assert group_perms(self.anon) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} # grant permission for u2 ! RepoGroupModel().grant_user_permission( @@ -339,16 +331,16 @@ class TestPermissions(object): # u1 and anon should have not change perms while u2 should ! assert group_perms(self.u1) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} assert group_perms(self.u2) == \ - {u'group1': u'group.read', u'group2': u'group.read'} + {'group1': 'group.read', 'group2': 'group.read'} assert group_perms(self.anon) == \ - {u'group1': u'group.none', u'group2': u'group.none'} + {'group1': 'group.none', 'group2': 'group.none'} def test_repo_group_user_as_user_group_member(self): # create Group1 self.g1 = fixture.create_repo_group('group1', skip_if_exists=True) - assert group_perms(self.anon) == {u'group1': u'group.read'} + assert group_perms(self.anon) == {'group1': 'group.read'} # set default permission to none RepoGroupModel().grant_user_permission( @@ -368,8 +360,8 @@ class TestPermissions(object): # add some user to that group # check his permissions - assert group_perms(self.anon) == {u'group1': u'group.none'} - assert group_perms(self.u1) == {u'group1': u'group.none'} + assert group_perms(self.anon) == {'group1': 'group.none'} + assert group_perms(self.u1) == {'group1': 'group.none'} # grant ug1 read permissions for RepoGroupModel().grant_user_group_permission( @@ -383,8 +375,8 @@ class TestPermissions(object): .scalar() assert obj.permission.permission_name == 'group.read' - assert group_perms(self.anon) == {u'group1': u'group.none'} - assert group_perms(self.u1) == {u'group1': u'group.read'} + assert group_perms(self.anon) == {'group1': 'group.none'} + assert group_perms(self.u1) == {'group1': 'group.read'} def test_inherited_permissions_from_default_on_user_enabled(self): # enable fork and create on default user @@ -598,7 +590,7 @@ class TestPermissions(object): # Make him owner of the repo group self.g1.user = self.u1 - assert group_perms(self.u1) == {u'test1': 'group.admin'} + assert group_perms(self.u1) == {'test1': 'group.admin'} def assert_user_perm_equal( self, user, change_factor=0, compare_keys=None): diff --git a/rhodecode/tests/models/test_pullrequest.py b/rhodecode/tests/models/test_pullrequest.py --- a/rhodecode/tests/models/test_pullrequest.py +++ b/rhodecode/tests/models/test_pullrequest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -16,6 +15,7 @@ # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import os import mock import pytest @@ -23,8 +23,7 @@ import textwrap import rhodecode from rhodecode.lib.vcs.backends import get_backend -from rhodecode.lib.vcs.backends.base import ( - MergeResponse, MergeFailureReason, Reference) +from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason, Reference from rhodecode.lib.vcs.exceptions import RepositoryError from rhodecode.lib.vcs.nodes import FileNode from rhodecode.model.comment import CommentsModel @@ -39,54 +38,42 @@ pytestmark = [ ] -@pytest.mark.usefixtures('config_stub') +@pytest.mark.usefixtures("config_stub") class TestPullRequestModel(object): - @pytest.fixture() def pull_request(self, request, backend, pr_util): """ A pull request combined with multiples patches. """ BackendClass = get_backend(backend.alias) - merge_resp = MergeResponse( - False, False, None, MergeFailureReason.UNKNOWN, - metadata={'exception': 'MockError'}) - self.merge_patcher = mock.patch.object( - BackendClass, 'merge', return_value=merge_resp) - self.workspace_remove_patcher = mock.patch.object( - BackendClass, 'cleanup_merge_workspace') + merge_resp = MergeResponse(False, False, None, MergeFailureReason.UNKNOWN, metadata={"exception": "MockError"}) + self.merge_patcher = mock.patch.object(BackendClass, "merge", return_value=merge_resp) + self.workspace_remove_patcher = mock.patch.object(BackendClass, "cleanup_merge_workspace") self.workspace_remove_mock = self.workspace_remove_patcher.start() self.merge_mock = self.merge_patcher.start() - self.comment_patcher = mock.patch( - 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status') + self.comment_patcher = mock.patch("rhodecode.model.changeset_status.ChangesetStatusModel.set_status") self.comment_patcher.start() - self.notification_patcher = mock.patch( - 'rhodecode.model.notification.NotificationModel.create') + self.notification_patcher = mock.patch("rhodecode.model.notification.NotificationModel.create") self.notification_patcher.start() - self.helper_patcher = mock.patch( - 'rhodecode.lib.helpers.route_path') + self.helper_patcher = mock.patch("rhodecode.lib.helpers.route_path") self.helper_patcher.start() - self.hook_patcher = mock.patch.object(PullRequestModel, - 'trigger_pull_request_hook') + self.hook_patcher = mock.patch.object(PullRequestModel, "trigger_pull_request_hook") self.hook_mock = self.hook_patcher.start() - self.invalidation_patcher = mock.patch( - 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation') + self.invalidation_patcher = mock.patch("rhodecode.model.pull_request.ScmModel.mark_for_invalidation") self.invalidation_mock = self.invalidation_patcher.start() - self.pull_request = pr_util.create_pull_request( - mergeable=True, name_suffix=u'ąć') + self.pull_request = pr_util.create_pull_request(mergeable=True, name_suffix="ąć") self.source_commit = self.pull_request.source_ref_parts.commit_id self.target_commit = self.pull_request.target_ref_parts.commit_id - self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id + self.workspace_id = f"pr-{self.pull_request.pull_request_id}" self.repo_id = self.pull_request.target_repo.repo_id @request.addfinalizer def cleanup_pull_request(): - calls = [mock.call( - self.pull_request, self.pull_request.author, 'create')] + calls = [mock.call(self.pull_request, self.pull_request.author, "create")] self.hook_mock.assert_has_calls(calls) self.workspace_remove_patcher.stop() @@ -114,29 +101,30 @@ class TestPullRequestModel(object): assert len(prs) == 1 def test_count_awaiting_review(self, pull_request): - pr_count = PullRequestModel().count_awaiting_review( - pull_request.target_repo) + pr_count = PullRequestModel().count_awaiting_review(pull_request.target_repo) assert pr_count == 1 def test_get_awaiting_my_review(self, pull_request): PullRequestModel().update_reviewers( - pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])], - pull_request.author) + pull_request, [(pull_request.author, ["author"], False, "reviewer", [])], pull_request.author + ) Session().commit() prs = PullRequestModel().get_awaiting_my_review( - pull_request.target_repo.repo_name, user_id=pull_request.author.user_id) + pull_request.target_repo.repo_name, user_id=pull_request.author.user_id + ) assert isinstance(prs, list) assert len(prs) == 1 def test_count_awaiting_my_review(self, pull_request): PullRequestModel().update_reviewers( - pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])], - pull_request.author) + pull_request, [(pull_request.author, ["author"], False, "reviewer", [])], pull_request.author + ) Session().commit() pr_count = PullRequestModel().count_awaiting_my_review( - pull_request.target_repo.repo_name, user_id=pull_request.author.user_id) + pull_request.target_repo.repo_name, user_id=pull_request.author.user_id + ) assert pr_count == 1 def test_delete_calls_cleanup_merge(self, pull_request): @@ -144,24 +132,19 @@ class TestPullRequestModel(object): PullRequestModel().delete(pull_request, pull_request.author) Session().commit() - self.workspace_remove_mock.assert_called_once_with( - repo_id, self.workspace_id) + self.workspace_remove_mock.assert_called_once_with(repo_id, self.workspace_id) def test_close_calls_cleanup_and_hook(self, pull_request): - PullRequestModel().close_pull_request( - pull_request, pull_request.author) + PullRequestModel().close_pull_request(pull_request, pull_request.author) Session().commit() repo_id = pull_request.target_repo.repo_id - self.workspace_remove_mock.assert_called_once_with( - repo_id, self.workspace_id) - self.hook_mock.assert_called_with( - self.pull_request, self.pull_request.author, 'close') + self.workspace_remove_mock.assert_called_once_with(repo_id, self.workspace_id) + self.hook_mock.assert_called_with(self.pull_request, self.pull_request.author, "close") def test_merge_status(self, pull_request): - self.merge_mock.return_value = MergeResponse( - True, False, None, MergeFailureReason.NONE) + self.merge_mock.return_value = MergeResponse(True, False, None, MergeFailureReason.NONE) assert pull_request._last_merge_source_rev is None assert pull_request._last_merge_target_rev is None @@ -169,13 +152,17 @@ class TestPullRequestModel(object): merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is True - assert msg == 'This pull request can be automatically merged.' + assert msg == "This pull request can be automatically merged." self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), - pull_request.source_ref_parts, dry_run=True, - use_rebase=False, close_branch=False) + pull_request.source_ref_parts, + dry_run=True, + use_rebase=False, + close_branch=False, + ) assert pull_request._last_merge_source_rev == self.source_commit assert pull_request._last_merge_target_rev == self.target_commit @@ -184,13 +171,13 @@ class TestPullRequestModel(object): self.merge_mock.reset_mock() merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is True - assert msg == 'This pull request can be automatically merged.' + assert msg == "This pull request can be automatically merged." assert self.merge_mock.called is False def test_merge_status_known_failure(self, pull_request): self.merge_mock.return_value = MergeResponse( - False, False, None, MergeFailureReason.MERGE_FAILED, - metadata={'unresolved_files': 'file1'}) + False, False, None, MergeFailureReason.MERGE_FAILED, metadata={"unresolved_files": "file1"} + ) assert pull_request._last_merge_source_rev is None assert pull_request._last_merge_target_rev is None @@ -198,13 +185,17 @@ class TestPullRequestModel(object): merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == 'This pull request cannot be merged because of merge conflicts. file1' + assert msg == "This pull request cannot be merged because of merge conflicts. file1" self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), - pull_request.source_ref_parts, dry_run=True, - use_rebase=False, close_branch=False) + pull_request.source_ref_parts, + dry_run=True, + use_rebase=False, + close_branch=False, + ) assert pull_request._last_merge_source_rev == self.source_commit assert pull_request._last_merge_target_rev == self.target_commit @@ -213,13 +204,13 @@ class TestPullRequestModel(object): self.merge_mock.reset_mock() merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == 'This pull request cannot be merged because of merge conflicts. file1' + assert msg == "This pull request cannot be merged because of merge conflicts. file1" assert self.merge_mock.called is False def test_merge_status_unknown_failure(self, pull_request): self.merge_mock.return_value = MergeResponse( - False, False, None, MergeFailureReason.UNKNOWN, - metadata={'exception': 'MockError'}) + False, False, None, MergeFailureReason.UNKNOWN, metadata={"exception": "MockError"} + ) assert pull_request._last_merge_source_rev is None assert pull_request._last_merge_target_rev is None @@ -227,15 +218,17 @@ class TestPullRequestModel(object): merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == ( - 'This pull request cannot be merged because of an unhandled exception. ' - 'MockError') + assert msg == "This pull request cannot be merged because of an unhandled exception. MockError" self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), - pull_request.source_ref_parts, dry_run=True, - use_rebase=False, close_branch=False) + pull_request.source_ref_parts, + dry_run=True, + use_rebase=False, + close_branch=False, + ) assert pull_request._last_merge_source_rev is None assert pull_request._last_merge_target_rev is None @@ -244,155 +237,136 @@ class TestPullRequestModel(object): self.merge_mock.reset_mock() merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == ( - 'This pull request cannot be merged because of an unhandled exception. ' - 'MockError') + assert msg == "This pull request cannot be merged because of an unhandled exception. MockError" assert self.merge_mock.called is True def test_merge_status_when_target_is_locked(self, pull_request): - pull_request.target_repo.locked = [1, u'12345.50', 'lock_web'] + pull_request.target_repo.locked = [1, "12345.50", "lock_web"] merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == ( - 'This pull request cannot be merged because the target repository ' - 'is locked by user:1.') + assert msg == "This pull request cannot be merged because the target repository is locked by user:1." def test_merge_status_requirements_check_target(self, pull_request): - def has_largefiles(self, repo): return repo == pull_request.source_repo - patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) + patcher = mock.patch.object(PullRequestModel, "_has_largefiles", has_largefiles) with patcher: merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == 'Target repository large files support is disabled.' + assert msg == "Target repository large files support is disabled." def test_merge_status_requirements_check_source(self, pull_request): - def has_largefiles(self, repo): return repo == pull_request.target_repo - patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) + patcher = mock.patch.object(PullRequestModel, "_has_largefiles", has_largefiles) with patcher: merge_response, status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == 'Source repository large files support is disabled.' + assert msg == "Source repository large files support is disabled." def test_merge(self, pull_request, merge_extras): user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) - merge_ref = Reference( - 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6') - self.merge_mock.return_value = MergeResponse( - True, True, merge_ref, MergeFailureReason.NONE) + merge_ref = Reference("type", "name", "6126b7bfcc82ad2d3deaee22af926b082ce54cc6") + self.merge_mock.return_value = MergeResponse(True, True, merge_ref, MergeFailureReason.NONE) - merge_extras['repository'] = pull_request.target_repo.repo_name - PullRequestModel().merge_repo( - pull_request, pull_request.author, extras=merge_extras) + merge_extras["repository"] = pull_request.target_repo.repo_name + PullRequestModel().merge_repo(pull_request, pull_request.author, extras=merge_extras) Session().commit() - message = ( - u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}' - u'\n\n {pr_title}'.format( - pr_id=pull_request.pull_request_id, - source_repo=safe_str( - pull_request.source_repo.scm_instance().name), - source_ref_name=pull_request.source_ref_parts.name, - pr_title=safe_str(pull_request.title) - ) + message = "Merge pull request !{pr_id} from {source_repo} {source_ref_name}" "\n\n {pr_title}".format( + pr_id=pull_request.pull_request_id, + source_repo=safe_str(pull_request.source_repo.scm_instance().name), + source_ref_name=pull_request.source_ref_parts.name, + pr_title=safe_str(pull_request.title), ) self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), pull_request.source_ref_parts, - user_name=user.short_contact, user_email=user.email, message=message, - use_rebase=False, close_branch=False + user_name=user.short_contact, + user_email=user.email, + message=message, + use_rebase=False, + close_branch=False, ) - self.invalidation_mock.assert_called_once_with( - pull_request.target_repo.repo_name) + self.invalidation_mock.assert_called_once_with(pull_request.target_repo.repo_name) - self.hook_mock.assert_called_with( - self.pull_request, self.pull_request.author, 'merge') + self.hook_mock.assert_called_with(self.pull_request, self.pull_request.author, "merge") pull_request = PullRequest.get(pull_request.pull_request_id) - assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6' + assert pull_request.merge_rev == "6126b7bfcc82ad2d3deaee22af926b082ce54cc6" def test_merge_with_status_lock(self, pull_request, merge_extras): user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) - merge_ref = Reference( - 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6') - self.merge_mock.return_value = MergeResponse( - True, True, merge_ref, MergeFailureReason.NONE) + merge_ref = Reference("type", "name", "6126b7bfcc82ad2d3deaee22af926b082ce54cc6") + self.merge_mock.return_value = MergeResponse(True, True, merge_ref, MergeFailureReason.NONE) - merge_extras['repository'] = pull_request.target_repo.repo_name + merge_extras["repository"] = pull_request.target_repo.repo_name with pull_request.set_state(PullRequest.STATE_UPDATING): assert pull_request.pull_request_state == PullRequest.STATE_UPDATING - PullRequestModel().merge_repo( - pull_request, pull_request.author, extras=merge_extras) + PullRequestModel().merge_repo(pull_request, pull_request.author, extras=merge_extras) Session().commit() assert pull_request.pull_request_state == PullRequest.STATE_CREATED - message = ( - u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}' - u'\n\n {pr_title}'.format( - pr_id=pull_request.pull_request_id, - source_repo=safe_str( - pull_request.source_repo.scm_instance().name), - source_ref_name=pull_request.source_ref_parts.name, - pr_title=safe_str(pull_request.title) - ) + message = "Merge pull request !{pr_id} from {source_repo} {source_ref_name}" "\n\n {pr_title}".format( + pr_id=pull_request.pull_request_id, + source_repo=safe_str(pull_request.source_repo.scm_instance().name), + source_ref_name=pull_request.source_ref_parts.name, + pr_title=safe_str(pull_request.title), ) self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), pull_request.source_ref_parts, - user_name=user.short_contact, user_email=user.email, message=message, - use_rebase=False, close_branch=False + user_name=user.short_contact, + user_email=user.email, + message=message, + use_rebase=False, + close_branch=False, ) - self.invalidation_mock.assert_called_once_with( - pull_request.target_repo.repo_name) + self.invalidation_mock.assert_called_once_with(pull_request.target_repo.repo_name) - self.hook_mock.assert_called_with( - self.pull_request, self.pull_request.author, 'merge') + self.hook_mock.assert_called_with(self.pull_request, self.pull_request.author, "merge") pull_request = PullRequest.get(pull_request.pull_request_id) - assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6' + assert pull_request.merge_rev == "6126b7bfcc82ad2d3deaee22af926b082ce54cc6" def test_merge_failed(self, pull_request, merge_extras): user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) - merge_ref = Reference( - 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6') - self.merge_mock.return_value = MergeResponse( - False, False, merge_ref, MergeFailureReason.MERGE_FAILED) + merge_ref = Reference("type", "name", "6126b7bfcc82ad2d3deaee22af926b082ce54cc6") + self.merge_mock.return_value = MergeResponse(False, False, merge_ref, MergeFailureReason.MERGE_FAILED) - merge_extras['repository'] = pull_request.target_repo.repo_name - PullRequestModel().merge_repo( - pull_request, pull_request.author, extras=merge_extras) + merge_extras["repository"] = pull_request.target_repo.repo_name + PullRequestModel().merge_repo(pull_request, pull_request.author, extras=merge_extras) Session().commit() - message = ( - u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}' - u'\n\n {pr_title}'.format( - pr_id=pull_request.pull_request_id, - source_repo=safe_str( - pull_request.source_repo.scm_instance().name), - source_ref_name=pull_request.source_ref_parts.name, - pr_title=safe_str(pull_request.title) - ) + message = "Merge pull request !{pr_id} from {source_repo} {source_ref_name}" "\n\n {pr_title}".format( + pr_id=pull_request.pull_request_id, + source_repo=safe_str(pull_request.source_repo.scm_instance().name), + source_ref_name=pull_request.source_ref_parts.name, + pr_title=safe_str(pull_request.title), ) self.merge_mock.assert_called_with( - self.repo_id, self.workspace_id, + self.repo_id, + self.workspace_id, pull_request.target_ref_parts, pull_request.source_repo.scm_instance(), pull_request.source_ref_parts, - user_name=user.short_contact, user_email=user.email, message=message, - use_rebase=False, close_branch=False + user_name=user.short_contact, + user_email=user.email, + message=message, + use_rebase=False, + close_branch=False, ) pull_request = PullRequest.get(pull_request.pull_request_id) @@ -410,7 +384,7 @@ class TestPullRequestModel(object): assert commit_ids == pull_request.revisions # Merge revision is not in the revisions list - pull_request.merge_rev = 'f000' * 10 + pull_request.merge_rev = "f000" * 10 commit_ids = PullRequestModel()._get_commit_ids(pull_request) assert commit_ids == pull_request.revisions + [pull_request.merge_rev] @@ -419,147 +393,126 @@ class TestPullRequestModel(object): source_ref_id = pull_request.source_ref_parts.commit_id target_ref_id = pull_request.target_ref_parts.commit_id diff = PullRequestModel()._get_diff_from_pr_or_version( - source_repo, source_ref_id, target_ref_id, - hide_whitespace_changes=False, diff_context=6) - assert b'file_1' in diff.raw.tobytes() + source_repo, source_ref_id, target_ref_id, hide_whitespace_changes=False, diff_context=6 + ) + assert b"file_1" in diff.raw.tobytes() def test_generate_title_returns_unicode(self): title = PullRequestModel().generate_pullrequest_title( - source='source-dummy', - source_ref='source-ref-dummy', - target='target-dummy', + source="source-dummy", + source_ref="source-ref-dummy", + target="target-dummy", ) assert type(title) == str - @pytest.mark.parametrize('title, has_wip', [ - ('hello', False), - ('hello wip', False), - ('hello wip: xxx', False), - ('[wip] hello', True), - ('[wip] hello', True), - ('wip: hello', True), - ('wip hello', True), - - ]) + @pytest.mark.parametrize( + "title, has_wip", + [ + ("hello", False), + ("hello wip", False), + ("hello wip: xxx", False), + ("[wip] hello", True), + ("[wip] hello", True), + ("wip: hello", True), + ("wip hello", True), + ], + ) def test_wip_title_marker(self, pull_request, title, has_wip): pull_request.title = title assert pull_request.work_in_progress == has_wip -@pytest.mark.usefixtures('config_stub') +@pytest.mark.usefixtures("config_stub") class TestIntegrationMerge(object): - @pytest.mark.parametrize('extra_config', ( - {'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.direct_calls': False}, - )) - def test_merge_triggers_push_hooks( - self, pr_util, user_admin, capture_rcextensions, merge_extras, - extra_config): - - pull_request = pr_util.create_pull_request( - approved=True, mergeable=True) - # TODO: johbo: Needed for sqlite, try to find an automatic way for it - merge_extras['repository'] = pull_request.target_repo.repo_name - Session().commit() - - with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False): - merge_state = PullRequestModel().merge_repo( - pull_request, user_admin, extras=merge_extras) - Session().commit() - - assert merge_state.executed - assert '_pre_push_hook' in capture_rcextensions - assert '_push_hook' in capture_rcextensions - def test_merge_can_be_rejected_by_pre_push_hook( - self, pr_util, user_admin, capture_rcextensions, merge_extras): - pull_request = pr_util.create_pull_request( - approved=True, mergeable=True) - # TODO: johbo: Needed for sqlite, try to find an automatic way for it - merge_extras['repository'] = pull_request.target_repo.repo_name - Session().commit() - - with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull: - pre_pull.side_effect = RepositoryError("Disallow push!") - merge_status = PullRequestModel().merge_repo( - pull_request, user_admin, extras=merge_extras) - Session().commit() - - assert not merge_status.executed - assert 'pre_push' not in capture_rcextensions - assert 'post_push' not in capture_rcextensions - - def test_merge_fails_if_target_is_locked( - self, pr_util, user_regular, merge_extras): - pull_request = pr_util.create_pull_request( - approved=True, mergeable=True) - locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web'] + def test_merge_fails_if_target_is_locked(self, pr_util, user_regular, merge_extras): + pull_request = pr_util.create_pull_request(approved=True, mergeable=True) + locked_by = [user_regular.user_id + 1, 12345.50, "lock_web"] pull_request.target_repo.locked = locked_by # TODO: johbo: Check if this can work based on the database, currently # all data is pre-computed, that's why just updating the DB is not # enough. - merge_extras['locked_by'] = locked_by - merge_extras['repository'] = pull_request.target_repo.repo_name + merge_extras["locked_by"] = locked_by + merge_extras["repository"] = pull_request.target_repo.repo_name # TODO: johbo: Needed for sqlite, try to find an automatic way for it Session().commit() - merge_status = PullRequestModel().merge_repo( - pull_request, user_regular, extras=merge_extras) + merge_status = PullRequestModel().merge_repo(pull_request, user_regular, extras=merge_extras) Session().commit() assert not merge_status.executed -@pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [ - (False, 1, 0), - (True, 0, 1), -]) -def test_outdated_comments( - pr_util, use_outdated, inlines_count, outdated_count, config_stub): +@pytest.mark.parametrize( + "use_outdated, inlines_count, outdated_count", + [ + (False, 1, 0), + (True, 0, 1), + ], +) +def test_outdated_comments(pr_util, use_outdated, inlines_count, outdated_count, config_stub): pull_request = pr_util.create_pull_request() - pr_util.create_inline_comment(file_path='not_in_updated_diff') + pr_util.create_inline_comment(file_path="not_in_updated_diff") with outdated_comments_patcher(use_outdated) as outdated_comment_mock: pr_util.add_one_commit() - assert_inline_comments( - pull_request, visible=inlines_count, outdated=outdated_count) + assert_inline_comments(pull_request, visible=inlines_count, outdated=outdated_count) outdated_comment_mock.assert_called_with(pull_request) -@pytest.mark.parametrize('mr_type, expected_msg', [ - (MergeFailureReason.NONE, - 'This pull request can be automatically merged.'), - (MergeFailureReason.UNKNOWN, - 'This pull request cannot be merged because of an unhandled exception. CRASH'), - (MergeFailureReason.MERGE_FAILED, - 'This pull request cannot be merged because of merge conflicts. CONFLICT_FILE'), - (MergeFailureReason.PUSH_FAILED, - 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'), - (MergeFailureReason.TARGET_IS_NOT_HEAD, - 'This pull request cannot be merged because the target `ref_name` is not a head.'), - (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES, - 'This pull request cannot be merged because the source contains more branches than the target.'), - (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, - 'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'), - (MergeFailureReason.TARGET_IS_LOCKED, - 'This pull request cannot be merged because the target repository is locked by user:123.'), - (MergeFailureReason.MISSING_TARGET_REF, - 'This pull request cannot be merged because the target reference `ref_name` is missing.'), - (MergeFailureReason.MISSING_SOURCE_REF, - 'This pull request cannot be merged because the source reference `ref_name` is missing.'), - (MergeFailureReason.SUBREPO_MERGE_FAILED, - 'This pull request cannot be merged because of conflicts related to sub repositories.'), - -]) +@pytest.mark.parametrize( + "mr_type, expected_msg", + [ + (MergeFailureReason.NONE, "This pull request can be automatically merged."), + (MergeFailureReason.UNKNOWN, "This pull request cannot be merged because of an unhandled exception. CRASH"), + ( + MergeFailureReason.MERGE_FAILED, + "This pull request cannot be merged because of merge conflicts. CONFLICT_FILE", + ), + ( + MergeFailureReason.PUSH_FAILED, + "This pull request could not be merged because push to target:`some-repo@merge_commit` failed.", + ), + ( + MergeFailureReason.TARGET_IS_NOT_HEAD, + "This pull request cannot be merged because the target `ref_name` is not a head.", + ), + ( + MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES, + "This pull request cannot be merged because the source contains more branches than the target.", + ), + ( + MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, + "This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.", + ), + ( + MergeFailureReason.TARGET_IS_LOCKED, + "This pull request cannot be merged because the target repository is locked by user:123.", + ), + ( + MergeFailureReason.MISSING_TARGET_REF, + "This pull request cannot be merged because the target reference `ref_name` is missing.", + ), + ( + MergeFailureReason.MISSING_SOURCE_REF, + "This pull request cannot be merged because the source reference `ref_name` is missing.", + ), + ( + MergeFailureReason.SUBREPO_MERGE_FAILED, + "This pull request cannot be merged because of conflicts related to sub repositories.", + ), + ], +) def test_merge_response_message(mr_type, expected_msg): - merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6') + merge_ref = Reference("type", "ref_name", "6126b7bfcc82ad2d3deaee22af926b082ce54cc6") metadata = { - 'unresolved_files': 'CONFLICT_FILE', - 'exception': "CRASH", - 'target': 'some-repo', - 'merge_commit': 'merge_commit', - 'target_ref': merge_ref, - 'source_ref': merge_ref, - 'heads': ','.join(['a', 'b', 'c']), - 'locked_by': 'user:123' + "unresolved_files": "CONFLICT_FILE", + "exception": "CRASH", + "target": "some-repo", + "merge_commit": "merge_commit", + "target_ref": merge_ref, + "source_ref": merge_ref, + "heads": ",".join(["a", "b", "c"]), + "locked_by": "user:123", } merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata) @@ -567,35 +520,34 @@ def test_merge_response_message(mr_type, @pytest.fixture() -def merge_extras(user_regular): +def merge_extras(request, user_regular): """ Context for the vcs operation when running a merge. """ + extras = { - 'ip': '127.0.0.1', - 'username': user_regular.username, - 'user_id': user_regular.user_id, - 'action': 'push', - 'repository': 'fake_target_repo_name', - 'scm': 'git', - 'config': 'fake_config_ini_path', - 'repo_store': '', - 'make_lock': None, - 'locked_by': [None, None, None], - 'server_url': 'http://test.example.com:5000', - 'hooks': ['push', 'pull'], - 'is_shadow_repo': False, + "ip": "127.0.0.1", + "username": user_regular.username, + "user_id": user_regular.user_id, + "action": "push", + "repository": "fake_target_repo_name", + "scm": "git", + "config": request.config.getini("pyramid_config"), + "repo_store": "", + "make_lock": None, + "locked_by": [None, None, None], + "server_url": "http://test.example.com:5000", + "hooks": ["push", "pull"], + "is_shadow_repo": False, } return extras -@pytest.mark.usefixtures('config_stub') +@pytest.mark.usefixtures("config_stub") class TestUpdateCommentHandling(object): - - @pytest.fixture(autouse=True, scope='class') + @pytest.fixture(autouse=True, scope="class") def enable_outdated_comments(self, request, baseapp): - config_patch = mock.patch.dict( - 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True}) + config_patch = mock.patch.dict("rhodecode.CONFIG", {"rhodecode_use_outdated_comments": True}) config_patch.start() @request.addfinalizer @@ -604,206 +556,194 @@ class TestUpdateCommentHandling(object): def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util): commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [FileNode(b'file_b', b'test_content\n')]}, - {'message': 'c', 'added': [FileNode(b'file_c', b'test_content\n')]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", b"test_content\n")]}, + {"message": "c", "added": [FileNode(b"file_c", b"test_content\n")]}, ] - pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b']) - pr_util.create_inline_comment(file_path='file_b') - pr_util.add_one_commit(head='c') + pull_request = pr_util.create_pull_request(commits=commits, target_head="a", source_head="b", revisions=["b"]) + pr_util.create_inline_comment(file_path="file_b") + pr_util.add_one_commit(head="c") assert_inline_comments(pull_request, visible=1, outdated=0) def test_comment_stays_unflagged_on_change_above(self, pr_util): - original_content = b''.join((b'line %d\n' % x for x in range(1, 11))) - updated_content = b'new_line_at_top\n' + original_content + original_content = b"".join((b"line %d\n" % x for x in range(1, 11))) + updated_content = b"new_line_at_top\n" + original_content commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [FileNode(b'file_b', original_content)]}, - {'message': 'c', 'changed': [FileNode(b'file_b', updated_content)]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", original_content)]}, + {"message": "c", "changed": [FileNode(b"file_b", updated_content)]}, ] - pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b']) + pull_request = pr_util.create_pull_request(commits=commits, target_head="a", source_head="b", revisions=["b"]) with outdated_comments_patcher(): - comment = pr_util.create_inline_comment( - line_no=u'n8', file_path='file_b') - pr_util.add_one_commit(head='c') + comment = pr_util.create_inline_comment(line_no="n8", file_path="file_b") + pr_util.add_one_commit(head="c") assert_inline_comments(pull_request, visible=1, outdated=0) - assert comment.line_no == u'n9' + assert comment.line_no == "n9" def test_comment_stays_unflagged_on_change_below(self, pr_util): - original_content = b''.join([b'line %d\n' % x for x in range(10)]) - updated_content = original_content + b'new_line_at_end\n' + original_content = b"".join([b"line %d\n" % x for x in range(10)]) + updated_content = original_content + b"new_line_at_end\n" commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [FileNode(b'file_b', original_content)]}, - {'message': 'c', 'changed': [FileNode(b'file_b', updated_content)]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", original_content)]}, + {"message": "c", "changed": [FileNode(b"file_b", updated_content)]}, ] - pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b']) - pr_util.create_inline_comment(file_path='file_b') - pr_util.add_one_commit(head='c') + pull_request = pr_util.create_pull_request(commits=commits, target_head="a", source_head="b", revisions=["b"]) + pr_util.create_inline_comment(file_path="file_b") + pr_util.add_one_commit(head="c") assert_inline_comments(pull_request, visible=1, outdated=0) - @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9']) + @pytest.mark.parametrize("line_no", ["n4", "o4", "n10", "o9"]) def test_comment_flagged_on_change_around_context(self, pr_util, line_no): - base_lines = [b'line %d\n' % x for x in range(1, 13)] + base_lines = [b"line %d\n" % x for x in range(1, 13)] change_lines = list(base_lines) - change_lines.insert(6, b'line 6a added\n') + change_lines.insert(6, b"line 6a added\n") # Changes on the last line of sight update_lines = list(change_lines) - update_lines[0] = b'line 1 changed\n' - update_lines[-1] = b'line 12 changed\n' + update_lines[0] = b"line 1 changed\n" + update_lines[-1] = b"line 12 changed\n" def file_b(lines): - return FileNode(b'file_b', b''.join(lines)) + return FileNode(b"file_b", b"".join(lines)) commits = [ - {'message': 'a', 'added': [file_b(base_lines)]}, - {'message': 'b', 'changed': [file_b(change_lines)]}, - {'message': 'c', 'changed': [file_b(update_lines)]}, + {"message": "a", "added": [file_b(base_lines)]}, + {"message": "b", "changed": [file_b(change_lines)]}, + {"message": "c", "changed": [file_b(update_lines)]}, ] - pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b']) - pr_util.create_inline_comment(line_no=line_no, file_path='file_b') + pull_request = pr_util.create_pull_request(commits=commits, target_head="a", source_head="b", revisions=["b"]) + pr_util.create_inline_comment(line_no=line_no, file_path="file_b") with outdated_comments_patcher(): - pr_util.add_one_commit(head='c') + pr_util.add_one_commit(head="c") assert_inline_comments(pull_request, visible=0, outdated=1) - @pytest.mark.parametrize("change, content", [ - ('changed', b'changed\n'), - ('removed', b''), - ], ids=['changed', b'removed']) + @pytest.mark.parametrize( + "change, content", + [ + ("changed", b"changed\n"), + ("removed", b""), + ], + ids=["changed", b"removed"], + ) def test_comment_flagged_on_change(self, pr_util, change, content): commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [FileNode(b'file_b', b'test_content\n')]}, - {'message': 'c', change: [FileNode(b'file_b', content)]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", b"test_content\n")]}, + {"message": "c", change: [FileNode(b"file_b", content)]}, ] - pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b']) - pr_util.create_inline_comment(file_path='file_b') + pull_request = pr_util.create_pull_request(commits=commits, target_head="a", source_head="b", revisions=["b"]) + pr_util.create_inline_comment(file_path="file_b") with outdated_comments_patcher(): - pr_util.add_one_commit(head='c') + pr_util.add_one_commit(head="c") assert_inline_comments(pull_request, visible=0, outdated=1) -@pytest.mark.usefixtures('config_stub') +@pytest.mark.usefixtures("config_stub") class TestUpdateChangedFiles(object): - def test_no_changes_on_unchanged_diff(self, pr_util): commits = [ - {'message': 'a'}, - {'message': 'b', - 'added': [FileNode(b'file_b', b'test_content b\n')]}, - {'message': 'c', - 'added': [FileNode(b'file_c', b'test_content c\n')]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", b"test_content b\n")]}, + {"message": "c", "added": [FileNode(b"file_c", b"test_content c\n")]}, ] # open a PR from a to b, adding file_b pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b'], - name_suffix='per-file-review') + commits=commits, target_head="a", source_head="b", revisions=["b"], name_suffix="per-file-review" + ) # modify PR adding new file file_c - pr_util.add_one_commit(head='c') + pr_util.add_one_commit(head="c") - assert_pr_file_changes( - pull_request, - added=['file_c'], - modified=[], - removed=[]) + assert_pr_file_changes(pull_request, added=["file_c"], modified=[], removed=[]) def test_modify_and_undo_modification_diff(self, pr_util): commits = [ - {'message': 'a'}, - {'message': 'b', - 'added': [FileNode(b'file_b', b'test_content b\n')]}, - {'message': 'c', - 'changed': [FileNode(b'file_b', b'test_content b modified\n')]}, - {'message': 'd', - 'changed': [FileNode(b'file_b', b'test_content b\n')]}, + {"message": "a"}, + {"message": "b", "added": [FileNode(b"file_b", b"test_content b\n")]}, + {"message": "c", "changed": [FileNode(b"file_b", b"test_content b modified\n")]}, + {"message": "d", "changed": [FileNode(b"file_b", b"test_content b\n")]}, ] # open a PR from a to b, adding file_b pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b'], - name_suffix='per-file-review') + commits=commits, target_head="a", source_head="b", revisions=["b"], name_suffix="per-file-review" + ) # modify PR modifying file file_b - pr_util.add_one_commit(head='c') + pr_util.add_one_commit(head="c") - assert_pr_file_changes( - pull_request, - added=[], - modified=['file_b'], - removed=[]) + assert_pr_file_changes(pull_request, added=[], modified=["file_b"], removed=[]) # move the head again to d, which rollbacks change, # meaning we should indicate no changes - pr_util.add_one_commit(head='d') + pr_util.add_one_commit(head="d") - assert_pr_file_changes( - pull_request, - added=[], - modified=[], - removed=[]) + assert_pr_file_changes(pull_request, added=[], modified=[], removed=[]) def test_updated_all_files_in_pr(self, pr_util): commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [ - FileNode(b'file_a', b'test_content a\n'), - FileNode(b'file_b', b'test_content b\n'), - FileNode(b'file_c', b'test_content c\n')]}, - {'message': 'c', 'changed': [ - FileNode(b'file_a', b'test_content a changed\n'), - FileNode(b'file_b', b'test_content b changed\n'), - FileNode(b'file_c', b'test_content c changed\n')]}, + {"message": "a"}, + { + "message": "b", + "added": [ + FileNode(b"file_a", b"test_content a\n"), + FileNode(b"file_b", b"test_content b\n"), + FileNode(b"file_c", b"test_content c\n"), + ], + }, + { + "message": "c", + "changed": [ + FileNode(b"file_a", b"test_content a changed\n"), + FileNode(b"file_b", b"test_content b changed\n"), + FileNode(b"file_c", b"test_content c changed\n"), + ], + }, ] # open a PR from a to b, changing 3 files pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b'], - name_suffix='per-file-review') - - pr_util.add_one_commit(head='c') + commits=commits, target_head="a", source_head="b", revisions=["b"], name_suffix="per-file-review" + ) - assert_pr_file_changes( - pull_request, - added=[], - modified=['file_a', 'file_b', 'file_c'], - removed=[]) + pr_util.add_one_commit(head="c") + + assert_pr_file_changes(pull_request, added=[], modified=["file_a", "file_b", "file_c"], removed=[]) def test_updated_and_removed_all_files_in_pr(self, pr_util): commits = [ - {'message': 'a'}, - {'message': 'b', 'added': [ - FileNode(b'file_a', b'test_content a\n'), - FileNode(b'file_b', b'test_content b\n'), - FileNode(b'file_c', b'test_content c\n')]}, - {'message': 'c', 'removed': [ - FileNode(b'file_a', b'test_content a changed\n'), - FileNode(b'file_b', b'test_content b changed\n'), - FileNode(b'file_c', b'test_content c changed\n')]}, + {"message": "a"}, + { + "message": "b", + "added": [ + FileNode(b"file_a", b"test_content a\n"), + FileNode(b"file_b", b"test_content b\n"), + FileNode(b"file_c", b"test_content c\n"), + ], + }, + { + "message": "c", + "removed": [ + FileNode(b"file_a", b"test_content a changed\n"), + FileNode(b"file_b", b"test_content b changed\n"), + FileNode(b"file_c", b"test_content c changed\n"), + ], + }, ] # open a PR from a to b, removing 3 files pull_request = pr_util.create_pull_request( - commits=commits, target_head='a', source_head='b', revisions=['b'], - name_suffix='per-file-review') - - pr_util.add_one_commit(head='c') + commits=commits, target_head="a", source_head="b", revisions=["b"], name_suffix="per-file-review" + ) - assert_pr_file_changes( - pull_request, - added=[], - modified=[], - removed=['file_a', 'file_b', 'file_c']) + pr_util.add_one_commit(head="c") + + assert_pr_file_changes(pull_request, added=[], modified=[], removed=["file_a", "file_b", "file_c"]) def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub): @@ -865,8 +805,7 @@ def test_update_adds_a_comment_to_the_pu .. |under_review| replace:: *"Under Review"*""" ).format(commit_id[:12]) - pull_request_comments = sorted( - pull_request.comments, key=lambda c: c.modified_at) + pull_request_comments = sorted(pull_request.comments, key=lambda c: c.modified_at) update_comment = pull_request_comments[-1] assert update_comment.text == expected_message @@ -889,8 +828,8 @@ def test_create_version_from_snapshot_up version = PullRequestModel()._create_version_from_snapshot(pull_request) # Check attributes - assert version.title == pr_util.create_parameters['title'] - assert version.description == pr_util.create_parameters['description'] + assert version.title == pr_util.create_parameters["title"] + assert version.description == pr_util.create_parameters["description"] assert version.status == PullRequest.STATUS_CLOSED # versions get updated created_on @@ -898,11 +837,11 @@ def test_create_version_from_snapshot_up assert version.updated_on == updated_on assert version.user_id == pull_request.user_id - assert version.revisions == pr_util.create_parameters['revisions'] + assert version.revisions == pr_util.create_parameters["revisions"] assert version.source_repo == pr_util.source_repository - assert version.source_ref == pr_util.create_parameters['source_ref'] + assert version.source_ref == pr_util.create_parameters["source_ref"] assert version.target_repo == pr_util.target_repository - assert version.target_ref == pr_util.create_parameters['target_ref'] + assert version.target_ref == pr_util.create_parameters["target_ref"] assert version._last_merge_source_rev == pull_request._last_merge_source_rev assert version._last_merge_target_rev == pull_request._last_merge_target_rev assert version.last_merge_status == pull_request.last_merge_status @@ -920,15 +859,9 @@ def test_link_comments_to_version_only_u Session().commit() # Expect that only the new comment is linked to version2 - assert ( - comment_unlinked.pull_request_version_id == - version2.pull_request_version_id) - assert ( - comment_linked.pull_request_version_id == - version1.pull_request_version_id) - assert ( - comment_unlinked.pull_request_version_id != - comment_linked.pull_request_version_id) + assert comment_unlinked.pull_request_version_id == version2.pull_request_version_id + assert comment_linked.pull_request_version_id == version1.pull_request_version_id + assert comment_unlinked.pull_request_version_id != comment_linked.pull_request_version_id def test_calculate_commits(): @@ -944,35 +877,26 @@ def test_calculate_commits(): def assert_inline_comments(pull_request, visible=None, outdated=None): if visible is not None: inline_comments = CommentsModel().get_inline_comments( - pull_request.target_repo.repo_id, pull_request=pull_request) - inline_cnt = len(CommentsModel().get_inline_comments_as_list( - inline_comments)) + pull_request.target_repo.repo_id, pull_request=pull_request + ) + inline_cnt = len(CommentsModel().get_inline_comments_as_list(inline_comments)) assert inline_cnt == visible if outdated is not None: - outdated_comments = CommentsModel().get_outdated_comments( - pull_request.target_repo.repo_id, pull_request) + outdated_comments = CommentsModel().get_outdated_comments(pull_request.target_repo.repo_id, pull_request) assert len(outdated_comments) == outdated -def assert_pr_file_changes( - pull_request, added=None, modified=None, removed=None): +def assert_pr_file_changes(pull_request, added=None, modified=None, removed=None): pr_versions = PullRequestModel().get_versions(pull_request) # always use first version, ie original PR to calculate changes pull_request_version = pr_versions[0] - old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs( - pull_request, pull_request_version) - file_changes = PullRequestModel()._calculate_file_changes( - old_diff_data, new_diff_data) + old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(pull_request, pull_request_version) + file_changes = PullRequestModel()._calculate_file_changes(old_diff_data, new_diff_data) - assert added == file_changes.added, \ - 'expected added:%s vs value:%s' % (added, file_changes.added) - assert modified == file_changes.modified, \ - 'expected modified:%s vs value:%s' % (modified, file_changes.modified) - assert removed == file_changes.removed, \ - 'expected removed:%s vs value:%s' % (removed, file_changes.removed) + assert added == file_changes.added, "expected added:%s vs value:%s" % (added, file_changes.added) + assert modified == file_changes.modified, "expected modified:%s vs value:%s" % (modified, file_changes.modified) + assert removed == file_changes.removed, "expected removed:%s vs value:%s" % (removed, file_changes.removed) def outdated_comments_patcher(use_outdated=True): - return mock.patch.object( - CommentsModel, 'use_outdated_comments', - return_value=use_outdated) + return mock.patch.object(CommentsModel, "use_outdated_comments", return_value=use_outdated) diff --git a/rhodecode/tests/models/test_pullrequest_git.py b/rhodecode/tests/models/test_pullrequest_git.py --- a/rhodecode/tests/models/test_pullrequest_git.py +++ b/rhodecode/tests/models/test_pullrequest_git.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_repo_groups.py b/rhodecode/tests/models/test_repo_groups.py --- a/rhodecode/tests/models/test_repo_groups.py +++ b/rhodecode/tests/models/test_repo_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -23,7 +22,7 @@ from sqlalchemy.exc import IntegrityErro import pytest from rhodecode.tests import TESTS_TMP_PATH -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.repo import RepoModel diff --git a/rhodecode/tests/models/test_repo_readme.py b/rhodecode/tests/models/test_repo_readme.py --- a/rhodecode/tests/models/test_repo_readme.py +++ b/rhodecode/tests/models/test_repo_readme.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_repos.py b/rhodecode/tests/models/test_repos.py --- a/rhodecode/tests/models/test_repos.py +++ b/rhodecode/tests/models/test_repos.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -93,8 +92,8 @@ class TestRepoModel(object): is not None) @pytest.mark.parametrize("filename, expected", [ - ("README", True), - ("README.rst", False), + (b"README", True), + (b"README.rst", False), ]) def test_filenode_is_link(self, vcsbackend, filename, expected): repo = vcsbackend.repo diff --git a/rhodecode/tests/models/test_scm.py b/rhodecode/tests/models/test_scm.py --- a/rhodecode/tests/models/test_scm.py +++ b/rhodecode/tests/models/test_scm.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/models/test_user_group_permissions_on_repo_groups.py b/rhodecode/tests/models/test_user_group_permissions_on_repo_groups.py --- a/rhodecode/tests/models/test_user_group_permissions_on_repo_groups.py +++ b/rhodecode/tests/models/test_user_group_permissions_on_repo_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,7 +27,7 @@ from rhodecode.model.user_group import U from rhodecode.tests.models.common import ( _create_project_tree, check_tree_perms, _get_perms, _check_expected_count, expected_count, _destroy_project_tree) -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() diff --git a/rhodecode/tests/models/test_user_groups.py b/rhodecode/tests/models/test_user_groups.py --- a/rhodecode/tests/models/test_user_groups.py +++ b/rhodecode/tests/models/test_user_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -22,7 +21,7 @@ import pytest from rhodecode.model.db import User from rhodecode.tests import TEST_USER_REGULAR_LOGIN -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.model.user_group import UserGroupModel from rhodecode.model.meta import Session diff --git a/rhodecode/tests/models/test_user_permissions_on_repo_groups.py b/rhodecode/tests/models/test_user_permissions_on_repo_groups.py --- a/rhodecode/tests/models/test_user_permissions_on_repo_groups.py +++ b/rhodecode/tests/models/test_user_permissions_on_repo_groups.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -139,6 +138,8 @@ def test_user_permissions_on_group_with_ # set permission to g0 recursive mode, all children including # other repos and groups should have this permission now set ! + # except the PRIVATE repo which must remain repository.none permissions + # this is exclusive to the default user recursive = 'all' group = 'g0' default_user_id = User.get_default_user_id() @@ -156,10 +157,10 @@ def test_user_permissions_on_group_with_ _check_expected_count(items, repo_items, expected_count(group, True)) for name, perm in repo_items: - check_tree_perms(name, perm, group, 'repository.write') + check_tree_perms(name, perm, group, 'repository.write', default_user=True) for name, perm in items: - check_tree_perms(name, perm, group, 'group.write') + check_tree_perms(name, perm, group, 'group.write', default_user=True) def test_user_permissions_on_group_with_recursive_mode_inner_group(): diff --git a/rhodecode/tests/models/test_users.py b/rhodecode/tests/models/test_users.py --- a/rhodecode/tests/models/test_users.py +++ b/rhodecode/tests/models/test_users.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,7 +26,7 @@ from rhodecode.model.user import UserMod from rhodecode.model.user_group import UserGroupModel from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture from rhodecode.lib.str_utils import safe_str diff --git a/rhodecode/tests/other/__init__.py b/rhodecode/tests/other/__init__.py --- a/rhodecode/tests/other/__init__.py +++ b/rhodecode/tests/other/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/other/test_base_controller.py b/rhodecode/tests/other/test_base_controller.py --- a/rhodecode/tests/other/test_base_controller.py +++ b/rhodecode/tests/other/test_base_controller.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/other/test_validators.py b/rhodecode/tests/other/test_validators.py --- a/rhodecode/tests/other/test_validators.py +++ b/rhodecode/tests/other/test_validators.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,11 +31,11 @@ from rhodecode.model.meta import Session from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.db import ChangesetStatus, Repository from rhodecode.model.changeset_status import ChangesetStatusModel -from rhodecode.tests.fixture import Fixture +from rhodecode.tests.fixtures.rc_fixture import Fixture fixture = Fixture() -pytestmark = pytest.mark.usefixtures('baseapp') +pytestmark = pytest.mark.usefixtures("baseapp") @pytest.fixture() diff --git a/rhodecode/tests/other/test_views_utils.py b/rhodecode/tests/other/test_views_utils.py --- a/rhodecode/tests/other/test_views_utils.py +++ b/rhodecode/tests/other/test_views_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -111,7 +111,7 @@ app.base_url = http://rhodecode.local app.service_api.host = http://rhodecode.local:10020 ; Secret for Service API authentication. -app.service_api.token = +app.service_api.token = secret4 ; Unique application ID. Should be a random unique string for security. app_instance_uuid = rc-production @@ -229,7 +229,7 @@ license_token = abra-cada-bra1-rce3 license.hide_license_info = false ; Import EE license from this license path -#license.import_path = %(here)s/rhodecode_enterprise.license +license.import_path = %(here)s/rhodecode_enterprise.license ; import license 'if-missing' or 'force' (always override) ; if-missing means apply license if it doesn't exist. 'force' option always overrides it @@ -351,7 +351,7 @@ archive_cache.objectstore.retry_attempts ; and served from the cache during subsequent requests for the same archive of ; the repository. This path is important to be shared across filesystems and with ; RhodeCode and vcsserver -archive_cache.filesystem.store_dir = %(here)s/rc-tests/archive_cache +archive_cache.filesystem.store_dir = %(here)s/.rc-test-data/archive_cache ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb archive_cache.filesystem.cache_size_gb = 2 @@ -406,7 +406,7 @@ celery.task_store_eager_result = true ; Default cache dir for caches. Putting this into a ramdisk can boost performance. ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space -cache_dir = %(here)s/rc-test-data +cache_dir = %(here)s/.rc-test-data ; ********************************************* ; `sql_cache_short` cache for heavy SQL queries @@ -435,7 +435,7 @@ rc_cache.cache_repo_longterm.max_size = rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace rc_cache.cache_general.expiration_time = 43200 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set -rc_cache.cache_general.arguments.filename = %(here)s/rc-tests/cache-backend/cache_general_db +rc_cache.cache_general.arguments.filename = %(here)s/.rc-test-data/cache-backend/cache_general_db ; alternative `cache_general` redis backend with distributed lock #rc_cache.cache_general.backend = dogpile.cache.rc.redis @@ -454,6 +454,10 @@ rc_cache.cache_general.arguments.filenam ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_general.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_general.arguments.key_prefix = custom-prefix- + + ; ************************************************* ; `cache_perms` cache for permission tree, auth TTL ; for simplicity use rc.file_namespace backend, @@ -462,7 +466,7 @@ rc_cache.cache_general.arguments.filenam rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace rc_cache.cache_perms.expiration_time = 0 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set -rc_cache.cache_perms.arguments.filename = %(here)s/rc-tests/cache-backend/cache_perms_db +rc_cache.cache_perms.arguments.filename = %(here)s/.rc-test-data/cache-backend/cache_perms_db ; alternative `cache_perms` redis backend with distributed lock #rc_cache.cache_perms.backend = dogpile.cache.rc.redis @@ -481,6 +485,10 @@ rc_cache.cache_perms.arguments.filename ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_perms.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_perms.arguments.key_prefix = custom-prefix- + + ; *************************************************** ; `cache_repo` cache for file tree, Readme, RSS FEEDS ; for simplicity use rc.file_namespace backend, @@ -489,7 +497,7 @@ rc_cache.cache_perms.arguments.filename rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace rc_cache.cache_repo.expiration_time = 2592000 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set -rc_cache.cache_repo.arguments.filename = %(here)s/rc-tests/cache-backend/cache_repo_db +rc_cache.cache_repo.arguments.filename = %(here)s/.rc-test-data/cache-backend/cache_repo_db ; alternative `cache_repo` redis backend with distributed lock #rc_cache.cache_repo.backend = dogpile.cache.rc.redis @@ -508,6 +516,10 @@ rc_cache.cache_repo.arguments.filename = ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen #rc_cache.cache_repo.arguments.lock_auto_renewal = true +; prefix for redis keys used for this cache backend, the final key is constructed using {custom-prefix}{key} +#rc_cache.cache_repo.arguments.key_prefix = custom-prefix- + + ; ############## ; BEAKER SESSION ; ############## @@ -516,7 +528,7 @@ rc_cache.cache_repo.arguments.filename = ; types are file, ext:redis, ext:database, ext:memcached ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session beaker.session.type = file -beaker.session.data_dir = %(here)s/rc-tests/data/sessions +beaker.session.data_dir = %(here)s/.rc-test-data/data/sessions ; Redis based sessions #beaker.session.type = ext:redis @@ -532,7 +544,7 @@ beaker.session.data_dir = %(here)s/rc-te beaker.session.key = rhodecode beaker.session.secret = test-rc-uytcxaz -beaker.session.lock_dir = %(here)s/rc-tests/data/sessions/lock +beaker.session.lock_dir = %(here)s/.rc-test-data/data/sessions/lock ; Secure encrypted cookie. Requires AES and AES python libraries ; you must disable beaker.session.secret to use this @@ -564,7 +576,7 @@ beaker.session.secure = false ; WHOOSH Backend, doesn't require additional services to run ; it works good with few dozen repos search.module = rhodecode.lib.index.whoosh -search.location = %(here)s/rc-tests/data/index +search.location = %(here)s/.rc-test-data/data/index ; #################### ; CHANNELSTREAM CONFIG @@ -584,7 +596,7 @@ channelstream.server = channelstream:980 ; see Nginx/Apache configuration examples in our docs channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream channelstream.secret = ENV_GENERATED -channelstream.history.location = %(here)s/rc-tests/channelstream_history +channelstream.history.location = %(here)s/.rc-test-data/channelstream_history ; Internal application path that Javascript uses to connect into. ; If you use proxy-prefix the prefix should be added before /_channelstream @@ -601,7 +613,7 @@ channelstream.proxy_path = /_channelstre ; pymysql is an alternative driver for MySQL, use in case of problems with default one #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode -sqlalchemy.db1.url = sqlite:///%(here)s/rc-tests/rhodecode_test.db?timeout=30 +sqlalchemy.db1.url = sqlite:///%(here)s/.rc-test-data/rhodecode_test.db?timeout=30 ; see sqlalchemy docs for other advanced settings ; print the sql statements to output @@ -645,7 +657,7 @@ vcs.scm_app_implementation = http ; `http` - use http-rpc backend (default) ; `celery` - use celery based hooks #DEPRECATED:vcs.hooks.protocol = http -vcs.hooks.protocol = http +vcs.hooks.protocol.v2 = celery ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be ; accessible via network. @@ -737,7 +749,7 @@ ssh.generate_authorized_keyfile = true ; Path to the authorized_keys file where the generate entries are placed. ; It is possible to have multiple key files specified in `sshd_config` e.g. ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode -ssh.authorized_keys_file_path = %(here)s/rc-tests/authorized_keys_rhodecode +ssh.authorized_keys_file_path = %(here)s/.rc-test-data/authorized_keys_rhodecode ; Command to execute the SSH wrapper. The binary is available in the ; RhodeCode installation directory. diff --git a/rhodecode/tests/routes.py b/rhodecode/tests/routes.py --- a/rhodecode/tests/routes.py +++ b/rhodecode/tests/routes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -182,7 +182,8 @@ def get_url_defs(): "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX + "/settings/vcs/svn_pattern_delete", "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping", - "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update", + "admin_settings_mapping_create": ADMIN_PREFIX + "/settings/mapping/create", + "admin_settings_mapping_cleanup": ADMIN_PREFIX + "/settings/mapping/cleanup", "admin_settings_visual": ADMIN_PREFIX + "/settings/visual", "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update", "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker", diff --git a/rhodecode/tests/run_full_tests.sh b/rhodecode/tests/run_full_tests.sh deleted file mode 100755 --- a/rhodecode/tests/run_full_tests.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -set -e - -## MIGRATIONS AND DB TESTS ## -echo "DATABASE CREATION TESTS" -rhodecode/tests/database/test_creation.sh - -echo "DATABASE MIGRATIONS TESTS" -rhodecode/tests/database/test_migration.sh - -## TEST VCS OPERATIONS ## -echo "VCS FUNCTIONAL TESTS" -rhodecode/tests/test_vcs_operations.sh - -## TOX TESTS ## -echo "TOX TESTS" -tox -r --develop diff --git a/rhodecode/tests/scripts/test_concurency.py b/rhodecode/tests/scripts/test_concurency.py --- a/rhodecode/tests/scripts/test_concurency.py +++ b/rhodecode/tests/scripts/test_concurency.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/scripts/test_crawler.py b/rhodecode/tests/scripts/test_crawler.py --- a/rhodecode/tests/scripts/test_crawler.py +++ b/rhodecode/tests/scripts/test_crawler.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/server_utils.py b/rhodecode/tests/server_utils.py --- a/rhodecode/tests/server_utils.py +++ b/rhodecode/tests/server_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -24,13 +23,13 @@ import tempfile import pytest import subprocess import logging -from urllib.request import urlopen -from urllib.error import URLError +import requests import configparser from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS from rhodecode.tests.utils import is_url_reachable +from rhodecode.tests import console_printer log = logging.getLogger(__name__) @@ -49,7 +48,7 @@ def get_host_url(pyramid_config): def assert_no_running_instance(url): if is_url_reachable(url): - print(f"Hint: Usually this means another instance of server " + console_printer(f"Hint: Usually this means another instance of server " f"is running in the background at {url}.") pytest.fail(f"Port is not free at {url}, cannot start server at") @@ -58,8 +57,9 @@ class ServerBase(object): _args = [] log_file_name = 'NOT_DEFINED.log' status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping' + console_marker = " :warning: [green]pytest-setup[/green] " - def __init__(self, config_file, log_file): + def __init__(self, config_file, log_file, env): self.config_file = config_file config = configparser.ConfigParser() config.read(config_file) @@ -69,10 +69,10 @@ class ServerBase(object): self._args = [] self.log_file = log_file or os.path.join( tempfile.gettempdir(), self.log_file_name) + self.env = env self.process = None self.server_out = None - log.info("Using the {} configuration:{}".format( - self.__class__.__name__, config_file)) + log.info(f"Using the {self.__class__.__name__} configuration:{config_file}") if not os.path.isfile(config_file): raise RuntimeError(f'Failed to get config at {config_file}') @@ -110,18 +110,17 @@ class ServerBase(object): while time.time() - start < timeout: try: - urlopen(status_url) + requests.get(status_url) break - except URLError: + except requests.exceptions.ConnectionError: time.sleep(0.2) else: pytest.fail( - "Starting the {} failed or took more than {} " - "seconds. cmd: `{}`".format( - self.__class__.__name__, timeout, self.command)) + f"Starting the {self.__class__.__name__} failed or took more than {timeout} seconds." + f"cmd: `{self.command}`" + ) - log.info('Server of {} ready at url {}'.format( - self.__class__.__name__, status_url)) + log.info(f'Server of {self.__class__.__name__} ready at url {status_url}') def shutdown(self): self.process.kill() @@ -130,7 +129,7 @@ class ServerBase(object): def get_log_file_with_port(self): log_file = list(self.log_file.partition('.log')) - log_file.insert(1, get_port(self.config_file)) + log_file.insert(1, f'-{get_port(self.config_file)}') log_file = ''.join(log_file) return log_file @@ -140,11 +139,12 @@ class RcVCSServer(ServerBase): Represents a running VCSServer instance. """ - log_file_name = 'rc-vcsserver.log' + log_file_name = 'rhodecode-vcsserver.log' status_url_tmpl = 'http://{host}:{port}/status' - def __init__(self, config_file, log_file=None, workers='3'): - super(RcVCSServer, self).__init__(config_file, log_file) + def __init__(self, config_file, log_file=None, workers='3', env=None, info_prefix=''): + super(RcVCSServer, self).__init__(config_file, log_file, env) + self.info_prefix = info_prefix self._args = [ 'gunicorn', '--bind', self.bind_addr, @@ -164,9 +164,10 @@ class RcVCSServer(ServerBase): host_url = self.host_url() assert_no_running_instance(host_url) - print(f'rhodecode-vcsserver starting at: {host_url}') - print(f'rhodecode-vcsserver command: {self.command}') - print(f'rhodecode-vcsserver logfile: {self.log_file}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-vcsserver starting at: {host_url}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-vcsserver command: {self.command}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-vcsserver logfile: {self.log_file}') + console_printer() self.process = subprocess.Popen( self._args, bufsize=0, env=env, @@ -178,11 +179,12 @@ class RcWebServer(ServerBase): Represents a running RCE web server used as a test fixture. """ - log_file_name = 'rc-web.log' + log_file_name = 'rhodecode-ce.log' status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping' - def __init__(self, config_file, log_file=None, workers='2'): - super(RcWebServer, self).__init__(config_file, log_file) + def __init__(self, config_file, log_file=None, workers='2', env=None, info_prefix=''): + super(RcWebServer, self).__init__(config_file, log_file, env) + self.info_prefix = info_prefix self._args = [ 'gunicorn', '--bind', self.bind_addr, @@ -195,7 +197,8 @@ class RcWebServer(ServerBase): def start(self): env = os.environ.copy() - env['RC_NO_TMP_PATH'] = '1' + if self.env: + env.update(self.env) self.log_file = self.get_log_file_with_port() self.server_out = open(self.log_file, 'w') @@ -203,9 +206,10 @@ class RcWebServer(ServerBase): host_url = self.host_url() assert_no_running_instance(host_url) - print(f'rhodecode-web starting at: {host_url}') - print(f'rhodecode-web command: {self.command}') - print(f'rhodecode-web logfile: {self.log_file}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-ce starting at: {host_url}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-ce command: {self.command}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-ce logfile: {self.log_file}') + console_printer() self.process = subprocess.Popen( self._args, bufsize=0, env=env, @@ -229,3 +233,44 @@ class RcWebServer(ServerBase): } params.update(**kwargs) return params['user'], params['passwd'] + +class CeleryServer(ServerBase): + log_file_name = 'rhodecode-celery.log' + status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping' + + def __init__(self, config_file, log_file=None, workers='2', env=None, info_prefix=''): + super(CeleryServer, self).__init__(config_file, log_file, env) + self.info_prefix = info_prefix + self._args = \ + ['celery', + '--no-color', + '--app=rhodecode.lib.celerylib.loader', + 'worker', + '--autoscale=4,2', + '--max-tasks-per-child=30', + '--task-events', + '--loglevel=DEBUG', + '--ini=' + self.config_file] + + def start(self): + env = os.environ.copy() + env['RC_NO_TEST_ENV'] = '1' + + self.log_file = self.get_log_file_with_port() + self.server_out = open(self.log_file, 'w') + + host_url = "Celery" #self.host_url() + #assert_no_running_instance(host_url) + + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-celery starting at: {host_url}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-celery command: {self.command}') + console_printer(f'{self.console_marker}{self.info_prefix}rhodecode-celery logfile: {self.log_file}') + console_printer() + + self.process = subprocess.Popen( + self._args, bufsize=0, env=env, + stdout=self.server_out, stderr=self.server_out) + + + def wait_until_ready(self, timeout=30): + time.sleep(2) diff --git a/rhodecode/tests/test_vcs_operations.sh b/rhodecode/tests/test_vcs_operations.sh deleted file mode 100755 --- a/rhodecode/tests/test_vcs_operations.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -set -e - -SQLITE_DB_PATH=/mnt/hgfs/marcink-shared/workspace-python/rhodecode -RC_LOG=/tmp/rc.log -INI_FILE=test.ini -TEST_DB_NAME=rhodecode_test - - -for databaseName in p m s; do - # set the different DBs - if [ "$databaseName" = "s" ]; then - echo "sqlite" - rhodecode-config --filename=$INI_FILE --update '[app:main]sqlalchemy.db1.url=sqlite:///'$SQLITE_DB_PATH/$TEST_DB_NAME'.sqlite' - elif [ "$databaseName" = "p" ]; then - echo "postgres" - rhodecode-config --filename=$INI_FILE --update '[app:main]sqlalchemy.db1.url=postgresql://postgres:qweqwe@localhost/'$TEST_DB_NAME'' - elif [ "$databaseName" = "m" ]; then - echo "mysql" - rhodecode-config --filename=$INI_FILE --update '[app:main]sqlalchemy.db1.url=mysql://root:qweqwe@localhost/'$TEST_DB_NAME'' - fi - - # running just VCS tests - RC_NO_TMP_PATH=1 py.test \ - rhodecode/tests/other/test_vcs_operations.py - -done diff --git a/rhodecode/tests/utils.py b/rhodecode/tests/utils.py --- a/rhodecode/tests/utils.py +++ b/rhodecode/tests/utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -36,24 +36,29 @@ from webtest.app import TestResponse, Te import pytest -try: - import rc_testdata -except ImportError: - raise ImportError('Failed to import rc_testdata, ' - 'please make sure this package is installed from requirements_test.txt') - from rhodecode.model.db import User, Repository from rhodecode.model.meta import Session from rhodecode.model.scm import ScmModel from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository from rhodecode.lib.vcs.backends.base import EmptyCommit -from rhodecode.tests import login_user_session +from rhodecode.tests import login_user_session, console_printer +from rhodecode.authentication import AuthenticationPluginRegistry +from rhodecode.model.settings import SettingsModel log = logging.getLogger(__name__) -def print_to_func(value, print_to=sys.stderr): - print(value, file=print_to) +def console_printer_utils(msg): + console_printer(f" :white_check_mark: [green]test-utils[/green] {msg}") + + +def get_rc_testdata(): + try: + import rc_testdata + except ImportError: + raise ImportError('Failed to import rc_testdata, ' + 'please make sure this package is installed from requirements_test.txt') + return rc_testdata class CustomTestResponse(TestResponse): @@ -73,7 +78,6 @@ class CustomTestResponse(TestResponse): assert string in res """ print_body = kw.pop('print_body', False) - print_to = kw.pop('print_to', sys.stderr) if 'no' in kw: no = kw['no'] @@ -89,18 +93,18 @@ class CustomTestResponse(TestResponse): for s in strings: if s not in self: - print_to_func(f"Actual response (no {s!r}):", print_to=print_to) - print_to_func(f"body output saved as `{f}`", print_to=print_to) + console_printer_utils(f"Actual response (no {s!r}):") + console_printer_utils(f"body output saved as `{f}`") if print_body: - print_to_func(str(self), print_to=print_to) + console_printer_utils(str(self)) raise IndexError(f"Body does not contain string {s!r}, body output saved as {f}") for no_s in no: if no_s in self: - print_to_func(f"Actual response (has {no_s!r})", print_to=print_to) - print_to_func(f"body output saved as `{f}`", print_to=print_to) + console_printer_utils(f"Actual response (has {no_s!r})") + console_printer_utils(f"body output saved as `{f}`") if print_body: - print_to_func(str(self), print_to=print_to) + console_printer_utils(str(self)) raise IndexError(f"Body contains bad string {no_s!r}, body output saved as {f}") def assert_response(self): @@ -209,6 +213,7 @@ def extract_git_repo_from_dump(dump_name """Create git repo `repo_name` from dump `dump_name`.""" repos_path = ScmModel().repos_path target_path = os.path.join(repos_path, repo_name) + rc_testdata = get_rc_testdata() rc_testdata.extract_git_dump(dump_name, target_path) return target_path @@ -217,6 +222,7 @@ def extract_hg_repo_from_dump(dump_name, """Create hg repo `repo_name` from dump `dump_name`.""" repos_path = ScmModel().repos_path target_path = os.path.join(repos_path, repo_name) + rc_testdata = get_rc_testdata() rc_testdata.extract_hg_dump(dump_name, target_path) return target_path @@ -245,6 +251,7 @@ def _load_svn_dump_into_repo(dump_name, Currently the dumps are in rc_testdata. They might later on be integrated with the main repository once they stabilize more. """ + rc_testdata = get_rc_testdata() dump = rc_testdata.load_svn_dump(dump_name) load_dump = subprocess.Popen( ['svnadmin', 'load', repo_path], @@ -254,9 +261,7 @@ def _load_svn_dump_into_repo(dump_name, if load_dump.returncode != 0: log.error("Output of load_dump command: %s", out) log.error("Error output of load_dump command: %s", err) - raise Exception( - 'Failed to load dump "%s" into repository at path "%s".' - % (dump_name, repo_path)) + raise Exception(f'Failed to load dump "{dump_name}" into repository at path "{repo_path}".') class AssertResponse(object): @@ -296,7 +301,7 @@ class AssertResponse(object): sel = CSSSelector('a[href]') elements = [ e for e in sel(doc) if e.text_content().strip() == link_text] - assert len(elements) == 1, "Did not find link or found multiple links" + assert len(elements) == 1, f"Did not find link or found multiple links, found={len(elements)}" self._ensure_url_equal(elements[0].attrib.get('href'), href) def contains_one_anchor(self, anchor_id): @@ -304,14 +309,14 @@ class AssertResponse(object): doc = fromstring(self.response.body) sel = CSSSelector('#' + anchor_id) elements = sel(doc) - assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id) + assert len(elements) == 1, f'cannot find 1 element {anchor_id}' def _ensure_url_equal(self, found, expected): assert _Url(found) == _Url(expected) def get_element(self, css_selector): elements = self._get_elements(css_selector) - assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector) + assert len(elements) == 1, f'cannot find 1 element {css_selector}' return elements[0] def get_elements(self, css_selector): @@ -492,3 +497,54 @@ def permission_update_data_generator(csr ('perm_del_member_type_{}'.format(obj_id), obj_type), ]) return form_data + + + +class AuthPluginManager: + + def cleanup(self): + self._enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode']) + + def enable(self, plugins_list, override=None): + return self._enable_plugins(plugins_list, override) + + @classmethod + def _enable_plugins(cls, plugins_list, override: object = None): + override = override or {} + params = { + 'auth_plugins': ','.join(plugins_list), + } + + # helper translate some names to others, to fix settings code + name_map = { + 'token': 'authtoken' + } + log.debug('enable_auth_plugins: enabling following auth-plugins: %s', plugins_list) + + for module in plugins_list: + plugin_name = module.partition('#')[-1] + if plugin_name in name_map: + plugin_name = name_map[plugin_name] + enabled_plugin = f'auth_{plugin_name}_enabled' + cache_ttl = f'auth_{plugin_name}_cache_ttl' + + # default params that are needed for each plugin, + # `enabled` and `cache_ttl` + params.update({ + enabled_plugin: True, + cache_ttl: 0 + }) + if override.get: + params.update(override.get(module, {})) + + validated_params = params + + for k, v in validated_params.items(): + setting = SettingsModel().create_or_update_setting(k, v) + Session().add(setting) + Session().commit() + + AuthenticationPluginRegistry.invalidate_auth_plugins_cache(hard=True) + + enabled_plugins = SettingsModel().get_auth_plugins() + assert plugins_list == enabled_plugins diff --git a/rhodecode/tests/vcs/__init__.py b/rhodecode/tests/vcs/__init__.py --- a/rhodecode/tests/vcs/__init__.py +++ b/rhodecode/tests/vcs/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tests/vcs/conftest.py b/rhodecode/tests/vcs/conftest.py --- a/rhodecode/tests/vcs/conftest.py +++ b/rhodecode/tests/vcs/conftest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,8 +31,7 @@ from rhodecode.tests.utils import check_ @pytest.fixture() -def vcs_repository_support( - request, backend_alias, baseapp, _vcs_repo_container): +def vcs_repository_support(request, backend_alias, baseapp, _vcs_repo_container): """ Provide a test repository for the test run. @@ -63,7 +61,7 @@ def vcs_repository_support( return backend_alias, repo -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def _vcs_repo_container(request): """ Internal fixture intended to help support class based scoping on demand. @@ -73,13 +71,12 @@ def _vcs_repo_container(request): def _create_vcs_repo_container(request): repo_container = VcsRepoContainer() - if not request.config.getoption('--keep-tmp-path'): + if not request.config.getoption("--keep-tmp-path"): request.addfinalizer(repo_container.cleanup) return repo_container class VcsRepoContainer(object): - def __init__(self): self._cleanup_paths = [] self._repos = {} @@ -98,14 +95,14 @@ class VcsRepoContainer(object): def _should_create_repo_per_test(cls): - return getattr(cls, 'recreate_repo_per_test', False) + return getattr(cls, "recreate_repo_per_test", False) def _create_empty_repository(cls, backend_alias=None): Backend = get_backend(backend_alias or cls.backend_alias) repo_path = get_new_dir(str(time.time())) repo = Backend(repo_path, create=True) - if hasattr(cls, '_get_commits'): + if hasattr(cls, "_get_commits"): commits = cls._get_commits() cls.tip = _add_commits_to_repo(repo, commits) @@ -127,7 +124,7 @@ def config(): specific content is required. """ config = Config() - config.set('section-a', 'a-1', 'value-a-1') + config.set("section-a", "a-1", "value-a-1") return config @@ -136,24 +133,24 @@ def _add_commits_to_repo(repo, commits): tip = None for commit in commits: - for node in commit.get('added', []): + for node in commit.get("added", []): if not isinstance(node, FileNode): node = FileNode(safe_bytes(node.path), content=node.content) imc.add(node) - for node in commit.get('changed', []): + for node in commit.get("changed", []): if not isinstance(node, FileNode): node = FileNode(safe_bytes(node.path), content=node.content) imc.change(node) - for node in commit.get('removed', []): + for node in commit.get("removed", []): imc.remove(FileNode(safe_bytes(node.path))) tip = imc.commit( - message=str(commit['message']), - author=str(commit['author']), - date=commit['date'], - branch=commit.get('branch') + message=str(commit["message"]), + author=str(commit["author"]), + date=commit["date"], + branch=commit.get("branch"), ) return tip @@ -183,16 +180,15 @@ def generate_repo_with_commits(vcs_repo) start_date = datetime.datetime(2010, 1, 1, 20) for x in range(num): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'file_%d.txt' % x, content=b'Foobar %d' % x), + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [ + FileNode(b"file_%d.txt" % x, content=b"Foobar %d" % x), ], - 'modified': [ - FileNode(b'file_%d.txt' % x, - content=b'Foobar %d modified' % (x-1)), - ] + "modified": [ + FileNode(b"file_%d.txt" % x, content=b"Foobar %d modified" % (x - 1)), + ], } def commit_maker(num=5): @@ -231,34 +227,33 @@ class BackendTestMixin(object): created before every single test. Defaults to ``True``. """ + recreate_repo_per_test = True @classmethod def _get_commits(cls): commits = [ { - 'message': 'Initial commit', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'foobar', content=b'Foobar'), - FileNode(b'foobar2', content=b'Foobar II'), - FileNode(b'foo/bar/baz', content=b'baz here!'), + "message": "Initial commit", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"foobar", content=b"Foobar"), + FileNode(b"foobar2", content=b"Foobar II"), + FileNode(b"foo/bar/baz", content=b"baz here!"), ], }, { - 'message': 'Changes...', - 'author': 'Jane Doe ', - 'date': datetime.datetime(2010, 1, 1, 21), - 'added': [ - FileNode(b'some/new.txt', content=b'news...'), + "message": "Changes...", + "author": "Jane Doe ", + "date": datetime.datetime(2010, 1, 1, 21), + "added": [ + FileNode(b"some/new.txt", content=b"news..."), ], - 'changed': [ - FileNode(b'foobar', b'Foobar I'), + "changed": [ + FileNode(b"foobar", b"Foobar I"), ], - 'removed': [], + "removed": [], }, ] return commits - - diff --git a/rhodecode/tests/vcs/test_archives.py b/rhodecode/tests/vcs/test_archives.py --- a/rhodecode/tests/vcs/test_archives.py +++ b/rhodecode/tests/vcs/test_archives.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -29,7 +28,7 @@ import pytest import rhodecode from rhodecode.lib.archive_cache import get_archival_config -from rhodecode.lib.str_utils import ascii_bytes +from rhodecode.lib.str_utils import ascii_bytes, safe_bytes, safe_str from rhodecode.lib.vcs.backends import base from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError from rhodecode.lib.vcs.nodes import FileNode @@ -43,121 +42,122 @@ def d_cache_config(): @pytest.mark.usefixtures("vcs_repository_support") class TestArchives(BackendTestMixin): - @classmethod def _get_commits(cls): start_date = datetime.datetime(2010, 1, 1, 20) yield { - 'message': 'Initial Commit', - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12), - 'added': [ - FileNode(b'executable_0o100755', b'mode_755', mode=0o100755), - FileNode(b'executable_0o100500', b'mode_500', mode=0o100500), - FileNode(b'not_executable', b'mode_644', mode=0o100644), + "message": "Initial Commit", + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12), + "added": [ + FileNode(b"executable_0o100755", b"mode_755", mode=0o100755), + FileNode(b"executable_0o100500", b"mode_500", mode=0o100500), + FileNode(b"not_executable", b"mode_644", mode=0o100644), ], } for x in range(5): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'%d/file_%d.txt' % (x, x), content=b'Foobar %d' % x), + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [ + FileNode(b"%d/file_%d.txt" % (x, x), content=b"Foobar %d" % x), ], } - @pytest.mark.parametrize('compressor', ['gz', 'bz2']) + @pytest.mark.parametrize("compressor", ["gz", "bz2"]) def test_archive_tar(self, compressor, tmpdir, tmp_path, d_cache_config): - - archive_node = tmp_path / 'archive-node' + archive_node = tmp_path / "archive-node" archive_node.touch() archive_lnk = self.tip.archive_repo( - str(archive_node), kind=f't{compressor}', archive_dir_name='repo', cache_config=d_cache_config) + str(archive_node), kind=f"t{compressor}", archive_dir_name="repo", cache_config=d_cache_config + ) out_dir = tmpdir - out_file = tarfile.open(str(archive_lnk), f'r|{compressor}') + out_file = tarfile.open(str(archive_lnk), f"r|{compressor}") out_file.extractall(out_dir) out_file.close() for x in range(5): - node_path = '%d/file_%d.txt' % (x, x) - with open(os.path.join(out_dir, 'repo/' + node_path), 'rb') as f: + node_path = b"%d/file_%d.txt" % (x, x) + with open(os.path.join(safe_bytes(str(out_dir)), b"repo/" + node_path), "rb") as f: file_content = f.read() assert file_content == self.tip.get_node(node_path).content shutil.rmtree(out_dir) - @pytest.mark.parametrize('compressor', ['gz', 'bz2']) + @pytest.mark.parametrize("compressor", ["gz", "bz2"]) def test_archive_tar_symlink(self, compressor): - pytest.skip('Not supported') + pytest.skip("Not supported") - @pytest.mark.parametrize('compressor', ['gz', 'bz2']) + @pytest.mark.parametrize("compressor", ["gz", "bz2"]) def test_archive_tar_file_modes(self, compressor, tmpdir, tmp_path, d_cache_config): - archive_node = tmp_path / 'archive-node' + archive_node = tmp_path / "archive-node" archive_node.touch() archive_lnk = self.tip.archive_repo( - str(archive_node), kind='t{}'.format(compressor), archive_dir_name='repo', cache_config=d_cache_config) + str(archive_node), kind="t{}".format(compressor), archive_dir_name="repo", cache_config=d_cache_config + ) out_dir = tmpdir - out_file = tarfile.open(str(archive_lnk), 'r|{}'.format(compressor)) + out_file = tarfile.open(str(archive_lnk), "r|{}".format(compressor)) out_file.extractall(out_dir) out_file.close() def dest(inp): return os.path.join(out_dir, "repo/" + inp) - assert oct(os.stat(dest('not_executable')).st_mode) == '0o100644' + assert oct(os.stat(dest("not_executable")).st_mode) == "0o100644" def test_archive_zip(self, tmp_path, d_cache_config): - archive_node = tmp_path / 'archive-node' - archive_node.touch() - - archive_lnk = self.tip.archive_repo(str(archive_node), kind='zip', - archive_dir_name='repo', cache_config=d_cache_config) - zip_file = zipfile.ZipFile(str(archive_lnk)) - - for x in range(5): - node_path = '%d/file_%d.txt' % (x, x) - data = zip_file.read(f'repo/{node_path}') - - decompressed = io.BytesIO() - decompressed.write(data) - assert decompressed.getvalue() == \ - self.tip.get_node(node_path).content - decompressed.close() - - def test_archive_zip_with_metadata(self, tmp_path, d_cache_config): - archive_node = tmp_path / 'archive-node' + archive_node = tmp_path / "archive-node" archive_node.touch() archive_lnk = self.tip.archive_repo( - str(archive_node), kind='zip', - archive_dir_name='repo', write_metadata=True, cache_config=d_cache_config) + str(archive_node), kind="zip", archive_dir_name="repo", cache_config=d_cache_config + ) + zip_file = zipfile.ZipFile(str(archive_lnk)) + + for x in range(5): + node_path = b"%d/file_%d.txt" % (x, x) + # NOTE: zipfile operates only on strings inside the archive + data = zip_file.read(safe_str(b"repo/%s" % node_path)) + + decompressed = io.BytesIO() + decompressed.write(data) + assert decompressed.getvalue() == self.tip.get_node(node_path).content + decompressed.close() + + def test_archive_zip_with_metadata(self, tmp_path, d_cache_config): + archive_node = tmp_path / "archive-node" + archive_node.touch() + + archive_lnk = self.tip.archive_repo( + str(archive_node), kind="zip", archive_dir_name="repo", write_metadata=True, cache_config=d_cache_config + ) zip_file = zipfile.ZipFile(str(archive_lnk)) - metafile = zip_file.read('repo/.archival.txt') + metafile = zip_file.read("repo/.archival.txt") raw_id = ascii_bytes(self.tip.raw_id) - assert b'commit_id:%b' % raw_id in metafile + assert b"commit_id:%b" % raw_id in metafile for x in range(5): - node_path = '%d/file_%d.txt' % (x, x) - data = zip_file.read(f'repo/{node_path}') + node_path = b"%d/file_%d.txt" % (x, x) + # NOTE: zipfile operates only on strings inside the archive + data = zip_file.read(safe_str(b"repo/%s" % node_path)) decompressed = io.BytesIO() decompressed.write(data) - assert decompressed.getvalue() == \ - self.tip.get_node(node_path).content + assert decompressed.getvalue() == self.tip.get_node(node_path).content decompressed.close() def test_archive_wrong_kind(self, tmp_path, d_cache_config): - archive_node = tmp_path / 'archive-node' + archive_node = tmp_path / "archive-node" archive_node.touch() with pytest.raises(ImproperArchiveTypeError): - self.tip.archive_repo(str(archive_node), kind='wrong kind', cache_config=d_cache_config) + self.tip.archive_repo(str(archive_node), kind="wrong kind", cache_config=d_cache_config) @pytest.fixture() @@ -167,8 +167,8 @@ def base_commit(): """ commit = base.BaseCommit() commit.repository = mock.Mock() - commit.repository.name = 'fake_repo' - commit.short_id = 'fake_id' + commit.repository.name = "fake_repo" + commit.short_id = "fake_id" return commit @@ -180,19 +180,17 @@ def test_validate_archive_prefix_enforce def test_validate_archive_prefix_empty_prefix(base_commit): # TODO: johbo: Should raise a ValueError here. with pytest.raises(VCSError): - base_commit._validate_archive_prefix('') + base_commit._validate_archive_prefix("") def test_validate_archive_prefix_with_leading_slash(base_commit): # TODO: johbo: Should raise a ValueError here. with pytest.raises(VCSError): - base_commit._validate_archive_prefix('/any') + base_commit._validate_archive_prefix("/any") def test_validate_archive_prefix_falls_back_to_repository_name(base_commit): prefix = base_commit._validate_archive_prefix(None) - expected_prefix = base_commit._ARCHIVE_PREFIX_TEMPLATE.format( - repo_name='fake_repo', - short_id='fake_id') + expected_prefix = base_commit._ARCHIVE_PREFIX_TEMPLATE.format(repo_name="fake_repo", short_id="fake_id") assert isinstance(prefix, str) assert prefix == expected_prefix diff --git a/rhodecode/tests/vcs/test_branches.py b/rhodecode/tests/vcs/test_branches.py --- a/rhodecode/tests/vcs/test_branches.py +++ b/rhodecode/tests/vcs/test_branches.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -64,18 +64,14 @@ class TestBranches(BackendTestMixin): def test_new_head(self): tip = self.repo.get_commit() - self.imc.add( - FileNode(b"docs/index.txt", content=b"Documentation\n") - ) + self.imc.add(FileNode(b"docs/index.txt", content=b"Documentation\n")) foobar_tip = self.imc.commit( message="New branch: foobar", author="joe ", branch="foobar", parents=[tip], ) - self.imc.change( - FileNode(b"docs/index.txt", content=b"Documentation\nand more...\n") - ) + self.imc.change(FileNode(b"docs/index.txt", content=b"Documentation\nand more...\n")) assert foobar_tip.branch == "foobar" newtip = self.imc.commit( message="At foobar_tip branch", @@ -96,21 +92,15 @@ class TestBranches(BackendTestMixin): @pytest.mark.backends("git", "hg") def test_branch_with_slash_in_name(self): self.imc.add(FileNode(b"extrafile", content=b"Some data\n")) - self.imc.commit( - "Branch with a slash!", author="joe ", branch="issue/123" - ) + self.imc.commit("Branch with a slash!", author="joe ", branch="issue/123") assert "issue/123" in self.repo.branches @pytest.mark.backends("git", "hg") def test_branch_with_slash_in_name_and_similar_without(self): self.imc.add(FileNode(b"extrafile", content=b"Some data\n")) - self.imc.commit( - "Branch with a slash!", author="joe ", branch="issue/123" - ) + self.imc.commit("Branch with a slash!", author="joe ", branch="issue/123") self.imc.add(FileNode(b"extrafile II", content=b"Some data\n")) - self.imc.commit( - "Branch without a slash...", author="joe ", branch="123" - ) + self.imc.commit("Branch without a slash...", author="joe ", branch="123") assert "issue/123" in self.repo.branches assert "123" in self.repo.branches diff --git a/rhodecode/tests/vcs/test_client_http.py b/rhodecode/tests/vcs/test_client_http.py --- a/rhodecode/tests/vcs/test_client_http.py +++ b/rhodecode/tests/vcs/test_client_http.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,9 +27,7 @@ from rhodecode.lib.vcs import client_htt def is_new_connection(logger, level, message): - return ( - logger == 'requests.packages.urllib3.connectionpool' and - message.startswith('Starting new HTTP')) + return logger == "requests.packages.urllib3.connectionpool" and message.startswith("Starting new HTTP") @pytest.fixture() @@ -54,7 +51,7 @@ def stub_fail_session(): """ session = mock.Mock() post = session.post() - post.content = msgpack.packb({'error': '500'}) + post.content = msgpack.packb({"error": "500"}) post.status_code = 500 session.reset_mock() @@ -89,44 +86,37 @@ def test_uses_persistent_http_connection for x in range(5): remote_call(normal=True, closed=False) - new_connections = [ - r for r in caplog.record_tuples if is_new_connection(*r)] + new_connections = [r for r in caplog.record_tuples if is_new_connection(*r)] assert len(new_connections) <= 1 def test_repo_maker_uses_session_for_classmethods(stub_session_factory): - repo_maker = client_http.RemoteVCSMaker( - 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory) + repo_maker = client_http.RemoteVCSMaker("server_and_port", "endpoint", "test_dummy_scm", stub_session_factory) repo_maker.example_call() - stub_session_factory().post.assert_called_with( - 'http://server_and_port/endpoint', data=mock.ANY) + stub_session_factory().post.assert_called_with("http://server_and_port/endpoint", data=mock.ANY) -def test_repo_maker_uses_session_for_instance_methods( - stub_session_factory, config): - repo_maker = client_http.RemoteVCSMaker( - 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory) - repo = repo_maker('stub_path', 'stub_repo_id', config) +def test_repo_maker_uses_session_for_instance_methods(stub_session_factory, config): + repo_maker = client_http.RemoteVCSMaker("server_and_port", "endpoint", "test_dummy_scm", stub_session_factory) + repo = repo_maker("stub_path", "stub_repo_id", config) repo.example_call() - stub_session_factory().post.assert_called_with( - 'http://server_and_port/endpoint', data=mock.ANY) + stub_session_factory().post.assert_called_with("http://server_and_port/endpoint", data=mock.ANY) -@mock.patch('rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory') -@mock.patch('rhodecode.lib.vcs.connection') -def test_connect_passes_in_the_same_session( - connection, session_factory_class, stub_session): +@mock.patch("rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory") +@mock.patch("rhodecode.lib.vcs.connection") +def test_connect_passes_in_the_same_session(connection, session_factory_class, stub_session): session_factory = session_factory_class.return_value session_factory.return_value = stub_session - vcs.connect_http('server_and_port') + vcs.connect_http("server_and_port") -def test_repo_maker_uses_session_that_throws_error( - stub_session_failing_factory, config): +def test_repo_maker_uses_session_that_throws_error(stub_session_failing_factory, config): repo_maker = client_http.RemoteVCSMaker( - 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_failing_factory) - repo = repo_maker('stub_path', 'stub_repo_id', config) + "server_and_port", "endpoint", "test_dummy_scm", stub_session_failing_factory + ) + repo = repo_maker("stub_path", "stub_repo_id", config) with pytest.raises(exceptions.HttpVCSCommunicationError): repo.example_call() diff --git a/rhodecode/tests/vcs/test_commits.py b/rhodecode/tests/vcs/test_commits.py --- a/rhodecode/tests/vcs/test_commits.py +++ b/rhodecode/tests/vcs/test_commits.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -23,27 +22,27 @@ import time import pytest from rhodecode.lib.str_utils import safe_bytes -from rhodecode.lib.vcs.backends.base import ( - CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit) +from rhodecode.lib.vcs.backends.base import CollectionGenerator, EmptyCommit from rhodecode.lib.vcs.exceptions import ( - BranchDoesNotExistError, CommitDoesNotExistError, - RepositoryError, EmptyRepositoryError) -from rhodecode.lib.vcs.nodes import ( - FileNode, AddedFileNodesGenerator, - ChangedFileNodesGenerator, RemovedFileNodesGenerator) + BranchDoesNotExistError, + CommitDoesNotExistError, + RepositoryError, + EmptyRepositoryError, +) +from rhodecode.lib.vcs.nodes import FileNode from rhodecode.tests import get_new_dir from rhodecode.tests.vcs.conftest import BackendTestMixin +from rhodecode.lib.vcs_common import NodeKind, FILEMODE_EXECUTABLE, FILEMODE_DEFAULT, FILEMODE_LINK class TestBaseChangeset(object): - def test_is_deprecated(self): from rhodecode.lib.vcs.backends.base import BaseChangeset + pytest.deprecated_call(BaseChangeset) class TestEmptyCommit(object): - def test_branch_without_alias_returns_none(self): commit = EmptyCommit() assert commit.branch is None @@ -58,29 +57,28 @@ class TestCommitsInNonEmptyRepo(BackendT start_date = datetime.datetime(2010, 1, 1, 20) for x in range(5): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'file_%d.txt' % x, - content=b'Foobar %d' % x), + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [ + FileNode(b"file_%d.txt" % x, content=b"Foobar %d" % x), ], } def test_walk_returns_empty_list_in_case_of_file(self): - result = list(self.tip.walk('file_0.txt')) + result = list(self.tip.walk(b"file_0.txt")) assert result == [] @pytest.mark.backends("git", "hg") def test_new_branch(self): - self.imc.add(FileNode(b'docs/index.txt', content=b'Documentation\n')) + self.imc.add(FileNode(b"docs/index.txt", content=b"Documentation\n")) foobar_tip = self.imc.commit( - message='New branch: foobar', - author='joe ', - branch='foobar', + message="New branch: foobar", + author="joe ", + branch="foobar", ) - assert 'foobar' in self.repo.branches - assert foobar_tip.branch == 'foobar' + assert "foobar" in self.repo.branches + assert foobar_tip.branch == "foobar" # 'foobar' should be the only branch that contains the new commit branch = list(self.repo.branches.values()) assert branch[0] != branch[1] @@ -89,18 +87,14 @@ class TestCommitsInNonEmptyRepo(BackendT def test_new_head_in_default_branch(self): tip = self.repo.get_commit() - self.imc.add( - FileNode(b"docs/index.txt", content=b"Documentation\n") - ) + self.imc.add(FileNode(b"docs/index.txt", content=b"Documentation\n")) foobar_tip = self.imc.commit( message="New branch: foobar", author="joe ", branch="foobar", parents=[tip], ) - self.imc.change( - FileNode(b"docs/index.txt", content=b"Documentation\nand more...\n") - ) + self.imc.change(FileNode(b"docs/index.txt", content=b"Documentation\nand more...\n")) assert foobar_tip.branch == "foobar" newtip = self.imc.commit( message="At foobar_tip branch", @@ -132,51 +126,55 @@ class TestCommitsInNonEmptyRepo(BackendT :return: """ DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME - TEST_BRANCH = 'docs' + TEST_BRANCH = "docs" org_tip = self.repo.get_commit() - self.imc.add(FileNode(b'readme.txt', content=b'Document\n')) + self.imc.add(FileNode(b"readme.txt", content=b"Document\n")) initial = self.imc.commit( - message='Initial commit', - author='joe ', + message="Initial commit", + author="joe ", parents=[org_tip], - branch=DEFAULT_BRANCH,) + branch=DEFAULT_BRANCH, + ) - self.imc.add(FileNode(b'newdoc.txt', content=b'foobar\n')) + self.imc.add(FileNode(b"newdoc.txt", content=b"foobar\n")) docs_branch_commit1 = self.imc.commit( - message='New branch: docs', - author='joe ', + message="New branch: docs", + author="joe ", parents=[initial], - branch=TEST_BRANCH,) + branch=TEST_BRANCH, + ) - self.imc.add(FileNode(b'newdoc2.txt', content=b'foobar2\n')) + self.imc.add(FileNode(b"newdoc2.txt", content=b"foobar2\n")) docs_branch_commit2 = self.imc.commit( - message='New branch: docs2', - author='joe ', + message="New branch: docs2", + author="joe ", parents=[docs_branch_commit1], - branch=TEST_BRANCH,) + branch=TEST_BRANCH, + ) - self.imc.add(FileNode(b'newfile', content=b'hello world\n')) + self.imc.add(FileNode(b"newfile", content=b"hello world\n")) self.imc.commit( - message='Back in default branch', - author='joe ', + message="Back in default branch", + author="joe ", parents=[initial], - branch=DEFAULT_BRANCH,) + branch=DEFAULT_BRANCH, + ) default_branch_commits = self.repo.get_commits(branch_name=DEFAULT_BRANCH) assert docs_branch_commit1 not in list(default_branch_commits) assert docs_branch_commit2 not in list(default_branch_commits) docs_branch_commits = self.repo.get_commits( - start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1], - branch_name=TEST_BRANCH) + start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1], branch_name=TEST_BRANCH + ) assert docs_branch_commit1 in list(docs_branch_commits) assert docs_branch_commit2 in list(docs_branch_commits) @pytest.mark.backends("svn") def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn): - repo = vcsbackend_svn['svn-simple-layout'] - commits = repo.get_commits(branch_name='trunk') + repo = vcsbackend_svn["svn-simple-layout"] + commits = repo.get_commits(branch_name="trunk") commit_indexes = [c.idx for c in commits] assert commit_indexes == [1, 2, 3, 7, 12, 15] @@ -214,13 +212,10 @@ class TestCommits(BackendTestMixin): start_date = datetime.datetime(2010, 1, 1, 20) for x in range(5): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'file_%d.txt' % x, - content=b'Foobar %d' % x) - ], + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [FileNode(b"file_%d.txt" % x, content=b"Foobar %d" % x)], } def test_simple(self): @@ -231,11 +226,11 @@ class TestCommits(BackendTestMixin): tip = self.repo.get_commit() # json.dumps(tip) uses .__json__() method data = tip.__json__() - assert 'branch' in data - assert data['revision'] + assert "branch" in data + assert data["revision"] def test_retrieve_tip(self): - tip = self.repo.get_commit('tip') + tip = self.repo.get_commit("tip") assert tip == self.repo.get_commit() def test_invalid(self): @@ -259,34 +254,34 @@ class TestCommits(BackendTestMixin): def test_size(self): tip = self.repo.get_commit() - size = 5 * len('Foobar N') # Size of 5 files + size = 5 * len("Foobar N") # Size of 5 files assert tip.size == size def test_size_at_commit(self): tip = self.repo.get_commit() - size = 5 * len('Foobar N') # Size of 5 files + size = 5 * len("Foobar N") # Size of 5 files assert self.repo.size_at_commit(tip.raw_id) == size def test_size_at_first_commit(self): commit = self.repo[0] - size = len('Foobar N') # Size of 1 file + size = len("Foobar N") # Size of 1 file assert self.repo.size_at_commit(commit.raw_id) == size def test_author(self): tip = self.repo.get_commit() - assert_text_equal(tip.author, 'Joe Doe ') + assert_text_equal(tip.author, "Joe Doe ") def test_author_name(self): tip = self.repo.get_commit() - assert_text_equal(tip.author_name, 'Joe Doe') + assert_text_equal(tip.author_name, "Joe Doe") def test_author_email(self): tip = self.repo.get_commit() - assert_text_equal(tip.author_email, 'joe.doe@example.com') + assert_text_equal(tip.author_email, "joe.doe@example.com") def test_message(self): tip = self.repo.get_commit() - assert_text_equal(tip.message, 'Commit 4') + assert_text_equal(tip.message, "Commit 4") def test_diff(self): tip = self.repo.get_commit() @@ -296,7 +291,7 @@ class TestCommits(BackendTestMixin): def test_prev(self): tip = self.repo.get_commit() prev_commit = tip.prev() - assert prev_commit.message == 'Commit 3' + assert prev_commit.message == "Commit 3" def test_prev_raises_on_first_commit(self): commit = self.repo.get_commit(commit_idx=0) @@ -311,7 +306,7 @@ class TestCommits(BackendTestMixin): def test_next(self): commit = self.repo.get_commit(commit_idx=2) next_commit = commit.next() - assert next_commit.message == 'Commit 3' + assert next_commit.message == "Commit 3" def test_next_raises_on_tip(self): commit = self.repo.get_commit() @@ -320,36 +315,36 @@ class TestCommits(BackendTestMixin): def test_get_path_commit(self): commit = self.repo.get_commit() - commit.get_path_commit('file_4.txt') - assert commit.message == 'Commit 4' + commit.get_path_commit(b"file_4.txt") + assert commit.message == "Commit 4" def test_get_filenodes_generator(self): tip = self.repo.get_commit() filepaths = [node.path for node in tip.get_filenodes_generator()] - assert filepaths == ['file_%d.txt' % x for x in range(5)] + assert filepaths == ["file_%d.txt" % x for x in range(5)] def test_get_file_annotate(self): file_added_commit = self.repo.get_commit(commit_idx=3) - annotations = list(file_added_commit.get_file_annotate('file_3.txt')) + annotations = list(file_added_commit.get_file_annotate("file_3.txt")) line_no, commit_id, commit_loader, line = annotations[0] assert line_no == 1 assert commit_id == file_added_commit.raw_id assert commit_loader() == file_added_commit - assert b'Foobar 3' in line + assert b"Foobar 3" in line def test_get_file_annotate_does_not_exist(self): file_added_commit = self.repo.get_commit(commit_idx=2) # TODO: Should use a specific exception class here? with pytest.raises(Exception): - list(file_added_commit.get_file_annotate('file_3.txt')) + list(file_added_commit.get_file_annotate("file_3.txt")) def test_get_file_annotate_tip(self): tip = self.repo.get_commit() commit = self.repo.get_commit(commit_idx=3) - expected_values = list(commit.get_file_annotate('file_3.txt')) - annotations = list(tip.get_file_annotate('file_3.txt')) + expected_values = list(commit.get_file_annotate("file_3.txt")) + annotations = list(tip.get_file_annotate("file_3.txt")) # Note: Skip index 2 because the loader function is not the same for idx in (0, 1, 3): @@ -398,7 +393,7 @@ class TestCommits(BackendTestMixin): repo = self.Backend(repo_path, create=True) with pytest.raises(EmptyRepositoryError): - list(repo.get_commits(start_id='foobar')) + list(repo.get_commits(start_id="foobar")) def test_get_commits_respects_hidden(self): commits = self.repo.get_commits(show_hidden=True) @@ -424,8 +419,7 @@ class TestCommits(BackendTestMixin): def test_get_commits_respects_start_date_with_branch(self): start_date = datetime.datetime(2010, 1, 2) - commits = self.repo.get_commits( - start_date=start_date, branch_name=self.repo.DEFAULT_BRANCH_NAME) + commits = self.repo.get_commits(start_date=start_date, branch_name=self.repo.DEFAULT_BRANCH_NAME) assert isinstance(commits, CollectionGenerator) # Should be 4 commits after 2010-01-02 00:00:00 assert len(commits) == 4 @@ -435,8 +429,7 @@ class TestCommits(BackendTestMixin): def test_get_commits_respects_start_date_and_end_date(self): start_date = datetime.datetime(2010, 1, 2) end_date = datetime.datetime(2010, 1, 3) - commits = self.repo.get_commits(start_date=start_date, - end_date=end_date) + commits = self.repo.get_commits(start_date=start_date, end_date=end_date) assert isinstance(commits, CollectionGenerator) assert len(commits) == 2 for c in commits: @@ -459,23 +452,22 @@ class TestCommits(BackendTestMixin): assert list(commit_ids) == list(reversed(self.repo.commit_ids)) def test_get_commits_slice_generator(self): - commits = self.repo.get_commits( - branch_name=self.repo.DEFAULT_BRANCH_NAME) + commits = self.repo.get_commits(branch_name=self.repo.DEFAULT_BRANCH_NAME) assert isinstance(commits, CollectionGenerator) commit_slice = list(commits[1:3]) assert len(commit_slice) == 2 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self): with pytest.raises(CommitDoesNotExistError): - list(self.repo.get_commits(start_id='foobar')) + list(self.repo.get_commits(start_id="foobar")) def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self): with pytest.raises(CommitDoesNotExistError): - list(self.repo.get_commits(end_id='foobar')) + list(self.repo.get_commits(end_id="foobar")) def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self): with pytest.raises(BranchDoesNotExistError): - list(self.repo.get_commits(branch_name='foobar')) + list(self.repo.get_commits(branch_name="foobar")) def test_get_commits_raise_repositoryerror_for_wrong_start_end(self): start_id = self.repo.commit_ids[-1] @@ -498,13 +490,16 @@ class TestCommits(BackendTestMixin): assert commit1 is not None assert commit2 is not None assert 1 != commit1 - assert 'string' != commit1 + assert "string" != commit1 -@pytest.mark.parametrize("filename, expected", [ - ("README.rst", False), - ("README", True), -]) +@pytest.mark.parametrize( + "filename, expected", + [ + (b"README.rst", False), + (b"README", True), + ], +) def test_commit_is_link(vcsbackend, filename, expected): commit = vcsbackend.repo.get_commit() link_status = commit.is_link(filename) @@ -519,75 +514,66 @@ class TestCommitsChanges(BackendTestMixi def _get_commits(cls): return [ { - 'message': 'Initial', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'foo/bar', content=b'foo'), - FileNode(safe_bytes('foo/bał'), content=b'foo'), - FileNode(b'foobar', content=b'foo'), - FileNode(b'qwe', content=b'foo'), + "message": "Initial", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"foo/bar", content=b"foo"), + FileNode(safe_bytes("foo/bał"), content=b"foo"), + FileNode(b"foobar", content=b"foo"), + FileNode(b"qwe", content=b"foo"), ], }, { - 'message': 'Massive changes', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 22), - 'added': [FileNode(b'fallout', content=b'War never changes')], - 'changed': [ - FileNode(b'foo/bar', content=b'baz'), - FileNode(b'foobar', content=b'baz'), + "message": "Massive changes", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 22), + "added": [FileNode(b"fallout", content=b"War never changes")], + "changed": [ + FileNode(b"foo/bar", content=b"baz"), + FileNode(b"foobar", content=b"baz"), ], - 'removed': [FileNode(b'qwe')], + "removed": [FileNode(b"qwe")], }, ] def test_initial_commit(self, local_dt_to_utc): commit = self.repo.get_commit(commit_idx=0) - assert set(commit.added) == { - commit.get_node('foo/bar'), - commit.get_node('foo/bał'), - commit.get_node('foobar'), - commit.get_node('qwe') - } - assert set(commit.changed) == set() - assert set(commit.removed) == set() - assert set(commit.affected_files) == {'foo/bar', 'foo/bał', 'foobar', 'qwe'} - assert commit.date == local_dt_to_utc( - datetime.datetime(2010, 1, 1, 20, 0)) + assert sorted(commit.added_paths) == sorted([b"foo/bar", b"foo/ba\xc5\x82", b"foobar", b"qwe"]) + assert commit.changed_paths == [] + assert commit.removed_paths == [] + assert sorted(commit.affected_files) == sorted([b"foo/bar", b"foo/ba\xc5\x82", b"foobar", b"qwe"]) + assert commit.date == local_dt_to_utc(datetime.datetime(2010, 1, 1, 20, 0)) def test_head_added(self): commit = self.repo.get_commit() - assert isinstance(commit.added, AddedFileNodesGenerator) - assert set(commit.added) == {commit.get_node('fallout')} - assert isinstance(commit.changed, ChangedFileNodesGenerator) - assert set(commit.changed) == {commit.get_node('foo/bar'), commit.get_node('foobar')} - assert isinstance(commit.removed, RemovedFileNodesGenerator) - assert len(commit.removed) == 1 - assert list(commit.removed)[0].path == 'qwe' + + assert commit.added_paths == [b"fallout"] + assert commit.changed_paths == [b"foo/bar", b"foobar"] + assert commit.removed_paths == [b"qwe"] def test_get_filemode(self): commit = self.repo.get_commit() - assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar') + assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/bar") def test_get_filemode_non_ascii(self): commit = self.repo.get_commit() - assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał') - assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał') + assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/ba\xc5\x82") + assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/ba\xc5\x82") def test_get_path_history(self): commit = self.repo.get_commit() - history = commit.get_path_history('foo/bar') + history = commit.get_path_history(b"foo/bar") assert len(history) == 2 def test_get_path_history_with_limit(self): commit = self.repo.get_commit() - history = commit.get_path_history('foo/bar', limit=1) + history = commit.get_path_history(b"foo/bar", limit=1) assert len(history) == 1 def test_get_path_history_first_commit(self): commit = self.repo[0] - history = commit.get_path_history('foo/bar') + history = commit.get_path_history(b"foo/bar") assert len(history) == 1 diff --git a/rhodecode/tests/vcs/test_config.py b/rhodecode/tests/vcs/test_config.py --- a/rhodecode/tests/vcs/test_config.py +++ b/rhodecode/tests/vcs/test_config.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -21,14 +20,17 @@ import pytest def test_get_existing_value(config): - value = config.get('section-a', 'a-1') - assert value == 'value-a-1' + value = config.get("section-a", "a-1") + assert value == "value-a-1" -@pytest.mark.parametrize('section, option', [ - ('section-a', 'does-not-exist'), - ('does-not-exist', 'does-not-exist'), -]) +@pytest.mark.parametrize( + "section, option", + [ + ("section-a", "does-not-exist"), + ("does-not-exist", "does-not-exist"), + ], +) def test_get_unset_value_returns_none(config, section, option): value = config.get(section, option) assert value is None @@ -41,11 +43,11 @@ def test_allows_to_create_a_copy(config) def test_changes_in_the_copy_dont_affect_the_original(config): clone = config.copy() - clone.set('section-a', 'a-2', 'value-a-2') - assert set(config.serialize()) == {('section-a', 'a-1', 'value-a-1')} + clone.set("section-a", "a-2", "value-a-2") + assert set(config.serialize()) == {("section-a", "a-1", "value-a-1")} def test_changes_in_the_original_dont_affect_the_copy(config): clone = config.copy() - config.set('section-a', 'a-2', 'value-a-2') - assert set(clone.serialize()) == {('section-a', 'a-1', 'value-a-1')} + config.set("section-a", "a-2", "value-a-2") + assert set(clone.serialize()) == {("section-a", "a-1", "value-a-1")} diff --git a/rhodecode/tests/vcs/test_diff.py b/rhodecode/tests/vcs/test_diff.py --- a/rhodecode/tests/vcs/test_diff.py +++ b/rhodecode/tests/vcs/test_diff.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,7 +24,6 @@ from rhodecode.tests.vcs.conftest import class TestGetDiffValidation: - def test_raises_on_string_input(self, vcsbackend): repo = vcsbackend.repo with pytest.raises(TypeError): @@ -54,63 +52,58 @@ class TestGetDiffValidation: def test_supports_path1_parameter(self, vcsbackend): repo = vcsbackend.repo commit = repo[1] - repo.get_diff( - repo.EMPTY_COMMIT, commit, - path='vcs/__init__.py', path1='vcs/__init__.py') + repo.get_diff(repo.EMPTY_COMMIT, commit, path="vcs/__init__.py", path1="vcs/__init__.py") @pytest.mark.backends("git", "hg") def test_raises_value_error_if_paths_not_supported(self, vcsbackend): repo = vcsbackend.repo commit = repo[1] with pytest.raises(ValueError): - repo.get_diff( - repo.EMPTY_COMMIT, commit, - path='trunk/example.py', path1='branches/argparse/example.py') + repo.get_diff(repo.EMPTY_COMMIT, commit, path="trunk/example.py", path1="branches/argparse/example.py") @pytest.mark.usefixtures("vcs_repository_support") class TestRepositoryGetDiff(BackendTestMixin): - recreate_repo_per_test = False @classmethod def _get_commits(cls): commits = [ { - 'message': 'Initial commit', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'foobar', content=b'foobar'), - FileNode(b'foobar2', content=b'foobar2'), + "message": "Initial commit", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"foobar", content=b"foobar"), + FileNode(b"foobar2", content=b"foobar2"), ], }, { - 'message': 'Changed foobar, added foobar3', - 'author': 'Jane Doe ', - 'date': datetime.datetime(2010, 1, 1, 21), - 'added': [ - FileNode(b'foobar3', content=b'foobar3'), + "message": "Changed foobar, added foobar3", + "author": "Jane Doe ", + "date": datetime.datetime(2010, 1, 1, 21), + "added": [ + FileNode(b"foobar3", content=b"foobar3"), ], - 'changed': [ - FileNode(b'foobar', b'FOOBAR'), + "changed": [ + FileNode(b"foobar", b"FOOBAR"), ], }, { - 'message': 'Removed foobar, changed foobar3', - 'author': 'Jane Doe ', - 'date': datetime.datetime(2010, 1, 1, 22), - 'changed': [ - FileNode(b'foobar3', content=b'FOOBAR\nFOOBAR\nFOOBAR\n'), + "message": "Removed foobar, changed foobar3", + "author": "Jane Doe ", + "date": datetime.datetime(2010, 1, 1, 22), + "changed": [ + FileNode(b"foobar3", content=b"FOOBAR\nFOOBAR\nFOOBAR\n"), ], - 'removed': [FileNode(b'foobar')], + "removed": [FileNode(b"foobar")], }, { - 'message': 'Whitespace changes', - 'author': 'Jane Doe ', - 'date': datetime.datetime(2010, 1, 1, 23), - 'changed': [ - FileNode(b'foobar3', content=b'FOOBAR \nFOOBAR\nFOOBAR\n'), + "message": "Whitespace changes", + "author": "Jane Doe ", + "date": datetime.datetime(2010, 1, 1, 23), + "changed": [ + FileNode(b"foobar3", content=b"FOOBAR \nFOOBAR\nFOOBAR\n"), ], }, ] @@ -130,28 +123,24 @@ class TestRepositoryGetDiff(BackendTestM assert diff.raw.tobytes() == self.third_commit_diffs[self.repo.alias] def test_ignore_whitespace(self): - diff = self.repo.get_diff( - self.repo[2], self.repo[3], ignore_whitespace=True) - assert b'@@' not in diff.raw.tobytes() + diff = self.repo.get_diff(self.repo[2], self.repo[3], ignore_whitespace=True) + assert b"@@" not in diff.raw.tobytes() def test_only_one_file(self): - diff = self.repo.get_diff( - self.repo.EMPTY_COMMIT, self.repo[0], path='foobar') - assert b'foobar2' not in diff.raw.tobytes() + diff = self.repo.get_diff(self.repo.EMPTY_COMMIT, self.repo[0], path="foobar") + assert b"foobar2" not in diff.raw.tobytes() def test_context_parameter(self): first_commit = self.repo.get_commit(commit_idx=0) - diff = self.repo.get_diff( - self.repo.EMPTY_COMMIT, first_commit, context=2) + diff = self.repo.get_diff(self.repo.EMPTY_COMMIT, first_commit, context=2) assert diff.raw.tobytes() == self.first_commit_diffs[self.repo.alias] def test_context_only_one_file(self): - diff = self.repo.get_diff( - self.repo.EMPTY_COMMIT, self.repo[0], path='foobar', context=2) + diff = self.repo.get_diff(self.repo.EMPTY_COMMIT, self.repo[0], path="foobar", context=2) assert diff.raw.tobytes() == self.first_commit_one_file[self.repo.alias] first_commit_diffs = { - 'git': br"""diff --git a/foobar b/foobar + "git": rb"""diff --git a/foobar b/foobar new file mode 100644 index 0000000..f6ea049 --- /dev/null @@ -168,7 +157,7 @@ index 0000000..e8c9d6b +foobar2 \ No newline at end of file """, - 'hg': br"""diff --git a/foobar b/foobar + "hg": rb"""diff --git a/foobar b/foobar new file mode 100644 --- /dev/null +++ b/foobar @@ -183,7 +172,7 @@ new file mode 100644 +foobar2 \ No newline at end of file """, - 'svn': b"""Index: foobar + "svn": b"""Index: foobar =================================================================== diff --git a/foobar b/foobar new file mode 10644 @@ -205,7 +194,7 @@ new file mode 10644 } second_commit_diffs = { - 'git': br"""diff --git a/foobar b/foobar + "git": rb"""diff --git a/foobar b/foobar index f6ea049..389865b 100644 --- a/foobar +++ b/foobar @@ -223,7 +212,7 @@ index 0000000..c11c37d +foobar3 \ No newline at end of file """, - 'hg': br"""diff --git a/foobar b/foobar + "hg": rb"""diff --git a/foobar b/foobar --- a/foobar +++ b/foobar @@ -1,1 +1,1 @@ @@ -239,7 +228,7 @@ new file mode 100644 +foobar3 \ No newline at end of file """, - 'svn': b"""Index: foobar + "svn": b"""Index: foobar =================================================================== diff --git a/foobar b/foobar --- a/foobar\t(revision 1) @@ -262,7 +251,7 @@ new file mode 10644 } third_commit_diffs = { - 'git': br"""diff --git a/foobar b/foobar + "git": rb"""diff --git a/foobar b/foobar deleted file mode 100644 index 389865b..0000000 --- a/foobar @@ -281,7 +270,7 @@ index c11c37d..f932447 100644 +FOOBAR +FOOBAR """, - 'hg': br"""diff --git a/foobar b/foobar + "hg": rb"""diff --git a/foobar b/foobar deleted file mode 100644 --- a/foobar +++ /dev/null @@ -298,7 +287,7 @@ diff --git a/foobar3 b/foobar3 +FOOBAR +FOOBAR """, - 'svn': b"""Index: foobar + "svn": b"""Index: foobar =================================================================== diff --git a/foobar b/foobar deleted file mode 10644 @@ -322,7 +311,7 @@ diff --git a/foobar3 b/foobar3 } first_commit_one_file = { - 'git': br"""diff --git a/foobar b/foobar + "git": rb"""diff --git a/foobar b/foobar new file mode 100644 index 0000000..f6ea049 --- /dev/null @@ -331,7 +320,7 @@ index 0000000..f6ea049 +foobar \ No newline at end of file """, - 'hg': br"""diff --git a/foobar b/foobar + "hg": rb"""diff --git a/foobar b/foobar new file mode 100644 --- /dev/null +++ b/foobar @@ -339,7 +328,7 @@ new file mode 100644 +foobar \ No newline at end of file """, - 'svn': b"""Index: foobar + "svn": b"""Index: foobar =================================================================== diff --git a/foobar b/foobar new file mode 10644 @@ -353,13 +342,11 @@ new file mode 10644 class TestSvnGetDiff(object): - - @pytest.mark.parametrize('path, path1', [ - ('trunk/example.py', 'tags/v0.2/example.py'), - ('trunk', 'tags/v0.2') - ], ids=['file', 'dir']) + @pytest.mark.parametrize( + "path, path1", [("trunk/example.py", "tags/v0.2/example.py"), ("trunk", "tags/v0.2")], ids=["file", "dir"] + ) def test_diff_to_tagged_version(self, vcsbackend_svn, path, path1): - repo = vcsbackend_svn['svn-simple-layout'] + repo = vcsbackend_svn["svn-simple-layout"] commit1 = repo[-2] commit2 = repo[-1] diff = repo.get_diff(commit1, commit2, path=path, path1=path1) @@ -386,7 +373,7 @@ diff --git a/example.py b/example.py ''' def test_diff_of_moved_directory(self, vcsbackend_svn): - repo = vcsbackend_svn['svn-move-directory'] + repo = vcsbackend_svn["svn-move-directory"] diff = repo.get_diff(repo[0], repo[1]) # TODO: johbo: Think about supporting svn directory nodes # a little bit better, source is here like a file @@ -408,7 +395,6 @@ new file mode 10644 @pytest.mark.usefixtures("vcs_repository_support") class TestGetDiffBinary(BackendTestMixin): - recreate_repo_per_test = False # Note: "Fake" PNG files, has the correct magic as prefix @@ -419,26 +405,29 @@ class TestGetDiffBinary(BackendTestMixin def _get_commits(): commits = [ { - 'message': 'Add binary file image.png', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'image.png', content=TestGetDiffBinary.BINARY), - ]}, + "message": "Add binary file image.png", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"image.png", content=TestGetDiffBinary.BINARY), + ], + }, { - 'message': 'Modify image.png', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 21), - 'changed': [ - FileNode(b'image.png', content=TestGetDiffBinary.BINARY2), - ]}, + "message": "Modify image.png", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 21), + "changed": [ + FileNode(b"image.png", content=TestGetDiffBinary.BINARY2), + ], + }, { - 'message': 'Remove image.png', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 21), - 'removed': [ - FileNode(b'image.png'), - ]}, + "message": "Remove image.png", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 21), + "removed": [ + FileNode(b"image.png"), + ], + }, ] return commits @@ -446,7 +435,7 @@ class TestGetDiffBinary(BackendTestMixin diff = self.repo.get_diff(self.repo.EMPTY_COMMIT, self.repo[0]) expected = { - 'git': b"""diff --git a/image.png b/image.png + "git": b"""diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..28380fd4a25c58be1b68b523ba2a314f4459ee9c GIT binary patch @@ -457,7 +446,7 @@ literal 0 Hc$@. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import datetime + +import pytest + +from rhodecode.lib.str_utils import safe_bytes +from rhodecode.lib.vcs.nodes import FileNode +from rhodecode.lib.vcs_common import NodeKind +from rhodecode.tests.vcs.conftest import BackendTestMixin + + +class TestFileNodesListingAndCaches: + def test_filenode_get_root_node(self, vcsbackend): + repo = vcsbackend.repo + commit = repo.get_commit() + + # check if we start with empty nodes cache + assert commit.nodes == {} + assert commit._path_mode_cache == {} + assert commit._path_type_cache == {} + + top_dir = commit.get_node(b"") + + assert top_dir.is_dir() + + assert list(commit.nodes.keys()) == [b""] + assert list(commit._path_type_cache.keys()) == [b""] + assert commit._path_mode_cache == {} + + def test_filenode_get_file_node(self, vcsbackend): + repo = vcsbackend.repo + commit = repo.get_commit() + + # check if we start with empty nodes cache + assert commit.nodes == {} + assert commit._path_mode_cache == {} + assert commit._path_type_cache == {} + + file_node = commit.get_node(b"README.rst") + + assert file_node.is_file() + assert file_node.last_commit + + assert list(commit.nodes.keys()) == [b"README.rst"] + if repo.alias == "hg": + assert commit._path_type_cache == {b"README.rst": NodeKind.FILE} + assert commit._path_mode_cache == {b"README.rst": 33188} + + if repo.alias == "git": + assert commit._path_type_cache == { + b"README.rst": ["8f111ab5152b9fd34f52b0fb288e1216b5d2f55e", NodeKind.FILE] + } + assert commit._path_mode_cache == {b"README.rst": 33188} + + if repo.alias == "svn": + assert commit._path_type_cache == {b"README.rst": NodeKind.FILE} + assert commit._path_mode_cache == {b"README.rst": 33188} + + def test_filenode_get_nodes_from_top_dir(self, vcsbackend): + repo = vcsbackend.repo + commit = repo.get_commit() + + # check if we start with empty nodes cache + assert commit.nodes == {} + assert commit._path_mode_cache == {} + assert commit._path_type_cache == {} + + node_list = commit.get_nodes(b"") + + for node in node_list: + assert node + + if repo.alias == "svn": + assert list(commit.nodes.keys()) == [ + b"README", + b".hgignore", + b"MANIFEST.in", + b".travis.yml", + b"vcs", + b"tox.ini", + b"setup.py", + b"setup.cfg", + b"docs", + b"fakefile", + b"examples", + b"test_and_report.sh", + b"bin", + b".gitignore", + b".hgtags", + b"README.rst", + ] + assert commit._path_type_cache == { + b"": NodeKind.DIR, + b"README": 2, + b".hgignore": 2, + b"MANIFEST.in": 2, + b".travis.yml": 2, + b"vcs": 1, + b"tox.ini": 2, + b"setup.py": 2, + b"setup.cfg": 2, + b"docs": 1, + b"fakefile": 2, + b"examples": 1, + b"test_and_report.sh": 2, + b"bin": 1, + b".gitignore": 2, + b".hgtags": 2, + b"README.rst": 2, + } + assert commit._path_mode_cache == {} + + if repo.alias == "hg": + assert list(commit.nodes.keys()) == [ + b".gitignore", + b".hgignore", + b".hgtags", + b".travis.yml", + b"MANIFEST.in", + b"README", + b"README.rst", + b"docs", + b"run_test_and_report.sh", + b"setup.cfg", + b"setup.py", + b"test_and_report.sh", + b"tox.ini", + b"vcs", + ] + + assert commit._path_type_cache == { + b"": NodeKind.DIR, + b".gitignore": 2, + b".hgignore": 2, + b".hgtags": 2, + b".travis.yml": 2, + b"MANIFEST.in": 2, + b"README": 2, + b"README.rst": 2, + b"docs": 1, + b"run_test_and_report.sh": 2, + b"setup.cfg": 2, + b"setup.py": 2, + b"test_and_report.sh": 2, + b"tox.ini": 2, + b"vcs": 1, + } + assert commit._path_mode_cache == { + b".gitignore": 33188, + b".hgignore": 33188, + b".hgtags": 33188, + b".travis.yml": 33188, + b"MANIFEST.in": 33188, + b"README": 40960, + b"README.rst": 33188, + b"docs": 33188, + b"run_test_and_report.sh": 33261, + b"setup.cfg": 33188, + b"setup.py": 33188, + b"test_and_report.sh": 33261, + b"tox.ini": 33188, + b"vcs": 33188, + } + + if repo.alias == "git": + assert list(commit.nodes.keys()) == [ + b".gitignore", + b".hgignore", + b".hgtags", + b"MANIFEST.in", + b"README", + b"README.rst", + b"docs", + b"run_test_and_report.sh", + b"setup.cfg", + b"setup.py", + b"test_and_report.sh", + b"tox.ini", + b"vcs", + ] + assert commit._path_type_cache == { + b"": ["2dab7f7377e36948b5633ae0877310380fdf64ba", NodeKind.DIR], + b".gitignore": ["7f95b0917e31b0cce1ad96a033b1a814c420cde3", 2], + b".hgignore": ["9bdee5ed67ceed7c4b5589ba26dada65df151aed", 2], + b".hgtags": ["63aa2e892deebdfdef1845fe52e2418ade03557b", 2], + b"MANIFEST.in": ["6d65aca5321c26589ae197b81511445cfb353c95", 2], + b"README": ["92cacd285355271487b7e379dba6ca60f9a554a4", 2], + b"README.rst": ["8f111ab5152b9fd34f52b0fb288e1216b5d2f55e", 2], + b"docs": ["eaa8cef76ec4e1baf06f515ab03bfc01719a913d", 1], + b"run_test_and_report.sh": ["e17011bdddc8812fda96e891a1087c73054f1f25", 2], + b"setup.cfg": ["2e76b65bc9106ed466d71fb2cfe710352eadfb49", 2], + b"setup.py": ["ccda2bd076eca961059deba6e39fb591d014052a", 2], + b"test_and_report.sh": ["0b14056dd7dc759a9719adef3d7bb529141a740e", 2], + b"tox.ini": ["c1735231e7af32f089b95455162db092824715a4", 2], + b"vcs": ["5868c69a8d74ccf3fdc655af8f3f185bc0ff2ef6", 1], + } + assert commit._path_mode_cache == { + b".gitignore": 33188, + b".hgignore": 33188, + b".hgtags": 33188, + b"MANIFEST.in": 33188, + b"README": 40960, + b"README.rst": 33188, + b"docs": 16384, + b"run_test_and_report.sh": 33261, + b"setup.cfg": 33188, + b"setup.py": 33188, + b"test_and_report.sh": 33261, + b"tox.ini": 33188, + b"vcs": 16384, + } + + def test_filenode_get_nodes_from_docs_dir(self, vcsbackend): + repo = vcsbackend.repo + commit = repo.get_commit() + + # check if we start with empty nodes cache + assert commit.nodes == {} + assert commit._path_mode_cache == {} + assert commit._path_type_cache == {} + + node_list = commit.get_nodes(b"docs") + + for node in node_list: + assert node diff --git a/rhodecode/tests/vcs/test_filenodes_unicode_path.py b/rhodecode/tests/vcs/test_filenodes_unicode_path.py --- a/rhodecode/tests/vcs/test_filenodes_unicode_path.py +++ b/rhodecode/tests/vcs/test_filenodes_unicode_path.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,22 +27,21 @@ from rhodecode.tests.vcs.conftest import @pytest.mark.usefixtures("vcs_repository_support") class TestFileNodeUnicodePath(BackendTestMixin): - - fname = safe_bytes('ąśðąęłąć.txt') + fname = safe_bytes("ąśðąęłąć.txt") ufname = fname @classmethod def _get_commits(cls): nodes = [ - FileNode(cls.fname, content=b'Foobar'), + FileNode(cls.fname, content=b"Foobar"), ] commits = [ { - 'message': 'Initial commit', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': nodes, + "message": "Initial commit", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": nodes, }, ] return commits diff --git a/rhodecode/tests/vcs/test_getitem.py b/rhodecode/tests/vcs/test_getitem.py --- a/rhodecode/tests/vcs/test_getitem.py +++ b/rhodecode/tests/vcs/test_getitem.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -28,28 +27,30 @@ from rhodecode.tests.vcs.conftest import @pytest.mark.usefixtures("vcs_repository_support") class TestGetitem(BackendTestMixin): - @classmethod def _get_commits(cls): start_date = datetime.datetime(2010, 1, 1, 20) for x in range(5): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'file_%d.txt' % x, content='Foobar %d' % x), + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [ + FileNode(b"file_%d.txt" % x, content="Foobar %d" % x), ], } def test_last_item_is_tip(self): assert self.repo[-1] == self.repo.get_commit() - @pytest.mark.parametrize("offset, message", [ - (-1, 'Commit 4'), - (-2, 'Commit 3'), - (-5, 'Commit 0'), - ]) + @pytest.mark.parametrize( + "offset, message", + [ + (-1, "Commit 4"), + (-2, "Commit 3"), + (-5, "Commit 0"), + ], + ) def test_negative_offset_fetches_correct_commit(self, offset, message): assert self.repo[offset].message == message diff --git a/rhodecode/tests/vcs/test_getslice.py b/rhodecode/tests/vcs/test_getslice.py --- a/rhodecode/tests/vcs/test_getslice.py +++ b/rhodecode/tests/vcs/test_getslice.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,17 +24,16 @@ from rhodecode.tests.vcs.conftest import @pytest.mark.usefixtures("vcs_repository_support") class TestGetslice(BackendTestMixin): - @classmethod def _get_commits(cls): start_date = datetime.datetime(2010, 1, 1, 20) for x in range(5): yield { - 'message': 'Commit %d' % x, - 'author': 'Joe Doe ', - 'date': start_date + datetime.timedelta(hours=12 * x), - 'added': [ - FileNode(b'file_%d.txt' % x, content='Foobar %d' % x), + "message": "Commit %d" % x, + "author": "Joe Doe ", + "date": start_date + datetime.timedelta(hours=12 * x), + "added": [ + FileNode(b"file_%d.txt" % x, content="Foobar %d" % x), ], } @@ -43,34 +41,24 @@ class TestGetslice(BackendTestMixin): assert list(self.repo[-1:])[0] == self.repo.get_commit() def test__getslice__respects_start_index(self): - assert list(self.repo[2:]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[2:]] + assert list(self.repo[2:]) == [self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[2:]] def test__getslice__respects_negative_start_index(self): - assert list(self.repo[-2:]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[-2:]] + assert list(self.repo[-2:]) == [self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[-2:]] def test__getslice__respects_end_index(self): - assert list(self.repo[:2]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[:2]] + assert list(self.repo[:2]) == [self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[:2]] def test__getslice__respects_negative_end_index(self): - assert list(self.repo[:-2]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[:-2]] + assert list(self.repo[:-2]) == [self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[:-2]] def test__getslice__start_grater_than_end(self): assert list(self.repo[10:0]) == [] def test__getslice__negative_iteration(self): - assert list(self.repo[::-1]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[::-1]] + assert list(self.repo[::-1]) == [self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[::-1]] def test__getslice__iterate_even(self): - assert list(self.repo[0:10:2]) == \ - [self.repo.get_commit(commit_id) - for commit_id in self.repo.commit_ids[0:10:2]] + assert list(self.repo[0:10:2]) == [ + self.repo.get_commit(commit_id) for commit_id in self.repo.commit_ids[0:10:2] + ] diff --git a/rhodecode/tests/vcs/test_git.py b/rhodecode/tests/vcs/test_git.py --- a/rhodecode/tests/vcs/test_git.py +++ b/rhodecode/tests/vcs/test_git.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,12 +26,9 @@ import pytest from rhodecode.lib.utils import make_db_config from rhodecode.lib.vcs.backends.base import Reference -from rhodecode.lib.vcs.backends.git import ( - GitRepository, GitCommit, discover_git_version) -from rhodecode.lib.vcs.exceptions import ( - RepositoryError, VCSError, NodeDoesNotExistError) -from rhodecode.lib.vcs.nodes import ( - NodeKind, FileNode, DirNode, NodeState, SubModuleNode) +from rhodecode.lib.vcs.backends.git import GitRepository, GitCommit, discover_git_version +from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError +from rhodecode.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState, SubModuleNode, RootNode from rhodecode.tests import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir from rhodecode.tests.vcs.conftest import BackendTestMixin @@ -40,7 +36,7 @@ from rhodecode.tests.vcs.conftest import pytestmark = pytest.mark.backends("git") -DIFF_FROM_REMOTE = br"""diff --git a/foobar b/foobar +DIFF_FROM_REMOTE = rb"""diff --git a/foobar b/foobar new file mode 100644 index 0000000..f6ea049 --- /dev/null @@ -64,7 +60,6 @@ def callable_get_diff(*args, **kwargs): class TestGitRepository(object): - @pytest.fixture(autouse=True) def prepare(self, request, baseapp): self.repo = GitRepository(TEST_GIT_REPO, bare=True) @@ -74,9 +69,8 @@ class TestGitRepository(object): """ Return a non bare clone of the base repo. """ - clone_path = str(tmpdir.join('clone-repo')) - repo_clone = GitRepository( - clone_path, create=True, src_url=self.repo.path, bare=False) + clone_path = str(tmpdir.join("clone-repo")) + repo_clone = GitRepository(clone_path, create=True, src_url=self.repo.path, bare=False) return repo_clone @@ -84,20 +78,18 @@ class TestGitRepository(object): """ Return a non bare empty repo. """ - clone_path = str(tmpdir.join('empty-repo')) + clone_path = str(tmpdir.join("empty-repo")) return GitRepository(clone_path, create=True, bare=bare) def test_wrong_repo_path(self): - wrong_repo_path = '/tmp/errorrepo_git' + wrong_repo_path = "/tmp/errorrepo_git" with pytest.raises(RepositoryError): GitRepository(wrong_repo_path) def test_repo_clone(self, tmp_path_factory): repo = GitRepository(TEST_GIT_REPO) - clone_path = '{}_{}'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE) - repo_clone = GitRepository( - clone_path, - src_url=TEST_GIT_REPO, create=True, do_workspace_checkout=True) + clone_path = f"{tmp_path_factory.mktemp('_')}_{TEST_GIT_REPO_CLONE}" + repo_clone = GitRepository(clone_path, src_url=TEST_GIT_REPO, create=True, do_workspace_checkout=True) assert len(repo.commit_ids) == len(repo_clone.commit_ids) # Checking hashes of commits should be enough @@ -107,48 +99,42 @@ class TestGitRepository(object): def test_repo_clone_without_create(self): with pytest.raises(RepositoryError): - GitRepository( - TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO) + GitRepository(TEST_GIT_REPO_CLONE + "_wo_create", src_url=TEST_GIT_REPO) def test_repo_clone_with_update(self, tmp_path_factory): repo = GitRepository(TEST_GIT_REPO) - clone_path = '{}_{}_update'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE) + clone_path = "{}_{}_update".format(tmp_path_factory.mktemp("_"), TEST_GIT_REPO_CLONE) - repo_clone = GitRepository( - clone_path, - create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=True) + repo_clone = GitRepository(clone_path, create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=True) assert len(repo.commit_ids) == len(repo_clone.commit_ids) # check if current workdir was updated - fpath = os.path.join(clone_path, 'MANIFEST.in') + fpath = os.path.join(clone_path, "MANIFEST.in") assert os.path.isfile(fpath) def test_repo_clone_without_update(self, tmp_path_factory): repo = GitRepository(TEST_GIT_REPO) - clone_path = '{}_{}_without_update'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE) - repo_clone = GitRepository( - clone_path, - create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=False) + clone_path = "{}_{}_without_update".format(tmp_path_factory.mktemp("_"), TEST_GIT_REPO_CLONE) + repo_clone = GitRepository(clone_path, create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=False) assert len(repo.commit_ids) == len(repo_clone.commit_ids) # check if current workdir was *NOT* updated - fpath = os.path.join(clone_path, 'MANIFEST.in') + fpath = os.path.join(clone_path, "MANIFEST.in") # Make sure it's not bare repo assert not repo_clone.bare assert not os.path.isfile(fpath) def test_repo_clone_into_bare_repo(self, tmp_path_factory): repo = GitRepository(TEST_GIT_REPO) - clone_path = '{}_{}_bare.git'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE) - repo_clone = GitRepository( - clone_path, create=True, src_url=repo.path, bare=True) + clone_path = "{}_{}_bare.git".format(tmp_path_factory.mktemp("_"), TEST_GIT_REPO_CLONE) + repo_clone = GitRepository(clone_path, create=True, src_url=repo.path, bare=True) assert repo_clone.bare def test_create_repo_is_not_bare_by_default(self): - repo = GitRepository(get_new_dir('not-bare-by-default'), create=True) + repo = GitRepository(get_new_dir("not-bare-by-default"), create=True) assert not repo.bare def test_create_bare_repo(self): - repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True) + repo = GitRepository(get_new_dir("bare-repo"), create=True, bare=True) assert repo.bare def test_update_server_info(self): @@ -167,37 +153,38 @@ class TestGitRepository(object): def test_commit_ids(self): # there are 112 commits (by now) # so we can assume they would be available from now on - subset = {'c1214f7e79e02fc37156ff215cd71275450cffc3', - '38b5fe81f109cb111f549bfe9bb6b267e10bc557', - 'fa6600f6848800641328adbf7811fd2372c02ab2', - '102607b09cdd60e2793929c4f90478be29f85a17', - '49d3fd156b6f7db46313fac355dca1a0b94a0017', - '2d1028c054665b962fa3d307adfc923ddd528038', - 'd7e0d30fbcae12c90680eb095a4f5f02505ce501', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b', - 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f', - '8430a588b43b5d6da365400117c89400326e7992', - 'd955cd312c17b02143c04fa1099a352b04368118', - 'f67b87e5c629c2ee0ba58f85197e423ff28d735b', - 'add63e382e4aabc9e1afdc4bdc24506c269b7618', - 'f298fe1189f1b69779a4423f40b48edf92a703fc', - 'bd9b619eb41994cac43d67cf4ccc8399c1125808', - '6e125e7c890379446e98980d8ed60fba87d0f6d1', - 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd', - '0b05e4ed56c802098dfc813cbe779b2f49e92500', - '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e', - '45223f8f114c64bf4d6f853e3c35a369a6305520', - 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e', - 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68', - '27d48942240f5b91dfda77accd2caac94708cc7d', - '622f0eb0bafd619d2560c26f80f09e3b0b0d78af', - 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'} + subset = { + "c1214f7e79e02fc37156ff215cd71275450cffc3", + "38b5fe81f109cb111f549bfe9bb6b267e10bc557", + "fa6600f6848800641328adbf7811fd2372c02ab2", + "102607b09cdd60e2793929c4f90478be29f85a17", + "49d3fd156b6f7db46313fac355dca1a0b94a0017", + "2d1028c054665b962fa3d307adfc923ddd528038", + "d7e0d30fbcae12c90680eb095a4f5f02505ce501", + "ff7ca51e58c505fec0dd2491de52c622bb7a806b", + "dd80b0f6cf5052f17cc738c2951c4f2070200d7f", + "8430a588b43b5d6da365400117c89400326e7992", + "d955cd312c17b02143c04fa1099a352b04368118", + "f67b87e5c629c2ee0ba58f85197e423ff28d735b", + "add63e382e4aabc9e1afdc4bdc24506c269b7618", + "f298fe1189f1b69779a4423f40b48edf92a703fc", + "bd9b619eb41994cac43d67cf4ccc8399c1125808", + "6e125e7c890379446e98980d8ed60fba87d0f6d1", + "d4a54db9f745dfeba6933bf5b1e79e15d0af20bd", + "0b05e4ed56c802098dfc813cbe779b2f49e92500", + "191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", + "45223f8f114c64bf4d6f853e3c35a369a6305520", + "ca1eb7957a54bce53b12d1a51b13452f95bc7c7e", + "f5ea29fc42ef67a2a5a7aecff10e1566699acd68", + "27d48942240f5b91dfda77accd2caac94708cc7d", + "622f0eb0bafd619d2560c26f80f09e3b0b0d78af", + "e686b958768ee96af8029fe19c6050b1a8dd3b2b", + } assert subset.issubset(set(self.repo.commit_ids)) def test_slicing(self): # 4 1 5 10 95 - for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5), - (10, 20, 10), (5, 100, 95)]: + for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5), (10, 20, 10), (5, 100, 95)]: commit_ids = list(self.repo[sfrom:sto]) assert len(commit_ids) == size assert commit_ids[0] == self.repo.get_commit(commit_idx=sfrom) @@ -214,8 +201,8 @@ class TestGitRepository(object): def test_tags(self): # TODO: Need more tests here - assert 'v0.1.1' in self.repo.tags - assert 'v0.1.2' in self.repo.tags + assert "v0.1.1" in self.repo.tags + assert "v0.1.2" in self.repo.tags for __, commit_id in self.repo.tags.items(): assert isinstance(self.repo.get_commit(commit_id), GitCommit) @@ -229,37 +216,49 @@ class TestGitRepository(object): init_commit = self.repo.get_commit(commit_id) init_author = init_commit.author - assert init_commit.message == 'initial import\n' - assert init_author == 'Marcin Kuzminski ' + assert init_commit.message == "initial import\n" + assert init_author == "Marcin Kuzminski " assert init_author == init_commit.committer - for path in ('vcs/__init__.py', - 'vcs/backends/BaseRepository.py', - 'vcs/backends/__init__.py'): + assert sorted(init_commit.added_paths) == sorted( + [ + b"vcs/__init__.py", + b"vcs/backends/BaseRepository.py", + b"vcs/backends/__init__.py", + ] + ) + assert sorted(init_commit.affected_files) == sorted( + [ + b"vcs/__init__.py", + b"vcs/backends/BaseRepository.py", + b"vcs/backends/__init__.py", + ] + ) + + for path in (b"vcs/__init__.py", b"vcs/backends/BaseRepository.py", b"vcs/backends/__init__.py"): assert isinstance(init_commit.get_node(path), FileNode) - for path in ('', 'vcs', 'vcs/backends'): + for path in (b"", b"vcs", b"vcs/backends"): assert isinstance(init_commit.get_node(path), DirNode) with pytest.raises(NodeDoesNotExistError): - init_commit.get_node(path='foobar') + init_commit.get_node(path=b"foobar") - node = init_commit.get_node('vcs/') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs/") + assert hasattr(node, "kind") assert node.kind == NodeKind.DIR - node = init_commit.get_node('vcs') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs") + assert hasattr(node, "kind") assert node.kind == NodeKind.DIR - node = init_commit.get_node('vcs/__init__.py') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs/__init__.py") + assert hasattr(node, "kind") assert node.kind == NodeKind.FILE def test_not_existing_commit(self): with pytest.raises(RepositoryError): - self.repo.get_commit('f' * 40) + self.repo.get_commit("f" * 40) def test_commit10(self): - commit10 = self.repo.get_commit(self.repo.commit_ids[9]) README = """=== VCS @@ -273,7 +272,7 @@ Introduction TODO: To be written... """ - node = commit10.get_node('README.rst') + node = commit10.get_node(b"README.rst") assert node.kind == NodeKind.FILE assert node.str_content == README @@ -283,39 +282,39 @@ TODO: To be written... def test_checkout_with_create(self, tmpdir): repo_clone = self.get_clone_repo(tmpdir) - new_branch = 'new_branch' - assert repo_clone._current_branch() == 'master' - assert set(repo_clone.branches) == {'master'} + new_branch = "new_branch" + assert repo_clone._current_branch() == "master" + assert set(repo_clone.branches) == {"master"} repo_clone._checkout(new_branch, create=True) # Branches is a lazy property so we need to recrete the Repo object. repo_clone = GitRepository(repo_clone.path) - assert set(repo_clone.branches) == {'master', new_branch} + assert set(repo_clone.branches) == {"master", new_branch} assert repo_clone._current_branch() == new_branch def test_checkout(self, tmpdir): repo_clone = self.get_clone_repo(tmpdir) - repo_clone._checkout('new_branch', create=True) - repo_clone._checkout('master') + repo_clone._checkout("new_branch", create=True) + repo_clone._checkout("master") - assert repo_clone._current_branch() == 'master' + assert repo_clone._current_branch() == "master" def test_checkout_same_branch(self, tmpdir): repo_clone = self.get_clone_repo(tmpdir) - repo_clone._checkout('master') - assert repo_clone._current_branch() == 'master' + repo_clone._checkout("master") + assert repo_clone._current_branch() == "master" def test_checkout_branch_already_exists(self, tmpdir): repo_clone = self.get_clone_repo(tmpdir) with pytest.raises(RepositoryError): - repo_clone._checkout('master', create=True) + repo_clone._checkout("master", create=True) def test_checkout_bare_repo(self): with pytest.raises(RepositoryError): - self.repo._checkout('master') + self.repo._checkout("master") def test_current_branch_bare_repo(self): with pytest.raises(RepositoryError): @@ -326,8 +325,8 @@ TODO: To be written... assert repo._current_branch() is None def test_local_clone(self, tmp_path_factory): - clone_path = str(tmp_path_factory.mktemp('test-local-clone')) - self.repo._local_clone(clone_path, 'master') + clone_path = str(tmp_path_factory.mktemp("test-local-clone")) + self.repo._local_clone(clone_path, "master") repo_clone = GitRepository(clone_path) assert self.repo.commit_ids == repo_clone.commit_ids @@ -338,23 +337,23 @@ TODO: To be written... # Create a new branch in source repo new_branch_commit = source_repo.commit_ids[-3] source_repo._checkout(new_branch_commit) - source_repo._checkout('new_branch', create=True) + source_repo._checkout("new_branch", create=True) - clone_path = str(tmpdir.join('git-clone-path-1')) - source_repo._local_clone(clone_path, 'new_branch') + clone_path = str(tmpdir.join("git-clone-path-1")) + source_repo._local_clone(clone_path, "new_branch") repo_clone = GitRepository(clone_path) - assert source_repo.commit_ids[:-3 + 1] == repo_clone.commit_ids + assert source_repo.commit_ids[: -3 + 1] == repo_clone.commit_ids - clone_path = str(tmpdir.join('git-clone-path-2')) - source_repo._local_clone(clone_path, 'master') + clone_path = str(tmpdir.join("git-clone-path-2")) + source_repo._local_clone(clone_path, "master") repo_clone = GitRepository(clone_path) assert source_repo.commit_ids == repo_clone.commit_ids def test_local_clone_fails_if_target_exists(self): with pytest.raises(RepositoryError): - self.repo._local_clone(self.repo.path, 'master') + self.repo._local_clone(self.repo.path, "master") def test_local_fetch(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) @@ -364,30 +363,30 @@ TODO: To be written... master_commit = source_repo.commit_ids[-1] new_branch_commit = source_repo.commit_ids[-3] source_repo._checkout(new_branch_commit) - source_repo._checkout('new_branch', create=True) + source_repo._checkout("new_branch", create=True) - target_repo._local_fetch(source_repo.path, 'new_branch') + target_repo._local_fetch(source_repo.path, "new_branch") assert target_repo._last_fetch_heads() == [new_branch_commit] - target_repo._local_fetch(source_repo.path, 'master') + target_repo._local_fetch(source_repo.path, "master") assert target_repo._last_fetch_heads() == [master_commit] def test_local_fetch_from_bare_repo(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) - target_repo._local_fetch(self.repo.path, 'master') + target_repo._local_fetch(self.repo.path, "master") master_commit = self.repo.commit_ids[-1] assert target_repo._last_fetch_heads() == [master_commit] def test_local_fetch_from_same_repo(self): with pytest.raises(ValueError): - self.repo._local_fetch(self.repo.path, 'master') + self.repo._local_fetch(self.repo.path, "master") def test_local_fetch_branch_does_not_exist(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) with pytest.raises(RepositoryError): - target_repo._local_fetch(self.repo.path, 'new_branch') + target_repo._local_fetch(self.repo.path, "new_branch") def test_local_pull(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) @@ -397,19 +396,19 @@ TODO: To be written... master_commit = source_repo.commit_ids[-1] new_branch_commit = source_repo.commit_ids[-3] source_repo._checkout(new_branch_commit) - source_repo._checkout('new_branch', create=True) + source_repo._checkout("new_branch", create=True) - target_repo._local_pull(source_repo.path, 'new_branch') + target_repo._local_pull(source_repo.path, "new_branch") target_repo = GitRepository(target_repo.path) assert target_repo.head == new_branch_commit - target_repo._local_pull(source_repo.path, 'master') + target_repo._local_pull(source_repo.path, "master") target_repo = GitRepository(target_repo.path) assert target_repo.head == master_commit def test_local_pull_in_bare_repo(self): with pytest.raises(RepositoryError): - self.repo._local_pull(self.repo.path, 'master') + self.repo._local_pull(self.repo.path, "master") def test_local_merge(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) @@ -419,159 +418,144 @@ TODO: To be written... master_commit = source_repo.commit_ids[-1] new_branch_commit = source_repo.commit_ids[-3] source_repo._checkout(new_branch_commit) - source_repo._checkout('new_branch', create=True) + source_repo._checkout("new_branch", create=True) # This is required as one cannot do a -ff-only merge in an empty repo. - target_repo._local_pull(source_repo.path, 'new_branch') + target_repo._local_pull(source_repo.path, "new_branch") - target_repo._local_fetch(source_repo.path, 'master') - merge_message = 'Merge message\n\nDescription:...' - user_name = 'Albert Einstein' - user_email = 'albert@einstein.com' - target_repo._local_merge(merge_message, user_name, user_email, - target_repo._last_fetch_heads()) + target_repo._local_fetch(source_repo.path, "master") + merge_message = "Merge message\n\nDescription:..." + user_name = "Albert Einstein" + user_email = "albert@einstein.com" + target_repo._local_merge(merge_message, user_name, user_email, target_repo._last_fetch_heads()) target_repo = GitRepository(target_repo.path) assert target_repo.commit_ids[-2] == master_commit last_commit = target_repo.get_commit(target_repo.head) assert last_commit.message.strip() == merge_message - assert last_commit.author == '%s <%s>' % (user_name, user_email) + assert last_commit.author == "%s <%s>" % (user_name, user_email) - assert not os.path.exists( - os.path.join(target_repo.path, '.git', 'MERGE_HEAD')) + assert not os.path.exists(os.path.join(target_repo.path, ".git", "MERGE_HEAD")) def test_local_merge_raises_exception_on_conflict(self, vcsbackend_git): target_repo = vcsbackend_git.create_repo(number_of_commits=1) - vcsbackend_git.ensure_file(b'README', b'I will conflict with you!!!') + vcsbackend_git.ensure_file(b"README", b"I will conflict with you!!!") - target_repo._local_fetch(self.repo.path, 'master') + target_repo._local_fetch(self.repo.path, "master") with pytest.raises(RepositoryError): - target_repo._local_merge( - 'merge_message', 'user name', 'user@name.com', - target_repo._last_fetch_heads()) + target_repo._local_merge("merge_message", "user name", "user@name.com", target_repo._last_fetch_heads()) # Check we are not left in an intermediate merge state - assert not os.path.exists( - os.path.join(target_repo.path, '.git', 'MERGE_HEAD')) + assert not os.path.exists(os.path.join(target_repo.path, ".git", "MERGE_HEAD")) def test_local_merge_into_empty_repo(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) # This is required as one cannot do a -ff-only merge in an empty repo. - target_repo._local_fetch(self.repo.path, 'master') + target_repo._local_fetch(self.repo.path, "master") with pytest.raises(RepositoryError): - target_repo._local_merge( - 'merge_message', 'user name', 'user@name.com', - target_repo._last_fetch_heads()) + target_repo._local_merge("merge_message", "user name", "user@name.com", target_repo._last_fetch_heads()) def test_local_merge_in_bare_repo(self): with pytest.raises(RepositoryError): - self.repo._local_merge( - 'merge_message', 'user name', 'user@name.com', None) + self.repo._local_merge("merge_message", "user name", "user@name.com", None) def test_local_push_non_bare(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) - pushed_branch = 'pushed_branch' - self.repo._local_push('master', target_repo.path, pushed_branch) + pushed_branch = "pushed_branch" + self.repo._local_push("master", target_repo.path, pushed_branch) # Fix the HEAD of the target repo, or otherwise GitRepository won't # report any branches. - with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f: - f.write('ref: refs/heads/%s' % pushed_branch) + with open(os.path.join(target_repo.path, ".git", "HEAD"), "w") as f: + f.write("ref: refs/heads/%s" % pushed_branch) target_repo = GitRepository(target_repo.path) - assert (target_repo.branches[pushed_branch] == - self.repo.branches['master']) + assert target_repo.branches[pushed_branch] == self.repo.branches["master"] def test_local_push_bare(self, tmpdir): target_repo = self.get_empty_repo(tmpdir, bare=True) - pushed_branch = 'pushed_branch' - self.repo._local_push('master', target_repo.path, pushed_branch) + pushed_branch = "pushed_branch" + self.repo._local_push("master", target_repo.path, pushed_branch) # Fix the HEAD of the target repo, or otherwise GitRepository won't # report any branches. - with open(os.path.join(target_repo.path, 'HEAD'), 'w') as f: - f.write('ref: refs/heads/%s' % pushed_branch) + with open(os.path.join(target_repo.path, "HEAD"), "w") as f: + f.write("ref: refs/heads/%s" % pushed_branch) target_repo = GitRepository(target_repo.path) - assert (target_repo.branches[pushed_branch] == - self.repo.branches['master']) + assert target_repo.branches[pushed_branch] == self.repo.branches["master"] def test_local_push_non_bare_target_branch_is_checked_out(self, tmpdir): target_repo = self.get_clone_repo(tmpdir) - pushed_branch = 'pushed_branch' + pushed_branch = "pushed_branch" # Create a new branch in source repo new_branch_commit = target_repo.commit_ids[-3] target_repo._checkout(new_branch_commit) target_repo._checkout(pushed_branch, create=True) - self.repo._local_push('master', target_repo.path, pushed_branch) + self.repo._local_push("master", target_repo.path, pushed_branch) target_repo = GitRepository(target_repo.path) - assert (target_repo.branches[pushed_branch] == - self.repo.branches['master']) + assert target_repo.branches[pushed_branch] == self.repo.branches["master"] def test_local_push_raises_exception_on_conflict(self, vcsbackend_git): target_repo = vcsbackend_git.create_repo(number_of_commits=1) with pytest.raises(RepositoryError): - self.repo._local_push('master', target_repo.path, 'master') + self.repo._local_push("master", target_repo.path, "master") def test_hooks_can_be_enabled_via_env_variable_for_local_push(self, tmpdir): target_repo = self.get_empty_repo(tmpdir, bare=True) - with mock.patch.object(self.repo, 'run_git_command') as run_mock: - self.repo._local_push( - 'master', target_repo.path, 'master', enable_hooks=True) - env = run_mock.call_args[1]['extra_env'] - assert 'RC_SKIP_HOOKS' not in env + with mock.patch.object(self.repo, "run_git_command") as run_mock: + self.repo._local_push("master", target_repo.path, "master", enable_hooks=True) + env = run_mock.call_args[1]["extra_env"] + assert "RC_SKIP_HOOKS" not in env def _add_failing_hook(self, repo_path, hook_name, bare=False): - path_components = ( - ['hooks', hook_name] if bare else ['.git', 'hooks', hook_name]) + path_components = ["hooks", hook_name] if bare else [".git", "hooks", hook_name] hook_path = os.path.join(repo_path, *path_components) - with open(hook_path, 'w') as f: + with open(hook_path, "w") as f: script_lines = [ - '#!%s' % sys.executable, - 'import os', - 'import sys', + "#!%s" % sys.executable, + "import os", + "import sys", 'if os.environ.get("RC_SKIP_HOOKS"):', - ' sys.exit(0)', - 'sys.exit(1)', + " sys.exit(0)", + "sys.exit(1)", ] - f.write('\n'.join(script_lines)) + f.write("\n".join(script_lines)) os.chmod(hook_path, 0o755) def test_local_push_does_not_execute_hook(self, tmpdir): target_repo = self.get_empty_repo(tmpdir) - pushed_branch = 'pushed_branch' - self._add_failing_hook(target_repo.path, 'pre-receive') - self.repo._local_push('master', target_repo.path, pushed_branch) + pushed_branch = "pushed_branch" + self._add_failing_hook(target_repo.path, "pre-receive") + self.repo._local_push("master", target_repo.path, pushed_branch) # Fix the HEAD of the target repo, or otherwise GitRepository won't # report any branches. - with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f: - f.write('ref: refs/heads/%s' % pushed_branch) + with open(os.path.join(target_repo.path, ".git", "HEAD"), "w") as f: + f.write("ref: refs/heads/%s" % pushed_branch) target_repo = GitRepository(target_repo.path) - assert (target_repo.branches[pushed_branch] == - self.repo.branches['master']) + assert target_repo.branches[pushed_branch] == self.repo.branches["master"] def test_local_push_executes_hook(self, tmpdir): target_repo = self.get_empty_repo(tmpdir, bare=True) - self._add_failing_hook(target_repo.path, 'pre-receive', bare=True) + self._add_failing_hook(target_repo.path, "pre-receive", bare=True) with pytest.raises(RepositoryError): - self.repo._local_push( - 'master', target_repo.path, 'master', enable_hooks=True) + self.repo._local_push("master", target_repo.path, "master", enable_hooks=True) def test_maybe_prepare_merge_workspace(self): workspace = self.repo._maybe_prepare_merge_workspace( - 2, 'pr2', Reference('branch', 'master', 'unused'), - Reference('branch', 'master', 'unused')) + 2, "pr2", Reference("branch", "master", "unused"), Reference("branch", "master", "unused") + ) assert os.path.isdir(workspace) workspace_repo = GitRepository(workspace) @@ -579,14 +563,14 @@ TODO: To be written... # Calling it a second time should also succeed workspace = self.repo._maybe_prepare_merge_workspace( - 2, 'pr2', Reference('branch', 'master', 'unused'), - Reference('branch', 'master', 'unused')) + 2, "pr2", Reference("branch", "master", "unused"), Reference("branch", "master", "unused") + ) assert os.path.isdir(workspace) def test_maybe_prepare_merge_workspace_different_refs(self): workspace = self.repo._maybe_prepare_merge_workspace( - 2, 'pr2', Reference('branch', 'master', 'unused'), - Reference('branch', 'develop', 'unused')) + 2, "pr2", Reference("branch", "master", "unused"), Reference("branch", "develop", "unused") + ) assert os.path.isdir(workspace) workspace_repo = GitRepository(workspace) @@ -594,48 +578,47 @@ TODO: To be written... # Calling it a second time should also succeed workspace = self.repo._maybe_prepare_merge_workspace( - 2, 'pr2', Reference('branch', 'master', 'unused'), - Reference('branch', 'develop', 'unused')) + 2, "pr2", Reference("branch", "master", "unused"), Reference("branch", "develop", "unused") + ) assert os.path.isdir(workspace) def test_cleanup_merge_workspace(self): workspace = self.repo._maybe_prepare_merge_workspace( - 2, 'pr3', Reference('branch', 'master', 'unused'), - Reference('branch', 'master', 'unused')) - self.repo.cleanup_merge_workspace(2, 'pr3') + 2, "pr3", Reference("branch", "master", "unused"), Reference("branch", "master", "unused") + ) + self.repo.cleanup_merge_workspace(2, "pr3") assert not os.path.exists(workspace) def test_cleanup_merge_workspace_invalid_workspace_id(self): # No assert: because in case of an inexistent workspace this function # should still succeed. - self.repo.cleanup_merge_workspace(1, 'pr4') + self.repo.cleanup_merge_workspace(1, "pr4") def test_set_refs(self): - test_ref = 'refs/test-refs/abcde' - test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623' + test_ref = "refs/test-refs/abcde" + test_commit_id = "ecb86e1f424f2608262b130db174a7dfd25a6623" self.repo.set_refs(test_ref, test_commit_id) - stdout, _ = self.repo.run_git_command(['show-ref']) + stdout, _ = self.repo.run_git_command(["show-ref"]) assert test_ref in stdout assert test_commit_id in stdout def test_remove_ref(self): - test_ref = 'refs/test-refs/abcde' - test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623' + test_ref = "refs/test-refs/abcde" + test_commit_id = "ecb86e1f424f2608262b130db174a7dfd25a6623" self.repo.set_refs(test_ref, test_commit_id) - stdout, _ = self.repo.run_git_command(['show-ref']) + stdout, _ = self.repo.run_git_command(["show-ref"]) assert test_ref in stdout assert test_commit_id in stdout self.repo.remove_ref(test_ref) - stdout, _ = self.repo.run_git_command(['show-ref']) + stdout, _ = self.repo.run_git_command(["show-ref"]) assert test_ref not in stdout assert test_commit_id not in stdout class TestGitCommit(object): - @pytest.fixture(autouse=True) def prepare(self): self.repo = GitRepository(TEST_GIT_REPO) @@ -643,11 +626,11 @@ class TestGitCommit(object): def test_default_commit(self): tip = self.repo.get_commit() assert tip == self.repo.get_commit(None) - assert tip == self.repo.get_commit('tip') + assert tip == self.repo.get_commit("tip") def test_root_node(self): tip = self.repo.get_commit() - assert tip.root is tip.get_node('') + assert tip.root is tip.get_node(b"") def test_lazy_fetch(self): """ @@ -655,7 +638,7 @@ class TestGitCommit(object): the commit. This test is somewhat hard to write as order of tests is a key here. Written by running command after command in a shell. """ - commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc' + commit_id = "2a13f185e4525f9d4b59882791a2d397b90d5ddc" assert commit_id in self.repo.commit_ids commit = self.repo.get_commit(commit_id) assert len(commit.nodes) == 0 @@ -665,31 +648,29 @@ class TestGitCommit(object): # accessing root.nodes updates commit.nodes assert len(commit.nodes) == 9 - docs = root.get_node('docs') + docs = commit.get_node(b"docs") # we haven't yet accessed anything new as docs dir was already cached assert len(commit.nodes) == 9 assert len(docs.nodes) == 8 # accessing docs.nodes updates commit.nodes assert len(commit.nodes) == 17 - assert docs is commit.get_node('docs') + assert docs is commit.get_node(b"docs") assert docs is root.nodes[0] assert docs is root.dirs[0] - assert docs is commit.get_node('docs') + assert docs is commit.get_node(b"docs") def test_nodes_with_commit(self): - commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc' + commit_id = "2a13f185e4525f9d4b59882791a2d397b90d5ddc" commit = self.repo.get_commit(commit_id) root = commit.root - docs = root.get_node('docs') - assert docs is commit.get_node('docs') - api = docs.get_node('api') - assert api is commit.get_node('docs/api') - index = api.get_node('index.rst') - assert index is commit.get_node('docs/api/index.rst') - assert index is commit.get_node('docs')\ - .get_node('api')\ - .get_node('index.rst') + assert isinstance(root, RootNode) + docs = commit.get_node(b"docs") + assert docs is commit.get_node(b"docs") + api = commit.get_node(b"docs/api") + assert api is commit.get_node(b"docs/api") + index = commit.get_node(b"docs/api/index.rst") + assert index is commit.get_node(b"docs/api/index.rst") def test_branch_and_tags(self): """ @@ -716,19 +697,12 @@ class TestGitCommit(object): def test_file_size(self): to_check = ( - ('c1214f7e79e02fc37156ff215cd71275450cffc3', - 'vcs/backends/BaseRepository.py', 502), - ('d7e0d30fbcae12c90680eb095a4f5f02505ce501', - 'vcs/backends/hg.py', 854), - ('6e125e7c890379446e98980d8ed60fba87d0f6d1', - 'setup.py', 1068), - - ('d955cd312c17b02143c04fa1099a352b04368118', - 'vcs/backends/base.py', 2921), - ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e', - 'vcs/backends/base.py', 3936), - ('f50f42baeed5af6518ef4b0cb2f1423f3851a941', - 'vcs/backends/base.py', 6189), + ("c1214f7e79e02fc37156ff215cd71275450cffc3", b"vcs/backends/BaseRepository.py", 502), + ("d7e0d30fbcae12c90680eb095a4f5f02505ce501", b"vcs/backends/hg.py", 854), + ("6e125e7c890379446e98980d8ed60fba87d0f6d1", b"setup.py", 1068), + ("d955cd312c17b02143c04fa1099a352b04368118", b"vcs/backends/base.py", 2921), + ("ca1eb7957a54bce53b12d1a51b13452f95bc7c7e", b"vcs/backends/base.py", 3936), + ("f50f42baeed5af6518ef4b0cb2f1423f3851a941", b"vcs/backends/base.py", 6189), ) for commit_id, path, size in to_check: node = self.repo.get_commit(commit_id).get_node(path) @@ -736,80 +710,77 @@ class TestGitCommit(object): assert node.size == size def test_file_history_from_commits(self): - node = self.repo[10].get_node('setup.py') + node = self.repo[10].get_node(b"setup.py") commit_ids = [commit.raw_id for commit in node.history] - assert ['ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == commit_ids + assert ["ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == commit_ids - node = self.repo[20].get_node('setup.py') + node = self.repo[20].get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids + assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids # special case we check history from commit that has this particular # file changed this means we check if it's included as well - node = self.repo.get_commit('191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e') \ - .get_node('setup.py') + node = self.repo.get_commit("191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e").get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids + assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids def test_file_history(self): # we can only check if those commits are present in the history # as we cannot update this test every time file is changed files = { - 'setup.py': [ - '54386793436c938cff89326944d4c2702340037d', - '51d254f0ecf5df2ce50c0b115741f4cf13985dab', - '998ed409c795fec2012b1c0ca054d99888b22090', - '5e0eb4c47f56564395f76333f319d26c79e2fb09', - '0115510b70c7229dbc5dc49036b32e7d91d23acd', - '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e', - '2a13f185e4525f9d4b59882791a2d397b90d5ddc', - '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b', + b"setup.py": [ + "54386793436c938cff89326944d4c2702340037d", + "51d254f0ecf5df2ce50c0b115741f4cf13985dab", + "998ed409c795fec2012b1c0ca054d99888b22090", + "5e0eb4c47f56564395f76333f319d26c79e2fb09", + "0115510b70c7229dbc5dc49036b32e7d91d23acd", + "7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e", + "2a13f185e4525f9d4b59882791a2d397b90d5ddc", + "191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", + "ff7ca51e58c505fec0dd2491de52c622bb7a806b", ], - 'vcs/nodes.py': [ - '33fa3223355104431402a888fa77a4e9956feb3e', - 'fa014c12c26d10ba682fadb78f2a11c24c8118e1', - 'e686b958768ee96af8029fe19c6050b1a8dd3b2b', - 'ab5721ca0a081f26bf43d9051e615af2cc99952f', - 'c877b68d18e792a66b7f4c529ea02c8f80801542', - '4313566d2e417cb382948f8d9d7c765330356054', - '6c2303a793671e807d1cfc70134c9ca0767d98c2', - '54386793436c938cff89326944d4c2702340037d', - '54000345d2e78b03a99d561399e8e548de3f3203', - '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b', - '2d03ca750a44440fb5ea8b751176d1f36f8e8f46', - '2a08b128c206db48c2f0b8f70df060e6db0ae4f8', - '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b', - 'ac71e9503c2ca95542839af0ce7b64011b72ea7c', - '12669288fd13adba2a9b7dd5b870cc23ffab92d2', - '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382', - '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5', - '5eab1222a7cd4bfcbabc218ca6d04276d4e27378', - 'f50f42baeed5af6518ef4b0cb2f1423f3851a941', - 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25', - 'f15c21f97864b4f071cddfbf2750ec2e23859414', - 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade', - 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b', - '84dec09632a4458f79f50ddbbd155506c460b4f9', - '0115510b70c7229dbc5dc49036b32e7d91d23acd', - '2a13f185e4525f9d4b59882791a2d397b90d5ddc', - '3bf1c5868e570e39569d094f922d33ced2fa3b2b', - 'b8d04012574729d2c29886e53b1a43ef16dd00a1', - '6970b057cffe4aab0a792aa634c89f4bebf01441', - 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b', + b"vcs/nodes.py": [ + "33fa3223355104431402a888fa77a4e9956feb3e", + "fa014c12c26d10ba682fadb78f2a11c24c8118e1", + "e686b958768ee96af8029fe19c6050b1a8dd3b2b", + "ab5721ca0a081f26bf43d9051e615af2cc99952f", + "c877b68d18e792a66b7f4c529ea02c8f80801542", + "4313566d2e417cb382948f8d9d7c765330356054", + "6c2303a793671e807d1cfc70134c9ca0767d98c2", + "54386793436c938cff89326944d4c2702340037d", + "54000345d2e78b03a99d561399e8e548de3f3203", + "1c6b3677b37ea064cb4b51714d8f7498f93f4b2b", + "2d03ca750a44440fb5ea8b751176d1f36f8e8f46", + "2a08b128c206db48c2f0b8f70df060e6db0ae4f8", + "30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b", + "ac71e9503c2ca95542839af0ce7b64011b72ea7c", + "12669288fd13adba2a9b7dd5b870cc23ffab92d2", + "5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382", + "12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5", + "5eab1222a7cd4bfcbabc218ca6d04276d4e27378", + "f50f42baeed5af6518ef4b0cb2f1423f3851a941", + "d7e390a45f6aa96f04f5e7f583ad4f867431aa25", + "f15c21f97864b4f071cddfbf2750ec2e23859414", + "e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade", + "ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b", + "84dec09632a4458f79f50ddbbd155506c460b4f9", + "0115510b70c7229dbc5dc49036b32e7d91d23acd", + "2a13f185e4525f9d4b59882791a2d397b90d5ddc", + "3bf1c5868e570e39569d094f922d33ced2fa3b2b", + "b8d04012574729d2c29886e53b1a43ef16dd00a1", + "6970b057cffe4aab0a792aa634c89f4bebf01441", + "dd80b0f6cf5052f17cc738c2951c4f2070200d7f", + "ff7ca51e58c505fec0dd2491de52c622bb7a806b", ], - 'vcs/backends/git.py': [ - '4cf116ad5a457530381135e2f4c453e68a1b0105', - '9a751d84d8e9408e736329767387f41b36935153', - 'cb681fb539c3faaedbcdf5ca71ca413425c18f01', - '428f81bb652bcba8d631bce926e8834ff49bdcc6', - '180ab15aebf26f98f714d8c68715e0f05fa6e1c7', - '2b8e07312a2e89e92b90426ab97f349f4bce2a3a', - '50e08c506174d8645a4bb517dd122ac946a0f3bf', - '54000345d2e78b03a99d561399e8e548de3f3203', + b"vcs/backends/git.py": [ + "4cf116ad5a457530381135e2f4c453e68a1b0105", + "9a751d84d8e9408e736329767387f41b36935153", + "cb681fb539c3faaedbcdf5ca71ca413425c18f01", + "428f81bb652bcba8d631bce926e8834ff49bdcc6", + "180ab15aebf26f98f714d8c68715e0f05fa6e1c7", + "2b8e07312a2e89e92b90426ab97f349f4bce2a3a", + "50e08c506174d8645a4bb517dd122ac946a0f3bf", + "54000345d2e78b03a99d561399e8e548de3f3203", ], } for path, commit_ids in files.items(): @@ -817,79 +788,79 @@ class TestGitCommit(object): node_ids = [commit.raw_id for commit in node.history] assert set(commit_ids).issubset(set(node_ids)), ( "We assumed that %s is subset of commit_ids for which file %s " - "has been changed, and history of that node returned: %s" - % (commit_ids, path, node_ids)) + "has been changed, and history of that node returned: %s" % (commit_ids, path, node_ids) + ) def test_file_annotate(self): files = { - 'vcs/backends/__init__.py': { - 'c1214f7e79e02fc37156ff215cd71275450cffc3': { - 'lines_no': 1, - 'commits': [ - 'c1214f7e79e02fc37156ff215cd71275450cffc3', + b"vcs/backends/__init__.py": { + "c1214f7e79e02fc37156ff215cd71275450cffc3": { + "lines_no": 1, + "commits": [ + "c1214f7e79e02fc37156ff215cd71275450cffc3", ], }, - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': { - 'lines_no': 21, - 'commits': [ - '49d3fd156b6f7db46313fac355dca1a0b94a0017', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647": { + "lines_no": 21, + "commits": [ + "49d3fd156b6f7db46313fac355dca1a0b94a0017", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", ], }, - 'e29b67bd158580fc90fc5e9111240b90e6e86064': { - 'lines_no': 32, - 'commits': [ - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '5eab1222a7cd4bfcbabc218ca6d04276d4e27378', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '54000345d2e78b03a99d561399e8e548de3f3203', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '78c3f0c23b7ee935ec276acb8b8212444c33c396', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '2a13f185e4525f9d4b59882791a2d397b90d5ddc', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '78c3f0c23b7ee935ec276acb8b8212444c33c396', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '992f38217b979d0b0987d0bae3cc26dac85d9b19', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', - '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647', + "e29b67bd158580fc90fc5e9111240b90e6e86064": { + "lines_no": 32, + "commits": [ + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "5eab1222a7cd4bfcbabc218ca6d04276d4e27378", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "54000345d2e78b03a99d561399e8e548de3f3203", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "78c3f0c23b7ee935ec276acb8b8212444c33c396", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "2a13f185e4525f9d4b59882791a2d397b90d5ddc", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "78c3f0c23b7ee935ec276acb8b8212444c33c396", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "992f38217b979d0b0987d0bae3cc26dac85d9b19", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", + "16fba1ae9334d79b66d7afed2c2dfbfa2ae53647", ], }, }, @@ -903,87 +874,64 @@ class TestGitCommit(object): l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)] assert l1_1 == l1_2 l1 = l1_1 - l2 = files[fname][commit_id]['commits'] + l2 = files[fname][commit_id]["commits"] assert l1 == l2, ( "The lists of commit_ids for %s@commit_id %s" "from annotation list should match each other, " - "got \n%s \nvs \n%s " % (fname, commit_id, l1, l2)) + "got \n%s \nvs \n%s " % (fname, commit_id, l1, l2) + ) def test_files_state(self): """ Tests state of FileNodes. """ - node = self.repo\ - .get_commit('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\ - .get_node('vcs/utils/diffs.py') - assert node.state, NodeState.ADDED - assert node.added - assert not node.changed - assert not node.not_changed - assert not node.removed + commit = self.repo.get_commit("e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0") + node = commit.get_node(b"vcs/utils/diffs.py") + assert node.bytes_path in commit.added_paths - node = self.repo\ - .get_commit('33fa3223355104431402a888fa77a4e9956feb3e')\ - .get_node('.hgignore') - assert node.state, NodeState.CHANGED - assert not node.added - assert node.changed - assert not node.not_changed - assert not node.removed + commit = self.repo.get_commit("33fa3223355104431402a888fa77a4e9956feb3e") + node = commit.get_node(b".hgignore") + assert node.bytes_path in commit.changed_paths - node = self.repo\ - .get_commit('e29b67bd158580fc90fc5e9111240b90e6e86064')\ - .get_node('setup.py') - assert node.state, NodeState.NOT_CHANGED - assert not node.added - assert not node.changed - assert node.not_changed - assert not node.removed + commit = self.repo.get_commit("e29b67bd158580fc90fc5e9111240b90e6e86064") + node = commit.get_node(b"setup.py") + assert node.bytes_path not in commit.affected_files # If node has REMOVED state then trying to fetch it would raise # CommitError exception - commit = self.repo.get_commit( - 'fa6600f6848800641328adbf7811fd2372c02ab2') - path = 'vcs/backends/BaseRepository.py' + commit = self.repo.get_commit("fa6600f6848800641328adbf7811fd2372c02ab2") + path = b"vcs/backends/BaseRepository.py" with pytest.raises(NodeDoesNotExistError): commit.get_node(path) + # but it would be one of ``removed`` (commit's attribute) - assert path in [rf.path for rf in commit.removed] + assert path in [rf for rf in commit.removed_paths] - commit = self.repo.get_commit( - '54386793436c938cff89326944d4c2702340037d') - changed = [ - 'setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py', - 'vcs/nodes.py'] - assert set(changed) == set([f.path for f in commit.changed]) + commit = self.repo.get_commit("54386793436c938cff89326944d4c2702340037d") + changed = [b"setup.py", b"tests/test_nodes.py", b"vcs/backends/hg.py", b"vcs/nodes.py"] + assert set(changed) == set([f for f in commit.changed_paths]) def test_unicode_branch_refs(self): unicode_branches = { - 'refs/heads/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b', - u'refs/heads/uniçö∂e': 'ürl', + "refs/heads/unicode": "6c0ce52b229aa978889e91b38777f800e85f330b", + "refs/heads/uniçö∂e": "ürl", } - with mock.patch( - ("rhodecode.lib.vcs.backends.git.repository" - ".GitRepository._refs"), - unicode_branches): + with mock.patch(("rhodecode.lib.vcs.backends.git.repository" ".GitRepository._refs"), unicode_branches): branches = self.repo.branches - assert 'unicode' in branches - assert 'uniçö∂e' in branches + assert "unicode" in branches + assert "uniçö∂e" in branches def test_unicode_tag_refs(self): unicode_tags = { - 'refs/tags/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b', - u'refs/tags/uniçö∂e': '6c0ce52b229aa978889e91b38777f800e85f330b', + "refs/tags/unicode": "6c0ce52b229aa978889e91b38777f800e85f330b", + "refs/tags/uniçö∂e": "6c0ce52b229aa978889e91b38777f800e85f330b", } - with mock.patch( - ("rhodecode.lib.vcs.backends.git.repository" - ".GitRepository._refs"), - unicode_tags): + with mock.patch(("rhodecode.lib.vcs.backends.git.repository" ".GitRepository._refs"), unicode_tags): tags = self.repo.tags - assert 'unicode' in tags - assert 'uniçö∂e' in tags + assert "unicode" in tags + assert "uniçö∂e" in tags def test_commit_message_is_unicode(self): for commit in self.repo: @@ -995,190 +943,187 @@ class TestGitCommit(object): def test_repo_files_content_types(self): commit = self.repo.get_commit() - for node in commit.get_node('/'): + for node in commit.get_node(b"/"): if node.is_file(): assert type(node.content) == bytes assert type(node.str_content) == str def test_wrong_path(self): # There is 'setup.py' in the root dir but not there: - path = 'foo/bar/setup.py' + path = b"foo/bar/setup.py" tip = self.repo.get_commit() with pytest.raises(VCSError): tip.get_node(path) - @pytest.mark.parametrize("author_email, commit_id", [ - ('marcin@python-blog.com', 'c1214f7e79e02fc37156ff215cd71275450cffc3'), - ('lukasz.balcerzak@python-center.pl', - 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'), - ('none@none', '8430a588b43b5d6da365400117c89400326e7992'), - ]) + @pytest.mark.parametrize( + "author_email, commit_id", + [ + ("marcin@python-blog.com", "c1214f7e79e02fc37156ff215cd71275450cffc3"), + ("lukasz.balcerzak@python-center.pl", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"), + ("none@none", "8430a588b43b5d6da365400117c89400326e7992"), + ], + ) def test_author_email(self, author_email, commit_id): commit = self.repo.get_commit(commit_id) assert author_email == commit.author_email - @pytest.mark.parametrize("author, commit_id", [ - ('Marcin Kuzminski', 'c1214f7e79e02fc37156ff215cd71275450cffc3'), - ('Lukasz Balcerzak', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'), - ('marcink', '8430a588b43b5d6da365400117c89400326e7992'), - ]) + @pytest.mark.parametrize( + "author, commit_id", + [ + ("Marcin Kuzminski", "c1214f7e79e02fc37156ff215cd71275450cffc3"), + ("Lukasz Balcerzak", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"), + ("marcink", "8430a588b43b5d6da365400117c89400326e7992"), + ], + ) def test_author_username(self, author, commit_id): commit = self.repo.get_commit(commit_id) assert author == commit.author_name class TestLargeFileRepo(object): - def test_large_file(self, backend_git): conf = make_db_config() - repo = backend_git.create_test_repo('largefiles', conf) + git_largefiles_store = conf.get("vcs_git_lfs", "store_location") + + repo = backend_git.create_test_repo("largefiles", conf) tip = repo.scm_instance().get_commit() + node = tip.get_node(b"1MB.zip") # extract stored LF node into the origin cache - lfs_store = os.path.join(repo.repo_path, repo.repo_name, 'lfs_store') + repo_lfs_store: str = os.path.join(repo.repo_path, repo.repo_name, "lfs_store") + + oid: str = "7b331c02e313c7599d5a90212e17e6d3cb729bd2e1c9b873c302a63c95a2f9bf" + # where the OID actually is INSIDE the repo... + oid_path = os.path.join(repo_lfs_store, oid) - oid = '7b331c02e313c7599d5a90212e17e6d3cb729bd2e1c9b873c302a63c95a2f9bf' - oid_path = os.path.join(lfs_store, oid) - # Todo: oid path depends on LFSOidStorage.store_suffix. Once it will be changed update below line accordingly - oid_destination = os.path.join( - conf.get('vcs_git_lfs', 'store_location'), f'objects/{oid[:2]}/{oid[2:4]}/{oid}') + # Note: oid path depends on LFSOidStore.store_suffix. Once it will be changed update below line accordingly + oid_destination = os.path.join(git_largefiles_store, f"objects/{oid[:2]}/{oid[2:4]}/{oid}") - os.makedirs(os.path.dirname(oid_destination)) + spec_path = os.path.dirname(oid_destination) + os.makedirs(spec_path, exist_ok=True) shutil.copy(oid_path, oid_destination) - node = tip.get_node('1MB.zip') - lf_node = node.get_largefile_node() assert lf_node.is_largefile() is True assert lf_node.size == 1024000 - assert lf_node.name == '1MB.zip' + assert lf_node.name == b"1MB.zip" @pytest.mark.usefixtures("vcs_repository_support") class TestGitSpecificWithRepo(BackendTestMixin): - @classmethod def _get_commits(cls): return [ { - 'message': 'Initial', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'foobar/static/js/admin/base.js', content=b'base'), - FileNode(b'foobar/static/admin', content=b'admin', mode=0o120000), # this is a link - FileNode(b'foo', content=b'foo'), + "message": "Initial", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"foobar/static/js/admin/base.js", content=b"base"), + FileNode(b"foobar/static/admin", content=b"admin", mode=0o120000), # this is a link + FileNode(b"foo", content=b"foo"), ], }, { - 'message': 'Second', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 22), - 'added': [ - FileNode(b'foo2', content=b'foo2'), + "message": "Second", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 22), + "added": [ + FileNode(b"foo2", content=b"foo2"), ], }, ] def test_paths_slow_traversing(self): commit = self.repo.get_commit() - assert commit.get_node('foobar').get_node('static').get_node('js')\ - .get_node('admin').get_node('base.js').content == b'base' + assert commit.get_node(b"foobar/static/js/admin/base.js").content == b"base" def test_paths_fast_traversing(self): commit = self.repo.get_commit() - assert commit.get_node('foobar/static/js/admin/base.js').content == b'base' + assert commit.get_node(b"foobar/static/js/admin/base.js").content == b"base" def test_get_diff_runs_git_command_with_hashes(self): comm1 = self.repo[0] comm2 = self.repo[1] - with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock: + with mock.patch.object(self.repo, "_remote", return_value=mock.Mock()) as remote_mock: remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff) self.repo.get_diff(comm1, comm2) remote_mock.diff.assert_called_once_with( - comm1.raw_id, comm2.raw_id, - file_filter=None, opt_ignorews=False, context=3) + comm1.raw_id, comm2.raw_id, file_filter=None, opt_ignorews=False, context=3 + ) def test_get_diff_runs_git_command_with_str_hashes(self): comm2 = self.repo[1] - with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock: + with mock.patch.object(self.repo, "_remote", return_value=mock.Mock()) as remote_mock: remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff) self.repo.get_diff(self.repo.EMPTY_COMMIT, comm2) remote_mock.diff.assert_called_once_with( - self.repo.EMPTY_COMMIT.raw_id, comm2.raw_id, - file_filter=None, opt_ignorews=False, context=3) + self.repo.EMPTY_COMMIT.raw_id, comm2.raw_id, file_filter=None, opt_ignorews=False, context=3 + ) def test_get_diff_runs_git_command_with_path_if_its_given(self): comm1 = self.repo[0] comm2 = self.repo[1] - with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock: + with mock.patch.object(self.repo, "_remote", return_value=mock.Mock()) as remote_mock: remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff) - self.repo.get_diff(comm1, comm2, 'foo') + self.repo.get_diff(comm1, comm2, "foo") remote_mock.diff.assert_called_once_with( - self.repo._lookup_commit(0), comm2.raw_id, - file_filter='foo', opt_ignorews=False, context=3) + self.repo._lookup_commit(0), comm2.raw_id, file_filter="foo", opt_ignorews=False, context=3 + ) @pytest.mark.usefixtures("vcs_repository_support") class TestGitRegression(BackendTestMixin): - @classmethod def _get_commits(cls): return [ { - 'message': 'Initial', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 20), - 'added': [ - FileNode(b'bot/__init__.py', content=b'base'), - FileNode(b'bot/templates/404.html', content=b'base'), - FileNode(b'bot/templates/500.html', content=b'base'), + "message": "Initial", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 20), + "added": [ + FileNode(b"bot/__init__.py", content=b"base"), + FileNode(b"bot/templates/404.html", content=b"base"), + FileNode(b"bot/templates/500.html", content=b"base"), ], }, { - 'message': 'Second', - 'author': 'Joe Doe ', - 'date': datetime.datetime(2010, 1, 1, 22), - 'added': [ - FileNode(b'bot/build/migrations/1.py', content=b'foo2'), - FileNode(b'bot/build/migrations/2.py', content=b'foo2'), - FileNode(b'bot/build/static/templates/f.html', content=b'foo2'), - FileNode(b'bot/build/static/templates/f1.html', content=b'foo2'), - FileNode(b'bot/build/templates/err.html', content=b'foo2'), - FileNode(b'bot/build/templates/err2.html', content=b'foo2'), + "message": "Second", + "author": "Joe Doe ", + "date": datetime.datetime(2010, 1, 1, 22), + "added": [ + FileNode(b"bot/build/migrations/1.py", content=b"foo2"), + FileNode(b"bot/build/migrations/2.py", content=b"foo2"), + FileNode(b"bot/build/static/templates/f.html", content=b"foo2"), + FileNode(b"bot/build/static/templates/f1.html", content=b"foo2"), + FileNode(b"bot/build/templates/err.html", content=b"foo2"), + FileNode(b"bot/build/templates/err2.html", content=b"foo2"), ], }, ] - @pytest.mark.parametrize("path, expected_paths", [ - ('bot', [ - 'bot/build', - 'bot/templates', - 'bot/__init__.py']), - ('bot/build', [ - 'bot/build/migrations', - 'bot/build/static', - 'bot/build/templates']), - ('bot/build/static', [ - 'bot/build/static/templates']), - ('bot/build/static/templates', [ - 'bot/build/static/templates/f.html', - 'bot/build/static/templates/f1.html']), - ('bot/build/templates', [ - 'bot/build/templates/err.html', - 'bot/build/templates/err2.html']), - ('bot/templates/', [ - 'bot/templates/404.html', - 'bot/templates/500.html']), - ]) + @pytest.mark.parametrize( + "path, expected_paths", + [ + (b"bot", ["bot/build", "bot/templates", "bot/__init__.py"]), + (b"bot/build", ["bot/build/migrations", "bot/build/static", "bot/build/templates"]), + (b"bot/build/static", ["bot/build/static/templates"]), + ( + b"bot/build/static/templates", + ["bot/build/static/templates/f.html", "bot/build/static/templates/f1.html"], + ), + (b"bot/build/templates", ["bot/build/templates/err.html", "bot/build/templates/err2.html"]), + (b"bot/templates/", ["bot/templates/404.html", "bot/templates/500.html"]), + ], + ) def test_similar_paths(self, path, expected_paths): commit = self.repo.get_commit() paths = [n.path for n in commit.get_nodes(path)] @@ -1186,122 +1131,121 @@ class TestGitRegression(BackendTestMixin class TestDiscoverGitVersion(object): - def test_returns_git_version(self, baseapp): version = discover_git_version() assert version def test_returns_empty_string_without_vcsserver(self): mock_connection = mock.Mock() - mock_connection.discover_git_version = mock.Mock( - side_effect=Exception) - with mock.patch('rhodecode.lib.vcs.connection.Git', mock_connection): + mock_connection.discover_git_version = mock.Mock(side_effect=Exception) + with mock.patch("rhodecode.lib.vcs.connection.Git", mock_connection): version = discover_git_version() - assert version == '' + assert version == "" class TestGetSubmoduleUrl(object): def test_submodules_file_found(self): - commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1) + commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1) node = mock.Mock() - with mock.patch.object( - commit, 'get_node', return_value=node) as get_node_mock: + with mock.patch.object(commit, "get_node", return_value=node) as get_node_mock: node.str_content = ( - '[submodule "subrepo1"]\n' - '\tpath = subrepo1\n' - '\turl = https://code.rhodecode.com/dulwich\n' + '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n" ) - result = commit._get_submodule_url('subrepo1') - get_node_mock.assert_called_once_with('.gitmodules') - assert result == 'https://code.rhodecode.com/dulwich' + result = commit._get_submodule_url(b"subrepo1") + get_node_mock.assert_called_once_with(b".gitmodules") + assert result == "https://code.rhodecode.com/dulwich" def test_complex_submodule_path(self): - commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1) + commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1) node = mock.Mock() - with mock.patch.object( - commit, 'get_node', return_value=node) as get_node_mock: + with mock.patch.object(commit, "get_node", return_value=node) as get_node_mock: node.str_content = ( '[submodule "complex/subrepo/path"]\n' - '\tpath = complex/subrepo/path\n' - '\turl = https://code.rhodecode.com/dulwich\n' + "\tpath = complex/subrepo/path\n" + "\turl = https://code.rhodecode.com/dulwich\n" ) - result = commit._get_submodule_url('complex/subrepo/path') - get_node_mock.assert_called_once_with('.gitmodules') - assert result == 'https://code.rhodecode.com/dulwich' + result = commit._get_submodule_url(b"complex/subrepo/path") + get_node_mock.assert_called_once_with(b".gitmodules") + assert result == "https://code.rhodecode.com/dulwich" def test_submodules_file_not_found(self): - commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1) - with mock.patch.object( - commit, 'get_node', side_effect=NodeDoesNotExistError): - result = commit._get_submodule_url('complex/subrepo/path') + commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1) + with mock.patch.object(commit, "get_node", side_effect=NodeDoesNotExistError): + result = commit._get_submodule_url(b"complex/subrepo/path") assert result is None def test_path_not_found(self): - commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1) + commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1) node = mock.Mock() - with mock.patch.object( - commit, 'get_node', return_value=node) as get_node_mock: + with mock.patch.object(commit, "get_node", return_value=node) as get_node_mock: node.str_content = ( - '[submodule "subrepo1"]\n' - '\tpath = subrepo1\n' - '\turl = https://code.rhodecode.com/dulwich\n' + '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n" ) - result = commit._get_submodule_url('subrepo2') - get_node_mock.assert_called_once_with('.gitmodules') + result = commit._get_submodule_url(b"subrepo2") + get_node_mock.assert_called_once_with(b".gitmodules") assert result is None def test_returns_cached_values(self): - commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1) + commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1) node = mock.Mock() - with mock.patch.object( - commit, 'get_node', return_value=node) as get_node_mock: + with mock.patch.object(commit, "get_node", return_value=node) as get_node_mock: node.str_content = ( - '[submodule "subrepo1"]\n' - '\tpath = subrepo1\n' - '\turl = https://code.rhodecode.com/dulwich\n' + '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n" ) for _ in range(3): - commit._get_submodule_url('subrepo1') - get_node_mock.assert_called_once_with('.gitmodules') + commit._get_submodule_url(b"subrepo1") + get_node_mock.assert_called_once_with(b".gitmodules") def test_get_node_returns_a_link(self): repository = mock.Mock() - repository.alias = 'git' - commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1) - submodule_url = 'https://code.rhodecode.com/dulwich' - get_id_patch = mock.patch.object( - commit, '_get_tree_id_for_path', return_value=(1, 'link')) - get_submodule_patch = mock.patch.object( - commit, '_get_submodule_url', return_value=submodule_url) + repository.alias = "git" + commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1) + submodule_url = "https://code.rhodecode.com/dulwich" + get_id_patch = mock.patch.object(commit, "_get_path_tree_id_and_type", return_value=(1, NodeKind.SUBMODULE)) + get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url) with get_id_patch, get_submodule_patch as submodule_mock: - node = commit.get_node('/abcde') + node = commit.get_node(b"/abcde") - submodule_mock.assert_called_once_with('/abcde') + submodule_mock.assert_called_once_with(b"/abcde") assert type(node) == SubModuleNode assert node.url == submodule_url def test_get_nodes_returns_links(self): repository = mock.MagicMock() - repository.alias = 'git' - repository._remote.tree_items.return_value = [ - ('subrepo', 'stat', 1, 'link') - ] - commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1) - submodule_url = 'https://code.rhodecode.com/dulwich' - get_id_patch = mock.patch.object( - commit, '_get_tree_id_for_path', return_value=(1, 'tree')) - get_submodule_patch = mock.patch.object( - commit, '_get_submodule_url', return_value=submodule_url) + repository.alias = "git" + # obj_name, stat_, tree_item_id, node_kind, pre_load_data + repository._remote.get_nodes.return_value = [(b"subrepo", "stat", 1, NodeKind.SUBMODULE, [])] + commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1) + submodule_url = "https://code.rhodecode.com/dulwich" + + get_id_patch = mock.patch.object(commit, "_get_path_tree_id_and_type", return_value=(1, NodeKind.DIR)) + get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url) with get_id_patch, get_submodule_patch as submodule_mock: - nodes = commit.get_nodes('/abcde') + nodes = commit.get_nodes(b"/abcde") - submodule_mock.assert_called_once_with('/abcde/subrepo') assert len(nodes) == 1 assert type(nodes[0]) == SubModuleNode assert nodes[0].url == submodule_url + submodule_mock.assert_called_once_with(b"/abcde/subrepo") + + +class TestGetShadowInstance(object): + @pytest.fixture() + def repo(self, vcsbackend_git): + _git_repo = vcsbackend_git.repo + + mock.patch.object(_git_repo, "config", mock.Mock()) + connection_mock = mock.Mock(unsafe=True, name="connection.Hg") + + mock.patch("rhodecode.lib.vcs.connection.Git", connection_mock) + return _git_repo + + def test_getting_shadow_instance_copies_config(self, repo): + shadow = repo.get_shadow_instance(repo.path) + assert shadow.config.serialize() == repo.config.serialize() diff --git a/rhodecode/tests/vcs/test_hg.py b/rhodecode/tests/vcs/test_hg.py --- a/rhodecode/tests/vcs/test_hg.py +++ b/rhodecode/tests/vcs/test_hg.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,12 +24,10 @@ import pytest from rhodecode.lib.str_utils import safe_bytes from rhodecode.lib.utils import make_db_config from rhodecode.lib.vcs import backends -from rhodecode.lib.vcs.backends.base import ( - Reference, MergeResponse, MergeFailureReason) +from rhodecode.lib.vcs.backends.base import Reference, MergeResponse, MergeFailureReason from rhodecode.lib.vcs.backends.hg import MercurialRepository, MercurialCommit -from rhodecode.lib.vcs.exceptions import ( - RepositoryError, VCSError, NodeDoesNotExistError, CommitDoesNotExistError) -from rhodecode.lib.vcs.nodes import FileNode, NodeKind, NodeState +from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError, CommitDoesNotExistError +from rhodecode.lib.vcs.nodes import FileNode, NodeKind, DirNode, RootNode from rhodecode.tests import TEST_HG_REPO, TEST_HG_REPO_CLONE, repo_id_generator @@ -44,30 +41,24 @@ def repo_path_generator(): i = 0 while True: i += 1 - yield '%s-%d' % (TEST_HG_REPO_CLONE, i) + yield f"{TEST_HG_REPO_CLONE}-{i:d}" + REPO_PATH_GENERATOR = repo_path_generator() -@pytest.fixture(scope='class', autouse=True) -def repo(request, baseapp): - repo = MercurialRepository(TEST_HG_REPO) - if request.cls: - request.cls.repo = repo - return repo - - -class TestMercurialRepository(object): - +class TestMercurialRepository: # pylint: disable=protected-access + @pytest.fixture(autouse=True) + def prepare(self): + self.repo = MercurialRepository(TEST_HG_REPO) def get_clone_repo(self): """ Return a clone of the base repo. """ clone_path = next(REPO_PATH_GENERATOR) - repo_clone = MercurialRepository( - clone_path, create=True, src_url=self.repo.path) + repo_clone = MercurialRepository(clone_path, create=True, src_url=self.repo.path) return repo_clone @@ -78,40 +69,38 @@ class TestMercurialRepository(object): return MercurialRepository(next(REPO_PATH_GENERATOR), create=True) def test_wrong_repo_path(self): - wrong_repo_path = '/tmp/errorrepo_hg' + wrong_repo_path = "/tmp/errorrepo_hg" with pytest.raises(RepositoryError): MercurialRepository(wrong_repo_path) def test_unicode_path_repo(self): with pytest.raises(VCSError): - MercurialRepository('iShouldFail') + MercurialRepository("iShouldFail") def test_unicode_commit_id(self): with pytest.raises(CommitDoesNotExistError): - self.repo.get_commit('unicode-commit-id') + self.repo.get_commit("unicode-commit-id") with pytest.raises(CommitDoesNotExistError): - self.repo.get_commit('unícøde-spéçial-chäråcter-commit-id') + self.repo.get_commit("unícøde-spéçial-chäråcter-commit-id") def test_unicode_bookmark(self): - self.repo.bookmark('unicode-bookmark') - self.repo.bookmark('unícøde-spéçial-chäråcter-bookmark') + self.repo.bookmark("unicode-bookmark") + self.repo.bookmark("unícøde-spéçial-chäråcter-bookmark") def test_unicode_branch(self): with pytest.raises(KeyError): - assert self.repo.branches['unicode-branch'] + assert self.repo.branches["unicode-branch"] with pytest.raises(KeyError): - assert self.repo.branches['unícøde-spéçial-chäråcter-branch'] + assert self.repo.branches["unícøde-spéçial-chäråcter-branch"] def test_repo_clone(self): if os.path.exists(TEST_HG_REPO_CLONE): - self.fail( - 'Cannot test mercurial clone repo as location %s already ' - 'exists. You should manually remove it first.' - % TEST_HG_REPO_CLONE) + pytest.fail( + f"Cannot test mercurial clone repo as location {TEST_HG_REPO_CLONE} already exists. You should manually remove it first." + ) repo = MercurialRepository(TEST_HG_REPO) - repo_clone = MercurialRepository(TEST_HG_REPO_CLONE, - src_url=TEST_HG_REPO) + repo_clone = MercurialRepository(TEST_HG_REPO_CLONE, create=True, src_url=TEST_HG_REPO) assert len(repo.commit_ids) == len(repo_clone.commit_ids) # Checking hashes of commits should be enough for commit in repo.get_commits(): @@ -121,72 +110,80 @@ class TestMercurialRepository(object): def test_repo_clone_with_update(self): repo = MercurialRepository(TEST_HG_REPO) repo_clone = MercurialRepository( - TEST_HG_REPO_CLONE + '_w_update', - src_url=TEST_HG_REPO, do_workspace_checkout=True) + TEST_HG_REPO_CLONE + "_w_update", create=True, src_url=TEST_HG_REPO, do_workspace_checkout=True + ) assert len(repo.commit_ids) == len(repo_clone.commit_ids) # check if current workdir was updated - assert os.path.isfile( - os.path.join(TEST_HG_REPO_CLONE + '_w_update', 'MANIFEST.in')) + assert os.path.isfile(os.path.join(TEST_HG_REPO_CLONE + "_w_update", "MANIFEST.in")) def test_repo_clone_without_update(self): repo = MercurialRepository(TEST_HG_REPO) repo_clone = MercurialRepository( - TEST_HG_REPO_CLONE + '_wo_update', - src_url=TEST_HG_REPO, do_workspace_checkout=False) + TEST_HG_REPO_CLONE + "_wo_update", create=True, src_url=TEST_HG_REPO, do_workspace_checkout=False + ) assert len(repo.commit_ids) == len(repo_clone.commit_ids) - assert not os.path.isfile( - os.path.join(TEST_HG_REPO_CLONE + '_wo_update', 'MANIFEST.in')) + assert not os.path.isfile(os.path.join(TEST_HG_REPO_CLONE + "_wo_update", "MANIFEST.in")) def test_commit_ids(self): # there are 21 commits at bitbucket now # so we can assume they would be available from now on - subset = {'b986218ba1c9b0d6a259fac9b050b1724ed8e545', '3d8f361e72ab303da48d799ff1ac40d5ac37c67e', - '6cba7170863a2411822803fa77a0a264f1310b35', '56349e29c2af3ac913b28bde9a2c6154436e615b', - '2dda4e345facb0ccff1a191052dd1606dba6781d', '6fff84722075f1607a30f436523403845f84cd9e', - '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb', - 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c', 'be90031137367893f1c406e0a8683010fd115b79', - 'db8e58be770518cbb2b1cdfa69146e47cd481481', '84478366594b424af694a6c784cb991a16b87c21', - '17f8e105dddb9f339600389c6dc7175d395a535c', '20a662e756499bde3095ffc9bc0643d1def2d0eb', - '2e319b85e70a707bba0beff866d9f9de032aa4f9', '786facd2c61deb9cf91e9534735124fb8fc11842', - '94593d2128d38210a2fcd1aabff6dda0d6d9edf8', 'aa6a0de05b7612707db567078e130a6cd114a9a7', - 'eada5a770da98ab0dd7325e29d00e0714f228d09' - } + subset = { + "b986218ba1c9b0d6a259fac9b050b1724ed8e545", + "3d8f361e72ab303da48d799ff1ac40d5ac37c67e", + "6cba7170863a2411822803fa77a0a264f1310b35", + "56349e29c2af3ac913b28bde9a2c6154436e615b", + "2dda4e345facb0ccff1a191052dd1606dba6781d", + "6fff84722075f1607a30f436523403845f84cd9e", + "7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7", + "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb", + "dc5d2c0661b61928834a785d3e64a3f80d3aad9c", + "be90031137367893f1c406e0a8683010fd115b79", + "db8e58be770518cbb2b1cdfa69146e47cd481481", + "84478366594b424af694a6c784cb991a16b87c21", + "17f8e105dddb9f339600389c6dc7175d395a535c", + "20a662e756499bde3095ffc9bc0643d1def2d0eb", + "2e319b85e70a707bba0beff866d9f9de032aa4f9", + "786facd2c61deb9cf91e9534735124fb8fc11842", + "94593d2128d38210a2fcd1aabff6dda0d6d9edf8", + "aa6a0de05b7612707db567078e130a6cd114a9a7", + "eada5a770da98ab0dd7325e29d00e0714f228d09", + } assert subset.issubset(set(self.repo.commit_ids)) # check if we have the proper order of commits org = [ - 'b986218ba1c9b0d6a259fac9b050b1724ed8e545', - '3d8f361e72ab303da48d799ff1ac40d5ac37c67e', - '6cba7170863a2411822803fa77a0a264f1310b35', - '56349e29c2af3ac913b28bde9a2c6154436e615b', - '2dda4e345facb0ccff1a191052dd1606dba6781d', - '6fff84722075f1607a30f436523403845f84cd9e', - '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7', - '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb', - 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c', - 'be90031137367893f1c406e0a8683010fd115b79', - 'db8e58be770518cbb2b1cdfa69146e47cd481481', - '84478366594b424af694a6c784cb991a16b87c21', - '17f8e105dddb9f339600389c6dc7175d395a535c', - '20a662e756499bde3095ffc9bc0643d1def2d0eb', - '2e319b85e70a707bba0beff866d9f9de032aa4f9', - '786facd2c61deb9cf91e9534735124fb8fc11842', - '94593d2128d38210a2fcd1aabff6dda0d6d9edf8', - 'aa6a0de05b7612707db567078e130a6cd114a9a7', - 'eada5a770da98ab0dd7325e29d00e0714f228d09', - '2c1885c735575ca478bf9e17b0029dca68824458', - 'd9bcd465040bf869799b09ad732c04e0eea99fe9', - '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7', - '4fb8326d78e5120da2c7468dcf7098997be385da', - '62b4a097164940bd66030c4db51687f3ec035eed', - '536c1a19428381cfea92ac44985304f6a8049569', - '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4', - '9bb326a04ae5d98d437dece54be04f830cf1edd9', - 'f8940bcb890a98c4702319fbe36db75ea309b475', - 'ff5ab059786ebc7411e559a2cc309dfae3625a3b', - '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08', - 'ee87846a61c12153b51543bf860e1026c6d3dcba', + "b986218ba1c9b0d6a259fac9b050b1724ed8e545", + "3d8f361e72ab303da48d799ff1ac40d5ac37c67e", + "6cba7170863a2411822803fa77a0a264f1310b35", + "56349e29c2af3ac913b28bde9a2c6154436e615b", + "2dda4e345facb0ccff1a191052dd1606dba6781d", + "6fff84722075f1607a30f436523403845f84cd9e", + "7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7", + "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb", + "dc5d2c0661b61928834a785d3e64a3f80d3aad9c", + "be90031137367893f1c406e0a8683010fd115b79", + "db8e58be770518cbb2b1cdfa69146e47cd481481", + "84478366594b424af694a6c784cb991a16b87c21", + "17f8e105dddb9f339600389c6dc7175d395a535c", + "20a662e756499bde3095ffc9bc0643d1def2d0eb", + "2e319b85e70a707bba0beff866d9f9de032aa4f9", + "786facd2c61deb9cf91e9534735124fb8fc11842", + "94593d2128d38210a2fcd1aabff6dda0d6d9edf8", + "aa6a0de05b7612707db567078e130a6cd114a9a7", + "eada5a770da98ab0dd7325e29d00e0714f228d09", + "2c1885c735575ca478bf9e17b0029dca68824458", + "d9bcd465040bf869799b09ad732c04e0eea99fe9", + "469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7", + "4fb8326d78e5120da2c7468dcf7098997be385da", + "62b4a097164940bd66030c4db51687f3ec035eed", + "536c1a19428381cfea92ac44985304f6a8049569", + "965e8ab3c44b070cdaa5bf727ddef0ada980ecc4", + "9bb326a04ae5d98d437dece54be04f830cf1edd9", + "f8940bcb890a98c4702319fbe36db75ea309b475", + "ff5ab059786ebc7411e559a2cc309dfae3625a3b", + "6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08", + "ee87846a61c12153b51543bf860e1026c6d3dcba", ] assert org == self.repo.commit_ids[:31] @@ -197,8 +194,7 @@ class TestMercurialRepository(object): def test_slicing(self): # 4 1 5 10 95 - for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5), - (10, 20, 10), (5, 100, 95)]: + for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5), (10, 20, 10), (5, 100, 95)]: indexes = list(self.repo[sfrom:sto]) assert len(indexes) == size assert indexes[0] == self.repo.get_commit(commit_idx=sfrom) @@ -208,64 +204,73 @@ class TestMercurialRepository(object): # TODO: Need more tests here # active branches - assert 'default' in self.repo.branches - assert 'stable' in self.repo.branches + assert "default" in self.repo.branches + assert "stable" in self.repo.branches # closed - assert 'git' in self.repo._get_branches(closed=True) - assert 'web' in self.repo._get_branches(closed=True) + assert "git" in self.repo._get_branches(closed=True) + assert "web" in self.repo._get_branches(closed=True) - for name, id in self.repo.branches.items(): - assert isinstance(self.repo.get_commit(id), MercurialCommit) + for name, commit_id in self.repo.branches.items(): + assert isinstance(self.repo.get_commit(commit_id), MercurialCommit) def test_tip_in_tags(self): # tip is always a tag - assert 'tip' in self.repo.tags + assert "tip" in self.repo.tags def test_tip_commit_in_tags(self): tip = self.repo.get_commit() - assert self.repo.tags['tip'] == tip.raw_id + assert self.repo.tags["tip"] == tip.raw_id def test_initial_commit(self): init_commit = self.repo.get_commit(commit_idx=0) init_author = init_commit.author - assert init_commit.message == 'initial import' - assert init_author == 'Marcin Kuzminski ' + assert init_commit.message == "initial import" + assert init_author == "Marcin Kuzminski " assert init_author == init_commit.committer - assert sorted(init_commit._file_paths) == sorted([ - 'vcs/__init__.py', - 'vcs/backends/BaseRepository.py', - 'vcs/backends/__init__.py', - ]) - assert sorted(init_commit._dir_paths) == sorted( - ['', 'vcs', 'vcs/backends']) + assert sorted(init_commit.added_paths) == sorted( + [ + b"vcs/__init__.py", + b"vcs/backends/BaseRepository.py", + b"vcs/backends/__init__.py", + ] + ) + assert sorted(init_commit.affected_files) == sorted( + [ + b"vcs/__init__.py", + b"vcs/backends/BaseRepository.py", + b"vcs/backends/__init__.py", + ] + ) - assert init_commit._dir_paths + init_commit._file_paths == \ - init_commit._paths + for path in (b"vcs/__init__.py", b"vcs/backends/BaseRepository.py", b"vcs/backends/__init__.py"): + assert isinstance(init_commit.get_node(path), FileNode) + for path in (b"", b"vcs", b"vcs/backends"): + assert isinstance(init_commit.get_node(path), DirNode) with pytest.raises(NodeDoesNotExistError): - init_commit.get_node(path='foobar') + init_commit.get_node(path=b"foobar") - node = init_commit.get_node('vcs/') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs/") + assert hasattr(node, "kind") assert node.kind == NodeKind.DIR - node = init_commit.get_node('vcs') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs") + assert hasattr(node, "kind") assert node.kind == NodeKind.DIR - node = init_commit.get_node('vcs/__init__.py') - assert hasattr(node, 'kind') + node = init_commit.get_node(b"vcs/__init__.py") + assert hasattr(node, "kind") assert node.kind == NodeKind.FILE def test_not_existing_commit(self): # rawid with pytest.raises(RepositoryError): - self.repo.get_commit('abcd' * 10) + self.repo.get_commit("abcd" * 10) # shortid with pytest.raises(RepositoryError): - self.repo.get_commit('erro' * 4) + self.repo.get_commit("erro" * 4) # numeric with pytest.raises(RepositoryError): self.repo.get_commit(commit_idx=self.repo.count() + 1) @@ -277,7 +282,7 @@ class TestMercurialRepository(object): def test_commit10(self): commit10 = self.repo.get_commit(commit_idx=10) - README = """=== + readme = """=== VCS === @@ -289,9 +294,9 @@ Introduction TODO: To be written... """ - node = commit10.get_node('README.rst') + node = commit10.get_node(b"README.rst") assert node.kind == NodeKind.FILE - assert node.str_content == README + assert node.str_content == readme def test_local_clone(self): clone_path = next(REPO_PATH_GENERATOR) @@ -308,77 +313,73 @@ TODO: To be written... repo_clone = self.get_clone_repo() branches = repo_clone.branches - repo_clone._update('default') - assert branches['default'] == repo_clone._identify() - repo_clone._update('stable') - assert branches['stable'] == repo_clone._identify() + repo_clone._update("default") + assert branches["default"] == repo_clone._identify() + repo_clone._update("stable") + assert branches["stable"] == repo_clone._identify() def test_local_pull_branch(self): target_repo = self.get_empty_repo() source_repo = self.get_clone_repo() - default = Reference( - 'branch', 'default', source_repo.branches['default']) + default = Reference("branch", "default", source_repo.branches["default"]) target_repo._local_pull(source_repo.path, default) target_repo = MercurialRepository(target_repo.path) - assert (target_repo.branches['default'] == - source_repo.branches['default']) + assert target_repo.branches["default"] == source_repo.branches["default"] - stable = Reference('branch', 'stable', source_repo.branches['stable']) + stable = Reference("branch", "stable", source_repo.branches["stable"]) target_repo._local_pull(source_repo.path, stable) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['stable'] == source_repo.branches['stable'] + assert target_repo.branches["stable"] == source_repo.branches["stable"] def test_local_pull_bookmark(self): target_repo = self.get_empty_repo() source_repo = self.get_clone_repo() - commits = list(source_repo.get_commits(branch_name='default')) + commits = list(source_repo.get_commits(branch_name="default")) foo1_id = commits[-5].raw_id - foo1 = Reference('book', 'foo1', foo1_id) + foo1 = Reference("book", "foo1", foo1_id) source_repo._update(foo1_id) - source_repo.bookmark('foo1') + source_repo.bookmark("foo1") foo2_id = commits[-3].raw_id - foo2 = Reference('book', 'foo2', foo2_id) + foo2 = Reference("book", "foo2", foo2_id) source_repo._update(foo2_id) - source_repo.bookmark('foo2') + source_repo.bookmark("foo2") target_repo._local_pull(source_repo.path, foo1) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['default'] == commits[-5].raw_id + assert target_repo.branches["default"] == commits[-5].raw_id target_repo._local_pull(source_repo.path, foo2) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['default'] == commits[-3].raw_id + assert target_repo.branches["default"] == commits[-3].raw_id def test_local_pull_commit(self): target_repo = self.get_empty_repo() source_repo = self.get_clone_repo() - commits = list(source_repo.get_commits(branch_name='default')) + commits = list(source_repo.get_commits(branch_name="default")) commit_id = commits[-5].raw_id - commit = Reference('rev', commit_id, commit_id) + commit = Reference("rev", commit_id, commit_id) target_repo._local_pull(source_repo.path, commit) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['default'] == commit_id + assert target_repo.branches["default"] == commit_id commit_id = commits[-3].raw_id - commit = Reference('rev', commit_id, commit_id) + commit = Reference("rev", commit_id, commit_id) target_repo._local_pull(source_repo.path, commit) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['default'] == commit_id + assert target_repo.branches["default"] == commit_id def test_local_pull_from_same_repo(self): - reference = Reference('branch', 'default', None) + reference = Reference("branch", "default", "") with pytest.raises(ValueError): self.repo._local_pull(self.repo.path, reference) - def test_validate_pull_reference_raises_on_missing_reference( - self, vcsbackend_hg): + def test_validate_pull_reference_raises_on_missing_reference(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) - reference = Reference( - 'book', 'invalid_reference', 'a' * 40) + reference = Reference("book", "invalid_reference", "a" * 40) with pytest.raises(CommitDoesNotExistError): target_repo._validate_pull_reference(reference) @@ -387,51 +388,48 @@ TODO: To be written... assert set(self.repo._heads()) == set(self.repo.branches.values()) def test_ancestor(self): - commits = [ - c.raw_id for c in self.repo.get_commits(branch_name='default')] + commits = [c.raw_id for c in self.repo.get_commits(branch_name="default")] assert self.repo._ancestor(commits[-3], commits[-5]) == commits[-5] assert self.repo._ancestor(commits[-5], commits[-3]) == commits[-5] def test_local_push(self): target_repo = self.get_empty_repo() - revisions = list(self.repo.get_commits(branch_name='default')) + revisions = list(self.repo.get_commits(branch_name="default")) revision = revisions[-5].raw_id self.repo._local_push(revision, target_repo.path) target_repo = MercurialRepository(target_repo.path) - assert target_repo.branches['default'] == revision + assert target_repo.branches["default"] == revision def test_hooks_can_be_enabled_for_local_push(self): - revision = 'deadbeef' - repo_path = 'test_group/test_repo' - with mock.patch.object(self.repo, '_remote') as remote_mock: + revision = "deadbeef" + repo_path = "test_group/test_repo" + with mock.patch.object(self.repo, "_remote") as remote_mock: self.repo._local_push(revision, repo_path, enable_hooks=True) - remote_mock.push.assert_called_once_with( - [revision], repo_path, hooks=True, push_branches=False) + remote_mock.push.assert_called_once_with([revision], repo_path, hooks=True, push_branches=False) def test_local_merge(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) - vcsbackend_hg.add_file(target_repo, b'README_MERGE1', b'Version 1') + vcsbackend_hg.add_file(target_repo, b"README_MERGE1", b"Version 1") target_repo = MercurialRepository(target_repo.path) - target_rev = target_repo.branches['default'] - target_ref = Reference( - type='branch', name='default', commit_id=target_rev) - vcsbackend_hg.add_file(source_repo, b'README_MERGE2', b'Version 2') + target_rev = target_repo.branches["default"] + target_ref = Reference(type="branch", name="default", commit_id=target_rev) + vcsbackend_hg.add_file(source_repo, b"README_MERGE2", b"Version 2") source_repo = MercurialRepository(source_repo.path) - source_rev = source_repo.branches['default'] - source_ref = Reference( - type='branch', name='default', commit_id=source_rev) + source_rev = source_repo.branches["default"] + source_ref = Reference(type="branch", name="default", commit_id=source_rev) target_repo._local_pull(source_repo.path, source_ref) - merge_message = 'Merge message\n\nDescription:...' - user_name = 'Albert Einstein' - user_email = 'albert@einstein.com' + merge_message = "Merge message\n\nDescription:..." + user_name = "Albert Einstein" + user_email = "albert@einstein.com" merge_commit_id, needs_push = target_repo._local_merge( - target_ref, merge_message, user_name, user_email, source_ref) + target_ref, merge_message, user_name, user_email, source_ref + ) assert needs_push target_repo = MercurialRepository(target_repo.path) @@ -439,30 +437,28 @@ TODO: To be written... assert target_repo.commit_ids[-2] == source_rev last_commit = target_repo.get_commit(merge_commit_id) assert last_commit.message.strip() == merge_message - assert last_commit.author == '%s <%s>' % (user_name, user_email) + assert last_commit.author == "%s <%s>" % (user_name, user_email) - assert not os.path.exists( - os.path.join(target_repo.path, '.hg', 'merge', 'state')) + assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state")) def test_local_merge_source_is_fast_forward(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) - target_rev = target_repo.branches['default'] - target_ref = Reference( - type='branch', name='default', commit_id=target_rev) - vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2') + target_rev = target_repo.branches["default"] + target_ref = Reference(type="branch", name="default", commit_id=target_rev) + vcsbackend_hg.add_file(source_repo, "README_MERGE2", "Version 2") source_repo = MercurialRepository(source_repo.path) - source_rev = source_repo.branches['default'] - source_ref = Reference( - type='branch', name='default', commit_id=source_rev) + source_rev = source_repo.branches["default"] + source_ref = Reference(type="branch", name="default", commit_id=source_rev) target_repo._local_pull(source_repo.path, source_ref) - merge_message = 'Merge message\n\nDescription:...' - user_name = 'Albert Einstein' - user_email = 'albert@einstein.com' + merge_message = "Merge message\n\nDescription:..." + user_name = "Albert Einstein" + user_email = "albert@einstein.com" merge_commit_id, needs_push = target_repo._local_merge( - target_ref, merge_message, user_name, user_email, source_ref) + target_ref, merge_message, user_name, user_email, source_ref + ) assert merge_commit_id == source_rev assert needs_push @@ -470,70 +466,62 @@ TODO: To be written... assert target_repo.commit_ids[-2] == target_rev assert target_repo.commit_ids[-1] == source_rev - assert not os.path.exists( - os.path.join(target_repo.path, '.hg', 'merge', 'state')) + assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state")) def test_local_merge_source_is_integrated(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) - target_rev = target_repo.branches['default'] - target_ref = Reference( - type='branch', name='default', commit_id=target_rev) + target_rev = target_repo.branches["default"] + target_ref = Reference(type="branch", name="default", commit_id=target_rev) - merge_message = 'Merge message\n\nDescription:...' - user_name = 'Albert Einstein' - user_email = 'albert@einstein.com' + merge_message = "Merge message\n\nDescription:..." + user_name = "Albert Einstein" + user_email = "albert@einstein.com" merge_commit_id, needs_push = target_repo._local_merge( - target_ref, merge_message, user_name, user_email, target_ref) + target_ref, merge_message, user_name, user_email, target_ref + ) assert merge_commit_id == target_rev assert not needs_push target_repo = MercurialRepository(target_repo.path) assert target_repo.commit_ids[-1] == target_rev - assert not os.path.exists( - os.path.join(target_repo.path, '.hg', 'merge', 'state')) + assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state")) def test_local_merge_raises_exception_on_conflict(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) - vcsbackend_hg.add_file(target_repo, 'README_MERGE', 'Version 1') + vcsbackend_hg.add_file(target_repo, "README_MERGE", "Version 1") target_repo = MercurialRepository(target_repo.path) - target_rev = target_repo.branches['default'] - target_ref = Reference( - type='branch', name='default', commit_id=target_rev) - vcsbackend_hg.add_file(source_repo, 'README_MERGE', 'Version 2') + target_rev = target_repo.branches["default"] + target_ref = Reference(type="branch", name="default", commit_id=target_rev) + vcsbackend_hg.add_file(source_repo, "README_MERGE", "Version 2") source_repo = MercurialRepository(source_repo.path) - source_rev = source_repo.branches['default'] - source_ref = Reference( - type='branch', name='default', commit_id=source_rev) + source_rev = source_repo.branches["default"] + source_ref = Reference(type="branch", name="default", commit_id=source_rev) target_repo._local_pull(source_repo.path, source_ref) with pytest.raises(RepositoryError): - target_repo._local_merge( - target_ref, 'merge_message', 'user name', 'user@name.com', - source_ref) + target_repo._local_merge(target_ref, "merge_message", "user name", "user@name.com", source_ref) # Check we are not left in an intermediate merge state - assert not os.path.exists( - os.path.join(target_repo.path, '.hg', 'merge', 'state')) + assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state")) - def test_local_merge_of_two_branches_of_the_same_repo(self, backend_hg): + def test_local_merge_of_two_branches_of_the_same_repo(self, backend_hg, vcs_repo): commits = [ - {'message': 'a'}, - {'message': 'b', 'branch': 'b'}, + {"message": "a"}, + {"message": "b", "branch": "b"}, ] repo = backend_hg.create_repo(commits) commit_ids = backend_hg.commit_ids - target_ref = Reference( - type='branch', name='default', commit_id=commit_ids['a']) - source_ref = Reference( - type='branch', name='b', commit_id=commit_ids['b']) - merge_message = 'Merge message\n\nDescription:...' - user_name = 'Albert Einstein' - user_email = 'albert@einstein.com' + target_ref = Reference(type="branch", name="default", commit_id=commit_ids["a"]) + source_ref = Reference(type="branch", name="b", commit_id=commit_ids["b"]) + merge_message = "Merge message\n\nDescription:..." + user_name = "Albert Einstein" + user_email = "albert@einstein.com" vcs_repo = repo.scm_instance() merge_commit_id, needs_push = vcs_repo._local_merge( - target_ref, merge_message, user_name, user_email, source_ref) + target_ref, merge_message, user_name, user_email, source_ref + ) assert merge_commit_id != source_ref.commit_id assert needs_push is True commit = vcs_repo.get_commit(merge_commit_id) @@ -541,63 +529,62 @@ TODO: To be written... assert commit.message == merge_message def test_maybe_prepare_merge_workspace(self): - workspace = self.repo._maybe_prepare_merge_workspace( - 1, 'pr2', 'unused', 'unused2') + workspace = self.repo._maybe_prepare_merge_workspace(1, "pr2", "unused", "unused2") assert os.path.isdir(workspace) workspace_repo = MercurialRepository(workspace) assert workspace_repo.branches == self.repo.branches # Calling it a second time should also succeed - workspace = self.repo._maybe_prepare_merge_workspace( - 1, 'pr2', 'unused', 'unused2') + workspace = self.repo._maybe_prepare_merge_workspace(1, "pr2", "unused", "unused2") assert os.path.isdir(workspace) def test_cleanup_merge_workspace(self): - workspace = self.repo._maybe_prepare_merge_workspace( - 1, 'pr3', 'unused', 'unused2') + workspace = self.repo._maybe_prepare_merge_workspace(1, "pr3", "unused", "unused2") assert os.path.isdir(workspace) - self.repo.cleanup_merge_workspace(1, 'pr3') + self.repo.cleanup_merge_workspace(1, "pr3") assert not os.path.exists(workspace) def test_cleanup_merge_workspace_invalid_workspace_id(self): # No assert: because in case of an inexistent workspace this function # should still succeed. - self.repo.cleanup_merge_workspace(1, 'pr4') + self.repo.cleanup_merge_workspace(1, "pr4") def test_merge_target_is_bookmark(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) - vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1') - vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2') + vcsbackend_hg.add_file(target_repo, "README_MERGE1", "Version 1") + vcsbackend_hg.add_file(source_repo, "README_MERGE2", "Version 2") imc = source_repo.in_memory_commit - imc.add(FileNode(b'file_x', content=source_repo.name)) - imc.commit( - message='Automatic commit from repo merge test', - author='Automatic ') + imc.add(FileNode(b"file_x", content=source_repo.name)) + imc.commit(message="Automatic commit from repo merge test", author="Automatic ") target_commit = target_repo.get_commit() source_commit = source_repo.get_commit() default_branch = target_repo.DEFAULT_BRANCH_NAME - bookmark_name = 'bookmark' + bookmark_name = "bookmark" target_repo._update(default_branch) target_repo.bookmark(bookmark_name) - target_ref = Reference('book', bookmark_name, target_commit.raw_id) - source_ref = Reference('branch', default_branch, source_commit.raw_id) - workspace_id = 'test-merge' + target_ref = Reference("book", bookmark_name, target_commit.raw_id) + source_ref = Reference("branch", default_branch, source_commit.raw_id) + workspace_id = "test-merge" repo_id = repo_id_generator(target_repo.path) merge_response = target_repo.merge( - repo_id, workspace_id, target_ref, source_repo, source_ref, - 'test user', 'test@rhodecode.com', 'merge message 1', - dry_run=False) - expected_merge_response = MergeResponse( - True, True, merge_response.merge_ref, - MergeFailureReason.NONE) + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + "test user", + "test@rhodecode.com", + "merge message 1", + dry_run=False, + ) + expected_merge_response = MergeResponse(True, True, merge_response.merge_ref, MergeFailureReason.NONE) assert merge_response == expected_merge_response - target_repo = backends.get_backend(vcsbackend_hg.alias)( - target_repo.path) + target_repo = backends.get_backend(vcsbackend_hg.alias)(target_repo.path) target_commits = list(target_repo.get_commits()) commit_ids = [c.raw_id for c in target_commits[:-1]] assert source_ref.commit_id in commit_ids @@ -605,43 +592,43 @@ TODO: To be written... merge_commit = target_commits[-1] assert merge_commit.raw_id == merge_response.merge_ref.commit_id - assert merge_commit.message.strip() == 'merge message 1' - assert merge_commit.author == 'test user ' + assert merge_commit.message.strip() == "merge message 1" + assert merge_commit.author == "test user " # Check the bookmark was updated in the target repo - assert ( - target_repo.bookmarks[bookmark_name] == - merge_response.merge_ref.commit_id) + assert target_repo.bookmarks[bookmark_name] == merge_response.merge_ref.commit_id def test_merge_source_is_bookmark(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) imc = source_repo.in_memory_commit - imc.add(FileNode(b'file_x', content=source_repo.name)) - imc.commit( - message='Automatic commit from repo merge test', - author='Automatic ') + imc.add(FileNode(b"file_x", content=source_repo.name)) + imc.commit(message="Automatic commit from repo merge test", author="Automatic ") target_commit = target_repo.get_commit() source_commit = source_repo.get_commit() default_branch = target_repo.DEFAULT_BRANCH_NAME - bookmark_name = 'bookmark' - target_ref = Reference('branch', default_branch, target_commit.raw_id) + bookmark_name = "bookmark" + target_ref = Reference("branch", default_branch, target_commit.raw_id) source_repo._update(default_branch) source_repo.bookmark(bookmark_name) - source_ref = Reference('book', bookmark_name, source_commit.raw_id) - workspace_id = 'test-merge' + source_ref = Reference("book", bookmark_name, source_commit.raw_id) + workspace_id = "test-merge" repo_id = repo_id_generator(target_repo.path) merge_response = target_repo.merge( - repo_id, workspace_id, target_ref, source_repo, source_ref, - 'test user', 'test@rhodecode.com', 'merge message 1', - dry_run=False) - expected_merge_response = MergeResponse( - True, True, merge_response.merge_ref, - MergeFailureReason.NONE) + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + "test user", + "test@rhodecode.com", + "merge message 1", + dry_run=False, + ) + expected_merge_response = MergeResponse(True, True, merge_response.merge_ref, MergeFailureReason.NONE) assert merge_response == expected_merge_response - target_repo = backends.get_backend(vcsbackend_hg.alias)( - target_repo.path) + target_repo = backends.get_backend(vcsbackend_hg.alias)(target_repo.path) target_commits = list(target_repo.get_commits()) commit_ids = [c.raw_id for c in target_commits] assert source_ref.commit_id == commit_ids[-1] @@ -650,78 +637,89 @@ TODO: To be written... def test_merge_target_has_multiple_heads(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=2) source_repo = vcsbackend_hg.clone_repo(target_repo) - vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1') - vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2') + vcsbackend_hg.add_file(target_repo, "README_MERGE1", "Version 1") + vcsbackend_hg.add_file(source_repo, "README_MERGE2", "Version 2") # add an extra head to the target repo imc = target_repo.in_memory_commit - imc.add(FileNode(b'file_x', content='foo')) + imc.add(FileNode(b"file_x", content=b"foo")) commits = list(target_repo.get_commits()) imc.commit( - message='Automatic commit from repo merge test', - author='Automatic ', parents=commits[0:1]) + message="Automatic commit from repo merge test", + author="Automatic ", + parents=commits[0:1], + ) target_commit = target_repo.get_commit() source_commit = source_repo.get_commit() default_branch = target_repo.DEFAULT_BRANCH_NAME target_repo._update(default_branch) - target_ref = Reference('branch', default_branch, target_commit.raw_id) - source_ref = Reference('branch', default_branch, source_commit.raw_id) - workspace_id = 'test-merge' + target_ref = Reference("branch", default_branch, target_commit.raw_id) + source_ref = Reference("branch", default_branch, source_commit.raw_id) + workspace_id = "test-merge" - assert len(target_repo._heads(branch='default')) == 2 - heads = target_repo._heads(branch='default') + assert len(target_repo._heads(branch="default")) == 2 + heads = target_repo._heads(branch="default") expected_merge_response = MergeResponse( - False, False, None, - MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, - metadata={'heads': heads}) + False, False, None, MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, metadata={"heads": heads} + ) repo_id = repo_id_generator(target_repo.path) merge_response = target_repo.merge( - repo_id, workspace_id, target_ref, source_repo, source_ref, - 'test user', 'test@rhodecode.com', 'merge message 1', - dry_run=False) + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + "test user", + "test@rhodecode.com", + "merge message 1", + dry_run=False, + ) assert merge_response == expected_merge_response def test_merge_rebase_source_is_updated_bookmark(self, vcsbackend_hg): target_repo = vcsbackend_hg.create_repo(number_of_commits=1) source_repo = vcsbackend_hg.clone_repo(target_repo) - vcsbackend_hg.add_file(target_repo, b'README_MERGE1', b'Version 1') - vcsbackend_hg.add_file(source_repo, b'README_MERGE2', b'Version 2') + vcsbackend_hg.add_file(target_repo, b"README_MERGE1", b"Version 1") + vcsbackend_hg.add_file(source_repo, b"README_MERGE2", b"Version 2") imc = source_repo.in_memory_commit - imc.add(FileNode(b'file_x', content=safe_bytes(source_repo.name))) - imc.commit( - message='Automatic commit from repo merge test', - author='Automatic ') + imc.add(FileNode(b"file_x", content=safe_bytes(source_repo.name))) + imc.commit(message="Automatic commit from repo merge test", author="Automatic ") target_commit = target_repo.get_commit() source_commit = source_repo.get_commit() - vcsbackend_hg.add_file(source_repo, b'LICENSE', b'LICENSE Info') + vcsbackend_hg.add_file(source_repo, b"LICENSE", b"LICENSE Info") default_branch = target_repo.DEFAULT_BRANCH_NAME - bookmark_name = 'bookmark' + bookmark_name = "bookmark" source_repo._update(default_branch) source_repo.bookmark(bookmark_name) - target_ref = Reference('branch', default_branch, target_commit.raw_id) - source_ref = Reference('book', bookmark_name, source_commit.raw_id) + target_ref = Reference("branch", default_branch, target_commit.raw_id) + source_ref = Reference("book", bookmark_name, source_commit.raw_id) repo_id = repo_id_generator(target_repo.path) - workspace_id = 'test-merge' + workspace_id = "test-merge" merge_response = target_repo.merge( - repo_id, workspace_id, target_ref, source_repo, source_ref, - 'test user', 'test@rhodecode.com', 'merge message 1', - dry_run=False, use_rebase=True) + repo_id, + workspace_id, + target_ref, + source_repo, + source_ref, + "test user", + "test@rhodecode.com", + "merge message 1", + dry_run=False, + use_rebase=True, + ) - expected_merge_response = MergeResponse( - True, True, merge_response.merge_ref, - MergeFailureReason.NONE) + expected_merge_response = MergeResponse(True, True, merge_response.merge_ref, MergeFailureReason.NONE) assert merge_response == expected_merge_response - target_repo = backends.get_backend(vcsbackend_hg.alias)( - target_repo.path) + target_repo = backends.get_backend(vcsbackend_hg.alias)(target_repo.path) last_commit = target_repo.get_commit() assert last_commit.message == source_commit.message assert last_commit.author == source_commit.author @@ -733,29 +731,33 @@ TODO: To be written... assert len(target_repo.commit_ids) == 2 + 2 -class TestGetShadowInstance(object): - +class TestGetShadowInstance: @pytest.fixture() - def repo(self, vcsbackend_hg, monkeypatch): - repo = vcsbackend_hg.repo - monkeypatch.setattr(repo, 'config', mock.Mock()) - monkeypatch.setattr('rhodecode.lib.vcs.connection.Hg', mock.Mock()) - return repo + def repo(self, vcsbackend_hg): + _hg_repo = vcsbackend_hg.repo + connection_mock = mock.Mock(unsafe=True, name="connection.Hg") + mock.patch("rhodecode.lib.vcs.connection.Hg", connection_mock) + return _hg_repo - def test_passes_config(self, repo): + def test_getting_shadow_instance_copies_config(self, repo): shadow = repo.get_shadow_instance(repo.path) - assert shadow.config == repo.config.copy() + assert shadow.config.serialize() == repo.config.serialize() - def test_disables_hooks(self, repo): + def test_disables_hooks_section(self, repo): + repo.config.set("hooks", "foo", "val") shadow = repo.get_shadow_instance(repo.path) - shadow.config.clear_section.assert_called_once_with('hooks') + assert not shadow.config.items("hooks") def test_allows_to_keep_hooks(self, repo): + repo.config.set("hooks", "foo", "val") shadow = repo.get_shadow_instance(repo.path, enable_hooks=True) - assert not shadow.config.clear_section.called + assert shadow.config.items("hooks") -class TestMercurialCommit(object): +class TestMercurialCommit: + @pytest.fixture(autouse=True) + def prepare(self): + self.repo = MercurialRepository(TEST_HG_REPO) def _test_equality(self, commit): idx = commit.idx @@ -768,15 +770,15 @@ class TestMercurialCommit(object): self._test_equality(commit) def test_default_commit(self): - tip = self.repo.get_commit('tip') + tip = self.repo.get_commit("tip") assert tip == self.repo.get_commit() assert tip == self.repo.get_commit(commit_id=None) assert tip == self.repo.get_commit(commit_idx=None) assert tip == list(self.repo[-1:])[0] def test_root_node(self): - tip = self.repo.get_commit('tip') - assert tip.root is tip.get_node('') + tip = self.repo.get_commit("tip") + assert tip.root is tip.get_node(b"") def test_lazy_fetch(self): """ @@ -792,44 +794,43 @@ class TestMercurialCommit(object): # accessing root.nodes updates commit.nodes assert len(commit.nodes) == 9 - docs = root.get_node('docs') + docs = commit.get_node(b"docs") # we haven't yet accessed anything new as docs dir was already cached assert len(commit.nodes) == 9 assert len(docs.nodes) == 8 # accessing docs.nodes updates commit.nodes assert len(commit.nodes) == 17 - assert docs is commit.get_node('docs') + assert docs is commit.get_node(b"docs") assert docs is root.nodes[0] assert docs is root.dirs[0] - assert docs is commit.get_node('docs') + assert docs is commit.get_node(b"docs") def test_nodes_with_commit(self): commit = self.repo.get_commit(commit_idx=45) root = commit.root - docs = root.get_node('docs') - assert docs is commit.get_node('docs') - api = docs.get_node('api') - assert api is commit.get_node('docs/api') - index = api.get_node('index.rst') - assert index is commit.get_node('docs/api/index.rst') - assert index is commit.get_node( - 'docs').get_node('api').get_node('index.rst') + assert isinstance(root, RootNode) + docs = commit.get_node(b"docs") + assert docs is commit.get_node(b"docs") + api = commit.get_node(b"docs/api") + assert api is commit.get_node(b"docs/api") + index = commit.get_node(b"docs/api/index.rst") + assert index is commit.get_node(b"docs/api/index.rst") def test_branch_and_tags(self): commit0 = self.repo.get_commit(commit_idx=0) - assert commit0.branch == 'default' + assert commit0.branch == "default" assert commit0.tags == [] commit10 = self.repo.get_commit(commit_idx=10) - assert commit10.branch == 'default' + assert commit10.branch == "default" assert commit10.tags == [] commit44 = self.repo.get_commit(commit_idx=44) - assert commit44.branch == 'web' + assert commit44.branch == "web" - tip = self.repo.get_commit('tip') - assert 'tip' in tip.tags + tip = self.repo.get_commit("tip") + assert "tip" in tip.tags def test_bookmarks(self): commit0 = self.repo.get_commit(commit_idx=0) @@ -842,46 +843,84 @@ class TestMercurialCommit(object): def test_file_size(self): to_check = ( - (10, 'setup.py', 1068), - (20, 'setup.py', 1106), - (60, 'setup.py', 1074), - - (10, 'vcs/backends/base.py', 2921), - (20, 'vcs/backends/base.py', 3936), - (60, 'vcs/backends/base.py', 6189), + (10, b"setup.py", 1068), + (20, b"setup.py", 1106), + (60, b"setup.py", 1074), + (10, b"vcs/backends/base.py", 2921), + (20, b"vcs/backends/base.py", 3936), + (60, b"vcs/backends/base.py", 6189), ) for idx, path, size in to_check: self._test_file_size(idx, path, size) def test_file_history_from_commits(self): - node = self.repo[10].get_node('setup.py') + node = self.repo[10].get_node(b"setup.py") commit_ids = [commit.raw_id for commit in node.history] - assert ['3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == commit_ids + assert ["3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == commit_ids - node = self.repo[20].get_node('setup.py') + node = self.repo[20].get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['eada5a770da98ab0dd7325e29d00e0714f228d09', - '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == node_ids + assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids # special case we check history from commit that has this particular # file changed this means we check if it's included as well - node = self.repo.get_commit('eada5a770da98ab0dd7325e29d00e0714f228d09')\ - .get_node('setup.py') + node = self.repo.get_commit("eada5a770da98ab0dd7325e29d00e0714f228d09").get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['eada5a770da98ab0dd7325e29d00e0714f228d09', - '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'] == node_ids + assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids def test_file_history(self): # we can only check if those commits are present in the history # as we cannot update this test every time file is changed files = { - 'setup.py': [7, 18, 45, 46, 47, 69, 77], - 'vcs/nodes.py': [ - 7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60, 61, 73, 76], - 'vcs/backends/hg.py': [ - 4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23, 26, 27, 28, 30, - 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47, 48, 49, 53, 54, - 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79, 82], + b"setup.py": [7, 18, 45, 46, 47, 69, 77], + b"vcs/nodes.py": [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60, 61, 73, 76], + b"vcs/backends/hg.py": [ + 4, + 5, + 6, + 11, + 12, + 13, + 14, + 15, + 16, + 21, + 22, + 23, + 26, + 27, + 28, + 30, + 31, + 33, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 44, + 45, + 47, + 48, + 49, + 53, + 54, + 55, + 58, + 60, + 61, + 67, + 68, + 69, + 70, + 73, + 77, + 78, + 79, + 82, + ], } for path, indexes in files.items(): tip = self.repo.get_commit(commit_idx=indexes[-1]) @@ -889,87 +928,120 @@ class TestMercurialCommit(object): node_indexes = [commit.idx for commit in node.history] assert set(indexes).issubset(set(node_indexes)), ( "We assumed that %s is subset of commits for which file %s " - "has been changed, and history of that node returned: %s" - % (indexes, path, node_indexes)) + "has been changed, and history of that node returned: %s" % (indexes, path, node_indexes) + ) def test_file_annotate(self): files = { - 'vcs/backends/__init__.py': { + b"vcs/backends/__init__.py": { 89: { - 'lines_no': 31, - 'commits': [ - 32, 32, 61, 32, 32, 37, 32, 32, 32, 44, - 37, 37, 37, 37, 45, 37, 44, 37, 37, 37, - 32, 32, 32, 32, 37, 32, 37, 37, 32, - 32, 32 - ] + "lines_no": 31, + "commits": [ + 32, + 32, + 61, + 32, + 32, + 37, + 32, + 32, + 32, + 44, + 37, + 37, + 37, + 37, + 45, + 37, + 44, + 37, + 37, + 37, + 32, + 32, + 32, + 32, + 37, + 32, + 37, + 37, + 32, + 32, + 32, + ], }, - 20: { - 'lines_no': 1, - 'commits': [4] - }, + 20: {"lines_no": 1, "commits": [4]}, 55: { - 'lines_no': 31, - 'commits': [ - 32, 32, 45, 32, 32, 37, 32, 32, 32, 44, - 37, 37, 37, 37, 45, 37, 44, 37, 37, 37, - 32, 32, 32, 32, 37, 32, 37, 37, 32, - 32, 32 - ] - } + "lines_no": 31, + "commits": [ + 32, + 32, + 45, + 32, + 32, + 37, + 32, + 32, + 32, + 44, + 37, + 37, + 37, + 37, + 45, + 37, + 44, + 37, + 37, + 37, + 32, + 32, + 32, + 32, + 37, + 32, + 37, + 37, + 32, + 32, + 32, + ], + }, }, - 'vcs/exceptions.py': { + b"vcs/exceptions.py": { 89: { - 'lines_no': 18, - 'commits': [ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, - 16, 16, 17, 16, 16, 18, 18, 18 - ] + "lines_no": 18, + "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18], }, 20: { - 'lines_no': 18, - 'commits': [ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, - 16, 16, 17, 16, 16, 18, 18, 18 - ] + "lines_no": 18, + "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18], }, 55: { - 'lines_no': 18, - 'commits': [ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, - 17, 16, 16, 18, 18, 18 - ] - } + "lines_no": 18, + "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18], + }, }, - 'MANIFEST.in': { - 89: { - 'lines_no': 5, - 'commits': [7, 7, 7, 71, 71] - }, - 20: { - 'lines_no': 3, - 'commits': [7, 7, 7] - }, - 55: { - 'lines_no': 3, - 'commits': [7, 7, 7] - } - } + b"MANIFEST.in": { + 89: {"lines_no": 5, "commits": [7, 7, 7, 71, 71]}, + 20: {"lines_no": 3, "commits": [7, 7, 7]}, + 55: {"lines_no": 3, "commits": [7, 7, 7]}, + }, } - for fname, commit_dict in files.items(): + for file_name, commit_dict in files.items(): for idx, __ in commit_dict.items(): commit = self.repo.get_commit(commit_idx=idx) - l1_1 = [x[1] for x in commit.get_file_annotate(fname)] - l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)] + l1_1 = [x[1] for x in commit.get_file_annotate(file_name)] + l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(file_name)] assert l1_1 == l1_2 - l1 = l1_2 = [ - x[2]().idx for x in commit.get_file_annotate(fname)] - l2 = files[fname][idx]['commits'] + l1 = l1_2 = [x[2]().idx for x in commit.get_file_annotate(file_name)] + l2 = files[file_name][idx]["commits"] assert l1 == l2, ( "The lists of commit for %s@commit_id%s" "from annotation list should match each other," - "got \n%s \nvs \n%s " % (fname, idx, l1, l2)) + "got \n%s \nvs \n%s " % (file_name, idx, l1, l2) + ) def test_commit_state(self): """ @@ -981,42 +1053,61 @@ class TestMercurialCommit(object): # changed: 13 # added: 20 # removed: 1 - changed = set([ - '.hgignore', 'README.rst', 'docs/conf.py', 'docs/index.rst', - 'setup.py', 'tests/test_hg.py', 'tests/test_nodes.py', - 'vcs/__init__.py', 'vcs/backends/__init__.py', - 'vcs/backends/base.py', 'vcs/backends/hg.py', 'vcs/nodes.py', - 'vcs/utils/__init__.py']) + changed = { + b".hgignore", + b"README.rst", + b"docs/conf.py", + b"docs/index.rst", + b"setup.py", + b"tests/test_hg.py", + b"tests/test_nodes.py", + b"vcs/__init__.py", + b"vcs/backends/__init__.py", + b"vcs/backends/base.py", + b"vcs/backends/hg.py", + b"vcs/nodes.py", + b"vcs/utils/__init__.py", + } - added = set([ - 'docs/api/backends/hg.rst', 'docs/api/backends/index.rst', - 'docs/api/index.rst', 'docs/api/nodes.rst', - 'docs/api/web/index.rst', 'docs/api/web/simplevcs.rst', - 'docs/installation.rst', 'docs/quickstart.rst', 'setup.cfg', - 'vcs/utils/baseui_config.py', 'vcs/utils/web.py', - 'vcs/web/__init__.py', 'vcs/web/exceptions.py', - 'vcs/web/simplevcs/__init__.py', 'vcs/web/simplevcs/exceptions.py', - 'vcs/web/simplevcs/middleware.py', 'vcs/web/simplevcs/models.py', - 'vcs/web/simplevcs/settings.py', 'vcs/web/simplevcs/utils.py', - 'vcs/web/simplevcs/views.py']) + added = { + b"docs/api/backends/hg.rst", + b"docs/api/backends/index.rst", + b"docs/api/index.rst", + b"docs/api/nodes.rst", + b"docs/api/web/index.rst", + b"docs/api/web/simplevcs.rst", + b"docs/installation.rst", + b"docs/quickstart.rst", + b"setup.cfg", + b"vcs/utils/baseui_config.py", + b"vcs/utils/web.py", + b"vcs/web/__init__.py", + b"vcs/web/exceptions.py", + b"vcs/web/simplevcs/__init__.py", + b"vcs/web/simplevcs/exceptions.py", + b"vcs/web/simplevcs/middleware.py", + b"vcs/web/simplevcs/models.py", + b"vcs/web/simplevcs/settings.py", + b"vcs/web/simplevcs/utils.py", + b"vcs/web/simplevcs/views.py", + } - removed = set(['docs/api.rst']) + removed = {b"docs/api.rst"} - commit64 = self.repo.get_commit('46ad32a4f974') - assert set((node.path for node in commit64.added)) == added - assert set((node.path for node in commit64.changed)) == changed - assert set((node.path for node in commit64.removed)) == removed + commit64 = self.repo.get_commit("46ad32a4f974") + assert set((node for node in commit64.added_paths)) == added + assert set((node for node in commit64.changed_paths)) == changed + assert set((node for node in commit64.removed_paths)) == removed # commit_id b090f22d27d6: # hg st --rev b090f22d27d6 # changed: 13 # added: 20 # removed: 1 - commit88 = self.repo.get_commit('b090f22d27d6') - assert set((node.path for node in commit88.added)) == set() - assert set((node.path for node in commit88.changed)) == \ - set(['.hgignore']) - assert set((node.path for node in commit88.removed)) == set() + commit88 = self.repo.get_commit("b090f22d27d6") + assert set((node for node in commit88.added_paths)) == set() + assert set((node for node in commit88.changed_paths)) == {b".hgignore"} + assert set((node for node in commit88.removed_paths)) == set() # # 85: @@ -1025,54 +1116,40 @@ class TestMercurialCommit(object): # changed: 4 ['vcs/web/simplevcs/models.py', ...] # removed: 1 ['vcs/utils/web.py'] commit85 = self.repo.get_commit(commit_idx=85) - assert set((node.path for node in commit85.added)) == set([ - 'vcs/utils/diffs.py', - 'vcs/web/simplevcs/views/diffs.py']) - assert set((node.path for node in commit85.changed)) == set([ - 'vcs/web/simplevcs/models.py', - 'vcs/web/simplevcs/utils.py', - 'vcs/web/simplevcs/views/__init__.py', - 'vcs/web/simplevcs/views/repository.py', - ]) - assert set((node.path for node in commit85.removed)) == \ - set(['vcs/utils/web.py']) + assert set((node for node in commit85.added_paths)) == {b"vcs/utils/diffs.py", b"vcs/web/simplevcs/views/diffs.py"} + assert set((node for node in commit85.changed_paths)) == { + b"vcs/web/simplevcs/models.py", + b"vcs/web/simplevcs/utils.py", + b"vcs/web/simplevcs/views/__init__.py", + b"vcs/web/simplevcs/views/repository.py", + } + assert set((node for node in commit85.removed_paths)) == {b"vcs/utils/web.py"} def test_files_state(self): """ Tests state of FileNodes. """ commit = self.repo.get_commit(commit_idx=85) - node = commit.get_node('vcs/utils/diffs.py') - assert node.state, NodeState.ADDED - assert node.added - assert not node.changed - assert not node.not_changed - assert not node.removed + node = commit.get_node(b"vcs/utils/diffs.py") + assert node.bytes_path in commit.added_paths commit = self.repo.get_commit(commit_idx=88) - node = commit.get_node('.hgignore') - assert node.state, NodeState.CHANGED - assert not node.added - assert node.changed - assert not node.not_changed - assert not node.removed + node = commit.get_node(b".hgignore") + assert node.bytes_path in commit.changed_paths commit = self.repo.get_commit(commit_idx=85) - node = commit.get_node('setup.py') - assert node.state, NodeState.NOT_CHANGED - assert not node.added - assert not node.changed - assert node.not_changed - assert not node.removed + node = commit.get_node(b"setup.py") + assert node.bytes_path not in commit.affected_files # If node has REMOVED state then trying to fetch it would raise # CommitError exception commit = self.repo.get_commit(commit_idx=2) - path = 'vcs/backends/BaseRepository.py' + path = b"vcs/backends/BaseRepository.py" with pytest.raises(NodeDoesNotExistError): commit.get_node(path) + # but it would be one of ``removed`` (commit's attribute) - assert path in [rf.path for rf in commit.removed] + assert path in [rf for rf in commit.removed_paths] def test_commit_message_is_unicode(self): for cm in self.repo: @@ -1084,57 +1161,57 @@ class TestMercurialCommit(object): def test_repo_files_content_type(self): test_commit = self.repo.get_commit(commit_idx=100) - for node in test_commit.get_node('/'): + for node in test_commit.get_node(b"/"): if node.is_file(): assert type(node.content) == bytes assert type(node.str_content) == str def test_wrong_path(self): # There is 'setup.py' in the root dir but not there: - path = 'foo/bar/setup.py' + path = b"foo/bar/setup.py" with pytest.raises(VCSError): self.repo.get_commit().get_node(path) def test_author_email(self): - assert 'marcin@python-blog.com' == \ - self.repo.get_commit('b986218ba1c9').author_email - assert 'lukasz.balcerzak@python-center.pl' == \ - self.repo.get_commit('3803844fdbd3').author_email - assert '' == self.repo.get_commit('84478366594b').author_email + assert "marcin@python-blog.com" == self.repo.get_commit("b986218ba1c9").author_email + assert "lukasz.balcerzak@python-center.pl" == self.repo.get_commit("3803844fdbd3").author_email + assert "" == self.repo.get_commit("84478366594b").author_email def test_author_username(self): - assert 'Marcin Kuzminski' == \ - self.repo.get_commit('b986218ba1c9').author_name - assert 'Lukasz Balcerzak' == \ - self.repo.get_commit('3803844fdbd3').author_name - assert 'marcink' == \ - self.repo.get_commit('84478366594b').author_name + assert "Marcin Kuzminski" == self.repo.get_commit("b986218ba1c9").author_name + assert "Lukasz Balcerzak" == self.repo.get_commit("3803844fdbd3").author_name + assert "marcink" == self.repo.get_commit("84478366594b").author_name -class TestLargeFileRepo(object): - +class TestLargeFileRepo: def test_large_file(self, backend_hg): - repo = backend_hg.create_test_repo('largefiles', make_db_config()) + conf = make_db_config() + hg_largefiles_store = conf.get("largefiles", "usercache") + repo = backend_hg.create_test_repo("largefiles", conf) tip = repo.scm_instance().get_commit() - node = tip.get_node('.hglf/thisfileislarge') + node = tip.get_node(b".hglf/thisfileislarge") lf_node = node.get_largefile_node() assert lf_node.is_largefile() is True assert lf_node.size == 1024000 - assert lf_node.name == '.hglf/thisfileislarge' + assert lf_node.name == b".hglf/thisfileislarge" -class TestGetBranchName(object): +class TestGetBranchName: + @pytest.fixture(autouse=True) + def prepare(self): + self.repo = MercurialRepository(TEST_HG_REPO) + def test_returns_ref_name_when_type_is_branch(self): - ref = self._create_ref('branch', 'fake-name') + ref = self._create_ref("branch", "fake-name") result = self.repo._get_branch_name(ref) assert result == ref.name @pytest.mark.parametrize("type_", ("book", "tag")) def test_queries_remote_when_type_is_not_branch(self, type_): - ref = self._create_ref(type_, 'wrong-fake-name') + ref = self._create_ref(type_, "wrong-fake-name") with mock.patch.object(self.repo, "_remote") as remote_mock: remote_mock.ctx_branch.return_value = "fake-name" result = self.repo._get_branch_name(ref) @@ -1144,17 +1221,20 @@ class TestGetBranchName(object): def _create_ref(self, type_, name): ref = mock.Mock() ref.type = type_ - ref.name = 'wrong-fake-name' + ref.name = "wrong-fake-name" ref.commit_id = "deadbeef" return ref -class TestIsTheSameBranch(object): +class TestIsTheSameBranch: + @pytest.fixture(autouse=True) + def prepare(self): + self.repo = MercurialRepository(TEST_HG_REPO) + def test_returns_true_when_branches_are_equal(self): source_ref = mock.Mock(name="source-ref") target_ref = mock.Mock(name="target-ref") - branch_name_patcher = mock.patch.object( - self.repo, "_get_branch_name", return_value="default") + branch_name_patcher = mock.patch.object(self.repo, "_get_branch_name", return_value="default") with branch_name_patcher as branch_name_mock: result = self.repo._is_the_same_branch(source_ref, target_ref) @@ -1171,8 +1251,7 @@ class TestIsTheSameBranch(object): def side_effect(ref): return ref.name - branch_name_patcher = mock.patch.object( - self.repo, "_get_branch_name", side_effect=side_effect) + branch_name_patcher = mock.patch.object(self.repo, "_get_branch_name", side_effect=side_effect) with branch_name_patcher as branch_name_mock: result = self.repo._is_the_same_branch(source_ref, target_ref) diff --git a/rhodecode/tests/vcs/test_hg_vcsserver_cache_invalidation.py b/rhodecode/tests/vcs/test_hg_vcsserver_cache_invalidation.py --- a/rhodecode/tests/vcs/test_hg_vcsserver_cache_invalidation.py +++ b/rhodecode/tests/vcs/test_hg_vcsserver_cache_invalidation.py @@ -1,6 +1,4 @@ - - -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -35,45 +33,47 @@ class TestMercurialRemoteRepoInvalidatio """ # Default reference used as a dummy during tests. - default_ref = Reference('branch', 'default', None) + default_ref = Reference("branch", "default", None) # Methods of vcsserver.hg.HgRemote that are "writing" operations. writing_methods = [ - 'bookmark', - 'commit', - 'merge', - 'pull', - 'pull_cmd', - 'rebase', - 'strip', - 'tag', + "bookmark", + "commit", + "merge", + "pull", + "pull_cmd", + "rebase", + "strip", + "tag", ] - @pytest.mark.parametrize('method_name, method_args', [ - ('_local_merge', [default_ref, None, None, None, default_ref]), - ('_local_pull', ['', default_ref]), - ('bookmark', [None]), - ('pull', ['', default_ref]), - ('remove_tag', ['mytag', None]), - ('strip', [None]), - ('tag', ['newtag', None]), - ]) - def test_method_invokes_invalidate_on_remote_repo( - self, method_name, method_args, backend_hg): + @pytest.mark.parametrize( + "method_name, method_args", + [ + ("_local_merge", [default_ref, None, None, None, default_ref]), + ("_local_pull", ["", default_ref]), + ("bookmark", [None]), + ("pull", ["", default_ref]), + ("remove_tag", ["mytag", None]), + ("strip", [None]), + ("tag", ["newtag", None]), + ], + ) + def test_method_invokes_invalidate_on_remote_repo(self, method_name, method_args, backend_hg): """ Check that the listed methods are invalidating the VCSServer cache after invoking a writing method of their remote repository object. """ - tags = {'mytag': 'mytag-id'} + tags = {"mytag": "mytag-id"} def add_tag(name, raw_id, *args, **kwds): tags[name] = raw_id repo = backend_hg.repo.scm_instance() - with patch.object(repo, '_remote') as remote: + with patch.object(repo, "_remote") as remote: repo.tags = tags - remote.lookup.return_value = ('commit-id', 'commit-idx') + remote.lookup.return_value = ("commit-id", "commit-idx") remote.tags.return_value = tags remote._get_tags.return_value = tags remote.is_empty.return_value = False @@ -98,6 +98,7 @@ class TestMercurialRemoteRepoInvalidatio references. """ from rhodecode.model.pull_request import PullRequestModel + repo_id = pull_request.target_repo.repo_id target_vcs = pull_request.target_repo.scm_instance() target_ref = pull_request.target_ref_parts @@ -107,7 +108,8 @@ class TestMercurialRemoteRepoInvalidatio pr = PullRequestModel() workspace_id = pr._workspace_id(pull_request) shadow_repository_path = target_vcs._maybe_prepare_merge_workspace( - repo_id, workspace_id, target_ref, source_ref) + repo_id, workspace_id, target_ref, source_ref + ) shadow_repo = target_vcs.get_shadow_instance(shadow_repository_path, cache=True) # This will populate the cache of the mercurial repository object @@ -116,7 +118,7 @@ class TestMercurialRemoteRepoInvalidatio return shadow_repo, source_ref, target_ref - @pytest.mark.backends('hg') + @pytest.mark.backends("hg") def test_commit_does_not_exist_error_happens(self, pr_util, app): """ This test is somewhat special. It does not really test the system @@ -132,18 +134,18 @@ class TestMercurialRemoteRepoInvalidatio source_vcs = pull_request.source_repo.scm_instance() shadow_repo, source_ref, target_ref = self._prepare_shadow_repo(pull_request) - initial_cache_uid = shadow_repo._remote._wire['context'] - initial_commit_ids = shadow_repo._remote.get_all_commit_ids('visible') + initial_cache_uid = shadow_repo._remote._wire["context"] + initial_commit_ids = shadow_repo._remote.get_all_commit_ids("visible") # Pull from target and source references but without invalidation of # RemoteRepo objects and without VCSServer caching of mercurial repository objects. - with patch.object(shadow_repo._remote, 'invalidate_vcs_cache'): + with patch.object(shadow_repo._remote, "invalidate_vcs_cache"): # NOTE: Do not use patch.dict() to disable the cache because it # restores the WHOLE dict and not only the patched keys. - shadow_repo._remote._wire['cache'] = False + shadow_repo._remote._wire["cache"] = False shadow_repo._local_pull(target_vcs.path, target_ref) shadow_repo._local_pull(source_vcs.path, source_ref) - shadow_repo._remote._wire['cache'] = True + shadow_repo._remote._wire["cache"] = True # Try to lookup the target_ref in shadow repo. This should work because # test_repo_maker_uses_session_for_instance_methods @@ -153,14 +155,14 @@ class TestMercurialRemoteRepoInvalidatio # we ensure that call context has not changed, this is what # `invalidate_vcs_cache` does - assert initial_cache_uid == shadow_repo._remote._wire['context'] + assert initial_cache_uid == shadow_repo._remote._wire["context"] # If we try to lookup all commits. # repo commit cache doesn't get invalidated. (Due to patched # invalidation and caching above). - assert initial_commit_ids == shadow_repo._remote.get_all_commit_ids('visible') + assert initial_commit_ids == shadow_repo._remote.get_all_commit_ids("visible") - @pytest.mark.backends('hg') + @pytest.mark.backends("hg") def test_commit_does_not_exist_error_does_not_happen(self, pr_util, app): """ This test simulates a pull request merge in which the pull operations @@ -179,10 +181,10 @@ class TestMercurialRemoteRepoInvalidatio # of RemoteRepo objects. # NOTE: Do not use patch.dict() to disable the cache because it # restores the WHOLE dict and not only the patched keys. - shadow_repo._remote._wire['cache'] = False + shadow_repo._remote._wire["cache"] = False shadow_repo._local_pull(target_vcs.path, target_ref) shadow_repo._local_pull(source_vcs.path, source_ref) - shadow_repo._remote._wire['cache'] = True + shadow_repo._remote._wire["cache"] = True # Try to lookup the target and source references in shadow repo. This # should work because the RemoteRepo object gets invalidated during the diff --git a/rhodecode/tests/vcs/test_inmemory.py b/rhodecode/tests/vcs/test_inmemory.py --- a/rhodecode/tests/vcs/test_inmemory.py +++ b/rhodecode/tests/vcs/test_inmemory.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,15 +19,21 @@ """ Tests so called "in memory commits" commit API of vcs. """ + import datetime import pytest from rhodecode.lib.str_utils import safe_bytes, safe_str from rhodecode.lib.vcs.exceptions import ( - EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyExistsError, - NodeAlreadyRemovedError, NodeAlreadyChangedError, NodeDoesNotExistError, - NodeNotChangedError) + EmptyRepositoryError, + NodeAlreadyAddedError, + NodeAlreadyExistsError, + NodeAlreadyRemovedError, + NodeAlreadyChangedError, + NodeDoesNotExistError, + NodeNotChangedError, +) from rhodecode.lib.vcs.nodes import DirNode, FileNode from rhodecode.tests.vcs.conftest import BackendTestMixin @@ -36,19 +41,21 @@ from rhodecode.tests.vcs.conftest import @pytest.fixture() def nodes(): nodes = [ - FileNode(b'foobar', content=b'Foo & bar'), - FileNode(b'foobar2', content=b'Foo & bar, doubled!'), - FileNode(b'foo bar with spaces', content=b''), - FileNode(b'foo/bar/baz', content=b'Inside'), - FileNode(b'foo/bar/file.bin', content=( - b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x03\x00\xfe' - b'\xff\t\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x01\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00' - b'\x00\x18\x00\x00\x00\x01\x00\x00\x00\xfe\xff\xff\xff\x00\x00' - b'\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff' - b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' - ) + FileNode(b"foobar", content=b"Foo & bar"), + FileNode(b"foobar2", content=b"Foo & bar, doubled!"), + FileNode(b"foo bar with spaces", content=b""), + FileNode(b"foo/bar/baz", content=b"Inside"), + FileNode( + b"foo/bar/file.bin", + content=( + b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x03\x00\xfe" + b"\xff\t\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00" + b"\x00\x18\x00\x00\x00\x01\x00\x00\x00\xfe\xff\xff\xff\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + ), ), ] return nodes @@ -80,14 +87,14 @@ class TestInMemoryCommit(BackendTestMixi def test_add_on_branch_hg(self, nodes): for node in nodes: self.imc.add(node) - self.commit(branch='stable') + self.commit(branch="stable") self.assert_successful_commit(nodes) @pytest.mark.backends("git") def test_add_on_branch_git(self, nodes): for node in nodes: self.imc.add(node) - self.commit(branch='stable') + self.commit(branch="stable") self.assert_successful_commit(nodes) def test_add_in_bulk(self, nodes): @@ -98,10 +105,8 @@ class TestInMemoryCommit(BackendTestMixi def test_add_non_ascii_files(self): nodes = [ - FileNode(safe_bytes('żółwik/zwierzątko_utf8_str'), - content=safe_bytes('ćććć')), - FileNode(safe_bytes('żółwik/zwierzątko_unicode'), - content=safe_bytes('ćććć')), + FileNode(safe_bytes("żółwik/zwierzątko_utf8_str"), content=safe_bytes("ćććć")), + FileNode(safe_bytes("żółwik/zwierzątko_unicode"), content=safe_bytes("ćććć")), ] for node in nodes: @@ -112,60 +117,56 @@ class TestInMemoryCommit(BackendTestMixi def commit(self, branch=None): self.old_commit_count = len(self.repo.commit_ids) - self.commit_message = 'Test commit with unicode: żółwik' - self.commit_author = f'{self.__class__.__name__} ' - self.commit = self.imc.commit( - message=self.commit_message, author=self.commit_author, - branch=branch) + self.commit_message = "Test commit with unicode: żółwik" + self.commit_author = f"{self.__class__.__name__} " + self.commit = self.imc.commit(message=self.commit_message, author=self.commit_author, branch=branch) def test_add_actually_adds_all_nodes_at_second_commit_too(self): to_add = [ - FileNode(b'foo/bar/image.png', content=b'\0'), - FileNode(b'foo/README.txt', content=b'readme!'), + FileNode(b"foo/bar/image.png", content=b"\0"), + FileNode(b"foo/README.txt", content=b"readme!"), ] self.imc.add(*to_add) - commit = self.imc.commit('Initial', 'joe doe ') - assert isinstance(commit.get_node('foo'), DirNode) - assert isinstance(commit.get_node('foo/bar'), DirNode) + commit = self.imc.commit("Initial", "joe doe ") + assert isinstance(commit.get_node(b"foo"), DirNode) + assert isinstance(commit.get_node(b"foo/bar"), DirNode) self.assert_nodes_in_commit(commit, to_add) # commit some more files again to_add = [ - FileNode(b'foo/bar/foobaz/bar', content=b'foo'), - FileNode(b'foo/bar/another/bar', content=b'foo'), - FileNode(b'foo/baz.txt', content=b'foo'), - FileNode(b'foobar/foobaz/file', content=b'foo'), - FileNode(b'foobar/barbaz', content=b'foo'), + FileNode(b"foo/bar/foobaz/bar", content=b"foo"), + FileNode(b"foo/bar/another/bar", content=b"foo"), + FileNode(b"foo/baz.txt", content=b"foo"), + FileNode(b"foobar/foobaz/file", content=b"foo"), + FileNode(b"foobar/barbaz", content=b"foo"), ] self.imc.add(*to_add) - commit = self.imc.commit('Another', 'joe doe ') + commit = self.imc.commit("Another", "joe doe ") self.assert_nodes_in_commit(commit, to_add) def test_add_raise_already_added(self): - node = FileNode(b'foobar', content=b'baz') + node = FileNode(b"foobar", content=b"baz") self.imc.add(node) with pytest.raises(NodeAlreadyAddedError): self.imc.add(node) def test_check_integrity_raise_already_exist(self): - node = FileNode(b'foobar', content=b'baz') + node = FileNode(b"foobar", content=b"baz") self.imc.add(node) - self.imc.commit(message='Added foobar', - author='Some Name ') + self.imc.commit(message="Added foobar", author="Some Name ") self.imc.add(node) with pytest.raises(NodeAlreadyExistsError): - self.imc.commit(message='new message', - author='Some Name ') + self.imc.commit(message="new message", author="Some Name ") def test_change(self): - self.imc.add(FileNode(b'foo/bar/baz', content=b'foo')) - self.imc.add(FileNode(b'foo/fbar', content=b'foobar')) - tip = self.imc.commit('Initial', 'joe doe ') + self.imc.add(FileNode(b"foo/bar/baz", content=b"foo")) + self.imc.add(FileNode(b"foo/fbar", content=b"foobar")) + tip = self.imc.commit("Initial", "joe doe ") # Change node's content - node = FileNode(b'foo/bar/baz', content=b'My **changed** content') + node = FileNode(b"foo/bar/baz", content=b"My **changed** content") self.imc.change(node) - self.imc.commit('Changed %s' % node.path, 'joe doe ') + self.imc.commit("Changed %s" % node.path, "joe doe ") newtip = self.repo.get_commit() assert tip != newtip @@ -174,28 +175,22 @@ class TestInMemoryCommit(BackendTestMixi def test_change_non_ascii(self): to_add = [ - FileNode(safe_bytes('żółwik/zwierzątko'), - content=safe_bytes('ćććć')), - FileNode(safe_bytes('żółwik/zwierzątko_uni'), - content=safe_bytes('ćććć')), + FileNode(safe_bytes("żółwik/zwierzątko"), content=safe_bytes("ćććć")), + FileNode(safe_bytes("żółwik/zwierzątko_uni"), content=safe_bytes("ćććć")), ] for node in to_add: self.imc.add(node) - tip = self.imc.commit('Initial', 'joe doe ') + tip = self.imc.commit("Initial", "joe doe ") # Change node's content - node = FileNode(safe_bytes('żółwik/zwierzątko'), - content=b'My **changed** content') + node = FileNode(safe_bytes("żółwik/zwierzątko"), content=b"My **changed** content") self.imc.change(node) - self.imc.commit('Changed %s' % safe_str(node.path), - author='joe doe ') + self.imc.commit("Changed %s" % safe_str(node.path), author="joe doe ") - node_uni = FileNode(safe_bytes('żółwik/zwierzątko_uni'), - content=b'My **changed** content') + node_uni = FileNode(safe_bytes("żółwik/zwierzątko_uni"), content=b"My **changed** content") self.imc.change(node_uni) - self.imc.commit('Changed %s' % safe_str(node_uni.path), - author='joe doe ') + self.imc.commit("Changed %s" % safe_str(node_uni.path), author="joe doe ") newtip = self.repo.get_commit() assert tip != newtip @@ -204,24 +199,24 @@ class TestInMemoryCommit(BackendTestMixi self.assert_nodes_in_commit(newtip, (node, node_uni)) def test_change_raise_empty_repository(self): - node = FileNode(b'foobar') + node = FileNode(b"foobar") with pytest.raises(EmptyRepositoryError): self.imc.change(node) def test_check_integrity_change_raise_node_does_not_exist(self): - node = FileNode(b'foobar', content=b'baz') + node = FileNode(b"foobar", content=b"baz") self.imc.add(node) - self.imc.commit(message='Added foobar', author='Some Name ') - node = FileNode(b'not-foobar', content=b'') + self.imc.commit(message="Added foobar", author="Some Name ") + node = FileNode(b"not-foobar", content=b"") self.imc.change(node) with pytest.raises(NodeDoesNotExistError): - self.imc.commit(message='Changed not existing node', author='Some Name ') + self.imc.commit(message="Changed not existing node", author="Some Name ") def test_change_raise_node_already_changed(self): - node = FileNode(b'foobar', content=b'baz') + node = FileNode(b"foobar", content=b"baz") self.imc.add(node) - self.imc.commit(message='Added foobar', author='Some Nam ') - node = FileNode(b'foobar', content=b'more baz') + self.imc.commit(message="Added foobar", author="Some Nam ") + node = FileNode(b"foobar", content=b"more baz") self.imc.change(node) with pytest.raises(NodeAlreadyChangedError): self.imc.change(node) @@ -233,14 +228,14 @@ class TestInMemoryCommit(BackendTestMixi self.imc.change(node) with pytest.raises(NodeNotChangedError): self.imc.commit( - message='Trying to mark node as changed without touching it', - author='Some Name ') + message="Trying to mark node as changed without touching it", author="Some Name " + ) def test_change_raise_node_already_removed(self): - node = FileNode(b'foobar', content=b'baz') + node = FileNode(b"foobar", content=b"baz") self.imc.add(node) - self.imc.commit(message='Added foobar', author='Some Name ') - self.imc.remove(FileNode(b'foobar')) + self.imc.commit(message="Added foobar", author="Some Name ") + self.imc.remove(FileNode(b"foobar")) with pytest.raises(NodeAlreadyRemovedError): self.imc.change(node) @@ -249,42 +244,38 @@ class TestInMemoryCommit(BackendTestMixi tip = self.repo.get_commit() node = nodes[0] - assert node.content == tip.get_node(node.path).content + assert node.content == tip.get_node(node.bytes_path).content self.imc.remove(node) - self.imc.commit(message=f'Removed {node.path}', author='Some Name ') + self.imc.commit(message=f"Removed {node.path}", author="Some Name ") newtip = self.repo.get_commit() assert tip != newtip assert tip.id != newtip.id with pytest.raises(NodeDoesNotExistError): - newtip.get_node(node.path) + newtip.get_node(node.bytes_path) def test_remove_last_file_from_directory(self): - node = FileNode(b'omg/qwe/foo/bar', content=b'foobar') + node = FileNode(b"omg/qwe/foo/bar", content=b"foobar") self.imc.add(node) - self.imc.commit('added', author='joe doe ') + self.imc.commit("added", author="joe doe ") self.imc.remove(node) - tip = self.imc.commit('removed', 'joe doe ') + tip = self.imc.commit("removed", "joe doe ") with pytest.raises(NodeDoesNotExistError): - tip.get_node('omg/qwe/foo/bar') + tip.get_node(b"omg/qwe/foo/bar") def test_remove_raise_node_does_not_exist(self, nodes): self.imc.remove(nodes[0]) with pytest.raises(NodeDoesNotExistError): - self.imc.commit( - message='Trying to remove node at empty repository', - author='Some Name ') + self.imc.commit(message="Trying to remove node at empty repository", author="Some Name ") def test_check_integrity_remove_raise_node_does_not_exist(self, nodes): self.test_add(nodes) # Performs first commit - node = FileNode(b'no-such-file') + node = FileNode(b"no-such-file") self.imc.remove(node) with pytest.raises(NodeDoesNotExistError): - self.imc.commit( - message='Trying to remove not existing node', - author='Some Name ') + self.imc.commit(message="Trying to remove not existing node", author="Some Name ") def test_remove_raise_node_already_removed(self, nodes): self.test_add(nodes) # Performs first commit @@ -297,13 +288,13 @@ class TestInMemoryCommit(BackendTestMixi def test_remove_raise_node_already_changed(self, nodes): self.test_add(nodes) # Performs first commit - node = FileNode(nodes[0].bytes_path, content=b'Bending time') + node = FileNode(nodes[0].bytes_path, content=b"Bending time") self.imc.change(node) with pytest.raises(NodeAlreadyChangedError): self.imc.remove(node) def test_reset(self): - self.imc.add(FileNode(b'foo', content=b'bar')) + self.imc.add(FileNode(b"foo", content=b"bar")) # self.imc.change(FileNode(b'baz', content='new')) # self.imc.remove(FileNode(b'qwe')) self.imc.reset() @@ -313,11 +304,11 @@ class TestInMemoryCommit(BackendTestMixi N = 3 # number of commits to perform last = None for x in range(N): - fname = safe_bytes('file%s' % str(x).rjust(5, '0')) - content = safe_bytes('foobar\n' * x) + fname = safe_bytes("file%s" % str(x).rjust(5, "0")) + content = safe_bytes("foobar\n" * x) node = FileNode(fname, content=content) self.imc.add(node) - commit = self.imc.commit("Commit no. %s" % (x + 1), author='Vcs User ') + commit = self.imc.commit("Commit no. %s" % (x + 1), author="Vcs User ") assert last != commit last = commit @@ -329,12 +320,10 @@ class TestInMemoryCommit(BackendTestMixi assert len(repo.commit_ids) == N def test_date_attr(self, local_dt_to_utc): - node = FileNode(b'foobar.txt', content=b'Foobared!') + node = FileNode(b"foobar.txt", content=b"Foobared!") self.imc.add(node) date = datetime.datetime(1985, 1, 30, 1, 45) - commit = self.imc.commit( - "Committed at time when I was born ;-)", - author='Test User ', date=date) + commit = self.imc.commit("Committed at time when I was born ;-)", author="Test User ", date=date) assert commit.date == local_dt_to_utc(date) @@ -349,5 +338,5 @@ class TestInMemoryCommit(BackendTestMixi def assert_nodes_in_commit(self, commit, nodes): for node in nodes: - assert commit.get_node(node.path).path == node.path - assert commit.get_node(node.path).content == node.content + assert commit.get_node(node.bytes_path).path == node.path + assert commit.get_node(node.bytes_path).content == node.content diff --git a/rhodecode/tests/vcs/test_nodes.py b/rhodecode/tests/vcs/test_nodes.py --- a/rhodecode/tests/vcs/test_nodes.py +++ b/rhodecode/tests/vcs/test_nodes.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -38,34 +37,34 @@ def binary_filenode(): b"\x10\x08\x06\x00\x00\x00\x1f??a\x00\x00\x00\x04gAMA\x00\x00\xaf?7" b"\x05\x8a?\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq?e<\x00" b"\x00\x025IDAT8?\xa5\x93?K\x94Q\x14\x87\x9f\xf7?Q\x1bs4?\x03\x9a" - b"\xa8?B\x02\x8b$\x10[U;i\x13?6h?&h[?\"\x14j?\xa2M\x7fB\x14F\x9aQ?&" - b"\x842?\x0b\x89\"\x82??!?\x9c!\x9c2l??{N\x8bW\x9dY\xb4\t/\x1c?=" - b"\x9b?}????\xa9*;9!?\x83\x91?[?\\v*?D\x04\'`EpNp\xa2X\'U?pVq\"Sw." + b'\xa8?B\x02\x8b$\x10[U;i\x13?6h?&h[?"\x14j?\xa2M\x7fB\x14F\x9aQ?&' + b'\x842?\x0b\x89"\x82??!?\x9c!\x9c2l??{N\x8bW\x9dY\xb4\t/\x1c?=' + b"\x9b?}????\xa9*;9!?\x83\x91?[?\\v*?D\x04'`EpNp\xa2X'U?pVq\"Sw." b"\x1e?\x08\x01D?jw????\xbc??7{|\x9b?\x89$\x01??W@\x15\x9c\x05q`Lt/" b"\x97?\x94\xa1d?\x18~?\x18?\x18W[%\xb0?\x83??\x14\x88\x8dB?\xa6H" - b"\tL\tl\x19>/\x01`\xac\xabx?\x9cl\nx\xb0\x98\x07\x95\x88D$\"q[" + b'\tL\tl\x19>/\x01`\xac\xabx?\x9cl\nx\xb0\x98\x07\x95\x88D$"q[' b"\x19?d\x00(o\n\xa0??\x7f\xb9\xa4?\x1bF\x1f\x8e\xac\xa8?j??eUU}?.?" - b"\x9f\x8cE??x\x94??\r\xbdtoJU5\"0N\x10U?\x00??V\t\x02\x9f\x81?U?" - b"\x00\x9eM\xae2?r\x9b7\x83\x82\x8aP3????.?&\"?\xb7ZP \x0cJ?\x80\x15T\x95\x9a\x00??S\x8c\r?\xa1" b"\x03\x07?\x96\x9b\xa7\xab=E??\xa4\xb3?\x19q??B\x91=\x8d??k?J" - b"\x0bV\"??\xf7x?\xa1\x00?\\.\x87\x87???\x02F@D\x99],??\x10#?X" + b'\x0bV"??\xf7x?\xa1\x00?\\.\x87\x87???\x02F@D\x99],??\x10#?X' b"\xb7=\xb9\x10?Z\x1by???cI??\x1ag?\x92\xbc?T?t[\x92\x81?<_\x17~" b"\x92\x88?H%?\x10Q\x02\x9f\n\x81qQ\x0bm?\x1bX?\xb1AK\xa6\x9e\xb9?u" - b"\xb2?1\xbe|/\x92M@\xa2!F?\xa9>\"\r\x92\x8e?>\x9a9Qv\x127?a" + b'\xb2?1\xbe|/\x92M@\xa2!F?\xa9>"\r\x92\x8e?>\x9a9Qv\x127?a' b"\xac?Y?8?:??]X???9\x80\xb7?u?\x0b#BZ\x8d=\x1d?p\x00\x00\x00\x00" - b"IEND\xaeB`\x82") + b"IEND\xaeB`\x82" + ) return FileNode(filename, content=data) + return node_maker class TestNodeBasics: - - @pytest.mark.parametrize("path", ['/foo', '/foo/bar']) - @pytest.mark.parametrize( - "kind", [NodeKind.FILE, NodeKind.DIR], ids=["FILE", "DIR"]) + @pytest.mark.parametrize("path", ["/foo", "/foo/bar"]) + @pytest.mark.parametrize("kind", [NodeKind.FILE, NodeKind.DIR], ids=["FILE", "DIR"]) def test_init_wrong_paths(self, path, kind): """ Cannot initialize Node objects with path with slash at the beginning. @@ -74,44 +73,46 @@ class TestNodeBasics: with pytest.raises(NodeError): Node(path, kind) - @pytest.mark.parametrize("path", ['path', 'some/path']) - @pytest.mark.parametrize( - "kind", [NodeKind.FILE, NodeKind.DIR], ids=["FILE", "DIR"]) + @pytest.mark.parametrize("path", ["path", "some/path"]) + @pytest.mark.parametrize("kind", [NodeKind.FILE, NodeKind.DIR], ids=["FILE", "DIR"]) def test_name(self, path, kind): path = safe_bytes(path) node = Node(path, kind) - assert node.name == 'path' + assert node.name == "path" def test_name_root(self): - node = Node(b'', NodeKind.DIR) - assert node.name == '' + node = Node(b"", NodeKind.DIR) + assert node.name == "" def test_root_node_cannot_be_file(self): with pytest.raises(NodeError): - Node(b'', NodeKind.FILE) + Node(b"", NodeKind.FILE) def test_kind_setter(self): - node = Node(b'', NodeKind.DIR) + node = Node(b"", NodeKind.DIR) with pytest.raises(NodeError): node.kind = NodeKind.FILE def test_compare_equal(self): - node1 = FileNode(b'test', content=b'') - node2 = FileNode(b'test', content=b'') + node1 = FileNode(b"test", content=b"") + node2 = FileNode(b"test", content=b"") assert node1 == node2 assert not node1 != node2 def test_compare_unequal(self): - node1 = FileNode(b'test', content=b'a') - node2 = FileNode(b'test', content=b'b') + node1 = FileNode(b"test", content=b"a") + node2 = FileNode(b"test", content=b"b") assert node1 != node2 assert not node1 == node2 - @pytest.mark.parametrize("node_path, expected_parent_path", [ - ('', b''), - ('some/path/', b'some/'), - ('some/longer/path/', b'some/longer/'), - ]) + @pytest.mark.parametrize( + "node_path, expected_parent_path", + [ + ("", b""), + ("some/path/", b"some/"), + ("some/longer/path/", b"some/longer/"), + ], + ) def test_parent_path_new(self, node_path, expected_parent_path): """ Tests if node's parent path are properly computed. @@ -119,11 +120,10 @@ class TestNodeBasics: node_path = safe_bytes(node_path) node = Node(node_path, NodeKind.DIR) parent_path = node.get_parent_path() - assert (parent_path.endswith(b'/') or - node.is_root() and parent_path == b'') + assert parent_path.endswith(b"/") or node.is_root() and parent_path == b"" assert parent_path == expected_parent_path - ''' + """ def _test_trailing_slash(self, path): if not path.endswith('/'): pytest.fail("Trailing slash tests needs paths to end with slash") @@ -134,53 +134,29 @@ class TestNodeBasics: def test_trailing_slash(self): for path in ('/', 'foo/', 'foo/bar/', 'foo/bar/biz/'): self._test_trailing_slash(path) - ''' + """ def test_is_file(self): - node = Node(b'any', NodeKind.FILE) + node = Node(b"any", NodeKind.FILE) assert node.is_file() - node = FileNode(b'any') + node = FileNode(b"any") assert node.is_file() with pytest.raises(AttributeError): node.nodes # noqa def test_is_dir(self): - node = Node(b'any_dir', NodeKind.DIR) + node = Node(b"any_dir", NodeKind.DIR) assert node.is_dir() - node = DirNode(b'any_dir') + node = DirNode(b"any_dir") assert node.is_dir() with pytest.raises(NodeError): node.content # noqa - def test_dir_node_iter(self): - nodes = [ - DirNode(b'docs'), - DirNode(b'tests'), - FileNode(b'bar'), - FileNode(b'foo'), - FileNode(b'readme.txt'), - FileNode(b'setup.py'), - ] - dirnode = DirNode(b'', nodes=nodes) - for node in dirnode: - assert node == dirnode.get_node(node.path) - - def test_node_state(self): - """ - Without link to commit nodes should raise NodeError. - """ - node = FileNode(b'anything') - with pytest.raises(NodeError): - node.state # noqa - node = DirNode(b'anything') - with pytest.raises(NodeError): - node.state # noqa - def test_file_node_stat(self): - node = FileNode(b'foobar', b'empty... almost') + node = FileNode(b"foobar", b"empty... almost") mode = node.mode # default should be 0100644 assert mode & stat.S_IRUSR assert mode & stat.S_IWUSR @@ -193,36 +169,36 @@ class TestNodeBasics: assert not mode & stat.S_IXOTH def test_file_node_is_executable(self): - node = FileNode(b'foobar', b'empty... almost', mode=0o100755) + node = FileNode(b"foobar", b"empty... almost", mode=0o100755) assert node.is_executable - node = FileNode(b'foobar', b'empty... almost', mode=0o100500) + node = FileNode(b"foobar", b"empty... almost", mode=0o100500) assert node.is_executable - node = FileNode(b'foobar', b'empty... almost', mode=0o100644) + node = FileNode(b"foobar", b"empty... almost", mode=0o100644) assert not node.is_executable def test_file_node_is_not_symlink(self): - node = FileNode(b'foobar', b'empty...') + node = FileNode(b"foobar", b"empty...") assert not node.is_link() def test_mimetype(self): - py_node = FileNode(b'test.py') - tar_node = FileNode(b'test.tar.gz') + py_node = FileNode(b"test.py") + tar_node = FileNode(b"test.tar.gz") - ext = 'CustomExtension' + ext = "CustomExtension" - my_node2 = FileNode(b'myfile2') + my_node2 = FileNode(b"myfile2") my_node2._mimetype = [ext] - my_node3 = FileNode(b'myfile3') + my_node3 = FileNode(b"myfile3") my_node3._mimetype = [ext, ext] - assert py_node.mimetype == 'text/x-python' - assert py_node.get_mimetype() == ('text/x-python', None) + assert py_node.mimetype == "text/x-python" + assert py_node.get_mimetype() == ("text/x-python", None) - assert tar_node.mimetype == 'application/x-tar' - assert tar_node.get_mimetype() == ('application/x-tar', 'gzip') + assert tar_node.mimetype == "application/x-tar" + assert tar_node.get_mimetype() == ("application/x-tar", "gzip") with pytest.raises(NodeError): my_node2.get_mimetype() @@ -232,47 +208,45 @@ class TestNodeBasics: def test_lines_counts(self): lines = [ - b'line1\n', - b'line2\n', - b'line3\n', - b'\n', - b'\n', - b'line4\n', + b"line1\n", + b"line2\n", + b"line3\n", + b"\n", + b"\n", + b"line4\n", ] - py_node = FileNode(b'test.py', b''.join(lines)) + py_node = FileNode(b"test.py", b"".join(lines)) assert (len(lines), len(lines)) == py_node.lines() assert (len(lines), len(lines) - 2) == py_node.lines(count_empty=True) def test_lines_no_newline(self): - py_node = FileNode(b'test.py', b'oneline') + py_node = FileNode(b"test.py", b"oneline") assert (1, 1) == py_node.lines() assert (1, 1) == py_node.lines(count_empty=True) class TestNodeContent(object): - def test_if_binary(self, binary_filenode): - filenode = binary_filenode(b'calendar.jpg') + filenode = binary_filenode(b"calendar.jpg") assert filenode.is_binary def test_binary_line_counts(self, binary_filenode): - tar_node = binary_filenode(b'archive.tar.gz') + tar_node = binary_filenode(b"archive.tar.gz") assert (0, 0) == tar_node.lines(count_empty=True) def test_binary_mimetype(self, binary_filenode): - tar_node = binary_filenode(b'archive.tar.gz') - assert tar_node.mimetype == 'application/x-tar' + tar_node = binary_filenode(b"archive.tar.gz") + assert tar_node.mimetype == "application/x-tar" @pytest.mark.usefixtures("vcs_repository_support") class TestNodesCommits(BackendTestMixin): - def test_node_last_commit(self, generate_repo_with_commits): repo = generate_repo_with_commits(20) last_commit = repo.get_commit() for x in range(3): - node = last_commit.get_node(f'file_{x}.txt') + node = last_commit.get_node(b"file_%d.txt" % x) assert node.last_commit == repo[x] diff --git a/rhodecode/tests/vcs/test_repository.py b/rhodecode/tests/vcs/test_repository.py --- a/rhodecode/tests/vcs/test_repository.py +++ b/rhodecode/tests/vcs/test_repository.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -114,9 +114,7 @@ class TestRepositoryBase(BackendTestMixi assert len(self.repo.get_hook_location()) != 0 def test_last_change(self, local_dt_to_utc): - assert self.repo.last_change >= local_dt_to_utc( - datetime.datetime(2010, 1, 1, 21, 0) - ) + assert self.repo.last_change >= local_dt_to_utc(datetime.datetime(2010, 1, 1, 21, 0)) def test_last_change_in_empty_repository(self, vcsbackend, local_dt_to_utc): delta = datetime.timedelta(seconds=1) @@ -195,9 +193,7 @@ class TestRepositoryCompare: @pytest.mark.parametrize("merge", [True, False]) def test_compare_commits_of_same_repository(self, vcsbackend, merge): target_repo = vcsbackend.create_repo(number_of_commits=5) - target_repo.compare( - target_repo[1].raw_id, target_repo[3].raw_id, target_repo, merge=merge - ) + target_repo.compare(target_repo[1].raw_id, target_repo[3].raw_id, target_repo, merge=merge) @pytest.mark.xfail_backends("svn") @pytest.mark.parametrize("merge", [True, False]) @@ -209,9 +205,7 @@ class TestRepositoryCompare: vcsbackend.add_file(source_repo, b"newfile", b"somecontent") source_commit = source_repo.get_commit() - target_repo.compare( - target_repo[1].raw_id, source_repo[3].raw_id, source_repo, merge=merge - ) + target_repo.compare(target_repo[1].raw_id, source_repo[3].raw_id, source_repo, merge=merge) @pytest.mark.xfail_backends("svn") @pytest.mark.parametrize("merge", [True, False]) @@ -351,9 +345,7 @@ class TestRepositoryMerge(object): "merge message 1", dry_run=False, ) - expected_merge_response = MergeResponse( - True, True, merge_response.merge_ref, MergeFailureReason.NONE - ) + expected_merge_response = MergeResponse(True, True, merge_response.merge_ref, MergeFailureReason.NONE) assert merge_response == expected_merge_response target_repo = backends.get_backend(vcsbackend.alias)(self.target_repo.path) @@ -385,9 +377,7 @@ class TestRepositoryMerge(object): "merge message 2", dry_run=False, ) - expected_merge_response = MergeResponse( - True, True, merge_response.merge_ref, MergeFailureReason.NONE - ) + expected_merge_response = MergeResponse(True, True, merge_response.merge_ref, MergeFailureReason.NONE) assert merge_response == expected_merge_response target_repo = backends.get_backend(vcsbackend.alias)(self.target_repo.path) @@ -422,9 +412,9 @@ class TestRepositoryMerge(object): # Multiple merges may differ in their commit id. Therefore, we set the # commit id to `None` before comparing the merge responses. - merge_response.merge_ref.commit_id = 'abcdeabcde' + merge_response.merge_ref.commit_id = "abcdeabcde" - merge_response_update.merge_ref.commit_id = 'abcdeabcde' + merge_response_update.merge_ref.commit_id = "abcdeabcde" assert merge_response == merge_response_update assert merge_response.possible is True @@ -436,9 +426,7 @@ class TestRepositoryMerge(object): def test_merge_conflict(self, vcsbackend, dry_run): self.prepare_for_conflict(vcsbackend) - expected_merge_response = MergeResponse( - False, False, None, MergeFailureReason.MERGE_FAILED - ) + expected_merge_response = MergeResponse(False, False, None, MergeFailureReason.MERGE_FAILED) merge_response = self.target_repo.merge( self.repo_id, @@ -491,9 +479,7 @@ class TestRepositoryMerge(object): def test_merge_missing_source_reference(self, vcsbackend): self.prepare_for_success(vcsbackend) - source_ref = Reference( - self.source_ref.type, "not_existing", self.source_ref.commit_id - ) + source_ref = Reference(self.source_ref.type, "not_existing", self.source_ref.commit_id) expected_merge_response = MergeResponse( False, False, @@ -523,9 +509,7 @@ class TestRepositoryMerge(object): metadata={"exception": "ErrorForTest"}, ) - with mock.patch.object( - self.target_repo, "_merge_repo", side_effect=RepositoryError() - ): + with mock.patch.object(self.target_repo, "_merge_repo", side_effect=RepositoryError()): merge_response = self.target_repo.merge( self.repo_id, self.workspace_id, @@ -559,9 +543,7 @@ class TestRepositoryMerge(object): workspace_id = "test-errors-in-merge" repo_id = repo_id_generator(workspace_id) with pytest.raises(ValueError): - repo.merge( - repo_id, workspace_id, ref, self, ref, "user name", "user@email.com" - ) + repo.merge(repo_id, workspace_id, ref, self, ref, "user name", "user@email.com") @pytest.mark.usefixtures("vcs_repository_support") diff --git a/rhodecode/tests/vcs/test_svn.py b/rhodecode/tests/vcs/test_svn.py --- a/rhodecode/tests/vcs/test_svn.py +++ b/rhodecode/tests/vcs/test_svn.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -47,13 +46,13 @@ def head(repo): def test_init_fails_if_path_does_not_exist(): - path = os.path.join(TEST_DIR, 'i-do-not-exist') + path = os.path.join(TEST_DIR, "i-do-not-exist") with pytest.raises(VCSError): SubversionRepository(path) def test_init_fails_if_path_is_not_a_valid_repository(tmpdir): - path = str(tmpdir.mkdir('unicode ä')) + path = str(tmpdir.mkdir("unicode ä")) with pytest.raises(VCSError): SubversionRepository(path) @@ -61,17 +60,14 @@ def test_init_fails_if_path_is_not_a_val def test_repo_clone(vcsbackend, reposerver): source = vcsbackend.create_repo(number_of_commits=3) reposerver.serve(source) - repo = SubversionRepository( - vcsbackend.new_repo_path(), - create=True, - src_url=reposerver.url) + repo = SubversionRepository(vcsbackend.new_repo_path(), create=True, src_url=reposerver.url) assert source.commit_ids == repo.commit_ids assert source[0].message == repo[0].message def test_latest_commit(head): - assert head.raw_id == '393' + assert head.raw_id == "393" def test_commit_description(head): @@ -79,18 +75,22 @@ def test_commit_description(head): def test_commit_author(head): - assert head.author == 'marcin' + assert head.author == "marcin" -@pytest.mark.parametrize("filename, content, mime_type", [ - (b'test.txt', b'Text content\n', None), - (b'test.bin', b'\0 binary \0', 'application/octet-stream'), -], ids=['text', 'binary']) +@pytest.mark.parametrize( + "filename, content, mime_type", + [ + (b"test.txt", b"Text content\n", None), + (b"test.bin", b"\0 binary \0", "application/octet-stream"), + ], + ids=["text", "binary"], +) def test_sets_mime_type_correctly(vcsbackend, filename, content, mime_type): repo = vcsbackend.create_repo() vcsbackend.ensure_file(filename, content) file_properties = repo._remote.node_properties(filename, 1) - assert file_properties.get('svn:mime-type') == mime_type + assert file_properties.get("svn:mime-type") == mime_type def test_slice_access(repo): @@ -100,7 +100,7 @@ def test_slice_access(repo): end = start + page_size - 1 commits = list(repo[start:end]) - assert [commit.raw_id for commit in commits] == ['1', '2', '3', '4'] + assert [commit.raw_id for commit in commits] == ["1", "2", "3", "4"] def test_walk_changelog_page(repo): @@ -110,14 +110,14 @@ def test_walk_changelog_page(repo): end = start + page_size - 1 commits = list(repo[start:end]) - changelog = [ - 'r%s, %s, %s' % (c.raw_id, c.author, c.message) for c in commits] + changelog = ["r%s, %s, %s" % (c.raw_id, c.author, c.message) for c in commits] expexted_messages = [ - 'r1, marcin, initial import', - 'r2, marcin, hg ignore', - 'r3, marcin, Pip standards refactor', - 'r4, marcin, Base repository few new functions added'] + "r1, marcin, initial import", + "r2, marcin, hg ignore", + "r3, marcin, Pip standards refactor", + "r4, marcin, Base repository few new functions added", + ] assert changelog == expexted_messages @@ -128,68 +128,68 @@ def test_read_full_file_tree(head): def test_topnode_files_attribute(head): - topnode = head.get_node('') + topnode = head.get_node(b"") topnode.files - - -@pytest.mark.parametrize("filename, content, branch, mime_type", [ - ('branches/plain/test.txt', b'Text content\n', 'plain', None), - ('branches/uniçö∂e/test.bin', b'\0 binary \0', 'uniçö∂e', 'application/octet-stream'), -], ids=['text', 'binary']) +@pytest.mark.parametrize( + "filename, content, branch, mime_type", + [ + ("branches/plain/test.txt", b"Text content\n", "plain", None), + ("branches/uniçö∂e/test.bin", b"\0 binary \0", "uniçö∂e", "application/octet-stream"), + ], + ids=["text", "binary"], +) def test_unicode_refs(vcsbackend, filename, content, branch, mime_type): filename = safe_bytes(filename) repo = vcsbackend.create_repo() vcsbackend.ensure_file(filename, content) - with mock.patch(("rhodecode.lib.vcs.backends.svn.repository" - ".SubversionRepository._patterns_from_section"), - return_value=['branches/*']): - assert f'branches/{branch}' in repo.branches + with mock.patch( + ("rhodecode.lib.vcs.backends.svn.repository" ".SubversionRepository._patterns_from_section"), + return_value=["branches/*"], + ): + assert f"branches/{branch}" in repo.branches def test_compatible_version(monkeypatch, vcsbackend): - monkeypatch.setattr(settings, 'SVN_COMPATIBLE_VERSION', 'pre-1.8-compatible') + monkeypatch.setattr(settings, "SVN_COMPATIBLE_VERSION", "pre-1.8-compatible") path = vcsbackend.new_repo_path() SubversionRepository(path, create=True) - with open(f'{path}/db/format') as f: + with open(f"{path}/db/format") as f: first_line = f.readline().strip() - assert first_line == '4' + assert first_line == "4" def test_invalid_compatible_version(monkeypatch, vcsbackend): - monkeypatch.setattr(settings, 'SVN_COMPATIBLE_VERSION', 'i-am-an-invalid-setting') + monkeypatch.setattr(settings, "SVN_COMPATIBLE_VERSION", "i-am-an-invalid-setting") path = vcsbackend.new_repo_path() with pytest.raises(Exception): SubversionRepository(path, create=True) class TestSVNCommit(object): - @pytest.fixture(autouse=True) def prepare(self, repo): self.repo = repo def test_file_history_from_commits(self): - node = self.repo[10].get_node('setup.py') + node = self.repo[10].get_node(b"setup.py") commit_ids = [commit.raw_id for commit in node.history] - assert ['8'] == commit_ids + assert ["8"] == commit_ids - node = self.repo[20].get_node('setup.py') + node = self.repo[20].get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['18', - '8'] == node_ids + assert ["18", "8"] == node_ids # special case we check history from commit that has this particular # file changed this means we check if it's included as well - node = self.repo.get_commit('18').get_node('setup.py') + node = self.repo.get_commit("18").get_node(b"setup.py") node_ids = [commit.raw_id for commit in node.history] - assert ['18', - '8'] == node_ids + assert ["18", "8"] == node_ids def test_repo_files_content_type(self): test_commit = self.repo.get_commit(commit_idx=100) - for node in test_commit.get_node('/'): + for node in test_commit.get_node(b"/"): if node.is_file(): assert type(node.content) == bytes assert type(node.str_content) == str diff --git a/rhodecode/tests/vcs/test_tags.py b/rhodecode/tests/vcs/test_tags.py --- a/rhodecode/tests/vcs/test_tags.py +++ b/rhodecode/tests/vcs/test_tags.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,8 +19,7 @@ import pytest from rhodecode.tests.vcs.conftest import BackendTestMixin -from rhodecode.lib.vcs.exceptions import ( - TagAlreadyExistError, TagDoesNotExistError) +from rhodecode.lib.vcs.exceptions import TagAlreadyExistError, TagDoesNotExistError pytestmark = pytest.mark.backends("git", "hg") @@ -29,41 +27,40 @@ pytestmark = pytest.mark.backends("git", @pytest.mark.usefixtures("vcs_repository_support") class TestTags(BackendTestMixin): - def test_new_tag(self): tip = self.repo.get_commit() tagsize = len(self.repo.tags) - tag = self.repo.tag('last-commit', 'joe', tip.raw_id) + tag_commit = self.repo.tag("last-commit", "joe", tip.raw_id) assert len(self.repo.tags) == tagsize + 1 for top, __, __ in tip.walk(): - assert top == tag.get_node(top.path) + assert top == tag_commit.get_node(top.bytes_path) def test_tag_already_exist(self): tip = self.repo.get_commit() - self.repo.tag('last-commit', 'joe', tip.raw_id) + self.repo.tag("last-commit", "joe", tip.raw_id) with pytest.raises(TagAlreadyExistError): - self.repo.tag('last-commit', 'joe', tip.raw_id) + self.repo.tag("last-commit", "joe", tip.raw_id) commit = self.repo.get_commit(commit_idx=0) with pytest.raises(TagAlreadyExistError): - self.repo.tag('last-commit', 'jane', commit.raw_id) + self.repo.tag("last-commit", "jane", commit.raw_id) def test_remove_tag(self): tip = self.repo.get_commit() - self.repo.tag('last-commit', 'joe', tip.raw_id) + self.repo.tag("last-commit", "joe", tip.raw_id) tagsize = len(self.repo.tags) - self.repo.remove_tag('last-commit', user='evil joe') + self.repo.remove_tag("last-commit", user="evil joe") assert len(self.repo.tags) == tagsize - 1 def test_remove_tag_which_does_not_exist(self): with pytest.raises(TagDoesNotExistError): - self.repo.remove_tag('last-commit', user='evil joe') + self.repo.remove_tag("last-commit", user="evil joe") def test_name_with_slash(self): - self.repo.tag('19/10/11', 'joe') - assert '19/10/11' in self.repo.tags - self.repo.tag('rel.11', 'joe') - assert 'rel.11' in self.repo.tags + self.repo.tag("19/10/11", "joe") + assert "19/10/11" in self.repo.tags + self.repo.tag("rel.11", "joe") + assert "rel.11" in self.repo.tags diff --git a/rhodecode/tests/vcs/test_utils.py b/rhodecode/tests/vcs/test_utils.py --- a/rhodecode/tests/vcs/test_utils.py +++ b/rhodecode/tests/vcs/test_utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -34,23 +33,20 @@ from rhodecode.lib.vcs.utils.paths impor @pytest.mark.usefixtures("baseapp") class TestPaths(object): - def _test_get_dirs_for_path(self, path, expected): """ Tests if get_dirs_for_path returns same as expected. """ expected = sorted(expected) result = sorted(get_dirs_for_path(path)) - assert result == expected, ( - "%s != %s which was expected result for path %s" - % (result, expected, path)) + assert result == expected, "%s != %s which was expected result for path %s" % (result, expected, path) def test_get_dirs_for_path(self): - path = 'foo/bar/baz/file' + path = "foo/bar/baz/file" paths_and_results = ( - ('foo/bar/baz/file', ['foo', 'foo/bar', 'foo/bar/baz']), - ('foo/bar/', ['foo', 'foo/bar']), - ('foo/bar', ['foo']), + ("foo/bar/baz/file", ["foo", "foo/bar", "foo/bar/baz"]), + ("foo/bar/", ["foo", "foo/bar"]), + ("foo/bar", ["foo"]), ) for path, expected in paths_and_results: self._test_get_dirs_for_path(path, expected) @@ -59,18 +55,17 @@ class TestPaths(object): new = tmpdir.strpath assert get_scms_for_path(new) == [] - os.mkdir(os.path.join(new, '.tux')) + os.mkdir(os.path.join(new, ".tux")) assert get_scms_for_path(new) == [] - os.mkdir(os.path.join(new, '.git')) - assert set(get_scms_for_path(new)) == set(['git']) + os.mkdir(os.path.join(new, ".git")) + assert set(get_scms_for_path(new)) == set(["git"]) - os.mkdir(os.path.join(new, '.hg')) - assert set(get_scms_for_path(new)) == set(['git', 'hg']) + os.mkdir(os.path.join(new, ".hg")) + assert set(get_scms_for_path(new)) == set(["git", "hg"]) class TestGetScm(object): - def test_existing_repository(self, vcs_repository_support): alias, repo = vcs_repository_support assert (alias, repo.path) == get_scm(repo.path) @@ -81,114 +76,101 @@ class TestGetScm(object): def test_get_scm_error_path(self): with pytest.raises(VCSError): - get_scm('err') + get_scm("err") def test_get_two_scms_for_path(self, tmpdir): multialias_repo_path = str(tmpdir) git_default_branch = GitRepository.DEFAULT_BRANCH_NAME - subprocess.check_call(['hg', 'init', multialias_repo_path]) - subprocess.check_call(['git', '-c', f'init.defaultBranch={git_default_branch}', 'init', multialias_repo_path]) + subprocess.check_call(["hg", "init", multialias_repo_path]) + subprocess.check_call(["git", "-c", f"init.defaultBranch={git_default_branch}", "init", multialias_repo_path]) with pytest.raises(VCSError): get_scm(multialias_repo_path) def test_ignores_svn_working_copy(self, tmpdir): - tmpdir.mkdir('.svn') + tmpdir.mkdir(".svn") with pytest.raises(VCSError): get_scm(tmpdir.strpath) class TestParseDatetime(object): - def test_datetime_text(self): - assert parse_datetime('2010-04-07 21:29:41') == \ - datetime.datetime(2010, 4, 7, 21, 29, 41) + assert parse_datetime("2010-04-07 21:29:41") == datetime.datetime(2010, 4, 7, 21, 29, 41) def test_no_seconds(self): - assert parse_datetime('2010-04-07 21:29') == \ - datetime.datetime(2010, 4, 7, 21, 29) + assert parse_datetime("2010-04-07 21:29") == datetime.datetime(2010, 4, 7, 21, 29) def test_date_only(self): - assert parse_datetime('2010-04-07') == \ - datetime.datetime(2010, 4, 7) + assert parse_datetime("2010-04-07") == datetime.datetime(2010, 4, 7) def test_another_format(self): - assert parse_datetime('04/07/10 21:29:41') == \ - datetime.datetime(2010, 4, 7, 21, 29, 41) + assert parse_datetime("04/07/10 21:29:41") == datetime.datetime(2010, 4, 7, 21, 29, 41) def test_now(self): - assert parse_datetime('now') - datetime.datetime.now() < \ - datetime.timedelta(seconds=1) + assert parse_datetime("now") - datetime.datetime.now() < datetime.timedelta(seconds=1) def test_today(self): today = datetime.date.today() - assert parse_datetime('today') == \ - datetime.datetime(*today.timetuple()[:3]) + assert parse_datetime("today") == datetime.datetime(*today.timetuple()[:3]) def test_yesterday(self): yesterday = datetime.date.today() - datetime.timedelta(days=1) - assert parse_datetime('yesterday') == \ - datetime.datetime(*yesterday.timetuple()[:3]) + assert parse_datetime("yesterday") == datetime.datetime(*yesterday.timetuple()[:3]) def test_tomorrow(self): tomorrow = datetime.date.today() + datetime.timedelta(days=1) args = tomorrow.timetuple()[:3] + (23, 59, 59) - assert parse_datetime('tomorrow') == datetime.datetime(*args) + assert parse_datetime("tomorrow") == datetime.datetime(*args) def test_days(self): timestamp = datetime.datetime.today() - datetime.timedelta(days=3) args = timestamp.timetuple()[:3] + (0, 0, 0, 0) expected = datetime.datetime(*args) - assert parse_datetime('3d') == expected - assert parse_datetime('3 d') == expected - assert parse_datetime('3 day') == expected - assert parse_datetime('3 days') == expected + assert parse_datetime("3d") == expected + assert parse_datetime("3 d") == expected + assert parse_datetime("3 day") == expected + assert parse_datetime("3 days") == expected def test_weeks(self): timestamp = datetime.datetime.today() - datetime.timedelta(days=3 * 7) args = timestamp.timetuple()[:3] + (0, 0, 0, 0) expected = datetime.datetime(*args) - assert parse_datetime('3w') == expected - assert parse_datetime('3 w') == expected - assert parse_datetime('3 week') == expected - assert parse_datetime('3 weeks') == expected + assert parse_datetime("3w") == expected + assert parse_datetime("3 w") == expected + assert parse_datetime("3 week") == expected + assert parse_datetime("3 weeks") == expected def test_mixed(self): - timestamp = ( - datetime.datetime.today() - datetime.timedelta(days=2 * 7 + 3)) + timestamp = datetime.datetime.today() - datetime.timedelta(days=2 * 7 + 3) args = timestamp.timetuple()[:3] + (0, 0, 0, 0) expected = datetime.datetime(*args) - assert parse_datetime('2w3d') == expected - assert parse_datetime('2w 3d') == expected - assert parse_datetime('2w 3 days') == expected - assert parse_datetime('2 weeks 3 days') == expected + assert parse_datetime("2w3d") == expected + assert parse_datetime("2w 3d") == expected + assert parse_datetime("2w 3 days") == expected + assert parse_datetime("2 weeks 3 days") == expected -@pytest.mark.parametrize("test_str, name, email", [ - ('Marcin Kuzminski ', - 'Marcin Kuzminski', 'marcin@python-works.com'), - ('Marcin Kuzminski Spaces < marcin@python-works.com >', - 'Marcin Kuzminski Spaces', 'marcin@python-works.com'), - ('Marcin Kuzminski ', - 'Marcin Kuzminski', 'marcin.kuzminski@python-works.com'), - ('mrf RFC_SPEC ', - 'mrf RFC_SPEC', 'marcin+kuzminski@python-works.com'), - ('username ', - 'username', 'user@email.com'), - ('username ', - '', 'justemail@mail.com'), - ('justname', - 'justname', ''), - ('Mr Double Name withemail@email.com ', - 'Mr Double Name', 'withemail@email.com'), -]) +@pytest.mark.parametrize( + "test_str, name, email", + [ + ("Marcin Kuzminski ", "Marcin Kuzminski", "marcin@python-works.com"), + ("Marcin Kuzminski Spaces < marcin@python-works.com >", "Marcin Kuzminski Spaces", "marcin@python-works.com"), + ( + "Marcin Kuzminski ", + "Marcin Kuzminski", + "marcin.kuzminski@python-works.com", + ), + ("mrf RFC_SPEC ", "mrf RFC_SPEC", "marcin+kuzminski@python-works.com"), + ("username ", "username", "user@email.com"), + ("username ", "", "justemail@mail.com"), + ("justname", "justname", ""), + ("Mr Double Name withemail@email.com ", "Mr Double Name", "withemail@email.com"), + ], +) class TestAuthorExtractors(object): - def test_author_email(self, test_str, name, email): assert email == author_email(test_str) diff --git a/rhodecode/tests/vcs/test_vcs.py b/rhodecode/tests/vcs/test_vcs.py --- a/rhodecode/tests/vcs/test_vcs.py +++ b/rhodecode/tests/vcs/test_vcs.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,6 +19,7 @@ """ Tests for main module's methods. """ + import os import tempfile import shutil @@ -48,7 +48,7 @@ def test_alias_detect(backend): def test_wrong_alias(): - alias = 'wrong_alias' + alias = "wrong_alias" with pytest.raises(VCSError): get_backend(alias) @@ -77,12 +77,13 @@ def test_get_vcs_instance_by_path_multip Test that ``get_vcs_instance_by_path`` returns None if a path is passed to a directory with multiple repositories. """ - empty_dir = tempfile.mkdtemp(prefix='pytest-empty-dir-') - os.mkdir(os.path.join(empty_dir, '.git')) - os.mkdir(os.path.join(empty_dir, '.hg')) + empty_dir = tempfile.mkdtemp(prefix="pytest-empty-dir-") + os.mkdir(os.path.join(empty_dir, ".git")) + os.mkdir(os.path.join(empty_dir, ".hg")) def fin(): shutil.rmtree(empty_dir) + request.addfinalizer(fin) repo = get_vcs_instance(empty_dir) @@ -90,39 +91,32 @@ def test_get_vcs_instance_by_path_multip assert repo is None -@mock.patch('rhodecode.lib.vcs.backends.get_scm') -@mock.patch('rhodecode.lib.vcs.backends.get_backend') -def test_get_vcs_instance_by_path_args_passed( - get_backend_mock, get_scm_mock, tmpdir, vcs_repo): +@mock.patch("rhodecode.lib.vcs.backends.get_scm") +@mock.patch("rhodecode.lib.vcs.backends.get_backend") +def test_get_vcs_instance_by_path_args_passed(get_backend_mock, get_scm_mock, tmpdir, vcs_repo): """ Test that the arguments passed to ``get_vcs_instance_by_path`` are forwarded to the vcs backend class. """ backend = mock.MagicMock() get_backend_mock.return_value = backend - args = ['these-are-test-args', 0, True, None] + args = ["these-are-test-args", 0, True, None] repo = vcs_repo.path get_vcs_instance(repo, *args) backend.assert_called_with(*args, repo_path=repo) -@mock.patch('rhodecode.lib.vcs.backends.get_scm') -@mock.patch('rhodecode.lib.vcs.backends.get_backend') -def test_get_vcs_instance_by_path_kwargs_passed( - get_backend_mock, get_scm_mock, vcs_repo): +@mock.patch("rhodecode.lib.vcs.backends.get_scm") +@mock.patch("rhodecode.lib.vcs.backends.get_backend") +def test_get_vcs_instance_by_path_kwargs_passed(get_backend_mock, get_scm_mock, vcs_repo): """ Test that the keyword arguments passed to ``get_vcs_instance_by_path`` are forwarded to the vcs backend class. """ backend = mock.MagicMock() get_backend_mock.return_value = backend - kwargs = { - 'foo': 'these-are-test-args', - 'bar': 0, - 'baz': True, - 'foobar': None - } + kwargs = {"foo": "these-are-test-args", "bar": 0, "baz": True, "foobar": None} repo = vcs_repo.path get_vcs_instance(repo, **kwargs) diff --git a/rhodecode/tests/vcs/utils.py b/rhodecode/tests/vcs/utils.py --- a/rhodecode/tests/vcs/utils.py +++ b/rhodecode/tests/vcs/utils.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -37,7 +36,7 @@ def run_command(cmd, args): """ Runs command on the system with given ``args``. """ - command = ' '.join((cmd, args)) + command = " ".join((cmd, args)) p = Popen(command, shell=True) status = os.waitpid(p.pid, 0)[1] return status @@ -51,12 +50,11 @@ def eprint(msg): Appends line break. """ sys.stderr.write(msg) - sys.stderr.write('\n') + sys.stderr.write("\n") # TODO: Revisit once we have CI running, if this is not helping us, remove it class SCMFetcher(object): - def __init__(self, alias, test_repo_path): """ :param clone_cmd: command which would clone remote repository; pass @@ -75,9 +73,8 @@ class SCMFetcher(object): Tries to fetch repository from remote path. """ remote = self.remote_repo - eprint( - "Fetching repository %s into %s" % (remote, self.test_repo_path)) - run_command(self.clone_cmd, '%s %s' % (remote, self.test_repo_path)) + eprint("Fetching repository %s into %s" % (remote, self.test_repo_path)) + run_command(self.clone_cmd, "%s %s" % (remote, self.test_repo_path)) def get_normalized_path(path): @@ -88,29 +85,29 @@ def get_normalized_path(path): """ if os.path.exists(path): dir, basename = os.path.split(path) - splitted_name = basename.split('.') + splitted_name = basename.split(".") if len(splitted_name) > 1: ext = splitted_name[-1] else: ext = None - name = '.'.join(splitted_name[:-1]) - matcher = re.compile(r'^.*-(\d{5})$') + name = ".".join(splitted_name[:-1]) + matcher = re.compile(r"^.*-(\d{5})$") start = 0 m = matcher.match(name) if not m: # Haven't append number yet so return first - newname = f'{name}-00000' + newname = f"{name}-00000" newpath = os.path.join(dir, newname) if ext: - newpath = '.'.join((newpath, ext)) + newpath = ".".join((newpath, ext)) return get_normalized_path(newpath) else: start = int(m.group(1)[-5:]) + 1 for x in range(start, 10000): - newname = name[:-5] + str(x).rjust(5, '0') + newname = name[:-5] + str(x).rjust(5, "0") newpath = os.path.join(dir, newname) if ext: - newpath = '.'.join((newpath, ext)) + newpath = ".".join((newpath, ext)) if not os.path.exists(newpath): return newpath raise VCSTestError("Couldn't compute new path for %s" % path) diff --git a/rhodecode/tests/vcs_operations/__init__.py b/rhodecode/tests/vcs_operations/__init__.py --- a/rhodecode/tests/vcs_operations/__init__.py +++ b/rhodecode/tests/vcs_operations/__init__.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -36,7 +35,6 @@ from rhodecode.lib.str_utils import safe from rhodecode.tests import GIT_REPO, HG_REPO, SVN_REPO DEBUG = True -RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log') REPO_GROUP = 'a_repo_group' HG_REPO_WITH_GROUP = f'{REPO_GROUP}/{HG_REPO}' GIT_REPO_WITH_GROUP = f'{REPO_GROUP}/{GIT_REPO}' diff --git a/rhodecode/tests/vcs_operations/conftest.py b/rhodecode/tests/vcs_operations/conftest.py --- a/rhodecode/tests/vcs_operations/conftest.py +++ b/rhodecode/tests/vcs_operations/conftest.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,22 +18,17 @@ """ py.test config for test suite for making push/pull operations. - -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ import os -import tempfile -import textwrap + +import pyramid.paster import pytest import logging import requests from rhodecode import events -from rhodecode.lib.str_utils import safe_bytes +from rhodecode.lib.type_utils import AttributeDict from rhodecode.model.db import Integration, UserRepoToPerm, Permission, \ UserToRepoBranchPermission, User from rhodecode.model.integration import IntegrationModel @@ -42,11 +36,13 @@ from rhodecode.model.db import Repositor from rhodecode.model.meta import Session from rhodecode.integrations.types.webhook import WebhookIntegrationType + from rhodecode.tests import GIT_REPO, HG_REPO, SVN_REPO from rhodecode.tests.conftest import HTTPBIN_DOMAIN, HTTPBIN_POST -from rhodecode.tests.fixture import Fixture -from rhodecode.tests.server_utils import RcWebServer - +from rhodecode.tests.fixtures.rc_fixture import Fixture +from rhodecode.tests.fixtures.fixture_utils import backend_base +from rhodecode.tests.utils import set_anonymous_access, AuthPluginManager +from rhodecode.tests import console_printer REPO_GROUP = 'a_repo_group' HG_REPO_WITH_GROUP = f'{REPO_GROUP}/{HG_REPO}' @@ -62,39 +58,42 @@ def check_httpbin_connection(): response = requests.get(HTTPBIN_DOMAIN, timeout=5) return response.status_code == 200 except Exception as e: - print(e) + console_printer(e) return False +#overrides backend_N with init_pyramid_app instead of baseapp +@pytest.fixture() +def vcs_backend_git(request, init_pyramid_app, test_repo): + return backend_base(request, 'git', test_repo) + + +@pytest.fixture() +def vcs_backend_hg(request, init_pyramid_app, test_repo): + return backend_base(request, 'hg', test_repo) + + +@pytest.fixture() +def vcs_backend_svn(request, init_pyramid_app, test_repo): + return backend_base(request, 'svn', test_repo) + @pytest.fixture(scope="module") -def rcextensions(request, db_connection, tmpdir_factory): - """ - Installs a testing rcextensions pack to ensure they work as expected. +def tmp_storage_location(request, tmpdir_factory): """ - init_content = textwrap.dedent(""" - # Forward import the example rcextensions to make it - # active for our tests. - from rhodecode.tests.other.example_rcextensions import * - """) + Defines a module level storage_location, used mostly to define per-test persistent repo storage + shared across vcsserver, rhodecode and celery + """ - # Note: rcextensions are looked up based on the path of the ini file - root_path = tmpdir_factory.getbasetemp() - rcextensions_path = root_path.join('rcextensions') - init_path = rcextensions_path.join('__init__.py') - - if rcextensions_path.check(): - pytest.fail( - "Path for rcextensions already exists, please clean up before " - "test run this path: %s" % (rcextensions_path, )) - else: - request.addfinalizer(rcextensions_path.remove) - init_path.write_binary(safe_bytes(init_content), ensure=True) + dest = tmpdir_factory.mktemp('tmp_storage_location_', numbered=True) + log.info("Creating test TMP directory at %s", dest) + return dest @pytest.fixture(scope="module") -def repos(request, db_connection): +def repo_group_repos(request): """Create a copy of each test repo in a repo group.""" + fixture = Fixture() repo_group = fixture.create_repo_group(REPO_GROUP) repo_group_id = repo_group.group_id @@ -116,67 +115,117 @@ def repos(request, db_connection): fixture.destroy_repo_group(repo_group_id) -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [] +@pytest.fixture(scope='module') +def rcstack_vcsserver_factory(vcsserver_factory): + return vcsserver_factory + + +@pytest.fixture(scope='module') +def rcstack_celery_factory(celery_factory): + return celery_factory + + +@pytest.fixture(scope='module') +def rcstack_rhodecode_factory(rhodecode_factory): + return rhodecode_factory -@pytest.fixture(scope="module") -def rc_web_server_config_factory(testini_factory, rc_web_server_config_modification): - """ - Configuration file used for the fixture `rc_web_server`. - """ +@pytest.fixture(scope='module') +def init_pyramid_app(request, available_port_factory, ini_config_factory, rcstack_vcsserver_factory, tmp_storage_location): + from rhodecode.lib.config_utils import get_app_config + from rhodecode.config.middleware import make_pyramid_app - def factory(rcweb_port, vcsserver_port): - custom_params = [ - {'handler_console': {'level': 'DEBUG'}}, - {'server:main': {'port': rcweb_port}}, - {'app:main': {'vcs.server': 'localhost:%s' % vcsserver_port}} - ] - custom_params.extend(rc_web_server_config_modification) - return testini_factory(custom_params) - return factory + store_dir = tmp_storage_location + port = available_port_factory() + rcstack_vcsserver_factory( + request, + store_dir=store_dir, + port=port, + info_prefix='init-app-' + ) + + app_ini_config = ini_config_factory(store_dir) + + pyramid.paster.setup_logging(app_ini_config) + + settings = get_app_config(app_ini_config) + settings['startup.import_repos'] = True + settings['vcs.server'] = f'localhost:{port}' + settings['repo_store.path'] = str(store_dir) + pyramid_app = make_pyramid_app({'__file__': app_ini_config}, **settings) + + return pyramid_app -@pytest.fixture(scope="module") -def rc_web_server( - request, vcsserver_factory, available_port_factory, - rc_web_server_config_factory, repos, rcextensions): +@pytest.fixture(scope='module') +def rcstack(request, tmp_storage_location, rcextensions, available_port_factory, rcstack_vcsserver_factory, rcstack_celery_factory, rcstack_rhodecode_factory): """ - Run the web server as a subprocess. with its own instance of vcsserver + Runs minimal rcstack, i.e vcsserver, celery, rhodecode unpacks rcextensions and repos to a shared location """ - rcweb_port: int = available_port_factory() - log.info('Using rcweb ops test port %s', rcweb_port) + rcstack_data = AttributeDict() + store_dir = tmp_storage_location vcsserver_port: int = available_port_factory() - log.info('Using vcsserver ops test port %s', vcsserver_port) + vcsserver_log = os.path.join(tmp_storage_location, 'vcsserver.log') + + log.info('Using vcsserver test port %s and log %s', vcsserver_port, vcsserver_log) # start vcsserver + _factory = rcstack_vcsserver_factory( + request, + store_dir=store_dir, + port=vcsserver_port, + log_file=vcsserver_log, + overrides=( + {'handler_console': {'level': 'DEBUG'}}, + )) + rcstack_data.vcsserver_port = vcsserver_port + rcstack_data.vcsserver_log = _factory.log_file + - vcs_log = os.path.join(tempfile.gettempdir(), 'rc_op_vcs.log') - vcsserver_factory( - request, vcsserver_port=vcsserver_port, - log_file=vcs_log, + celery_log = os.path.join(tmp_storage_location, 'celery.log') + + + log.info('Using celery log %s', celery_log) + # start celery + _factory = rcstack_celery_factory( + request, + store_dir=store_dir, + port=None, + log_file=celery_log, overrides=( - {'server:main': {'workers': 2}}, - {'server:main': {'graceful_timeout': 10}}, + {'handler_console': {'level': 'DEBUG'}}, + {'app:main': {'vcs.server': f'localhost:{vcsserver_port}'}}, + {'app:main': {'repo_store.path': store_dir}} )) - rc_log = os.path.join(tempfile.gettempdir(), 'rc_op_web.log') - rc_web_server_config = rc_web_server_config_factory( - rcweb_port=rcweb_port, - vcsserver_port=vcsserver_port) - server = RcWebServer(rc_web_server_config, log_file=rc_log) - server.start() + rcstack_data.celery_log = _factory.log_file + + rhodecode_port: int = available_port_factory() + rhodecode_log = os.path.join(tmp_storage_location, 'rhodecode.log') + + + log.info('Using rhodecode test port %s and log %s', rhodecode_port, rhodecode_port) - @request.addfinalizer - def cleanup(): - server.shutdown() + # start rhodecode + rc = rcstack_rhodecode_factory( + request, + store_dir=store_dir, + port=rhodecode_port, + log_file=rhodecode_log, + overrides=( + {'handler_console': {'level': 'DEBUG'}}, + {'app:main': {'vcs.server': f'localhost:{vcsserver_port}'}}, + {'app:main': {'repo_store.path': store_dir}} + )) - server.wait_until_ready() - return server + rcstack_data.rhodecode_port = rhodecode_port + rcstack_data.rhodecode_log = rc.log_file + + rc.rcstack_data = rcstack_data + return rc @pytest.fixture() -def disable_locking(baseapp): +def disable_locking(init_pyramid_app): r = Repository.get_by_repo_name(GIT_REPO) Repository.unlock(r) r.enable_locking = False @@ -191,6 +240,28 @@ def disable_locking(baseapp): @pytest.fixture() +def disable_anonymous_user(request, init_pyramid_app, db_connection): + set_anonymous_access(False) + + @request.addfinalizer + def cleanup(): + set_anonymous_access(True) + + +@pytest.fixture(scope='module') +def enable_auth_plugins(request, init_pyramid_app): + """ + Return a factory object that when called, allows to control which + authentication plugins are enabled. + """ + + enabler = AuthPluginManager() + request.addfinalizer(enabler.cleanup) + + return enabler + + +@pytest.fixture() def fs_repo_only(request, rhodecode_fixtures): def fs_repo_fabric(repo_name, repo_type): rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type) diff --git a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_403.py b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_403.py --- a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_403.py +++ b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_403.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,25 +31,27 @@ from rhodecode.tests import (GIT_REPO, H from rhodecode.tests.vcs_operations import Command -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [ - {'app:main': {'auth_ret_code': '403'}}, - #{'app:main': {'auth_ret_code_detection': 'true'}}, - ] +custom_code = [ + {'app:main': {'auth_ret_code': '403'}}, +] - -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperationsOnCustomIniConfig(object): +@pytest.mark.parametrize('rcstack', custom_code, indirect=True) +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsAuthCode403(object): - def test_clone_wrong_credentials_hg_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_hg_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: HTTP Error 403: Forbidden' in stderr - def test_clone_wrong_credentials_git_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_git_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'The requested URL returned error: 403' in stderr diff --git a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_404.py b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_404.py --- a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_404.py +++ b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_404.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -31,26 +30,27 @@ import pytest from rhodecode.tests import (GIT_REPO, HG_REPO) from rhodecode.tests.vcs_operations import Command - -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [ - {'app:main': {'auth_ret_code': '404'}}, - #{'app:main': {'auth_ret_code_detection': 'false'}}, - ] +custom_code = [ + {'app:main': {'auth_ret_code': '404'}}, +] - -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperationsOnCustomIniConfig(object): +@pytest.mark.parametrize('rcstack', custom_code, indirect=True) +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsOnCustomAuthCode404(object): - def test_clone_wrong_credentials_hg_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_hg_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: HTTP Error 404: Not Found' in stderr - def test_clone_wrong_credentials_git_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_git_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'not found' in stderr diff --git a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_bad_code.py b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_bad_code.py --- a/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_bad_code.py +++ b/rhodecode/tests/vcs_operations/test_vcs_calls_custom_auth_code_bad_code.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,25 +31,27 @@ from rhodecode.tests import (GIT_REPO, H from rhodecode.tests.vcs_operations import Command -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [ - {'app:main': {'auth_ret_code': '600'}}, - #{'app:main': {'auth_ret_code_detection': 'false'}}, - ] +custom_code = [ + {'app:main': {'auth_ret_code': '600'}}, +] - -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") +@pytest.mark.parametrize('rcstack', custom_code, indirect=True) +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) class TestVCSOperationsOnCustomIniConfig(object): - def test_clone_wrong_credentials_hg_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_hg_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr - def test_clone_wrong_credentials_git_ret_code(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_git_ret_code(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'fatal: Authentication failed' in stderr diff --git a/rhodecode/tests/vcs_operations/test_vcs_calls_small_post_buffer.py b/rhodecode/tests/vcs_operations/test_vcs_calls_small_post_buffer.py --- a/rhodecode/tests/vcs_operations/test_vcs_calls_small_post_buffer.py +++ b/rhodecode/tests/vcs_operations/test_vcs_calls_small_post_buffer.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,13 +19,10 @@ """ Test suite for making push/pull operations, on specially modified INI files -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ import os +import pytest from rhodecode.lib.vcs.backends.git.repository import GitRepository from rhodecode.lib.vcs.nodes import FileNode @@ -35,31 +31,39 @@ from rhodecode.tests.vcs_operations impo from .test_vcs_operations_git import _check_proper_clone, _check_proper_git_push -def test_git_clone_with_small_push_buffer(backend_git, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - cmd = Command('/tmp') - stdout, stderr = cmd.execute( - 'git -c http.postBuffer=1024 clone', clone_url, tmpdir.strpath) - _check_proper_clone(stdout, stderr, 'git') - cmd.assert_returncode_success() +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsOnCustomIniConfig(object): + + def test_git_clone_with_small_push_buffer(self, vcs_backend_git, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + cmd = Command(tmpdir.strpath) + stdout, stderr = cmd.execute( + 'git -c http.postBuffer=1024 clone', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'git') + cmd.assert_returncode_success() -def test_git_push_with_small_push_buffer(backend_git, rc_web_server, tmpdir): - empty_repo = backend_git.create_repo() + def test_git_push_with_small_push_buffer(self, vcs_backend_git, rcstack, tmpdir): + empty_repo = vcs_backend_git.create_repo() - clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + clone_url = rcstack.repo_clone_url(empty_repo.repo_name) - cmd = Command(tmpdir.strpath) - cmd.execute('git clone', clone_url) + cmd = Command(tmpdir.strpath) + cmd.execute('git clone', clone_url) - repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) - repo.in_memory_commit.add(FileNode(b'readme.md', content=b'## Hello')) - repo.in_memory_commit.commit( - message='Commit on branch Master', - author='Automatic test ', - branch='master') + repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(b'readme.md', content=b'## Hello')) + repo.in_memory_commit.commit( + message='Commit on branch Master', + author='Automatic test ', + branch='master') - repo_cmd = Command(repo.path) - stdout, stderr = repo_cmd.execute( - f'git -c http.postBuffer=1024 push --verbose {clone_url} master') - _check_proper_git_push(stdout, stderr, branch='master') + repo_cmd = Command(repo.path) + stdout, stderr = repo_cmd.execute( + f'git -c http.postBuffer=1024 push --verbose {clone_url} master') + _check_proper_git_push(stdout, stderr, branch='master') diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_bad_client.py b/rhodecode/tests/vcs_operations/test_vcs_operations_bad_client.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_bad_client.py @@ -0,0 +1,91 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Test suite for making push/pull operations, on specially modified INI files +""" + +import pytest +from importlib.metadata import version + +from rhodecode.model.meta import Session +from rhodecode.model.settings import SettingsModel + +from rhodecode.tests import GIT_REPO, HG_REPO +from rhodecode.tests.vcs_operations import Command, _add_files_and_push + + +@pytest.fixture() +def bad_client_setter_factory(request): + def _factory(client_type, client_str_val): + # set allowed clients + setting = SettingsModel().create_or_update_setting(name=f"{client_type}_allowed_clients", val=client_str_val) + Session().add(setting) + Session().commit() + + @request.addfinalizer + def cleanup(): + setting2 = SettingsModel().create_or_update_setting(name=f"{client_type}_allowed_clients", val="*") + Session().add(setting2) + Session().commit() + + return _factory + + +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsOnUsingBadClient(object): + def test_push_with_bad_client_repo_by_other_user_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) + + # set allowed clients + setting = SettingsModel().create_or_update_setting(name=f"hg_allowed_clients", val="0.0.0") + Session().add(setting) + Session().commit() + + # push fails repo is locked by other user ! + push_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=push_url) + current_version = version('mercurial') + msg = f"Your hg client (version=mercurial/proto-1.0 (Mercurial {current_version})) is forbidden by security rules" + assert msg in stderr + + def test_push_with_bad_client_repo_by_other_user_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) + + # set allowed clients + setting = SettingsModel().create_or_update_setting(name=f"git_allowed_clients", val="0.0.0") + Session().add(setting) + Session().commit() + + # push fails repo is locked by other user! + push_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=push_url) + + err = "Your git client (version=git/2.45.2) is forbidden by security rules" + assert err in stderr + + @pytest.mark.xfail(reason="Lack of proper SVN support of cloning") + def test_push_with_bad_client_repo_by_other_user_svn(self, rcstack, tmpdir): + raise NotImplementedError("lacks svn support") diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_branch_protection.py b/rhodecode/tests/vcs_operations/test_vcs_operations_branch_protection.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_branch_protection.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_branch_protection.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,7 +26,12 @@ from rhodecode.tests.vcs_operations impo Command, _check_proper_hg_push, _check_proper_git_push, _add_files_and_push) -@pytest.mark.usefixtures("disable_anonymous_user") +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) class TestVCSOperations(object): @pytest.mark.parametrize('username, password', [ @@ -41,13 +45,13 @@ class TestVCSOperations(object): 'branch.push_force', ]) def test_push_to_protected_branch_fails_with_message_hg( - self, rc_web_server, tmpdir, branch_perm, user_util, + self, rcstack, tmpdir, branch_perm, user_util, branch_permission_setter, username, password): repo = user_util.create_repo(repo_type='hg') repo_name = repo.repo_name branch_permission_setter(repo_name, username, permission=branch_perm) - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( repo.repo_name, user=username, passwd=password) Command(os.path.dirname(tmpdir.strpath)).execute( 'hg clone', clone_url, tmpdir.strpath) @@ -58,8 +62,8 @@ class TestVCSOperations(object): _check_proper_hg_push(stdout, stderr) else: msg = f"Branch `default` changes rejected by rule `*`=>{branch_perm}" - assert msg in stdout - assert "transaction abort" in stdout + assert msg in stderr + #assert "transaction abort" in stdout @pytest.mark.parametrize('username, password', [ (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS), @@ -72,13 +76,13 @@ class TestVCSOperations(object): 'branch.push_force', ]) def test_push_to_protected_branch_fails_with_message_git( - self, rc_web_server, tmpdir, branch_perm, user_util, + self, rcstack, tmpdir, branch_perm, user_util, branch_permission_setter, username, password): repo = user_util.create_repo(repo_type='git') repo_name = repo.repo_name branch_permission_setter(repo_name, username, permission=branch_perm) - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( repo.repo_name, user=username, passwd=password) Command(os.path.dirname(tmpdir.strpath)).execute( 'git clone', clone_url, tmpdir.strpath) diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_by_auth_tokens.py b/rhodecode/tests/vcs_operations/test_vcs_operations_by_auth_tokens.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_by_auth_tokens.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_by_auth_tokens.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -20,10 +19,6 @@ """ Test suite for making push/pull operations, on specially modified INI files -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ import pytest @@ -36,10 +31,15 @@ from rhodecode.tests import (GIT_REPO, H from rhodecode.tests.vcs_operations import (Command, _check_proper_clone) -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperations(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsByAuthTokens: def test_clone_by_auth_token( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', @@ -49,16 +49,16 @@ class TestVCSOperations(object): user = user_util.create_user() token = user.auth_tokens[1] - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') def test_clone_by_auth_token_expired( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', 'egg:rhodecode-enterprise-ce#rhodecode' @@ -69,18 +69,18 @@ class TestVCSOperations(object): user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS) token = auth_token.api_key - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr msg = 'reason: bad or inactive token.' - rc_web_server.assert_message_in_server_logs(msg) + rcstack.assert_message_in_server_logs(msg) def test_clone_by_auth_token_bad_role( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', 'egg:rhodecode-enterprise-ce#rhodecode' @@ -91,15 +91,15 @@ class TestVCSOperations(object): user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API) token = auth_token.api_key - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr def test_clone_by_auth_token_user_disabled( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', 'egg:rhodecode-enterprise-ce#rhodecode' @@ -111,18 +111,18 @@ class TestVCSOperations(object): Session().commit() token = user.auth_tokens[1] - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr msg = 'reason: account not active.' - rc_web_server.assert_message_in_server_logs(msg) + rcstack.assert_message_in_server_logs(msg) def test_clone_by_auth_token_with_scope( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', 'egg:rhodecode-enterprise-ce#rhodecode' @@ -138,15 +138,15 @@ class TestVCSOperations(object): Session().add(auth_token) Session().commit() - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') def test_clone_by_auth_token_with_wrong_scope( - self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + self, rcstack, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins.enable([ 'egg:rhodecode-enterprise-ce#token', 'egg:rhodecode-enterprise-ce#rhodecode' @@ -162,13 +162,13 @@ class TestVCSOperations(object): Session().add(auth_token) Session().commit() - clone_url = rc_web_server.repo_clone_url( + clone_url = rcstack.repo_clone_url( HG_REPO, user=user.username, passwd=token) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr msg = 'reason: bad or inactive token.' - rc_web_server.assert_message_in_server_logs(msg) + rcstack.assert_message_in_server_logs(msg) diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_clone.py b/rhodecode/tests/vcs_operations/test_vcs_operations_clone.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_clone.py @@ -0,0 +1,62 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Test suite for making push/pull operations, on specially modified INI files +""" + +import pytest + +from rhodecode.tests import GIT_REPO, SVN_REPO, HG_REPO + +from rhodecode.tests.vcs_operations import (Command, _check_proper_clone) +from rhodecode.tests.vcs_operations.test_vcs_operations_svn import get_cli_flags + + +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsClone: + + def test_clone_git_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + cmd = Command(tmpdir.strpath) + stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'git') + cmd.assert_returncode_success() + + def test_clone_hg_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + cmd = Command(tmpdir.strpath) + stdout, stderr = cmd.execute('hg clone', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'hg') + cmd.assert_returncode_success() + + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_svn_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) + username, password = rcstack.repo_clone_credentials() + flags, auth = get_cli_flags(username, password) + cmd = Command(tmpdir.strpath) + stdout, stderr = cmd.execute( + f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'svn') + cmd.assert_returncode_success() diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_force_push.py b/rhodecode/tests/vcs_operations/test_vcs_operations_force_push.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_force_push.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_force_push.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -27,12 +26,17 @@ from rhodecode.tests.vcs_operations impo _add_files, _add_files_and_push) -@pytest.mark.usefixtures("disable_anonymous_user") +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) class TestVCSOperations(object): - def test_push_force_hg(self, rc_web_server, tmpdir, user_util): + def test_push_force_hg(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='hg') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'hg clone', clone_url, tmpdir.strpath) @@ -50,9 +54,9 @@ class TestVCSOperations(object): _check_proper_hg_push(stdout, stderr) - def test_push_force_git(self, rc_web_server, tmpdir, user_util): + def test_push_force_git(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='git') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'git clone', clone_url, tmpdir.strpath) @@ -69,13 +73,12 @@ class TestVCSOperations(object): assert '(forced update)' in stderr def test_push_force_hg_blocked_by_branch_permissions( - self, rc_web_server, tmpdir, user_util, branch_permission_setter): + self, rcstack, tmpdir, user_util, branch_permission_setter): repo = user_util.create_repo(repo_type='hg') repo_name = repo.repo_name username = TEST_USER_ADMIN_LOGIN - branch_permission_setter(repo_name, username, permission='branch.push') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'hg clone', clone_url, tmpdir.strpath) @@ -88,21 +91,21 @@ class TestVCSOperations(object): 'hg checkout -r 1 && hg commit -m "starting new head"') _add_files('hg', tmpdir.strpath, clone_url=clone_url) + branch_permission_setter(repo_name, username, permission='branch.push') stdout, stderr = Command(tmpdir.strpath).execute( f'hg push --verbose -f {clone_url}') - assert "Branch `default` changes rejected by rule `*`=>branch.push" in stdout - assert "FORCE PUSH FORBIDDEN" in stdout - assert "transaction abort" in stdout + assert "Branch `default` changes rejected by rule `*`=>branch.push" in stderr + assert "FORCE PUSH FORBIDDEN" in stderr def test_push_force_git_blocked_by_branch_permissions( - self, rc_web_server, tmpdir, user_util, branch_permission_setter): + self, rcstack, tmpdir, user_util, branch_permission_setter): repo = user_util.create_repo(repo_type='git') repo_name = repo.repo_name username = TEST_USER_ADMIN_LOGIN branch_permission_setter(repo_name, username, permission='branch.push') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'git clone', clone_url, tmpdir.strpath) diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_git.py b/rhodecode/tests/vcs_operations/test_vcs_operations_git.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_git.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_git.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -25,8 +24,6 @@ Test suite for making push/pull operatio You must have git >= 1.8.5 for tests to work fine. With 68b939b git started to redirect things to stderr instead of stdout. """ - - import time import pytest @@ -42,41 +39,46 @@ from rhodecode.tests.vcs_operations impo _add_files_and_push, GIT_REPO_WITH_GROUP) -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperations(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsGit: - def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - cmd = Command('/tmp') + def test_clone_git_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'git') cmd.assert_returncode_success() - def test_clone_git_repo_by_admin_with_git_suffix(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - cmd = Command('/tmp') + def test_clone_git_repo_by_admin_with_git_suffix(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute('git clone', clone_url+".git", tmpdir.strpath) _check_proper_clone(stdout, stderr, 'git') cmd.assert_returncode_success() - def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir): + def test_clone_git_repo_by_id_by_admin(self, rcstack, tmpdir): repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id - clone_url = rc_web_server.repo_clone_url('_%s' % repo_id) - cmd = Command('/tmp') + clone_url = rcstack.repo_clone_url('_%s' % repo_id) + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'git') cmd.assert_returncode_success() - def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP) - cmd = Command('/tmp') + def test_clone_git_repo_with_group_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO_WITH_GROUP) + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'git') cmd.assert_returncode_success() - def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - cmd = Command('/tmp') + def test_clone_git_repo_shallow_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute( 'git clone --depth=1', clone_url, tmpdir.strpath) @@ -84,65 +86,64 @@ class TestVCSOperations(object): assert 'Cloning into' in stderr cmd.assert_returncode_success() - - def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'fatal: Authentication failed' in stderr - def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + def test_clone_git_dir_as_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('trololo') - stdout, stderr = Command('/tmp').execute( + def test_clone_non_existing_path_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('trololo') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_clone_non_existing_path_git(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('trololo') - stdout, stderr = Command('/tmp').execute('git clone', clone_url) + def test_clone_non_existing_path_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('trololo') + stdout, stderr = Command(tmpdir.strpath).execute('git clone', clone_url) assert 'not found' in stderr - def test_clone_git_with_slashes(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('//' + GIT_REPO) - stdout, stderr = Command('/tmp').execute('git clone', clone_url) + def test_clone_git_with_slashes(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('//' + GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute('git clone', clone_url) assert 'not found' in stderr def test_clone_existing_path_git_not_in_database( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-git', repo_type='git') - clone_url = rc_web_server.repo_clone_url(db_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(db_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'not found' in stderr def test_clone_existing_path_git_not_in_database_different_scm( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-hg', repo_type='hg') - clone_url = rc_web_server.repo_clone_url(db_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(db_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'not found' in stderr - def test_clone_non_existing_store_path_git(self, rc_web_server, tmpdir, user_util): + def test_clone_non_existing_store_path_git(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='git') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) # Damage repo by removing it's folder RepoModel()._delete_filesystem_repo(repo) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) assert 'not found' in stderr - def test_push_new_file_git(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_new_file_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) # commit some stuff into this repo @@ -151,37 +152,37 @@ class TestVCSOperations(object): _check_proper_git_push(stdout, stderr) - def test_push_wrong_credentials_git(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_wrong_credentials_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url( + push_url = rcstack.repo_clone_url( GIT_REPO, user='bad', passwd='name') stdout, stderr = _add_files_and_push( 'git', tmpdir.strpath, clone_url=push_url) assert 'fatal: Authentication failed' in stderr - def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_back_to_wrong_url_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( 'git', tmpdir.strpath, - clone_url=rc_web_server.repo_clone_url('not-existing')) + clone_url=rcstack.repo_clone_url('not-existing')) assert 'not found' in stderr - def test_ip_restriction_git(self, rc_web_server, tmpdir): + def test_ip_restriction_git(self, rcstack, tmpdir): user_model = UserModel() try: user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32') Session().commit() time.sleep(2) - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) msg = "The requested URL returned error: 403" assert msg in stderr @@ -193,7 +194,7 @@ class TestVCSOperations(object): time.sleep(2) - cmd = Command('/tmp') + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath) cmd.assert_returncode_success() _check_proper_clone(stdout, stderr, 'git') diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_hg.py b/rhodecode/tests/vcs_operations/test_vcs_operations_hg.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_hg.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_hg.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,11 +18,6 @@ """ Test suite for making push/pull operations, on specially modified INI files - -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ @@ -42,96 +36,101 @@ from rhodecode.tests.vcs_operations impo Command, _check_proper_clone, _add_files_and_push, HG_REPO_WITH_GROUP) -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperations(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsHg(object): - def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_clone_hg_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') - def test_clone_hg_repo_by_admin_pull_protocol(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_clone_hg_repo_by_admin_pull_protocol(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone --pull', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') - def test_clone_hg_repo_by_admin_pull_stream_protocol(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_clone_hg_repo_by_admin_pull_stream_protocol(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone --pull --stream', clone_url, tmpdir.strpath) assert 'files to transfer,' in stdout assert 'transferred 1.' in stdout assert '114 files updated,' in stdout - def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir): + def test_clone_hg_repo_by_id_by_admin(self, rcstack, tmpdir): repo_id = Repository.get_by_repo_name(HG_REPO).repo_id - clone_url = rc_web_server.repo_clone_url('_%s' % repo_id) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url('_%s' % repo_id) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') - def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP) - stdout, stderr = Command('/tmp').execute( + def test_clone_hg_repo_with_group_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO_WITH_GROUP) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') - def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!') - stdout, stderr = Command('/tmp').execute( + def test_clone_wrong_credentials_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO, passwd='bad!') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr - def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + def test_clone_git_dir_as_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('trololo') - stdout, stderr = Command('/tmp').execute( + def test_clone_non_existing_path_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('trololo') + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_clone_hg_with_slashes(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('//' + HG_REPO) - stdout, stderr = Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath) + def test_clone_hg_with_slashes(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('//' + HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute('hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr def test_clone_existing_path_hg_not_in_database( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-hg', repo_type='hg') - clone_url = rc_web_server.repo_clone_url(db_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(db_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr def test_clone_existing_path_hg_not_in_database_different_scm( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-git', repo_type='git') - clone_url = rc_web_server.repo_clone_url(db_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(db_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_clone_non_existing_store_path_hg(self, rc_web_server, tmpdir, user_util): + def test_clone_non_existing_store_path_hg(self, rcstack, tmpdir, user_util): repo = user_util.create_repo() - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) # Damage repo by removing it's folder RepoModel()._delete_filesystem_repo(repo) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'HTTP Error 404: Not Found' in stderr - def test_push_new_file_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_new_file_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( @@ -140,7 +139,7 @@ class TestVCSOperations(object): assert 'pushing to' in stdout assert 'size summary' in stdout - def test_push_invalidates_cache(self, rc_web_server, tmpdir): + def test_push_invalidates_cache(self, rcstack, tmpdir): hg_repo = Repository.get_by_repo_name(HG_REPO) # init cache objects @@ -159,8 +158,8 @@ class TestVCSOperations(object): old_ids = [x.cache_state_uid for x in cache_keys] # clone to init cache - clone_url = rc_web_server.repo_clone_url(hg_repo.repo_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(hg_repo.repo_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) cache_keys = hg_repo.cache_keys @@ -180,37 +179,37 @@ class TestVCSOperations(object): new_ids = [x.cache_state_uid for x in cache_keys] assert new_ids != old_ids - def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_wrong_credentials_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url( + push_url = rcstack.repo_clone_url( HG_REPO, user='bad', passwd='name') stdout, stderr = _add_files_and_push( 'hg', tmpdir.strpath, clone_url=push_url) assert 'abort: authorization failed' in stderr - def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_back_to_wrong_url_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( 'hg', tmpdir.strpath, - clone_url=rc_web_server.repo_clone_url('not-existing')) + clone_url=rcstack.repo_clone_url('not-existing')) assert 'HTTP Error 404: Not Found' in stderr - def test_ip_restriction_hg(self, rc_web_server, tmpdir): + def test_ip_restriction_hg(self, rcstack, tmpdir): user_model = UserModel() try: user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32') Session().commit() time.sleep(2) - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) assert 'abort: HTTP Error 403: Forbidden' in stderr finally: @@ -221,6 +220,6 @@ class TestVCSOperations(object): time.sleep(2) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'hg') diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_integrations_trigger.py b/rhodecode/tests/vcs_operations/test_vcs_operations_integrations_trigger.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_integrations_trigger.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_integrations_trigger.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,11 +18,6 @@ """ Test suite for making push/pull operations, on specially modified INI files - -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ import pytest @@ -37,32 +31,41 @@ from rhodecode.tests.vcs_operations.conf connection_available = pytest.mark.skipif( not check_httpbin_connection(), reason="No outside internet connection available") - -@pytest.mark.usefixtures("baseapp", "enable_webhook_push_integration") -class TestVCSOperationsOnCustomIniConfig(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", + "enable_webhook_push_integration" +) +class TestVCSOperationsOnIntegrationsTrigger(object): - def test_push_with_webhook_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) + def test_push_with_webhook_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) - Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath) + Command(tmpdir.strpath).execute('hg clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url(HG_REPO) + push_url = rcstack.repo_clone_url(HG_REPO) _add_files_and_push('hg', tmpdir.strpath, clone_url=push_url) - rc_log = rc_web_server.get_rc_log() - assert 'ERROR' not in rc_log - assert "executing task TASK:<@task: rhodecode.integrations.types.webhook.post_to_webhook" in rc_log - assert "handling event repo-push with integration = 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ - import pytest from rhodecode.model.db import User, Repository from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel -from rhodecode.tests import ( - GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, - TEST_USER_REGULAR_PASS) -from rhodecode.tests.vcs_operations import ( - Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push) +from rhodecode.tests import GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS +from rhodecode.tests.vcs_operations import Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [ - {'app:main': {'lock_ret_code': '423'}}, - ] +custom_code = [ + {'app:main': {'auth_ret_code': '423'}}, +] - -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperationsOnCustomIniConfig(object): - - def test_clone_and_create_lock_hg(self, rc_web_server, tmpdir): +@pytest.mark.parametrize('rcstack', custom_code, indirect=True) +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsOnLockingRepos(object): + def test_clone_and_create_lock_hg(self, rcstack, tmpdir): # enable locking r = Repository.get_by_repo_name(HG_REPO) r.enable_locking = True Session().add(r) Session().commit() # clone - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) # check if lock was made r = Repository.get_by_repo_name(HG_REPO) - assert r.locked[0] == User.get_by_username( - TEST_USER_ADMIN_LOGIN).user_id + assert r.locked[0] == User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id - def test_clone_and_create_lock_git(self, rc_web_server, tmpdir): + def test_clone_and_create_lock_git(self, rcstack, tmpdir): # enable locking r = Repository.get_by_repo_name(GIT_REPO) r.enable_locking = True Session().add(r) Session().commit() # clone - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) # check if lock was made r = Repository.get_by_repo_name(GIT_REPO) - assert r.locked[0] == User.get_by_username( - TEST_USER_ADMIN_LOGIN).user_id + assert r.locked[0] == User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id - def test_clone_after_repo_was_locked_hg(self, rc_web_server, tmpdir): + def test_clone_after_repo_was_locked_hg(self, rcstack, tmpdir): # lock repo r = Repository.get_by_repo_name(HG_REPO) Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # pull fails since repo is locked - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) - msg = ("""abort: HTTP Error 423: Repository `%s` locked by user `%s`""" - % (HG_REPO, TEST_USER_ADMIN_LOGIN)) + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) + msg = f"abort: HTTP Error 423: Repository `{HG_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" assert msg in stderr - def test_clone_after_repo_was_locked_git(self, rc_web_server, tmpdir): + def test_clone_after_repo_was_locked_git(self, rcstack, tmpdir): # lock repo r = Repository.get_by_repo_name(GIT_REPO) Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # pull fails since repo is locked - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) - lock_msg = ( - 'remote: ERROR: Repository `vcs_test_git` locked by user ' + - '`test_admin`. Reason:`lock_auto`') + lock_msg = "remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`" assert lock_msg in stderr - assert 'remote: Pre pull hook failed: aborting' in stderr - assert 'fatal: remote did not send all necessary objects' in stderr + assert "fatal: remote did not send all necessary objects" in stderr + assert "remote: Pre pull hook failed: aborting" in stderr - def test_push_on_locked_repo_by_other_user_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) + def test_push_on_locked_repo_by_other_user_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) # lock repo r = Repository.get_by_repo_name(HG_REPO) # let this user actually push ! - RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, - perm='repository.write') + RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, perm="repository.write") Session().commit() Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # push fails repo is locked by other user ! - push_url = rc_web_server.repo_clone_url( - HG_REPO, - user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) - stdout, stderr = _add_files_and_push( - 'hg', tmpdir.strpath, clone_url=push_url) - msg = ("""abort: HTTP Error 423: Repository `%s` locked by user `%s`""" - % (HG_REPO, TEST_USER_ADMIN_LOGIN)) + push_url = rcstack.repo_clone_url(HG_REPO, user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) + stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=push_url) + msg = f"abort: HTTP Error 423: Repository `{HG_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" assert msg in stderr - def test_push_on_locked_repo_by_other_user_git( - self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) + def test_push_on_locked_repo_by_other_user_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) # lock repo r = Repository.get_by_repo_name(GIT_REPO) # let this user actually push ! - RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, - perm='repository.write') + RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, perm="repository.write") Session().commit() Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # push fails repo is locked by other user! - push_url = rc_web_server.repo_clone_url( - GIT_REPO, - user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) - stdout, stderr = _add_files_and_push( - 'git', tmpdir.strpath, clone_url=push_url) + push_url = rcstack.repo_clone_url(GIT_REPO, user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) + stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=push_url) - err = 'Repository `%s` locked by user `%s`' % ( - GIT_REPO, TEST_USER_ADMIN_LOGIN) + err = f"Repository `{GIT_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" # err = 'RPC failed; result=22, HTTP code = 423' assert err in stderr - def test_push_unlocks_repository_hg(self, rc_web_server, tmpdir): + def test_push_unlocks_repository_hg(self, rcstack, tmpdir): # enable locking r = Repository.get_by_repo_name(HG_REPO) r.enable_locking = True Session().add(r) Session().commit() - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) - _check_proper_clone(stdout, stderr, 'hg') + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, "hg") # check for lock repo after clone r = Repository.get_by_repo_name(HG_REPO) @@ -177,16 +146,14 @@ class TestVCSOperationsOnCustomIniConfig assert r.locked[0] == uid # push is ok and repo is now unlocked - stdout, stderr = _add_files_and_push( - 'hg', tmpdir.strpath, clone_url=clone_url) - assert ('remote: Released lock on repo `%s`' % HG_REPO) in stdout + stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=clone_url) + assert f"remote: Released lock on repo `{HG_REPO}`" in stdout # we need to cleanup the Session Here ! Session.remove() r = Repository.get_by_repo_name(HG_REPO) assert r.locked == [None, None, None] - def test_push_unlocks_repository_git(self, rc_web_server, tmpdir): - + def test_push_unlocks_repository_git(self, rcstack, tmpdir): # Note: Did a first debugging session. Seems that # Repository.get_locking_state is called twice. The second call # has the action "pull" and does not reset the lock. @@ -197,19 +164,16 @@ class TestVCSOperationsOnCustomIniConfig Session().add(r) Session().commit() - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) - _check_proper_clone(stdout, stderr, 'git') + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, "git") # check for lock repo after clone r = Repository.get_by_repo_name(GIT_REPO) - assert r.locked[0] == User.get_by_username( - TEST_USER_ADMIN_LOGIN).user_id + assert r.locked[0] == User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id # push is ok and repo is now unlocked - stdout, stderr = _add_files_and_push( - 'git', tmpdir.strpath, clone_url=clone_url) + stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=clone_url) _check_proper_git_push(stdout, stderr) # assert ('remote: Released lock on repo `%s`' % GIT_REPO) in stdout diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_locking_custom_code.py b/rhodecode/tests/vcs_operations/test_vcs_operations_locking_custom_code.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_locking_custom_code.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_locking_custom_code.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,102 +25,83 @@ Test suite for making push/pull operatio to redirect things to stderr instead of stdout. """ - import pytest from rhodecode.model.db import User, Repository from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel -from rhodecode.tests import ( - GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, - TEST_USER_REGULAR_PASS) +from rhodecode.tests import GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS from rhodecode.tests.vcs_operations import Command, _add_files_and_push +custom_code = [ + {'app:main': {'lock_ret_code': '400'}}, +] -@pytest.fixture(scope="module") -def rc_web_server_config_modification(): - return [ - {'app:main': {'lock_ret_code': '400'}}, - ] - - -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperationsOnCustomIniConfig(object): - - def test_clone_after_repo_was_locked_hg(self, rc_web_server, tmpdir): +@pytest.mark.parametrize('rcstack', custom_code, indirect=True) +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsOnCustomLockingCode(object): + def test_clone_after_repo_was_locked_hg(self, rcstack, tmpdir): # lock repo r = Repository.get_by_repo_name(HG_REPO) Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # pull fails since repo is locked - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) - msg = ("""abort: HTTP Error 400: Repository `%s` locked by user `%s`""" - % (HG_REPO, TEST_USER_ADMIN_LOGIN)) + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) + msg = f"abort: HTTP Error 400: Repository `{HG_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" assert msg in stderr - def test_clone_after_repo_was_locked_git(self, rc_web_server, tmpdir): + def test_clone_after_repo_was_locked_git(self, rcstack, tmpdir): # lock repo r = Repository.get_by_repo_name(GIT_REPO) Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # pull fails since repo is locked - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) - lock_msg = ( - 'remote: ERROR: Repository `vcs_test_git` locked by user ' + - '`test_admin`. Reason:`lock_auto`') + lock_msg = "remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`" assert lock_msg in stderr - assert 'remote: Pre pull hook failed: aborting' in stderr - assert 'fatal: remote did not send all necessary objects' in stderr + assert "remote: Pre pull hook failed: aborting" in stderr + assert "fatal: remote did not send all necessary objects" in stderr - def test_push_on_locked_repo_by_other_user_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( - 'hg clone', clone_url, tmpdir.strpath) + def test_push_on_locked_repo_by_other_user_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("hg clone", clone_url, tmpdir.strpath) # lock repo r = Repository.get_by_repo_name(HG_REPO) # let this user actually push ! - RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, - perm='repository.write') + RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, perm="repository.write") Session().commit() Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # push fails repo is locked by other user ! - push_url = rc_web_server.repo_clone_url( - HG_REPO, - user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) - stdout, stderr = _add_files_and_push( - 'hg', tmpdir.strpath, clone_url=push_url) - msg = ("""abort: HTTP Error 400: Repository `%s` locked by user `%s`""" - % (HG_REPO, TEST_USER_ADMIN_LOGIN)) + push_url = rcstack.repo_clone_url(HG_REPO, user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) + stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=push_url) + msg = f"abort: HTTP Error 400: Repository `{HG_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" + assert msg in stderr - def test_push_on_locked_repo_by_other_user_git( - self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( - 'git clone', clone_url, tmpdir.strpath) + def test_push_on_locked_repo_by_other_user_git(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute("git clone", clone_url, tmpdir.strpath) # lock repo r = Repository.get_by_repo_name(GIT_REPO) # let this user actually push ! - RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, - perm='repository.write') + RepoModel().grant_user_permission(repo=r, user=TEST_USER_REGULAR_LOGIN, perm="repository.write") Session().commit() Repository.lock(r, User.get_by_username(TEST_USER_ADMIN_LOGIN).user_id) # push fails repo is locked by other user! - push_url = rc_web_server.repo_clone_url( - GIT_REPO, - user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) - stdout, stderr = _add_files_and_push( - 'git', tmpdir.strpath, clone_url=push_url) + push_url = rcstack.repo_clone_url(GIT_REPO, user=TEST_USER_REGULAR_LOGIN, passwd=TEST_USER_REGULAR_PASS) + stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=push_url) - err = 'Repository `%s` locked by user `%s`' % ( - GIT_REPO, TEST_USER_ADMIN_LOGIN) + err = f"Repository `{GIT_REPO}` locked by user `{TEST_USER_ADMIN_LOGIN}`" assert err in stderr diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_new_branch_push.py b/rhodecode/tests/vcs_operations/test_vcs_operations_new_branch_push.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_new_branch_push.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_new_branch_push.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -26,12 +25,16 @@ from rhodecode.tests.vcs_operations impo Command, _check_proper_hg_push, _check_proper_git_push, _add_files_and_push) -@pytest.mark.usefixtures("disable_anonymous_user") -class TestVCSOperations(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", +) +class TestVCSOperationsNewBranchPush(object): - def test_push_new_branch_hg(self, rc_web_server, tmpdir, user_util): + def test_push_new_branch_hg(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='hg') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'hg clone', clone_url, tmpdir.strpath) @@ -48,9 +51,9 @@ class TestVCSOperations(object): _check_proper_hg_push(stdout, stderr) - def test_push_new_branch_git(self, rc_web_server, tmpdir, user_util): + def test_push_new_branch_git(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='git') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'git clone', clone_url, tmpdir.strpath) @@ -67,13 +70,13 @@ class TestVCSOperations(object): _check_proper_git_push(stdout, stderr, branch='dev') def test_push_new_branch_hg_with_branch_permissions_no_force_push( - self, rc_web_server, tmpdir, user_util, branch_permission_setter): + self, rcstack, tmpdir, user_util, branch_permission_setter): repo = user_util.create_repo(repo_type='hg') repo_name = repo.repo_name username = TEST_USER_ADMIN_LOGIN branch_permission_setter(repo_name, username, permission='branch.push') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'hg clone', clone_url, tmpdir.strpath) @@ -91,13 +94,13 @@ class TestVCSOperations(object): _check_proper_hg_push(stdout, stderr) def test_push_new_branch_git_with_branch_permissions_no_force_push( - self, rc_web_server, tmpdir, user_util, branch_permission_setter): + self, rcstack, tmpdir, user_util, branch_permission_setter): repo = user_util.create_repo(repo_type='git') repo_name = repo.repo_name username = TEST_USER_ADMIN_LOGIN branch_permission_setter(repo_name, username, permission='branch.push') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) Command(os.path.dirname(tmpdir.strpath)).execute( 'git clone', clone_url, tmpdir.strpath) diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_rcextensions_push.py b/rhodecode/tests/vcs_operations/test_vcs_operations_rcextensions_push.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_rcextensions_push.py @@ -0,0 +1,81 @@ +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import os +import pytest + +from rhodecode.tests.fixtures.rcextensions_fixtures import store_rcextensions +from rhodecode.tests.vcs_operations import ( + Command, _check_proper_hg_push, _check_proper_git_push, + _add_files_and_push) + + +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsWithRCExtensions(object): + + def test_push_when_rcextensions_fail_hg(self, rcstack, tmpdir, user_util, rcextensions, rcextensions_modification): + repo = user_util.create_repo(repo_type='hg') + clone_url = rcstack.repo_clone_url(repo.repo_name) + Command(os.path.dirname(tmpdir.strpath)).execute( + 'hg clone', clone_url, tmpdir.strpath) + + stdout, stderr = _add_files_and_push( + 'hg', tmpdir.strpath, clone_url=clone_url) + _check_proper_hg_push(stdout, stderr) + + mods = [ + ('_pre_push_hook', + """ + return HookResponse(1, 'HOOK_FAIL_TEST_HG') + """) + ] + rcstack_location = os.path.dirname(rcstack.config_file) + with rcextensions_modification(rcstack_location, mods): + stdout, stderr = _add_files_and_push( + 'hg', tmpdir.strpath, clone_url=clone_url) + assert 'HOOK_FAIL_TEST_HG' in stdout + + def test_push_when_rcextensions_fail_git(self, rcstack, tmpdir, user_util, rcextensions, rcextensions_modification): + repo = user_util.create_repo(repo_type='git') + clone_url = rcstack.repo_clone_url(repo.repo_name) + Command(os.path.dirname(tmpdir.strpath)).execute( + 'git clone', clone_url, tmpdir.strpath) + + stdout, stderr = _add_files_and_push( + 'git', tmpdir.strpath, clone_url=clone_url) + _check_proper_git_push(stdout, stderr) + + mods = [ + ('_pre_push_hook', + """ + return HookResponse(1, 'HOOK_FAIL_TEST_GIT') + """) + ] + + rcstack_location = os.path.dirname(rcstack.config_file) + with rcextensions_modification(rcstack_location, mods): + stdout, stderr = _add_files_and_push( + 'git', tmpdir.strpath, clone_url=clone_url) + assert 'HOOK_FAIL_TEST_GIT' in stderr + diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_special.py b/rhodecode/tests/vcs_operations/test_vcs_operations_special.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_special.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_special.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -32,13 +31,16 @@ from rhodecode.tests.vcs_operations impo _add_files_and_push) -@pytest.mark.usefixtures("disable_locking") +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_locking", +) class TestVCSOperationsSpecial(object): - def test_git_sets_default_branch_if_not_master( - self, backend_git, tmpdir, rc_web_server): - empty_repo = backend_git.create_repo() - clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + def test_git_sets_default_branch_if_not_master(self, vcs_backend_git, tmpdir, rcstack): + empty_repo = vcs_backend_git.create_repo() + clone_url = rcstack.repo_clone_url(empty_repo.repo_name) cmd = Command(tmpdir.strpath) cmd.execute('git clone', clone_url) @@ -63,25 +65,24 @@ class TestVCSOperationsSpecial(object): # Doing an explicit commit in order to get latest user logs on MySQL Session().commit() - def test_git_fetches_from_remote_repository_with_annotated_tags( - self, backend_git, rc_web_server): + def test_git_fetches_from_remote_repository_with_annotated_tags(self, vcs_backend_git, rcstack): # Note: This is a test specific to the git backend. It checks the # integration of fetching from a remote repository which contains # annotated tags. # Dulwich shows this specific behavior only when # operating against a remote repository. - source_repo = backend_git['annotated-tag'] - target_vcs_repo = backend_git.create_repo().scm_instance() - target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name)) + source_repo = vcs_backend_git['annotated-tag'] + target_vcs_repo = vcs_backend_git.create_repo().scm_instance() + target_vcs_repo.fetch(rcstack.repo_clone_url(source_repo.repo_name)) - def test_git_push_shows_pull_request_refs(self, backend_git, rc_web_server, tmpdir): + def test_git_push_shows_pull_request_refs(self, vcs_backend_git, rcstack, tmpdir): """ test if remote info about refs is visible """ - empty_repo = backend_git.create_repo() + empty_repo = vcs_backend_git.create_repo() - clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + clone_url = rcstack.repo_clone_url(empty_repo.repo_name) cmd = Command(tmpdir.strpath) cmd.execute('git clone', clone_url) @@ -97,7 +98,7 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('git push --verbose origin master') _check_proper_git_push(stdout, stderr, branch='master') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=master' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=master' assert f'remote: RhodeCode: open pull request link: {ref}' in stderr assert 'remote: RhodeCode: push completed' in stderr @@ -129,14 +130,14 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('git push --verbose origin feature') _check_proper_git_push(stdout, stderr, branch='feature') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=feature' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=feature' assert f'remote: RhodeCode: open pull request link: {ref}' in stderr assert 'remote: RhodeCode: push completed' in stderr - def test_hg_push_shows_pull_request_refs(self, backend_hg, rc_web_server, tmpdir): - empty_repo = backend_hg.create_repo() + def test_hg_push_shows_pull_request_refs(self, vcs_backend_hg, rcstack, tmpdir): + empty_repo = vcs_backend_hg.create_repo() - clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + clone_url = rcstack.repo_clone_url(empty_repo.repo_name) cmd = Command(tmpdir.strpath) cmd.execute('hg clone', clone_url) @@ -154,7 +155,7 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('hg push --verbose', clone_url) _check_proper_hg_push(stdout, stderr, branch='default') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' assert f'remote: RhodeCode: open pull request link: {ref}' in stdout assert 'remote: RhodeCode: push completed' in stdout @@ -189,14 +190,14 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('hg push --new-branch --verbose', clone_url) _check_proper_hg_push(stdout, stderr, branch='feature') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=feature' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=feature' assert f'remote: RhodeCode: open pull request link: {ref}' in stdout assert 'remote: RhodeCode: push completed' in stdout - def test_hg_push_shows_pull_request_refs_book(self, backend_hg, rc_web_server, tmpdir): - empty_repo = backend_hg.create_repo() + def test_hg_push_shows_pull_request_refs_book(self, vcs_backend_hg, rcstack, tmpdir): + empty_repo = vcs_backend_hg.create_repo() - clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + clone_url = rcstack.repo_clone_url(empty_repo.repo_name) cmd = Command(tmpdir.strpath) cmd.execute('hg clone', clone_url) @@ -214,7 +215,7 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('hg push --verbose', clone_url) _check_proper_hg_push(stdout, stderr, branch='default') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' assert f'remote: RhodeCode: open pull request link: {ref}' in stdout assert 'remote: RhodeCode: push completed' in stdout @@ -232,23 +233,23 @@ class TestVCSOperationsSpecial(object): stdout, stderr = repo_cmd.execute('hg push -B feature2 --verbose', clone_url) _check_proper_hg_push(stdout, stderr, branch='default') - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?branch=default' assert f'remote: RhodeCode: open pull request link: {ref}' in stdout - ref = f'{rc_web_server.host_url()}/{empty_repo.repo_name}/pull-request/new?bookmark=feature2' + ref = f'{rcstack.host_url()}/{empty_repo.repo_name}/pull-request/new?bookmark=feature2' assert f'remote: RhodeCode: open pull request link: {ref}' in stdout assert 'remote: RhodeCode: push completed' in stdout assert 'exporting bookmark feature2' in stdout - def test_push_is_forbidden_on_archived_repo_hg(self, backend_hg, rc_web_server, tmpdir): - empty_repo = backend_hg.create_repo() + def test_push_is_forbidden_on_archived_repo_hg(self, vcs_backend_hg, rcstack, tmpdir): + empty_repo = vcs_backend_hg.create_repo() repo_name = empty_repo.repo_name repo = Repository.get_by_repo_name(repo_name) repo.archived = True Session().commit() - clone_url = rc_web_server.repo_clone_url(repo_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(repo_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( @@ -256,16 +257,16 @@ class TestVCSOperationsSpecial(object): assert 'abort: HTTP Error 403: Forbidden' in stderr - def test_push_is_forbidden_on_archived_repo_git(self, backend_git, rc_web_server, tmpdir): - empty_repo = backend_git.create_repo() + def test_push_is_forbidden_on_archived_repo_git(self, vcs_backend_git, rcstack, tmpdir): + empty_repo = vcs_backend_git.create_repo() repo_name = empty_repo.repo_name repo = Repository.get_by_repo_name(repo_name) repo.archived = True Session().commit() - clone_url = rc_web_server.repo_clone_url(repo_name) - stdout, stderr = Command('/tmp').execute( + clone_url = rcstack.repo_clone_url(repo_name) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_svn.py b/rhodecode/tests/vcs_operations/test_vcs_operations_svn.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_svn.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_svn.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -49,97 +49,107 @@ def get_cli_flags(username, password): return flags, auth -@pytest.mark.usefixtures("disable_locking", "disable_anonymous_user") -class TestVCSOperations(object): +@pytest.mark.usefixtures( + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", +) +class TestVCSOperationsSVN(object): - def test_clone_svn_repo_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO) - username, password = rc_web_server.repo_clone_credentials() + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_svn_repo_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) + username, password = rcstack.repo_clone_credentials() - cmd = Command('/tmp') + cmd = Command(tmpdir.strpath) flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'svn') cmd.assert_returncode_success() - def test_clone_svn_repo_by_id_by_admin(self, rc_web_server, tmpdir): + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_svn_repo_by_id_by_admin(self, rcstack, tmpdir): repo_id = Repository.get_by_repo_name(SVN_REPO).repo_id - username, password = rc_web_server.repo_clone_credentials() + username, password = rcstack.repo_clone_credentials() - clone_url = rc_web_server.repo_clone_url('_%s' % repo_id) - cmd = Command('/tmp') + clone_url = rcstack.repo_clone_url('_%s' % repo_id) + cmd = Command(tmpdir.strpath) flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( - f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) - - _check_proper_clone(stdout, stderr, 'svn') - cmd.assert_returncode_success() - - def test_clone_svn_repo_with_group_by_admin(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO_WITH_GROUP) - username, password = rc_web_server.repo_clone_credentials() - - flags, auth = get_cli_flags(username, password) - - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) _check_proper_clone(stdout, stderr, 'svn') cmd.assert_returncode_success() - def test_clone_wrong_credentials_svn(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO) - username, password = rc_web_server.repo_clone_credentials() + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_svn_repo_with_group_by_admin(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO_WITH_GROUP) + username, password = rcstack.repo_clone_credentials() + + flags, auth = get_cli_flags(username, password) + + stdout, stderr = Command(tmpdir.strpath).execute( + f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) + + _check_proper_clone(stdout, stderr, 'svn') + rcstack.assert_returncode_success() + + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_wrong_credentials_svn(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) + username, password = rcstack.repo_clone_credentials() password = 'bad-password' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) assert 'fatal: Authentication failed' in stderr - def test_clone_svn_with_slashes(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url('//' + SVN_REPO) + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_clone_svn_with_slashes(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url('//' + SVN_REPO) username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url) assert 'not found' in stderr def test_clone_existing_path_svn_not_in_database( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-git', repo_type='git') - clone_url = rc_web_server.repo_clone_url(db_name) + clone_url = rcstack.repo_clone_url(db_name) username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) assert 'not found' in stderr def test_clone_existing_path_svn_not_in_database_different_scm( - self, rc_web_server, tmpdir, fs_repo_only): + self, rcstack, tmpdir, fs_repo_only): db_name = fs_repo_only('not-in-db-hg', repo_type='hg') - clone_url = rc_web_server.repo_clone_url(db_name) + clone_url = rcstack.repo_clone_url(db_name) username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) assert 'not found' in stderr - def test_clone_non_existing_store_path_svn(self, rc_web_server, tmpdir, user_util): + def test_clone_non_existing_store_path_svn(self, rcstack, tmpdir, user_util): repo = user_util.create_repo(repo_type='git') - clone_url = rc_web_server.repo_clone_url(repo.repo_name) + clone_url = rcstack.repo_clone_url(repo.repo_name) # Damage repo by removing it's folder RepoModel()._delete_filesystem_repo(repo) @@ -147,16 +157,17 @@ class TestVCSOperations(object): username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) assert 'not found' in stderr - def test_push_new_file_svn(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO) + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_push_new_file_svn(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) # commit some stuff into this repo @@ -165,37 +176,40 @@ class TestVCSOperations(object): _check_proper_svn_push(stdout, stderr) - def test_push_wrong_credentials_svn(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO) + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_push_wrong_credentials_svn(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) - username, password = rc_web_server.repo_clone_credentials() + username, password = rcstack.repo_clone_credentials() flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url( + push_url = rcstack.repo_clone_url( SVN_REPO, user='bad', passwd='name') stdout, stderr = _add_files_and_push( 'svn', tmpdir.strpath, clone_url=push_url, username=username, password=password) assert 'fatal: Authentication failed' in stderr - def test_push_back_to_wrong_url_svn(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(SVN_REPO) + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_push_back_to_wrong_url_svn(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(SVN_REPO) username, password = '', '' flags, auth = get_cli_flags(username, password) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) stdout, stderr = _add_files_and_push( 'svn', tmpdir.strpath, - clone_url=rc_web_server.repo_clone_url('not-existing'), username=username, password=password) + clone_url=rcstack.repo_clone_url('not-existing'), username=username, password=password) assert 'not found' in stderr - def test_ip_restriction_svn(self, rc_web_server, tmpdir): + @pytest.mark.xfail(reason='Lack of proper SVN support of cloning') + def test_ip_restriction_svn(self, rcstack, tmpdir): user_model = UserModel() username, password = '', '' flags, auth = get_cli_flags(username, password) @@ -204,9 +218,9 @@ class TestVCSOperations(object): user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32') Session().commit() time.sleep(2) - clone_url = rc_web_server.repo_clone_url(SVN_REPO) + clone_url = rcstack.repo_clone_url(SVN_REPO) - stdout, stderr = Command('/tmp').execute( + stdout, stderr = Command(tmpdir.strpath).execute( f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) msg = "The requested URL returned error: 403" assert msg in stderr @@ -218,7 +232,7 @@ class TestVCSOperations(object): time.sleep(2) - cmd = Command('/tmp') + cmd = Command(tmpdir.strpath) stdout, stderr = cmd.execute(f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath) cmd.assert_returncode_success() _check_proper_clone(stdout, stderr, 'svn') diff --git a/rhodecode/tests/vcs_operations/test_vcs_operations_tag_push.py b/rhodecode/tests/vcs_operations/test_vcs_operations_tag_push.py --- a/rhodecode/tests/vcs_operations/test_vcs_operations_tag_push.py +++ b/rhodecode/tests/vcs_operations/test_vcs_operations_tag_push.py @@ -1,5 +1,4 @@ - -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -19,11 +18,6 @@ """ Test suite for making push/pull operations, on specially modified INI files - -.. important:: - - You must have git >= 1.8.5 for tests to work fine. With 68b939b git started - to redirect things to stderr instead of stdout. """ import pytest @@ -37,54 +31,59 @@ connection_available = pytest.mark.skipi not check_httpbin_connection(), reason="No outside internet connection available") + @pytest.mark.usefixtures( - "disable_locking", "disable_anonymous_user", - "enable_webhook_push_integration") -class TestVCSOperationsOnCustomIniConfig(object): + "init_pyramid_app", + "repo_group_repos", + "disable_anonymous_user", + "disable_locking", + "enable_webhook_push_integration" +) +class TestVCSOperationsTagPush(object): @connection_available - def test_push_tag_with_commit_hg(self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(HG_REPO) - stdout, stderr = Command('/tmp').execute( + def test_push_tag_with_commit_hg(self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(HG_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'hg clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url(HG_REPO) + push_url = rcstack.repo_clone_url(HG_REPO) _add_files_and_push( 'hg', tmpdir.strpath, clone_url=push_url, tags=[{'name': 'v1.0.0', 'commit': 'added tag v1.0.0'}]) - rc_log = rc_web_server.get_rc_log() - assert 'ERROR' not in rc_log - assert "{'name': 'v1.0.0'," in rc_log + celery_log = open(rcstack.rcstack_data.celery_log).read() + assert 'ERROR' not in celery_log + assert "{'name': 'v1.0.0'," in celery_log @connection_available def test_push_tag_with_commit_git( - self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url(GIT_REPO) + push_url = rcstack.repo_clone_url(GIT_REPO) _add_files_and_push( 'git', tmpdir.strpath, clone_url=push_url, tags=[{'name': 'v1.0.0', 'commit': 'added tag v1.0.0'}]) - rc_log = rc_web_server.get_rc_log() - assert 'ERROR' not in rc_log - assert "{'name': 'v1.0.0'," in rc_log + celery_log = open(rcstack.rcstack_data.celery_log).read() + assert 'ERROR' not in celery_log + assert "{'name': 'v1.0.0'," in celery_log @connection_available def test_push_tag_with_no_commit_git( - self, rc_web_server, tmpdir): - clone_url = rc_web_server.repo_clone_url(GIT_REPO) - stdout, stderr = Command('/tmp').execute( + self, rcstack, tmpdir): + clone_url = rcstack.repo_clone_url(GIT_REPO) + stdout, stderr = Command(tmpdir.strpath).execute( 'git clone', clone_url, tmpdir.strpath) - push_url = rc_web_server.repo_clone_url(GIT_REPO) + push_url = rcstack.repo_clone_url(GIT_REPO) _add_files_and_push( 'git', tmpdir.strpath, clone_url=push_url, tags=[{'name': 'v1.0.0', 'commit': 'added tag v1.0.0'}]) - rc_log = rc_web_server.get_rc_log() - assert 'ERROR' not in rc_log - assert "{'name': 'v1.0.0'," in rc_log + celery_log = open(rcstack.rcstack_data.celery_log).read() + assert 'ERROR' not in celery_log + assert "{'name': 'v1.0.0'," in celery_log diff --git a/rhodecode/tests/vcsserver_http.ini b/rhodecode/tests/vcsserver_http.ini --- a/rhodecode/tests/vcsserver_http.ini +++ b/rhodecode/tests/vcsserver_http.ini @@ -60,7 +60,7 @@ vcs.svn.redis_conn = redis://redis:6379/ ; Default cache dir for caches. Putting this into a ramdisk can boost performance. ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space -cache_dir = %(here)s/data +cache_dir = %(here)s/.rc-vcs-test-data ; *************************************** ; `repo_object` cache, default file based @@ -141,7 +141,7 @@ level = NOTSET handlers = console [logger_vcsserver] -level = INFO +level = DEBUG handlers = qualname = vcsserver propagate = 1 diff --git a/rhodecode/translation.py b/rhodecode/translation.py --- a/rhodecode/translation.py +++ b/rhodecode/translation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2023 RhodeCode GmbH +# Copyright (C) 2016-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/tweens.py b/rhodecode/tweens.py --- a/rhodecode/tweens.py +++ b/rhodecode/tweens.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/rhodecode/typing/__init__.py b/rhodecode/typing/__init__.py --- a/rhodecode/typing/__init__.py +++ b/rhodecode/typing/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH +# Copyright (C) 2010-2024 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 diff --git a/setup.py b/setup.py deleted file mode 100644 --- a/setup.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (C) 2010-2023 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -# Import early to make sure things are patched up properly -from setuptools import setup, find_packages, Extension - -import os -import re -import sys -import pkgutil -import platform -import codecs - -import pip - -pip_major_version = int(pip.__version__.split(".")[0]) -if pip_major_version >= 20: - from pip._internal.req import parse_requirements - from pip._internal.network.session import PipSession -elif pip_major_version >= 10: - from pip._internal.req import parse_requirements - from pip._internal.download import PipSession -else: - from pip.req import parse_requirements - from pip.download import PipSession - - -def get_package_name(req_object): - package_name = None - try: - from pip._internal.req.constructors import install_req_from_parsed_requirement - except ImportError: - install_req_from_parsed_requirement = None - - # In 20.1 of pip, the requirements object changed - if hasattr(req_object, 'req'): - package_name = req_object.req.name - - if package_name is None: - if install_req_from_parsed_requirement: - package = install_req_from_parsed_requirement(req_object) - package_name = package.req.name - - if package_name is None: - # fallback for older pip - package_name = re.split('===|<=|!=|==|>=|~=|<|>', req_object.requirement)[0] - - return package_name - - -if sys.version_info < (3, 10): - raise Exception('RhodeCode requires Python 3.10 or later') - -here = os.path.abspath(os.path.dirname(__file__)) - -# defines current platform -__platform__ = platform.system() -__license__ = 'AGPLv3, and Commercial License' -__author__ = 'RhodeCode GmbH' -__url__ = 'https://code.rhodecode.com' -is_windows = __platform__ in ('Windows',) - - -def _get_requirements(req_filename, exclude=None, extras=None): - extras = extras or [] - exclude = exclude or [] - - try: - parsed = parse_requirements( - os.path.join(here, req_filename), session=PipSession()) - except TypeError: - # try pip < 6.0.0, that doesn't support session - parsed = parse_requirements(os.path.join(here, req_filename)) - - requirements = [] - for int_req in parsed: - req_name = get_package_name(int_req) - if req_name not in exclude: - requirements.append(req_name) - return requirements + extras - - -# requirements extract -setup_requirements = ['PasteScript'] -install_requirements = _get_requirements( - 'requirements.txt', exclude=['setuptools', 'entrypoints']) -test_requirements = _get_requirements( - 'requirements_test.txt') - - -def get_version(): - here = os.path.abspath(os.path.dirname(__file__)) - ver_file = os.path.join(here, "rhodecode", "VERSION") - with open(ver_file, "rt") as f: - version = f.read().strip() - - return version - - -# additional files that goes into package itself -package_data = { - '': ['*.txt', '*.rst'], - 'configs': ['*.ini'], - 'rhodecode': ['VERSION', 'i18n/*/LC_MESSAGES/*.mo', ], -} - -description = 'Source Code Management Platform' -keywords = ' '.join([ - 'rhodecode', 'mercurial', 'git', 'svn', - 'code review', - 'repo groups', 'ldap', 'repository management', 'hgweb', - 'hgwebdir', 'gitweb', 'serving hgweb', -]) - - -# README/DESCRIPTION generation -readme_file = 'README.rst' -changelog_file = 'CHANGES.rst' -try: - long_description = codecs.open(readme_file).read() + '\n\n' + \ - codecs.open(changelog_file).read() -except IOError as err: - sys.stderr.write( - "[WARNING] Cannot find file specified as long_description (%s)\n " - "or changelog (%s) skipping that file" % (readme_file, changelog_file)) - long_description = description - - -setup( - name='rhodecode-enterprise-ce', - version=get_version(), - description=description, - long_description=long_description, - keywords=keywords, - license=__license__, - author=__author__, - author_email='support@rhodecode.com', - url=__url__, - setup_requires=setup_requirements, - install_requires=install_requirements, - tests_require=test_requirements, - zip_safe=False, - packages=find_packages(exclude=["docs", "tests*"]), - package_data=package_data, - include_package_data=True, - classifiers=[ - 'Development Status :: 6 - Mature', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Version Control', - 'License :: OSI Approved :: Affero GNU General Public License v3 or later (AGPLv3+)', - 'Programming Language :: Python :: 3.10', - ], - message_extractors={ - 'rhodecode': [ - ('**.py', 'python', None), - ('**.js', 'javascript', None), - ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}), - ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}), - ('public/**', 'ignore', None), - ] - }, - - entry_points={ - 'paste.app_factory': [ - 'main=rhodecode.config.middleware:make_pyramid_app', - ], - 'pyramid.pshell_runner': [ - 'ipython = rhodecode.lib.pyramid_shell:ipython_shell_runner', - ], - 'console_scripts': [ - 'rc-setup-app=rhodecode.lib.rc_commands.setup_rc:main', - 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main', - 'rc-ishell=rhodecode.lib.rc_commands.ishell:main', - 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main', - 'rc-migrate-artifact=rhodecode.lib.rc_commands.migrate_artifact:main', - 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main', - 'rc-ssh-wrapper-v2=rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main', - ], - 'beaker.backends': [ - 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase', - 'memorylru_debug=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerDebug' - ] - }, -)