[IMP] runbot_merge: add related PRs to top comment

Discussing #238 with @odony, the main concern was the difficulty of
understanding if things merged in one repo were related to things
merged in an other repo: currently, knowing this requires going to the
merged PR, getting its label, and checking the PRs with the same HEAD
in the other repository to see if there's a correlation (e.g. PRs
merged around the same time).

The current structure of the mergebot makes it reasonably easy to add
the other PRs of the batch in the pseudo-headers, such that we get
links to all "related" PRs in the head commit (and links back from the
commits which is probably less useful but...)

Fixes #238
This commit is contained in:
Xavier Morel 2019-11-22 09:21:40 +01:00
parent 1b5a05e40c
commit 629e00a117
2 changed files with 31 additions and 11 deletions

View File

@ -1074,7 +1074,7 @@ class PullRequests(models.Model):
"""
return Message.from_message(message)
def _build_merge_message(self, message):
def _build_merge_message(self, message, related_prs=()):
# handle co-authored commits (https://help.github.com/articles/creating-a-commit-with-multiple-authors/)
m = self._parse_commit_message(message)
pattern = r'( |{repository})#{pr.number}\b'.format(
@ -1084,12 +1084,15 @@ class PullRequests(models.Model):
if not re.search(pattern, m.body):
m.body += '\n\ncloses {pr.display_name}'.format(pr=self)
for r in related_prs:
m.headers.add('Related', r.display_name)
if self.reviewed_by:
m.headers.add('signed-off-by', self.reviewed_by.formatted_email)
return str(m)
def _stage(self, gh, target):
def _stage(self, gh, target, related_prs=()):
# nb: pr_commits is oldest to newest so pr.head is pr_commits[-1]
_, prdict = gh.pr(self.number)
commits = prdict['commits']
@ -1110,25 +1113,26 @@ class PullRequests(models.Model):
# NOTE: lost merge v merge/copy distinction (head being
# a merge commit reused instead of being re-merged)
return method, getattr(self, '_stage_' + method.replace('-', '_'))(gh, target, pr_commits)
return method, getattr(self, '_stage_' + method.replace('-', '_'))(
gh, target, pr_commits, related_prs=related_prs)
def _stage_rebase_ff(self, gh, target, commits):
def _stage_rebase_ff(self, gh, target, commits, related_prs=()):
# updates head commit with PR number (if necessary) then rebases
# on top of target
msg = self._build_merge_message(commits[-1]['commit']['message'])
msg = self._build_merge_message(commits[-1]['commit']['message'], related_prs=related_prs)
commits[-1]['commit']['message'] = msg
head, mapping = gh.rebase(self.number, target, commits=commits)
self.commits_map = json.dumps({**mapping, '': head})
return head
def _stage_rebase_merge(self, gh, target, commits):
msg = self._build_merge_message(self.message)
def _stage_rebase_merge(self, gh, target, commits, related_prs=()):
msg = self._build_merge_message(self.message, related_prs=related_prs)
h, mapping = gh.rebase(self.number, target, reset=True, commits=commits)
merge_head = gh.merge(h, target, msg)['sha']
self.commits_map = json.dumps({**mapping, '': merge_head})
return merge_head
def _stage_merge(self, gh, target, commits):
def _stage_merge(self, gh, target, commits, related_prs=()):
pr_head = commits[-1] # oldest to newest
base_commit = None
head_parents = {p['sha'] for p in pr_head['parents']}
@ -1148,7 +1152,7 @@ class PullRequests(models.Model):
original_head = gh.head(target)
merge_tree = gh.merge(pr_head['sha'], target, 'temp merge')['tree']['sha']
new_parents = [original_head] + list(head_parents - {base_commit})
msg = self._build_merge_message(pr_head['commit']['message'])
msg = self._build_merge_message(pr_head['commit']['message'], related_prs=related_prs)
copy = gh('post', 'git/commits', json={
'message': msg,
'tree': merge_tree,
@ -1735,7 +1739,7 @@ class Batch(models.Model):
target = 'tmp.{}'.format(pr.target.name)
original_head = gh.head(target)
try:
method, new_heads[pr] = pr._stage(gh, target)
method, new_heads[pr] = pr._stage(gh, target, related_prs=(prs - pr))
_logger.info(
"Staged pr %s:%s to %s by %s: %s -> %s",
pr.repository.name, pr.number,

View File

@ -126,6 +126,17 @@ def test_stage_match(env, project, repo_a, repo_b, config):
assert pr_a.staging_id == pr_b.staging_id, \
"branch-matched PRs should be part of the same staging"
for repo in [repo_a, repo_b]:
with repo:
repo.post_status('staging.master', 'success', 'legal/cla')
repo.post_status('staging.master', 'success', 'ci/runbot')
env.run_crons()
assert pr_a.state == 'merged'
assert pr_b.state == 'merged'
assert 'Related: {}#{}'.format(repo_b.name, pr_b.number) in repo_a.commit('master').message
assert 'Related: {}#{}'.format(repo_a.name, pr_a.number) in repo_b.commit('master').message
def test_unmatch_patch(env, project, repo_a, repo_b, config):
""" When editing files via the UI for a project you don't have write
access to, a branch called patch-XXX is automatically created in your
@ -263,7 +274,12 @@ def test_merge_fail(env, project, repo_a, repo_b, users, config):
for c in repo_a.log('heads/staging.master')
] == [
re_matches('^force rebuild'),
'commit_do-b-thing_00\n\ncloses %s#2\n\nSigned-off-by: %s' % (repo_a.name, reviewer),
"""commit_do-b-thing_00
closes %s#%d
Related: %s#%d
Signed-off-by: %s""" % (repo_a.name, pr2a.number, repo_b.name, pr2b.number, reviewer),
'initial'
], "dummy commit + squash-merged PR commit + root commit"