mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
parent
1a5c143a00
commit
57a176ac87
5
runbot_merge/changelog/2022-10/squash.md
Normal file
5
runbot_merge/changelog/2022-10/squash.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ADD: enable "squash" merge mode for multi-commit PRs
|
||||||
|
|
||||||
|
After 4 years, the world is apparently ready. Squashing tries to preserve
|
||||||
|
authorship, depending on the number of authors (and committers) on the PR.
|
||||||
|
Squashing does *not* preserve any part of existing commit messages.
|
@ -916,17 +916,14 @@ class PullRequests(models.Model):
|
|||||||
)
|
)
|
||||||
elif command == 'method':
|
elif command == 'method':
|
||||||
if is_reviewer:
|
if is_reviewer:
|
||||||
if param == 'squash' and not self.squash:
|
self.merge_method = param
|
||||||
msg = "squash can only be used with a single commit at this time."
|
ok = True
|
||||||
else:
|
explanation = next(label for value, label in type(self).merge_method.selection if value == param)
|
||||||
self.merge_method = param
|
Feedback.create({
|
||||||
ok = True
|
'repository': self.repository.id,
|
||||||
explanation = next(label for value, label in type(self).merge_method.selection if value == param)
|
'pull_request': self.number,
|
||||||
Feedback.create({
|
'message':"Merge method set to %s." % explanation
|
||||||
'repository': self.repository.id,
|
})
|
||||||
'pull_request': self.number,
|
|
||||||
'message':"Merge method set to %s." % explanation
|
|
||||||
})
|
|
||||||
elif command == 'override':
|
elif command == 'override':
|
||||||
overridable = author.override_rights\
|
overridable = author.override_rights\
|
||||||
.filtered(lambda r: not r.repository_id or (r.repository_id == self.repository))\
|
.filtered(lambda r: not r.repository_id or (r.repository_id == self.repository))\
|
||||||
@ -1325,11 +1322,43 @@ class PullRequests(models.Model):
|
|||||||
gh, target, pr_commits, related_prs=related_prs)
|
gh, target, pr_commits, related_prs=related_prs)
|
||||||
|
|
||||||
def _stage_squash(self, gh, target, commits, related_prs=()):
|
def _stage_squash(self, gh, target, commits, related_prs=()):
|
||||||
assert len(commits) == 1, "can only squash a single commit"
|
|
||||||
msg = self._build_merge_message(self, related_prs=related_prs)
|
msg = self._build_merge_message(self, related_prs=related_prs)
|
||||||
commits[0]['commit']['message'] = str(msg)
|
authors = {
|
||||||
head, mapping = gh.rebase(self.number, target, commits=commits)
|
(c['commit']['author']['name'], c['commit']['author']['email'])
|
||||||
self.commits_map = json.dumps({**mapping, '': head})
|
for c in commits
|
||||||
|
}
|
||||||
|
author = None
|
||||||
|
if len(authors) == 1:
|
||||||
|
name, email = authors.pop()
|
||||||
|
author = {'name': name, 'email': email}
|
||||||
|
for author in authors:
|
||||||
|
msg.headers['Co-Authored-By'] = "%s <%s>" % author
|
||||||
|
|
||||||
|
committers = {
|
||||||
|
(c['commit']['committer']['name'], c['commit']['committer']['email'])
|
||||||
|
for c in commits
|
||||||
|
}
|
||||||
|
committer = None
|
||||||
|
if len(committers) == 1:
|
||||||
|
name, email = committers.pop()
|
||||||
|
committer = {'name': name, 'email': email}
|
||||||
|
# should committers also be added to co-authors?
|
||||||
|
|
||||||
|
original_head = gh.head(target)
|
||||||
|
merge_tree = gh.merge(self.head, target, 'temp merge')['tree']['sha']
|
||||||
|
head = gh('post', 'git/commits', json={
|
||||||
|
'message': str(msg),
|
||||||
|
'tree': merge_tree,
|
||||||
|
'parents': [original_head],
|
||||||
|
'author': author,
|
||||||
|
'committer': committer,
|
||||||
|
}).json()['sha']
|
||||||
|
gh.set_ref(target, head)
|
||||||
|
|
||||||
|
commits_map = {c['sha']: head for c in commits}
|
||||||
|
commits_map[''] = head
|
||||||
|
self.commits_map = json.dumps(commits_map)
|
||||||
|
|
||||||
return head
|
return head
|
||||||
|
|
||||||
def _stage_rebase_ff(self, gh, target, commits, related_prs=()):
|
def _stage_rebase_ff(self, gh, target, commits, related_prs=()):
|
||||||
|
@ -2043,74 +2043,44 @@ Part-of: {pr_id.display_name}"""
|
|||||||
(users['reviewer'], 'hansen r+ squash'),
|
(users['reviewer'], 'hansen r+ squash'),
|
||||||
(users['user'], 'Merge method set to squash.')
|
(users['user'], 'Merge method set to squash.')
|
||||||
]
|
]
|
||||||
merged_head = repo.commit('master')
|
|
||||||
assert merged_head.message == f"""first pr
|
pr2_id = to_pr(env, pr2)
|
||||||
|
assert pr2_id.state == 'merged'
|
||||||
|
assert pr2.comments == [
|
||||||
|
seen(env, pr2, users),
|
||||||
|
(users['reviewer'], 'hansen r+ squash'),
|
||||||
|
(users['user'], 'Merge method set to squash.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
two, one, _root = repo.log('master')
|
||||||
|
|
||||||
|
assert one['commit']['message'] == f"""first pr
|
||||||
|
|
||||||
closes {pr1_id.display_name}
|
closes {pr1_id.display_name}
|
||||||
|
|
||||||
Signed-off-by: {get_partner(env, users["reviewer"]).formatted_email}\
|
Signed-off-by: {get_partner(env, users["reviewer"]).formatted_email}\
|
||||||
"""
|
"""
|
||||||
assert merged_head.committer['name'] == 'bob'
|
assert one['commit']['committer']['name'] == 'bob'
|
||||||
assert merged_head.committer['email'] == 'builder@example.org'
|
assert one['commit']['committer']['email'] == 'builder@example.org'
|
||||||
commit_date = datetime.datetime.strptime(merged_head.committer['date'], '%Y-%m-%dT%H:%M:%SZ')
|
commit_date = datetime.datetime.strptime(one['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ')
|
||||||
# using timestamp (and working in seconds) because `pytest.approx`
|
# using timestamp (and working in seconds) because `pytest.approx`
|
||||||
# silently fails on datetimes (#8395)
|
# silently fails on datetimes (#8395)
|
||||||
assert commit_date.timestamp() == pytest.approx(time.time(), abs=5*60), \
|
assert commit_date.timestamp() == pytest.approx(time.time(), abs=5*60), \
|
||||||
"the commit date of the merged commit should be about now, despite" \
|
"the commit date of the merged commit should be about now, despite" \
|
||||||
" the source commit being >20 years old"
|
" the source commit being >20 years old"
|
||||||
|
|
||||||
pr2_id = to_pr(env, pr2)
|
assert two['commit']['message'] == f"""second pr
|
||||||
assert pr2_id.state == 'ready'
|
|
||||||
assert not pr2_id.merge_method
|
|
||||||
assert pr2.comments == [
|
|
||||||
seen(env, pr2, users),
|
|
||||||
(users['reviewer'], 'hansen r+ squash'),
|
|
||||||
(users['user'], f"I'm sorry, @{users['reviewer']}: squash can only be used with a single commit at this time."),
|
|
||||||
(users['user'], """@{user} @{reviewer} because this PR has multiple commits, I need to know how to merge it:
|
|
||||||
|
|
||||||
* `merge` to merge directly, using the PR as merge commit message
|
closes {pr2_id.display_name}
|
||||||
* `rebase-merge` to rebase and merge, using the PR as merge commit message
|
|
||||||
* `rebase-ff` to rebase and fast-forward
|
|
||||||
""".format_map(users))
|
|
||||||
]
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="removed support for squash- command")
|
Signed-off-by: {get_partner(env, users["reviewer"]).formatted_email}\
|
||||||
def test_disable_squash_merge(self, repo, env, config):
|
"""
|
||||||
m = repo.make_commit(None, 'initial', None, tree={'m': 'm'})
|
assert repo.read_tree(repo.commit(two['sha'])) == {
|
||||||
m2 = repo.make_commit(m, 'second', None, tree={'m': 'm', 'm2': 'm2'})
|
'a': '0',
|
||||||
repo.make_ref('heads/master', m2)
|
'b': '0',
|
||||||
|
'x': '1',
|
||||||
c1 = repo.make_commit(m, 'first', None, tree={'m': 'c1'})
|
|
||||||
prx = repo.make_pr(title='title', body='body', target='master', head=c1)
|
|
||||||
repo.post_status(prx.head, 'success', 'legal/cla')
|
|
||||||
repo.post_status(prx.head, 'success', 'ci/runbot')
|
|
||||||
prx.post_comment('hansen r+ squash-', config['role_reviewer']['token'])
|
|
||||||
assert not env['runbot_merge.pull_requests'].search([
|
|
||||||
('repository.name', '=', repo.name),
|
|
||||||
('number', '=', prx.number)
|
|
||||||
]).squash
|
|
||||||
|
|
||||||
env.run_crons()
|
|
||||||
assert env['runbot_merge.pull_requests'].search([
|
|
||||||
('repository.name', '=', repo.name),
|
|
||||||
('number', '=', prx.number)
|
|
||||||
]).staging_id
|
|
||||||
|
|
||||||
staging = repo.commit('heads/staging.master')
|
|
||||||
assert repo.is_ancestor(prx.head, of=staging.id)
|
|
||||||
assert staging.parents == [m2, c1]
|
|
||||||
assert repo.read_tree(staging) == {
|
|
||||||
'm': 'c1', 'm2': 'm2',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.post_status(staging.id, 'success', 'legal/cla')
|
|
||||||
repo.post_status(staging.id, 'success', 'ci/runbot')
|
|
||||||
env.run_crons()
|
|
||||||
assert env['runbot_merge.pull_requests'].search([
|
|
||||||
('repository.name', '=', repo.name),
|
|
||||||
('number', '=', prx.number)
|
|
||||||
]).state == 'merged'
|
|
||||||
assert prx.state == 'closed'
|
|
||||||
|
|
||||||
class TestPRUpdate(object):
|
class TestPRUpdate(object):
|
||||||
""" Pushing on a PR should update the HEAD except for merged PRs, it
|
""" Pushing on a PR should update the HEAD except for merged PRs, it
|
||||||
|
Loading…
Reference in New Issue
Block a user