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

Reply via email to