]> git.proxmox.com Git - ceph.git/blame - ceph/src/script/ceph-release-notes
buildsys: fix parallel builds
[ceph.git] / ceph / src / script / ceph-release-notes
CommitLineData
7c673cae
FG
1#!/usr/bin/env python
2# Originally modified from A. Israel's script seen at
3# https://gist.github.com/aisrael/b2b78d9dfdd176a232b9
4"""To run this script first install the dependencies
5
6
7 virtualenv v
8 source v/bin/activate
9 pip install githubpy GitPython requests
10
11Generate a github access token; this is needed as the anonymous access
12to Github's API will easily hit the limit even with a single invocation.
13For details see:
14https://help.github.com/articles/creating-an-access-token-for-command-line-use/
15
16Next either set the github token as an env variable
17`GITHUB_ACCESS_TOKEN` or alternatively invoke the script with
18`--token` switch.
19
20Example:
21
22 ceph-release-notes -r tags/v0.87..origin/giant \
23 $(git rev-parse --show-toplevel)
24
25"""
26
27from __future__ import print_function
28import argparse
29import github
30import os
31import re
32import sys
33import requests
34
35from git import Repo
36
37
38fixes_re = re.compile(r"Fixes\:? #(\d+)")
39# labels is the list of relevant labels defined for github.com/ceph/ceph
40labels = ['bluestore', 'build/ops', 'cephfs', 'common', 'core', 'mgr',
41 'mon', 'performance', 'pybind', 'rdma', 'rgw', 'rbd', 'tests',
42 'tools']
43merge_re = re.compile("Merge pull request #(\d+).*")
44# prefixes is the list of commit description prefixes we recognize
45prefixes = ['bluestore', 'build/ops', 'cephfs', 'cephx', 'cli', 'cmake',
46 'common', 'core', 'crush', 'doc', 'fs', 'librados', 'librbd',
47 'log', 'mds', 'mgr', 'mon', 'msg', 'objecter', 'osd', 'pybind',
48 'rbd', 'rbd-mirror', 'rbd-nbd', 'rgw', 'tests', 'tools']
49signed_off_re = re.compile("Signed-off-by: (.+) <")
50tracker_re = re.compile("http://tracker.ceph.com/issues/(\d+)")
51tracker_uri = "http://tracker.ceph.com/issues/{0}.json"
52
53
54def get_original_issue(issue, verbose):
55 r = requests.get(tracker_uri.format(issue),
56 params={"include": "relations"}).json()
57
58 # looking up for the original issue only makes sense
59 # when dealing with an issue in the Backport tracker
60 if r["issue"]["tracker"]["name"] != "Backport":
61 if verbose:
62 print ("http://tracker.ceph.com/issues/" + issue +
63 " is from the tracker " + r["issue"]["tracker"]["name"] +
64 ", do not look for the original issue")
65 return issue
66
67 # if a Backport issue does not have a relation, keep it
68 if "relations" not in r["issue"]:
69 if verbose:
70 print ("http://tracker.ceph.com/issues/" + issue +
71 " has no relations, do not look for the original issue")
72 return issue
73
74 copied_to = [
75 str(i['issue_id']) for i in r["issue"]["relations"]
76 if i["relation_type"] == "copied_to"
77 ]
78 if copied_to:
79 if len(copied_to) > 1:
80 if verbose:
81 print ("ERROR: http://tracker.ceph.com/issues/" + issue +
82 " has more than one Copied To relation")
83 return issue
84 if verbose:
85 print ("http://tracker.ceph.com/issues/" + issue +
86 " is the backport of http://tracker.ceph.com/issues/" +
87 copied_to[0])
88 return copied_to[0]
89 else:
90 if verbose:
91 print ("http://tracker.ceph.com/issues/" + issue +
92 " has no copied_to relations; do not look for the" +
93 " original issue")
94 return issue
95
96
97def split_component(title, gh, number):
98 title_re = '(' + '|'.join(prefixes) + ')(:.*)'
99 match = re.match(title_re, title)
100 if match:
101 return match.group(1)+match.group(2)
102 else:
103 issue = gh.repos("ceph")("ceph").issues(number).get()
104 issue_labels = {it['name'] for it in issue['labels']}
105 if 'documentation' in issue_labels:
106 return 'doc: ' + title
107 item = labels.intersection(issue_labels)
108 if item:
109 return ",".join(item) + ': ' + title
110 else:
111 return 'UNKNOWN: ' + title
112
113
114def make_release_notes(gh, repo, ref, plaintext, verbose, strict, use_tags):
115
116 issue2prs = {}
117 pr2issues = {}
118 pr2info = {}
119
120 for commit in repo.iter_commits(ref, merges=True):
121 merge = merge_re.match(commit.summary)
122 if merge:
123 number = merge.group(1)
124 print ("Considering PR#" + number)
125 # do not pick up ceph/ceph-qa-suite.git PRs
126 if int(number) < 1311:
127 print ("Ignoring low-numbered PR, probably picked up from"
128 " ceph/ceph-qa-suite.git")
129 continue
130 pr = gh.repos("ceph")("ceph").pulls(number).get()
131 title = pr['title']
132 message_lines = commit.message.split('\n')
133 if not strict and len(message_lines) > 1:
134 lines = []
135 for line in message_lines[1:]:
136 if 'Reviewed-by' in line:
137 continue
138 line = line.strip()
139 if line:
140 lines.append(line)
141 duplicates_pr_title = lines[0] == pr['title'].strip()
142 if duplicates_pr_title:
143 lines.pop(0)
144 if len(lines) == 0:
145 if duplicates_pr_title:
146 message = None
147 elif len(lines) == 1:
148 # assume that a single line means the intention is to
149 # re-write the PR title
150 title = lines[0]
151 message = None
152 else:
153 message = " " + "\n ".join(lines)
154 else:
155 message = None
156 issues = []
157 if pr['body']:
158 issues = fixes_re.findall(pr['body']) + tracker_re.findall(
159 pr['body']
160 )
161
162 authors = {}
163 for c in repo.iter_commits(
164 "{sha1}^1..{sha1}^2".format(sha1=commit.hexsha)
165 ):
166 for author in re.findall(
167 "Signed-off-by:\s*(.*?)\s*<", c.message
168 ):
169 authors[author] = 1
170 issues.extend(fixes_re.findall(c.message) +
171 tracker_re.findall(c.message))
172 if authors:
173 author = ", ".join(authors.keys())
174 else:
175 author = commit.parents[-1].author.name
176
177 if strict and not issues:
178 print ("ERROR: https://github.com/ceph/ceph/pull/" +
179 str(number) + " has no associated issue")
180 continue
181
182 if strict:
183 title_re = (
184 '^(?:hammer|infernalis|jewel|kraken):\s+(' +
185 '|'.join(prefixes) +
186 ')(:.*)'
187 )
188 match = re.match(title_re, title)
189 if not match:
190 print ("ERROR: https://github.com/ceph/ceph/pull/" +
191 str(number) + " title " + title.encode("utf-8") +
192 " does not match " + title_re)
193 else:
194 title = match.group(1) + match.group(2)
195 if use_tags:
196 title = split_component(title, gh, number)
197
198 title = title.strip(' \t\n\r\f\v\.\,\;\:\-\=')
199 pr2info[number] = (author, title, message)
200
201 for issue in set(issues):
202 if strict:
203 issue = get_original_issue(issue, verbose)
204 issue2prs.setdefault(issue, set([])).add(number)
205 pr2issues.setdefault(number, set([])).add(issue)
206 sys.stdout.write('.')
207
208 print (" done collecting merges.")
209
210 if strict:
211 for (issue, prs) in issue2prs.iteritems():
212 if len(prs) > 1:
213 print (">>>>>>> " + str(len(prs)) + " pr for issue " +
214 issue + " " + str(prs))
215
216 for (pr, (author, title, message)) in sorted(
217 pr2info.iteritems(), key=lambda (k, v): (v[2], v[1])
218 ):
219 if pr in pr2issues:
220 if plaintext:
221 issues = map(lambda issue: '#' + str(issue), pr2issues[pr])
222 else:
223 issues = map(lambda issue: (
224 '`issue#{issue} <http://tracker.ceph.com/issues/{issue}>`_'
225 ).format(issue=issue), pr2issues[pr]
226 )
227 issues = ", ".join(issues) + ", "
228 else:
229 issues = ''
230 if plaintext:
231 print ("* {title} ({issues}{author})".format(
232 title=title.encode("utf-8"),
233 issues=issues,
234 author=author.encode("utf-8")
235 )
236 )
237 else:
238 print (
239 (
240 "* {title} ({issues}`pr#{pr} <"
241 "https://github.com/ceph/ceph/pull/{pr}"
242 ">`_, {author})"
243 ).format(
244 title=title.encode("utf-8"),
245 issues=issues,
246 author=author.encode("utf-8"), pr=pr
247 )
248 )
249 if message:
250 print (message)
251
252
253if __name__ == "__main__":
254 desc = '''
255 Make ceph release notes for a given revision. Eg usage:
256
257 $ ceph-release-notes -r tags/v0.87..origin/giant \
258 $(git rev-parse --show-toplevel)
259
260 It is recommended to set the github env. token in order to avoid
261 hitting the api rate limits.
262 '''
263
264 parser = argparse.ArgumentParser(
265 description=desc,
266 formatter_class=argparse.RawTextHelpFormatter
267 )
268
269 parser.add_argument("--rev", "-r",
270 help="git revision range for creating release notes")
271 parser.add_argument("--text", "-t",
272 action='store_true', default=None,
273 help="output plain text only, no links")
274 parser.add_argument("--verbose", "-v",
275 action='store_true', default=None,
276 help="verbose")
277 parser.add_argument("--strict",
278 action='store_true', default=None,
279 help="strict, recommended only for backport releases")
280 parser.add_argument("repo", metavar="repo",
281 help="path to ceph git repo")
282 parser.add_argument(
283 "--token",
284 default=os.getenv("GITHUB_ACCESS_TOKEN"),
285 help="Github Access Token ($GITHUB_ACCESS_TOKEN otherwise)",
286 )
287 parser.add_argument("--use-tags", default=False,
288 help="Use github tags to guess the component")
289
290 args = parser.parse_args()
291 gh = github.GitHub(
292 access_token=args.token)
293
294 make_release_notes(
295 gh,
296 Repo(args.repo),
297 args.rev,
298 args.text,
299 args.verbose,
300 args.strict,
301 args.use_tags
302 )