diff options
Diffstat (limited to 'scripts/rfr')
-rwxr-xr-x | scripts/rfr | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/scripts/rfr b/scripts/rfr new file mode 100755 index 00000000..1b12a9ce --- /dev/null +++ b/scripts/rfr | |||
@@ -0,0 +1,243 @@ | |||
1 | #!/usr/bin/python | ||
2 | # | ||
3 | # Simple script for formatting RFR messages for nvgpu changes. | ||
4 | # | ||
5 | |||
6 | import os | ||
7 | import re | ||
8 | import json | ||
9 | import argparse | ||
10 | import subprocess | ||
11 | |||
12 | VERSION = '1.0.0' | ||
13 | |||
14 | # Gerrit commit URL formats. These are regular expressions to match the | ||
15 | # incoming URLs against. | ||
16 | gr_fmts = [ r'http://git-master/r/(\d+)', | ||
17 | r'http://git-master/r/#/c/(\d+)/', | ||
18 | r'http://git-master.nvidia.com/r/(\d+)', | ||
19 | r'http://git-master.nvidia.com/r/#/c/(\d+)/', | ||
20 | r'https://git-master/r/(\d+)', | ||
21 | r'https://git-master/r/#/c/(\d+)/', | ||
22 | r'https://git-master.nvidia.com/r/(\d+)', | ||
23 | r'https://git-master.nvidia.com/r/#/c/(\d+)/' ] | ||
24 | |||
25 | # The user to use. May be overridden but the default comes from the environment. | ||
26 | user = os.environ['USER'] | ||
27 | |||
28 | # Gerrit query command to obtain the patch URL. The substitution will be the | ||
29 | # gerrit Change-ID parsed from the git commit message. | ||
30 | gr_query_cmd = 'ssh %s@git-master -p 29418 gerrit query --format json %s' | ||
31 | |||
32 | def parse_args(): | ||
33 | """ | ||
34 | Parse arguments to rfr. | ||
35 | """ | ||
36 | |||
37 | ep="""This program will format commit messages into something that can be | ||
38 | sent to the nvgpu mailing list for review | ||
39 | """ | ||
40 | help_msg="""Git or gerrit commits to describe. Can be either a git or gerrit | ||
41 | commit ID. If the ID starts with a 'I' then it will be treated as a gerrit ID. | ||
42 | If the commit ID looks like a gerrit URL then it is treated as a gerrit URL. | ||
43 | Otherwise it's treated as a git commit ID. | ||
44 | """ | ||
45 | parser = argparse.ArgumentParser(description='RFR formatting tool', | ||
46 | epilog=ep) | ||
47 | |||
48 | parser.add_argument('-V', '--version', action='store_true', default=False, | ||
49 | help='print the program version') | ||
50 | parser.add_argument('-m', '--msg', action='store', default=None, | ||
51 | help='Custom message to add to the RFR email') | ||
52 | |||
53 | # Positionals: the gerrit URLs. | ||
54 | parser.add_argument('commits', metavar='Commit-IDs', | ||
55 | nargs='+', | ||
56 | help=help_msg) | ||
57 | |||
58 | arg_parser = parser.parse_args() | ||
59 | |||
60 | return arg_parser | ||
61 | |||
62 | def get_gerrit_url_id(cmt): | ||
63 | """ | ||
64 | Determines if the passed cmt is a gerrit commit URL. If it is then this | ||
65 | returns the URL ID; otherwise it returns None. | ||
66 | """ | ||
67 | |||
68 | for fmt in gr_fmts: | ||
69 | p = re.compile(fmt) | ||
70 | m = p.search(cmt) | ||
71 | if m: | ||
72 | return m.group(1) | ||
73 | |||
74 | return None | ||
75 | |||
76 | def gerrit_query(change): | ||
77 | """ | ||
78 | Query gerrit for the JSON change information. Return a python object | ||
79 | describing the JSON data. | ||
80 | |||
81 | change can either be a Change-Id or the numeric change number from a URL. | ||
82 | |||
83 | Note there is an interesting limitation with this: gerrit can have multiple | ||
84 | changes with the same Change-Id (./sigh). So if you query a change ID that | ||
85 | points to multiple changes you get back all of them. | ||
86 | |||
87 | This script just uses the first. Ideally one could filter by branch or by | ||
88 | some other distinguishing factor. | ||
89 | """ | ||
90 | |||
91 | query_cmd = gr_query_cmd % (user, change) | ||
92 | |||
93 | prog = subprocess.Popen(query_cmd, shell=True, | ||
94 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
95 | |||
96 | stdout_data, stderr_data = prog.communicate() | ||
97 | if prog.returncode != 0: | ||
98 | print('`%s\' failed!' % query_cmd) | ||
99 | return False | ||
100 | |||
101 | commit = json.loads(stdout_data.decode('utf-8').splitlines()[0]) | ||
102 | if 'id' not in commit: | ||
103 | print('%s is not a gerrit commit!?' % change) | ||
104 | print('Most likely you need to push the change.') | ||
105 | return None | ||
106 | |||
107 | return commit | ||
108 | |||
109 | def commit_info_from_gerrit_change_id(change_id): | ||
110 | """ | ||
111 | Return a dict with all the gerrit info from a gerrit change ID. | ||
112 | """ | ||
113 | |||
114 | return gerrit_query(change_id) | ||
115 | |||
116 | def gerrit_change_id_from_gerrit_cl(cmt): | ||
117 | """ | ||
118 | Return the gerrit Change-Id from the passed Gerrit URL. | ||
119 | """ | ||
120 | |||
121 | cl = get_gerrit_url_id(cmt) | ||
122 | if not cl: | ||
123 | return None | ||
124 | |||
125 | commit = gerrit_query(cl) | ||
126 | if not commit: | ||
127 | return None | ||
128 | |||
129 | return commit['id'] | ||
130 | |||
131 | def gerrit_change_id_from_git_commit(cmt_id): | ||
132 | """ | ||
133 | Return the gerrit Change-Id from the passed git cmt_id. Returns None if | ||
134 | this doesn't appear to be a cmt_id or doesn't have a Change-Id line. | ||
135 | """ | ||
136 | |||
137 | cid_re = re.compile(r'Change-Id: (I[a-z0-9]{40})') | ||
138 | |||
139 | # First obtain the commit message itself. | ||
140 | prog = subprocess.Popen('git show --stat %s' % cmt_id, shell=True, | ||
141 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
142 | |||
143 | stdout_data, stderr_data = prog.communicate() | ||
144 | if prog.returncode != 0: | ||
145 | print('`git show %s\' failed?!' % cmt_id) | ||
146 | return None | ||
147 | |||
148 | m = cid_re.search(stdout_data.decode('utf-8')) | ||
149 | if m: | ||
150 | return m.group(1) | ||
151 | |||
152 | return None | ||
153 | |||
154 | def indent_lines(text, ind): | ||
155 | """ | ||
156 | Prepend each new line in the passed text with ind. | ||
157 | """ | ||
158 | return ''.join(ind + l + '\n' for l in text.splitlines()) | ||
159 | |||
160 | def display_commits(commits, extra_message): | ||
161 | """ | ||
162 | Takes a list of the commits to print. | ||
163 | """ | ||
164 | |||
165 | whole_template = """ | ||
166 | Hi All, | ||
167 | |||
168 | I would like you to review the following changes. | ||
169 | {extra_message} | ||
170 | {cmt_descriptions} | ||
171 | Thanks! | ||
172 | {cmt_verbose}""" | ||
173 | |||
174 | cmt_template = """ | ||
175 | +---------------------------------------- | ||
176 | | {url} | ||
177 | | {subject} | ||
178 | | Author: {author} | ||
179 | |||
180 | {cmtmsg}""" | ||
181 | |||
182 | commits_info = [ ] | ||
183 | for cmt in commits: | ||
184 | commits_info.append(commit_info_from_gerrit_change_id(cmt)) | ||
185 | |||
186 | cmt_descriptions = '' | ||
187 | for c in commits_info: | ||
188 | cmt_descriptions += " %s - %s\n" % (c['url'], c['subject']) | ||
189 | print(cmt_descriptions) | ||
190 | |||
191 | # Add new lines around the extra_message, if applicable. Otherwise we don't | ||
192 | # want anything to show up for extra_message. | ||
193 | if extra_message: | ||
194 | extra_message = '\n%s\n' % extra_message | ||
195 | else: | ||
196 | extra_message = '' | ||
197 | |||
198 | cmt_verbose = '' | ||
199 | for c in commits_info: | ||
200 | cmt_verbose += cmt_template.format(url=c['url'], subject=c['subject'], | ||
201 | author=c['owner']['name'], | ||
202 | cmtmsg=indent_lines( | ||
203 | c['commitMessage'], ' ')) | ||
204 | |||
205 | print(whole_template.format(cmt_descriptions=cmt_descriptions, | ||
206 | extra_message=extra_message, | ||
207 | cmt_verbose=cmt_verbose)) | ||
208 | |||
209 | def main(): | ||
210 | """ | ||
211 | The magic happens here. | ||
212 | """ | ||
213 | |||
214 | arg_parser = parse_args() | ||
215 | commits = [ ] | ||
216 | |||
217 | if arg_parser.version: | ||
218 | print('Version: %s' % VERSION) | ||
219 | exit(0) | ||
220 | |||
221 | # Builds a dictionary of Gerrit Change-Ids. From the Change-Ids we can then | ||
222 | # get the commit message and URL. | ||
223 | # | ||
224 | # This also builds an array of those same commit IDs to track the ordering | ||
225 | # of the commits so that the user can choose the order of the patches based | ||
226 | # on the order in which they pass the commits. | ||
227 | for cmt in arg_parser.commits: | ||
228 | if cmt[0] == 'I': | ||
229 | cid = cmt | ||
230 | elif get_gerrit_url_id(cmt): | ||
231 | cid = gerrit_change_id_from_gerrit_cl(cmt) | ||
232 | else: | ||
233 | cid = gerrit_change_id_from_git_commit(cmt) | ||
234 | |||
235 | if cid: | ||
236 | commits.append(cid) | ||
237 | else: | ||
238 | print('Warning: \'%s\' doesn\'t appear to be a commit!' % cmt) | ||
239 | |||
240 | display_commits(commits, arg_parser.msg) | ||
241 | |||
242 | if __name__ == '__main__': | ||
243 | main() | ||