diff --git a/runbot_merge/models/pull_requests.py b/runbot_merge/models/pull_requests.py index 58a7bdcd..60105137 100644 --- a/runbot_merge/models/pull_requests.py +++ b/runbot_merge/models/pull_requests.py @@ -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, diff --git a/runbot_merge/tests/test_multirepo.py b/runbot_merge/tests/test_multirepo.py index c9aa2014..4c9ebe81 100644 --- a/runbot_merge/tests/test_multirepo.py +++ b/runbot_merge/tests/test_multirepo.py @@ -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"