mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[CHG] runbot_merge: convert freeze wizard to local repo
Probably less necessary than for the regular staging stuff, but might as well while at it. Requires updating one of the test to generate a non-ff push, as O_CREAT doesn't exist at the git level, and the client (and it is client-side) only protects against force pushes. So there is no way to trigger an issue with just the creation of the new branch, it needs to exist *and point to a non-ancestor commit*. Also remove a sleep in the ref update loop as there are no ref updates anymore, until the very final sync via git. NB: maybe it'd be possible to push both bump and release PRs together for each repo, but getting which update failed in case of failure seems difficult.
This commit is contained in:
parent
1f8d534c02
commit
dff7f102ea
@ -313,92 +313,6 @@ class GH(object):
|
|||||||
f"Sanity check ref update of {branch}, expected {sha} got {head}"
|
f"Sanity check ref update of {branch}, expected {sha} got {head}"
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def merge(self, sha, dest, message):
|
|
||||||
r = self('post', 'merges', json={
|
|
||||||
'base': dest,
|
|
||||||
'head': sha,
|
|
||||||
'commit_message': message,
|
|
||||||
}, check={409: MergeError})
|
|
||||||
try:
|
|
||||||
r = r.json()
|
|
||||||
except Exception:
|
|
||||||
raise MergeError("got non-JSON reponse from github: %s %s (%s)" % (r.status_code, r.reason, r.text))
|
|
||||||
_logger.debug(
|
|
||||||
"merge(%s, %s (%s), %s) -> %s",
|
|
||||||
self._repo, dest, r['parents'][0]['sha'],
|
|
||||||
shorten(message), r['sha']
|
|
||||||
)
|
|
||||||
return dict(r['commit'], sha=r['sha'], parents=r['parents'])
|
|
||||||
|
|
||||||
def rebase(self, pr, dest, reset=False, commits=None):
|
|
||||||
""" Rebase pr's commits on top of dest, updates dest unless ``reset``
|
|
||||||
is set.
|
|
||||||
|
|
||||||
Returns the hash of the rebased head and a map of all PR commits (to the PR they were rebased to)
|
|
||||||
"""
|
|
||||||
logger = _logger.getChild('rebase')
|
|
||||||
original_head = self.head(dest)
|
|
||||||
if commits is None:
|
|
||||||
commits = self.commits(pr)
|
|
||||||
|
|
||||||
logger.debug("rebasing %s, %s on %s (reset=%s, commits=%s)",
|
|
||||||
self._repo, pr, dest, reset, len(commits))
|
|
||||||
|
|
||||||
if not commits:
|
|
||||||
raise MergeError("PR has no commits")
|
|
||||||
prev = original_head
|
|
||||||
for original in commits:
|
|
||||||
if len(original['parents']) != 1:
|
|
||||||
raise MergeError(
|
|
||||||
"commits with multiple parents ({sha}) can not be rebased, "
|
|
||||||
"either fix the branch to remove merges or merge without "
|
|
||||||
"rebasing".format_map(
|
|
||||||
original
|
|
||||||
))
|
|
||||||
tmp_msg = 'temp rebasing PR %s (%s)' % (pr, original['sha'])
|
|
||||||
merged = self.merge(original['sha'], dest, tmp_msg)
|
|
||||||
|
|
||||||
# whichever parent is not original['sha'] should be what dest
|
|
||||||
# deref'd to, and we want to check that matches the "left parent" we
|
|
||||||
# expect (either original_head or the previously merged commit)
|
|
||||||
[base_commit] = (parent['sha'] for parent in merged['parents']
|
|
||||||
if parent['sha'] != original['sha'])
|
|
||||||
if prev != base_commit:
|
|
||||||
raise MergeError(
|
|
||||||
f"Inconsistent view of branch {dest} while rebasing "
|
|
||||||
f"PR {pr} expected commit {prev} but the other parent of "
|
|
||||||
f"merge commit {merged['sha']} is {base_commit}.\n\n"
|
|
||||||
f"The branch may be getting concurrently modified."
|
|
||||||
)
|
|
||||||
prev = merged['sha']
|
|
||||||
original['new_tree'] = merged['tree']['sha']
|
|
||||||
|
|
||||||
prev = original_head
|
|
||||||
mapping = {}
|
|
||||||
for c in commits:
|
|
||||||
committer = c['commit']['committer']
|
|
||||||
committer.pop('date')
|
|
||||||
copy = self('post', 'git/commits', json={
|
|
||||||
'message': c['commit']['message'],
|
|
||||||
'tree': c['new_tree'],
|
|
||||||
'parents': [prev],
|
|
||||||
'author': c['commit']['author'],
|
|
||||||
'committer': committer,
|
|
||||||
}, check={409: MergeError}).json()
|
|
||||||
logger.debug('copied %s to %s (parent: %s)', c['sha'], copy['sha'], prev)
|
|
||||||
prev = mapping[c['sha']] = copy['sha']
|
|
||||||
|
|
||||||
if reset:
|
|
||||||
self.set_ref(dest, original_head)
|
|
||||||
else:
|
|
||||||
self.set_ref(dest, prev)
|
|
||||||
|
|
||||||
logger.debug('rebased %s, %s on %s (reset=%s, commits=%s) -> %s',
|
|
||||||
self._repo, pr, dest, reset, len(commits),
|
|
||||||
prev)
|
|
||||||
# prev is updated after each copy so it's the rebased PR head
|
|
||||||
return prev, mapping
|
|
||||||
|
|
||||||
# fetch various bits of issues / prs to load them
|
# fetch various bits of issues / prs to load them
|
||||||
def pr(self, number):
|
def pr(self, number):
|
||||||
return (
|
return (
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import contextlib
|
|
||||||
import enum
|
import enum
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
from odoo import models, fields, api, Command
|
from odoo import models, fields, api, Command
|
||||||
from odoo.addons.runbot_merge.exceptions import FastForwardError
|
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.tools import drop_view_if_exists
|
from odoo.tools import drop_view_if_exists
|
||||||
|
|
||||||
|
from ... import git
|
||||||
|
from ..pull_requests import Repository
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
class FreezeWizard(models.Model):
|
class FreezeWizard(models.Model):
|
||||||
_name = 'runbot_merge.project.freeze'
|
_name = 'runbot_merge.project.freeze'
|
||||||
@ -211,50 +212,50 @@ class FreezeWizard(models.Model):
|
|||||||
master_name = master.name
|
master_name = master.name
|
||||||
|
|
||||||
gh_sessions = {r: r.github() for r in self.project_id.repo_ids}
|
gh_sessions = {r: r.github() for r in self.project_id.repo_ids}
|
||||||
|
repos: Dict[Repository, git.Repo] = {
|
||||||
|
r: git.get_local(r, 'github').check(False)
|
||||||
|
for r in self.project_id.repo_ids
|
||||||
|
}
|
||||||
|
for repo, copy in repos.items():
|
||||||
|
copy.fetch(git.source_url(repo, 'github'), '+refs/heads/*:refs/heads/*')
|
||||||
|
|
||||||
# prep new branch (via tmp refs) on every repo
|
# prep new branch (via tmp refs) on every repo
|
||||||
rel_heads = {}
|
rel_heads: Dict[Repository, str] = {}
|
||||||
# store for master heads as odds are high the bump pr(s) will be on the
|
# store for master heads as odds are high the bump pr(s) will be on the
|
||||||
# same repo as one of the release PRs
|
# same repo as one of the release PRs
|
||||||
prevs = {}
|
prevs: Dict[Repository, str] = {}
|
||||||
for rel in self.release_pr_ids:
|
for rel in self.release_pr_ids:
|
||||||
repo_id = rel.repository_id
|
repo_id = rel.repository_id
|
||||||
gh = gh_sessions[repo_id]
|
gh = gh_sessions[repo_id]
|
||||||
try:
|
try:
|
||||||
prev = prevs[repo_id] = gh.head(master_name)
|
prev = prevs[repo_id] = gh.head(master_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
raise UserError(f"Unable to resolve branch {master_name} of repository {repo_id.name} to a commit.")
|
raise UserError(f"Unable to resolve branch {master_name} of repository {repo_id.name} to a commit.") from e
|
||||||
|
|
||||||
# create the tmp branch to merge the PR into
|
|
||||||
tmp_branch = f'tmp.{self.branch_name}'
|
|
||||||
try:
|
try:
|
||||||
gh.set_ref(tmp_branch, prev)
|
commits = gh.commits(rel.pr_id.number)
|
||||||
except Exception as err:
|
except Exception as e:
|
||||||
raise UserError(f"Unable to create branch {self.branch_name} of repository {repo_id.name}: {err}.")
|
raise UserError(f"Unable to fetch commits of release PR {rel.pr_id.display_name}.") from e
|
||||||
|
|
||||||
rel_heads[repo_id], _ = gh.rebase(rel.pr_id.number, tmp_branch)
|
rel_heads[repo_id] = repos[repo_id].rebase(prev, commits)[0]
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# prep bump
|
# prep bump
|
||||||
bump_heads = {}
|
bump_heads: Dict[Repository, str] = {}
|
||||||
for bump in self.bump_pr_ids:
|
for bump in self.bump_pr_ids:
|
||||||
repo_id = bump.repository_id
|
repo_id = bump.repository_id
|
||||||
gh = gh_sessions[repo_id]
|
gh = gh_sessions[repo_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prev = prevs[repo_id] = prevs.get(repo_id) or gh.head(master_name)
|
prev = prevs[repo_id] = prevs.get(repo_id) or gh.head(master_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
raise UserError(f"Unable to resolve branch {master_name} of repository {repo_id.name} to a commit.")
|
raise UserError(f"Unable to resolve branch {master_name} of repository {repo_id.name} to a commit.") from e
|
||||||
|
|
||||||
# create the tmp branch to merge the PR into
|
|
||||||
tmp_branch = f'tmp.{master_name}'
|
|
||||||
try:
|
try:
|
||||||
gh.set_ref(tmp_branch, prev)
|
commits = gh.commits(bump.pr_id.number)
|
||||||
except Exception as err:
|
except Exception as e:
|
||||||
raise UserError(f"Unable to create branch {master_name} of repository {repo_id.name}: {err}.")
|
raise UserError(f"Unable to fetch commits of bump PR {bump.pr_id.display_name}.") from e
|
||||||
|
|
||||||
bump_heads[repo_id], _ = gh.rebase(bump.pr_id.number, tmp_branch)
|
bump_heads[repo_id] = repos[repo_id].rebase(prev, commits)[0]
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
deployed = {}
|
deployed = {}
|
||||||
# at this point we've got a bunch of tmp branches with merged release
|
# at this point we've got a bunch of tmp branches with merged release
|
||||||
@ -264,38 +265,39 @@ class FreezeWizard(models.Model):
|
|||||||
failure = None
|
failure = None
|
||||||
for rel in self.release_pr_ids:
|
for rel in self.release_pr_ids:
|
||||||
repo_id = rel.repository_id
|
repo_id = rel.repository_id
|
||||||
# helper API currently has no API to ensure we're just creating a
|
|
||||||
# new branch (as cheaply as possible) so do it by hand
|
|
||||||
status = None
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
status = gh_sessions[repo_id].create_ref(self.branch_name, rel_heads[repo_id])
|
|
||||||
deployed[rel.pr_id.id] = rel_heads[repo_id]
|
|
||||||
to_delete.append(repo_id)
|
|
||||||
|
|
||||||
if status != 201:
|
if repos[repo_id].push(
|
||||||
|
git.source_url(repo_id, 'github'),
|
||||||
|
f'{rel_heads[repo_id]}:refs/heads/{self.branch_name}',
|
||||||
|
).returncode:
|
||||||
failure = ('create', repo_id.name, self.branch_name)
|
failure = ('create', repo_id.name, self.branch_name)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
deployed[rel.pr_id.id] = rel_heads[repo_id]
|
||||||
|
to_delete.append(repo_id)
|
||||||
else: # all release deployments succeeded
|
else: # all release deployments succeeded
|
||||||
for bump in self.bump_pr_ids:
|
for bump in self.bump_pr_ids:
|
||||||
repo_id = bump.repository_id
|
repo_id = bump.repository_id
|
||||||
try:
|
if repos[repo_id].push(
|
||||||
gh_sessions[repo_id].fast_forward(master_name, bump_heads[repo_id])
|
git.source_url(repo_id, 'github'),
|
||||||
deployed[bump.pr_id.id] = bump_heads[repo_id]
|
f'{bump_heads[repo_id]}:refs/heads/{master_name}'
|
||||||
to_revert.append(repo_id)
|
).returncode:
|
||||||
except FastForwardError:
|
|
||||||
failure = ('fast-forward', repo_id.name, master_name)
|
failure = ('fast-forward', repo_id.name, master_name)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
deployed[bump.pr_id.id] = bump_heads[repo_id]
|
||||||
|
to_revert.append(repo_id)
|
||||||
|
|
||||||
if failure:
|
if failure:
|
||||||
addendums = []
|
addendums = []
|
||||||
# creating the branch failed, try to delete all previous branches
|
# creating the branch failed, try to delete all previous branches
|
||||||
failures = []
|
failures = []
|
||||||
for prev_id in to_revert:
|
for prev_id in to_revert:
|
||||||
revert = gh_sessions[prev_id]('PATCH', f'git/refs/heads/{master_name}', json={
|
if repos[prev_id].push(
|
||||||
'sha': prevs[prev_id],
|
'-f',
|
||||||
'force': True
|
git.source_url(prev_id, 'github'),
|
||||||
}, check=False)
|
f'{prevs[prev_id]}:refs/heads/{master_name}',
|
||||||
if not revert.ok:
|
).returncode:
|
||||||
failures.append(prev_id.name)
|
failures.append(prev_id.name)
|
||||||
if failures:
|
if failures:
|
||||||
addendums.append(
|
addendums.append(
|
||||||
@ -305,8 +307,10 @@ class FreezeWizard(models.Model):
|
|||||||
failures.clear()
|
failures.clear()
|
||||||
|
|
||||||
for prev_id in to_delete:
|
for prev_id in to_delete:
|
||||||
deletion = gh_sessions[prev_id]('DELETE', f'git/refs/heads/{self.branch_name}', check=False)
|
if repos[prev_id].push(
|
||||||
if not deletion.ok:
|
git.source_url(prev_id, 'github'),
|
||||||
|
f':refs/heads/{self.branch_name}'
|
||||||
|
).returncode:
|
||||||
failures.append(prev_id.name)
|
failures.append(prev_id.name)
|
||||||
if failures:
|
if failures:
|
||||||
addendums.append(
|
addendums.append(
|
||||||
@ -468,7 +472,7 @@ class OpenPRLabels(models.Model):
|
|||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
super().init()
|
super().init()
|
||||||
drop_view_if_exists(self.env.cr, "runbot_merge_freeze_labels");
|
drop_view_if_exists(self.env.cr, "runbot_merge_freeze_labels")
|
||||||
self.env.cr.execute("""
|
self.env.cr.execute("""
|
||||||
CREATE VIEW runbot_merge_freeze_labels AS (
|
CREATE VIEW runbot_merge_freeze_labels AS (
|
||||||
SELECT DISTINCT ON (label)
|
SELECT DISTINCT ON (label)
|
||||||
|
@ -5,7 +5,6 @@ source branches).
|
|||||||
When preparing a staging, we simply want to ensure branch-matched PRs
|
When preparing a staging, we simply want to ensure branch-matched PRs
|
||||||
are staged concurrently in all repos
|
are staged concurrently in all repos
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import xmlrpc.client
|
import xmlrpc.client
|
||||||
|
|
||||||
@ -1256,12 +1255,12 @@ def test_freeze_complete(env, project, repo_a, repo_b, repo_c, users, config):
|
|||||||
|
|
||||||
c_b = repo_b.commit('1.1')
|
c_b = repo_b.commit('1.1')
|
||||||
assert c_b.message.startswith('Release 1.1 (B)')
|
assert c_b.message.startswith('Release 1.1 (B)')
|
||||||
assert repo_b.read_tree(c_b) == {'f': '1', 'version': ''}
|
assert repo_b.read_tree(c_b) == {'f': '1', 'version': '1.1'}
|
||||||
assert c_b.parents[0] == master_head_b
|
assert c_b.parents[0] == master_head_b
|
||||||
|
|
||||||
c_c = repo_c.commit('1.1')
|
c_c = repo_c.commit('1.1')
|
||||||
assert c_c.message.startswith('Release 1.1 (C)')
|
assert c_c.message.startswith('Release 1.1 (C)')
|
||||||
assert repo_c.read_tree(c_c) == {'f': '2', 'version': ''}
|
assert repo_c.read_tree(c_c) == {'f': '2', 'version': '1.1'}
|
||||||
assert repo_c.commit(c_c.parents[0]).parents[0] == master_head_c
|
assert repo_c.commit(c_c.parents[0]).parents[0] == master_head_c
|
||||||
|
|
||||||
|
|
||||||
@ -1272,7 +1271,7 @@ def setup_mess(repo_a, repo_b, repo_c):
|
|||||||
[root, _] = r.make_commits(
|
[root, _] = r.make_commits(
|
||||||
None,
|
None,
|
||||||
Commit('base', tree={'version': '', 'f': '0'}),
|
Commit('base', tree={'version': '', 'f': '0'}),
|
||||||
Commit('release 1.0', tree={'version': '1.0'} if r is repo_a else None),
|
Commit('release 1.0', tree={'version': '1.0'}),
|
||||||
ref='heads/1.0'
|
ref='heads/1.0'
|
||||||
)
|
)
|
||||||
master_heads.extend(r.make_commits(root, Commit('other', tree={'f': '1'}), ref='heads/master'))
|
master_heads.extend(r.make_commits(root, Commit('other', tree={'f': '1'}), ref='heads/master'))
|
||||||
@ -1294,7 +1293,7 @@ def setup_mess(repo_a, repo_b, repo_c):
|
|||||||
with repo_b:
|
with repo_b:
|
||||||
repo_b.make_commits(
|
repo_b.make_commits(
|
||||||
master_heads[1],
|
master_heads[1],
|
||||||
Commit('Release 1.1 (B)', tree=None),
|
Commit('Release 1.1 (B)', tree={'version': '1.1'}),
|
||||||
ref='heads/release-1.1'
|
ref='heads/release-1.1'
|
||||||
)
|
)
|
||||||
pr_rel_b = repo_b.make_pr(target='master', head='release-1.1')
|
pr_rel_b = repo_b.make_pr(target='master', head='release-1.1')
|
||||||
@ -1303,7 +1302,7 @@ def setup_mess(repo_a, repo_b, repo_c):
|
|||||||
pr_other = repo_c.make_pr(target='master', head='whocares')
|
pr_other = repo_c.make_pr(target='master', head='whocares')
|
||||||
repo_c.make_commits(
|
repo_c.make_commits(
|
||||||
master_heads[2],
|
master_heads[2],
|
||||||
Commit('Release 1.1 (C)', tree=None),
|
Commit('Release 1.1 (C)', tree={'version': '1.1'}),
|
||||||
ref='heads/release-1.1'
|
ref='heads/release-1.1'
|
||||||
)
|
)
|
||||||
pr_rel_c = repo_c.make_pr(target='master', head='release-1.1')
|
pr_rel_c = repo_c.make_pr(target='master', head='release-1.1')
|
||||||
@ -1431,7 +1430,8 @@ def test_freeze_conflict(env, project, repo_a, repo_b, repo_c, users, config):
|
|||||||
|
|
||||||
# create conflicting branch
|
# create conflicting branch
|
||||||
with repo_c:
|
with repo_c:
|
||||||
repo_c.make_ref('heads/1.1', heads[2])
|
[c] = repo_c.make_commits(heads[2], Commit("exists", tree={'version': ''}))
|
||||||
|
repo_c.make_ref('heads/1.1', c)
|
||||||
|
|
||||||
# actually perform the freeze
|
# actually perform the freeze
|
||||||
with pytest.raises(xmlrpc.client.Fault) as e:
|
with pytest.raises(xmlrpc.client.Fault) as e:
|
||||||
|
Loading…
Reference in New Issue
Block a user