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