Merge branch 'main' into RCCE-255-unit-tests-fix

This commit is contained in:
Andrii V 2025-07-03 16:24:17 +02:00
commit 2f627a31e8
16 changed files with 463 additions and 144 deletions

View file

@ -117,7 +117,7 @@ line-ending = "auto"
[tool.bumpversion] [tool.bumpversion]
current_version = "5.6.0" current_version = "5.6.1"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"] serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}" search = "{current_version}"

View file

@ -1 +1 @@
5.6.0 5.6.1

View file

@ -100,7 +100,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {} EXTENSIONS = {}
__version__ = ".".join((str(each) for each in VERSION[:3])) __version__ = ".".join((str(each) for each in VERSION[:3]))
__dbversion__ = 115 # defines current db version for migrations __dbversion__ = 116 # defines current db version for migrations
__license__ = "AGPLv3, and Commercial License" __license__ = "AGPLv3, and Commercial License"
__author__ = "RhodeCode GmbH" __author__ = "RhodeCode GmbH"
__url__ = "https://code.rhodecode.com" __url__ = "https://code.rhodecode.com"

View file

@ -20,8 +20,8 @@ import os
import mock import mock
import pytest import pytest
from mock.mock import patch, MagicMock
import rhodecode
from rhodecode.lib import helpers as h from rhodecode.lib import helpers as h
from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason, Reference from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason, Reference
from rhodecode.lib.vcs.nodes import FileNode from rhodecode.lib.vcs.nodes import FileNode
@ -37,6 +37,7 @@ from rhodecode.model.db import (
) )
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.settings import VcsSettingsModel
from rhodecode.model.user import UserModel from rhodecode.model.user import UserModel
from rhodecode.model.comment import CommentsModel from rhodecode.model.comment import CommentsModel
from rhodecode.tests import ( from rhodecode.tests import (
@ -44,7 +45,7 @@ from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_LOGIN,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_LOGIN,
) )
from rhodecode.tests.fixtures.fixture_utils import PRTestUtility from rhodecode.tests.fixtures.fixture_utils import PRTestUtility, Backend, temporary_settings
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
@ -141,43 +142,19 @@ class TestPullrequestsView(object):
source_ref = "branch:{branch}:{commit_id}".format( source_ref = "branch:{branch}:{commit_id}".format(
branch=backend.default_branch_name, commit_id=commit_ids["commit-1"] branch=backend.default_branch_name, commit_id=commit_ids["commit-1"]
) )
pr_post_for_params = self._get_pr_create_post_form_params(
response = self.app.post( csrf_token, source, target, revisions=[commit_ids["commit-1"]]
route_path("pullrequest_create", repo_name=source.repo_name),
[
("source_repo", source_repo_name),
("source_ref", source_ref),
("target_repo", target_repo_name),
("target_ref", target_ref),
("common_ancestor", commit_ids["initial-commit"]),
("pullrequest_title", "Title"),
("pullrequest_desc", "Description"),
("description_renderer", "markdown"),
("__start__", "review_members:sequence"),
("__start__", "reviewer:mapping"),
("user_id", "1"),
("__start__", "reasons:sequence"),
("reason", "Some reason"),
("__end__", "reasons:sequence"),
("__start__", "rules:sequence"),
("__end__", "rules:sequence"),
("mandatory", "False"),
("__end__", "reviewer:mapping"),
("__end__", "review_members:sequence"),
("__start__", "revisions:sequence"),
("revisions", commit_ids["commit-1"]),
("__end__", "revisions:sequence"),
("user", ""),
("csrf_token", csrf_token),
],
status=302,
) )
pr_post_for_params.extend(
[
("target_ref", target_ref),
("source_ref", source_ref),
("common_ancestor", commit_ids["initial-commit"]),
]
)
pull_request_id, _ = self._create_pr_and_initial_validation(pr_post_for_params, source)
location = response.headers["Location"] pull_request = PullRequest.get(pull_request_id)
pull_request_id = location.rsplit("/", 1)[1]
assert pull_request_id != "new"
pull_request = PullRequest.get(int(pull_request_id))
pull_request_id = pull_request.pull_request_id pull_request_id = pull_request.pull_request_id
@ -354,6 +331,17 @@ class TestPullrequestsView(object):
response.mustcontain(cb_context("LINE3")) response.mustcontain(cb_context("LINE3"))
response.mustcontain(cb_line("LINE4")) response.mustcontain(cb_line("LINE4"))
def _create_pr_and_initial_validation(self, pr_post_for_params, source):
response = self.app.post(
route_path("pullrequest_create", repo_name=source.repo_name),
pr_post_for_params,
status=302,
)
location = response.headers["Location"]
pull_request_id = location.rsplit("/", 1)[1]
assert pull_request_id != "new"
return int(pull_request_id), response
def test_close_status_visibility(self, pr_util, user_util, csrf_token): def test_close_status_visibility(self, pr_util, user_util, csrf_token):
# Logout # Logout
response = self.app.post(h.route_path("logout"), params={"csrf_token": csrf_token}) response = self.app.post(h.route_path("logout"), params={"csrf_token": csrf_token})
@ -867,7 +855,109 @@ class TestPullrequestsView(object):
) )
assert response.status_int == 403 assert response.status_int == 403
def test_create_pull_request(self, backend, csrf_token): def test_new_pull_request_view_close_branch_before_merging_setting_present_for_ee(self, backend, baseapp):
with temporary_settings(baseapp, **{"rhodecode.edition_id": "EE"}):
response = self.app.get(route_path("pullrequest_new", repo_name=backend.repo_name), status=200)
assert_response = response.assert_response()
assert_response.one_element_exists("#close_branch_before_merging")
@pytest.mark.parametrize(
"global_close_branch_before_merging_value",
[
False,
True,
],
)
def test_new_pull_request_view_close_branch_before_merging_setting_inherit_global_settings(
self, backend, baseapp, global_close_branch_before_merging_value
):
def get_repo_settings_inherited(settings_key, default):
if settings_key == f"rhodecode_{backend.repo.repo_type}_close_branch_before_merging":
return global_close_branch_before_merging_value
return None
with temporary_settings(baseapp, **{"rhodecode.edition_id": "EE"}):
with patch.object(VcsSettingsModel, "get_repo_settings_inherited") as repo_settings_inherited:
settings_mock = MagicMock()
settings_mock.get.side_effect = get_repo_settings_inherited
repo_settings_inherited.return_value = settings_mock
response = self.app.get(route_path("pullrequest_new", repo_name=backend.repo_name), status=200)
assert_response = response.assert_response()
if global_close_branch_before_merging_value:
assert_response.element_contain_attribute("#close_branch_before_merging", "checked", "checked")
else:
assert_response.element_contain_no_attribute("#close_branch_before_merging", "checked")
def test_new_pull_request_view_close_branch_before_merging_setting_not_present_for_ce(self, backend):
response = self.app.get(route_path("pullrequest_new", repo_name=backend.repo_name), status=200)
assert_response = response.assert_response()
assert_response.no_element_exists("#close_branch_before_merging")
@pytest.mark.parametrize(
"close_branch_before_merging",
[
True,
False,
],
)
def test_update_pull_request_view_close_branch_before_merging_setting(
self, backend, pr_util, csrf_token, close_branch_before_merging
):
pull_request = pr_util.create_pull_request(mergeable=True, enable_notifications=False)
pr_id = pull_request.pull_request_id
data = [
("close_branch_before_merging", close_branch_before_merging),
("csrf_token", csrf_token),
]
self.app.post(
route_path(
"pullrequest_update",
repo_name=pull_request.target_repo.scm_instance().name,
pull_request_id=pr_id,
),
data,
)
db_pr = PullRequest.get(pr_id)
assert db_pr.settings["close_branch_before_merging"] == close_branch_before_merging
def test_show_pull_request_view_close_branch_before_merging_setting_present_for_ee(self, backend, pr_util, baseapp):
with temporary_settings(baseapp, **{"rhodecode.edition_id": "EE"}):
pull_request = pr_util.create_pull_request(mergeable=True, enable_notifications=False)
response = self.app.get(
route_path(
"pullrequest_show",
repo_name=pull_request.target_repo.scm_instance().name,
pull_request_id=pull_request.pull_request_id,
)
)
assert_response = response.assert_response()
assert_response.one_element_exists("#close_branch_before_merging")
def test_show_pull_request_view_close_branch_before_merging_setting_not_present_for_ce(self, backend, pr_util):
pull_request = pr_util.create_pull_request(mergeable=True, enable_notifications=False)
response = self.app.get(
route_path(
"pullrequest_show",
repo_name=pull_request.target_repo.scm_instance().name,
pull_request_id=pull_request.pull_request_id,
)
)
assert_response = response.assert_response()
assert_response.no_element_exists("#close_branch_before_merging")
def test_create_pull_request_user_set_test_create_pull_request_to_true(self, backend, csrf_token):
commits = [ commits = [
{"message": "ancestor"}, {"message": "ancestor"},
{"message": "change"}, {"message": "change"},
@ -877,48 +967,82 @@ class TestPullrequestsView(object):
target = backend.create_repo(heads=["ancestor"]) target = backend.create_repo(heads=["ancestor"])
source = backend.create_repo(heads=["change2"]) source = backend.create_repo(heads=["change2"])
response = self.app.post( pr_post_for_params = self._get_pr_create_post_form_params(
route_path("pullrequest_create", repo_name=source.repo_name), csrf_token, source, target, revisions=[commit_ids["change"], commit_ids["change2"]]
)
pr_post_for_params.extend(
[ [
("source_repo", source.repo_name), ("close_branch_before_merging", "true"),
("source_ref", "branch:default:" + commit_ids["change2"]), ("source_ref", "branch:default:" + commit_ids["change2"]),
("target_repo", target.repo_name),
("target_ref", "branch:default:" + commit_ids["ancestor"]), ("target_ref", "branch:default:" + commit_ids["ancestor"]),
("common_ancestor", commit_ids["ancestor"]), ("common_ancestor", commit_ids["ancestor"]),
("pullrequest_title", "Title"), ]
("pullrequest_desc", "Description"), )
("description_renderer", "markdown"), pull_request_id, _ = self._create_pr_and_initial_validation(pr_post_for_params, source)
("__start__", "review_members:sequence"),
("__start__", "reviewer:mapping"), pull_request = PullRequest.get(pull_request_id)
("user_id", "1"),
("__start__", "reasons:sequence"), assert len(pull_request.settings) == 1
("reason", "Some reason"), assert pull_request.settings["close_branch_before_merging"]
("__end__", "reasons:sequence"),
("__start__", "rules:sequence"), def _get_pr_create_post_form_params(
("__end__", "rules:sequence"), self, csrf_token: str, source: Backend, target: Backend, revisions: list, user_id: str = "1"
("mandatory", "False"), ):
("__end__", "reviewer:mapping"), return [
("__end__", "review_members:sequence"), ("source_repo", source.repo_name),
("__start__", "revisions:sequence"), ("target_repo", target.repo_name),
("revisions", commit_ids["change"]), ("pullrequest_title", "Title"),
("revisions", commit_ids["change2"]), ("pullrequest_desc", "Description"),
("__end__", "revisions:sequence"), ("description_renderer", "markdown"),
("user", ""), ("__start__", "review_members:sequence"),
("csrf_token", csrf_token), ("__start__", "reviewer:mapping"),
], ("user_id", user_id),
status=302, ("__start__", "reasons:sequence"),
("reason", "Some reason"),
("__end__", "reasons:sequence"),
("__start__", "rules:sequence"),
("__end__", "rules:sequence"),
("mandatory", "False"),
("__end__", "reviewer:mapping"),
("__end__", "review_members:sequence"),
("__start__", "revisions:sequence"),
*[("revisions", r) for r in revisions],
("__end__", "revisions:sequence"),
("user", ""),
("csrf_token", csrf_token),
]
def test_create_pull_request(self, backend, csrf_token):
commits = [
{"message": "ancestor"},
{"message": "change"},
{"message": "change2"},
]
commit_ids = backend.create_master_repo(commits)
target = backend.create_repo(heads=["ancestor"])
source = backend.create_repo(heads=["change2"])
pr_post_for_params = self._get_pr_create_post_form_params(
csrf_token, source, target, revisions=[commit_ids["change"], commit_ids["change2"]]
)
pr_post_for_params.extend(
[
("source_ref", "branch:default:" + commit_ids["change2"]),
("target_ref", "branch:default:" + commit_ids["ancestor"]),
("common_ancestor", commit_ids["ancestor"]),
]
) )
location = response.headers["Location"] pull_request_id, _ = self._create_pr_and_initial_validation(pr_post_for_params, source)
pull_request_id = location.rsplit("/", 1)[1]
assert pull_request_id != "new" pull_request = PullRequest.get(pull_request_id)
pull_request = PullRequest.get(int(pull_request_id))
# check that we have now both revisions # check that we have now both revisions
assert pull_request.revisions == [commit_ids["change2"], commit_ids["change"]] assert pull_request.revisions == [commit_ids["change2"], commit_ids["change"]]
assert pull_request.source_ref == "branch:default:" + commit_ids["change2"] assert pull_request.source_ref == "branch:default:" + commit_ids["change2"]
expected_target_ref = "branch:default:" + commit_ids["ancestor"] expected_target_ref = "branch:default:" + commit_ids["ancestor"]
assert pull_request.target_ref == expected_target_ref assert pull_request.target_ref == expected_target_ref
assert len(pull_request.settings) == 1
assert not pull_request.settings["close_branch_before_merging"]
def test_reviewer_notifications(self, backend, csrf_token): def test_reviewer_notifications(self, backend, csrf_token):
# We have to use the app.post for this test, so it will create the # We have to use the app.post for this test, so it will create the
@ -945,42 +1069,20 @@ class TestPullrequestsView(object):
target = backend.create_repo(heads=["ancestor-child"]) target = backend.create_repo(heads=["ancestor-child"])
source = backend.create_repo(heads=["change"]) source = backend.create_repo(heads=["change"])
response = self.app.post( pr_post_for_params = self._get_pr_create_post_form_params(
route_path("pullrequest_create", repo_name=source.repo_name), csrf_token, source, target, user_id="2", revisions=[commit_ids["change"]]
)
pr_post_for_params.extend(
[ [
("source_repo", source.repo_name),
("source_ref", "branch:default:" + commit_ids["change"]), ("source_ref", "branch:default:" + commit_ids["change"]),
("target_repo", target.repo_name),
("target_ref", "branch:default:" + commit_ids["ancestor-child"]), ("target_ref", "branch:default:" + commit_ids["ancestor-child"]),
("common_ancestor", commit_ids["ancestor"]), ("common_ancestor", commit_ids["ancestor"]),
("pullrequest_title", "Title"), ]
("pullrequest_desc", "Description"),
("description_renderer", "markdown"),
("__start__", "review_members:sequence"),
("__start__", "reviewer:mapping"),
("user_id", "2"),
("__start__", "reasons:sequence"),
("reason", "Some reason"),
("__end__", "reasons:sequence"),
("__start__", "rules:sequence"),
("__end__", "rules:sequence"),
("mandatory", "False"),
("__end__", "reviewer:mapping"),
("__end__", "review_members:sequence"),
("__start__", "revisions:sequence"),
("revisions", commit_ids["change"]),
("__end__", "revisions:sequence"),
("user", ""),
("csrf_token", csrf_token),
],
status=302,
) )
location = response.headers["Location"] pull_request_id, _ = self._create_pr_and_initial_validation(pr_post_for_params, source)
pull_request_id = location.rsplit("/", 1)[1] pull_request = PullRequest.get(pull_request_id)
assert pull_request_id != "new"
pull_request = PullRequest.get(int(pull_request_id))
# Check that a notification was made # Check that a notification was made
notifications = Notification.query().filter( notifications = Notification.query().filter(
@ -1023,43 +1125,20 @@ class TestPullrequestsView(object):
commit_ids = backend.create_master_repo(commits) commit_ids = backend.create_master_repo(commits)
target = backend.create_repo(heads=["ancestor-child"]) target = backend.create_repo(heads=["ancestor-child"])
source = backend.create_repo(heads=["change"]) source = backend.create_repo(heads=["change"])
pr_post_for_params = self._get_pr_create_post_form_params(
response = self.app.post( csrf_token, source, target, revisions=[commit_ids["change"]]
route_path("pullrequest_create", repo_name=source.repo_name), )
pr_post_for_params.extend(
[ [
("source_repo", source.repo_name),
("source_ref", "branch:default:" + commit_ids["change"]), ("source_ref", "branch:default:" + commit_ids["change"]),
("target_repo", target.repo_name),
("target_ref", "branch:default:" + commit_ids["ancestor-child"]), ("target_ref", "branch:default:" + commit_ids["ancestor-child"]),
("common_ancestor", commit_ids["ancestor"]), ("common_ancestor", commit_ids["ancestor"]),
("pullrequest_title", "Title"), ]
("pullrequest_desc", "Description"),
("description_renderer", "markdown"),
("__start__", "review_members:sequence"),
("__start__", "reviewer:mapping"),
("user_id", "1"),
("__start__", "reasons:sequence"),
("reason", "Some reason"),
("__end__", "reasons:sequence"),
("__start__", "rules:sequence"),
("__end__", "rules:sequence"),
("mandatory", "False"),
("__end__", "reviewer:mapping"),
("__end__", "review_members:sequence"),
("__start__", "revisions:sequence"),
("revisions", commit_ids["change"]),
("__end__", "revisions:sequence"),
("user", ""),
("csrf_token", csrf_token),
],
status=302,
) )
location = response.headers["Location"] pull_request_id, response = self._create_pr_and_initial_validation(pr_post_for_params, source)
pull_request_id = location.rsplit("/", 1)[1] pull_request = PullRequest.get(pull_request_id)
assert pull_request_id != "new"
pull_request = PullRequest.get(int(pull_request_id))
# target_ref has to point to the ancestor's commit_id in order to # target_ref has to point to the ancestor's commit_id in order to
# show the correct diff # show the correct diff
@ -1153,7 +1232,7 @@ class TestPullrequestsView(object):
mods = [ mods = [
( (
"_pre_push_hook", "_pre_push_hook",
f""" """
return HookResponse(1, 'HOOK_TEST_FORBIDDEN') return HookResponse(1, 'HOOK_TEST_FORBIDDEN')
""", """,
) )

View file

@ -166,6 +166,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
"comments": _render("pullrequest_comments", comments_count), "comments": _render("pullrequest_comments", comments_count),
"comments_raw": comments_count, "comments_raw": comments_count,
"closed": pr.is_closed(), "closed": pr.is_closed(),
"settings": pr.settings,
} }
) )
@ -934,6 +935,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
} }
c.default_source_ref = selected_source_ref c.default_source_ref = selected_source_ref
close_branch_before_merging_key = "rhodecode_%s_close_branch_before_merging" % source_repo.repo_type
c.repo_close_branch_before_merging = self._get_repo_setting(source_repo, close_branch_before_merging_key)
return self._get_template_context(c) return self._get_template_context(c)
@LoginRequired() @LoginRequired()
@ -1220,6 +1224,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
description = _form["pullrequest_desc"] description = _form["pullrequest_desc"]
description_renderer = _form["description_renderer"] description_renderer = _form["description_renderer"]
settings = {"close_branch_before_merging": _form["close_branch_before_merging"]}
try: try:
pull_request = PullRequestModel().create( pull_request = PullRequestModel().create(
@ -1237,6 +1242,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
description_renderer=description_renderer, description_renderer=description_renderer,
reviewer_data=reviewer_rules, reviewer_data=reviewer_rules,
auth_user=self._rhodecode_user, auth_user=self._rhodecode_user,
settings=settings,
) )
Session().commit() Session().commit()
@ -1283,6 +1289,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
controls = peppercorn.parse(self.request.POST.items()) controls = peppercorn.parse(self.request.POST.items())
force_refresh = str2bool(self.request.POST.get("force_refresh", "false")) force_refresh = str2bool(self.request.POST.get("force_refresh", "false"))
do_update_commits = str2bool(self.request.POST.get("update_commits", "false")) do_update_commits = str2bool(self.request.POST.get("update_commits", "false"))
do_update_branch_close = "close_branch_before_merging" in self.request.POST
if "review_members" in controls: if "review_members" in controls:
self._update_reviewers( self._update_reviewers(
@ -1321,6 +1328,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
) )
elif str2bool(self.request.POST.get("edit_pull_request", "false")): elif str2bool(self.request.POST.get("edit_pull_request", "false")):
self._edit_pull_request(pull_request) self._edit_pull_request(pull_request)
elif do_update_branch_close:
self._update_settings(pull_request)
else: else:
log.error("Unhandled update data.") log.error("Unhandled update data.")
raise HTTPBadRequest() raise HTTPBadRequest()
@ -1328,6 +1337,14 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return {"response": True, "redirect_url": redirect_url} return {"response": True, "redirect_url": redirect_url}
raise HTTPForbidden() raise HTTPForbidden()
def _update_settings(self, pull_request):
try:
close_branch_before_merging = str2bool(self.request.POST.get("close_branch_before_merging", "false"))
PullRequestModel().update_settings(pull_request, close_branch_before_merging)
except ValueError:
msg = self.request.translate("Cannot update closed pull requests.")
h.flash(msg, category="error")
def _edit_pull_request(self, pull_request): def _edit_pull_request(self, pull_request):
""" """
Edit title and description Edit title and description

View file

@ -0,0 +1,75 @@
import json
import logging
from sqlalchemy import *
from sqlalchemy.engine import reflection
from alembic.migration import MigrationContext
from alembic.operations import Operations
from rhodecode.lib.dbmigrate.versions import _reset_base
from rhodecode.lib.jsonalchemy import MutationObj, JsonType
from rhodecode.model import meta, init_model_encryption
def upgrade(migrate_engine):
"""
Upgrade operations go here.
Don't create your own engine; bind migrate_engine to your metadata
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
init_model_encryption(db)
context = MigrationContext.configure(migrate_engine.connect())
op = Operations(context)
pr_table = db.PullRequest.__table__
with op.batch_alter_table(pr_table.name) as batch_op:
new_column = Column(
"settings_json",
MutationObj.as_mutable(
JsonType(dialect_map=dict(mysql=UnicodeText(16384))),
),
default=dict,
)
batch_op.add_column(new_column)
pr_version_table = db.PullRequestVersion.__table__
with op.batch_alter_table(pr_version_table.name) as batch_op:
new_column = Column(
"settings_json",
MutationObj.as_mutable(
JsonType(dialect_map=dict(mysql=UnicodeText(16384))),
),
default=dict,
)
batch_op.add_column(new_column)
_inherit_settings(db, meta.Session, op)
def downgrade(migrate_engine):
pass
def _inherit_settings(models, _SESSION, op):
for pr in _SESSION.query(models.PullRequest).all():
repo = pr.target_repo
repo_type = repo.repo_type
close_branch_before_merging = False
if repo_type in ["git", "hg"]:
close_branch_before_merging = getattr(repo, f"{repo_type}_close_branch_before_merging", False)
json_settings = {"close_branch_before_merging": close_branch_before_merging}
params = {"id": pr.pull_request_id, "value": json.dumps(json_settings)}
query = text(
"""UPDATE pull_requests
SET settings_json = :value
WHERE pull_request_id = :id"""
).bindparams(**params)
op.execute(query)
_SESSION().commit()

View file

@ -77,7 +77,6 @@ from zope.cachedescriptors.property import Lazy as LazyProperty
from webhelpers2.text import remove_formatting from webhelpers2.text import remove_formatting
from rhodecode import ConfigGet
from rhodecode.lib.str_utils import safe_bytes from rhodecode.lib.str_utils import safe_bytes
from rhodecode.translation import _ from rhodecode.translation import _
from rhodecode.lib.vcs import get_vcs_instance, VCSError from rhodecode.lib.vcs import get_vcs_instance, VCSError
@ -108,6 +107,8 @@ from rhodecode.lib.exceptions import ArtifactMetadataDuplicate, ArtifactMetadata
from rhodecode.lib.pyramid_utils import get_current_request from rhodecode.lib.pyramid_utils import get_current_request
from rhodecode.model.meta import Base, Session from rhodecode.model.meta import Base, Session
DEFAULT_JSON_OBJ_SIZE = 16384
URL_SEP = "/" URL_SEP = "/"
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -4443,13 +4444,23 @@ class _PullRequestBase(BaseModel):
_last_merge_target_rev = Column("last_merge_other_rev", String(40), nullable=True) _last_merge_target_rev = Column("last_merge_other_rev", String(40), nullable=True)
_last_merge_status = Column("merge_status", Integer(), nullable=True) _last_merge_status = Column("merge_status", Integer(), nullable=True)
last_merge_metadata = Column( last_merge_metadata = Column(
"last_merge_metadata", MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) "last_merge_metadata",
MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE)))),
) )
merge_rev = Column("merge_rev", String(40), nullable=True) merge_rev = Column("merge_rev", String(40), nullable=True)
reviewer_data = Column( reviewer_data = Column(
"reviewer_data_json", MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) "reviewer_data_json",
MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE)))),
)
settings = Column(
"settings_json",
MutationObj.as_mutable(
JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE))),
),
default=dict,
) )
@property @property
@ -4711,6 +4722,7 @@ class PullRequest(Base, _PullRequestBase):
attrs.target_ref_parts = pull_request_obj.target_ref_parts attrs.target_ref_parts = pull_request_obj.target_ref_parts
attrs.revisions = pull_request_obj.revisions attrs.revisions = pull_request_obj.revisions
attrs.common_ancestor_id = pull_request_obj.common_ancestor_id attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
attrs.settings = pull_request_obj.settings
attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
attrs.reviewer_data = org_pull_request_obj.reviewer_data attrs.reviewer_data = org_pull_request_obj.reviewer_data
attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
@ -4798,6 +4810,26 @@ class PullRequest(Base, _PullRequestBase):
return self.versions_count return self.versions_count
@event.listens_for(PullRequest, "before_insert")
def _init_pr_default_settings(mapper, connection, pull_request):
if not pull_request.settings:
pull_request.settings = {
"close_branch_before_merging": _inherit_global_settings(pull_request),
}
def _inherit_global_settings(pull_request):
from rhodecode.model.settings import VcsSettingsModel # handle circular dependency issue
repo_type = pull_request.target_repo.repo_type
settings_model = VcsSettingsModel(repo=pull_request.target_repo)
settings = settings_model.get_general_settings()
key = "rhodecode_{}_close_branch_before_merging"
if repo_type in ["hg", "git"]:
return settings.get(key.format(repo_type), False)
return False
class PullRequestVersion(Base, _PullRequestBase): class PullRequestVersion(Base, _PullRequestBase):
__tablename__ = "pull_request_versions" __tablename__ = "pull_request_versions"
__table_args__ = (base_table_args,) __table_args__ = (base_table_args,)
@ -4860,7 +4892,9 @@ class PullRequestReviewers(Base, BaseModel):
pull_requests_reviewers_id = Column("pull_requests_reviewers_id", Integer(), nullable=False, primary_key=True) pull_requests_reviewers_id = Column("pull_requests_reviewers_id", Integer(), nullable=False, primary_key=True)
pull_request_id = Column("pull_request_id", Integer(), ForeignKey("pull_requests.pull_request_id"), nullable=False) pull_request_id = Column("pull_request_id", Integer(), ForeignKey("pull_requests.pull_request_id"), nullable=False)
user_id = Column("user_id", Integer(), ForeignKey("users.user_id"), nullable=True) user_id = Column("user_id", Integer(), ForeignKey("users.user_id"), nullable=True)
_reasons = Column("reason", MutationList.as_mutable(JsonType("list", dialect_map=dict(mysql=UnicodeText(16384))))) _reasons = Column(
"reason", MutationList.as_mutable(JsonType("list", dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE))))
)
mandatory = Column("mandatory", Boolean(), nullable=False, default=False) mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
role = Column("role", Unicode(255), nullable=True, default=ROLE_REVIEWER) role = Column("role", Unicode(255), nullable=True, default=ROLE_REVIEWER)
@ -4868,7 +4902,7 @@ class PullRequestReviewers(Base, BaseModel):
user = relationship("User") user = relationship("User")
pull_request = relationship("PullRequest", back_populates="reviewers") pull_request = relationship("PullRequest", back_populates="reviewers")
rule_data = Column("rule_data_json", JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) rule_data = Column("rule_data_json", JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE))))
def rule_user_group_data(self): def rule_user_group_data(self):
""" """
@ -5233,7 +5267,9 @@ class Integration(Base, BaseModel):
name = Column("name", String(255), nullable=False) name = Column("name", String(255), nullable=False)
child_repos_only = Column("child_repos_only", Boolean(), nullable=False, default=False) child_repos_only = Column("child_repos_only", Boolean(), nullable=False, default=False)
settings = Column("settings_json", MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) settings = Column(
"settings_json", MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE))))
)
repo_id = Column("repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None) repo_id = Column("repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
repo = relationship("Repository", lazy="joined", back_populates="integrations") repo = relationship("Repository", lazy="joined", back_populates="integrations")

View file

@ -647,6 +647,7 @@ def PullRequestForm(localizer, repo_id):
pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255) pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
pullrequest_desc = v.UnicodeString(strip=True, required=False) pullrequest_desc = v.UnicodeString(strip=True, required=False)
description_renderer = v.UnicodeString(strip=True, required=False) description_renderer = v.UnicodeString(strip=True, required=False)
close_branch_before_merging = v.StringBoolean(if_missing=False)
return _PullRequestForm return _PullRequestForm

View file

@ -31,6 +31,10 @@ import urllib.error
import collections import collections
import dataclasses as dataclasses import dataclasses as dataclasses
from copy import deepcopy
from pyramid.threadlocal import get_current_registry
from rhodecode.lib.pyramid_utils import get_current_request from rhodecode.lib.pyramid_utils import get_current_request
from rhodecode.lib.vcs.nodes import FileNode from rhodecode.lib.vcs.nodes import FileNode
@ -809,6 +813,7 @@ class PullRequestModel(BaseModel):
reviewer_data=None, reviewer_data=None,
translator=None, translator=None,
auth_user=None, auth_user=None,
settings=None,
): ):
translator = translator or get_current_request().translate translator = translator or get_current_request().translate
@ -830,6 +835,8 @@ class PullRequestModel(BaseModel):
pull_request.reviewer_data = reviewer_data pull_request.reviewer_data = reviewer_data
pull_request.pull_request_state = pull_request.STATE_CREATING pull_request.pull_request_state = pull_request.STATE_CREATING
pull_request.common_ancestor_id = common_ancestor_id pull_request.common_ancestor_id = common_ancestor_id
if self._settings_valid(settings):
pull_request.settings = settings
Session().add(pull_request) Session().add(pull_request)
Session().flush() Session().flush()
@ -938,6 +945,19 @@ class PullRequestModel(BaseModel):
return pull_request return pull_request
def _settings_valid(self, settings):
if not settings:
return False
if not isinstance(settings, dict):
return False
if len(settings) > 1:
return False
if "close_branch_before_merging" not in settings:
return False
if not isinstance(settings["close_branch_before_merging"], bool):
return False
return True
def trigger_pull_request_hook(self, pull_request, user, action, data=None): def trigger_pull_request_hook(self, pull_request, user, action, data=None):
pull_request = self.__get_pull_request(pull_request) pull_request = self.__get_pull_request(pull_request)
target_scm = pull_request.target_repo.scm_instance() target_scm = pull_request.target_repo.scm_instance()
@ -1470,6 +1490,19 @@ class PullRequestModel(BaseModel):
renderer = RstTemplateRenderer() renderer = RstTemplateRenderer()
return renderer.render("pull_request_update.mako", **params) return renderer.render("pull_request_update.mako", **params)
def update_settings(self, pull_request: PullRequest, close_branch_before_merging: bool):
pull_request = self.__get_pull_request(pull_request)
if pull_request.is_closed():
raise ValueError("This pull request is closed")
if pull_request.settings["close_branch_before_merging"] == close_branch_before_merging:
return
settings = deepcopy(pull_request.settings) # need to copy, otherwise SQLalchemy not tracking changes
settings["close_branch_before_merging"] = close_branch_before_merging
pull_request.settings = settings
Session().commit()
def edit(self, pull_request, title, description, description_renderer, user): def edit(self, pull_request, title, description, description_renderer, user):
pull_request = self.__get_pull_request(pull_request) pull_request = self.__get_pull_request(pull_request)
old_data = pull_request.get_api_data(with_merge_state=False) old_data = pull_request.get_api_data(with_merge_state=False)
@ -2191,14 +2224,20 @@ class PullRequestModel(BaseModel):
user_name = getattr(user, user_name_attr) user_name = getattr(user, user_name_attr)
return user_name return user_name
def _close_branch_before_merging(self, pull_request): def _close_branch_before_merging(self, pull_request: PullRequest):
repo_type = pull_request.target_repo.repo_type repo_type = pull_request.target_repo.repo_type
if repo_type == "hg": if repo_type not in ["hg", "git"]:
return self._get_general_setting(pull_request, "rhodecode_hg_close_branch_before_merging") return False
elif repo_type == "git":
return self._get_general_setting(pull_request, "rhodecode_git_close_branch_before_merging")
return False registry = get_current_registry()
is_enterprise = registry.settings.get("rhodecode.edition_id") == "EE"
if is_enterprise and pull_request.settings and "close_branch_before_merging" in pull_request.settings:
# this feature is available only for the EE edition
return pull_request.settings["close_branch_before_merging"]
key = "rhodecode_{}_close_branch_before_merging".format(repo_type)
return self._get_general_setting(pull_request, key)
def _get_general_setting(self, pull_request, settings_key, default=False): def _get_general_setting(self, pull_request, settings_key, default=False):
settings_model = VcsSettingsModel(repo=pull_request.target_repo) settings_model = VcsSettingsModel(repo=pull_request.target_repo)

View file

@ -1976,6 +1976,10 @@ BIN_FILENODE = 7
} }
} }
.pull-request-settings {
margin: 2px 7px;
}
.pull-request-merge ul { .pull-request-merge ul {
padding: 0px 0px; padding: 0px 0px;
} }

View file

@ -528,6 +528,13 @@ var autoCompleteHandler = function (inputId, controller, role) {
} }
} }
let updateCloseBranchSetting = function(repo_name, pull_request_id, close_branch_before_merging) {
const postData = {
'close_branch_before_merging': close_branch_before_merging,
};
_updatePullRequest(repo_name, pull_request_id, postData);
}
/** /**
* Reviewer autocomplete * Reviewer autocomplete
*/ */

View file

@ -296,7 +296,7 @@
<div class="checkbox"> <div class="checkbox">
${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)} ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
<label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label> <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging')}</label>
</div> </div>
<div class="label"> <div class="label">
<span class="help-block">${_('Delete branch after merging it into destination branch.')}</span> <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>

View file

@ -216,6 +216,19 @@
<input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}"> <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
</div> </div>
</div> </div>
% if c.rhodecode_edition_id == 'EE':
<div class="pull-request-settings">
${h.checkbox('close_branch_before_merging',
checked=c.repo_close_branch_before_merging, value=True)}
<label for="close_branch_before_merging">
% if c.rhodecode_db_repo.repo_type == 'hg':
${h.literal(_('Close branch before merging'))}
% elif c.rhodecode_db_repo.repo_type == 'git':
${h.literal(_('Delete branch after merging'))}
% endif
</label>
</div>
% endif
</div> </div>
</div> </div>
</div> </div>

View file

@ -66,6 +66,21 @@
<input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}> <input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
${h.end_form()} ${h.end_form()}
% if c.rhodecode_edition_id == 'EE':
<div class="pull-request-settings"
id="close_branch_before_merging_wrapper"
>
${h.checkbox('close_branch_before_merging', checked=c.pull_request.settings.get("close_branch_before_merging", False))}
<label for="close_branch_before_merging">
% if c.rhodecode_db_repo.repo_type == 'hg':
${h.literal(_('Close branch before merging'))}
% elif c.rhodecode_db_repo.repo_type == 'git':
${h.literal(_('Delete branch after merging'))}
% endif
</label>
</div>
% endif
<div class="pull-request-merge-refresh"> <div class="pull-request-merge-refresh">
<a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a> <a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
</div> </div>
@ -80,3 +95,14 @@
</div> </div>
</div> </div>
<script>
$("#close_branch_before_merging").on('change', function () {
const $checkbox = $(this);
updateCloseBranchSetting(
"${c.repo_name}",
${c.pull_request.pull_request_id},
$checkbox.prop('checked'),
);
});
</script>

View file

@ -26,6 +26,9 @@ import socket
import subprocess import subprocess
import time import time
import uuid import uuid
from contextlib import contextmanager
from copy import copy, deepcopy
import dateutil.tz import dateutil.tz
import logging import logging
import functools import functools
@ -172,6 +175,17 @@ def plain_http_environ():
} }
@contextmanager
def temporary_settings(baseapp, **overrides):
original = deepcopy(baseapp.config.registry.settings)
try:
baseapp.config.registry.settings.update(overrides)
yield
finally:
baseapp.config.registry.settings.clear()
baseapp.config.registry.settings.update(original)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def baseapp(request, ini_config, http_environ_session, available_port_factory, vcsserver_factory, celery_factory): 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.lib.config_utils import get_app_config

View file

@ -284,6 +284,14 @@ class AssertResponse(object):
element = self.get_element(css_selector) element = self.get_element(css_selector)
assert expected_content in element.text_content() assert expected_content in element.text_content()
def element_contain_attribute(self, css_selector, attr_name, attr_value):
element = self.get_element(css_selector)
assert element.attrib.get(attr_name) == attr_value
def element_contain_no_attribute(self, css_selector, attr_name):
element = self.get_element(css_selector)
assert not element.attrib.get(attr_name)
def element_value_contains(self, css_selector, expected_content): def element_value_contains(self, css_selector, expected_content):
element = self.get_element(css_selector) element = self.get_element(css_selector)
assert expected_content in element.value assert expected_content in element.value