############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, you can obtain one at https://mozilla.org/MPL/2.0/. # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. ############################################################################ import os import re import gitlab # Helper functions and variables def added_lines(target_branch, paths): import subprocess subprocess.check_output(['/usr/bin/git', 'fetch', '--depth', '1', 'origin', target_branch]) diff = subprocess.check_output(['/usr/bin/git', 'diff', 'FETCH_HEAD..', '--'] + paths) added_lines = [] for line in diff.splitlines(): if line.startswith(b'+') and not line.startswith(b'+++'): added_lines.append(line) return added_lines def lines_containing(lines, string): return [l for l in lines if bytes(string, 'utf-8') in l] issue_or_mr_id_regex = re.compile(br'\[(GL [#!]|RT #)[0-9]+\]') release_notes_regex = re.compile(r'doc/(arm|notes)/notes-.*\.(rst|xml)') modified_files = danger.git.modified_files mr_labels = danger.gitlab.mr.labels target_branch = danger.gitlab.mr.target_branch is_backport = "Backport" in mr_labels or "Backport::Partial" in mr_labels is_full_backport = is_backport and "Backport::Partial" not in mr_labels gl = gitlab.Gitlab( url=f"https://{os.environ['CI_SERVER_HOST']}", private_token=os.environ["BIND_TEAM_API_TOKEN"], ) proj = gl.projects.get(os.environ["CI_PROJECT_ID"]) mr = proj.mergerequests.get(os.environ["CI_MERGE_REQUEST_IID"]) ############################################################################### # COMMIT MESSAGES ############################################################################### # # - FAIL if any of the following is true for any commit on the MR branch: # # * The subject line starts with "fixup!" or "Apply suggestion". # # * The subject line contains a trailing dot. # # * There is no empty line between the subject line and the log message. # # - WARN if any of the following is true for any commit on the MR branch: # # * The length of the subject line for a non-merge commit exceeds 72 # characters. # # * There is no log message present (i.e. commit only has a subject) and # the subject line does not contain any of the following strings: # "fixup!", " CHANGES ", " release note". # # * Any line of the log message is longer than 72 characters. This rule is # not evaluated for: # # - lines starting with four spaces, which allows long lines to be # included in the commit log message by prefixing them with four # spaces (useful for pasting compiler warnings, static analyzer # messages, log lines, etc.), # # - lines which contain references (i.e. those starting with "[1]", # "[2]", etc.) which allows e.g. long URLs to be included in the # commit log message. fixup_error_logged = False for commit in danger.git.commits: message_lines = commit.message.splitlines() subject = message_lines[0] if (not fixup_error_logged and (subject.startswith('fixup!') or subject.startswith('Apply suggestion'))): fail('Fixup commits are still present in this merge request. ' 'Please squash them before merging.') fixup_error_logged = True if len(subject) > 72 and not subject.startswith('Merge branch '): warn( f'Subject line for commit {commit.sha} is too long: ' f'```{subject}``` ({len(subject)} > 72 characters).' ) if subject[-1] == '.': fail(f'Trailing dot found in the subject of commit {commit.sha}.') if len(message_lines) > 1 and message_lines[1]: fail(f'No empty line after subject for commit {commit.sha}.') if (len(message_lines) < 3 and 'fixup! ' not in subject and ' CHANGES ' not in subject and ' release note' not in subject): warn(f'Please write a log message for commit {commit.sha}.') for line in message_lines[2:]: if (len(line) > 72 and not line.startswith(' ') and not re.match(r'\[[0-9]+\]', line)): warn( f'Line too long in log message for commit {commit.sha}: ' f'```{line}``` ({len(line)} > 72 characters).' ) ############################################################################### # BACKPORT & VERSION LABELS ############################################################################### # # FAIL if any of the following is true for the merge request: # # * The MR is marked as Backport and the number of version labels set is # different than 1. (For backports, the version label is used for indicating # its target branch. This is a rather ugly attempt to address a UI # deficiency - the target branch for each MR is not visible on milestone # dashboards.) # # * The MR is not marked as "Backport" nor any version label is set. (If the # merge request is not a backport, version labels are used for indicating # backporting preferences.) # # * The Backport MR doesn't have target branch in the merge request title. # # * The Backport MR doesn't link to the original MR is its description. # # * The original MR linked to from Backport MR hasn't been merged. BACKPORT_OF_RE = re.compile( r"Backport\s+of.*(merge_requests/|!)([0-9]+)", flags=re.IGNORECASE ) backport_desc = BACKPORT_OF_RE.search(danger.gitlab.mr.description) version_labels = [l for l in mr_labels if l.startswith("v9.")] if is_backport: if len(version_labels) != 1: fail( "This MR was marked as *Backport*. " "Please also set exactly one version label (*v9.x*)." ) else: mr_title_version = f"[{version_labels[0].replace('.', '_').replace(' (EoL)', '')}]" if mr_title_version not in danger.gitlab.mr.title: fail( "Backport MRs must have their target branch in the " f"title. Please put `{mr_title_version}` in the MR title." ) if backport_desc is None: fail( "Backport MRs must link to the original MR. Please put " "`Backport of MR !XXXX` in the MR description." ) else: # backport MR is linked to original MR original_mr_id = backport_desc.groups()[1] original_mr = proj.mergerequests.get(original_mr_id) if original_mr.state != "merged": fail( f"Original MR !{original_mr_id} has not been merged. " "Please re-run `danger` check once it's merged." ) else: # check for commit IDs once original MR is merged original_mr_commits = list(original_mr.commits(all=True)) backport_mr_commits = list(mr.commits(all=True)) for orig_commit in original_mr_commits: for backport_commit in backport_mr_commits: if orig_commit.id in backport_commit.message: break else: msg = ( f"Commit {orig_commit.id} from original MR !{original_mr_id} " "is not referenced in any of the backport commits." ) if not is_full_backport: message(msg) else: msg += ( " Please use `-x` when cherry-picking to include " "the full original commit ID. Alternately, use the " "`Backport::Partial` label if not all original " "commits are meant to be backported." ) fail(msg) if not is_backport and not version_labels: fail( "If this merge request is a backport, set the *Backport* label and " "a single version label (*v9.x*) indicating the target branch. " "If not, set version labels for all targeted backport branches." ) ############################################################################### # OTHER LABELS ############################################################################### # # WARN if any of the following is true for the merge request: # # * The "Review" label is not set. (It may be intentional, but rarely is.) # # * The "Review" label is set, but the "LGTM" label is not set. (This aims to # remind developers about the need to set the latter on merge requests which # passed review.) if 'Review' not in mr_labels: warn('This merge request does not have the *Review* label set. ' 'Please set it if you would like the merge request to be reviewed.') elif 'LGTM (Merge OK)' not in mr_labels: warn('This merge request is currently in review. ' 'It should not be merged until it is marked with the *LGTM* label.') ############################################################################### # PAIRWISE TESTING ############################################################################### # # FAIL if the merge request adds any new ./configure switch without an # associated annotation used for pairwise testing. configure_added_lines = added_lines(target_branch, ['configure.ac']) switches_added = (lines_containing(configure_added_lines, 'AC_ARG_ENABLE') + lines_containing(configure_added_lines, 'AC_ARG_WITH')) annotations_added = lines_containing(configure_added_lines, '# [pairwise: ') if len(switches_added) > len(annotations_added): fail('This merge request adds at least one new `./configure` switch that ' 'is not annotated for pairwise testing purposes.')