mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00

- fix incorrect view specs (the action id comes first) - add a wizard form and hook it into the PR, completely forgot to do that - usability improvements: filter branches to be in the same project as the PR being backported, and older than the current PR's branch The latter is a somewhat incomplete condition: ideally we'd want to only allow selecting branches preceding the target of the *source* of the PR being backported, that way we don't risk errors when backporting forward-ports (the condition should be checked in the final action but still). Also we're only filtering by sequence, so we're missing the name part of the ordering, hence if multiple branches have the same sequence we may not allow selecting some of the preceding branches.
146 lines
5.0 KiB
Python
146 lines
5.0 KiB
Python
import logging
|
|
import re
|
|
import secrets
|
|
|
|
import requests
|
|
|
|
from odoo import models, fields
|
|
from odoo.exceptions import UserError
|
|
|
|
from ..batch import Batch
|
|
from ..project import Project
|
|
from ..pull_requests import Repository
|
|
from ... import git
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PullRequest(models.Model):
|
|
_inherit = 'runbot_merge.pull_requests'
|
|
|
|
id: int
|
|
display_name: str
|
|
project: Project
|
|
repository: Repository
|
|
batch_id: Batch
|
|
|
|
def backport(self) -> dict:
|
|
if len(self) != 1:
|
|
raise UserError(f"Backporting works one PR at a time, got {len(self)}")
|
|
|
|
if len(self.batch_id.prs) > 1:
|
|
raise UserError("Automatic backport of multi-pr batches is not currently supported")
|
|
|
|
if not self.project.fp_github_token:
|
|
raise UserError(f"Can not backport {self.display_name}: no token on project {self.project.display_name}")
|
|
|
|
if not self.repository.fp_remote_target:
|
|
raise UserError(f"Can not backport {self.display_name}: no remote on {self.project.display_name}")
|
|
|
|
w = self.env['runbot_merge.pull_requests.backport'].create({
|
|
'pr_id': self.id,
|
|
})
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': f"Backport of {self.display_name}",
|
|
'views': [(False, 'form')],
|
|
'target': 'new',
|
|
'res_model': w._name,
|
|
'res_id': w.id,
|
|
}
|
|
|
|
class PullRequestBackport(models.TransientModel):
|
|
_name = 'runbot_merge.pull_requests.backport'
|
|
_description = "PR backport wizard"
|
|
_rec_name = 'pr_id'
|
|
|
|
pr_id = fields.Many2one('runbot_merge.pull_requests', required=True)
|
|
project_id = fields.Many2one(related='pr_id.repository.project_id')
|
|
source_seq = fields.Integer(related='pr_id.target.sequence')
|
|
target = fields.Many2one(
|
|
'runbot_merge.branch',
|
|
domain="[('project_id', '=', project_id), ('sequence', '>', source_seq)]",
|
|
)
|
|
|
|
def action_apply(self) -> dict:
|
|
if not self.target:
|
|
raise UserError("A backport needs a backport target")
|
|
|
|
project = self.pr_id.project
|
|
branches = project._forward_port_ordered().ids
|
|
source = self.pr_id.source_id or self.pr_id
|
|
source_idx = branches.index(source.target.id)
|
|
if branches.index(self.target.id) >= source_idx:
|
|
raise UserError(
|
|
"The backport branch needs to be before the source's branch "
|
|
f"(got {self.target.name!r} and {source.target.name!r})"
|
|
)
|
|
|
|
_logger.info(
|
|
"backporting %s (on %s) to %s",
|
|
self.pr_id.display_name,
|
|
self.pr_id.target.name,
|
|
self.target.name,
|
|
)
|
|
|
|
bp_branch = "%s-%s-%s-bp" % (
|
|
self.target.name,
|
|
self.pr_id.refname,
|
|
secrets.token_urlsafe(3),
|
|
)
|
|
repo_id = self.pr_id.repository
|
|
repo = git.get_local(repo_id)
|
|
|
|
old_map = self.pr_id.commits_map
|
|
self.pr_id.commits_map = "{}"
|
|
conflict, head = self.pr_id._create_port_branch(repo, self.target, forward=False)
|
|
self.pr_id.commits_map = old_map
|
|
|
|
if conflict:
|
|
feedback = "\n".join(filter(None, conflict[1:3]))
|
|
raise UserError(f"backport conflict:\n\n{feedback}")
|
|
repo.push(git.fw_url(repo_id), f"{head}:refs/heads/{bp_branch}")
|
|
|
|
self.env.cr.execute('LOCK runbot_merge_pull_requests IN SHARE MODE')
|
|
|
|
owner, _repo = repo_id.fp_remote_target.split('/', 1)
|
|
message = source.message + f"\n\nBackport of {self.pr_id.display_name}"
|
|
title, body = re.fullmatch(r'(?P<title>[^\n]+)\n*(?P<body>.*)', message, flags=re.DOTALL).groups()
|
|
|
|
r = requests.post(
|
|
f'https://api.github.com/repos/{repo_id.name}/pulls',
|
|
headers={'Authorization': f'token {project.fp_github_token}'},
|
|
json={
|
|
'base': self.target.name,
|
|
'head': f'{owner}:{bp_branch}',
|
|
'title': '[Backport]' + ('' if title[0] == '[' else ' ') + title,
|
|
'body': body
|
|
}
|
|
)
|
|
if not r.ok:
|
|
raise UserError(f"Backport PR creation failure: {r.text}")
|
|
|
|
backport = self.env['runbot_merge.pull_requests']._from_gh(r.json())
|
|
_logger.info("Created backport %s for %s", backport.display_name, self.pr_id.display_name)
|
|
|
|
backport.write({
|
|
'merge_method': self.pr_id.merge_method,
|
|
# the backport's own forwardport should stop right before the
|
|
# original PR by default
|
|
'limit_id': branches[source_idx - 1],
|
|
})
|
|
self.env['runbot_merge.pull_requests.tagging'].create({
|
|
'repository': repo_id.id,
|
|
'pull_request': backport.number,
|
|
'tags_add': ['backport'],
|
|
})
|
|
# scheduling fp followup probably doesn't make sense since we don't copy the fw_policy...
|
|
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': "new backport",
|
|
'views': [(False, 'form')],
|
|
'res_model': backport._name,
|
|
'res_id': backport.id,
|
|
}
|