Add rebase-merge merge mode

GitHub supports a "rebase" merge mode where it will rebase the PR
onto the target branch and fast-forward the target branch to the
result of the rebase.

Add support for this process to the merger so that it can prepare
an effective simulated repo, and map the merge-mode to the merge
operation in the reporter so that gating behavior matches.

This change also makes a few tweaks to the merger to improve
consistency (including renaming a variable ref->base), and corrects
some typos in the similar squash merge test methods.

Change-Id: I9db1d163bafda38204360648bb6781800d2a09b4
This commit is contained in:
James E. Blair 2022-10-17 14:27:05 -07:00
parent e2a472bc97
commit 26b9b0e2fb
8 changed files with 132 additions and 16 deletions

View File

@ -138,13 +138,20 @@ pipeline.
.. value:: cherry-pick
Cherry-picks each change onto the branch rather than
performing any merges. This is not supported by Github and GitLab.
performing any merges. This is not supported by GitHub and GitLab.
.. value:: squash-merge
Squash merges each change onto the branch. This maps to the
merge mode ``squash`` in GitHub and GitLab.
.. value:: rebase
Rebases the changes onto the branch. This is only supported
by GitHub and maps to the ``rebase`` merge mode (but
does not alter committer information in the way that GitHub
does in the repos that Zuul prepares for jobs).
.. attr:: vars
:default: None

View File

@ -0,0 +1,5 @@
---
features:
- |
The :value:`project.merge-mode.rebase` merge-mode is now supported
for GitHub.

View File

@ -0,0 +1,38 @@
- pipeline:
name: gate
manager: dependent
trigger:
github:
- event: pull_request
action:
- opened
- changed
- reopened
branch: ^master$
success:
github:
status: success
merge: true
failure:
github: {}
- job:
name: base
parent: null
run: playbooks/base.yaml
- job:
name: project-test1
run: playbooks/project-test1.yaml
- job:
name: project-test2
run: playbooks/project-test2.yaml
- project:
name: org/project
merge-mode: rebase
gate:
jobs:
- project-test1
- project-test2

View File

@ -1356,11 +1356,59 @@ class TestGithubDriver(ZuulTestCase):
self.assertEquals(A.comments[1],
'Merge mode cherry-pick not supported by Github')
@simple_layout('layouts/gate-github-rebase.yaml', driver='github')
def test_merge_method_rebase(self):
"""
Tests that the merge mode gets forwarded to the reporter and the
PR was rebased.
"""
self.executor_server.keep_jobdir = True
self.executor_server.hold_jobs_in_build = True
github = self.fake_github.getGithubClient()
repo = github.repo_from_project('org/project')
repo._set_branch_protection(
'master', contexts=['tenant-one/check', 'tenant-one/gate'])
A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
repo.create_status(A.head_sha, 'success', 'example.com', 'description',
'tenant-one/check')
# Create a second commit on master to verify rebase behavior
self.create_commit('org/project', message="Test rebase commit")
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
self.waitUntilSettled()
build = self.builds[-1]
path = os.path.join(build.jobdir.src_root, 'github.com/org/project')
repo = git.Repo(path)
repo_messages = [c.message.strip() for c in repo.iter_commits()]
repo_messages.reverse()
expected = [
'initial commit',
'initial commit', # simple_layout adds a second "initial commit"
'Test rebase commit',
'A-1',
]
self.assertEqual(expected, repo_messages)
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
# the change should have entered the gate
self.assertEqual(2, len(self.history))
# now check if the merge was done via rebase
merges = [report for report in self.fake_github.github_data.reports
if report[2] == 'merge']
assert (len(merges) == 1 and merges[0][3] == 'rebase')
@simple_layout('layouts/gate-github-squash-merge.yaml', driver='github')
def test_merge_method_squash_merge(self):
"""
Tests that the merge mode gets forwarded to the reporter and the
merge fails because cherry-pick is not supported by github.
PR was squashed.
"""
github = self.fake_github.getGithubClient()
repo = github.repo_from_project('org/project')
@ -1381,7 +1429,7 @@ class TestGithubDriver(ZuulTestCase):
# the change should have entered the gate
self.assertEqual(2, len(self.history))
# now check if the merge was done via rebase
# now check if the merge was done via squash
merges = [report for report in self.fake_github.github_data.reports
if report[2] == 'merge']
assert (len(merges) == 1 and merges[0][3] == 'squash')

View File

@ -1113,7 +1113,8 @@ class ProjectParser(object):
'vars': ansible_vars_dict,
'templates': [str],
'merge-mode': vs.Any('merge', 'merge-resolve',
'cherry-pick', 'squash-merge'),
'cherry-pick', 'squash-merge',
'rebase'),
'default-branch': str,
'queue': str,
str: pipeline_contents,

View File

@ -17,9 +17,8 @@ import logging
import voluptuous as v
import time
from zuul import model
from zuul.lib.logutil import get_annotated_logger
from zuul.model import MERGER_MERGE_RESOLVE, MERGER_MERGE, MERGER_MAP, \
MERGER_SQUASH_MERGE
from zuul.reporter import BaseReporter
from zuul.exceptions import MergeFailure
from zuul.driver.util import scalar_or_list
@ -34,9 +33,10 @@ class GithubReporter(BaseReporter):
# Merge modes supported by github
merge_modes = {
MERGER_MERGE: 'merge',
MERGER_MERGE_RESOLVE: 'merge',
MERGER_SQUASH_MERGE: 'squash',
model.MERGER_MERGE: 'merge',
model.MERGER_MERGE_RESOLVE: 'merge',
model.MERGER_SQUASH_MERGE: 'squash',
model.MERGER_REBASE: 'rebase',
}
def __init__(self, driver, connection, pipeline, config=None):
@ -189,7 +189,8 @@ class GithubReporter(BaseReporter):
merge_mode = item.current_build_set.getMergeMode()
if merge_mode not in self.merge_modes:
mode = [x[0] for x in MERGER_MAP.items() if x[1] == merge_mode][0]
mode = [x[0] for x in model.MERGER_MAP.items()
if x[1] == merge_mode][0]
self.log.warning('Merge mode %s not supported by Github', mode)
raise MergeFailure('Merge mode %s not supported by Github' % mode)

View File

@ -582,7 +582,7 @@ class Repo(object):
repo.git.merge(*args)
return repo.head.commit
def squash_merge(self, item, zuul_event_id=None):
def squashMerge(self, item, zuul_event_id=None):
log = get_annotated_logger(self.log, zuul_event_id)
repo = self.createRepoObject(zuul_event_id)
args = ['--squash', 'FETCH_HEAD']
@ -594,6 +594,17 @@ class Repo(object):
'Merge change %s,%s' % (item['number'], item['patchset']))
return repo.head.commit
def rebaseMerge(self, item, base, zuul_event_id=None):
log = get_annotated_logger(self.log, zuul_event_id)
repo = self.createRepoObject(zuul_event_id)
args = [base]
ref = item['ref']
self.fetch(ref, zuul_event_id=zuul_event_id)
log.debug("Rebasing %s with args %s", ref, args)
repo.git.checkout('FETCH_HEAD')
repo.git.rebase(*args)
return repo.head.commit
def fetch(self, ref, zuul_event_id=None):
repo = self.createRepoObject(zuul_event_id)
# NOTE: The following is currently not applicable, but if we
@ -1029,14 +1040,14 @@ class Merger(object):
for message in messages:
ref_log.debug(message)
def _mergeChange(self, item, ref, zuul_event_id):
def _mergeChange(self, item, base, zuul_event_id):
log = get_annotated_logger(self.log, zuul_event_id)
repo = self.getRepo(item['connection'], item['project'],
zuul_event_id=zuul_event_id)
try:
repo.checkout(ref, zuul_event_id=zuul_event_id)
repo.checkout(base, zuul_event_id=zuul_event_id)
except Exception:
log.exception("Unable to checkout %s", ref)
log.exception("Unable to checkout %s", base)
return None, None
try:
@ -1050,8 +1061,11 @@ class Merger(object):
commit = repo.cherryPick(item['ref'],
zuul_event_id=zuul_event_id)
elif mode == zuul.model.MERGER_SQUASH_MERGE:
commit = repo.squash_merge(
commit = repo.squashMerge(
item, zuul_event_id=zuul_event_id)
elif mode == zuul.model.MERGER_REBASE:
commit = repo.rebaseMerge(
item, base, zuul_event_id=zuul_event_id)
else:
raise Exception("Unsupported merge mode: %s" % mode)
except git.GitCommandError:

View File

@ -55,13 +55,15 @@ from zuul.zk.components import COMPONENT_REGISTRY
MERGER_MERGE = 1 # "git merge"
MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
MERGER_CHERRY_PICK = 3 # "git cherry-pick"
MERGER_SQUASH_MERGE = 4 # "git merge --squash"
MERGER_SQUASH_MERGE = 4 # "git merge --squash"
MERGER_REBASE = 5 # "git rebase"
MERGER_MAP = {
'merge': MERGER_MERGE,
'merge-resolve': MERGER_MERGE_RESOLVE,
'cherry-pick': MERGER_CHERRY_PICK,
'squash-merge': MERGER_SQUASH_MERGE,
'rebase': MERGER_REBASE,
}
PRECEDENCE_NORMAL = 0