diff --git a/.gitignore b/.gitignore index d1c7edd4..9603c0b3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ rhodecode/public/js/src/components/**/*.css /.project /.pydevproject /.coverage +.coverage.* /.cache* /.ruff_cache* /.rhodecode diff --git a/Makefile b/Makefile index 5f2d1a5d..cb1e602d 100644 --- a/Makefile +++ b/Makefile @@ -84,13 +84,22 @@ test-only: rhodecode/tests/database .PHONY: test-simple -# test-simple: Run tests only for main test suite witout coverage +# test-simple: Run tests only for main test suite without coverage test-simple: PYTHONHASHSEED=random \ py.test -x -vv -r xw -p no:sugar \ --ignore=rhodecode/tests/vcs_operations \ --ignore=rhodecode/tests/database + +.PHONY: test-custom +# test-custom: Run tests without coverage, only run tests matching PATTERN +test-custom: + PYTHONHASHSEED=random \ + py.test $(if $(PATTERN),-k "$(PATTERN)",) -vv -r xw -p no:sugar \ + --ignore=rhodecode/tests/vcs_operations \ + --ignore=rhodecode/tests/database + # >>> Docs commands .PHONY: docs diff --git a/README.rst b/README.rst index cccf2b17..ca2e419a 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ RhodeCode uses `PEP386 versioning `_ Installation ------------ -Please visit https://docs.rhodecode.com/RhodeCode-Control/tasks/install-cli.html +Please visit https://docs.rhodecode.com/5.x/rcstack/install/installation.html for more details @@ -56,7 +56,7 @@ RhodeCode is open-source; contributions are welcome! Please see the contribution documentation inside of the docs folder, which is also available at -https://docs.rhodecode.com/RhodeCode-Enterprise/contributing/contributing.html +https://docs.rhodecode.com/5.x/rce/contributing/overview.html For additional information about collaboration tools, our issue tracker, licensing, and contribution credit, visit https://rhodecode.com/open-source diff --git a/docs/release-notes/release-notes-5.5.1.rst b/docs/release-notes/release-notes-5.5.1.rst new file mode 100644 index 00000000..d6a20536 --- /dev/null +++ b/docs/release-notes/release-notes-5.5.1.rst @@ -0,0 +1,40 @@ +|RCE| 5.5.1 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2025-04-07 + +New Features +^^^^^^^^^^^^ + + + +General +^^^^^^^ + + +Security +^^^^^^^^ + +- API: changed impersonate features to only be allowed by super-admins instead of repo admins. + This applies better practices to this feature of API. + +Performance +^^^^^^^^^^^ + + + +Fixes +^^^^^ + +- Fixed issues with caches key on windows WSL that were over 255 characters +- Fix for SVN dumps with empty revisions +- Fix for slack integration now showing proper actor for PR-related events + +Upgrade notes +^^^^^^^^^^^^^ + +- RhodeCode 5.5.1 is a planned bugfix release. + diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst index 58763251..fd3584b1 100644 --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,7 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-5.5.1.rst release-notes-5.5.0.rst release-notes-5.4.2.rst release-notes-5.4.1.rst diff --git a/docs/requirements_docs.txt b/docs/requirements_docs.txt index 19dfa5a9..aa3e8a92 100644 --- 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.5 +jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 5a522e97..8e240a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,7 @@ line-ending = "auto" [tool.bumpversion] -current_version = "5.5.0" +current_version = "5.6.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/requirements.txt b/requirements.txt index e18d0aaa..f2c82d1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,7 +48,7 @@ channelstream==0.7.1 zope.deprecation==5.1.0 zope.interface==7.2.0 pyramid-jinja2==2.10 - jinja2==3.1.5 + jinja2==3.1.6 markupsafe==3.0.2 markupsafe==3.0.2 pyramid==2.0.2 @@ -122,7 +122,7 @@ nbconvert==7.7.3 six==1.16.0 webencodings==0.5.1 defusedxml==0.7.1 - jinja2==3.1.5 + jinja2==3.1.6 markupsafe==3.0.2 jupyter_core==5.3.1 platformdirs==3.10.0 diff --git a/rhodecode/VERSION b/rhodecode/VERSION index c7ba1e87..4cc0e35c 100644 --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1 +1 @@ -5.5.0 \ No newline at end of file +5.6.0 \ No newline at end of file diff --git a/rhodecode/api/tests/test_close_pull_request.py b/rhodecode/api/tests/test_close_pull_request.py index b9064039..d035824d 100644 --- a/rhodecode/api/tests/test_close_pull_request.py +++ b/rhodecode/api/tests/test_close_pull_request.py @@ -91,7 +91,7 @@ class TestClosePullRequest(object): ) response = api_call(self.app, params) - expected = "userid is not the same as your user" + expected = "Provided userid is not the same the user calling this function" assert_error(id_, expected, given=response.body) @pytest.mark.backends("git", "hg") diff --git a/rhodecode/api/tests/test_comment_pull_request.py b/rhodecode/api/tests/test_comment_pull_request.py index ba68a0d3..838aeb32 100644 --- a/rhodecode/api/tests/test_comment_pull_request.py +++ b/rhodecode/api/tests/test_comment_pull_request.py @@ -234,7 +234,7 @@ class TestCommentPullRequest(object): ) response = api_call(self.app, params) - expected = "userid is not the same as your user" + expected = "Provided userid is not the same the user calling this function" assert_error(id_, expected, given=response.body) @pytest.mark.backends("git", "hg") @@ -360,7 +360,7 @@ class TestCommentPullRequest(object): userid=TEST_USER_ADMIN_LOGIN, ) response = api_call(self.app, params) - expected = "userid is not the same as your user" + expected = "Provided userid is not the same the user calling this function" assert_error(id_, expected, given=response.body) @pytest.mark.backends("git", "hg") diff --git a/rhodecode/api/tests/test_get_repo_changeset.py b/rhodecode/api/tests/test_get_repo_changeset.py index 36580ad1..464c538d 100644 --- a/rhodecode/api/tests/test_get_repo_changeset.py +++ b/rhodecode/api/tests/test_get_repo_changeset.py @@ -155,3 +155,27 @@ class TestGetRepoChangeset(object): response = api_call(self.app, params) expected = "commit_id must be a string value got instead" assert_error(id_, expected, given=response.body) + + @pytest.mark.skip(reason="This test is currently broken") + @pytest.mark.parametrize("details", ["basic", "extended", "full"]) + def test_get_repo_changeset_empty_revs(self, details, backend_svn): + """ + Test case for repositories with empty revisions. + Ensures that no crashes occur and an empty list is returned. + """ + empty_rev_id = "1000" + + __, params = build_data( + self.apikey, + "get_repo_changesets", + repoid=backend_svn.repo_name, + start_rev=empty_rev_id, + limit=10, + details=details, + ) + + response = api_call(self.app, params) + result = response.json["result"] + + assert isinstance(result, list) + assert len(result) == 0 diff --git a/rhodecode/api/tests/test_merge_pull_request.py b/rhodecode/api/tests/test_merge_pull_request.py index 9f9e22e8..cc5c951b 100644 --- a/rhodecode/api/tests/test_merge_pull_request.py +++ b/rhodecode/api/tests/test_merge_pull_request.py @@ -263,5 +263,5 @@ class TestMergePullRequest(object): ) response = api_call(self.app, params) - expected = "userid is not the same as your user" + expected = "Provided userid is not the same the user calling this function" assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/views/pull_request_api.py b/rhodecode/api/views/pull_request_api.py index 59b5fa29..0e499349 100644 --- a/rhodecode/api/views/pull_request_api.py +++ b/rhodecode/api/views/pull_request_api.py @@ -147,7 +147,7 @@ def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None), mer repo = pull_request.target_repo if not PullRequestModel().check_user_read(pull_request, apiuser, api=True): - raise JSONRPCError("repository `%s` or pull request `%s` does not exist" % (repoid, pullrequestid)) + raise JSONRPCError(f"repository `{repoid}` or pull request `{pullrequestid}` does not exist") # NOTE(marcink): only calculate and return merge state if the pr state is 'created' # otherwise we can lock the repo on calculation of merge state while update/merge @@ -302,21 +302,19 @@ def merge_pull_request(request, apiuser, pullrequestid, repoid=Optional(None), u repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo - auth_user = apiuser + auth_user = apiuser if not isinstance(userid, Optional): - is_repo_admin = HasRepoPermissionAnyApi("repository.admin")(user=apiuser, repo_name=repo.repo_name) - if has_superadmin_permission(apiuser) or is_repo_admin: + # super-admin can impersonate other users + if has_superadmin_permission(apiuser): apiuser = get_user_or_error(userid) auth_user = apiuser.AuthUser() else: - raise JSONRPCError("userid is not the same as your user") + raise JSONRPCError("Provided userid is not the same the user calling this function") if pull_request.pull_request_state != PullRequest.STATE_CREATED: raise JSONRPCError( - "Operation forbidden because pull request is in state {}, only state {} is allowed.".format( - pull_request.pull_request_state, PullRequest.STATE_CREATED - ) + f"Operation forbidden because pull request is in state {pull_request.pull_request_state}, only state {PullRequest.STATE_CREATED} is allowed." ) with pull_request.set_state(PullRequest.STATE_UPDATING): @@ -533,20 +531,26 @@ def comment_pull_request( repo = pull_request.target_repo db_repo_name = repo.repo_name + auth_user = apiuser - if not isinstance(userid, Optional): - is_repo_admin = HasRepoPermissionAnyApi("repository.admin")(user=apiuser, repo_name=db_repo_name) - if has_superadmin_permission(apiuser) or is_repo_admin: + if isinstance(userid, Optional): + userid = apiuser.user_id + else: + # super-admin can impersonate other users + if has_superadmin_permission(apiuser): apiuser = get_user_or_error(userid) + userid = apiuser.user_id auth_user = apiuser.AuthUser() else: - raise JSONRPCError("userid is not the same as your user") + raise JSONRPCError("Provided userid is not the same the user calling this function") if pull_request.is_closed(): raise JSONRPCError(f"pull request `{pullrequestid}` comment failed, pull request is closed") if not PullRequestModel().check_user_read(pull_request, apiuser, api=True): raise JSONRPCError(f"repository `{repoid}` does not exist") + + user = get_user_or_error(userid) message = Optional.extract(message) status = Optional.extract(status) commit_id = Optional.extract(commit_id) @@ -594,7 +598,7 @@ def comment_pull_request( comment = CommentsModel().create( text=text, repo=pull_request.target_repo.repo_id, - user=apiuser.user_id, + user=user, pull_request=pull_request.pull_request_id, f_path=None, line_no=None, @@ -928,9 +932,9 @@ def update_pull_request( repo = pull_request.target_repo if not PullRequestModel().check_user_update(pull_request, apiuser, api=True): - raise JSONRPCError("pull request `{}` update failed, no permission to update.".format(pullrequestid)) + raise JSONRPCError(f"pull request `{pullrequestid}` update failed, no permission to update.") if pull_request.is_closed(): - raise JSONRPCError("pull request `{}` update failed, pull request is closed".format(pullrequestid)) + raise JSONRPCError(f"pull request `{pullrequestid}` update failed, pull request is closed") reviewer_objects = Optional.extract(reviewers) or [] observer_objects = Optional.extract(observers) or [] @@ -959,9 +963,7 @@ def update_pull_request( if str2bool(Optional.extract(update_commits)): if pull_request.pull_request_state != PullRequest.STATE_CREATED: raise JSONRPCError( - "Operation forbidden because pull request is in state {}, only state {} is allowed.".format( - pull_request.pull_request_state, PullRequest.STATE_CREATED - ) + f"Operation forbidden because pull request is in state {pull_request.pull_request_state}, only state {PullRequest.STATE_CREATED} is allowed." ) with pull_request.set_state(PullRequest.STATE_UPDATING): @@ -1092,12 +1094,13 @@ def close_pull_request( else: repo = pull_request.target_repo - is_repo_admin = HasRepoPermissionAnyApi("repository.admin")(user=apiuser, repo_name=repo.repo_name) + auth_user = apiuser if not isinstance(userid, Optional): - if has_superadmin_permission(apiuser) or is_repo_admin: - apiuser = get_user_or_error(userid) + if has_superadmin_permission(apiuser): + # super-admin can impersonate other users + auth_user = get_user_or_error(userid) else: - raise JSONRPCError("userid is not the same as your user") + raise JSONRPCError("Provided userid is not the same the user calling this function") if pull_request.is_closed(): raise JSONRPCError(f"pull request `{pullrequestid}` is already closed") @@ -1106,14 +1109,14 @@ def close_pull_request( allowed_to_close = PullRequestModel().check_user_update(pull_request, apiuser, api=True) if not allowed_to_close: - raise JSONRPCError("pull request `{}` close failed, no permission to close.".format(pullrequestid)) + raise JSONRPCError(f"pull request `{pullrequestid}` close failed, no permission to close.") # message we're using to close the PR, else it's automatically generated message = Optional.extract(message) # finally close the PR, with proper message comment comment, status = PullRequestModel().close_pull_request_with_comment( - pull_request, apiuser, repo, message=message, auth_user=apiuser + pull_request, apiuser, repo, message=message, auth_user=auth_user ) status_lbl = ChangesetStatus.get_status_lbl(status) diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py index 4dd11cee..bdd90be6 100644 --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -43,7 +43,7 @@ from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int from rhodecode.lib.ext_json import json from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch from rhodecode.lib.vcs import RepositoryError -from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError +from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError, EmptyRepositoryError from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import CommentsModel from rhodecode.model.db import Session, ChangesetStatus, RepositoryField, Repository, RepoGroup, ChangesetComment @@ -393,6 +393,8 @@ def get_repo_changesets(request, apiuser, repoid, start_rev, limit, details=Opti commits = vcs_repo.get_commits(start_id=start_rev, pre_load=pre_load, translate_tags=False) except TypeError as e: raise JSONRPCError(safe_str(e)) + except EmptyRepositoryError: + return [] except Exception: log.exception("Fetching of commits failed") raise JSONRPCError("Error occurred during commit fetching") @@ -600,7 +602,7 @@ def get_repo_file( _extended_types = ["minimal", "minimal+search", "basic", "full"] if details not in _extended_types: ret_types = ",".join(_extended_types) - raise JSONRPCError(f"ret_type must be one of %s, got {ret_types}", details) + raise JSONRPCError(f"ret_type must be one of {_extended_types}, got {ret_types}", details) extended_info = False content = False @@ -695,7 +697,7 @@ def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path): except Exception: log.exception("Exception occurred while trying to get repo nodes") - raise JSONRPCError("failed to get repo: `%s` nodes" % repo.repo_name) + raise JSONRPCError(f"failed to get repo: `{repo.repo_name}` nodes") @jsonrpc_method() @@ -755,7 +757,7 @@ def get_repo_refs(request, apiuser, repoid): return refs except Exception: log.exception("Exception occurred while trying to get repo refs") - raise JSONRPCError("failed to get repo: `%s` references" % repo.repo_name) + raise JSONRPCError(f"failed to get repo: `{repo.repo_name}` references") @jsonrpc_method() @@ -918,14 +920,14 @@ def create_repo( task_id = get_task_id(task) # no commit, it's done in RepoModel, or async via celery return { - "msg": "Created new repository `{}`".format(schema_data["repo_name"]), + "msg": f"Created new repository `{schema_data['repo_name']}`", "success": True, # cannot return the repo data here since fork # can be done async "task": task_id, } except Exception: log.exception("Exception while trying to create the repository %s", schema_data["repo_name"]) - raise JSONRPCError("failed to create repository `{}`".format(schema_data["repo_name"])) + raise JSONRPCError(f"failed to create repository `{schema_data['repo_name']}`") @jsonrpc_method() @@ -994,7 +996,7 @@ def remove_field_from_repo(request, apiuser, repoid, key): field = RepositoryField.get_by_key_name(key, repo) if not field: - raise JSONRPCError("Field with key `%s` does not exists for repo `%s`" % (key, repoid)) + raise JSONRPCError(f"Field with key `{key}` does not exists for repo `{repoid}`") try: RepoModel().delete_repo_field(repo, field_key=key) @@ -1165,7 +1167,7 @@ def update_repo( } except Exception: log.exception("Exception while trying to update the repository %s", repoid) - raise JSONRPCError("failed to update repo `%s`" % repoid) + raise JSONRPCError(f"failed to update repo `{repoid}`") @jsonrpc_method() @@ -1310,14 +1312,14 @@ def fork_repo( task_id = get_task_id(task) return { - "msg": "Created fork of `{}` as `{}`".format(repo.repo_name, schema_data["repo_name"]), + "msg": f"Created fork of `{repo.repo_name}` as `{schema_data['repo_name']}`", "success": True, # cannot return the repo data here since fork # can be done async "task": task_id, } except Exception: log.exception("Exception while trying to create fork %s", schema_data["repo_name"]) - raise JSONRPCError("failed to fork repository `{}` as `{}`".format(repo_name, schema_data["repo_name"])) + raise JSONRPCError(f"failed to fork repository `{repo_name}` as `{schema_data['repo_name']}`") @jsonrpc_method() @@ -1361,11 +1363,11 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")): _forks_msg = "" _forks = [f for f in repo.forks] if handle_forks == "detach": - _forks_msg = " " + "Detached %s forks" % len(_forks) + _forks_msg = " " + f"Detached {len(_forks)} forks" elif handle_forks == "delete": - _forks_msg = " " + "Deleted %s forks" % len(_forks) + _forks_msg = " " + f"Deleted {len(_forks)} forks" elif _forks: - raise JSONRPCError("Cannot delete `%s` it still contains attached forks" % (repo.repo_name,)) + raise JSONRPCError(f"Cannot delete `{repo.repo_name}` it still contains attached forks") old_data = repo.get_api_data() RepoModel().delete(repo, forks=forks) @@ -1509,7 +1511,7 @@ def lock(request, apiuser, repoid, locked=Optional(None), userid=Optional(OAttr( # make sure normal user does not pass someone else userid, # he is not allowed to do that if not isinstance(userid, Optional) and userid != apiuser.user_id: - raise JSONRPCError("userid is not the same as your user") + raise JSONRPCError("Provided userid is not the same the user calling this function") if isinstance(userid, Optional): userid = apiuser.user_id @@ -1527,7 +1529,7 @@ def lock(request, apiuser, repoid, locked=Optional(None), userid=Optional(OAttr( "locked_by": None, "lock_reason": None, "lock_state_changed": False, - "msg": "Repo `%s` not locked." % repo.repo_name, + "msg": f"Repo `{repo.repo_name}` not locked.", } return _d else: @@ -1541,8 +1543,7 @@ def lock(request, apiuser, repoid, locked=Optional(None), userid=Optional(OAttr( "lock_reason": _reason, "lock_state_changed": False, "msg": ( - "Repo `%s` locked by `%s` on `%s`." - % (repo.repo_name, lock_user.username, json.dumps(time_to_datetime(_time))) + f"Repo `{repo.repo_name}` locked by `{lock_user.username}` on `{json.dumps(time_to_datetime(_time))}`." ), } return _d @@ -1565,12 +1566,12 @@ def lock(request, apiuser, repoid, locked=Optional(None), userid=Optional(OAttr( "locked_by": user.username, "lock_reason": lock_reason, "lock_state_changed": True, - "msg": ("User `%s` set lock state for repo `%s` to `%s`" % (user.username, repo.repo_name, locked)), + "msg": f"User `{user.username}` set lock state for repo `{repo.repo_name}` to `{locked}`", } return _d except Exception: log.exception("Exception occurred while trying to lock repository") - raise JSONRPCError("Error occurred locking repository `%s`" % repo.repo_name) + raise JSONRPCError(f"Error occurred locking repository `{repo.repo_name}`") @jsonrpc_method() @@ -1643,8 +1644,17 @@ def comment_commit( log.exception("Failed to fetch commit") raise JSONRPCError(safe_str(e)) + auth_user = apiuser if isinstance(userid, Optional): userid = apiuser.user_id + else: + # super-admin can impersonate other users + if has_superadmin_permission(apiuser): + apiuser = get_user_or_error(userid) + userid = apiuser.user_id + auth_user = apiuser.AuthUser() + else: + raise JSONRPCError("Provided userid is not the same the user calling this function") user = get_user_or_error(userid) status = Optional.extract(status) @@ -1655,21 +1665,14 @@ def comment_commit( allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES] if status and status not in allowed_statuses: - raise JSONRPCError( - "Bad status, must be on " - "of %s got %s" - % ( - allowed_statuses, - status, - ) - ) + raise JSONRPCError(f"Bad status, must be on of {allowed_statuses} got {status}") if resolves_comment_id: comment = ChangesetComment.get(resolves_comment_id) if not comment: - raise JSONRPCError("Invalid resolves_comment_id `%s` for this commit." % resolves_comment_id) + raise JSONRPCError(f"Invalid resolves_comment_id `{resolves_comment_id}` for this commit.") if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO: - raise JSONRPCError("Comment `%s` is wrong type for setting status to resolved." % resolves_comment_id) + raise JSONRPCError(f"Comment `{resolves_comment_id}` is wrong type for setting status to resolved.") try: rc_config = SettingsModel().get_all_settings() @@ -1685,7 +1688,7 @@ def comment_commit( renderer=renderer, comment_type=comment_type, resolves_comment_id=resolves_comment_id, - auth_user=apiuser, + auth_user=auth_user, extra_recipients=extra_recipients, send_email=send_email, ) @@ -1721,7 +1724,7 @@ def comment_commit( ) return { - "msg": ("Commented on commit `{}` for repository `{}`".format(comment.revision, repo.repo_name)), + "msg": f"Commented on commit `{comment.revision}` for repository `{repo.repo_name}`", "status_change": status, "success": True, } @@ -1792,9 +1795,7 @@ def get_repo_comments( comment_type = Optional.extract(comment_type) if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES: - raise JSONRPCError( - "comment_type must be one of `{}` got {}".format(ChangesetComment.COMMENT_TYPES, comment_type) - ) + raise JSONRPCError(f"comment_type must be one of `{ChangesetComment.COMMENT_TYPES}` got {comment_type}") comments = CommentsModel().get_repository_comments( repo=repo, comment_type=comment_type, user=user, commit_id=commit_id @@ -1886,11 +1887,12 @@ def edit_comment(request, apiuser, message, comment_id, version, userid=Optional is_repo_admin = HasRepoPermissionAnyApi("repository.admin")(user=apiuser, repo_name=comment.repo.repo_name) if not isinstance(userid, Optional): - if is_super_admin or is_repo_admin: + if is_super_admin: + # super-admin can impersonate other users apiuser = get_user_or_error(userid) auth_user = apiuser.AuthUser() else: - raise JSONRPCError("userid is not the same as your user") + raise JSONRPCError("Provided userid is not the same the user calling this function") comment_author = comment.author.user_id == auth_user.user_id @@ -2001,14 +2003,12 @@ def grant_user_permission(request, apiuser, repoid, userid, perm): PermissionModel().flush_user_permission_caches(changes) return { - "msg": "Granted perm: `{}` for user: `{}` in repo: `{}`".format( - perm.permission_name, user.username, repo.repo_name - ), + "msg": f"Granted perm: `{perm.permission_name}` for user: `{user.username}` in repo: `{repo.repo_name}`", "success": True, } except Exception: log.exception("Exception occurred while trying edit permissions for repo") - raise JSONRPCError("failed to edit permission for user: `{}` in repo: `{}`".format(userid, repoid)) + raise JSONRPCError(f"failed to edit permission for user: `{userid}` in repo: `{repoid}`") @jsonrpc_method() @@ -2058,12 +2058,12 @@ def revoke_user_permission(request, apiuser, repoid, userid): PermissionModel().flush_user_permission_caches(changes) return { - "msg": "Revoked perm for user: `{}` in repo: `{}`".format(user.username, repo.repo_name), + "msg": f"Revoked perm for user: `{user.username}` in repo: `{repo.repo_name}`", "success": True, } except Exception: log.exception("Exception occurred while trying revoke permissions to repo") - raise JSONRPCError("failed to edit permission for user: `{}` in repo: `{}`".format(userid, repoid)) + raise JSONRPCError(f"failed to edit permission for user: `{userid}` in repo: `{repoid}`") @jsonrpc_method() @@ -2145,9 +2145,7 @@ def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm): } except Exception: log.exception("Exception occurred while trying change permission on repo") - raise JSONRPCError( - "failed to edit permission for user group: `%s` in repo: `%s`" % (usergroupid, repo.repo_name) - ) + raise JSONRPCError(f"failed to edit permission for user group: `{usergroupid}` in repo: `{repo.repo_name}`") @jsonrpc_method() @@ -2206,16 +2204,13 @@ def revoke_user_group_permission(request, apiuser, repoid, usergroupid): PermissionModel().flush_user_permission_caches(changes) return { - "msg": "Revoked perm for user group: `{}` in repo: `{}`".format( - user_group.users_group_name, repo.repo_name - ), + "msg": f"Revoked perm for user group: `{user_group.users_group_name}` in repo: `{repo.repo_name}`", "success": True, } except Exception: log.exception("Exception occurred while trying revoke user group permission on repo") raise JSONRPCError( - "failed to edit permission for user group: `%s` in " - "repo: `%s`" % (user_group.users_group_name, repo.repo_name) + f"failed to edit permission for user group: `{user_group.users_group_name}` in repo: `{repo.repo_name}`" ) @@ -2276,12 +2271,12 @@ def pull(request, apiuser, repoid, remote_uri=Optional(None), sync_large_objects repo.repo_name, apiuser.username, remote_uri=remote_uri, sync_large_objects=sync_large_objects ) return { - "msg": "Pulled from url `{}` on repo `{}`".format(remote_uri_display, repo.repo_name), + "msg": f"Pulled from url `{remote_uri_display}` on repo `{repo.repo_name}`", "repository": repo.repo_name, } except Exception: log.exception("Exception occurred while trying to pull changes from remote location") - raise JSONRPCError("Unable to pull changes from `%s`" % remote_uri_display) + raise JSONRPCError(f"Unable to pull changes from `{remote_uri_display}`") @jsonrpc_method() @@ -2340,12 +2335,12 @@ def strip(request, apiuser, repoid, revision, branch): ) return { - "msg": "Stripped commit {} from repo `{}`".format(revision, repo.repo_name), + "msg": f"Stripped commit {revision} from repo `{repo.repo_name}`", "repository": repo.repo_name, } except Exception: log.exception("Exception while trying to strip") - raise JSONRPCError("Unable to strip commit {} from repo `{}`".format(revision, repo.repo_name)) + raise JSONRPCError(f"Unable to strip commit {revision} from repo `{repo.repo_name}`") @jsonrpc_method() @@ -2515,4 +2510,4 @@ def maintenance(request, apiuser, repoid): } except Exception: log.exception("Exception occurred while trying to run maintenance") - raise JSONRPCError("Unable to execute maintenance on `%s`" % repo.repo_name) + raise JSONRPCError(f"Unable to execute maintenance on `{repo.repo_name}`") diff --git a/rhodecode/apps/ssh_support/tests/test_server_svn.py b/rhodecode/apps/ssh_support/tests/test_server_svn.py index 64017573..d926299f 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_svn.py +++ b/rhodecode/apps/ssh_support/tests/test_server_svn.py @@ -44,6 +44,8 @@ class SubversionServerCreator(object): def __init__(self, service_api_url, ini_file): self.service_api_url = service_api_url self.ini_file = ini_file + if not os.path.isdir(self.root): + os.makedirs(self.root) def create(self, **kwargs): self.config_data["app:main"]["app.service_api.host"] = self.service_api_url diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py index 019c2ca6..9ea6f9e4 100644 --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -82,14 +82,22 @@ class RhodecodeEvent(object): 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: + if instance: + return AttributeDict( + dict( + username=instance.username, + user_id=instance.user_id, + ) + ) + else: + # we can't find this DB user... return AttributeDict( dict( username=auth_user.username, user_id=auth_user.user_id, ) ) + elif auth_user: return auth_user return SYSTEM_USER diff --git a/rhodecode/integrations/types/handlers/slack.py b/rhodecode/integrations/types/handlers/slack.py index 2ca104b8..db64d2aa 100644 --- a/rhodecode/integrations/types/handlers/slack.py +++ b/rhodecode/integrations/types/handlers/slack.py @@ -17,7 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import re import textwrap -import dataclasses +from dataclasses import dataclass, asdict import logging from mako.template import Template @@ -28,13 +28,16 @@ from rhodecode.integrations.types.base import CommitParsingDataHandler, render_w log = logging.getLogger(__name__) -@dataclasses.dataclass +@dataclass class SlackData: title: str text: str fields: list[dict] | None = None overrides: dict | None = None + def asdict(self): + return asdict(self) + def html_to_slack_links(message): return re.compile(r'(.+?)').sub(r"<\1|\2>", message) diff --git a/rhodecode/integrations/types/slack.py b/rhodecode/integrations/types/slack.py index 75e769d8..07898b1e 100644 --- a/rhodecode/integrations/types/slack.py +++ b/rhodecode/integrations/types/slack.py @@ -134,6 +134,8 @@ class SlackIntegrationType(IntegrationTypeBase): handler = SlackDataHandler() slack_data = handler(event, data) + # Transform to dict so its serializable + slack_data = slack_data.asdict() # title, text, fields, overrides run_task(post_text_to_slack, self.settings, slack_data) diff --git a/rhodecode/lib/vcs/backends/svn/repository.py b/rhodecode/lib/vcs/backends/svn/repository.py index 2241ba43..d0ae04de 100644 --- a/rhodecode/lib/vcs/backends/svn/repository.py +++ b/rhodecode/lib/vcs/backends/svn/repository.py @@ -306,8 +306,13 @@ class SubversionRepository(base.BaseRepository): self._validate_commit_id(end_id) start_raw_id = self._sanitize_commit_id(start_id) - start_pos = self.commit_ids.index(start_raw_id) if start_id else None end_raw_id = self._sanitize_commit_id(end_id) + + # for empty revisions in SVN + if start_raw_id not in self.commit_ids or end_raw_id not in self.commit_ids: + return base.CollectionGenerator(self, []) + + start_pos = self.commit_ids.index(start_raw_id) if start_id else None end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None if None not in [start_id, end_id] and start_pos > end_pos: diff --git a/rhodecode/lib/vcs/client_http.py b/rhodecode/lib/vcs/client_http.py index 1b42d4c0..4dec389d 100644 --- a/rhodecode/lib/vcs/client_http.py +++ b/rhodecode/lib/vcs/client_http.py @@ -203,8 +203,10 @@ class RemoteRepo(object): self.stream_url = remote_maker.stream_url self._session = remote_maker._session_factory() - cache_repo_id = self._repo_id_sanitizer(repo_id) _repo_name = self._get_repo_name(config, path) + # at this point this is actually a PATH, so we turn it into a hash to prevent 255 char limits + cache_repo_id = self._repo_id_sanitizer(repo_id) + cache_repo_id = rc_cache.utils.compute_key_from_params("repo_name", cache_repo_id) self._cache_region, self._cache_namespace = remote_maker.init_cache_region(cache_repo_id) with_wire = with_wire or {"cache": False} diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py index 1c3b46eb..924ca33a 100644 --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -60,30 +60,30 @@ def add_renderer_globals(event): def auto_merge_pr_if_needed(event): from rhodecode.model.db import PullRequest - from rhodecode.model.pull_request import ( - PullRequestModel, ChangesetStatus, MergeCheck - ) + 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)): + 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 - ) + 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 + 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): diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index 7d0191cc..63ff8cf6 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -1111,39 +1111,6 @@ } }); - var dismissNotice = function(noticeId) { - - var url = pyroutes.url('user_notice_dismiss', - {"user_id": templateContext.rhodecode_user.user_id}); - - var postData = { - 'csrf_token': CSRF_TOKEN, - 'notice_id': noticeId, - }; - - var success = function(response) { - $('#notice-message-' + noticeId).remove(); - return false; - }; - var failure = function(data, textStatus, xhr) { - alert("error processing request: " + textStatus); - return false; - }; - ajaxPOST(url, postData, success, failure); - } - - var hideLicenseWarning = function () { - var fingerprint = templateContext.session_attrs.license_fingerprint; - storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint); - $('#notifications').hide(); - } - - var hideLicenseError = function () { - var fingerprint = templateContext.session_attrs.license_fingerprint; - storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint); - $('#notifications').hide(); - } - diff --git a/rhodecode/templates/base/root.mako b/rhodecode/templates/base/root.mako index d431a2d3..3654783d 100644 --- a/rhodecode/templates/base/root.mako +++ b/rhodecode/templates/base/root.mako @@ -104,6 +104,38 @@ c.template_context['attachment_store'] = { rhodecode_edition: '${c.rhodecode_edition}' } }; + var dismissNotice = function(noticeId) { + + var url = pyroutes.url('user_notice_dismiss', + {"user_id": templateContext.rhodecode_user.user_id}); + + var postData = { + 'csrf_token': CSRF_TOKEN, + 'notice_id': noticeId, + }; + + var success = function(response) { + $('#notice-message-' + noticeId).remove(); + return false; + }; + var failure = function(data, textStatus, xhr) { + alert("error processing request: " + textStatus); + return false; + }; + ajaxPOST(url, postData, success, failure); + } + + var hideLicenseWarning = function () { + var fingerprint = templateContext.session_attrs.license_fingerprint; + storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint); + $('#notifications').hide(); + } + + var hideLicenseError = function () { + var fingerprint = templateContext.session_attrs.license_fingerprint; + storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint); + $('#notifications').hide(); + } <%include file="/base/plugins_base.mako"/> diff --git a/rhodecode/tests/integrations/test_slack.py b/rhodecode/tests/integrations/test_slack.py index 42daedc6..7e2babee 100644 --- a/rhodecode/tests/integrations/test_slack.py +++ b/rhodecode/tests/integrations/test_slack.py @@ -21,7 +21,7 @@ import mock from mock import patch from rhodecode import events -from rhodecode.integrations.types.handlers.slack import SlackDataHandler +from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData from rhodecode.model.db import Session, Integration from rhodecode.integrations.types.slack import SlackIntegrationType from rhodecode.tests import GIT_REPO @@ -100,10 +100,14 @@ def slack_integration_empty(request, app, slack_settings): def test_slack_push(slack_integration, repo_push_event): with patch("rhodecode.integrations.types.slack.post_text_to_slack") as call: events.trigger(repo_push_event) + # make sure we can handle both dicts and dataclass objects + slack_data = call.call_args[0][1] + if isinstance(slack_data, dict): + slack_data = SlackData(**slack_data) - assert "pushed to" in call.call_args[0][1].title + assert "pushed to" in slack_data.title # specific commit was parsed and serialized - assert "change that fixes #41" in call.call_args[0][1].text + assert "change that fixes #41" in slack_data.text def test_slack_push_no_events(slack_integration_empty, repo_push_event):