# ============LICENSE_START======================================================= # Copyright (C) 2022 Nordix Foundation # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ============LICENSE_END========================================================= import subprocess import csv import re import datetime #constants import sys COMMITTERS_CONFIG_FILE = '' TEMPLATE_COPYRIGHT_FILE = '' IGNORE_FILE = '' if len(sys.argv) == 4: COMMITTERS_CONFIG_FILE = sys.argv[1] TEMPLATE_COPYRIGHT_FILE = sys.argv[2] IGNORE_FILE = sys.argv[3] BANNER = '=' * 120 def main(): print(BANNER + '\nCopyright Check Python Script:') PermissionsCheck() committerEmailExtension = GetCommitterEmailExtension() projectCommitters = ReadProjectCommittersConfigFile() CheckCommitterInConfigFile(committerEmailExtension, projectCommitters) alteredFiles = FindAlteredFiles() if alteredFiles: issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension) else: issueCounter = 0 print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked') print(BANNER) # Check that Script has access to command line functions to use git def PermissionsCheck(): if 'permission denied' in subprocess.run('git', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').lower(): print('Error, I may not have the necessary permissions. Exiting...') print(BANNER) sys.exit() else: return # Returns List of Strings of file tracked by git which have been changed/added def FindAlteredFiles(): ignoreFilePaths = GetIgnoredFiles() #Before Stage lower case d removes deleted files stream = subprocess.run('git diff --name-only --diff-filter=d', shell=True, stdout=subprocess.PIPE) fileNames = stream.stdout.decode('utf-8') #Staged stream = subprocess.run('git diff --name-only --cached --diff-filter=d', shell=True, stdout=subprocess.PIPE) fileNames += '\n' + stream.stdout.decode('utf-8') #New committed stream = subprocess.run('git diff --name-only HEAD^ HEAD --diff-filter=d', shell=True, stdout=subprocess.PIPE) fileNames += '\n' + stream.stdout.decode('utf-8') #String to list of strings alteredFiles = fileNames.split("\n") #Remove duplicates alteredFiles = list(dict.fromkeys(alteredFiles)) #Remove blank string(s) alteredFiles = list(filter(None, alteredFiles)) #Remove ignored-extensions alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles)) return alteredFiles # Get the email of the most recent committer def GetCommitterEmailExtension(): email = subprocess.run('git show -s --format=\'%ce\'', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') return email[email.index('@'):] # Read the config file with names of companies and respective email extensions def ReadProjectCommittersConfigFile(): try: with open(COMMITTERS_CONFIG_FILE, 'r') as file: reader = csv.reader(file, delimiter=',') projectCommitters = {row[0]:row[1] for row in reader} projectCommitters.pop('email') #Remove csv header except FileNotFoundError: print('Unable to open Project Committers Config File, have the command line arguments been set?') print(BANNER) sys.exit() return projectCommitters def CheckCommitterInConfigFile(committerEmailExtension, projectCommitters): if not committerEmailExtension in projectCommitters: print('Error, Committer email is not included in config file.') print('If your company is new to the project please make appropriate changes to project-committers-config.csv') print('for Copyright Check to work.') print('Exiting...') print(BANNER) sys.exit() else: return True # Read config file with list of files to ignore def GetIgnoredFiles(): try: with open(IGNORE_FILE, 'r') as file: reader = csv.reader(file) ignoreFilePaths = [row[0] for row in reader] ignoreFilePaths.pop(0) #Remove csv header ignoreFilePaths = [filePath.replace('*', '.*') for filePath in ignoreFilePaths] except FileNotFoundError: print('Unable to open File Ignore Config File, have the command line arguments been set?') print(BANNER) sys.exit() return ignoreFilePaths # Read the template copyright file def GetCopyrightTemplate(): try: with open(TEMPLATE_COPYRIGHT_FILE, 'r') as file: copyrightTemplate = file.readlines() except FileNotFoundError: print('Unable to open Template Copyright File, have the command line arguments been set?') print(BANNER) sys.exit() return copyrightTemplate def GetProjectRootDir(): return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/' # Get the Copyright from the altered file def ParseFileCopyright(fileObject): global issueCounter copyrightFlag = False copyrightInFile = {} lineNumber = 1 for line in fileObject: if 'LICENSE_START' in line: copyrightFlag = True if copyrightFlag: copyrightInFile[lineNumber] = line if 'LICENSE_END' in line: break lineNumber += 1 if not copyrightFlag: print(fileObject.name + ' | no copyright found') return {}, {} copyrightSignatures = {} copyrightLineNumbers = list(copyrightInFile.keys()) #Capture signature lines after LICENSE_START line for lineNumber in copyrightLineNumbers: if '=' not in copyrightInFile[lineNumber]: copyrightSignatures[lineNumber] = copyrightInFile[lineNumber] copyrightInFile.pop(lineNumber) elif 'LICENSE_START' not in copyrightInFile[lineNumber]: break return (copyrightInFile, copyrightSignatures) # Remove the Block comment syntax def RemoveCommentBlock(fileCopyright): # Comment Characters can very depending on file # *.. endOfCommentsIndex = list(fileCopyright.values())[0].index('=') for key in fileCopyright: fileCopyright[key] = fileCopyright[key][endOfCommentsIndex:] if fileCopyright[key] == '': fileCopyright[key] = '\n' return fileCopyright def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension): issueCounter = 0 templateCopyright = GetCopyrightTemplate() #Get Copyright Template projectRootDir = GetProjectRootDir() for fileName in alteredFiles: # Not removed files try: with open(projectRootDir + fileName, 'r') as fileObject: (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject) #Empty dict evaluates to false if fileCopyright and fileSignatures: fileCopyright = RemoveCommentBlock(fileCopyright) issueCounter += CheckCopyrightFormat(fileCopyright, templateCopyright, projectRootDir + fileName) committerCompany = projectCommitters[committerEmailExtension] issueCounter += CheckCopyrightSignature(fileSignatures, committerCompany, projectRootDir + fileName) else: issueCounter += 1 except FileNotFoundError: issueCounter += 1 print('Unable to find file ' + projectRootDir + fileName) return issueCounter # Check that the filecopyright matches the template copyright and print comparison def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath): issueCounter = 0 errorWithComparison = '' for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright): if copyrightInFile[copyrightInFileKey] != templateLine: issueCounter += 1 errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t ' + repr(copyrightInFile[copyrightInFileKey]) + '\n' errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n' if errorWithComparison != '': print(errorWithComparison.rstrip('\n')) return issueCounter # Check the signatures and compare with committer signature and current year def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath): issueCounter = 0 errorWithSignature = '' signatureExists = False #signatureExistsForCommitter afterFirstLine = False #afterFirstCopyright for key in copyrightSignatures: if afterFirstLine and 'Modifications Copyright' not in copyrightSignatures[key]: issueCounter += 1 errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n' elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]: issueCounter += 1 errorWithSignature += filePath + ' | line ' + str(key) + ' expected Copyright\n' if committerCompany in copyrightSignatures[key]: signatureExists = True signatureYear = int(re.findall(r'\d+', copyrightSignatures[key])[-1]) currentYear = datetime.date.today().year if signatureYear != currentYear: issueCounter += 1 errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n' afterFirstLine = True if not signatureExists: issueCounter += 1 errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany if errorWithSignature != '': print(errorWithSignature.rstrip('\n')) return issueCounter if __name__ == '__main__': main()