1############################################################################ 2# Copyright (C) Internet Systems Consortium, Inc. ("ISC") 3# 4# This Source Code Form is subject to the terms of the Mozilla Public 5# License, v. 2.0. If a copy of the MPL was not distributed with this 6# file, you can obtain one at https://mozilla.org/MPL/2.0/. 7# 8# See the COPYRIGHT file distributed with this work for additional 9# information regarding copyright ownership. 10############################################################################ 11 12import re 13 14# Helper functions and variables 15 16def added_lines(target_branch, paths): 17 import subprocess 18 subprocess.check_output(['/usr/bin/git', 'fetch', '--depth', '1', 'origin', 19 target_branch]) 20 diff = subprocess.check_output(['/usr/bin/git', 'diff', 'FETCH_HEAD..', 21 '--'] + paths) 22 added_lines = [] 23 for line in diff.splitlines(): 24 if line.startswith(b'+') and not line.startswith(b'+++'): 25 added_lines.append(line) 26 return added_lines 27 28def lines_containing(lines, string): 29 return [l for l in lines if bytes(string, 'utf-8') in l] 30 31changes_issue_or_mr_id_regex = re.compile(br'\[(GL [#!]|RT #)[0-9]+\]') 32relnotes_issue_or_mr_id_regex = re.compile(br':gl:`[#!][0-9]+`') 33release_notes_regex = re.compile(r'doc/(arm|notes)/notes-.*\.(rst|xml)') 34 35modified_files = danger.git.modified_files 36mr_labels = danger.gitlab.mr.labels 37target_branch = danger.gitlab.mr.target_branch 38 39############################################################################### 40# COMMIT MESSAGES 41############################################################################### 42# 43# - FAIL if any of the following is true for any commit on the MR branch: 44# 45# * The subject line starts with "fixup!" or "Apply suggestion". 46# 47# * The subject line contains a trailing dot. 48# 49# * There is no empty line between the subject line and the log message. 50# 51# - WARN if any of the following is true for any commit on the MR branch: 52# 53# * The length of the subject line for a non-merge commit exceeds 72 54# characters. 55# 56# * There is no log message present (i.e. commit only has a subject) and 57# the subject line does not contain any of the following strings: 58# "fixup!", " CHANGES ", " release note". 59# 60# * Any line of the log message is longer than 72 characters. This rule is 61# not evaluated for: 62# 63# - lines starting with four spaces, which allows long lines to be 64# included in the commit log message by prefixing them with four 65# spaces (useful for pasting compiler warnings, static analyzer 66# messages, log lines, etc.), 67# 68# - lines which contain references (i.e. those starting with "[1]", 69# "[2]", etc.) which allows e.g. long URLs to be included in the 70# commit log message. 71 72fixup_error_logged = False 73for commit in danger.git.commits: 74 message_lines = commit.message.splitlines() 75 subject = message_lines[0] 76 if (not fixup_error_logged and 77 (subject.startswith('fixup!') or 78 subject.startswith('Apply suggestion'))): 79 fail('Fixup commits are still present in this merge request. ' 80 'Please squash them before merging.') 81 fixup_error_logged = True 82 if len(subject) > 72 and not subject.startswith('Merge branch '): 83 warn( 84 f'Subject line for commit {commit.sha} is too long: ' 85 f'```{subject}``` ({len(subject)} > 72 characters).' 86 ) 87 if subject[-1] == '.': 88 fail(f'Trailing dot found in the subject of commit {commit.sha}.') 89 if len(message_lines) > 1 and message_lines[1]: 90 fail(f'No empty line after subject for commit {commit.sha}.') 91 if (len(message_lines) < 3 and 92 'fixup! ' not in subject and 93 ' CHANGES ' not in subject and 94 ' release note' not in subject): 95 warn(f'Please write a log message for commit {commit.sha}.') 96 for line in message_lines[2:]: 97 if (len(line) > 72 and 98 not line.startswith(' ') and 99 not re.match(r'\[[0-9]+\]', line)): 100 warn( 101 f'Line too long in log message for commit {commit.sha}: ' 102 f'```{line}``` ({len(line)} > 72 characters).' 103 ) 104 105############################################################################### 106# MILESTONE 107############################################################################### 108# 109# FAIL if the merge request is not assigned to any milestone. 110 111if not danger.gitlab.mr.milestone: 112 fail('Please assign this merge request to a milestone.') 113 114############################################################################### 115# VERSION LABELS 116############################################################################### 117# 118# FAIL if any of the following is true for the merge request: 119# 120# * The "Backport" label is set and the number of version labels set is 121# different than 1. (For backports, the version label is used for indicating 122# its target branch. This is a rather ugly attempt to address a UI 123# deficiency - the target branch for each MR is not visible on milestone 124# dashboards.) 125# 126# * Neither the "Backport" label nor any version label is set. (If the merge 127# request is not a backport, version labels are used for indicating 128# backporting preferences.) 129 130backport_label_set = 'Backport' in mr_labels 131version_labels = [l for l in mr_labels if l.startswith('v9.')] 132if backport_label_set and len(version_labels) != 1: 133 fail('The *Backport* label is set for this merge request. ' 134 'Please also set exactly one version label (*v9.x*).') 135if not backport_label_set and not version_labels: 136 fail('If this merge request is a backport, set the *Backport* label and ' 137 'a single version label (*v9.x*) indicating the target branch. ' 138 'If not, set version labels for all targeted backport branches.') 139 140############################################################################### 141# OTHER LABELS 142############################################################################### 143# 144# WARN if any of the following is true for the merge request: 145# 146# * The "Review" label is not set. (It may be intentional, but rarely is.) 147# 148# * The "Review" label is set, but the "LGTM" label is not set. (This aims to 149# remind developers about the need to set the latter on merge requests which 150# passed review.) 151 152if 'Review' not in mr_labels: 153 warn('This merge request does not have the *Review* label set. ' 154 'Please set it if you would like the merge request to be reviewed.') 155elif 'LGTM (Merge OK)' not in mr_labels: 156 warn('This merge request is currently in review. ' 157 'It should not be merged until it is marked with the *LGTM* label.') 158 159############################################################################### 160# 'CHANGES' FILE 161############################################################################### 162# 163# FAIL if any of the following is true: 164# 165# * The merge request does not update the CHANGES file, but it does not have 166# the "No CHANGES" label set. (This attempts to ensure that the author of 167# the MR did not forget about adding a CHANGES entry.) 168# 169# * The merge request updates the CHANGES file, but it has the "No CHANGES" 170# label set. (This attempts to ensure that the "No CHANGES" label is used in 171# a sane way.) 172# 173# * The merge request adds any placeholder entries to the CHANGES file, but it 174# does not target the "main" branch. 175# 176# * The merge request adds a new CHANGES entry that is not a placeholder and 177# does not contain any GitLab/RT issue/MR identifiers. 178 179changes_modified = 'CHANGES' in modified_files 180no_changes_label_set = 'No CHANGES' in mr_labels 181if not changes_modified and not no_changes_label_set: 182 fail('This merge request does not modify `CHANGES`. ' 183 'Add a `CHANGES` entry or set the *No CHANGES* label.') 184if changes_modified and no_changes_label_set: 185 fail('This merge request modifies `CHANGES`. ' 186 'Revert `CHANGES` modifications or unset the *No Changes* label.') 187 188changes_added_lines = added_lines(target_branch, ['CHANGES']) 189placeholders_added = lines_containing(changes_added_lines, '[placeholder]') 190identifiers_found = filter(changes_issue_or_mr_id_regex.search, changes_added_lines) 191if changes_added_lines: 192 if placeholders_added: 193 if target_branch != 'main': 194 fail('This MR adds at least one placeholder entry to `CHANGES`. ' 195 'It should be targeting the `main` branch.') 196 elif not any(identifiers_found): 197 fail('No valid issue/MR identifiers found in added `CHANGES` entries.') 198 199############################################################################### 200# RELEASE NOTES 201############################################################################### 202# 203# - FAIL if any of the following is true: 204# 205# * The merge request does not update release notes and has the "Release 206# Notes" label set. (This attempts to point out missing release notes.) 207# 208# * The merge request updates release notes but does not have the "Release 209# Notes" label set. (This ensures that merge requests updating release 210# notes can be easily found using the "Release Notes" label.) 211# 212# - WARN if any of the following is true: 213# 214# * This merge request does not update release notes and has the "Customer" 215# label set. (Except for trivial changes, all merge requests which may 216# be of interest to customers should include a release note.) 217# 218# * This merge request updates release notes, but no GitLab/RT issue/MR 219# identifiers are found in the lines added to the release notes by this 220# MR. 221 222release_notes_regex = re.compile(r'doc/(arm|notes)/notes-.*\.(rst|xml)') 223release_notes_changed = list(filter(release_notes_regex.match, modified_files)) 224release_notes_label_set = 'Release Notes' in mr_labels 225if not release_notes_changed: 226 if release_notes_label_set: 227 fail('This merge request has the *Release Notes* label set. ' 228 'Add a release note or unset the *Release Notes* label.') 229 elif 'Customer' in mr_labels: 230 warn('This merge request has the *Customer* label set. ' 231 'Add a release note unless the changes introduced are trivial.') 232if release_notes_changed and not release_notes_label_set: 233 fail('This merge request modifies release notes. ' 234 'Revert release note modifications or set the *Release Notes* label.') 235 236if release_notes_changed: 237 notes_added_lines = added_lines(target_branch, release_notes_changed) 238 identifiers_found = filter(relnotes_issue_or_mr_id_regex.search, notes_added_lines) 239 if notes_added_lines and not any(identifiers_found): 240 warn('No valid issue/MR identifiers found in added release notes.') 241else: 242 notes_added_lines = [] 243 244############################################################################### 245# CVE IDENTIFIERS 246############################################################################### 247# 248# FAIL if the merge request adds a CHANGES entry of type [security] and a CVE 249# identifier is missing from either the added CHANGES entry or the added 250# release note. 251 252if lines_containing(changes_added_lines, '[security]'): 253 if not lines_containing(changes_added_lines, '(CVE-20'): 254 fail('This merge request fixes a security issue. ' 255 'Please add a CHANGES entry which includes a CVE identifier.') 256 if not lines_containing(notes_added_lines, 'CVE-20'): 257 fail('This merge request fixes a security issue. ' 258 'Please add a release note which includes a CVE identifier.') 259 260############################################################################### 261# PAIRWISE TESTING 262############################################################################### 263# 264# FAIL if the merge request adds any new ./configure switch without an 265# associated annotation used for pairwise testing. 266 267configure_added_lines = added_lines(target_branch, ['configure.ac']) 268switches_added = (lines_containing(configure_added_lines, 'AC_ARG_ENABLE') + 269 lines_containing(configure_added_lines, 'AC_ARG_WITH')) 270annotations_added = lines_containing(configure_added_lines, '# [pairwise: ') 271if len(switches_added) > len(annotations_added): 272 fail('This merge request adds at least one new `./configure` switch that ' 273 'is not annotated for pairwise testing purposes.') 274