As we use git-format-patch under the hood, we need to handle all its arguents and some of git arguments too to handle all the use cases that are already covered by git format-patch.
If we don't use this approach, we'll probably end up having to wrap many of the features of git format-patch anyway. Ideally it should be able to retrieve the commit ID or range in an automatic way without harcoded knowledge of git-format-patch arguments, but that could be implemented in a following commit if needed. The advantages of an approach like that are mutiple: not only it would lower maintenance, but merely using a newer or patched git would automatically make the new git-format-patch improvements automatically usable with this tool, with no change of code needed. Signed-off-by: Denis 'GNUtoo' Carikli <gnu...@cyberdimension.org> --- patches/replicant_prepare_patch.py | 226 +++++++++++++++++++++++------ 1 file changed, 180 insertions(+), 46 deletions(-) diff --git a/patches/replicant_prepare_patch.py b/patches/replicant_prepare_patch.py index c1f52e7..e6b3a2d 100755 --- a/patches/replicant_prepare_patch.py +++ b/patches/replicant_prepare_patch.py @@ -14,10 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -import os, sys +import argparse +import configparser +import enum +import os import re +import sys -import configparser import sh # Settings @@ -36,10 +39,25 @@ git show {commit} """ def usage(progname): - print('Usage:\n\t{} [-C <directory>] <git_revision> [nr_patches] <patches_serie_revision>'.format( - progname)) - print('Options:') - print('\t-C <directory>\t\tRun as if it was started in the given git directory') + output = "" + + try: + output = sh.git("format-patch", "-h", _ok_code=129) + except: + pass + + output = output.replace('git format-patch', '{} [-C <path>]'.format(progname)) + + found = False + for line in output.split(os.linesep): + if line == "" and not found: + print("") + print(" -C Run as if it was started in <path> instead of the current working directory.") + print("") + found = True + else: + print(line, sep='') + sys.exit(1) def get_config(): @@ -91,14 +109,20 @@ def get_config(): config.read_file(config_file) return config +def match_array(regex_str, array): + regex = re.compile(regex_str) + + for elm in array: + if re.match(regex, elm): + return True + return False + class GitRepo(object): - def __init__(self, config, directory): + def __init__(self, config, directory=None): self.config = config - self.directory = None - - if directory: - self.directory = re.sub('/+$', '', directory) - self.git = sh.git.bake("-C", self.directory) + self.directory = directory + if self.directory: + self.git = sh.git.bake('-C', self.directory) else: self.git = sh.git.bake() @@ -142,27 +166,21 @@ class GitRepo(object): return '{project}] [PATCH'.format(project=project_name) - def generate_patches(self, git_revision, nr_patches, patches_revision): - subject_prefix = self.get_subject_prefix() - - git_arguments = ['format-patch', git_revision, '-{}'.format(nr_patches)] + def format_patch(self, git_format_patch_arguments): + git_arguments = ['format-patch'] - if subject_prefix != None: + if not match_array('^--subject-prefix', git_format_patch_arguments): + subject_prefix = self.get_subject_prefix() git_arguments.append('--subject-prefix={}'.format(subject_prefix)) - if patches_revision != None: - git_arguments.append('-v{}'.format(patches_revision)) + git_arguments += git_format_patch_arguments patches = self.git(*git_arguments).split(os.linesep) + patches.remove('') - if self.directory: - results = [] - for patch in patches: - results.append(self.directory + os.sep + patch) - return results - else: - return patches + return patches + def generate_cover_mail_text(self, commit, nr_patches, repo): cgit_url = 'https://git.replicant.us' @@ -195,34 +213,134 @@ class GitRepo(object): '--compose', '--to={}'.format(self.config['project']['mailing_list'])] - command += patches + for patch in patches: + if self.directory: + command.append('{}/{}'.format(self.directory, patch)) + else: + command.append(patches) return command - def get_git_revision(self, git_revision): + def get_commit_hash(self, git_revision): output = self.git('--no-pager', 'log', '--oneline', git_revision, '-1', '--format=%H') revision = re.sub(os.linesep, '', str(output)) return revision -if __name__ == '__main__': - nr_patches = 1 - patches_revision = None - directory = None + def get_top_commit_revision_from_range(self, rev_range, nr_patches): + output = self.git('--no-pager', 'rev-parse', rev_range).split(os.linesep) + output.remove('') + + # With git-format-patch, there are "two ways to specify which commits to + # operate on.": + # "1. A single commit, <since>, [...] leading to the tip of the current + # branch" + # "2. Generic <revision range> expression" + # See man git-format-patch for more details on how to parse. + + # Single commit + if len(output) == 1: + output = self.git('--no-pager', 'rev-parse', rev_range + '..HEAD') + output = output.split(os.linesep) + output.remove('') + return output[0] + # Generic revision range + else: + return output[0] + +def get_git_revision_args_from_cmdline(cmdline): + class Arg(enum.Enum): + NONE = 0, + REQUIRED = 1, + OPTIONAL = 2, + + git_format_patch_opts = [ + # shortopt, longopt, argument + # TODO: --no-[option] + # TODO: --base + [ None, 'add-header', Arg.REQUIRED ], + [ None, 'attach', Arg.OPTIONAL ], + [ None, 'cc', Arg.REQUIRED ], + [ None, 'cover-from-description', Arg.REQUIRED ], + [ None, 'cover-letter', Arg.NONE ], + [ None, 'creation-factor', Arg.REQUIRED ], + [ None, 'filename-max-length', Arg.REQUIRED ], + [ None, 'from', Arg.REQUIRED ], + [ None, 'inline', Arg.OPTIONAL ], + [ None, 'in-reply-to', Arg.REQUIRED ], + [ None, 'interdiff', Arg.REQUIRED ], + [ 'k', 'keep-subject', Arg.NONE ], + [ None, 'no-attach', Arg.NONE ], + [ None, 'no-binary', Arg.NONE ], + [ None, 'no-indent-heuristic', Arg.NONE ], + [ None, 'no-notes', Arg.NONE ], + [ 'N', 'no-numbered', Arg.NONE ], + [ None, 'no-renames', Arg.NONE ], + [ None, 'no-relative', Arg.NONE ], + [ None, 'no-signature', Arg.NONE ], + [ 'p', 'no-stat', Arg.NONE ], + [ None, 'no-thread', Arg.NONE ], + [ None, 'notes', Arg.OPTIONAL ], + [ 'n', 'numbered', Arg.NONE ], + [ None, 'numbered-files', Arg.NONE ], + [ 'o', 'output-directory', Arg.REQUIRED ], + [ None, 'progress', Arg.NONE ], + [ 'q', 'quiet', Arg.NONE ], + [ None, 'range-diff', Arg.REQUIRED ], + [ 'v', 'reroll-count', Arg.REQUIRED ], + [ None, 'rfc', Arg.NONE ], + [ None, 'signature', Arg.REQUIRED ], + [ None, 'signature-file', Arg.REQUIRED ], + [ 's', 'signoff', Arg.NONE ], + [ None, 'start-number', Arg.REQUIRED ], + [ None, 'stdout', Arg.NONE ], + [ None, 'subject-prefix', Arg.REQUIRED ], + [ None, 'suffix', Arg.REQUIRED ], + [ None, 'thread', Arg.OPTIONAL ], + [ None, 'to', Arg.REQUIRED ], + [ None, 'zero-commit', Arg.NONE ], + ] + + # We cannot use getopt because "Optional arguments [for long options] are + # not supported". Reference: https://docs.python.org/3.8/library/getopt.html + parser = argparse.ArgumentParser() + parser.add_argument('args', nargs=argparse.REMAINDER) + + for option in git_format_patch_opts: + if option[2] == Arg.REQUIRED: + if option[0] != None: + parser.add_argument('-' + option[0], nargs=1) + if option[1] != None: + parser.add_argument('--' + option[1], nargs=1) + elif option[2] != Arg.OPTIONAL: + if option[0] != None: + parser.add_argument('-' + option[0], nargs='?') + if option[1] != None: + parser.add_argument('--' + option[1], nargs='?') + else: + # For now, we can filter out those with - or -- + continue - if len(sys.argv) not in [2, 3, 4, 5, 6]: - usage(sys.argv[0]) + remaining_args = parser.parse_args(cmdline).__dict__.get('args', []) - if sys.argv[1] == '-C': - sys.argv.pop(1) - directory = sys.argv.pop(1) + revision = remaining_args[0] + return revision - if len (sys.argv) >= 3: - nr_patches = int(sys.argv[2]) +def get_git_revision_range_from_args(args): + if git_revision_args.startswith('-'): + nr_patches = int(args[1:]) + return 'HEAD~{}..HEAD'.format(nr_patches) + else: + return args - if len (sys.argv) >= 4: - patches_revision = int(sys.argv[3]) +if __name__ == '__main__': + directory = None + patches = None + progname = sys.argv.pop(0) + + if len(sys.argv) == 0: + usage(progname) config = get_config() @@ -230,24 +348,40 @@ if __name__ == '__main__': print('Failed to find a configuration file') sys.exit(1) + if sys.argv[0] == '-C': + sys.argv.pop(0) + directory = sys.argv.pop(0) + repo = GitRepo(config, directory) - git_revision = repo.get_git_revision(sys.argv[1]) - patches = repo.generate_patches(git_revision, nr_patches, patches_revision) + try: + patches = repo.format_patch(sys.argv) + except: + usage(progname) print('patches:') print('--------') for patch in patches: - print(patch) + if directory: + print('{}/{}'.format(directory, patch)) + else: + print(patch) + + git_revision_args = get_git_revision_args_from_cmdline(sys.argv) + + git_revision_range = get_git_revision_range_from_args(git_revision_args) + + top_revision = repo.get_top_commit_revision_from_range(git_revision_range, + len(patches)) print() print('git command:') print('------------') print('git ' + ' '.join( - repo.generate_git_send_email_command(git_revision, patches))) + repo.generate_git_send_email_command(git_revision_range, patches))) print() print('Cover mail:') print('-----------') - print(repo.generate_cover_mail_text(git_revision, nr_patches, + print(repo.generate_cover_mail_text(top_revision, len(patches), repo.get_repo_name())) -- 2.33.0 _______________________________________________ Replicant mailing list Replicant@osuosl.org https://lists.osuosl.org/mailman/listinfo/replicant