import os.path import sys ## To Do # - Breaking Changes # - Merge Commits # - Deal with wrong user input (spelling, no repo) def getSeparatedGitLog(repo): try: stream = os.popen("git -C {} log --format=%B--SEP--%H--SEP--%d--END--".format(repo)) except: raise ValueError("Not a valid git-repository!") else: gitLog = stream.read() commitList = gitLog.split("--END--") del commitList[-1] return commitList class RawCommit: def __init__(self, completeCommit): self.raw = completeCommit.split("--SEP--") self.body = self.raw[0].strip() self.hash = self.raw[1].strip() self.tag = self.raw[2].strip() class CommitBody: commitTypes = ("build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test") def __init__(self, completeMessage): self.completeMessage = completeMessage #self.subject = None #self.body = None #self.scope = None #self.commitType = None self.setSubjectAndBody() self.setScope() self.setCommitType() def setSubjectAndBody(self): try: subStart = self.completeMessage.index(": ")+2 except: try: subStart = self.completeMessage.index("\n") except: subStart = 0 try: subEnd = self.completeMessage.index("\n") except: self.subject = self.completeMessage[subStart:] self.body = None else: self.subject = self.completeMessage[subStart:subEnd] self.body = self.completeMessage[subEnd:].strip() def setScope(self): try: start = self.completeMessage.index("(")+1 end = self.completeMessage.index("):") except: self.scope = None else: self.scope = self.completeMessage[start:end].strip().lower() def setCommitType(self): for commitType in self.commitTypes: if (self.completeMessage.startswith((commitType + ": "))) or (self.completeMessage.startswith((commitType + "("))) or (self.completeMessage.startswith((commitType + " ("))): self.commitType = commitType break else: self.commitType = "nonconform" def getCommitMessageWithType(self): return self.commitType + ": " + self.subject class Scope: def __init__(self, name): self.name = name.strip().lower() @staticmethod def createScope(name): return Scope(name) class CommitTag: def __init__(self, completeTag): if not ("tag: " in completeTag): self.completeTag = None self.tagAsString = None self.major = None self.minor = None self.bugfix = None else: self.completeTag = completeTag self.setTagAsString() self.setMajorMinorBugfix() def setTagAsString(self): try: ## this one is temporary self.tagAsString = self.completeTag[(self.completeTag.rindex(": v.")+4):-1] except: try: self.tagAsString = self.completeTag[(self.completeTag.rindex(": v")+3):-1] except: self.tagAsString = self.completeTag[(self.completeTag.rindex(": ")+2):-1] def setMajorMinorBugfix(self): versionList = self.tagAsString.split(".") self.major = versionList[0] self.minor = versionList[1] self.bugfix = versionList[2] @staticmethod def getUpdateType(newTag, previousTag): if newTag.major > previousTag.major: return "major" elif newTag.minor > previousTag.minor: return "minor" elif newTag.bugfix > previousTag.bugfix: return "bugfix" class Commit: def __init__(self, rawCommit): self.body = CommitBody(rawCommit.body) self.tag = CommitTag(rawCommit.tag) self.hash = rawCommit.hash def appendShortHash(self): return " (" + self.hash[:6] + ")" #### Main #### inputPath = input("Please enter the base path of the repository: ") userDecision = input("Should the generated changelog be stored in another location (y/n)? ").lower() if userDecision == "y": outputPath = (input("Please enter the output path: ")) elif userDecision == "n": print("The changelog will be stored in the same location as the repository.") outputPath = inputPath else: print("invalid input") sys.exit(1) commitList = getSeparatedGitLog(inputPath) # Create a list of commits commitHistory = [] for commit in commitList: commitHistory.append(Commit(RawCommit(commit))) # Create a two-dimensional list by tags: [[tag, [commits]],[tag, [commits]],...] taggedHistory = [] for commit in commitHistory: if commit.tag.tagAsString: taggedHistory.append([commit.tag, commit]) else: if len(taggedHistory) == 0: taggedHistory.append([None, commit]) else: taggedHistory[-1].append(commit) # Construction of the changelog-file fileTemplate = ["# Changelog"] for tag in taggedHistory: # If latest commit has no tag: if not tag[0]: fileTemplate.append("\n## Without version number") else: fileTemplate.append("\n## Version " + tag[0].tagAsString) # Grouping by Type featType = ["Features"] fixType = ["Fixes"] otherType = ["Other"] nonconformCommits = [] for commit in tag[1:]: if commit.body.commitType == CommitBody.commitTypes[5]: # fix fixType.append(commit) elif commit.body.commitType == CommitBody.commitTypes[4]: # feat featType.append(commit) elif commit.body.commitType == "nonconform": nonconformCommits.append(commit) else: otherType.append(commit) # Sub-Grouping by Scopes within Types commitlistByType = [featType, fixType, otherType] for commitsByType in commitlistByType: if len(commitsByType) == 1: continue commitlistByScope = {} noScope = [] for commit in commitsByType: if type(commit) == str: continue if commit.body.scope == None: noScope.append(commit) elif commit.body.scope in commitlistByScope: commitlistByScope[commit.body.scope].append(commit) else: commitlistByScope[commit.body.scope] = [commit] fileTemplate.append("\n### " + commitsByType[0]) while len(commitlistByScope) > 0: scope, commits = commitlistByScope.popitem() fileTemplate.append("- *" + str(scope) + "*") for commit in commits: if commitsByType[0] == "Other": fileTemplate.append(" - (" + commit.body.commitType + ") " + commit.body.subject + commit.appendShortHash()) else: fileTemplate.append(" - " + commit.body.subject + commit.appendShortHash()) if len(noScope) > 0: fileTemplate.append("- *no scope*") for commit in noScope: fileTemplate.append(" - " + commit.body.subject + commit.appendShortHash()) # nonconform commits if len(nonconformCommits) > 0: fileTemplate.append("\n### Non-conform commits") for commit in nonconformCommits: fileTemplate.append("- " + commit.body.subject + commit.appendShortHash()) # write into changelog with open(outputPath + "/changelog.md", "w") as file: for line in fileTemplate: file.write(line + "\n")