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 |
|
37d983
|
138 |
inputPath = input("Please enter the base path of the repository: ").strip() |
D |
139 |
userDecision = input("Should the generated changelog be stored in another location (y/n)? ").lower().strip() |
1f69ad
|
140 |
if userDecision == "y": |
37d983
|
141 |
outputPath = input("Please enter the output path: ").strip() |
1f69ad
|
142 |
elif userDecision == "n": |
D |
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") |