blob: ff3d4029b5414274679354c690f795841a359cd8 [file] [log] [blame]
lukegleeson165e3b82022-03-08 11:41:52 +00001# ============LICENSE_START=======================================================
2# Copyright (C) 2022 Nordix Foundation
3# ================================================================================
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16# SPDX-License-Identifier: Apache-2.0
17# ============LICENSE_END=========================================================
18
19import subprocess
20import csv
21import re
22import datetime
23
24#constants
25import sys
26
27COMMITTERS_CONFIG_FILE = ''
28TEMPLATE_COPYRIGHT_FILE = ''
29IGNORE_FILE = ''
30if len(sys.argv) == 4:
31 COMMITTERS_CONFIG_FILE = sys.argv[1]
32 TEMPLATE_COPYRIGHT_FILE = sys.argv[2]
33 IGNORE_FILE = sys.argv[3]
34
35BANNER = '=' * 120
36
37def main():
38 print(BANNER + '\nCopyright Check Python Script:')
39 PermissionsCheck()
40
41 committerEmailExtension = GetCommitterEmailExtension()
42 projectCommitters = ReadProjectCommittersConfigFile()
43
44 CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
45
46 alteredFiles = FindAlteredFiles()
47
48 if alteredFiles:
49 issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension)
50 else:
51 issueCounter = 0
52
53 print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked')
54 print(BANNER)
55
56
57# Check that Script has access to command line functions to use git
58def PermissionsCheck():
59 if 'permission denied' in subprocess.run('git', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').lower():
60 print('Error, I may not have the necessary permissions. Exiting...')
61 print(BANNER)
62 sys.exit()
63 else:
64 return
65
66# Returns List of Strings of file tracked by git which have been changed/added
67def FindAlteredFiles():
68 ignoreFilePaths = GetIgnoredFiles()
69
70 #Before Stage lower case d removes deleted files
71 stream = subprocess.run('git diff --name-only --diff-filter=d', shell=True, stdout=subprocess.PIPE)
72 fileNames = stream.stdout.decode('utf-8')
73 #Staged
74 stream = subprocess.run('git diff --name-only --cached --diff-filter=d', shell=True, stdout=subprocess.PIPE)
75 fileNames += '\n' + stream.stdout.decode('utf-8')
76 #New committed
77 stream = subprocess.run('git diff --name-only HEAD^ HEAD --diff-filter=d', shell=True, stdout=subprocess.PIPE)
78 fileNames += '\n' + stream.stdout.decode('utf-8')
79
80 #String to list of strings
81 alteredFiles = fileNames.split("\n")
82
83 #Remove duplicates
84 alteredFiles = list(dict.fromkeys(alteredFiles))
85
86 #Remove blank string(s)
87 alteredFiles = list(filter(None, alteredFiles))
88
89 #Remove ignored-extensions
90 alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles))
91
92 return alteredFiles
93
94# Get the email of the most recent committer
95def GetCommitterEmailExtension():
96 email = subprocess.run('git show -s --format=\'%ce\'', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n')
97 return email[email.index('@'):]
98
99# Read the config file with names of companies and respective email extensions
100def ReadProjectCommittersConfigFile():
101 try:
102 with open(COMMITTERS_CONFIG_FILE, 'r') as file:
103 reader = csv.reader(file, delimiter=',')
104 projectCommitters = {row[0]:row[1] for row in reader}
105 projectCommitters.pop('email') #Remove csv header
106 except FileNotFoundError:
107 print('Unable to open Project Committers Config File, have the command line arguments been set?')
108 print(BANNER)
109 sys.exit()
110 return projectCommitters
111
112def CheckCommitterInConfigFile(committerEmailExtension, projectCommitters):
113 if not committerEmailExtension in projectCommitters:
114 print('Error, Committer email is not included in config file.')
115 print('If your company is new to the project please make appropriate changes to project-committers-config.csv')
116 print('for Copyright Check to work.')
117 print('Exiting...')
118 print(BANNER)
119 sys.exit()
120 else:
121 return True
122
123# Read config file with list of files to ignore
124def GetIgnoredFiles():
125 try:
126 with open(IGNORE_FILE, 'r') as file:
127 reader = csv.reader(file)
128 ignoreFilePaths = [row[0] for row in reader]
129 ignoreFilePaths.pop(0) #Remove csv header
130 ignoreFilePaths = [filePath.replace('*', '.*') for filePath in ignoreFilePaths]
131 except FileNotFoundError:
132 print('Unable to open File Ignore Config File, have the command line arguments been set?')
133 print(BANNER)
134 sys.exit()
135 return ignoreFilePaths
136
137# Read the template copyright file
138def GetCopyrightTemplate():
139 try:
140 with open(TEMPLATE_COPYRIGHT_FILE, 'r') as file:
141 copyrightTemplate = file.readlines()
142 except FileNotFoundError:
143 print('Unable to open Template Copyright File, have the command line arguments been set?')
144 print(BANNER)
145 sys.exit()
146 return copyrightTemplate
147
148def GetProjectRootDir():
149 return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/'
150
151# Get the Copyright from the altered file
152def ParseFileCopyright(fileObject):
153 global issueCounter
154 copyrightFlag = False
155 copyrightInFile = {}
156 lineNumber = 1
157 for line in fileObject:
158 if 'LICENSE_START' in line:
159 copyrightFlag = True
160 if copyrightFlag:
161 copyrightInFile[lineNumber] = line
162 if 'LICENSE_END' in line:
163 break
164 lineNumber += 1
165
166 if not copyrightFlag:
167 print(fileObject.name + ' | no copyright found')
168 return {}, {}
169
170 copyrightSignatures = {}
171 copyrightLineNumbers = list(copyrightInFile.keys())
172 #Capture signature lines after LICENSE_START line
173 for lineNumber in copyrightLineNumbers:
174 if '=' not in copyrightInFile[lineNumber]:
175 copyrightSignatures[lineNumber] = copyrightInFile[lineNumber]
176 copyrightInFile.pop(lineNumber)
177 elif 'LICENSE_START' not in copyrightInFile[lineNumber]:
178 break
179
180 return (copyrightInFile, copyrightSignatures)
181
182# Remove the Block comment syntax
183def RemoveCommentBlock(fileCopyright):
184 # Comment Characters can very depending on file # *..
185 endOfCommentsIndex = list(fileCopyright.values())[0].index('=')
186 for key in fileCopyright:
187 fileCopyright[key] = fileCopyright[key][endOfCommentsIndex:]
188 if fileCopyright[key] == '':
189 fileCopyright[key] = '\n'
190
191 return fileCopyright
192
193def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension):
194 issueCounter = 0
195 templateCopyright = GetCopyrightTemplate() #Get Copyright Template
196 projectRootDir = GetProjectRootDir()
197
198 for fileName in alteredFiles: # Not removed files
199 try:
200 with open(projectRootDir + fileName, 'r') as fileObject:
201 (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject)
202
203 #Empty dict evaluates to false
204 if fileCopyright and fileSignatures:
205 fileCopyright = RemoveCommentBlock(fileCopyright)
206 issueCounter += CheckCopyrightFormat(fileCopyright, templateCopyright, projectRootDir + fileName)
207 committerCompany = projectCommitters[committerEmailExtension]
208 issueCounter += CheckCopyrightSignature(fileSignatures, committerCompany, projectRootDir + fileName)
209 else:
210 issueCounter += 1
211
212 except FileNotFoundError:
213 issueCounter += 1
214 print('Unable to find file ' + projectRootDir + fileName)
215 return issueCounter
216
217# Check that the filecopyright matches the template copyright and print comparison
218def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath):
219 issueCounter = 0
220 errorWithComparison = ''
221 for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright):
222 if copyrightInFile[copyrightInFileKey] != templateLine:
223 issueCounter += 1
lukegleesone5254a72022-08-26 10:55:04 +0100224 errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t ' + repr(copyrightInFile[copyrightInFileKey]) + '\n'
lukegleeson165e3b82022-03-08 11:41:52 +0000225 errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n'
226 if errorWithComparison != '':
227 print(errorWithComparison.rstrip('\n'))
228 return issueCounter
229
230# Check the signatures and compare with committer signature and current year
231def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath):
232 issueCounter = 0
233 errorWithSignature = ''
234 signatureExists = False #signatureExistsForCommitter
235 afterFirstLine = False #afterFirstCopyright
236 for key in copyrightSignatures:
237 if afterFirstLine and 'Modifications Copyright' not in copyrightSignatures[key]:
238 issueCounter += 1
239 errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n'
240 elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]:
241 issueCounter += 1
242 errorWithSignature += filePath + ' | line ' + str(key) + ' expected Copyright\n'
243 if committerCompany in copyrightSignatures[key]:
244 signatureExists = True
245 signatureYear = int(re.findall(r'\d+', copyrightSignatures[key])[-1])
246 currentYear = datetime.date.today().year
247 if signatureYear != currentYear:
248 issueCounter += 1
249 errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n'
250 afterFirstLine = True
251
252 if not signatureExists:
253 issueCounter += 1
254 errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany
255
256 if errorWithSignature != '':
257 print(errorWithSignature.rstrip('\n'))
258
259 return issueCounter
260
261if __name__ == '__main__':
262 main()