| # SPDX-License-Identifier: GPL-2.0 | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import _hkml | |
| import _hkml_fmtstr | |
| import _hkml_list_cache | |
| import _hkml_sashiko_dev | |
| import hkml_list | |
| import hkml_open | |
| import hkml_patch_format | |
| import hkml_reply | |
| import hkml_write | |
| import hkml_send | |
| class Patch: | |
| mail = None | |
| collected_patch_tags = None | |
| cv_text = None | |
| additional_to = None | |
| additional_cc = None | |
| def __init__(self, mail): | |
| self.mail = mail | |
| self.collected_patch_tags = {} # {tag: [val]} | |
| self.cv_text = None | |
| self.additional_to = [] | |
| self.additional_cc = [] | |
| def set_cv_text(self, cv_mail, sz_series): | |
| subject = cv_mail.subject | |
| cv_text_lines = _hkml_fmtstr.wrap_line( | |
| 'Patch series', '\'%s\'' % subject, 72) | |
| cv_text_lines.append('') | |
| # drop summary, diff, baseline/git version | |
| cv_paragraphs = cv_mail.get_body().strip().split('\n\n') | |
| cv_msg = '\n\n'.join(cv_paragraphs[:-3]) | |
| cv_text_lines.append(cv_msg) | |
| cv_text_lines.append('') | |
| cv_text_lines.append('This patch (of %d):' % sz_series) | |
| self.cv_text = '\n'.join(cv_text_lines) | |
| def add_recipients(self, to, cc): | |
| if to is not None: | |
| self.additional_to += to | |
| if cc is not None: | |
| self.additional_cc += cc | |
| def add_tag(self, tag_line): | |
| fields = tag_line.split() | |
| tag_name = fields[0] | |
| val = ' '.join(fields[1:]) | |
| if not tag_name in self.collected_patch_tags: | |
| self.collected_patch_tags[tag_name] = [] | |
| if val in self.collected_patch_tags[tag_name]: | |
| return | |
| self.collected_patch_tags[tag_name].append(val) | |
| def format_tags_par(self, orig_tags_par): | |
| if len(self.collected_patch_tags) == 0: | |
| return orig_tags_par, None | |
| tags = {} | |
| orig_tag_lines = orig_tags_par.split('\n') | |
| for tag_line in orig_tag_lines: | |
| fields = tag_line.split() | |
| if len(fields) < 2: | |
| return None, 'invalid tag line (%s)' % tag_line | |
| tag = fields[0] | |
| val = ' '.join(fields[1:]) | |
| if not tag in tags: | |
| tags[tag] = [] | |
| tags[tag].append(val) | |
| for tag in self.collected_patch_tags: | |
| if not tag in tags: | |
| tags[tag] = [] | |
| for val in self.collected_patch_tags[tag]: | |
| if not val in tags[tag]: | |
| tags[tag].append(val) | |
| tag_lines = [] | |
| first_tags = ['Link:', 'Fixes:', 'Reported-by:', 'Closes:', 'Cc:'] | |
| for tag in first_tags: | |
| if not tag in tags: | |
| continue | |
| for val in tags[tag]: | |
| tag_lines.append('%s %s' % (tag, val)) | |
| del tags[tag] | |
| last_tags = ['Acked-by:', 'Reviewed-by:', 'Co-developed-by:', | |
| 'Signed-off-by:'] | |
| for tag in tags: | |
| if tag in last_tags: | |
| continue | |
| for val in tags[tag]: | |
| tag_lines.append('%s %s' % (tag, val)) | |
| for tag in last_tags: | |
| if not tag in tags: | |
| continue | |
| for val in tags[tag]: | |
| tag_lines.append('%s %s' % (tag, val)) | |
| return '\n'.join(tag_lines), None | |
| def format_str(self): | |
| '''Returns formatted text and error''' | |
| mail_text = hkml_open.mail_display_str( | |
| self.mail, head_columns=None, valid_mbox=True) | |
| pars = mail_text.split('\n\n') | |
| additional_header_lines = [] | |
| for recipient in self.additional_to: | |
| additional_header_lines.append('To: %s' % recipient) | |
| for recipient in self.additional_cc: | |
| additional_header_lines.append('Cc: %s' % recipient) | |
| if len(additional_header_lines) > 0: | |
| pars[0] += '\n' + '\n'.join(additional_header_lines) | |
| mail_text = '\n\n'.join(pars) | |
| if is_cover_letter(self.mail): | |
| return mail_text, None | |
| three_dash_split = mail_text.split('\n---\n') | |
| if len(three_dash_split) < 2: | |
| return None, 'No three dash' | |
| header_desc = three_dash_split[0].strip() | |
| header_desc_pars = header_desc.split('\n\n') | |
| if self.cv_text is not None: | |
| header_desc_pars.insert(1, self.cv_text) | |
| tags_par, err = self.format_tags_par(header_desc_pars[-1]) | |
| if err is not None: | |
| return None, 'tags section format fail (%s)' % err | |
| header_desc_pars[-1] = tags_par | |
| header_desc = '\n\n'.join(header_desc_pars) | |
| return '\n---\n'.join([header_desc] + three_dash_split[1:]), None | |
| def find_mail_item_from_thread(mail_item, msgid): | |
| if mail_item.mail is None: | |
| return None | |
| if mail_item.mail.get_msgid() == msgid: | |
| return mail_item | |
| for reply in mail_item.reply_items: | |
| found_item = find_mail_item_from_thread(reply, msgid) | |
| if found_item is not None: | |
| return found_item | |
| return None | |
| def get_mail_item_with_replies(msgid): | |
| list_data = _hkml_list_cache.get_last_list(except_thread=False) | |
| mail_items = list_data.mail_items | |
| for item in mail_items: | |
| item.set_mail() | |
| thread_items = hkml_list.thread_items_of( | |
| mail_items, do_find_ancestors_from_cache=False) | |
| for thread_root_item in thread_items: | |
| item_with_replies = find_mail_item_from_thread(thread_root_item, msgid) | |
| if item_with_replies is not None: | |
| return item_with_replies | |
| return None | |
| def user_pointed_mail_item(mail_identifier): | |
| if mail_identifier.isdigit(): | |
| list_data = _hkml_list_cache.get_last_list(except_thread=False) | |
| mail_items = list_data.mail_items | |
| mail_idx = int(mail_identifier) | |
| if mail_idx >= len(mail_items): | |
| return None, 'out of mail idx' | |
| mail_item = mail_items[int(mail_identifier)] | |
| elif mail_identifier == 'clipboard': | |
| mails, err = _hkml.read_mails_from_clipboard() | |
| if err != None: | |
| return None, 'reading mails in clipboard failed: %s' % err | |
| if len(mails) != 1: | |
| return None, 'multiple mails in clipboard' | |
| mail_item = hkml_list.MailListMailItem( | |
| mail_cache_key=None, mail=mails[0], prdepth=0, | |
| parent_item=None, added_by_tag=None) | |
| else: | |
| return None, 'unsupported <mail> (%s)' % mail_identifier | |
| mail_item.set_mail() | |
| mail_item = get_mail_item_with_replies( | |
| mail_item.mail.get_msgid()) | |
| if mail_item is None: | |
| return None, 'get replies of the mail fail' | |
| return mail_item, None | |
| def recipient_mail(recipient): | |
| fields = recipient.split() | |
| mail = fields[-1] | |
| if mail.startswith('<') and mail.endswith('>'): | |
| mail = mail[1:-1] | |
| return mail | |
| def do_check_recipients(patch_files, patch_mails): | |
| ''' | |
| Returns problematic patches and an error message | |
| ''' | |
| get_maintainer_pl = os.path.join('scripts', 'get_maintainer.pl') | |
| if not os.path.isfile(get_maintainer_pl): | |
| return [], '%s not found' % get_maintainer_pl | |
| cmd = [get_maintainer_pl, '--nogit', '--nogit-fallback', '--norolestats'] | |
| missing_recipients = {} | |
| for idx, patch_file in enumerate(patch_files): | |
| if is_cover_letter(patch_mails[idx]): | |
| continue | |
| expects = _hkml.cmd_lines_output(cmd + [patch_file]) | |
| patch_mail = patch_mails[idx] | |
| recipients = recipients_of(patch_mail, 'to') | |
| recipients += recipients_of(patch_mail, 'cc') | |
| recipient_mails = [recipient_mail(r) for r in recipients] | |
| for recipient in expects: | |
| if recipient == '': | |
| continue | |
| mail = recipient_mail(recipient) | |
| if mail in recipient_mails: | |
| continue | |
| subject = patch_mail.subject | |
| if not subject in missing_recipients: | |
| missing_recipients[subject] = [] | |
| missing_recipients[subject].append(recipient) | |
| if len(missing_recipients) == 0: | |
| return [], None | |
| for subject in missing_recipients: | |
| print('MISSING RECIPIENTS for "%s"' % subject) | |
| for recipient in missing_recipients[subject]: | |
| print('- %s' % recipient) | |
| return sorted(missing_recipients.keys()), None | |
| def rm_tmp_patch_dir(patch_files): | |
| dirname = os.path.dirname(patch_files[-1]) | |
| for patch_file in patch_files: | |
| os.remove(patch_file) | |
| os.rmdir(dirname) | |
| def run_checker(checker, patch_files, patch_mails, rm_patches): | |
| ''' | |
| Returns list of problematic patches and an error message | |
| ''' | |
| checkpatch = os.path.join('scripts', 'checkpatch.pl') | |
| if checker is None: | |
| if os.path.isfile(checkpatch): | |
| checker = checkpatch | |
| else: | |
| return [], '<checker> is none; checkpatch.pl is not found' | |
| complained_patches = [] | |
| for idx, patch_file in enumerate(patch_files): | |
| try: | |
| output = subprocess.check_output([checker, patch_file]).decode() | |
| if checker == checkpatch: | |
| last_par = output.split('\n\n')[-1] | |
| if not 'and is ready for submission.' in last_par: | |
| raise Exception('checkpatch.pl output seems wrong') | |
| except Exception as e: | |
| print('[!!!] %s complained by %s (%s)' % ( | |
| patch_mails[idx].subject, checker, e)) | |
| subprocess.call([checker, patch_file]) | |
| print() | |
| complained_patches.append(patch_mails[idx].subject) | |
| if rm_patches: | |
| rm_tmp_patch_dir(patch_files) | |
| return complained_patches, None | |
| def check_patches( | |
| checker, patch_files, patch_mails, rm_patches, check_recipients): | |
| if patch_mails is None: | |
| patch_mails = [] | |
| for patch_file in patch_files: | |
| patch_mails.append(_hkml.read_mbox_file(patch_file)[0]) | |
| if check_recipients in ['do', 'only']: | |
| rec_missing_patches, err = do_check_recipients( | |
| patch_files, patch_mails) | |
| if err is not None: | |
| print('recipient check fail (%s)' % err) | |
| if check_recipients == 'only': | |
| return None | |
| check_fail_patches, err = run_checker( | |
| checker, patch_files, patch_mails, rm_patches) | |
| print() | |
| if len(rec_missing_patches) + len(check_fail_patches) == 0: | |
| print('Seems all clean!') | |
| return None | |
| if len(rec_missing_patches) > 0: | |
| print('Below patches may missing some recipients') | |
| for p in rec_missing_patches: | |
| print('- %s' % p) | |
| if len(check_fail_patches) > 0: | |
| print('Below patches may have more problems') | |
| for p in check_fail_patches: | |
| print('- %s' % p) | |
| def git_am(patch_files, repo): | |
| for patch_file in patch_files: | |
| rc = subprocess.call(['git', '-C', repo, 'am', patch_file]) | |
| if rc != 0: | |
| return 'applying patch (%s) failed' % patch_file | |
| return None | |
| def add_noff_merge_commit(base_commit, message, git_cmd=['git']): | |
| final_commit = subprocess.check_output( | |
| git_cmd + ['rev-parse', 'HEAD']).decode().strip() | |
| subprocess.call(git_cmd + ['reset', '--hard', base_commit]) | |
| subprocess.call(git_cmd + ['merge', '--no-ff', '--no-edit', final_commit]) | |
| subprocess.call(git_cmd + ['commit', '--amend', '-s', '-m', message]) | |
| def git_cherrypick_merge(patch_files, cv_mail, repo): | |
| git_cmd = ['git', '-C', repo] | |
| head_commit = subprocess.check_output( | |
| git_cmd + ['rev-parse', 'HEAD']).decode().strip() | |
| err = git_am(patch_files[1:], repo) | |
| if err is not None: | |
| return err | |
| cv_merge_msg = '\n'.join([ | |
| 'Merge patch series \'%s\'' % cv_mail.subject, '', | |
| 'Below is the cover letter of the series', '', | |
| cv_mail.get_body()]) | |
| add_noff_merge_commit(head_commit, cv_merge_msg, git_cmd) | |
| return None | |
| def apply_patches(patches, repo): | |
| err = None | |
| has_cv = len(patches) > 0 and is_cover_letter(patches[0].mail) | |
| do_merge = False | |
| if has_cv: | |
| print('How should I apply the cover letter?') | |
| print() | |
| print('1: add to first patch\'s commit message # default') | |
| print('2: add as a bogus baseline commit') | |
| print('3: add as a merge commit') | |
| print('4: Ignore it.') | |
| print() | |
| answer = input('Enter the number: ') | |
| try: | |
| answer = int(answer) | |
| except: | |
| pass | |
| if not answer in [1, 2, 3, 4]: | |
| answer = 1 | |
| if answer == 1: | |
| patches[1].set_cv_text(patches[0].mail, len(patches) - 1) | |
| elif answer == 2: | |
| cv_mail = patches[0].mail | |
| subject = '==== %s ====' % cv_mail.subject | |
| content = '%s\n\n%s' % (cv_mail.subject, cv_mail.get_body()) | |
| make_cover_letter_commit(subject, content) | |
| elif answer == 3: | |
| do_merge = True | |
| patch_files, err = write_patches(patches) | |
| if err is not None: | |
| return 'writing patch files failed (%s)' % err | |
| if do_merge: | |
| err = git_cherrypick_merge(patch_files, patches[0].mail, repo) | |
| else: | |
| if has_cv: | |
| err = git_am(patch_files[1:], repo) | |
| else: | |
| err = git_am(patch_files, repo) | |
| if err is not None: | |
| return err | |
| # cleanup tempoeral patches only when success, to let investigation easy | |
| rm_tmp_patch_dir(patch_files) | |
| return None | |
| def add_patch_suffix(basename, count): | |
| patch_sections = basename.split('.') | |
| suffix = '-' + str(count) | |
| if len(patch_sections) < 2: # No file extension | |
| return basename + suffix | |
| patch_sections[-2] += suffix | |
| return '.'.join(patch_sections) | |
| def move_patches(patch_files, dest_dir): | |
| if len(patch_files) == 0: | |
| print('no patch to export') | |
| saved_dir = os.path.dirname(patch_files[-1]) | |
| if dest_dir is not None: | |
| for idx, patch_file in enumerate(patch_files): | |
| basename = os.path.basename(patch_file) | |
| new_path = os.path.join(dest_dir, basename) | |
| # Avoid overwriting existing patches; append -N to the end until | |
| # the file path is unique | |
| if (os.path.isfile(new_path)): | |
| count = 1 | |
| while os.path.isfile(new_path): | |
| new_path = os.path.join(dest_dir, | |
| add_patch_suffix(basename, count)) | |
| count += 1 | |
| shutil.move(patch_file, new_path) | |
| patch_files[idx] = new_path | |
| os.rmdir(saved_dir) | |
| saved_dir = dest_dir | |
| print('\npatch files are saved at \'%s\' with below names:' % saved_dir) | |
| for patch_file in patch_files: | |
| print('- %s' % os.path.basename(patch_file)) | |
| print() | |
| def get_patch_index(mail): | |
| tag_end_idx = mail.subject.find(']') | |
| for field in mail.subject[:tag_end_idx].split(): | |
| idx_total = field.split('/') | |
| if len(idx_total) != 2: | |
| continue | |
| if idx_total[0].isdigit() and idx_total[1].isdigit(): | |
| return int(idx_total[0]) | |
| return None | |
| def is_cover_letter(mail): | |
| return mail.series is not None and mail.series[0] == 0 | |
| def get_link_tag_domain(): | |
| if not _hkml.is_for_lore_kernel_org(): | |
| return None | |
| site = _hkml.get_manifest()['site'] | |
| print() | |
| print(' '.join([ | |
| 'Should we add Link: tag to the patch?', | |
| 'If so, what domain to use?'])) | |
| print() | |
| print('1. Yes. Use https://patch.msgid.link (default)') | |
| print('2. Yes. Use %s' % site) | |
| print('3. No. Don\'t add Link: tag') | |
| answer = input('Select: ') | |
| try: | |
| answer = int(answer) | |
| except: | |
| answer = 1 | |
| if answer == 1: | |
| return 'https://patch.msgid.link' | |
| elif answer == 2: | |
| return site | |
| else: | |
| return None | |
| def find_add_tags(patch, patch_mail_item, mail_item_to_check): | |
| for line in mail_item_to_check.mail.get_body().split('\n'): | |
| for tag in ['Tested-by:', 'Reviewed-by:', 'Acked-by:', 'Fixes:', | |
| 'Cc: stable@', 'Cc: <stable@']: | |
| if not line.startswith(tag): | |
| continue | |
| print('Found below from "%s"' % mail_item_to_check.mail.subject) | |
| print(' %s' % line) | |
| answer = input('add the tag to the patch? [Y/n] ') | |
| if answer.lower() != 'n': | |
| patch.add_tag(line) | |
| if mail_item_to_check.reply_items is None: | |
| return | |
| for reply in mail_item_to_check.reply_items: | |
| find_add_tags(patch, patch_mail_item, reply) | |
| def add_cc_tags(patch, patch_mail_item): | |
| for recipient in recipients_of(patch.mail, 'cc'): | |
| if recipient == patch.mail.get_from(): | |
| continue | |
| # add Cc: for formal recipients, e.g., having both name and email | |
| # address, excluding mailing list. | |
| if len(recipient.split()) == 1: | |
| continue | |
| patch.add_tag('Cc: %s' % recipient) | |
| def mail_item_to_patches(mail_item, dont_add_cv): | |
| patch_mail_items = [mail_item] | |
| is_cv = is_cover_letter(mail_item.mail) | |
| if is_cv is True: | |
| if dont_add_cv == 'ask': | |
| answer = input('Add cover letter to first patch? [Y/n] ') | |
| if answer.lower() != 'n': | |
| dont_add_cv = False | |
| else: | |
| dont_add_cv = True | |
| patch_mail_items += [ | |
| r for r in mail_item.reply_items | |
| if 'patch' in r.mail.subject_tags] | |
| patches = [] | |
| link_domain = get_link_tag_domain() | |
| for patch_mail_item in patch_mail_items: | |
| patch = Patch(patch_mail_item.mail) | |
| patches.append(patch) | |
| if link_domain is not None: | |
| msgid = patch_mail_item.mail.get_msgid() | |
| if msgid.startswith('<') and msgid.endswith('>'): | |
| msgid = msgid[1:-1] | |
| url = '%s/%s' % (link_domain, msgid) | |
| patch.add_tag('Link: %s' % url) | |
| if patch_mail_item.reply_items is None: | |
| continue | |
| if is_cover_letter(patch_mail_item.mail): | |
| continue | |
| for reply in patch_mail_item.reply_items: | |
| find_add_tags(patch, patch_mail_item, reply) | |
| add_cc_tags(patch, patch_mail_item) | |
| user_name = subprocess.check_output( | |
| ['git', 'config', 'user.name']).decode().strip() | |
| user_email = subprocess.check_output( | |
| ['git', 'config', 'user.email']).decode().strip() | |
| patch.add_tag('Signed-off-by: %s <%s>' % (user_name, user_email)) | |
| patches.sort(key=lambda patch: get_patch_index(patch.mail)) | |
| if is_cv and dont_add_cv is False: | |
| print('Given mail seems the cover letter of the patchset.') | |
| print('Adding the cover letter on the first patch.') | |
| patches[1].set_cv_text(patches[0].mail, len(patches) - 1) | |
| return patches | |
| def write_patches(patches): | |
| if len(patches) > 9999: | |
| return None, '>9999 patches' | |
| files = [] | |
| temp_dir = tempfile.mkdtemp(prefix='hkml_patch_') | |
| # give index 0 to only coverletter | |
| if is_cover_letter(patches[0].mail): | |
| idx_offset = 0 | |
| else: | |
| idx_offset = 1 | |
| for idx, patch in enumerate(patches): | |
| file_name_words = ['%04d-' % (idx + idx_offset)] | |
| subject = patch.mail.subject.lower() | |
| # exclude [PATCH ...] like suffix | |
| tag_closing_idx = subject.find(']') | |
| subject = subject[tag_closing_idx + 1:] | |
| for c in subject: | |
| if not c.isalpha() and not c.isdigit(): | |
| # avoid multiple '-' in the name | |
| if file_name_words[-1][-1] == '-': | |
| continue | |
| c = '-' | |
| file_name_words.append(c) | |
| file_name_words.append('.patch') | |
| file_name = ''.join(file_name_words) | |
| file_name = os.path.join(temp_dir, file_name) | |
| text, err = patch.format_str() | |
| if err is not None: | |
| return None, err | |
| # Without ending new line, 'git am' says "error: corrupt patch" | |
| if not text.endswith('\n'): | |
| text += '\n' | |
| with open(file_name, 'w') as f: | |
| f.write(text) | |
| files.append(file_name) | |
| return files, None | |
| def check_apply_or_export_item(mail_item, args): | |
| patches = mail_item_to_patches(mail_item, args.dont_add_cv) | |
| if args.action == 'apply': | |
| return apply_patches(patches, args.repo) | |
| patch_files, err = write_patches(patches) | |
| if err is not None: | |
| return 'writing patch files failed (%s)' % err | |
| if args.action == 'check': | |
| return check_patches( | |
| args.checker, patch_files, [patch.mail for patch in patches], | |
| rm_patches=True, check_recipients=args.check_recipients) | |
| if args.action == 'export': | |
| move_patches(patch_files, args.export_dir) | |
| return None | |
| def recipients_of(mail, to_cc): | |
| field = mail.get_field(to_cc) | |
| if field is None: | |
| return [] | |
| recipients = [] | |
| for r in field.split(','): | |
| r = r.strip() | |
| if r != '': | |
| recipients.append(r) | |
| return recipients | |
| def common_recipients(patch_mails, to_cc): | |
| if len(patch_mails) == 0: | |
| return [] | |
| to_return = [] | |
| for recipient in recipients_of(patch_mails[0], to_cc): | |
| is_common = True | |
| for mail in patch_mails[1:]: | |
| if not recipient in recipients_of(mail, to_cc): | |
| is_common = False | |
| break | |
| if is_common is True: | |
| to_return.append(recipient) | |
| return to_return | |
| def list_recipients(patch_files): | |
| patch_mails = [] | |
| for patch_file in patch_files: | |
| patch_mails.append(_hkml.read_mbox_file(patch_file)[0]) | |
| common_to = common_recipients(patch_mails, 'to') | |
| common_cc = common_recipients(patch_mails, 'cc') | |
| print('Common recipients:') | |
| for to in common_to: | |
| print(' To: %s' % to) | |
| for cc in common_cc: | |
| print(' Cc: %s' % cc) | |
| if len(common_to) + len(common_cc) == 0: | |
| print(' No one') | |
| for patch_mail in patch_mails: | |
| exclusive_to = [] | |
| exclusive_cc = [] | |
| for recipient in recipients_of(patch_mail, 'to'): | |
| if not recipient in common_to: | |
| exclusive_to.append(recipient) | |
| for recipient in recipients_of(patch_mail, 'cc'): | |
| if not recipient in common_cc: | |
| exclusive_cc.append(recipient) | |
| if len(exclusive_to + exclusive_cc) > 0: | |
| print('Additional recipients for "%s"' % patch_mail.subject) | |
| for to in exclusive_to: | |
| print(' To: %s' % to) | |
| for cc in exclusive_cc: | |
| print(' Cc: %s' % cc) | |
| def is_files_argument(arg): | |
| if type(arg) is not list: | |
| return False | |
| for entry in arg: | |
| if not os.path.isfile(entry): | |
| return False | |
| return True | |
| def make_cover_letter_commit(subject, content=None): | |
| bogus_dir = 'hkml_cv_bogus' | |
| if not os.path.isdir(bogus_dir): | |
| os.mkdir(bogus_dir) | |
| _, bogus_path = tempfile.mkstemp(prefix='hkml_cv_bogus_', dir=bogus_dir) | |
| err = subprocess.call(['git', 'add', bogus_path]) | |
| if err: | |
| print('git add failed') | |
| return -1 | |
| message = subject | |
| if content is not None: | |
| message = '%s\n\n%s' % (message, content) | |
| return subprocess.call(['git', 'commit', '-s', '-m', message]) | |
| def fmt_sashiko_reviews_summary(msgid, for_forwarding=False): | |
| reviews, err = _hkml_sashiko_dev.get_reviews(msgid) | |
| if err is not None: | |
| return None, 'fetching reviews fail (%s)' % err | |
| lines = [] | |
| if for_forwarding is True: | |
| lines = [ | |
| 'Forwarding sashiko.dev review status for this thread.', | |
| '', | |
| '# review url: https://sashiko.dev/#/patchset/%s' % msgid, | |
| '', | |
| ] | |
| if len(reviews) == 0: | |
| lines.append('zero review') | |
| return '\n'.join(lines), None | |
| for review in reviews: | |
| lines.append('- %s' % review.patch_subject) | |
| lines.append(' - status: %s' % review.status) | |
| result = review.result | |
| if result is not None and result != 'Review completed successfully.': | |
| lines.append(' - result: %s' % result) | |
| inline_review = review.inline_review | |
| if inline_review is not None and len(inline_review.split('\n')) == 1: | |
| lines.append(' - review: %s' % inline_review) | |
| elif inline_review is not None and len(inline_review.split('\n')) > 1: | |
| lines.append(' - review: ISSUES MAY FOUND') | |
| lines += [ | |
| '', | |
| '# hkml [1] generated a draft of this mail. It can be regenerated', | |
| '# using below command:', | |
| '#', | |
| '# hkml patch sashiko_dev --thread_status --for_forwarding \\', | |
| '# %s' % msgid, | |
| '#', | |
| '# [1] https://github.com/sjp38/hackermail', | |
| ] | |
| return '\n'.join(lines), None | |
| def fetch_pr_sashiko_reviews(msgid, for_forwarding): | |
| text, err = fmt_sashiko_reviews_summary(msgid, for_forwarding) | |
| if err is not None: | |
| print(err) | |
| return -1 | |
| print(text) | |
| return 0 | |
| def fmt_sashiko_forward_msg(review): | |
| msgid = review.patch_msgid | |
| review_url = 'https://sashiko.dev/#/patchset/%s' % msgid | |
| print() | |
| answer = input( | |
| '\n'.join([ | |
| 'Adding your opinion together with the sashiko review sharing', | |
| 'is a recommended practice for reducing traffic. Will you', | |
| 'do so? If so, I will adjust format to be easier for that', | |
| '[Y/n] ' | |
| ])) | |
| if answer.lower() != 'n': | |
| reply_form = True | |
| else: | |
| reply_form = False | |
| lines = [] | |
| if reply_form: | |
| lines += [ | |
| '# sashiko review suggestions', | |
| '#', | |
| '# 1. Consider reducing recipients. Maybe the author,', | |
| '# maintainers, reviewers, and mailing list of the', | |
| '# direct subsystem and parent susystem mailing lists', | |
| '# could be a starting point.', | |
| '# 2. Add short summary of your opinion at the beginning.', | |
| '# For example:', | |
| '#', | |
| '# TL;DR: sashiko found an issue. I will respin.', | |
| '# TL;DR: sashiko found no issue on this patch.', | |
| '#', | |
| '# Please DON\'T FORGET removing this comment block before', | |
| '# sending this!', | |
| '', | |
| 'Forwarding full sashiko review in a reply format with my ', | |
| 'inline comments below, for sharing details of my view and', | |
| 'doing followup discussions via mails if needed.', | |
| ] | |
| else: | |
| lines.append( | |
| 'Forwarding sashiko review for doing discussions via mails.') | |
| lines.append('') | |
| forward_lines = [ | |
| '# review url: %s' % review_url, | |
| '# start of sashiko.dev inline review', | |
| '%s' % review.inline_review, | |
| '# end of sashiko.dev inline review', | |
| '# review url: %s' % review_url, | |
| ] | |
| if reply_form: | |
| forward_lines = '\n'.join(forward_lines).split('\n') | |
| reply_lines = ['> %s' % l for l in forward_lines] | |
| lines += reply_lines | |
| else: | |
| lines += forward_lines | |
| lines += [ | |
| '', | |
| '# hkml [1] generated a draft of this mail. You can regenerate', | |
| '# this using below command:', | |
| '#', | |
| '# hkml patch sashiko_dev --for_forwarding \\', | |
| '# %s' % msgid, | |
| '#', | |
| '# [1] https://github.com/sjp38/hackermail', | |
| ] | |
| return '\n'.join(lines) | |
| def pr_sashiko_for_forwarding(review): | |
| print(fmt_sashiko_forward_msg(review)) | |
| def fetch_pr_sashiko_review(msgid, thread_status, for_forwarding): | |
| if thread_status is True: | |
| return fetch_pr_sashiko_reviews(msgid, for_forwarding) | |
| review, err = _hkml_sashiko_dev.get_review(msgid) | |
| if err is not None: | |
| print('fetching review fail (%s)' % err) | |
| return -1 | |
| if for_forwarding is True: | |
| return pr_sashiko_for_forwarding(review) | |
| print('# patch subject: %s' % review.patch_subject) | |
| print('# patch msgid: %s' % review.patch_msgid) | |
| print('# review status: %s' % review.status) | |
| print('# reivew result: %s' % review.result) | |
| print('# inline review:') | |
| print(review.inline_review) | |
| print('# end of sashiko.dev inline review') | |
| print('# review url: https://sashiko.dev/#/patchset/%s' % msgid) | |
| def forward_sashiko(msgid, thread_status, mail=None): | |
| if mail is None: | |
| mails, err = hkml_list.get_thread_mails_from_web(msgid) | |
| if err is not None: | |
| print('retrieving mail from web fail (%s)' % err) | |
| return -1 | |
| mail = None | |
| for m in mails: | |
| if m.get_msgid() == '<%s>' % msgid: | |
| mail = m | |
| break | |
| if mail is None: | |
| print('cannot find mail from web-retrieved mails') | |
| return -1 | |
| orig_subject = mail.subject | |
| if thread_status is True: | |
| text, err = fmt_sashiko_reviews_summary(msgid, for_forwarding=True) | |
| if err is not None: | |
| print(err) | |
| return -1 | |
| reply_subject = 'Re: (sashiko status) %s' % orig_subject | |
| else: | |
| review, err = _hkml_sashiko_dev.get_review(msgid) | |
| if err is not None: | |
| print('fetching review fail (%s)' % err) | |
| return -1 | |
| text = fmt_sashiko_forward_msg(review) | |
| reply_subject = 'Re: (sashiko review) %s' % orig_subject | |
| mbox_str = hkml_reply.format_reply( | |
| mail=mail, attach_file=None, body_lines=[text], | |
| subject=reply_subject) | |
| fd, forward_tmp_path = tempfile.mkstemp(prefix='hkml_sashiko_forward_') | |
| with open(forward_tmp_path, 'w') as f: | |
| f.write(mbox_str) | |
| err = hkml_write.open_editor(forward_tmp_path) | |
| if err is not None: | |
| print(err) | |
| return -1 | |
| hkml_send.send_mail(forward_tmp_path, get_confirm=True, erase_mbox=True, | |
| orig_draft_subject=None) | |
| def handle_sashiko(msgid, thread_status, for_forwarding, do_forward): | |
| if do_forward is False: | |
| return fetch_pr_sashiko_review(msgid, thread_status, for_forwarding) | |
| return forward_sashiko(msgid, thread_status) | |
| def main(args): | |
| if args.action == 'format': | |
| return hkml_patch_format.main(args) | |
| elif args.action == 'recipients': | |
| return list_recipients(args.patch_files) | |
| elif args.action == 'commit_cv': | |
| if args.as_merge is not None: | |
| return add_noff_merge_commit( | |
| args.as_merge, args.subject, git_cmd=['git']) | |
| return make_cover_letter_commit(args.subject) | |
| elif args.action == 'sashiko_dev': | |
| return handle_sashiko( | |
| args.msgid, args.thread_status, args.for_forwarding, | |
| args.forward) | |
| if args.action == 'check': | |
| if is_files_argument(args.patch): | |
| err = check_patches( | |
| args.checker, args.patch, None, rm_patches=False, | |
| check_recipients=args.check_recipients) | |
| if err is not None: | |
| print(err) | |
| return 1 | |
| return 0 | |
| elif len(args.patch) > 1: | |
| print('wrong patch argument') | |
| return 1 | |
| else: | |
| args.mail = args.patch[0] | |
| mail_item, err = user_pointed_mail_item(args.mail) | |
| if err is not None: | |
| print(err) | |
| exit(1) | |
| err = check_apply_or_export_item(mail_item, args) | |
| if err is not None: | |
| print(err) | |
| exit(1) | |
| def set_argparser(parser): | |
| parser.description = 'handle patch series mail thread' | |
| parser.add_argument('--dont_add_cv', action='store_true', | |
| help='don\'t add cover letter to first patch') | |
| if sys.version_info >= (3,7): | |
| subparsers = parser.add_subparsers( | |
| title='action', dest='action', metavar='<action>', | |
| required=True) | |
| else: | |
| subparsers = parser.add_subparsers( | |
| title='action', dest='action', metavar='<action>') | |
| parser_apply = subparsers.add_parser('apply', help='apply the patch') | |
| parser_apply.add_argument( | |
| 'mail', metavar='<mail>', | |
| help=' '.join( | |
| ['The mail to apply as a patch.', | |
| 'Could be index on the list, or \'clipboard\''])) | |
| parser_apply.add_argument('--repo', metavar='<dir>', default='./', | |
| help='git repo to apply the patch') | |
| parser_check = subparsers.add_parser('check', | |
| help='run a checker for the patch') | |
| parser_check.add_argument( | |
| 'patch', metavar='<mail or patch file>', nargs='+', | |
| help=' '.join( | |
| ['The mail or patch files to check.', | |
| 'In case of a mail, this could be index on the list,', | |
| 'or \'clipboard\''])) | |
| parser_check.add_argument('checker', metavar='<program>', nargs='?', | |
| help='patch checker program') | |
| parser_check.add_argument('--check_recipients', | |
| choices=['do', 'skip', 'only'], default='do', | |
| help='control recipients check') | |
| parser_export = subparsers.add_parser('export', help='save as patch files') | |
| parser_export.add_argument( | |
| 'mail', metavar='<mail>', | |
| help=' '.join( | |
| ['The mail to apply as a patch.', | |
| 'Could be index on the list, or \'clipboard\''])) | |
| parser_export.add_argument('--export_dir', metavar='<dir>', | |
| help='directory to save the patch files') | |
| parser_sashiko = subparsers.add_parser( | |
| 'sashiko_dev', help='fetch and show sashiko.dev review') | |
| parser_sashiko.add_argument('msgid', metavar='<message id>', | |
| help='message id of the patch mail') | |
| parser_sashiko.add_argument('--thread_status', action='store_true', | |
| help='print entire thread review status') | |
| parser_sashiko.add_argument('--for_forwarding', action='store_true', | |
| help='print in mail-forwarding friendly form') | |
| parser_sashiko.add_argument('--forward', action='store_true', | |
| help='forward the sashiko status or review') | |
| parser_format = subparsers.add_parser('format', help='format patch files') | |
| hkml_patch_format.set_argparser(parser_format) | |
| parser_recipients = subparsers.add_parser( | |
| 'recipients', help='show recipients of patch files') | |
| parser_recipients.add_argument('patch_files', metavar='<file>', nargs='+', | |
| help='the patch files') | |
| parser_cv_commit = subparsers.add_parser( | |
| 'commit_cv', help='make a commit of cover letter message') | |
| parser_cv_commit.add_argument('subject', metavar='<subject>', | |
| help='subject of the cover letter commit') | |
| parser_cv_commit.add_argument('--as_merge', metavar='<base commit>', | |
| help='make it as a no-ff merge commit') |