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 http://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
28issue_or_mr_id_regex = re.compile(br'\[(GL [#!]|RT #)[0-9]+\]')
29release_notes_regex = re.compile(r'doc/(arm|notes)/notes-.*\.(rst|xml)')
30
31modified_files = danger.git.modified_files
32mr_labels = danger.gitlab.mr.labels
33target_branch = danger.gitlab.mr.target_branch
34
35###############################################################################
36# COMMIT MESSAGES
37###############################################################################
38#
39# - FAIL if any of the following is true for any commit on the MR branch:
40#
41#     * The subject line starts with "fixup!" or "Apply suggestion".
42#
43#     * There is no empty line between the subject line and the log message.
44#
45# - WARN if any of the following is true for any commit on the MR branch:
46#
47#     * The length of the subject line exceeds 72 characters.
48#
49#     * There is no log message present (i.e. commit only has a subject) and the
50#       subject line does not contain any of the following strings: "fixup! ",
51#       " CHANGES ", " release note".
52#
53#     * Any line of the log message is longer than 72 characters.  This rule is
54#       not evaluated for lines starting with four spaces, which allows long
55#       lines to be included in the commit log message by prefixing them with
56#       four spaces (useful for pasting compiler warnings, static analyzer
57#       messages, log lines, etc.)
58
59for commit in danger.git.commits:
60    message_lines = commit.message.splitlines()
61    subject = message_lines[0]
62    if subject.startswith('fixup!') or subject.startswith('Apply suggestion'):
63        fail('Fixup commits are still present in this merge request. '
64             'Please squash them before merging.')
65    if len(subject) > 72:
66        warn(
67            f'Subject line for commit {commit.sha} is too long: '
68            f'```{subject}``` ({len(subject)} > 72 characters).'
69        )
70    if len(message_lines) > 1 and message_lines[1]:
71        fail(f'No empty line after subject for commit {commit.sha}.')
72    if (len(message_lines) < 3 and
73            'fixup! ' not in subject and
74            ' CHANGES ' not in subject and
75            ' release note' not in subject):
76        warn(f'Please write a log message for commit {commit.sha}.')
77    for line in message_lines[2:]:
78        if len(line) > 72 and not line.startswith('    '):
79            warn(
80                f'Line too long in log message for commit {commit.sha}: '
81                f'```{line}``` ({len(line)} > 72 characters).'
82            )
83
84###############################################################################
85# MILESTONE
86###############################################################################
87#
88# FAIL if the merge request is not assigned to any milestone.
89
90if not danger.gitlab.mr.milestone:
91    fail('Please assign this merge request to a milestone.')
92
93###############################################################################
94# VERSION LABELS
95###############################################################################
96#
97# FAIL if any of the following is true for the merge request:
98#
99# * The "Backport" label is set and the number of version labels set is
100#   different than 1.  (For backports, the version label is used for indicating
101#   its target branch.  This is a rather ugly attempt to address a UI
102#   deficiency - the target branch for each MR is not visible on milestone
103#   dashboards.)
104#
105# * Neither the "Backport" label nor any version label is set.  (If the merge
106#   request is not a backport, version labels are used for indicating
107#   backporting preferences.)
108
109backport_label_set = 'Backport' in mr_labels
110version_labels = [l for l in mr_labels if l.startswith('v9.')]
111if backport_label_set and len(version_labels) != 1:
112    fail('The *Backport* label is set for this merge request. '
113         'Please also set exactly one version label (*v9.x*).')
114if not backport_label_set and not version_labels:
115    fail('If this merge request is a backport, set the *Backport* label and '
116         'a single version label (*v9.x*) indicating the target branch. '
117         'If not, set version labels for all targeted backport branches.')
118
119###############################################################################
120# OTHER LABELS
121###############################################################################
122#
123# WARN if any of the following is true for the merge request:
124#
125# * The "Review" label is not set.  (It may be intentional, but rarely is.)
126#
127# * The "Review" label is set, but the "LGTM" label is not set.  (This aims to
128#   remind developers about the need to set the latter on merge requests which
129#   passed review.)
130
131if 'Review' not in mr_labels:
132    warn('This merge request does not have the *Review* label set. '
133         'Please set it if you would like the merge request to be reviewed.')
134elif 'LGTM (Merge OK)' not in mr_labels:
135    warn('This merge request is currently in review. '
136         'It should not be merged until it is marked with the *LGTM* label.')
137
138###############################################################################
139# 'CHANGES' FILE
140###############################################################################
141#
142# FAIL if any of the following is true:
143#
144# * The merge request does not update the CHANGES file, but it does not have
145#   the "No CHANGES" label set.  (This attempts to ensure that the author of
146#   the MR did not forget about adding a CHANGES entry.)
147#
148# * The merge request updates the CHANGES file, but it has the "No CHANGES"
149#   label set.  (This attempts to ensure the the "No CHANGES" label is used in
150#   a sane way.)
151#
152# * The merge request adds a new CHANGES entry that does not contain any
153#   GitLab/RT issue/MR identifiers.
154
155changes_modified = 'CHANGES' in modified_files
156no_changes_label_set = 'No CHANGES' in mr_labels
157if not changes_modified and not no_changes_label_set:
158    fail('This merge request does not modify `CHANGES`. '
159         'Add a `CHANGES` entry or set the *No CHANGES* label.')
160if changes_modified and no_changes_label_set:
161    fail('This merge request modifies `CHANGES`. '
162         'Revert `CHANGES` modifications or unset the *No Changes* label.')
163
164changes_added_lines = added_lines(target_branch, ['CHANGES'])
165identifiers_found = filter(issue_or_mr_id_regex.search, changes_added_lines)
166if changes_added_lines and not any(identifiers_found):
167    fail('No valid issue/MR identifiers found in added `CHANGES` entries.')
168
169###############################################################################
170# RELEASE NOTES
171###############################################################################
172#
173# - FAIL if any of the following is true:
174#
175#     * The merge request does not update release notes and has the "Release
176#       Notes" label set.  (This attempts to point out missing release notes.)
177#
178#     * The merge request updates release notes but does not have the "Release
179#       Notes" label set.  (This ensures that merge requests updating release
180#       notes can be easily found using the "Release Notes" label.)
181#
182# - WARN if this merge request updates release notes, but no GitLab/RT issue/MR
183#   identifiers are found in the lines added to the release notes by this MR.
184
185release_notes_regex = re.compile(r'doc/(arm|notes)/notes-.*\.(rst|xml)')
186release_notes_changed = list(filter(release_notes_regex.match, modified_files))
187release_notes_label_set = 'Release Notes' in mr_labels
188if not release_notes_changed and release_notes_label_set:
189    fail('This merge request has the *Release Notes* label set. '
190         'Add a release note or unset the *Release Notes* label.')
191if release_notes_changed and not release_notes_label_set:
192    fail('This merge request modifies release notes. '
193         'Revert release note modifications or set the *Release Notes* label.')
194
195if release_notes_changed:
196    notes_added_lines = added_lines(target_branch, release_notes_changed)
197    identifiers_found = filter(issue_or_mr_id_regex.search, notes_added_lines)
198    if notes_added_lines and not any(identifiers_found):
199        warn('No valid issue/MR identifiers found in added release notes.')
200