Copyright Check Script
[cps.git] / checkstyle / src / main / CopyrightCheck.py
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
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
19 import subprocess
20 import csv
21 import re
22 import datetime
23
24 #constants
25 import sys
26
27 COMMITTERS_CONFIG_FILE = ''
28 TEMPLATE_COPYRIGHT_FILE = ''
29 IGNORE_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]
34
35 BANNER = '=' * 120
36
37 def 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
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...')
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
67 def 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
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('@'):]
98
99 # Read the config file with names of companies and respective email extensions
100 def 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
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.')
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
124 def 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
138 def 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
148 def 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
152 def 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
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'
190
191     return fileCopyright
192
193 def 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
218 def 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
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'))
228     return issueCounter
229
230 # Check the signatures and compare with committer signature and current year
231 def 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
261 if __name__ == '__main__':
262     main()