| # ============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() |