aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Ryabitsev <konstantin@linuxfoundation.org>2020-04-09 18:21:42 -0400
committerKonstantin Ryabitsev <konstantin@linuxfoundation.org>2020-04-09 18:21:42 -0400
commit7210e188cc087f2f9a2aba23420d781d6d1a8691 (patch)
tree5eb825c8eec316e4c4859f61b559dbf438d76eef
parent18a3026a98c2e7c57c0c3e3988520b82da92aff5 (diff)
downloadb4-7210e188cc087f2f9a2aba23420d781d6d1a8691.tar.gz
Add b4 ty that aims to simplify common feedback
New experimental feature that aims to simplify a very common "thanks, applied" kind of feedback often expected of maintainers. Still needs documentation to explain its usage. Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
-rw-r--r--b4/__init__.py33
-rw-r--r--b4/command.py25
-rw-r--r--b4/mbox.py62
-rw-r--r--b4/pr.py58
-rw-r--r--b4/ty.py434
5 files changed, 591 insertions, 21 deletions
diff --git a/b4/__init__.py b/b4/__init__.py
index d108b76..54cc42b 100644
--- a/b4/__init__.py
+++ b/b4/__init__.py
@@ -174,8 +174,8 @@ class LoreMailbox:
chunks = line.split(':')
projmap[chunks[0]] = chunks[1].strip()
- allto = email.utils.getaddresses(patch.msg.get_all('to', []))
- allto += email.utils.getaddresses(patch.msg.get_all('cc', []))
+ allto = email.utils.getaddresses([str(x) for x in patch.msg.get_all('to', [])])
+ allto += email.utils.getaddresses([str(x) for x in patch.msg.get_all('cc', [])])
listarc = patch.msg.get_all('list-archive', [])
for entry in allto:
if entry[1] in projmap:
@@ -307,7 +307,6 @@ class LoreMailbox:
continue
lmsg.load_hashes()
if lmsg.attestation.attid in self.trailer_map:
- logger.info('WOO: %s', str(self.trailer_map[lmsg.attestation.attid]))
lmsg.followup_trailers.update(self.trailer_map[lmsg.attestation.attid])
return lser
@@ -442,7 +441,7 @@ class LoreSeries:
return 'undefined'
prefix = lmsg.date.strftime('%Y%m%d')
- authorline = email.utils.getaddresses(lmsg.msg.get_all('from', []))[0]
+ authorline = email.utils.getaddresses([str(x) for x in lmsg.msg.get_all('from', [])])[0]
if extended:
local = authorline[1].split('@')[0]
unsafe = '%s_%s_%s' % (prefix, local, lmsg.subject)
@@ -635,7 +634,7 @@ class LoreMessage:
self.in_reply_to = LoreMessage.get_clean_msgid(self.msg, header='In-Reply-To')
try:
- fromdata = email.utils.getaddresses(self.msg.get_all('from', []))[0]
+ fromdata = email.utils.getaddresses([str(x) for x in self.msg.get_all('from', [])])[0]
self.fromname = fromdata[0]
self.fromemail = fromdata[1]
except IndexError:
@@ -780,6 +779,9 @@ class LoreMessage:
@staticmethod
def clean_header(hdrval):
+ if hdrval is None:
+ return ''
+
decoded = ''
for hstr, hcs in email.header.decode_header(hdrval):
if hcs is None:
@@ -1448,6 +1450,7 @@ def get_data_dir():
datahome = os.path.join(str(Path.home()), '.local', 'share')
datadir = os.path.join(datahome, 'b4')
Path(datadir).mkdir(parents=True, exist_ok=True)
+ return datadir
def get_cache_dir():
@@ -1650,3 +1653,23 @@ def git_format_patches(gitdir, start, end, reroll=None):
gitargs += ['%s..%s' % (start, end)]
ecode, out = git_run_command(gitdir, gitargs)
return ecode, out
+
+
+def git_commit_exists(gitdir, commit_id):
+ gitargs = ['cat-file', '-e', commit_id]
+ ecode, out = git_run_command(gitdir, gitargs)
+ return ecode == 0
+
+
+def git_branch_contains(gitdir, commit_id):
+ gitargs = ['branch', '--format=%(refname:short)', '--contains', commit_id]
+ lines = git_get_command_lines(gitdir, gitargs)
+ return lines
+
+
+def format_addrs(pairs):
+ addrs = set()
+ for pair in pairs:
+ # Remove any quoted-printable header junk from the name
+ addrs.add(email.utils.formataddr((LoreMessage.clean_header(pair[0]), LoreMessage.clean_header(pair[1]))))
+ return ', '.join(addrs)
diff --git a/b4/command.py b/b4/command.py
index d465732..d195509 100644
--- a/b4/command.py
+++ b/b4/command.py
@@ -55,6 +55,11 @@ def cmd_pr(cmdargs):
b4.pr.main(cmdargs)
+def cmd_ty(cmdargs):
+ import b4.ty
+ b4.ty.main(cmdargs)
+
+
def cmd():
parser = argparse.ArgumentParser(
description='A tool to work with public-inbox patches',
@@ -133,6 +138,26 @@ def cmd():
help='Message ID to process, or pipe a raw message')
sp_pr.set_defaults(func=cmd_pr)
+ # b4 ty
+ sp_ty = subparsers.add_parser('ty', help='Generate thanks email when something gets merged/applied')
+ sp_ty.add_argument('-g', '--gitdir', default=None,
+ help='Operate on this git tree instead of current dir')
+ sp_ty.add_argument('-o', '--outdir', default='.',
+ help='Write thanks files into this dir (default=.)')
+ sp_ty.add_argument('-l', '--list', action='store_true', default=False,
+ help='List pull requests and patch series you have retrieved')
+ sp_ty.add_argument('-s', '--send', nargs='+',
+ help='Generate thankyous for specified messages (use -l to get the list or "all")')
+ sp_ty.add_argument('-d', '--discard', nargs='+',
+ help='Discard specified messages (use -l to get the list, or use "_all")')
+ sp_ty.add_argument('-a', '--auto', action='store_true', default=False,
+ help='Use the Auto-Thankanator to figure out what got applied/merged')
+ sp_ty.add_argument('-b', '--branch', default=None,
+ help='The branch to check against, instead of current (use with -a)')
+ sp_ty.add_argument('--since', default='1.week',
+ help='The --since option to use when auto-matching patches (default=1.week)')
+ sp_ty.set_defaults(func=cmd_ty)
+
cmdargs = parser.parse_args()
logger.setLevel(logging.DEBUG)
diff --git a/b4/mbox.py b/b4/mbox.py
index 3b00fd8..b11d7d1 100644
--- a/b4/mbox.py
+++ b/b4/mbox.py
@@ -13,6 +13,7 @@ import email.message
import email.utils
import re
import time
+import json
import urllib.parse
import xml.etree.ElementTree
@@ -135,10 +136,71 @@ def mbox_to_am(mboxfile, cmdargs):
logger.critical(' git am %s', am_filename)
am_mbx.close()
+ thanks_record_am(lser)
return am_filename
+def thanks_record_am(lser):
+ if not lser.complete:
+ logger.debug('Incomplete series, not tracking for thanks')
+ return
+
+ # Are we tracking this already?
+ datadir = b4.get_data_dir()
+ slug = lser.get_slug(extended=True)
+ filename = '%s.am' % slug
+ # Check if we're tracking it already
+ for entry in os.listdir(datadir):
+ if entry == filename:
+ return
+
+ # Get patch-id of each patch in the series
+ gitargs = ['patch-id', '--stable']
+ patches = list()
+ for pmsg in lser.patches[1:]:
+ ecode, out = b4.git_run_command(None, gitargs, stdin=pmsg.body.encode('utf-8'))
+ if ecode > 0 or not len(out.strip()):
+ logger.debug('Could not get patch-id of %s', pmsg.full_subject)
+ return
+ chunks = out.split()
+ patches.append((pmsg.subject, chunks[0]))
+
+ lmsg = lser.patches[0]
+ if lmsg is None:
+ lmsg = lser.patches[1]
+
+ allto = email.utils.getaddresses([str(x) for x in lmsg.msg.get_all('to', [])])
+ allcc = email.utils.getaddresses([str(x) for x in lmsg.msg.get_all('cc', [])])
+ quotelines = list()
+ qcount = 0
+ for line in lmsg.body.split('\n'):
+ # Quote the first paragraph only and then [snip] if we quoted more than 5 lines
+ if qcount > 5 and (not len(line.strip()) or line.strip().find('---') == 0):
+ quotelines.append('> ')
+ quotelines.append('> [...]')
+ break
+ quotelines.append('> %s' % line.strip('\r\n'))
+ qcount += 1
+
+ out = {
+ 'msgid': lmsg.msgid,
+ 'subject': lmsg.full_subject,
+ 'fromname': lmsg.fromname,
+ 'fromemail': lmsg.fromemail,
+ 'to': b4.format_addrs(allto),
+ 'cc': b4.format_addrs(allcc),
+ 'references': b4.LoreMessage.clean_header(lmsg.msg['References']),
+ 'sentdate': b4.LoreMessage.clean_header(lmsg.msg['Date']),
+ 'quote': '\n'.join(quotelines),
+ 'patches': patches,
+ }
+ fullpath = os.path.join(datadir, filename)
+ with open(fullpath, 'w', encoding='utf-8') as fh:
+ json.dump(out, fh, ensure_ascii=False, indent=4)
+ logger.debug('Wrote %s for thanks tracking', filename)
+
+
def am_mbox_to_quilt(am_mbx, q_dirname):
if os.path.exists(q_dirname):
logger.critical('ERROR: Directory %s exists, not saving quilt patches', q_dirname)
diff --git a/b4/pr.py b/b4/pr.py
index 7efd398..2215861 100644
--- a/b4/pr.py
+++ b/b4/pr.py
@@ -10,6 +10,7 @@ import sys
import b4
import re
import mailbox
+import json
from datetime import timedelta
from tempfile import mkstemp
@@ -69,18 +70,6 @@ def git_get_commit_id_from_repo_ref(repo, ref):
return commit_id
-def git_commit_exists(gitdir, commit_id):
- gitargs = ['cat-file', '-e', commit_id]
- ecode, out = b4.git_run_command(gitdir, gitargs)
- return ecode == 0
-
-
-def git_branch_contains(gitdir, commit_id):
- gitargs = ['branch', '--contains', commit_id]
- lines = b4.git_get_command_lines(gitdir, gitargs)
- return lines
-
-
def parse_pr_data(msg):
lmsg = b4.LoreMessage(msg)
if lmsg.body is None:
@@ -168,7 +157,7 @@ def attest_fetch_head(gitdir, lmsg):
def fetch_remote(gitdir, lmsg, branch=None):
# Do we know anything about this base commit?
- if lmsg.pr_base_commit and not git_commit_exists(gitdir, lmsg.pr_base_commit):
+ if lmsg.pr_base_commit and not b4.git_commit_exists(gitdir, lmsg.pr_base_commit):
logger.critical('ERROR: git knows nothing about commit %s', lmsg.pr_base_commit)
logger.critical(' Are you running inside a git checkout and is it up-to-date?')
return 1
@@ -203,9 +192,45 @@ def fetch_remote(gitdir, lmsg, branch=None):
else:
logger.info('Successfully fetched into FETCH_HEAD')
+ thanks_record_pr(lmsg)
+
return 0
+def thanks_record_pr(lmsg):
+ datadir = b4.get_data_dir()
+ # Check if we're tracking it already
+ filename = '%s.pr' % lmsg.pr_remote_tip_commit
+ for entry in os.listdir(datadir):
+ if entry == filename:
+ return
+ allto = utils.getaddresses([str(x) for x in lmsg.msg.get_all('to', [])])
+ allcc = utils.getaddresses([str(x) for x in lmsg.msg.get_all('cc', [])])
+ quotelines = list()
+ for line in lmsg.body.split('\n'):
+ if line.find('---') == 0:
+ break
+ quotelines.append('> %s' % line.strip('\r\n'))
+
+ out = {
+ 'msgid': lmsg.msgid,
+ 'subject': lmsg.full_subject,
+ 'fromname': lmsg.fromname,
+ 'fromemail': lmsg.fromemail,
+ 'to': b4.format_addrs(allto),
+ 'cc': b4.format_addrs(allcc),
+ 'references': b4.LoreMessage.clean_header(lmsg.msg['References']),
+ 'remote': lmsg.pr_repo,
+ 'ref': lmsg.pr_ref,
+ 'sentdate': b4.LoreMessage.clean_header(lmsg.msg['Date']),
+ 'quote': '\n'.join(quotelines),
+ }
+ fullpath = os.path.join(datadir, filename)
+ with open(fullpath, 'w', encoding='utf-8') as fh:
+ json.dump(out, fh, ensure_ascii=False, indent=4)
+ logger.debug('Wrote %s for thanks tracking', filename)
+
+
def explode(gitdir, lmsg, savefile):
# We always fetch into FETCH_HEAD when exploding
ecode = fetch_remote(gitdir, lmsg)
@@ -293,6 +318,8 @@ def explode(gitdir, lmsg, savefile):
def main(cmdargs):
+ gitdir = cmdargs.gitdir
+
msgid = b4.get_msgid(cmdargs)
savefile = mkstemp()[1]
mboxfile = b4.get_pi_thread_by_msgid(msgid, savefile)
@@ -315,7 +342,6 @@ def main(cmdargs):
logger.critical('ERROR: Could not find pull request info in %s', msgid)
sys.exit(1)
- gitdir = cmdargs.gitdir
if not lmsg.pr_tip_commit:
lmsg.pr_tip_commit = lmsg.pr_remote_tip_commit
@@ -328,11 +354,11 @@ def main(cmdargs):
sys.exit(1)
explode(gitdir, lmsg, savefile)
- exists = git_commit_exists(gitdir, lmsg.pr_tip_commit)
+ exists = b4.git_commit_exists(gitdir, lmsg.pr_tip_commit)
if exists:
# Is it in any branch, or just flapping in the wind?
- branches = git_branch_contains(gitdir, lmsg.pr_tip_commit)
+ branches = b4.git_branch_contains(gitdir, lmsg.pr_tip_commit)
if len(branches):
logger.info('Pull request tip commit exists in the following branches:')
for branch in branches:
diff --git a/b4/ty.py b/b4/ty.py
new file mode 100644
index 0000000..245525a
--- /dev/null
+++ b/b4/ty.py
@@ -0,0 +1,434 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2020 by the Linux Foundation
+#
+__author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>'
+
+import os
+import sys
+import b4
+import re
+import email
+import email.message
+import json
+
+from string import Template
+from email import utils
+from pathlib import Path
+
+logger = b4.logger
+
+DEFAULT_PR_TEMPLATE = """
+On ${sentdate} ${fromname} wrote:
+${quote}
+
+Merged, thanks!
+
+Best regards,
+--
+${myname} <${myemail}>
+"""
+
+DEFAULT_AM_TEMPLATE = """
+On ${sentdate} ${fromname} wrote:
+${quote}
+
+Applied, thanks!
+
+Best regards,
+--
+${myname} <${myemail}>
+"""
+
+# Used to track commits created by current user
+MY_COMMITS = None
+
+
+def git_get_merge_id(gitdir, commit_id):
+ # get merge commit id
+ args = ['rev-list', '%s..' % commit_id, '--ancestry-path']
+ lines = b4.git_get_command_lines(gitdir, args)
+ if not len(lines):
+ return None
+ return lines[-1]
+
+
+def git_get_rev_diff(gitdir, rev):
+ args = ['diff', '%s~..%s' % (rev, rev)]
+ return b4.git_run_command(gitdir, args)
+
+
+def make_reply(reply_template, jsondata):
+ body = Template(reply_template).safe_substitute(jsondata)
+ # Conform to email standards
+ body = body.replace('\n', '\r\n')
+ msg = email.message_from_string(body)
+ msg['From'] = '%s <%s>' % (jsondata['myname'], jsondata['myemail'])
+ allto = utils.getaddresses([jsondata['to']])
+ allcc = utils.getaddresses([jsondata['cc']])
+ # Remove ourselves and original sender from allto or allcc
+ for entry in list(allto):
+ if entry[1] == jsondata['myemail'] or entry[1] == jsondata['fromemail']:
+ allto.remove(entry)
+ for entry in list(allcc):
+ if entry[1] == jsondata['myemail'] or entry[1] == jsondata['fromemail']:
+ allcc.remove(entry)
+
+ # Add original sender to the To
+ allto.append((jsondata['fromname'], jsondata['fromemail']))
+
+ msg['To'] = b4.format_addrs(allto)
+ msg['Cc'] = b4.format_addrs(allcc)
+ msg['In-Reply-To'] = '<%s>' % jsondata['msgid']
+ if len(jsondata['references']):
+ msg['References'] = '%s <%s>' % (jsondata['references'], jsondata['msgid'])
+ else:
+ msg['References'] = '<%s>' % jsondata['msgid']
+
+ if jsondata['subject'].find('Re: ') < 0:
+ msg['Subject'] = 'Re: %s' % jsondata['subject']
+ else:
+ msg['Subject'] = jsondata['subject']
+
+ mydomain = jsondata['myemail'].split('@')[1]
+ msg['Message-Id'] = email.utils.make_msgid(idstring='b4-ty', domain=mydomain)
+ return msg
+
+
+def auto_locate_pr(gitdir, jsondata, branch):
+ pr_commit_id = jsondata['pr_commit_id']
+ logger.debug('Checking %s', jsondata['pr_commit_id'])
+ if not b4.git_commit_exists(gitdir, pr_commit_id):
+ return None
+
+ onbranches = b4.git_branch_contains(gitdir, pr_commit_id)
+ if not len(onbranches):
+ logger.debug('%s is not on any branches', pr_commit_id)
+ return None
+ if branch not in onbranches:
+ logger.debug('%s is not on branch %s', pr_commit_id, branch)
+ return None
+
+ # Get the merge commit
+ merge_commit_id = git_get_merge_id(gitdir, pr_commit_id)
+ if not merge_commit_id:
+ logger.debug('Could not get a merge commit-id for %s', pr_commit_id)
+ return None
+
+ # Check that we are the author of the merge commit
+ gitargs = ['show', '--format=%ae', merge_commit_id]
+ out = b4.git_get_command_lines(gitdir, gitargs)
+ if not out:
+ logger.debug('Could not get merge commit author for %s', pr_commit_id)
+ return None
+
+ usercfg = b4.get_user_config()
+ if usercfg['email'] not in out:
+ logger.debug('Merged by a different author, ignoring %s', pr_commit_id)
+ logger.debug('Author: %s', out[0])
+ return None
+
+ return merge_commit_id
+
+
+def get_all_commits(gitdir, branch, since='1.week', committer=None):
+ global MY_COMMITS
+ if MY_COMMITS is not None:
+ return MY_COMMITS
+
+ MY_COMMITS = dict()
+ if committer is None:
+ usercfg = b4.get_user_config()
+ committer = usercfg['email']
+
+ gitargs = ['log', '--committer', committer, '--no-abbrev', '--oneline', '--since', since, branch]
+ lines = b4.git_get_command_lines(gitdir, gitargs)
+ if not len(lines):
+ logger.debug('No new commits from the current user --since=%s', since)
+ return MY_COMMITS
+
+ logger.info('Found %s of your comits since %s', len(lines), since)
+ logger.info('Calculating patch-ids, may take a moment...')
+ # Get patch-id of each commit
+ for line in lines:
+ commit_id, subject = line.split(maxsplit=1)
+ ecode, out = git_get_rev_diff(gitdir, commit_id)
+ gitargs = ['patch-id', '--stable']
+ ecode, out = b4.git_run_command(None, gitargs, stdin=out.encode('utf-8'))
+ chunks = out.split()
+ MY_COMMITS[chunks[0]] = (commit_id, subject)
+
+ return MY_COMMITS
+
+
+def auto_locate_series(gitdir, jsondata, branch, since='1.week'):
+ commits = get_all_commits(gitdir, branch, since)
+
+ patchids = set(commits.keys())
+ # We need to find all of them in the commits
+ found = list()
+ for patch in jsondata['patches']:
+ if patch[1] in patchids:
+ logger.debug('Found: %s', patch[0])
+ found.append(commits[patch[1]])
+
+ if len(found) == len(jsondata['patches']):
+ return found
+
+ return None
+
+
+def generate_pr_thanks(jsondata):
+ config = b4.get_main_config()
+ thanks_template = DEFAULT_PR_TEMPLATE
+ if 'thanks-pr-template' in config:
+ # Try to load this template instead
+ try:
+ with open(config['thanks-pr-template'], 'r', encoding='utf-8') as fh:
+ thanks_template = fh.read()
+ except FileNotFoundError:
+ logger.critical('ERROR: thanks-pr-template says to use %s, but it does not exist',
+ config['thanks-pr-template'])
+ sys.exit(2)
+
+ msg = make_reply(thanks_template, jsondata)
+ return msg
+
+
+def generate_am_thanks(jsondata):
+ config = b4.get_main_config()
+ thanks_template = DEFAULT_AM_TEMPLATE
+ if 'thanks-am-template' in config:
+ # Try to load this template instead
+ try:
+ with open(config['thanks-am-template'], 'r', encoding='utf-8') as fh:
+ thanks_template = fh.read()
+ except FileNotFoundError:
+ logger.critical('ERROR: thanks-am-template says to use %s, but it does not exist',
+ config['thanks-am-template'])
+ sys.exit(2)
+
+ msg = make_reply(thanks_template, jsondata)
+ return msg
+
+
+def auto_thankanator(cmdargs):
+ gitdir = cmdargs.gitdir
+ if not cmdargs.branch:
+ # Find out our current branch
+ gitargs = ['branch', '--show-current']
+ ecode, out = b4.git_run_command(gitdir, gitargs)
+ if ecode > 0:
+ logger.critical('Not able to get current branch (git branch --show-current)')
+ sys.exit(1)
+ wantbranch = out.strip()
+ else:
+ # Make sure it's a real branch
+ gitargs = ['branch', '--format=%(refname:short)', '--list']
+ lines = b4.git_get_command_lines(gitdir, gitargs)
+ if not len(lines):
+ logger.critical('Not able to get a list of branches (git branch --list)')
+ sys.exit(1)
+ if cmdargs.branch not in lines:
+ logger.critical('Requested branch %s not found in git branch --list', cmdargs.branch)
+ sys.exit(1)
+ wantbranch = cmdargs.branch
+
+ logger.info('Auto-thankinating using branch %s', wantbranch)
+ tracked = list_tracked()
+ if not len(tracked):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ applied = list()
+ for jsondata in tracked:
+ if 'pr_commit_id' in jsondata:
+ # this is a pull request
+ merge_commit_id = auto_locate_pr(gitdir, jsondata, wantbranch)
+ if merge_commit_id is None:
+ continue
+ jsondata['merge_commit_id'] = merge_commit_id
+ else:
+ # This is a patch series
+ patches = auto_locate_series(gitdir, jsondata, wantbranch, since=cmdargs.since)
+ if patches is None:
+ continue
+ applied.append(jsondata)
+ logger.info(' Located: %s', jsondata['subject'])
+
+ if not len(applied):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ logger.info('---')
+ send_messages(applied, cmdargs.outdir)
+ sys.exit(0)
+
+
+def send_messages(listing, outdir):
+ # Not really sending, but writing them out to be sent on your own
+ # We'll probably gain ability to send these once the feature is
+ # more mature and we're less likely to mess things up
+ datadir = b4.get_data_dir()
+ logger.info('Generating %s thank-you letters', len(listing))
+ # Check if the outdir exists and if it has any .thanks files in it
+ if not os.path.exists(outdir):
+ os.mkdir(outdir)
+
+ for jsondata in listing:
+ slug_from = re.sub(r'\W', '_', jsondata['fromemail'])
+ slug_subj = re.sub(r'\W', '_', jsondata['subject'])
+ slug = '%s_%s' % (slug_from.lower(), slug_subj.lower())
+ slug = re.sub(r'_+', '_', slug)
+ if 'pr_commit_id' in jsondata:
+ # This is a pull request
+ msg = generate_pr_thanks(jsondata)
+ else:
+ # This is a patch series
+ msg = generate_am_thanks(jsondata)
+
+ outfile = os.path.join(outdir, '%s.thanks' % slug)
+ logger.info(' Writing: %s', outfile)
+ with open(outfile, 'wb') as fh:
+ fh.write(msg.as_bytes(policy=b4.emlpolicy))
+ logger.debug('Cleaning up: %s', jsondata['trackfile'])
+ fullpath = os.path.join(datadir, jsondata['trackfile'])
+ os.rename(fullpath, '%s.sent' % fullpath)
+ logger.info('---')
+ logger.info('You can now run:')
+ logger.info(' git send-email %s/*.thanks', outdir)
+
+
+def list_tracked():
+ # find all tracked bits
+ tracked = list()
+ datadir = b4.get_data_dir()
+ paths = sorted(Path(datadir).iterdir(), key=os.path.getmtime)
+ usercfg = b4.get_user_config()
+ for fullpath in paths:
+ if fullpath.suffix not in ('.pr', '.am'):
+ continue
+ with fullpath.open('r', encoding='utf-8') as fh:
+ jsondata = json.load(fh)
+ jsondata['myname'] = usercfg['name']
+ jsondata['myemail'] = usercfg['email']
+ jsondata['trackfile'] = fullpath.name
+ if fullpath.suffix == '.pr':
+ jsondata['pr_commit_id'] = fullpath.stem
+ tracked.append(jsondata)
+ return tracked
+
+
+def write_tracked(tracked):
+ counter = 1
+ config = b4.get_main_config()
+ logger.info('Currently tracking:')
+ for entry in tracked:
+ logger.info('%3d: %s', counter, entry['subject'])
+ logger.info(' From: %s <%s>', entry['fromname'], entry['fromemail'])
+ logger.info(' Date: %s', entry['sentdate'])
+ logger.info(' Link: %s', config['linkmask'] % entry['msgid'])
+ counter += 1
+
+
+def send_selected(cmdargs):
+ tracked = list_tracked()
+ if not len(tracked):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ listing = list()
+ for num in cmdargs.send:
+ try:
+ index = int(num) - 1
+ listing.append(tracked[index])
+ except ValueError:
+ logger.critical('Please provide the number of the message')
+ logger.info('---')
+ write_tracked(tracked)
+ sys.exit(1)
+ except IndexError:
+ logger.critical('Invalid index: %s', num)
+ logger.info('---')
+ write_tracked(tracked)
+ sys.exit(1)
+ if not len(listing):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ send_messages(listing, cmdargs.outdir)
+
+
+def discard_selected(cmdargs):
+ tracked = list_tracked()
+ if not len(tracked):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ if '_all' in cmdargs.discard:
+ listing = tracked
+ else:
+ listing = list()
+ for num in cmdargs.discard:
+ try:
+ index = int(num) - 1
+ listing.append(tracked[index])
+ except ValueError:
+ logger.critical('Please provide the number of the message')
+ logger.info('---')
+ write_tracked(tracked)
+ sys.exit(1)
+ except IndexError:
+ logger.critical('Invalid index: %s', num)
+ logger.info('---')
+ write_tracked(tracked)
+ sys.exit(1)
+
+ if not len(listing):
+ logger.info('Nothing to do')
+ sys.exit(0)
+
+ datadir = b4.get_data_dir()
+ logger.info('Discarding %s messages', len(listing))
+ for jsondata in listing:
+ fullpath = os.path.join(datadir, jsondata['trackfile'])
+ os.rename(fullpath, '%s.discarded' % fullpath)
+ logger.info(' Discarded: %s', jsondata['subject'])
+
+ sys.exit(0)
+
+
+def check_stale_thanks(outdir):
+ if os.path.exists(outdir):
+ for entry in Path(outdir).iterdir():
+ if entry.suffix == '.thanks':
+ logger.critical('ERROR: Found existing .thanks files in: %s', outdir)
+ logger.critical(' Please send them first (or delete if already sent).')
+ logger.critical(' Refusing to run to avoid potential confusion.')
+ sys.exit(1)
+
+
+def main(cmdargs):
+ usercfg = b4.get_user_config()
+ if 'email' not in usercfg:
+ logger.critical('Please set user.email in gitconfig to use this feature.')
+ sys.exit(1)
+
+ if cmdargs.auto:
+ auto_thankanator(cmdargs)
+ check_stale_thanks(cmdargs.outdir)
+ elif cmdargs.send:
+ send_selected(cmdargs)
+ check_stale_thanks(cmdargs.outdir)
+ elif cmdargs.discard:
+ discard_selected(cmdargs)
+ else:
+ tracked = list_tracked()
+ if not len(tracked):
+ logger.info('No thanks necessary.')
+ sys.exit(0)
+ write_tracked(tracked)
+ logger.info('---')
+ logger.info('You can send them using:')
+ logger.info(' b4 ty -s 1 [2 3 ...]')