import os.path ## To Do # - Breaking Changes # - Merge Commits # - Error Handling 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 #### pathToRepo = "/Users/daniel/Developer/Repos/HfM/schumacher/Prisma-Binauralize" #pathToRepo = "/Users/daniel/Desktop/testrepo" commitList = getSeparatedGitLog(pathToRepo) # 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 commitsByType = {"Other":[], "Features":[], "Fixes":[]} nonconformCommits = [] for commit in tag[1:]: if commit.body.commitType == CommitBody.commitTypes[5]: # fix commitsByType["Fixes"].append(commit) elif commit.body.commitType == CommitBody.commitTypes[4]: # feat commitsByType["Features"].append(commit) elif commit.body.commitType == "nonconform": nonconformCommits.append(commit) else: commitsByType["Other"].append(commit) # Sub-Grouping by Scopes within Types while len(commitsByType) > 0: commitsByScope = {} commitType, commits = commitsByType.popitem() if len(commits) == 0: continue for commit in commits: if commit.body.scope == None: if "no scope" not in commitsByScope: commitsByScope["no scope"] = [commit] else: commitsByScope["no scope"].append(commit) elif commit.body.scope in commitsByScope: commitsByScope[commit.body.scope].append(commit) else: commitsByScope[commit.body.scope] = [commit] fileTemplate.append("\n### " + str(commitType)) while len(commitsByScope) > 0: scope, commits = commitsByScope.popitem() fileTemplate.append("- *" + str(scope) + "*") for commit in commits: if commitType == "Other": fileTemplate.append(" - (" + commit.body.commitType + ") " + commit.body.subject + commit.appendShortHash()) else: 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(pathToRepo + "/changelog.md", "w") as file: for line in fileTemplate: file.write(line + "\n")