1 # ============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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 # SPDX-License-Identifier: Apache-2.0
17 # ============LICENSE_END=========================================================
27 COMMITTERS_CONFIG_FILE = ''
28 TEMPLATE_COPYRIGHT_FILE = ''
30 if len(sys.argv) == 4:
31 COMMITTERS_CONFIG_FILE = sys.argv[1]
32 TEMPLATE_COPYRIGHT_FILE = sys.argv[2]
33 IGNORE_FILE = sys.argv[3]
38 print(BANNER + '\nCopyright Check Python Script:')
41 committerEmailExtension = GetCommitterEmailExtension()
42 projectCommitters = ReadProjectCommittersConfigFile()
44 CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
46 alteredFiles = FindAlteredFiles()
49 issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension)
53 print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked')
57 # Check that Script has access to command line functions to use git
58 def 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...')
66 # Returns List of Strings of file tracked by git which have been changed/added
67 def FindAlteredFiles():
68 ignoreFilePaths = GetIgnoredFiles()
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')
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')
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')
80 #String to list of strings
81 alteredFiles = fileNames.split("\n")
84 alteredFiles = list(dict.fromkeys(alteredFiles))
86 #Remove blank string(s)
87 alteredFiles = list(filter(None, alteredFiles))
89 #Remove ignored-extensions
90 alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles))
94 # Get the email of the most recent committer
95 def 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('@'):]
99 # Read the config file with names of companies and respective email extensions
100 def ReadProjectCommittersConfigFile():
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?')
110 return projectCommitters
112 def 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.')
123 # Read config file with list of files to ignore
124 def GetIgnoredFiles():
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?')
135 return ignoreFilePaths
137 # Read the template copyright file
138 def GetCopyrightTemplate():
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?')
146 return copyrightTemplate
148 def GetProjectRootDir():
149 return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/'
151 # Get the Copyright from the altered file
152 def ParseFileCopyright(fileObject):
154 copyrightFlag = False
157 for line in fileObject:
158 if 'LICENSE_START' in line:
161 copyrightInFile[lineNumber] = line
162 if 'LICENSE_END' in line:
166 if not copyrightFlag:
167 print(fileObject.name + ' | no copyright found')
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]:
180 return (copyrightInFile, copyrightSignatures)
182 # Remove the Block comment syntax
183 def 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'
193 def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension):
195 templateCopyright = GetCopyrightTemplate() #Get Copyright Template
196 projectRootDir = GetProjectRootDir()
198 for fileName in alteredFiles: # Not removed files
200 with open(projectRootDir + fileName, 'r') as fileObject:
201 (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject)
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)
212 except FileNotFoundError:
214 print('Unable to find file ' + projectRootDir + fileName)
217 # Check that the filecopyright matches the template copyright and print comparison
218 def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath):
220 errorWithComparison = ''
221 for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright):
222 if copyrightInFile[copyrightInFileKey] != templateLine:
224 errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t ' + repr(copyrightInFile[copyrightInFileKey]) + '\n'
225 errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n'
226 if errorWithComparison != '':
227 print(errorWithComparison.rstrip('\n'))
230 # Check the signatures and compare with committer signature and current year
231 def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath):
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]:
239 errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n'
240 elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]:
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:
249 errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n'
250 afterFirstLine = True
252 if not signatureExists:
254 errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany
256 if errorWithSignature != '':
257 print(errorWithSignature.rstrip('\n'))
261 if __name__ == '__main__':