mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[CHG] runbot_merge: switch staging from github API to local
It has been a consideration for a while, but the pain of subtly interacting with git via the ignominous CLI kept it back. Then ~~the fire nation attacked~~ github got more and more tight-fisted (and in some ways less reliable) with their API. Staging pretty much just interacts with the git database, so it's both a facultative github operator (it can just interact with git directly) and a big consumer of API requests (because the git database endpoints are very low level so it takes quite a bit of work to do anything especially when high-level operations like rebase have to be replicated by hand). Nota: it might be a good idea to use pygit2 instead of the CLI, the library seems to be typed and might allow doing the processing we need on a bare repository directly. Which we can do via the plumbing commands anyway, but still. Fixes #247
This commit is contained in:
parent
5703840b3b
commit
747162d6e1
@ -349,8 +349,7 @@ def stage_batch(env: api.Environment, prs: PullRequests, staging: StagingState)
|
||||
new_heads: Dict[PullRequests, str] = {}
|
||||
pr_fields = env['runbot_merge.pull_requests']._fields
|
||||
for pr in prs:
|
||||
gh = staging[pr.repository].gh
|
||||
|
||||
info = staging[pr.repository]
|
||||
_logger.info(
|
||||
"Staging pr %s for target %s; method=%s",
|
||||
pr.display_name, pr.target.name,
|
||||
@ -358,10 +357,10 @@ def stage_batch(env: api.Environment, prs: PullRequests, staging: StagingState)
|
||||
)
|
||||
|
||||
target = 'tmp.{}'.format(pr.target.name)
|
||||
original_head = gh.head(target)
|
||||
original_head = info.gh.head(target)
|
||||
try:
|
||||
try:
|
||||
method, new_heads[pr] = stage(pr, gh, target, related_prs=(prs - pr))
|
||||
method, new_heads[pr] = stage(pr, info, target, related_prs=(prs - pr))
|
||||
_logger.info(
|
||||
"Staged pr %s to %s by %s: %s -> %s",
|
||||
pr.display_name, pr.target.name, method,
|
||||
@ -370,7 +369,7 @@ def stage_batch(env: api.Environment, prs: PullRequests, staging: StagingState)
|
||||
except Exception:
|
||||
# reset the head which failed, as rebase() may have partially
|
||||
# updated it (despite later steps failing)
|
||||
gh.set_ref(target, original_head)
|
||||
info.gh.set_ref(target, original_head)
|
||||
# then reset every previous update
|
||||
for to_revert in new_heads.keys():
|
||||
it = staging[to_revert.repository]
|
||||
@ -419,9 +418,9 @@ def format_for_difflib(items: Iterator[Tuple[str, object]]) -> Iterator[str]:
|
||||
|
||||
|
||||
Method = Literal['merge', 'rebase-merge', 'rebase-ff', 'squash']
|
||||
def stage(pr: PullRequests, gh: github.GH, target: str, related_prs: PullRequests) -> Tuple[Method, str]:
|
||||
def stage(pr: PullRequests, info: StagingSlice, target: str, related_prs: PullRequests) -> Tuple[Method, str]:
|
||||
# nb: pr_commits is oldest to newest so pr.head is pr_commits[-1]
|
||||
_, prdict = gh.pr(pr.number)
|
||||
_, prdict = info.gh.pr(pr.number)
|
||||
commits = prdict['commits']
|
||||
method: Method = pr.merge_method or ('rebase-ff' if commits == 1 else None)
|
||||
if commits > 50 and method.startswith('rebase'):
|
||||
@ -431,7 +430,7 @@ def stage(pr: PullRequests, gh: github.GH, target: str, related_prs: PullRequest
|
||||
pr, "Merging PRs of 250 or more commits is not supported "
|
||||
"(https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request)"
|
||||
)
|
||||
pr_commits = gh.commits(pr.number)
|
||||
pr_commits = info.gh.commits(pr.number)
|
||||
for c in pr_commits:
|
||||
if not (c['commit']['author']['email'] and c['commit']['committer']['email']):
|
||||
raise exceptions.Unmergeable(
|
||||
@ -475,7 +474,7 @@ def stage(pr: PullRequests, gh: github.GH, target: str, related_prs: PullRequest
|
||||
|
||||
if pr.reviewed_by and pr.reviewed_by.name == pr.reviewed_by.github_login:
|
||||
# XXX: find other trigger(s) to sync github name?
|
||||
gh_name = gh.user(pr.reviewed_by.github_login)['name']
|
||||
gh_name = info.gh.user(pr.reviewed_by.github_login)['name']
|
||||
if gh_name:
|
||||
pr.reviewed_by.name = gh_name
|
||||
|
||||
@ -488,9 +487,9 @@ def stage(pr: PullRequests, gh: github.GH, target: str, related_prs: PullRequest
|
||||
fn = stage_rebase_ff
|
||||
case 'squash':
|
||||
fn = stage_squash
|
||||
return method, fn(pr, gh, target, pr_commits, related_prs=related_prs)
|
||||
return method, fn(pr, info, target, pr_commits, related_prs=related_prs)
|
||||
|
||||
def stage_squash(pr: PullRequests, gh: github.GH, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
def stage_squash(pr: PullRequests, info: StagingSlice, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
msg = pr._build_merge_message(pr, related_prs=related_prs)
|
||||
authorship = {}
|
||||
|
||||
@ -516,15 +515,15 @@ def stage_squash(pr: PullRequests, gh: github.GH, target: str, commits: List[git
|
||||
authorship['committer'] = {'name': name, 'email': email}
|
||||
# should committers also be added to co-authors?
|
||||
|
||||
original_head = gh.head(target)
|
||||
merge_tree = gh.merge(pr.head, target, 'temp merge')['tree']['sha']
|
||||
head = gh('post', 'git/commits', json={
|
||||
original_head = info.gh.head(target)
|
||||
merge_tree = info.gh.merge(pr.head, target, 'temp merge')['tree']['sha']
|
||||
head = info.gh('post', 'git/commits', json={
|
||||
**authorship,
|
||||
'message': str(msg),
|
||||
'tree': merge_tree,
|
||||
'parents': [original_head],
|
||||
}).json()['sha']
|
||||
gh.set_ref(target, head)
|
||||
info.gh.set_ref(target, head)
|
||||
|
||||
commits_map = {c['sha']: head for c in commits}
|
||||
commits_map[''] = head
|
||||
@ -532,25 +531,25 @@ def stage_squash(pr: PullRequests, gh: github.GH, target: str, commits: List[git
|
||||
|
||||
return head
|
||||
|
||||
def stage_rebase_ff(pr: PullRequests, gh: github.GH, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
def stage_rebase_ff(pr: PullRequests, info: StagingSlice, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
# updates head commit with PR number (if necessary) then rebases
|
||||
# on top of target
|
||||
msg = pr._build_merge_message(commits[-1]['commit']['message'], related_prs=related_prs)
|
||||
commits[-1]['commit']['message'] = str(msg)
|
||||
add_self_references(pr, commits[:-1])
|
||||
head, mapping = gh.rebase(pr.number, target, commits=commits)
|
||||
head, mapping = info.gh.rebase(pr.number, target, commits=commits)
|
||||
pr.commits_map = json.dumps({**mapping, '': head})
|
||||
return head
|
||||
|
||||
def stage_rebase_merge(pr: PullRequests, gh: github.GH, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str :
|
||||
def stage_rebase_merge(pr: PullRequests, info: StagingSlice, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str :
|
||||
add_self_references(pr, commits)
|
||||
h, mapping = gh.rebase(pr.number, target, reset=True, commits=commits)
|
||||
h, mapping = info.gh.rebase(pr.number, target, reset=True, commits=commits)
|
||||
msg = pr._build_merge_message(pr, related_prs=related_prs)
|
||||
merge_head = gh.merge(h, target, str(msg))['sha']
|
||||
merge_head = info.gh.merge(h, target, str(msg))['sha']
|
||||
pr.commits_map = json.dumps({**mapping, '': merge_head})
|
||||
return merge_head
|
||||
|
||||
def stage_merge(pr: PullRequests, gh: github.GH, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
def stage_merge(pr: PullRequests, info: StagingSlice, target: str, commits: List[github.PrCommit], related_prs: PullRequests) -> str:
|
||||
pr_head = commits[-1] # oldest to newest
|
||||
base_commit = None
|
||||
head_parents = {p['sha'] for p in pr_head['parents']}
|
||||
@ -573,18 +572,18 @@ def stage_merge(pr: PullRequests, gh: github.GH, target: str, commits: List[gith
|
||||
if base_commit:
|
||||
# replicate pr_head with base_commit replaced by
|
||||
# the current head
|
||||
original_head = gh.head(target)
|
||||
merge_tree = gh.merge(pr_head['sha'], target, 'temp merge')['tree']['sha']
|
||||
original_head = info.gh.head(target)
|
||||
merge_tree = info.gh.merge(pr_head['sha'], target, 'temp merge')['tree']['sha']
|
||||
new_parents = [original_head] + list(head_parents - {base_commit})
|
||||
msg = pr._build_merge_message(pr_head['commit']['message'], related_prs=related_prs)
|
||||
copy = gh('post', 'git/commits', json={
|
||||
copy = info.gh('post', 'git/commits', json={
|
||||
'message': str(msg),
|
||||
'tree': merge_tree,
|
||||
'author': pr_head['commit']['author'],
|
||||
'committer': pr_head['commit']['committer'],
|
||||
'parents': new_parents,
|
||||
}).json()
|
||||
gh.set_ref(target, copy['sha'])
|
||||
info.gh.set_ref(target, copy['sha'])
|
||||
# merge commit *and old PR head* map to the pr head replica
|
||||
commits_map[''] = commits_map[pr_head['sha']] = copy['sha']
|
||||
pr.commits_map = json.dumps(commits_map)
|
||||
@ -592,7 +591,7 @@ def stage_merge(pr: PullRequests, gh: github.GH, target: str, commits: List[gith
|
||||
else:
|
||||
# otherwise do a regular merge
|
||||
msg = pr._build_merge_message(pr)
|
||||
merge_head = gh.merge(pr.head, target, str(msg))['sha']
|
||||
merge_head = info.gh.merge(pr.head, target, str(msg))['sha']
|
||||
# and the merge commit is the normal merge head
|
||||
commits_map[''] = merge_head
|
||||
pr.commits_map = json.dumps(commits_map)
|
||||
|
Loading…
Reference in New Issue
Block a user