diff --git a/b4/__init__.py b/b4/__init__.py index 0d506bba..3398172e 100644 --- a/b4/__init__.py +++ b/b4/__init__.py @@ -104,6 +104,7 @@ def _dkim_log_filter(record): 'linkmask': LOREADDR + '/r/%s', 'trailer-order': DEFAULT_TRAILER_ORDER, 'listid-preference': '*.feeds.kernel.org,*.linux.dev,*.kernel.org,*', + 'pr-tracker-email': 'pr-tracker-bot@kernel.org', 'save-maildirs': 'no', # off: do not bother checking attestation # check: print an attaboy when attestation is found @@ -274,7 +275,7 @@ def get_series(self, revision=None, sloppytrailers=False, reroll=True): for member in lser.patches: if member is not None and member.in_reply_to is not None: potential = self.get_by_msgid(member.in_reply_to) - if potential is not None and potential.has_diffstat and not potential.has_diff: + if potential is not None and potential.maybe_cover(): # This is *probably* the cover letter lser.patches[0] = potential lser.has_cover = True @@ -350,7 +351,7 @@ def get_series(self, revision=None, sloppytrailers=False, reroll=True): return lser - def add_message(self, msg): + def add_message(self, msg, needcover=False): msgid = LoreMessage.get_clean_msgid(msg) if msgid in self.msgid_map: logger.debug('Already have a message with this msgid, skipping %s', msgid) @@ -366,7 +367,7 @@ def add_message(self, msg): self.followups.append(lmsg) return - if lmsg.counter == 0 and (not lmsg.counters_inferred or lmsg.has_diffstat): + if lmsg.likely_cover(): # Cover letter # Add it to covers -- we'll deal with them later logger.debug(' adding as v%s cover letter', lmsg.revision) @@ -374,12 +375,12 @@ def add_message(self, msg): return if lmsg.has_diff: - if lmsg.revision not in self.series: + if lmsg.revision not in self.series or needcover: if lmsg.revision_inferred and lmsg.in_reply_to: # We have an inferred revision here. # Do we have an upthread cover letter that specifies a revision? irt = self.get_by_msgid(lmsg.in_reply_to) - if irt is not None and irt.has_diffstat and not irt.has_diff: + if irt is not None and irt.likely_cover(): # Yes, this is very likely our cover letter logger.debug(' fixed revision to v%s', irt.revision) lmsg.revision = irt.revision @@ -860,6 +861,7 @@ def __init__(self, msg): self.pr_repo = None self.pr_ref = None self.pr_tip_commit = None + self.pr_merge_commit = None self.pr_remote_tip_commit = None # Patchwork hash @@ -966,6 +968,12 @@ def __init__(self, msg): if trailer[0].lower() not in badtrailers: self.trailers.append(trailer) + def maybe_cover(self): + return self.has_diffstat and not self.has_diff + + def likely_cover(self): + return self.counter == 0 and (not self.counters_inferred or self.mayby_cover()) + def get_trailers(self, sloppy=False): trailers = list() mismatches = set() @@ -1285,6 +1293,17 @@ def get_clean_msgid(msg, header='Message-Id'): msgid = matches.groups()[0] return msgid + # Get commit id from git am formatted patch + @staticmethod + def get_commit_id(msg): + commitid = None + unixhdr = msg.get_unixfrom() + if unixhdr: + matches = re.search(r'^From ([0-9a-f]+)', unixhdr) + if matches: + commitid = matches.groups()[0] + return commitid + @staticmethod def get_preferred_duplicate(msg1, msg2): config = get_main_config() @@ -2047,7 +2066,7 @@ def get_cache_dir(appname: str = 'b4') -> str: fullpath = os.path.join(cachedir, entry) st = os.stat(fullpath) if st.st_mtime < expage: - logger.debug('Cleaning up cache: %s', entry) + logger.debug('Cleaning up cache: %s mtime=%d < %d', entry, st.st_mtime, expage) if os.path.isdir(fullpath): shutil.rmtree(fullpath) else: diff --git a/b4/command.py b/b4/command.py index 3a2d58f3..b9e5aed5 100644 --- a/b4/command.py +++ b/b4/command.py @@ -103,6 +103,11 @@ def cmd_diff(cmdargs): b4.diff.main(cmdargs) +def cmd_rn(cmdargs): + import b4.rn + b4.rn.main(cmdargs) + + def cmd(): # noinspection PyTypeChecker parser = argparse.ArgumentParser( @@ -191,6 +196,8 @@ def cmd(): 'the identity must match a [sendemail "identity"] config section')) sp_pr.add_argument('--dry-run', dest='dryrun', action='store_true', default=False, help='Force a --dry-run on git-send-email invocation (use with -s)') + sp_pr.add_argument('--no-cover', dest='nocover', action='store_true', default=False, + help='Do not save the cover letter (on by default when using -o -)') sp_pr.add_argument('msgid', nargs='?', help='Message ID to process, or pipe a raw message') sp_pr.set_defaults(func=cmd_pr) @@ -248,6 +255,15 @@ def cmd(): help='Show all developer keys found in a thread') sp_kr.set_defaults(func=cmd_kr) + # b4 rn + sp_rn = subparsers.add_parser('rn', help='Generate release notes from pull request') + cmd_retrieval_common_opts(sp_rn) + sp_rn.add_argument('-g', '--gitdir', default=None, + help='Operate on this git tree instead of current dir') + sp_rn.add_argument('-o', '--output-file', dest='outfile', default=None, + help='Write release notes into this file instead of outputting to stdout') + sp_rn.set_defaults(func=cmd_rn) + cmdargs = parser.parse_args() logger.setLevel(logging.DEBUG) diff --git a/b4/diff.py b/b4/diff.py index b21e25f8..524072db 100644 --- a/b4/diff.py +++ b/b4/diff.py @@ -44,7 +44,8 @@ def diff_same_thread_series(cmdargs): if not msgs: logger.critical('Unable to retrieve thread: %s', msgid) return - msgs = b4.mbox.get_extra_series(msgs, direction=-1, wantvers=wantvers, useproject=cmdargs.useproject) + msgs = b4.mbox.get_extra_series(msgs, direction=-1, wantvers=wantvers, + nocache=cmdargs.nocache, useproject=cmdargs.useproject) if os.path.exists(cachedir): shutil.rmtree(cachedir) pathlib.Path(cachedir).mkdir(parents=True) diff --git a/b4/mbox.py b/b4/mbox.py index 05d46b5f..26d5f5ea 100644 --- a/b4/mbox.py +++ b/b4/mbox.py @@ -479,11 +479,15 @@ def save_as_quilt(am_msgs, q_dirname): for patch_filename in patch_filenames: sfh.write('%s\n' % patch_filename) - -def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = None, nocache: bool = False, - useproject: Optional[str] = None) -> list: - base_msg = None - latest_revision = None +# Get older/newer revisions of a patch series from public-inbox +# +# @direction: +# 1 - look for newer revisions (default) +# -1 - look for older revisions +# 0 - look for latest revision in public-inbox (regardless of @base_msg) +def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[list] = None, nocache: bool = False, + base_msg = None, useproject: Optional[str] = None) -> list: + latest_revision = 0 seen_msgids = set() seen_covers = set() obsoleted = list() @@ -521,6 +525,36 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N logger.debug('Could not find cover of 1st patch in mbox') return msgs + # For query by @base_msg, check if we have a cache of this lookup + base_msgid = b4.LoreMessage.get_clean_msgid(base_msg) + identifier = base_msgid + # Use commit id as key to cache of git am formatted @base_msg + if not identifier: + identifier = b4.LoreMessage.get_commit_id(base_msg) + if identifier is None: + logger.debug('Could not find find base msgid for series') + return msgs + + cachedir = None + if identifier and len(msgs) == 0 and not wantvers: + if useproject: + identifier += '-' + useproject + if direction > 0: + identifier += '+' + elif direction < 0: + identifier += '-' + cachedir = b4.get_cache_file(identifier, suffix='extra.msgs') + + if cachedir and os.path.exists(cachedir) and not nocache: + logger.debug('Using cached copy of %s at %s', identifier, cachedir) + msgs = list() + for msg in os.listdir(cachedir): + with open(os.path.join(cachedir, msg), 'rb') as fh: + msgs.append(email.message_from_binary_file(fh)) + return msgs + else: + logger.debug('No cached copy for %s', identifier) + config = b4.get_main_config() loc = urllib.parse.urlparse(config['midmask']) if not useproject: @@ -552,7 +586,7 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N else: # Get subject info from base_msg again lsub = b4.LoreSubject(base_msg['Subject']) - if not len(lsub.prefixes): + if direction > 0 and not len(lsub.prefixes): logger.debug('Not checking for new revisions: no prefixes on the cover letter.') return msgs if direction < 0 and latest_revision <= 1: @@ -561,7 +595,6 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N if direction < 0 and wantvers is None: wantvers = [latest_revision - 1] - base_msgid = b4.LoreMessage.get_clean_msgid(base_msg) fromeml = email.utils.getaddresses(base_msg.get_all('from', []))[0][1] msgdate = email.utils.parsedate_tz(str(base_msg['Date'])) startdate = time.strftime('%Y%m%d', msgdate[:9]) @@ -569,10 +602,15 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N q = 's:"%s" AND f:"%s" AND d:%s..' % (lsub.subject.replace('"', ''), fromeml, startdate) queryurl = '%s?%s' % (listarc, urllib.parse.urlencode({'q': q, 'x': 'A', 'o': '-1'})) logger.critical('Checking for newer revisions on %s', listarc) - else: + elif direction < 0: q = 's:"%s" AND f:"%s" AND d:..%s' % (lsub.subject.replace('"', ''), fromeml, startdate) queryurl = '%s?%s' % (listarc, urllib.parse.urlencode({'q': q, 'x': 'A', 'o': '1'})) logger.critical('Checking for older revisions on %s', listarc) + else: + # Find latest revision in public-inbox to match base_msg subject + q = 's:"%s"' % (lsub.subject.replace('"', '')) + queryurl = '%s?%s' % (listarc, urllib.parse.urlencode({'q': q, 'x': 'A'})) + logger.debug('Checking for revisions on %s', listarc) logger.debug('Query URL: %s', queryurl) session = b4.get_requests_session() @@ -592,11 +630,11 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N for entry in entries: title = entry.find('atom:title', ns).text lsub = b4.LoreSubject(title) - if lsub.reply or lsub.counter > 1: + if lsub.reply or (direction != 0 and lsub.counter > 1): logger.debug('Ignoring result (not interesting): %s', title) continue link = entry.find('atom:link', ns).get('href') - if direction > 0 and lsub.revision <= latest_revision: + if direction >= 0 and lsub.revision <= latest_revision: logger.debug('Ignoring result (not new revision): %s', title) continue elif direction < 0 and lsub.revision >= latest_revision: @@ -610,13 +648,13 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N continue if lsub.revision == 1 and lsub.revision == latest_revision: # Someone sent a separate message with an identical title but no new vX in the subject line - if direction > 0: + if direction >= 0: # It's *probably* a new revision. logger.debug('Likely a new revision: %s', title) else: # It's *probably* an older revision. logger.debug('Likely an older revision: %s', title) - elif direction > 0 and lsub.revision > latest_revision: + elif direction >= 0 and lsub.revision > latest_revision: logger.debug('Definitely a new revision [v%s]: %s', lsub.revision, title) elif direction < 0 and lsub.revision < latest_revision: logger.debug('Definitely an older revision [v%s]: %s', lsub.revision, title) @@ -633,6 +671,13 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N nt_msgs += potentials logger.info(' Added %s messages from that thread', len(potentials)) + # Write results of @base_msg query to cache + if cachedir: + if os.path.exists(cachedir): + shutil.rmtree(cachedir) + pathlib.Path(cachedir).mkdir(parents=True) + at = 0 + # Append all of these to the existing mailbox for nt_msg in nt_msgs: nt_msgid = b4.LoreMessage.get_clean_msgid(nt_msg) @@ -643,6 +688,10 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N logger.debug('Adding: %s', nt_subject) msgs.append(nt_msg) seen_msgids.add(nt_msgid) + if cachedir: + with open(os.path.join(cachedir, '%04d' % at), 'wb') as fh: + fh.write(nt_msg.as_bytes(policy=b4.emlpolicy)) + at += 1 return msgs @@ -725,7 +774,8 @@ def main(cmdargs): return if len(msgs) and cmdargs.checknewer: - msgs = get_extra_series(msgs, direction=1, useproject=cmdargs.useproject) + msgs = get_extra_series(msgs, direction=1, nocache=cmdargs.nocache, + useproject=cmdargs.useproject) if cmdargs.subcmd in ('am', 'shazam'): make_am(msgs, cmdargs, msgid) diff --git a/b4/pr.py b/b4/pr.py index 5a661805..55b4d56f 100644 --- a/b4/pr.py +++ b/b4/pr.py @@ -46,6 +46,10 @@ re.compile(r'^\s*([\w+-]+(?:://|@)[\w/.@~-]+)\s*$', re.M | re.I), ] +# PR tracker +PULL_BODY_MERGE_COMMIT_ID_RE = [ + re.compile(r'^https://git.kernel.org/torvalds/c/([0-9a-f]{5,40})$', re.M | re.I), +] def format_addrs(pairs): return ', '.join([utils.formataddr(pair) for pair in pairs]) @@ -85,7 +89,30 @@ def git_get_commit_id_from_repo_ref(repo, ref): return commit_id -def parse_pr_data(msg): +def parse_pr_tracker_data(lmsg, gitdir): + for merge_cid_re in PULL_BODY_MERGE_COMMIT_ID_RE: + matches = merge_cid_re.search(lmsg.body) + if matches: + merge_cid = matches.groups()[0] + break + + if merge_cid is None: + logger.debug('did not find merge commit-id, ignoring pull request') + return None + + gitargs = ['rev-parse', '%s^2' % merge_cid] + ecode, out = b4.git_run_command(gitdir, gitargs) + if ecode > 0: + logger.debug('invalid merge commit: %s', merge_cid) + return None + + lmsg.pr_remote_tip_commit = out.split('\n')[0] + lmsg.pr_merge_commit = merge_cid + logger.debug('success, merge commit-ids: %s <- %s', merge_cid, lmsg.pr_remote_tip_commit) + return lmsg + + +def parse_pr_data(msg, gitdir): lmsg = b4.LoreMessage(msg) if lmsg.body is None: logger.critical('Could not find a plain part in the message body') @@ -93,6 +120,10 @@ def parse_pr_data(msg): logger.info('Looking at: %s', lmsg.full_subject) + config = b4.get_main_config() + if lmsg.fromemail == config['pr-tracker-email']: + return parse_pr_tracker_data(lmsg, gitdir) + for since_re in PULL_BODY_SINCE_ID_RE: matches = since_re.search(lmsg.body) if matches: @@ -258,15 +289,19 @@ def thanks_record_pr(lmsg): def explode(gitdir, lmsg, mailfrom=None, retrieve_links=True, fpopts=None): - ecode = fetch_remote(gitdir, lmsg, check_sig=False, ty_track=False) - if ecode > 0: - raise RuntimeError('Fetching unsuccessful') + if lmsg.pr_repo and lmsg.pr_ref: + ecode = fetch_remote(gitdir, lmsg, check_sig=False, ty_track=False) + if ecode > 0: + raise RuntimeError('Fetching unsuccessful') if not lmsg.pr_base_commit: - # Use git merge-base between HEAD and FETCH_HEAD to find + # Use git merge-base between HEAD and PR tip to find # where we should start logger.info('Running git merge-base to find common ancestry') - gitargs = ['merge-base', 'HEAD', 'FETCH_HEAD'] + head = 'HEAD' + if lmsg.pr_merge_commit: + head = '%s^' % lmsg.pr_merge_commit + gitargs = ['merge-base', head, lmsg.pr_tip_commit] ecode, out = b4.git_run_command(gitdir, gitargs, logstderr=True) if ecode > 0: logger.critical('Could not find common ancestry.') @@ -313,7 +348,7 @@ def explode(gitdir, lmsg, mailfrom=None, retrieve_links=True, fpopts=None): # of the archived threads. linked_ids.add(lmsg.msgid) - with b4.git_format_patches(gitdir, lmsg.pr_base_commit, 'FETCH_HEAD', prefixes=prefixes, extraopts=fpopts) as pdir: + with b4.git_format_patches(gitdir, lmsg.pr_base_commit, lmsg.pr_tip_commit, prefixes=prefixes, extraopts=fpopts) as pdir: if pdir is None: raise RuntimeError('Could not run format-patches') @@ -341,6 +376,9 @@ def explode(gitdir, lmsg, mailfrom=None, retrieve_links=True, fpopts=None): cmsg.add_header('From', mailfrom) cmsg.add_header('Subject', '[' + ' '.join(msubj.prefixes) + '] ' + lmsg.subject) cmsg.add_header('Date', lmsg.msg.get('Date')) + # For PR tracker reply, include a referenece to original PR + if lmsg.fromemail == config['pr-tracker-email'] and lmsg.in_reply_to: + cmsg.add_header('In-Reply-To', '<%s>' % lmsg.in_reply_to) cmsg.set_charset('utf-8') cmsg.replace_header('Content-Transfer-Encoding', '8bit') @@ -505,11 +543,11 @@ def main(cmdargs): gitdir = cmdargs.gitdir lmsg = None - if not sys.stdin.isatty(): + if not cmdargs.msgid and not sys.stdin.isatty(): logger.debug('Getting PR message from stdin') msg = email.message_from_bytes(sys.stdin.buffer.read()) cmdargs.msgid = b4.LoreMessage.get_clean_msgid(msg) - lmsg = parse_pr_data(msg) + lmsg = parse_pr_data(msg, gitdir) else: if cmdargs.msgid and 'github.com' in cmdargs.msgid and '/pull/' in cmdargs.msgid: logger.debug('Getting PR info from Github') @@ -524,7 +562,7 @@ def main(cmdargs): for msg in msgs: mmsgid = b4.LoreMessage.get_clean_msgid(msg) if mmsgid == msgid: - lmsg = parse_pr_data(msg) + lmsg = parse_pr_data(msg, gitdir) break if lmsg is None or lmsg.pr_remote_tip_commit is None: @@ -569,6 +607,13 @@ def main(cmdargs): logger.info(out) sys.exit(ecode) + # With "-o -" write mbox with no cover to stdout + if cmdargs.outmbox == '-': + b4.save_git_am_mbox(msgs[1:], sys.stdout) + sys.exit(0) + elif cmdargs.nocover: + msgs = msgs[1:] + config = b4.get_main_config() if config.get('save-maildirs', 'no') == 'yes': save_maildir = True @@ -619,4 +664,5 @@ def main(cmdargs): logger.info('Pull request does not appear to be in this tree.') sys.exit(0) - fetch_remote(gitdir, lmsg, branch=cmdargs.branch) + if lmsg.pr_repo and lmsg.pr_ref: + fetch_remote(gitdir, lmsg, branch=cmdargs.branch) diff --git a/b4/rn.py b/b4/rn.py new file mode 100644 index 00000000..c9958eb7 --- /dev/null +++ b/b4/rn.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2022 CTERA Networks. All Rights Reserved. +# +__author__ = 'Amir Goldstein ' + +import os +import sys +import b4 +import b4.mbox +import mailbox +import email +import shutil +import pathlib +import re + +logger = b4.logger + +MSG_BODY_TEST_REF_RE = [ + re.compile(r'\b(btrfs|ceph|cifs|ext4|f2fs|generic|nfs|ocfs2|overlay|perf|shared|udf|xfs)/([0-9]{3})\b'), +] + +def note_series(lser, notes, tests, fh, rst): + cover = None + if lser.has_cover: + cover = lser.patches[0] + elif len(lser.patches) > 1: + cover = lser.patches[1] + + if not cover: + logger.critical('No cover letter found for patch series') + return False + + if cover.msgid in notes: + logger.debug('Duplicate series: %s', cover.subject) + return True + + notes[cover.msgid] = cover + config = b4.get_main_config() + link = (config['linkmask'] % cover.msgid) + if rst: + fh.write('\n- `%s <%s>`_\n' % (cover.full_subject, link)) + else: + fh.write('\n- %s\n [%s]\n' % (cover.full_subject, link)) + if tests: + fh.write(' Tests: %s\n' % ' '.join(sorted(tests))) + return True + +def note_latest_series(msgs, notes, fh, rst): + count = len(msgs) + logger.debug('---') + logger.debug('Analyzing %s messages in the thread', count) + lmbx = b4.LoreMailbox() + tests = set() + # Add covers of all revisions first, so we are sure to find the right cover + # when we add the message + for msg in msgs: + lmsg = b4.LoreMessage(msg) + if lmsg.body is None: + logger.critical('Could not find a plain part in the message body') + continue + lmbx.add_message(msg, needcover=True) + for tests_re in MSG_BODY_TEST_REF_RE: + for match in re.finditer(tests_re, lmsg.body): + test = match.group(0) + if not test in tests: + logger.debug('Found reference to test: %s\n' % test) + tests.add(test) + + lser = lmbx.get_series() + if lser is None or len(lser.patches) == 0: + logger.critical('No posted patches found') + return False + + return note_series(lser, notes, tests, fh, rst) + + +# Breakup patch queue into series and report notes for every series +def release_notes(msgs, cmdargs, fh, rst): + fh.write('\n'); + if not rst: + fh.write('---\n') + notes = {} + + for msg in msgs: + # Strip prefixes from subject + lsub = b4.LoreSubject(msg['Subject']) + if not lsub.subject: + continue; + logger.debug('Message: %s', lsub.subject) + if lsub.counter == 0: + # Cover letter of PR + prsub = lsub.subject + if prsub.startswith('Re: '): + prsub = prsub[4:] + prmsgid = b4.LoreMessage.get_clean_msgid(msg, header='In-Reply-To') + if prmsgid: + config = b4.get_main_config() + link = (config['linkmask'] % prmsgid) + if rst: + fh.write('`%s: <%s>`_\n\n' % (prsub, link)) + else: + fh.write('Changes in %s:\n [%s]\n' % (prsub, link)) + continue; + + # Find public-inbox series whose first patch matches this msg subject + ser_msgs = b4.mbox.get_extra_series([], base_msg=msg, direction=0, + nocache=cmdargs.nocache, + useproject=cmdargs.useproject) + # Report notes for found series + found = False + if len(ser_msgs) > 0: + found = note_latest_series(ser_msgs, notes, fh, rst) + if not found: + fh.write('\n- [PATH ?/?] %s\n' % lsub.subject) + + if not notes: + logger.critical('No posted patches found') + + fh.write('\n'); + if not rst: + fh.write('---\n') + + +def main(cmdargs): + msgid, msgs = b4.mbox.get_msgs(cmdargs) + if not msgs: + logger.critical('Unable to retrieve messages') + sys.exit(1) + + rst = False + if cmdargs.outfile is not None: + logger.info('Writing %s', cmdargs.outfile) + fh = open(cmdargs.outfile, 'w') + rst = cmdargs.outfile.endswith('.rst') + else: + fh = sys.stdout + + release_notes(msgs, cmdargs, fh, rst) diff --git a/man/b4.5.rst b/man/b4.5.rst index 19e96af4..f6ca7f88 100644 --- a/man/b4.5.rst +++ b/man/b4.5.rst @@ -13,7 +13,7 @@ Work with code submissions in a public-inbox archive SYNOPSIS -------- -b4 {mbox,am,shazam,attest,pr,ty,diff} [options] +b4 {mbox,am,shazam,attest,pr,rn,ty,diff} [options] DESCRIPTION ----------- @@ -31,6 +31,7 @@ SUBCOMMANDS * *b4 am*: Create an mbox file that is ready to git-am * *b4 shazam*: Similar to *am*, but lets you apply patches directly * *b4 pr*: Work with pull requests +* *b4 rn*: Generate release notes * *b4 diff*: Show range-diff style diffs between patch versions * *b4 ty*: Create templated replies for processed patches and pull requests * *b4 attest*: (EXPERIMENTAL) Add cryptographic attestation to patches @@ -163,6 +164,8 @@ optional arguments: -l, --retrieve-links Attempt to retrieve any Link: URLs (use with -e) -f MAILFROM, --from-addr MAILFROM Use this From: in exploded messages (use with -e) + --no-cover + Do not save the cover letter (on by default when using -o -) *Example*: b4 pr 202003292120.2BDCB41@keescook @@ -247,6 +250,26 @@ optional arguments: *Example*: b4 kr --show-keys 20210521184811.617875-1-konstantin@linuxfoundation.org +b4 rn +~~~~~ +usage: + command.py rn [-h] [-g GITDIR] [-p USEPROJECT] [-C] [-c] [msgid] + +positional arguments: + msgid Message ID to process, or pipe a raw message + +optional arguments: + -h, --help show this help message and exit + -g GITDIR, --gitdir GITDIR + Operate on this git tree instead of current dir + -p USEPROJECT, --use-project USEPROJECT + Use a specific project instead of guessing (linux-mm, linux-hardening, etc) + -o OUTFILE, --output-file OUTFILE + Write release notes into this file instead of outputting to stdout + -C, --no-cache Do not use local cache + +*Example*: b4 rn 202003292120.2BDCB41@keescook + CONFIGURATION ------------- B4 configuration is handled via git-config(1), so you can store it in diff --git a/tests/linux_rn.sh b/tests/linux_rn.sh new file mode 100755 index 00000000..77863a53 --- /dev/null +++ b/tests/linux_rn.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# +# b4 rn examples to be run from linux source tree +# +# Usage: +# +# T=$PWD/tests/ +# cd ~/src/linux +# $T/linux_rn.sh -d 2>rn.log +# $T/linux_rn.sh -d $T/xfs-5.10..5.17.in -p linux-xfs +# + +if ! (git rev-parse v2.6.12-rc2 2> /dev/null | \ + grep -q 9e734775f7c22d2f89943ad6c745571f1930105f); then + echo "Please run this test from a linux source tree" + exit 1 +fi + +RNMBX=/tmp/linux_rn.mbx +# Default to release notes in reStructuredText format +RNFMT=rst +# Uncomment to generate release notes in plain text format +#RNFMT=txt +RNOUT=/tmp/linux_rn.$RNFMT + +if [ "$1" = "-d" ]; then + RNDEBUG=$1 + shift +fi +RNOPTS="$*" + +git_fixes_rn() +{ + local descr="$1" + local range="$2" + local path="$3" + + echo "---" + echo "GIT log - $descr" + echo "---" + git log -p --grep Fixes: --pretty=email $range -- $path | \ + b4 $RNDEBUG rn $RNOPTS -m - +} + +pr_tracker_rn() +{ + local descr="$1" + local msgid="$2" + + rm -rf $RNMBX $RNOUT + + echo "---" + echo "PR tracker - $descr" + echo "---" + b4 $RNDEBUG pr -e -o $RNMBX "$msgid" + b4 $RNDEBUG rn $RNOPTS -m $RNMBX -o $RNOUT + cat $RNOUT +} + + +# Process PR list from input file +if [ -f "$1" ]; then + PRFILE="$1" + shift + RNOPTS="$*" + + rnoutall=`basename ${PRFILE%.*}`-rn.$RNFMT + echo "# Release notes auto-generated by 'b4 rn'" > $rnoutall + cat "$PRFILE" | while read pr name; do + echo "Writing release notes of '$name' to $rnoutall..." + >"$name".$RNFMT 2>"$name".log > $rnoutall + done + exit +fi + +git_fixes_rn "series with fix patches for subsystem" \ + v5.13..v5.14 fs/xfs + +pr_tracker_rn "patches not posted" \ + 164374837231.6282.14818932060276777076.pr-tracker-bot@kernel.org + +pr_tracker_rn "individual patch posted" \ + 164590250253.22829.8421551678388979175.pr-tracker-bot@kernel.org + +pr_tracker_rn "patch series posted" \ + 164408216661.7836.4930013315804213982.pr-tracker-bot@kernel.org + +pr_tracker_rn "series with fstest reference" \ + 162784611031.1186.18214929758593020802.pr-tracker-bot@kernel.org + +pr_tracker_rn "patch series including partial reroll" \ + 164817214223.9489.12483808836905609419.pr-tracker-bot@kernel.org + +pr_tracker_rn "many patch series" \ + 163060423908.29568.14182828511329643634.pr-tracker-bot@kernel.org diff --git a/tests/xfs-5.10..5.17.in b/tests/xfs-5.10..5.17.in new file mode 100644 index 00000000..a355fd94 --- /dev/null +++ b/tests/xfs-5.10..5.17.in @@ -0,0 +1,24 @@ +164590250253.22829.8421551678388979175.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.17-rc6 +164408216661.7836.4930013315804213982.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.17-rc3 +164284441552.7666.5195906929759259618.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.17-rc1 +164274816250.27527.6119097451475838528.pr-tracker-bot@kernel.org [GIT PULL] xfs: DMAPI ioctl housecleaning for 5.17-rc1 +164274816283.27527.4445590209473650660.pr-tracker-bot@kernel.org [GIT PULL] xfs: legacy Irix ioctl housecleaning for 5.17-rc1, part 2 +164274816267.27527.14866415947964585469.pr-tracker-bot@kernel.org [GIT PULL] xfs: legacy Irix ioctl housecleaning for 5.17-rc1, part 1 +164194642310.21161.8563846497332350289.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.17 +163926928546.10000.18339109912268195117.pr-tracker-bot@kernel.org [GIT PULL] xfs: bug fixes for 5.16-rc4 +163866768073.6146.4028609779231917827.pr-tracker-bot@kernel.org [GIT PULL] xfs: bug fixes for 5.16-rc3 +163804699189.3764.12554584751673448384.pr-tracker-bot@kernel.org [GIT PULL] xfs: bug fixes for 5.16-rc2 +163692146690.4278.335385691531056076.pr-tracker-bot@kernel.org [GIT PULL] xfs: cleanups and resyncs for 5.16 +163588274577.22794.1156896528638745710.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.16 +163060423908.29568.14182828511329643634.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.15 +162784611031.1186.18214929758593020802.pr-tracker-bot@kernel.org [GIT PULL] xfs: bug fixes for 5.14-rc4 +162663410637.7372.13651239253430897917.pr-tracker-bot@kernel.org [GIT PULL] xfs: bug fixes for 5.14-rc2 +162526246366.28144.4236351495860897999.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.14 +162234900431.23697.17510919033109580334.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.13-rc4 +162170495407.3077.13033559630823026395.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.13-rc3 +162037389652.26493.11092818693927608843.pr-tracker-bot@kernel.org [GIT PULL] xfs: more new code for 5.13 +161971872723.11214.4279033295206868895.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.13 +161609653113.4441.2474187984850625994.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.12-rc4 +161454325648.2182.2660010480088666972.pr-tracker-bot@kernel.org [GIT PULL] xfs: fixes for 5.12-rc1 +161393278374.20435.8997839961461434518.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.12 +160832487637.19372.7448008445325982345.pr-tracker-bot@kernel.org [GIT PULL] xfs: new code for 5.11