df89
08.06.21 1f69ad4631ef12f60f8f54d060cd3656aececc27
commit | author | age
b07d2e 1 import os.path
1f69ad 2 import sys
b07d2e 3
DF 4 ## To Do
5 # - Breaking Changes
6 # - Merge Commits
1f69ad 7 # - Deal with wrong user input (spelling, no repo)
b07d2e 8
DF 9 def getSeparatedGitLog(repo):
10     try:
11         stream = os.popen("git -C {} log --format=%B--SEP--%H--SEP--%d--END--".format(repo))
12     except:
13         raise ValueError("Not a valid git-repository!")
14     else:
15         gitLog = stream.read()
16         commitList = gitLog.split("--END--")
17         del commitList[-1]
18         return commitList
19      
20
21 class RawCommit:
22     def __init__(self, completeCommit):
23         self.raw = completeCommit.split("--SEP--")
24         self.body = self.raw[0].strip()
25         self.hash = self.raw[1].strip()
26         self.tag = self.raw[2].strip()
27
28
29 class CommitBody:
30     commitTypes = ("build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test")
31     def __init__(self, completeMessage):
32         self.completeMessage = completeMessage
33         #self.subject = None
34         #self.body = None
35         #self.scope = None
36         #self.commitType = None
37
38         self.setSubjectAndBody()
39         self.setScope()
40         self.setCommitType()
41     
42     def setSubjectAndBody(self):
43         try:
44             subStart = self.completeMessage.index(": ")+2
45         except:
46             try:
47                 subStart = self.completeMessage.index("\n")
48             except:
49                 subStart = 0
50         try:
51             subEnd = self.completeMessage.index("\n")
52         except:
53             self.subject = self.completeMessage[subStart:]
54             self.body = None
55         else:
56             self.subject = self.completeMessage[subStart:subEnd]
57             self.body = self.completeMessage[subEnd:].strip()
58     
59     def setScope(self):
60         try:
61             start = self.completeMessage.index("(")+1
d03d64 62             end = self.completeMessage.index("):")
b07d2e 63         except:
DF 64             self.scope = None
65         else:
66             self.scope = self.completeMessage[start:end].strip().lower()
67     
68     def setCommitType(self):
69         for commitType in self.commitTypes:
70             if (self.completeMessage.startswith((commitType + ": "))) or (self.completeMessage.startswith((commitType + "("))) or (self.completeMessage.startswith((commitType + " ("))):
71                 self.commitType = commitType
72                 break
73             else:
74                 self.commitType = "nonconform"
75     
76     def getCommitMessageWithType(self):
77         return self.commitType + ": " + self.subject
78         
79 class Scope:
80     def __init__(self, name):
81         self.name = name.strip().lower()
82     
83     @staticmethod
84     def createScope(name):
85         return Scope(name)
86
87 class CommitTag:
88     def __init__(self, completeTag):
89         if not ("tag: " in completeTag):
90             self.completeTag = None
91             self.tagAsString = None
92             self.major = None
93             self.minor = None
94             self.bugfix = None
95         else:
96             self.completeTag = completeTag
97             self.setTagAsString()
98             self.setMajorMinorBugfix()
99
100     def setTagAsString(self):
101         try: ## this one is temporary
102             self.tagAsString = self.completeTag[(self.completeTag.rindex(": v.")+4):-1]
103         except:
104             try:
105                 self.tagAsString = self.completeTag[(self.completeTag.rindex(": v")+3):-1]
106             except:
107                 self.tagAsString = self.completeTag[(self.completeTag.rindex(": ")+2):-1]
108
109     def setMajorMinorBugfix(self):
110         versionList = self.tagAsString.split(".")
111         self.major = versionList[0]
112         self.minor = versionList[1]
113         self.bugfix = versionList[2]
114     
115     @staticmethod
116     def getUpdateType(newTag, previousTag):
117         if newTag.major > previousTag.major:
118             return "major"
119         elif newTag.minor > previousTag.minor:
120             return "minor"
121         elif newTag.bugfix > previousTag.bugfix:
122             return "bugfix"
123
124
125 class Commit:
126     def __init__(self, rawCommit):
127         self.body = CommitBody(rawCommit.body)
128         self.tag = CommitTag(rawCommit.tag)
129         self.hash = rawCommit.hash
130     
131     def appendShortHash(self):
132         return " (" + self.hash[:6] + ")"
133
134
135
136 #### Main ####
137
1f69ad 138 inputPath = input("Please enter the base path of the repository: ")
D 139 userDecision = input("Should the generated changelog be stored in another location (y/n)? ").lower()
140 if userDecision == "y":
141     outputPath = (input("Please enter the output path: "))
142 elif userDecision == "n":
143     print("The changelog will be stored in the same location as the repository.")
144     outputPath = inputPath
145 else:
146     print("invalid input")
147     sys.exit(1)
b07d2e 148
1f69ad 149 commitList = getSeparatedGitLog(inputPath)
b07d2e 150
DF 151 # Create a list of commits
152 commitHistory = []
153 for commit in commitList:
154     commitHistory.append(Commit(RawCommit(commit)))
155
156 # Create a two-dimensional list by tags: [[tag, [commits]],[tag, [commits]],...]
157 taggedHistory = []
158 for commit in commitHistory:
159     if commit.tag.tagAsString:
160         taggedHistory.append([commit.tag, commit])
161     else:
162         if len(taggedHistory) == 0:
163             taggedHistory.append([None, commit])
164         else:
165             taggedHistory[-1].append(commit)
166
167
168 # Construction of the changelog-file
169 fileTemplate = ["# Changelog"]
170 for tag in taggedHistory:
171     # If latest commit has no tag:
172     if not tag[0]:
173         fileTemplate.append("\n## Without version number")
174     else:
175         fileTemplate.append("\n## Version " + tag[0].tagAsString)
176
177
178     # Grouping by Type
0d5361 179     featType = ["Features"]
D 180     fixType = ["Fixes"]
181     otherType = ["Other"]
b07d2e 182     nonconformCommits = []
0d5361 183     
b07d2e 184     for commit in tag[1:]:
DF 185         if commit.body.commitType == CommitBody.commitTypes[5]: # fix
0d5361 186             fixType.append(commit)
b07d2e 187         elif commit.body.commitType == CommitBody.commitTypes[4]: # feat
0d5361 188             featType.append(commit)
b07d2e 189         elif commit.body.commitType == "nonconform":
DF 190             nonconformCommits.append(commit)
191         else:
0d5361 192             otherType.append(commit)
b07d2e 193
DF 194     # Sub-Grouping by Scopes within Types    
0d5361 195     commitlistByType = [featType, fixType, otherType]
D 196     for commitsByType in commitlistByType:
197         if len(commitsByType) == 1:
b07d2e 198             continue
0d5361 199         commitlistByScope = {}
D 200         noScope = []
201         for commit in commitsByType:
202             if type(commit) == str:
203                 continue
b07d2e 204             if commit.body.scope == None:
b2e728 205                 noScope.append(commit)
0d5361 206             elif commit.body.scope in commitlistByScope:
D 207                 commitlistByScope[commit.body.scope].append(commit)
b07d2e 208             else:
0d5361 209                 commitlistByScope[commit.body.scope] = [commit]
b07d2e 210         
0d5361 211         fileTemplate.append("\n### " + commitsByType[0])
D 212         while len(commitlistByScope) > 0:
213             scope, commits = commitlistByScope.popitem()
b07d2e 214             fileTemplate.append("- *" + str(scope) + "*")
DF 215             for commit in commits:
0d5361 216                 if commitsByType[0] == "Other":
b07d2e 217                     fileTemplate.append("    - (" + commit.body.commitType + ") " + commit.body.subject + commit.appendShortHash())
DF 218                 else:
219                     fileTemplate.append("    - " + commit.body.subject + commit.appendShortHash())
b2e728 220         if len(noScope) > 0:
DF 221             fileTemplate.append("- *no scope*")
222         for commit in noScope:
223             fileTemplate.append("    - " + commit.body.subject + commit.appendShortHash())
b07d2e 224     
DF 225     # nonconform commits
226     if len(nonconformCommits) > 0:
227         fileTemplate.append("\n### Non-conform commits")
228         for commit in nonconformCommits:
229             fileTemplate.append("- " + commit.body.subject + commit.appendShortHash())
230
231
232 # write into changelog
1f69ad 233 with open(outputPath + "/changelog.md", "w") as file:
b07d2e 234     for line in fileTemplate:
DF 235         file.write(line + "\n")