stable-rt-tools 2.0__tar.gz → 2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/PKG-INFO +1 -1
  2. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/about.py +1 -1
  3. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/commit-tmpl.txt +2 -0
  4. stable_rt_tools-2.1/stable_rt_tools/srt_announce.py +377 -0
  5. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/PKG-INFO +1 -1
  6. stable_rt_tools-2.0/stable_rt_tools/srt_announce.py +0 -255
  7. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/README.rst +0 -0
  8. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/pyproject.toml +0 -0
  9. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/setup.cfg +0 -0
  10. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/__init__.py +0 -0
  11. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/announce-srt-rc.txt +0 -0
  12. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/announce-srt.txt +0 -0
  13. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/py.typed +0 -0
  14. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt.py +0 -0
  15. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_commit.py +0 -0
  16. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_config.py +0 -0
  17. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_create.py +0 -0
  18. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_prep.py +0 -0
  19. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_push.py +0 -0
  20. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_quilt.py +0 -0
  21. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_setup.py +0 -0
  22. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_sign.py +0 -0
  23. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_status.py +0 -0
  24. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_tag.py +0 -0
  25. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_upload.py +0 -0
  26. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_util.py +0 -0
  27. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_util_context.py +0 -0
  28. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools/srt_util_tag.py +0 -0
  29. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/SOURCES.txt +0 -0
  30. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/dependency_links.txt +0 -0
  31. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/entry_points.txt +0 -0
  32. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/requires.txt +0 -0
  33. {stable_rt_tools-2.0 → stable_rt_tools-2.1}/stable_rt_tools.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stable-rt-tools
3
- Version: 2.0
3
+ Version: 2.1
4
4
  Summary: Support out of tree patch workflows
5
5
  Author-email: Daniel Wagner <dwagner@suse.de>
6
6
  License-Expression: MIT
@@ -21,4 +21,4 @@
21
21
  # SOFTWARE.
22
22
  """The stable-rt-tools version, the single source for pyproject.toml."""
23
23
 
24
- __version__ = '2.0'
24
+ __version__ = '2.1'
@@ -7,6 +7,8 @@ I'm pleased to announce the {new_version} stable release.
7
7
  Changes since {old_version}:
8
8
 
9
9
  {series_changes}
10
+ The delta patch against {old_version} is appended below.
11
+
10
12
  You can get this release via the git tree at:
11
13
 
12
14
  https://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-stable-rt.git {release_tag}
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # srt - stable rt tooling
4
+ #
5
+ # Copyright (c) Siemens AG, 2018
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE
24
+
25
+
26
+ import argparse
27
+ import os
28
+ import re
29
+ import sys
30
+ from datetime import date, timedelta
31
+ from email.utils import make_msgid
32
+ from subprocess import DEVNULL, PIPE, run
33
+ from time import gmtime, strftime
34
+
35
+ from stable_rt_tools.srt_util import (
36
+ Config,
37
+ SubParsers,
38
+ check_context,
39
+ cmd,
40
+ confirm,
41
+ get_config,
42
+ get_gpg_fingerprint,
43
+ get_remote_branch_name,
44
+ get_suffix,
45
+ get_template_path,
46
+ )
47
+ from stable_rt_tools.srt_util_context import SrtContext
48
+
49
+ REVIEW_TEXT = """\
50
+ {tag} stable review patch.
51
+ If anyone has any objections, please let me know.
52
+
53
+ -----------
54
+
55
+ """
56
+
57
+
58
+ def add_review_text(path: str, tag: str) -> None:
59
+ """Put the review note at the top of the body of a patch mail."""
60
+ with open(path) as f:
61
+ headers, sep, body = f.read().partition('\n\n')
62
+ with open(path, 'w') as f:
63
+ f.write(headers + sep + REVIEW_TEXT.format(tag=tag) + body)
64
+
65
+
66
+ MERGE_NOTE = """\
67
+ The resolution of the merge conflicts, as shown by
68
+ git show --remerge-diff: the conflicts git left in the merge on the
69
+ - lines, the merge as committed on the + lines.
70
+
71
+ """
72
+
73
+
74
+ def resolved_merges(ref: str) -> list[str]:
75
+ """The merges of the release candidate with conflict resolutions.
76
+
77
+ git format-patch leaves out merges, but the conflict resolution of
78
+ a stable merge is a change to the series which needs review.
79
+ """
80
+ merges = cmd(['git', 'rev-list', '--reverse', '--first-parent',
81
+ '--merges', f'{ref}..']).split()
82
+ # a clean merge has no remerge diff
83
+ return [m for m in merges
84
+ if cmd(['git', 'show', '--remerge-diff', '--format=', m])]
85
+
86
+
87
+ def mail_subject(text: str, prefix: str) -> str:
88
+ """Number the subject of a mail with prefix, e.g. 'PATCH RT 1/3'."""
89
+ return re.sub(r'^Subject: \[[^]\n]*\]', f'Subject: [{prefix}]', text,
90
+ count=1, flags=re.MULTILINE)
91
+
92
+
93
+ def merge_mail(merge: str) -> tuple[str, str]:
94
+ """The file name slug and the mail of a merge, like git format-patch."""
95
+ mail = cmd(['git', 'show', '--format=email', '--remerge-diff',
96
+ '--stat', '--patch', merge]) + '\n'
97
+ head, sep, rest = mail.partition('\n---\n')
98
+ subject = cmd(['git', 'show', '-s', '--format=%s', merge])
99
+ slug = re.sub(r'[^A-Za-z0-9_]+', '-', subject).strip('-')[:64]
100
+ return slug, head + sep + MERGE_NOTE + rest
101
+
102
+
103
+ def series_summary(ref: str, commits: list[str]) -> str:
104
+ """The shortlog and diffstat of the series for the cover letter.
105
+
106
+ git format-patch neither lists the merges nor gets the diffstat
107
+ right across a merge: it would count the stable update in.
108
+ """
109
+ authors: dict[str, list[str]] = {}
110
+ for c in commits:
111
+ author, subject = cmd(['git', 'show', '-s', '--format=%an%x00%s',
112
+ c]).split('\0', 1)
113
+ authors.setdefault(author, []).append(subject)
114
+ text = ''
115
+ for author, subjects in authors.items():
116
+ text += f'{author} ({len(subjects)}):\n'
117
+ text += ''.join(f' {s}\n' for s in subjects) + '\n'
118
+ if cmd(['git', 'rev-list', '--first-parent', '--merges', f'{ref}..']):
119
+ # a merge with conflicts has a mail of its own
120
+ return text + 'The diffstat is left out, it would contain the ' \
121
+ 'stable update.\n'
122
+ return text + ' ' + cmd(['git', 'diff', '--stat', f'{ref}..']) + '\n'
123
+
124
+
125
+ def rewrite_cover(cover: str, prefix: str, total: int, summary: str) -> None:
126
+ with open(cover) as f:
127
+ text = f.read()
128
+ text = mail_subject(text, f'{prefix} 0/{total}')
129
+ blurb = '*** BLURB HERE ***\n'
130
+ head, sep, rest = text.partition(blurb)
131
+ signature = rest[rest.rfind('\n-- \n'):] if '\n-- \n' in rest else ''
132
+ with open(cover, 'w') as f:
133
+ f.write(head + sep + '\n' + summary + signature)
134
+
135
+
136
+ def add_merge_mails(mails: list[str], ref: str, merges: list[str],
137
+ prefix: str) -> list[str]:
138
+ """Put the merge mails into the series in commit order.
139
+
140
+ The patches of git format-patch are numbered again, the series has
141
+ more mails now, and the cover letter lists all of them.
142
+ """
143
+ order = cmd(['git', 'rev-list', '--reverse', '--first-parent',
144
+ f'{ref}..']).split()
145
+
146
+ cover = mails[0]
147
+ by_commit: dict[str, tuple[str, str]] = {}
148
+ for path in mails[1:]:
149
+ with open(path) as f:
150
+ text = f.read()
151
+ # format-patch starts a mail with 'From <commit> <date>'
152
+ by_commit[text.split()[1]] = (os.path.basename(path)[5:], text)
153
+ os.remove(path)
154
+ for m in merges:
155
+ slug, text = merge_mail(m)
156
+ by_commit[m] = (slug + '.patch', text)
157
+
158
+ commits = [c for c in order if c in by_commit]
159
+ series = [by_commit[c] for c in commits]
160
+ total = len(series)
161
+ directory = os.path.dirname(cover)
162
+ result = [cover]
163
+ for n, (name, text) in enumerate(series, start=1):
164
+ path = os.path.join(directory, f'{n:04d}-{name}')
165
+ with open(path, 'w') as f:
166
+ f.write(mail_subject(text, f'{prefix} {n}/{total}'))
167
+ result.append(path)
168
+
169
+ rewrite_cover(cover, prefix, total, series_summary(ref, commits))
170
+ return result
171
+
172
+
173
+ def create_rc_patches(config: Config, ctx: SrtContext) -> None:
174
+ """Mail everything added to the series since the last release.
175
+
176
+ These are the commits on the series' own line (--first-parent):
177
+ patches before and after the stable merge, the merge itself if it
178
+ has a conflict resolution, the release commit. The commits of the
179
+ stable releases merged in are reviewed elsewhere.
180
+ """
181
+ ref = str(ctx.old_tag)
182
+ prefix = f'PATCH {get_suffix(config).upper()}'
183
+ mails = cmd(['git', 'format-patch', '--first-parent', f'{ref}..',
184
+ '-o', ctx.new_dir_mails, '--subject-prefix', prefix,
185
+ '--cover-letter']).splitlines()
186
+ mails = add_merge_mails(mails, ref, resolved_merges(ref), prefix)
187
+ for mail in mails:
188
+ if not mail.endswith('/0000-cover-letter.patch'):
189
+ add_review_text(mail, str(ctx.new_tag))
190
+
191
+
192
+ def cover_letter_replacements(config: Config,
193
+ ctx: SrtContext) -> dict[str, object]:
194
+ r = {
195
+ "mail_to": config['MAIL_TO'],
196
+ "major": ctx.new_tag.major,
197
+ "minor": ctx.new_tag.minor,
198
+ "patch": ctx.new_tag.patch,
199
+ "new_version": ctx.new_short_tag,
200
+ "old_version": ctx.old_short_tag,
201
+ "prj_dir": config['PRJ_DIR'],
202
+ "message_id": make_msgid(),
203
+ "sender": config['SENDER'],
204
+ "name": config['NAME'],
205
+ # new_tag_rt: the name templates know from before 2.0
206
+ "new_tag_rt": ctx.new_tag.number,
207
+ "new_tag_number": ctx.new_tag.number,
208
+ "gpg_key_fingerprint": get_gpg_fingerprint(config),
209
+ }
210
+ if ctx.is_rc:
211
+ r["new_tag_rc"] = ctx.new_tag.rc
212
+ return r
213
+
214
+
215
+ def write_rc_cover_letter(config: Config, ctx: SrtContext) -> None:
216
+ with open(ctx.new_dir_mails + '/0000-cover-letter.patch') as f:
217
+ coverletter = f.read()
218
+
219
+ coverletter = coverletter.replace('*** SUBJECT HERE ***',
220
+ 'Linux ' + str(ctx.new_tag))
221
+
222
+ rc_text = ''
223
+ with open(get_rc_templ_path(config)) as f:
224
+ rc_text = f.read()
225
+
226
+ r = cover_letter_replacements(config, ctx)
227
+
228
+ today = date.today()
229
+ rc_date = today + timedelta(weeks=1)
230
+ r["release_date"] = rc_date
231
+
232
+ rc_text = rc_text.format(**r)
233
+
234
+ coverletter = coverletter.replace('*** BLURB HERE ***',
235
+ rc_text)
236
+
237
+ with open(ctx.new_dir_mails + '/0000-cover-letter.patch', 'w') as f:
238
+ f.write(coverletter)
239
+
240
+
241
+ def send_rc_patches(config: Config, ctx: SrtContext,
242
+ args: argparse.Namespace) -> None:
243
+ gmd = ['git', 'send-email', '--confirm=never']
244
+ if args.suppress_cc:
245
+ gmd += ['--suppress-cc=all']
246
+ # an argument list, no shell: quotes would end up in the address
247
+ gmd += [f'--to={t.strip()}' for t in config['MAIL_TO'].split(',')]
248
+ gmd += [ctx.new_dir_mails]
249
+
250
+ print('Dry run')
251
+ gmdd = gmd.copy()
252
+ gmdd.insert(2, '--dry-run')
253
+ print(cmd(gmdd, verbose=True))
254
+ if confirm('OK to send patches?'):
255
+ cmd(gmd)
256
+
257
+
258
+ def announce_rc(config: Config, ctx: SrtContext,
259
+ args: argparse.Namespace) -> None:
260
+ create_rc_patches(config, ctx)
261
+ write_rc_cover_letter(config, ctx)
262
+ send_rc_patches(config, ctx, args)
263
+
264
+
265
+ def get_announce_tmpl_path(config: Config) -> str:
266
+ return get_template_path(config, 'ANNOUNCE', 'announce-srt.txt')
267
+
268
+
269
+ def get_rc_templ_path(config: Config) -> str:
270
+ return get_template_path(config, 'RC_TEXT', 'announce-srt-rc.txt')
271
+
272
+
273
+ def strip_trailers(msg: str) -> str:
274
+ """Remove the trailer block (Signed-off-by: ...) of a commit message."""
275
+ paragraphs = msg.rstrip().split('\n\n')
276
+ last = paragraphs[-1].splitlines()
277
+ if len(paragraphs) > 1 and all(
278
+ re.match(r'^[A-Za-z][A-Za-z0-9-]*: ', line) for line in last):
279
+ paragraphs = paragraphs[:-1]
280
+ return '\n\n'.join(paragraphs)
281
+
282
+
283
+ def delta_base(ctx: SrtContext) -> str:
284
+ """Where the delta of a release starts: the old release with the new
285
+ stable release merged in, as git merges it.
286
+
287
+ The delta is then the series' own change, also across a stable
288
+ update: patches before and after the merge, conflict resolutions
289
+ (as conflict markers which are removed) and the release commit, but
290
+ not the stable update.
291
+ """
292
+ old, base = str(ctx.old_tag), ctx.new_tag.base
293
+ if ctx.old_tag.base == base:
294
+ return old
295
+ p = run(['git', 'merge-tree', '--write-tree', old, base],
296
+ stdout=PIPE, stderr=DEVNULL, encoding='utf-8')
297
+ # 1: the merge has conflicts, the tree contains the conflict markers
298
+ if p.returncode not in (0, 1):
299
+ sys.exit(f'git merge-tree {old} {base} failed')
300
+ return p.stdout.splitlines()[0]
301
+
302
+
303
+ def print_changes(ctx: SrtContext) -> None:
304
+ """The shortlog, diffstat and delta of the release."""
305
+ new = str(ctx.new_tag)
306
+ base = delta_base(ctx)
307
+ print(cmd(['git', '--no-pager', 'shortlog', '--first-parent',
308
+ f'{ctx.old_tag}..{new}']))
309
+ print('---')
310
+ print(cmd(['git', '--no-pager', 'diff', '--stat', base, new]))
311
+ print('---')
312
+ print(cmd(['git', '--no-pager', 'diff', base, new]))
313
+
314
+
315
+ def quilt_announcement(ctx: SrtContext) -> str:
316
+ """The announcement of the queue commit, srt quilt queue wrote it."""
317
+ msg = cmd(['git', 'show', '-s', '--format=%B',
318
+ f'{ctx.new_tag}-patches^{{commit}}'])
319
+ subject, _, body = msg.partition('\n')
320
+ if not subject.startswith('[ANNOUNCE]'):
321
+ sys.exit(f'{ctx.new_tag}-patches: the commit message is not an '
322
+ 'announcement, it starts with ' + repr(subject))
323
+ return strip_trailers(body.lstrip('\n'))
324
+
325
+
326
+ def announce(config: Config, ctx: SrtContext,
327
+ args: argparse.Namespace) -> None:
328
+ from stable_rt_tools.srt_util import is_quilt_workflow
329
+ if ctx.is_rc:
330
+ announce_rc(config, ctx, args)
331
+ return
332
+
333
+ # rfc2822.html
334
+ # 3.3. Date and Time Specification
335
+ timestamp = strftime('%a, %d %b %Y %H:%M:%S -0000', gmtime())
336
+
337
+ if is_quilt_workflow(config):
338
+ print(f"From: {config.get('SENDER', '')}")
339
+ print(f"Subject: [ANNOUNCE] {ctx.new_tag}")
340
+ print(f"Date: {timestamp}")
341
+ print(f"Message-ID: {make_msgid()}")
342
+ print(f"To: {config.get('MAIL_TO', '')}")
343
+ print()
344
+ print(quilt_announcement(ctx))
345
+ print()
346
+ print('---')
347
+ print_changes(ctx)
348
+ return
349
+
350
+ stable_rt_text = ''
351
+ with open(get_announce_tmpl_path(config)) as f:
352
+ stable_rt_text = f.read()
353
+
354
+ r = cover_letter_replacements(config, ctx)
355
+
356
+ r["date"] = timestamp
357
+ r["branch_name"] = get_remote_branch_name()
358
+ r["branch_head"] = cmd(['git', 'rev-parse', 'HEAD'])
359
+
360
+ print(stable_rt_text.format(**r))
361
+ print_changes(ctx)
362
+
363
+
364
+ def add_argparser(parser: SubParsers) -> argparse.ArgumentParser:
365
+ prs = parser.add_parser('announce')
366
+ prs.add_argument('OLD_TAG', nargs='?')
367
+ prs.add_argument('NEW_TAG', nargs='?')
368
+ prs.add_argument('--suppress-cc', '-s', action="store_true", default=False,
369
+ help='Don''t auto-cc anyone (for testing)')
370
+ return prs
371
+
372
+
373
+ def execute(args: argparse.Namespace) -> None:
374
+ ctx = SrtContext(args)
375
+ check_context(ctx)
376
+
377
+ announce(get_config(), ctx, args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stable-rt-tools
3
- Version: 2.0
3
+ Version: 2.1
4
4
  Summary: Support out of tree patch workflows
5
5
  Author-email: Daniel Wagner <dwagner@suse.de>
6
6
  License-Expression: MIT
@@ -1,255 +0,0 @@
1
- #!/usr/bin/env python3
2
- #
3
- # srt - stable rt tooling
4
- #
5
- # Copyright (c) Siemens AG, 2018
6
- #
7
- # Permission is hereby granted, free of charge, to any person obtaining a copy
8
- # of this software and associated documentation files (the "Software"), to deal
9
- # in the Software without restriction, including without limitation the rights
10
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- # copies of the Software, and to permit persons to whom the Software is
12
- # furnished to do so, subject to the following conditions:
13
- #
14
- # The above copyright notice and this permission notice shall be
15
- # included in all copies or substantial portions of the Software.
16
- #
17
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- # SOFTWARE
24
-
25
-
26
- import argparse
27
- import re
28
- from datetime import date, timedelta
29
- from email.utils import make_msgid
30
- from time import gmtime, strftime
31
-
32
- from stable_rt_tools.srt_util import (
33
- Config,
34
- SubParsers,
35
- check_context,
36
- cmd,
37
- confirm,
38
- get_config,
39
- get_gpg_fingerprint,
40
- get_remote_branch_name,
41
- get_suffix,
42
- get_template_path,
43
- )
44
- from stable_rt_tools.srt_util_context import SrtContext
45
-
46
-
47
- def find_starting_ref(ctx: SrtContext) -> str:
48
- # find starting point. first check for stable update only
49
- ref = cmd(['git', 'log', '--pretty=format:%H', '--merges', '-n', '1'])
50
-
51
- # if patches have been added, we find the old tag and should start there
52
- # and not the last merge
53
- commits = cmd(['git', 'log', '--simplify-by-decoration',
54
- '--oneline', str(ref) + '..'])
55
- if ' '.join(commits.split('\n')).find(str(ctx.old_short_tag)) != -1:
56
- ref = str(ctx.old_tag)
57
-
58
- return ref
59
-
60
-
61
- REVIEW_TEXT = """\
62
- {tag} stable review patch.
63
- If anyone has any objections, please let me know.
64
-
65
- -----------
66
-
67
- """
68
-
69
-
70
- def add_review_text(path: str, tag: str) -> None:
71
- """Put the review note at the top of the body of a patch mail."""
72
- with open(path) as f:
73
- headers, sep, body = f.read().partition('\n\n')
74
- with open(path, 'w') as f:
75
- f.write(headers + sep + REVIEW_TEXT.format(tag=tag) + body)
76
-
77
-
78
- def create_rc_patches(config: Config, ctx: SrtContext) -> None:
79
- ref = find_starting_ref(ctx)
80
- mails = cmd(['git', 'format-patch', f'{ref}..',
81
- '-o', ctx.new_dir_mails, '--subject-prefix',
82
- f'PATCH {get_suffix(config).upper()}',
83
- '--cover-letter']).splitlines()
84
- for mail in mails:
85
- if not mail.endswith('/0000-cover-letter.patch'):
86
- add_review_text(mail, str(ctx.new_tag))
87
-
88
-
89
- def cover_letter_replacements(config: Config,
90
- ctx: SrtContext) -> dict[str, object]:
91
- r = {
92
- "mail_to": config['MAIL_TO'],
93
- "major": ctx.new_tag.major,
94
- "minor": ctx.new_tag.minor,
95
- "patch": ctx.new_tag.patch,
96
- "new_version": ctx.new_short_tag,
97
- "old_version": ctx.old_short_tag,
98
- "prj_dir": config['PRJ_DIR'],
99
- "message_id": make_msgid(),
100
- "sender": config['SENDER'],
101
- "name": config['NAME'],
102
- # new_tag_rt: the name templates know from before 2.0
103
- "new_tag_rt": ctx.new_tag.number,
104
- "new_tag_number": ctx.new_tag.number,
105
- "gpg_key_fingerprint": get_gpg_fingerprint(config),
106
- }
107
- if ctx.is_rc:
108
- r["new_tag_rc"] = ctx.new_tag.rc
109
- return r
110
-
111
-
112
- def write_rc_cover_letter(config: Config, ctx: SrtContext) -> None:
113
- with open(ctx.new_dir_mails + '/0000-cover-letter.patch') as f:
114
- coverletter = f.read()
115
-
116
- coverletter = coverletter.replace('*** SUBJECT HERE ***',
117
- 'Linux ' + str(ctx.new_tag))
118
-
119
- rc_text = ''
120
- with open(get_rc_templ_path(config)) as f:
121
- rc_text = f.read()
122
-
123
- r = cover_letter_replacements(config, ctx)
124
-
125
- today = date.today()
126
- rc_date = today + timedelta(weeks=1)
127
- r["release_date"] = rc_date
128
-
129
- rc_text = rc_text.format(**r)
130
-
131
- coverletter = coverletter.replace('*** BLURB HERE ***',
132
- rc_text)
133
-
134
- with open(ctx.new_dir_mails + '/0000-cover-letter.patch', 'w') as f:
135
- f.write(coverletter)
136
-
137
-
138
- def send_rc_patches(config: Config, ctx: SrtContext,
139
- args: argparse.Namespace) -> None:
140
- gmd = ['git', 'send-email', '--confirm=never']
141
- if args.suppress_cc:
142
- gmd += ['--suppress-cc=all']
143
- # an argument list, no shell: quotes would end up in the address
144
- gmd += [f'--to={t.strip()}' for t in config['MAIL_TO'].split(',')]
145
- gmd += [ctx.new_dir_mails]
146
-
147
- print('Dry run')
148
- gmdd = gmd.copy()
149
- gmdd.insert(2, '--dry-run')
150
- print(cmd(gmdd, verbose=True))
151
- if confirm('OK to send patches?'):
152
- cmd(gmd)
153
-
154
-
155
- def announce_rc(config: Config, ctx: SrtContext,
156
- args: argparse.Namespace) -> None:
157
- create_rc_patches(config, ctx)
158
- write_rc_cover_letter(config, ctx)
159
- send_rc_patches(config, ctx, args)
160
-
161
-
162
- def get_announce_tmpl_path(config: Config) -> str:
163
- return get_template_path(config, 'ANNOUNCE', 'announce-srt.txt')
164
-
165
-
166
- def get_rc_templ_path(config: Config) -> str:
167
- return get_template_path(config, 'RC_TEXT', 'announce-srt-rc.txt')
168
-
169
-
170
- def strip_trailers(msg: str) -> str:
171
- """Remove the trailer block (Signed-off-by: ...) of a commit message."""
172
- paragraphs = msg.rstrip().split('\n\n')
173
- last = paragraphs[-1].splitlines()
174
- if len(paragraphs) > 1 and all(
175
- re.match(r'^[A-Za-z][A-Za-z0-9-]*: ', line) for line in last):
176
- paragraphs = paragraphs[:-1]
177
- return '\n\n'.join(paragraphs)
178
-
179
-
180
- def announce(config: Config, ctx: SrtContext,
181
- args: argparse.Namespace) -> None:
182
- from stable_rt_tools.srt_util import is_quilt_workflow
183
- if ctx.is_rc:
184
- announce_rc(config, ctx, args)
185
- return
186
-
187
- if is_quilt_workflow(config):
188
- import subprocess
189
- tag = str(ctx.new_tag) + '-patches'
190
- try:
191
- msg = subprocess.check_output([
192
- 'git', 'show', '-s', '--format=%B', tag
193
- ], encoding='utf-8').strip()
194
- announce_start = msg.find('[ANNOUNCE]')
195
- if announce_start != -1:
196
- lines = msg[announce_start:].splitlines()
197
- msg = '\n'.join(lines[1:]).lstrip('\n')
198
- else:
199
- pass
200
- msg = strip_trailers(msg)
201
- timestamp = strftime('%a, %d %b %Y %H:%M:%S -0000', gmtime())
202
- sender = config.get('SENDER', '')
203
- mail_to = config.get('MAIL_TO', '')
204
- print(f"From: {sender}")
205
- print(f"To: {mail_to}")
206
- print(f"Subject: [ANNOUNCE] {ctx.new_tag}")
207
- print(f"Date: {timestamp}\n")
208
- print(msg)
209
- except Exception as e:
210
- print(f"Error extracting announcement from tag {tag}: {e}")
211
- return
212
-
213
- # rfc2822.html
214
- # 3.3. Date and Time Specification
215
- timestamp = strftime('%a, %d %b %Y %H:%M:%S -0000', gmtime())
216
-
217
- stable_rt_text = ''
218
- with open(get_announce_tmpl_path(config)) as f:
219
- stable_rt_text = f.read()
220
-
221
- r = cover_letter_replacements(config, ctx)
222
-
223
- r["date"] = timestamp
224
- r["branch_name"] = get_remote_branch_name()
225
- r["branch_head"] = cmd(['git', 'rev-parse', 'HEAD'])
226
-
227
- print(stable_rt_text.format(**r))
228
-
229
- ref = find_starting_ref(ctx)
230
- print(cmd(['git', '--no-pager', 'shortlog', f'{ref}..{ctx.new_tag}']))
231
-
232
- print('---')
233
-
234
- print(cmd(['git', '--no-pager', 'diff', '--stat',
235
- f'{ref}..{ctx.new_tag}']))
236
-
237
- print('---')
238
-
239
- print(cmd(['git', '--no-pager', 'diff', f'{ref}..{ctx.new_tag}']))
240
-
241
-
242
- def add_argparser(parser: SubParsers) -> argparse.ArgumentParser:
243
- prs = parser.add_parser('announce')
244
- prs.add_argument('OLD_TAG', nargs='?')
245
- prs.add_argument('NEW_TAG', nargs='?')
246
- prs.add_argument('--suppress-cc', '-s', action="store_true", default=False,
247
- help='Don''t auto-cc anyone (for testing)')
248
- return prs
249
-
250
-
251
- def execute(args: argparse.Namespace) -> None:
252
- ctx = SrtContext(args)
253
- check_context(ctx)
254
-
255
- announce(get_config(), ctx, args)
File without changes
File without changes