summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>2021-02-23 14:30:50 +0100
committerDenis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>2021-09-01 15:55:43 +0200
commit3e0b744c66b3a8f00e5f1c2c393e25cfad74b798 (patch)
tree4f8e9cbd5f6d1d3e679a54a5998e5bd9baec383f
parentb3b9dae5d21750bc281f4482235d84de19367b4a (diff)
downloadvendor_replicant-scripts-3e0b744c66b3a8f00e5f1c2c393e25cfad74b798.tar.gz
vendor_replicant-scripts-3e0b744c66b3a8f00e5f1c2c393e25cfad74b798.tar.bz2
vendor_replicant-scripts-3e0b744c66b3a8f00e5f1c2c393e25cfad74b798.zip
replicant_prepare_patch.py: Handle all git arguments
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 <GNUtoo@cyberdimension.org>
-rwxr-xr-xpatches/replicant_prepare_patch.py226
1 files changed, 180 insertions, 46 deletions
diff --git a/patches/replicant_prepare_patch.py b/patches/replicant_prepare_patch.py
index c1f52e7..f75b1ec 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(patch)
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()))