diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py index 69edc3d9..402aa69f 100644 --- a/rhodecode/apps/repository/views/repo_pull_requests.py +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -1498,6 +1498,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): if merge_possible: log.debug("Pre-conditions checked, trying to merge.") + + controls = peppercorn.parse(self.request.POST.items()) + squash_before_merge = str2bool(controls.get("squash_before_merge", "false")) + extras = vcs_operation_context( self.request.environ, repo_name=pull_request.target_repo.repo_name, @@ -1506,7 +1510,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): scm=pull_request.target_repo.repo_type, ) with pull_request.set_state(PullRequest.STATE_UPDATING): - self._merge_pull_request(pull_request, self._rhodecode_db_user, extras) + self._merge_pull_request( + pull_request, self._rhodecode_db_user, extras, squash_commits=squash_before_merge + ) else: log.debug("Pre-conditions failed, NOT merging.") @@ -1518,9 +1524,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): ) ) - def _merge_pull_request(self, pull_request, user, extras): + def _merge_pull_request(self, pull_request, user, extras, squash_commits=False): _ = self.request.translate - merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras) + merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras, squash_commits=squash_commits) if merge_resp.executed: log.debug("The merge was successful, closing the pull request.") diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py index 806fe07b..15aca4be 100644 --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -1169,6 +1169,7 @@ class BaseRepository(object): dry_run=False, use_rebase=False, close_branch=False, + squash_commits=False, ): """ Merge the revisions specified in `source_ref` from `source_repo` @@ -1222,6 +1223,7 @@ class BaseRepository(object): dry_run=dry_run, use_rebase=use_rebase, close_branch=close_branch, + squash_commits=squash_commits, ) except RepositoryError as exc: log.exception("Unexpected failure when running merge, dry-run=%s", dry_run) @@ -1240,6 +1242,7 @@ class BaseRepository(object): dry_run=False, use_rebase=False, close_branch=False, + squash_commits=False, ): """Internal implementation of merge.""" raise NotImplementedError diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py index 0eb337c9..4d88c4e0 100644 --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -24,6 +24,7 @@ import logging import os import re +from celery.platforms import strargv from zope.cachedescriptors.property import Lazy as LazyProperty from collections import OrderedDict @@ -842,7 +843,7 @@ class GitRepository(BaseRepository): cmd.extend(["--no-tags", repository_path, branch_name]) self.run_git_command(cmd, fail_on_stderr=False) - def _local_merge(self, merge_message, user_name, user_email, heads): + def _local_merge(self, merge_message, user_name, user_email, heads, squash_commits=False): """ Merge the given head into the checked out branch. @@ -853,6 +854,7 @@ class GitRepository(BaseRepository): :param merge_message: The message to use for the merge commit. :param heads: the heads to merge. + :param squash_commits: Use squash strategy to merge. """ if self.bare: raise RepositoryError("Cannot merge into a bare git repository") @@ -863,40 +865,68 @@ class GitRepository(BaseRepository): if self.is_empty(): # TODO(skreft): do something more robust in this case. raise RepositoryError("Do not know how to merge into empty repositories yet") - unresolved = None - # N.B.(skreft): the --no-ff option is used to enforce the creation of a - # commit message. We also specify the user who is doing the merge. - cmd = [ + base_cmd = [ "-c", f'user.name="{user_name}"', "-c", f"user.email={user_email}", - "merge", - "--no-ff", - "-m", - safe_str(merge_message), ] - merge_cmd = cmd + heads + if squash_commits: + self._squash_merge(base_cmd, heads, merge_message) + else: + self._normal_merge(base_cmd, heads, merge_message) + def _normal_merge(self, base_cmd, heads, merge_message): try: + # N.B.(skreft): the --no-ff option is used to enforce the creation of a + # commit message. We also specify the user who is doing the merge. + merge_cmd = base_cmd + ["merge", "--no-ff", "-m", safe_str(merge_message)] + heads + + log.debug("Merge command: %s", merge_cmd) self.run_git_command(merge_cmd, fail_on_stderr=False) - except RepositoryError: - files = self.run_git_command(["diff", "--name-only", "--diff-filter", "U"], fail_on_stderr=False)[ - 0 - ].splitlines() - # NOTE(marcink): we add U notation for consistent with HG backend output - unresolved = [f"U {f}" for f in files] + except RepositoryError as e: + log.warning("Merge failed with error: %s", e) + self._abort_merge() - # Cleanup any merge leftovers - self._remote.invalidate_vcs_cache() - self.run_git_command(["merge", "--abort"], fail_on_stderr=False) + def _squash_merge(self, base_cmd, heads, merge_message): + try: + squash_cmd = ( + base_cmd + + [ + "merge", + "--squash", + ] + + heads + ) - if unresolved: - raise UnresolvedFilesInRepo(unresolved) - else: - raise + log.debug("Squash command: %s", squash_cmd) + self.run_git_command(squash_cmd, fail_on_stderr=False) + + commit_cmd = base_cmd + ["commit", "-m", safe_str(merge_message)] + + log.debug("Commit after squash command: %s", commit_cmd) + self.run_git_command(commit_cmd, fail_on_stderr=False) + except RepositoryError as e: + log.warning("Squash failed with error: %s", e) + self._abort_merge() + + def _abort_merge(self): + files = self.run_git_command(["diff", "--name-only", "--diff-filter", "U"], fail_on_stderr=False)[ + 0 + ].splitlines() + + # NOTE(marcink): we add U notation for consistent with HG backend output + unresolved = [f"U {f}" for f in files] + + # Cleanup merge state + self._remote.invalidate_vcs_cache() + self.run_git_command(["merge", "--abort"], fail_on_stderr=False) + + if unresolved: + raise UnresolvedFilesInRepo(unresolved) + raise RepositoryError("Merge failed without unresolved files") def _local_push(self, source_branch, repository_path, target_branch, enable_hooks=False, rc_scm_data=None): """ @@ -961,10 +991,16 @@ class GitRepository(BaseRepository): dry_run=False, use_rebase=False, close_branch=False, + squash_commits=False, ): - log.debug( - "Executing merge_repo with %s strategy, dry_run mode:%s", "rebase" if use_rebase else "merge", dry_run - ) + if squash_commits: + strategy = "squash and merge" + elif use_rebase: + strategy = "rebase" + else: + strategy = "merge" + + log.debug("Executing merge_repo with '%s' strategy, dry_run mode: %s", strategy, dry_run) from rhodecode.model.repo import RepoModel if target_ref.commit_id != self.branches[target_ref.name]: @@ -1026,7 +1062,9 @@ class GitRepository(BaseRepository): merge_failure_reason = MergeFailureReason.NONE metadata = {} try: - shadow_repo._local_merge(merge_message, merger_name, merger_email, [source_ref.commit_id]) + shadow_repo._local_merge( + merge_message, merger_name, merger_email, [source_ref.commit_id], squash_commits=squash_commits + ) merge_possible = True # Need to invalidate the cache, or otherwise we diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py index 3adc80b7..7424857b 100644 --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -680,6 +680,7 @@ class MercurialRepository(BaseRepository): use_rebase=False, close_commit_id=None, dry_run=False, + squash_commits=False, ): """ Merge the given source_revision into the checked out revision. @@ -711,11 +712,11 @@ class MercurialRepository(BaseRepository): return source_ref_commit_id, True unresolved = None - if use_rebase: + if use_rebase or squash_commits: try: bookmark_name = f"rcbook{source_ref_commit_id}{target_ref_commit_id}" self.bookmark(bookmark_name, revision=source_ref.commit_id) - self._remote.rebase(source=source_ref_commit_id, dest=target_ref_commit_id) + self._remote.rebase(source=source_ref_commit_id, dest=target_ref_commit_id, collapse=squash_commits) self._remote.invalidate_vcs_cache() self._update(bookmark_name, clean=True) return self._identify(), True @@ -809,10 +810,16 @@ class MercurialRepository(BaseRepository): dry_run=False, use_rebase=False, close_branch=False, + squash_commits=False, ): - log.debug( - "Executing merge_repo with %s strategy, dry_run mode:%s", "rebase" if use_rebase else "merge", dry_run - ) + if squash_commits: + strategy = "squash and merge" + elif use_rebase: + strategy = "rebase" + else: + strategy = "merge" + + log.debug("Executing merge_repo with '%s' strategy, dry_run mode: %s", strategy, dry_run) from rhodecode.model.repo import RepoModel if target_ref.commit_id not in self._heads(): @@ -899,6 +906,7 @@ class MercurialRepository(BaseRepository): use_rebase=use_rebase, close_commit_id=close_commit_id, dry_run=dry_run, + squash_commits=squash_commits, ) merge_possible = True diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py index 71156d36..b4734db2 100644 --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -1011,12 +1011,12 @@ class PullRequestModel(BaseModel): return commit_ids - def merge_repo(self, pull_request, user, extras): + def merge_repo(self, pull_request, user, extras, squash_commits=False): repo_type = pull_request.source_repo.repo_type log.debug("Merging pull request %s", pull_request) extras["user_agent"] = "{}/internal-merge".format(repo_type) - merge_state = self._merge_pull_request(pull_request, user, extras) + merge_state = self._merge_pull_request(pull_request, user, extras, squash_commits=squash_commits) if merge_state.executed: log.debug("Merge was successful, updating the pull request comments.") self._comment_and_close_pr(pull_request, user, merge_state) @@ -1027,7 +1027,7 @@ class PullRequestModel(BaseModel): log.warning("Merge failed, not updating the pull request.") return merge_state - def _merge_pull_request(self, pull_request, user, extras, merge_msg=None): + def _merge_pull_request(self, pull_request, user, extras, merge_msg=None, squash_commits=False): target_vcs = pull_request.target_repo.scm_instance() source_vcs = pull_request.source_repo.scm_instance() @@ -1067,6 +1067,7 @@ class PullRequestModel(BaseModel): message=message, use_rebase=use_rebase, close_branch=close_branch, + squash_commits=squash_commits, ) return merge_state diff --git a/rhodecode/public/css/buttons.less b/rhodecode/public/css/buttons.less index 3800f50c..8fa266b3 100644 --- a/rhodecode/public/css/buttons.less +++ b/rhodecode/public/css/buttons.less @@ -521,6 +521,15 @@ input[type="submit"] { } } +a[role="button"].btn { + &.disabled { + .border ( @border-thickness-buttons, @rcblue ); + background-color: @rcblue; + color: white; + opacity: .5; + } +} + input[type="reset"] { &:extend(.btn-default); diff --git a/rhodecode/templates/pullrequests/pullrequest_merge_checks.mako b/rhodecode/templates/pullrequests/pullrequest_merge_checks.mako index c1712b04..8e7cdfe6 100644 --- a/rhodecode/templates/pullrequests/pullrequest_merge_checks.mako +++ b/rhodecode/templates/pullrequests/pullrequest_merge_checks.mako @@ -1,70 +1,100 @@ -