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