Newer
Older
#!/usr/bin/env python3
"""
Simple changelog generator for Garz&Fricke gitlab projects.
Queries merge request from gitlab and outputs as sorted list in
markdown format.
Releases are tags on the given branch in the manifest project.
Changes to list are merged mergerequests with the given branch
as target. Match with releases is done by the timestamp (merged_at)
in comparison with the tags timestamp.
"""
import argparse
import logging
import sys
import gitlab as gl
__author__ = "Jonas Höppner"
__email__ = "jonas.hoeppner@garz-fricke.com"
GITLAB_SERVER = "https://git.seco.com"
# ID of the guf_yocto group
GITLAB_GROUP_ID = "556"
DISTRO_PROJECT_ID = "1748"
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
MANIFEST_PROJECT_ID = "1725"
DEFAULTBRANCH = "dunfell"
GITLAB_TIMEFORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
TIMEFORMAT = "%Y-%m-%d %H:%M"
verbose = 0
def decode_timestamp(t):
timestamp = datetime.datetime.strptime(t, GITLAB_TIMEFORMAT)
return timestamp
class Project:
def __init__(self, project):
self.project = project
def __str__(self):
return "## Project " + self.project.name + "\n"
def withlink(self):
return (
"\n\n## Project [" + self.project.name + "](" + self.project.web_url + ")\n"
)
def __eq__(self, p):
if not p:
return False
return self.project.id == p.project.id
class Tag:
def __init__(self, tag):
self.name = tag.name
self.message = tag.message
self.commit = tag.commit
"""
The tags timestamp is a little more complicated it normally points
to the tagged commit's timestamps. But the merge happens later.
To handle this, the relelated mergerequest is found by comparing the
sha's and also take the merged_at timestamp.
"""
self.timestamp = decode_timestamp(tag.commit["created_at"])
"""
The mr which introduced the taged commit
as gitlab-python does not support the V5 API yet
this is added later when traversing the mrs anyway
with V5 Api: https://docs.gitlab.com/ee/api/commits.html#list-merge-requests-associated-with-a-commit
"""
self.mergerequest = None
logging.debug(self.name + " -- " + self.commit["id"])
def __str__(self):
return self.name + " " + self.timestamp.strftime(TIMEFORMAT)
def add_mergerequest(self, m):
if self.mergerequest:
return
if m.mr.sha == self.commit["id"]:
self.mergerequest = m
# Update timestamp
# The tag points to the commit, but the merge of the merge request may has happend later
# as the commit, so the merged_at date is relevant. Otherwise the tagged commit and may be
# more end up in the wrong release
new_timestamp = decode_timestamp(self.mergerequest.mr.merged_at)
logging.debug("Found matching merge request for %s", self)
logging.debug(" - %s", self.timestamp.strftime(TIMEFORMAT))
logging.debug(" - %s", new_timestamp.strftime(TIMEFORMAT))
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
self.timestamp = new_timestamp
def header(self):
return (
"\n\n\n# Release "
+ self.name
+ "\n\nreleased at "
+ self.timestamp.strftime(TIMEFORMAT)
+ "\n\n"
)
class DummyTag:
def __init__(
self, name, message, date=datetime.datetime.now(tz=datetime.timezone.utc)
):
self.name = name
self.message = message
self.timestamp = date
def header(self):
return "\n\n\n# " + self.name + "\n\n"
def add_mergerequest(self, m):
# Needed as interface but does nothing
pass
class Release:
"""Store some release data"""
def __init__(self, tag):
self.tag = tag
self.mergerequests = []
def add_mergerequest(self, m):
# Check if this merge_request is related to the tag
self.tag.add_mergerequest(m)
# Adds a mergerequest to the project, but uses some filtering
# Ignore automated merge requests
if m.mr.author["username"] == "guf-gitbot":
return False
if m.mr.author["username"] == "gitbot":
return False
# With the movement to git.seco.com the MRs owned by
# the guf-gitbot have been transfered to tobias
# As it is not possible to change the owner back
# to gitbot we need an extra filter here on the
# branch name
if m.mr.source_branch.startswith("integrate/"):
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
return False
# Timestamp is not in this release
if self.tag.timestamp < m.timestamp:
return False
# Remove duplicates, don't print the same title
# twice in the same project and release
if any(
a.mr.title == m.mr.title and a.project == m.project
for a in self.mergerequests
):
return True
self.mergerequests.append(m)
return True
def header(self):
return self.tag.header()
def description(self):
m = self.tag.message
if not m:
return ""
return m
def __str__(self):
return self.tag.name
class MergeRequest:
def __init__(self, mr, p):
self.mr = mr
self.project = p
self.timestamp = decode_timestamp(self.mr.merged_at)
logging.debug("\nMergeRequest:")
logging.debug(mr)
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def __str__(self):
return self.mr.title
def withlink(self):
out = self.mr.title + " [" + self.mr.reference + "](" + self.mr.web_url + ")"
return out
def main(args):
parser = argparse.ArgumentParser(description=__doc__, usage="%(prog)s [OPTIONS]")
parser.add_argument(
"--gitlab-url",
help="""URL to the GitLab instance""",
dest="gitlab_url",
action="store",
default=GITLAB_SERVER,
)
parser.add_argument(
"--token",
help="""GitLab REST API private access token""",
dest="token",
required=True,
)
parser.add_argument(
"-b",
"--branch",
action="store",
dest="branch",
default=DEFAULTBRANCH,
help=("Specify the branch to work on, default is dunfell."),
)
parser.add_argument(
"-v",
"--verbose",
action="count",
dest="verbose",
default=0,
help=("Increase verbosity."),
)
options = parser.parse_args(args)
if options.verbose:
logging.basicConfig(level=logging.DEBUG)
logging.debug(options)
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
gitlab = gl.Gitlab(options.gitlab_url, private_token=options.token)
# Speed up, complete project lookup takes much longer
# then specifying the ID directly
distro = Project(gitlab.projects.get(DISTRO_PROJECT_ID))
machine = Project(gitlab.projects.get(MACHINE_PROJECT_ID))
manifest = Project(gitlab.projects.get(MANIFEST_PROJECT_ID))
releases = []
for t in manifest.project.tags.list(search=options.branch):
releases.append(Release(Tag(t)))
# Add dummy release with date today for new untaged commits
releases.append(
Release(
DummyTag(
"Not yet released",
"Merged Request already merged into "
+ options.branch
+ " but not yet released.",
)
)
)
# Sort by date, oldest first
releases = sorted(releases, key=lambda d: d.tag.timestamp, reverse=False)
for p in [manifest, distro, machine]:
for mr in p.project.mergerequests.list(
scope="all", state="merged", target_branch=options.branch, all=True
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
):
m = MergeRequest(mr, p)
for r in releases:
if r.add_mergerequest(m):
break
# Sort by date, newest first
releases = sorted(releases, key=lambda d: d.tag.timestamp, reverse=True)
for r in releases:
# Don't show empty releases/tags
if not len(r.mergerequests):
continue
print(r.header())
print(r.description())
current_project = None
for m in r.mergerequests:
if m.project != current_project:
current_project = m.project
print(current_project.withlink())
print(" - ", m.withlink())
if __name__ == "__main__":
main(sys.argv[1:])