lukegleeson | 165e3b8 | 2022-03-08 11:41:52 +0000 | [diff] [blame] | 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 |
lukegleeson | e5254a7 | 2022-08-26 10:55:04 +0100 | [diff] [blame] | 224 | errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t ' + repr(copyrightInFile[copyrightInFileKey]) + '\n' |
lukegleeson | 165e3b8 | 2022-03-08 11:41:52 +0000 | [diff] [blame] | 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() |