1cdf0c1d5Smjnelson#! /usr/bin/python
2cdf0c1d5Smjnelson#
3cdf0c1d5Smjnelson# CDDL HEADER START
4cdf0c1d5Smjnelson#
5cdf0c1d5Smjnelson# The contents of this file are subject to the terms of the
6cdf0c1d5Smjnelson# Common Development and Distribution License (the "License").
7cdf0c1d5Smjnelson# You may not use this file except in compliance with the License.
8cdf0c1d5Smjnelson#
9cdf0c1d5Smjnelson# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10cdf0c1d5Smjnelson# or http://www.opensolaris.org/os/licensing.
11cdf0c1d5Smjnelson# See the License for the specific language governing permissions
12cdf0c1d5Smjnelson# and limitations under the License.
13cdf0c1d5Smjnelson#
14cdf0c1d5Smjnelson# When distributing Covered Code, include this CDDL HEADER in each
15cdf0c1d5Smjnelson# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16cdf0c1d5Smjnelson# If applicable, add the following below this CDDL HEADER, with the
17cdf0c1d5Smjnelson# fields enclosed by brackets "[]" replaced with your own identifying
18cdf0c1d5Smjnelson# information: Portions Copyright [yyyy] [name of copyright owner]
19cdf0c1d5Smjnelson#
20cdf0c1d5Smjnelson# CDDL HEADER END
21cdf0c1d5Smjnelson#
22cdf0c1d5Smjnelson
23cdf0c1d5Smjnelson#
2441a5f560SMark J. Nelson# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
25cdf0c1d5Smjnelson# Use is subject to license terms.
26cdf0c1d5Smjnelson#
27cdf0c1d5Smjnelson
282f54b716SRichard Lowe# Copyright 2007, 2010 Richard Lowe
2918ce2efcSAndy Fiddaman# Copyright 2019 OmniOS Community Edition (OmniOSce) Association.
30*3f8945a7SBill Sommerfeld# Copyright 2024 Bill Sommerfeld
312f54b716SRichard Lowe
32cdf0c1d5Smjnelson#
33cdf0c1d5Smjnelson# Check delta comments:
34ca13eaa5SAndy Fiddaman#	- Have the correct form.
35ca13eaa5SAndy Fiddaman#	- Have a synopsis matching that of the bug
36ca13eaa5SAndy Fiddaman#	- Appear only once.
3718ce2efcSAndy Fiddaman#	- Do not contain common spelling errors.
38cdf0c1d5Smjnelson#
39cdf0c1d5Smjnelson
40cdf0c1d5Smjnelsonimport re, sys
412f54b716SRichard Lowefrom onbld.Checks.DbLookups import BugDB
4218ce2efcSAndy Fiddamanfrom onbld.Checks.SpellCheck import spellcheck_line
432f54b716SRichard Lowe
44cdf0c1d5Smjnelson
452f54b716SRichard Lowebugre = re.compile(r'^(\d{2,7}) (.*)$')
46ef62fef3SRichard Lowe
47cdf0c1d5Smjnelson
48cdf0c1d5Smjnelsondef isBug(comment):
49cdf0c1d5Smjnelson	return bugre.match(comment)
50cdf0c1d5Smjnelson
51*3f8945a7SBill Sommerfelddef changeid_present(comments):
52*3f8945a7SBill Sommerfeld	if len(comments) < 3:
53*3f8945a7SBill Sommerfeld		return False
54*3f8945a7SBill Sommerfeld	if comments[-2] != '':
55*3f8945a7SBill Sommerfeld		return False
56*3f8945a7SBill Sommerfeld	if re.match('^Change-Id: I[0-9a-f]+', comments[-1]):
57*3f8945a7SBill Sommerfeld		return True
58*3f8945a7SBill Sommerfeld	return False
59cdf0c1d5Smjnelson
60*3f8945a7SBill Sommerfelddef comchk(comments, check_db=True, output=sys.stderr, bugs=None):
61c08a253cSJohn Sonnenschein	'''Validate checkin comments against ON standards.
62ef62fef3SRichard Lowe
63c08a253cSJohn Sonnenschein	Comments must be a list of one-line comments, with no trailing
64c08a253cSJohn Sonnenschein	newline.
652f54b716SRichard Lowe
662f54b716SRichard Lowe	If check_db is True (the default), validate bug synopses against the
672f54b716SRichard Lowe	databases.
68ef62fef3SRichard Lowe
69c08a253cSJohn Sonnenschein	Error messages intended for the user are written to output,
70c08a253cSJohn Sonnenschein	which defaults to stderr
71c08a253cSJohn Sonnenschein	'''
722f54b716SRichard Lowe	bugnospcre = re.compile(r'^(\d{2,7})([^ ].*)')
732f54b716SRichard Lowe	ignorere = re.compile(r'^(' +
742f54b716SRichard Lowe                              r'Portions contributed by|' +
752f54b716SRichard Lowe                              r'Contributed by|' +
762f54b716SRichard Lowe                              r'Reviewed[ -]by|' +
772f54b716SRichard Lowe                              r'Approved[ -]by|' +
782f54b716SRichard Lowe                              r'back[ -]?out)' +
792f54b716SRichard Lowe                              r'[: ]')
80cdf0c1d5Smjnelson
81cdf0c1d5Smjnelson	errors = { 'bugnospc': [],
82*3f8945a7SBill Sommerfeld		   'changeid': [],
83cdf0c1d5Smjnelson		   'mutant': [],
84cdf0c1d5Smjnelson		   'dup': [],
85cdf0c1d5Smjnelson		   'nomatch': [],
8618ce2efcSAndy Fiddaman		   'nonexistent': [],
8718ce2efcSAndy Fiddaman		   'spelling': [] }
88*3f8945a7SBill Sommerfeld	if bugs is None:
89*3f8945a7SBill Sommerfeld		bugs = {}
90*3f8945a7SBill Sommerfeld	newbugs = set()
91ef62fef3SRichard Lowe	ret = 0
92ef62fef3SRichard Lowe	blanks = False
93cdf0c1d5Smjnelson
94*3f8945a7SBill Sommerfeld	if changeid_present(comments):
95*3f8945a7SBill Sommerfeld		comments = comments[:-2]
96*3f8945a7SBill Sommerfeld		errors['changeid'].append('Change Id present')
97*3f8945a7SBill Sommerfeld
9818ce2efcSAndy Fiddaman	lineno = 0
99cdf0c1d5Smjnelson	for com in comments:
10018ce2efcSAndy Fiddaman		lineno += 1
10118ce2efcSAndy Fiddaman
102c08a253cSJohn Sonnenschein		# Our input must be newline-free, comments are line-wise.
103c08a253cSJohn Sonnenschein		if com.find('\n') != -1:
104c08a253cSJohn Sonnenschein			raise ValueError("newline in comment '%s'" % com)
105c08a253cSJohn Sonnenschein
106cdf0c1d5Smjnelson		# Ignore valid comments we can't check
107cdf0c1d5Smjnelson		if ignorere.search(com):
108cdf0c1d5Smjnelson			continue
109cdf0c1d5Smjnelson
110cdf0c1d5Smjnelson		if not com or com.isspace():
111ef62fef3SRichard Lowe			blanks = True
112cdf0c1d5Smjnelson			continue
113cdf0c1d5Smjnelson
11418ce2efcSAndy Fiddaman		for err in spellcheck_line(com):
11518ce2efcSAndy Fiddaman			errors['spelling'].append(
11618ce2efcSAndy Fiddaman			    'comment line {} - {}'.format(lineno, err))
11718ce2efcSAndy Fiddaman
118cdf0c1d5Smjnelson		match = bugre.search(com)
119cdf0c1d5Smjnelson		if match:
120*3f8945a7SBill Sommerfeld			(bugid, synopsis) = match.groups()
121*3f8945a7SBill Sommerfeld			bugs.setdefault(bugid, []).append(synopsis)
122*3f8945a7SBill Sommerfeld			newbugs.add(bugid)
123cdf0c1d5Smjnelson			continue
124cdf0c1d5Smjnelson
125cdf0c1d5Smjnelson		#
126cdf0c1d5Smjnelson		# Bugs missing a space after the ID are still bugs
127cdf0c1d5Smjnelson		# for the purposes of the duplicate ID and synopsis
128cdf0c1d5Smjnelson		# checks.
129cdf0c1d5Smjnelson		#
130cdf0c1d5Smjnelson		match = bugnospcre.search(com)
131cdf0c1d5Smjnelson		if match:
132*3f8945a7SBill Sommerfeld			(bugid, synopsis) = match.groups()
133*3f8945a7SBill Sommerfeld			bugs.setdefault(bugid, []).append(synopsis)
134*3f8945a7SBill Sommerfeld			newbugs.add(bugid)
135cdf0c1d5Smjnelson			errors['bugnospc'].append(com)
136cdf0c1d5Smjnelson			continue
137cdf0c1d5Smjnelson
138cdf0c1d5Smjnelson		# Anything else is bogus
139cdf0c1d5Smjnelson		errors['mutant'].append(com)
140cdf0c1d5Smjnelson
141cdf0c1d5Smjnelson	if len(bugs) > 0 and check_db:
142cdf0c1d5Smjnelson		bugdb = BugDB()
143ca13eaa5SAndy Fiddaman		results = bugdb.lookup(list(bugs.keys()))
144cdf0c1d5Smjnelson
145*3f8945a7SBill Sommerfeld	for crid in sorted(newbugs):
146*3f8945a7SBill Sommerfeld		insts = bugs[crid]
147cdf0c1d5Smjnelson		if len(insts) > 1:
148cdf0c1d5Smjnelson			errors['dup'].append(crid)
149cdf0c1d5Smjnelson
150cdf0c1d5Smjnelson		if not check_db:
151cdf0c1d5Smjnelson			continue
152cdf0c1d5Smjnelson
153cdf0c1d5Smjnelson		if crid not in results:
154cdf0c1d5Smjnelson			errors['nonexistent'].append(crid)
155cdf0c1d5Smjnelson			continue
156cdf0c1d5Smjnelson
157ef62fef3SRichard Lowe		#
158ef62fef3SRichard Lowe		# For each synopsis, compare the real synopsis with
159ef62fef3SRichard Lowe		# that in the comments, allowing for possible '(fix
160ef62fef3SRichard Lowe		# stuff)'-like trailing text
161ef62fef3SRichard Lowe		#
162cdf0c1d5Smjnelson		for entered in insts:
163cdf0c1d5Smjnelson			synopsis = results[crid]["synopsis"]
164ef62fef3SRichard Lowe			if not re.search(r'^' + re.escape(synopsis) +
165c08a253cSJohn Sonnenschein					r'( \([^)]+\))?$', entered):
166cdf0c1d5Smjnelson				errors['nomatch'].append([crid, synopsis,
167c08a253cSJohn Sonnenschein							entered])
168c08a253cSJohn Sonnenschein
169cdf0c1d5Smjnelson
170cdf0c1d5Smjnelson	if blanks:
171cdf0c1d5Smjnelson		output.write("WARNING: Blank line(s) in comments\n")
172cdf0c1d5Smjnelson		ret = 1
173cdf0c1d5Smjnelson
174cdf0c1d5Smjnelson	if errors['dup']:
175cdf0c1d5Smjnelson		ret = 1
176cdf0c1d5Smjnelson		output.write("These IDs appear more than once in your "
177cdf0c1d5Smjnelson			     "comments:\n")
178cdf0c1d5Smjnelson		for err in errors['dup']:
179cdf0c1d5Smjnelson			output.write("  %s\n" % err)
180cdf0c1d5Smjnelson
181cdf0c1d5Smjnelson	if errors['bugnospc']:
182cdf0c1d5Smjnelson		ret = 1
183cdf0c1d5Smjnelson		output.write("These bugs are missing a single space following "
184cdf0c1d5Smjnelson			     "the ID:\n")
185cdf0c1d5Smjnelson		for com in errors['bugnospc']:
186cdf0c1d5Smjnelson			output.write("  %s\n" % com)
187cdf0c1d5Smjnelson
188*3f8945a7SBill Sommerfeld	if errors['changeid']:
189*3f8945a7SBill Sommerfeld		ret = 1
190*3f8945a7SBill Sommerfeld		output.write("NOTE: Change-Id present in comment\n")
191*3f8945a7SBill Sommerfeld
192cdf0c1d5Smjnelson	if errors['mutant']:
193cdf0c1d5Smjnelson		ret = 1
1942f54b716SRichard Lowe		output.write("These comments are not valid bugs:\n")
195cdf0c1d5Smjnelson		for com in errors['mutant']:
196cdf0c1d5Smjnelson			output.write("  %s\n" % com)
197cdf0c1d5Smjnelson
198cdf0c1d5Smjnelson	if errors['nonexistent']:
199cdf0c1d5Smjnelson		ret = 1
2002f54b716SRichard Lowe		output.write("These bugs were not found in the databases:\n")
201cdf0c1d5Smjnelson		for id in errors['nonexistent']:
202cdf0c1d5Smjnelson			output.write("  %s\n" % id)
203cdf0c1d5Smjnelson
204cdf0c1d5Smjnelson	if errors['nomatch']:
205cdf0c1d5Smjnelson		ret = 1
2062f54b716SRichard Lowe		output.write("These bug synopses don't match "
207cdf0c1d5Smjnelson			     "the database entries:\n")
208cdf0c1d5Smjnelson		for err in errors['nomatch']:
2092f54b716SRichard Lowe			output.write("Synopsis of %s is wrong:\n" % err[0])
210cdf0c1d5Smjnelson			output.write("  should be: '%s'\n" % err[1])
211cdf0c1d5Smjnelson			output.write("         is: '%s'\n" % err[2])
212cdf0c1d5Smjnelson
21318ce2efcSAndy Fiddaman	if errors['spelling']:
21418ce2efcSAndy Fiddaman		ret = 1
21518ce2efcSAndy Fiddaman		output.write("Spellcheck:\n")
21618ce2efcSAndy Fiddaman		for err in errors['spelling']:
21718ce2efcSAndy Fiddaman			output.write('{}\n'.format(err))
21818ce2efcSAndy Fiddaman
219cdf0c1d5Smjnelson	return ret
220