2024-10-22 15:53:45 +07:00
|
|
|
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}",
|
2024-11-18 15:48:48 +07:00
|
|
|
'views': [(False, 'form')],
|
2024-10-22 15:53:45 +07:00
|
|
|
'target': 'new',
|
|
|
|
'res_model': w._name,
|
|
|
|
'res_id': w.id,
|
|
|
|
}
|
|
|
|
|
|
|
|
class PullRequestBackport(models.TransientModel):
|
|
|
|
_name = 'runbot_merge.pull_requests.backport'
|
2024-10-22 20:08:00 +07:00
|
|
|
_description = "PR backport wizard"
|
2024-10-22 15:53:45 +07:00
|
|
|
_rec_name = 'pr_id'
|
|
|
|
|
|
|
|
pr_id = fields.Many2one('runbot_merge.pull_requests', required=True)
|
2024-11-18 15:48:48 +07:00
|
|
|
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)]",
|
|
|
|
)
|
2024-10-22 15:53:45 +07:00
|
|
|
|
|
|
|
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",
|
2024-11-18 15:48:48 +07:00
|
|
|
'views': [(False, 'form')],
|
2024-10-22 15:53:45 +07:00
|
|
|
'res_model': backport._name,
|
|
|
|
'res_id': backport.id,
|
|
|
|
}
|