diff --git a/CHANGELOG b/CHANGELOG index d053a89b..a9edbd29 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ # Change Log All notable changes to this project will be documented in this file. +## [3.11.0] - 2024-01-26 +# Added +- Support of merging a PR and skip the queue when it is not needed. + ## [3.10.0] - 2023-11-14 # Added - Support of tags created with `v` prefix. diff --git a/bert_e/settings.py b/bert_e/settings.py index 123a0854..b50e29fd 100644 --- a/bert_e/settings.py +++ b/bert_e/settings.py @@ -184,6 +184,8 @@ class Meta: quiet = fields.Bool(required=False, load_default=False) disable_queues = fields.Bool(required=False, load_default=False) use_queues = fields.Bool(required=False, load_default=True) + skip_queue_when_not_needed = fields.Bool( + required=False, load_default=False) cmd_line_options = fields.List(fields.Str(), load_default=[]) client_id = fields.Str(required=False, load_default='') diff --git a/bert_e/tests/test_bert_e.py b/bert_e/tests/test_bert_e.py index ecbfc5d9..44d4e616 100644 --- a/bert_e/tests/test_bert_e.py +++ b/bert_e/tests/test_bert_e.py @@ -7690,6 +7690,41 @@ def test_job_rebuild_queues_without_hotfix(self): self.assertFalse(self.gitrepo.remote_branch_exists(branch), "branch %s shouldn't exist" % branch) + def test_no_need_queuing(self): + """Expect Bert-E to skip the queue when there is no need to queue.""" + + # Two PRs created at the same time + # At the moment they were created they are both up to date with the + # destination branch + self.init_berte( + options=self.bypass_all, skip_queue_when_not_needed=True) + first_pr = self.create_pr('feature/TEST-1', 'development/4.3') + second_pr = self.create_pr('feature/TEST-2', 'development/4.3') + # The first PR is ready to merge, and is expected to merge directly + # without going through the queue. + self.process_pr_job(first_pr, 'SuccessMessage') + # When the second PR is merged we expect it to go through the queue + # as it is no longer up to date with the destination branch. + self.process_pr_job(second_pr, 'Queued') + # At this point the repository should now contain queue branches. + # We force the merge to get everything setup according for the next + # scenario. + self.process_job(ForceMergeQueuesJob(bert_e=self.berte), 'Merged') + # We expect the PR to be merged so there should be nothing left to do. + self.process_pr_job(second_pr, 'NothingToDo') + # We get the local repo setup for a third PR that should be up to + # date with the latest changes. + self.gitrepo.cmd('git checkout development/4.3') + self.gitrepo.cmd('git branch --set-upstream-to=origin/development/4.3') + self.gitrepo.cmd('git pull') + third_pr = self.create_pr('feature/TEST-3', 'development/4.3') + fourth_pr = self.create_pr('feature/TEST-4', 'development/4.3') + # Just like the first PR, we expect this one to be merged directly. + self.process_pr_job(third_pr, 'SuccessMessage') + # Now we want to know if when the queue is a bit late is Bert-E + # capable of reeastablishing the Queue in order, and queue PR number 4. + self.process_pr_job(fourth_pr, 'Queued') + def test_bypass_prefixes(self): self.init_berte() pr = self.create_pr('documentation/stuff', 'development/4.3') diff --git a/bert_e/workflow/gitwaterflow/__init__.py b/bert_e/workflow/gitwaterflow/__init__.py index a5ccd452..7056f593 100644 --- a/bert_e/workflow/gitwaterflow/__init__.py +++ b/bert_e/workflow/gitwaterflow/__init__.py @@ -207,10 +207,14 @@ def _handle_pull_request(job: PullRequestJob): # check for conflicts), and all builds were green, and we reached # this point without an error, then all conditions are met to enter # the queue. - if job.settings.use_queue: - # validate current state of queues + queues = queueing.build_queue_collection(job) if job.settings.use_queue \ + else None + + # If we need to go through the queue, we need to check if we can + # merge the integration branches right away, or if we need to add + # the pull request to the queue. + if queueing.is_needed(job, wbranches, queues): try: - queues = queueing.build_queue_collection(job) queues.validate() except messages.IncoherentQueues as err: raise messages.QueueOutOfOrder( @@ -224,8 +228,14 @@ def _handle_pull_request(job: PullRequestJob): issue=job.git.src_branch.jira_issue_key, author=job.pull_request.author_display_name, active_options=job.active_options) - else: + # If we don't need to go through the queue, we can merge the + # integration branches right away. + # But if the bot is configured with the 'use_queue' option, we + # still need to delete the queue to ensure that we don't raise + # IncoherentQueues in the next runs. + if queues is not None: + queues.delete() merge_integration_branches(job, wbranches) job.bert_e.add_merged_pr(job.pull_request.id) job.git.cascade.validate() diff --git a/bert_e/workflow/gitwaterflow/branches.py b/bert_e/workflow/gitwaterflow/branches.py index c3587bb3..0848b924 100644 --- a/bert_e/workflow/gitwaterflow/branches.py +++ b/bert_e/workflow/gitwaterflow/branches.py @@ -743,6 +743,18 @@ def has_version_queued_prs(self, version): return (self._queues.get(version, {}).get(QueueIntegrationBranch) is not None) + def delete(self): + """Delete the queues entirely.""" + + for branch in self._queues.values(): + queue: QueueBranch = branch[QueueBranch] + queue.dst_branch.checkout() + queue.remove(do_push=True) + queue_integration: QueueIntegrationBranch | None = branch.get( + QueueIntegrationBranch) + if queue_integration: + queue_integration.remove(do_push=True) + class BranchCascade(object): def __init__(self): diff --git a/bert_e/workflow/gitwaterflow/queueing.py b/bert_e/workflow/gitwaterflow/queueing.py index 2653f406..08c08c0d 100644 --- a/bert_e/workflow/gitwaterflow/queueing.py +++ b/bert_e/workflow/gitwaterflow/queueing.py @@ -18,16 +18,17 @@ from bert_e import exceptions from bert_e.job import handler as job_handler -from bert_e.job import QueuesJob +from bert_e.job import QueuesJob, PullRequestJob from bert_e.lib import git from ..git_utils import clone_git_repo, consecutive_merge, robust_merge, push from ..pr_utils import send_comment -from .branches import (BranchCascade, DevelopmentBranch, IntegrationBranch, - QueueBranch, QueueIntegrationBranch, - branch_factory, build_queue_collection) +from .branches import (BranchCascade, DevelopmentBranch, GWFBranch, + IntegrationBranch, QueueBranch, QueueCollection, + QueueIntegrationBranch, branch_factory, + build_queue_collection) from .integration import get_integration_branches - +from typing import List LOG = logging.getLogger(__name__) @@ -207,3 +208,38 @@ def close_queued_pull_request(job, pr_id, cascade): except git.RemoveFailedException: # not critical pass + + +def is_needed( + job: PullRequestJob, + wbranches: List[GWFBranch], + queues: QueueCollection | None): + """Determine if queuing is required to merge the given PR. + + Queuing a pull request should only be done if: + - The PR or the integration branches are not up to date + with the destination branch. + - Other PRs are already in the queue. + + Returns: + - True if the PR should be queued. + - False otherwise. + """ + + if queues is None or job.settings.use_queue is False: + return False + + if (job.settings.skip_queue_when_not_needed is False or + already_in_queue(job, wbranches) or + len(queues.queued_prs) > 0): + return True + + if not job.git.src_branch.includes_commit( + job.git.dst_branch.get_latest_commit()): + return True + # Check if the wbranches all contain the commits in the dst branches + for branch, dst_branch in zip(wbranches, job.git.cascade.dst_branches): + if not branch.includes_commit(dst_branch.get_latest_commit()): + return True + + return False