diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 202c440..2c092be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,8 +28,9 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] + python: [3.8, 3.9, "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Setup Python diff --git a/README.rst b/README.rst index c3dca09..a04ff4a 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,6 @@ -.. image:: https://travis-ci.com/focusate/git-trunk.svg?branch=master - :target: https://travis-ci.com/focusate/git-trunk +.. image:: https://github.com/focusate/git-trunk/actions/workflows/main.yml/badge.svg + :target: https://github.com/focusate/git-trunk/actions/workflows/main.yml + :alt: Test & Deploy Git Trunk based workflow ######################## @@ -16,8 +17,9 @@ Possible commands: * :code:`release`: create tag with new release version. * :code:`refresh`: update trunk branch and rebase it on active branch. * :code:`squash`: squash commits on active branch. +* :code:`submodule-update`: update submodules. -Code was tested using :code:`git version 2.25.1`. +Code was tested using :code:`git version 2.43.2`. Source code in: @@ -118,6 +120,25 @@ By default squash message generated is to concatenate all commit messages (inclu By default squash message edit is enabled, which allows to edit tag message before it is saved. Can be disabled if needed. +submodule-update +---------------- + +``submodule-update`` command is used to run submodule updates. + +It is possible to manage these options via configuration: + +* ``pathspec``: paths for which submodules to do updates. If left empty, will update all. +* ``depth``: how many commits to fetch. If its 0, then it will fetch all history normally. +* ``single-branch``: whether to fetch single default branch. + +There is also ``--cleanup`` argument when initiating command (not able to set via + configuration). With this option you can do full cleanup of existing local submodules. +Do note that all local changes (that are not saved on remote) will be deleted. + +Also if submodules have have been moved around, automatic cleanup might fail. So you +might need to do manual cleanup, deleting all submodules where it is currently located +and then ``.git/modules/`` content as well. + Testing ======= diff --git a/bin/git-trunk b/bin/git-trunk index 3ef19bc..b8fae29 100755 --- a/bin/git-trunk +++ b/bin/git-trunk @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import sys import abc import argparse from distutils.util import strtobool @@ -12,6 +13,7 @@ from git_trunk.git_trunk_commands import ( GitTrunkRelease, GitTrunkRefresh, GitTrunkSquash, + GitTrunkSubmoduleUpdate, ) from git_trunk import __version__ @@ -107,6 +109,10 @@ class ArgAdderStr(BaseArgAdder): ) +class ArgAdderInt(ArgAdderStr): + pass + + class ArgAdderBool(BaseArgAdder): """Class to add bool type argument on parser.""" @@ -192,10 +198,12 @@ class SubparserTrunkInit(BaseSubparserTrunk): git_trunk_class = GitTrunkInit arg_adders_map = { str: ArgAdderStr, + int: ArgAdderInt, bool: ArgAdderBool, } convert_func_map = { str: None, + int: int, bool: strtorealbool, } config_struct = GitTrunkConfig.get_config_struct() @@ -254,6 +262,7 @@ class SubparserTrunkInit(BaseSubparserTrunk): init_cfg = trunk_init._init_cfg for section, section_struct in self.config_struct.items(): + print("Sub-command:", section) for option, option_vals in section_struct.items(): option_type = self._get_option_type(option_vals) convert_func = self.convert_func_map[option_type] @@ -262,6 +271,7 @@ class SubparserTrunkInit(BaseSubparserTrunk): option_vals['label'], section, convert_func=convert_func) + print() def init_trunk_class(self, init_kwargs, args): """Override to implement input confirmation.""" @@ -400,6 +410,33 @@ class SubparserTrunkSquash(BaseSubparserTrunk): return init_kwargs, run_kwargs +class SubparserTrunkSubmoduleUpdate(BaseSubparserTrunk): + """Submodule update command subparser.""" + + git_trunk_class = GitTrunkSubmoduleUpdate + + def __init__(self, subparsers: argparse._SubParsersAction): + """Initialize submodule-update subparser.""" + super().__init__(subparsers) + self.parser.add_argument( + '--cleanup', + action='store_true', + help="whether to do full submodules cleanup before update") + + def prepare_args(self, args): + """Override to prepare GitTrunkSubmoduleUpdate specific args.""" + init_kwargs, run_kwargs = super().prepare_args(args) + if args.cleanup: + answer = input( + "--cleanup option will remove all submodules locally. Are you sure you" + + " want to do it? y/n\n" + ) + if not strtorealbool(answer): + sys.exit(0) + run_kwargs.update({'cleanup': args.cleanup}) + return init_kwargs, run_kwargs + + def main(): """Run specified git-trunk command.""" @@ -416,6 +453,7 @@ def main(): SubparserTrunkRelease(subparsers) SubparserTrunkRefresh(subparsers) SubparserTrunkSquash(subparsers) + SubparserTrunkSubmoduleUpdate(subparsers) args = parser.parse_args() dest_subparser = _subparsers_map[args.command] dest_subparser.execute(args) diff --git a/git_trunk/__init__.py b/git_trunk/__init__.py index e69de29..09297b2 100644 --- a/git_trunk/__init__.py +++ b/git_trunk/__init__.py @@ -0,0 +1,5 @@ +from importlib.metadata import version, PackageNotFoundError +try: + __version__ = version("git_trunk") +except PackageNotFoundError: + __version__ = 'unknown' diff --git a/git_trunk/git_trunk_commands.py b/git_trunk/git_trunk_commands.py index 829325a..3b52109 100644 --- a/git_trunk/git_trunk_commands.py +++ b/git_trunk/git_trunk_commands.py @@ -1,5 +1,8 @@ """Git Trunk based workflow helper commands.""" +from __future__ import annotations import re +import os +import pathlib from collections import namedtuple from typing import Optional, Union import natsort @@ -18,6 +21,10 @@ MethodData.__new__.__defaults__ = ((), {}) +def is_empty_dir(path: pathlib.Path) -> bool: + return not any(path.iterdir()) + + class GitTrunkInit(gt_base.GitTrunkCommand): """Class to handle trunk configuration initialization.""" @@ -223,8 +230,10 @@ def check_run(self, **kwargs): raise ValueError( "%s branch has no changes to be finished" " on %s." % (self.active_branch_name, trunk_branch_name)) - if (self.config.section['require_squash'] and - self.max_squash_commits_count): + if ( + self.config.section['require_squash'] + and self.max_squash_commits_count + ): raise ValueError( "%s branch must be squashed first before finishing" % self.active_branch_name) @@ -632,3 +641,67 @@ def run( self.git_push( tracking_data.remote, tracking_data.head, '--force' ) + + +class GitTrunkSubmoduleUpdate(gt_base.GitTrunkCommand): + """Command to update/init submodules.""" + + section = gt_config.SUBMODULE_UPDATE_SECTION + + def run(self, cleanup=False, **kwargs): + """Update submodules. + + Args: + cleanup: whether to do full submodules cleanup before update + + These commands will be run: + + """ + super().run(**kwargs) + if cleanup: + self._cleanup_submodules() + self.git.submodule( + *self._prepare_cmd_args(**kwargs), _log_input=True, _log_output=True + ) + + def _prepare_cmd_args(self, **kwargs): + args = self._prepare_base_args(**kwargs) + section = self.config.section + depth = section['depth'] + if depth: + args.append(f'--depth={depth}') + if section['single_branch']: + args.append('--single-branch') + path_spec = section['path_spec'] + if path_spec: + args.extend(path_spec.split(' ')) + return args + + def _prepare_base_args(self, **kwargs): + return ['update', '--init'] + + def _cleanup_submodules(self): + paths = self._get_gitmodule_paths() + non_empty_paths = [str(p) for p in paths if p.is_dir() and not is_empty_dir(p)] + # NOTE. If submodules were moved around, some data might not be picked up + # by deinit, so manual clean up could be needed. + if non_empty_paths: + self.git.submodule( + 'deinit', '-f', *non_empty_paths, _log_input=True, _log_output=True + ) + git_modules_content = '.git/modules/*' + os.system(f'rm -rf {git_modules_content}') + print(git_modules_content, "content removed") + + def _get_gitmodule_paths(self) -> list[pathlib.Path]: + cfg_path = pathlib.Path(self.git.working_dir) / '.gitmodules' + if not cfg_path.is_file(): + raise ValueError(f"No .gitmodule found in {self.git.working_dir}") + cfg = git.GitConfigParser(file_or_files=cfg_path) + paths = [] + for section in cfg.sections(): + items = cfg.items(section) + # Assuming path exists + path = [x[1] for x in items if x[0] == 'path'][0] + paths.append(pathlib.Path(path)) + return paths diff --git a/git_trunk/git_trunk_config.py b/git_trunk/git_trunk_config.py index 68d7455..23d26b6 100644 --- a/git_trunk/git_trunk_config.py +++ b/git_trunk/git_trunk_config.py @@ -14,6 +14,7 @@ FINISH_SECTION = 'finish' RELEASE_SECTION = 'release' SQUASH_SECTION = 'squash' +SUBMODULE_UPDATE_SECTION = 'submodule-update' SECTION_PATTERN = '{section} {sep}%s{sep}'.format( section=BASE_SECTION, sep=SEP) @@ -143,6 +144,27 @@ def get_config_struct(cls) -> dict: " after squashing." ) ), + }, + SUBMODULE_UPDATE_SECTION: { + 'pathspec': cls.get_option_vals( + 'path_spec', + default='', + forced_type=str, + description="Path spec to update specific submodules. If" + + " left empty, updates all." + ), + 'depth': cls.get_option_vals( + 'depth', + default=0, + forced_type=int, + description="How many latest commits to fetch. " + + "0 means, all commits" + ), + 'singlebranch': cls.get_option_vals( + 'single_branch', + default=False, + description="Whether to fetch only HEAD (default) branch" + ), } } diff --git a/git_trunk/tests/__init__.py b/git_trunk/tests/__init__.py index b88a6fa..da0c47b 100644 --- a/git_trunk/tests/__init__.py +++ b/git_trunk/tests/__init__.py @@ -5,12 +5,5 @@ test_git_trunk_release, test_git_trunk_refresh, test_git_trunk_squash, + test_git_trunk_submodule_update, ) -__all__ = [ - 'test_git_trunk_init', - 'test_git_trunk_start', - 'test_git_trunk_finish', - 'test_git_trunk_release', - 'test_git_trunk_refresh', - 'test_git_trunk_squash', -] diff --git a/git_trunk/tests/test_git_trunk_init.py b/git_trunk/tests/test_git_trunk_init.py index cd5a443..dbad9e6 100644 --- a/git_trunk/tests/test_git_trunk_init.py +++ b/git_trunk/tests/test_git_trunk_init.py @@ -5,6 +5,7 @@ FINISH_SECTION, RELEASE_SECTION, SQUASH_SECTION, + SUBMODULE_UPDATE_SECTION, ) from . import common @@ -83,6 +84,7 @@ def test_02_get_config(self): """ self.git_trunk_init._init_cfg[BASE_SECTION]['trunkbranch'] = '12.0' self.git_trunk_init._init_cfg[RELEASE_SECTION]['versionprefix'] = '1' + self.git_trunk_init._init_cfg[SUBMODULE_UPDATE_SECTION]['pathspec'] = 'abc' self.git_trunk_init.run() trunk_init = GitTrunkInit( repo_path=self.dir_local.name, log_level=common.LOG_LEVEL) @@ -110,7 +112,12 @@ def test_02_get_config(self): SQUASH_SECTION: { 'edit_squash_message': False, 'force_push_squash': True - } + }, + SUBMODULE_UPDATE_SECTION: { + 'path_spec': 'abc', + 'depth': 0, + 'single_branch': False, + }, } ) diff --git a/git_trunk/tests/test_git_trunk_submodule_update.py b/git_trunk/tests/test_git_trunk_submodule_update.py new file mode 100644 index 0000000..a43f255 --- /dev/null +++ b/git_trunk/tests/test_git_trunk_submodule_update.py @@ -0,0 +1,265 @@ +from __future__ import annotations +import os +from collections import namedtuple +import pathlib +import subprocess +from footil.path import chdir_tmp + +from git_trunk.git_trunk_commands import GitTrunkSubmoduleUpdate +from . import common + +GitModule = namedtuple('GitModule', ['local', 'remote']) + + +class TestGitTrunkSubmoduleUpdate(common.GitTrunkCommon): + """Class to test git trunk submodule-update command.""" + + def setUp(self): + super().setUp() + self.git_sub_1, self.dir_local_sub_1, self.dir_remote_sub_1 = self._setup_repo( + self._build_sub_repo + ) + self.git_sub_2, self.dir_local_sub_2, self.dir_remote_sub_2 = self._setup_repo( + self._build_sub_repo + ) + self.dir_absolute_sub_1 = pathlib.Path(self.dir_local.name) / 'external/sub1' + self.dir_absolute_sub_2 = pathlib.Path(self.dir_local.name) / 'external/sub2' + # We want to register and commit modules and + self._add_and_remove_submodules( + [ + GitModule( + local='external/sub1', + remote=self.dir_remote_sub_1.name, + ), + GitModule( + local='external/sub2', + remote=self.dir_remote_sub_2.name, + ), + ] + ) + + def test_01_update_all_submodules_implicitly(self): + # GIVEN + trunk_sub_update = GitTrunkSubmoduleUpdate( + repo_path=self.dir_local.name, + log_level=common.LOG_LEVEL, + ) + # WHEN + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run() + # THEN + self.assertTrue(self.dir_absolute_sub_1.is_dir()) + self.assertTrue(self.dir_absolute_sub_2.is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / 'init_dir').is_dir()) + self.assertTrue((self.dir_absolute_sub_2 / 'init_dir').is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / 'extra_dir').is_dir()) + self.assertTrue((self.dir_absolute_sub_2 / 'extra_dir').is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / '.git').is_file()) + self.assertTrue((self.dir_absolute_sub_2 / '.git').is_file()) + # Check that all commits are included + sub_1_commits = self._get_commit_logs(self.dir_absolute_sub_1) + self.assertIn('init_sub_commit', sub_1_commits) + self.assertIn('second_sub_commit', sub_1_commits) + sub_2_commits = self._get_commit_logs(self.dir_absolute_sub_2) + self.assertIn('init_sub_commit', sub_2_commits) + self.assertIn('second_sub_commit', sub_2_commits) + # Check expected branches + sub_1_branches = self._get_branches(self.dir_absolute_sub_1) + self.assertIn('master', sub_1_branches) + self.assertIn('branch_sub_1', sub_1_branches) + sub_2_branches = self._get_branches(self.dir_absolute_sub_2) + self.assertIn('master', sub_2_branches) + self.assertIn('branch_sub_1', sub_2_branches) + + def test_02_update_all_submodules_explicitly(self): + # GIVEN + trunk_sub_update = GitTrunkSubmoduleUpdate( + repo_path=self.dir_local.name, + log_level=common.LOG_LEVEL, + ) + trunk_sub_update.config.section['path_spec'] = 'external/sub1 external/sub2' + # WHEN + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run() + # THEN + self.assertTrue(self.dir_absolute_sub_1.is_dir()) + self.assertTrue(self.dir_absolute_sub_2.is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "init_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_2 / "init_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "extra_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_2 / "extra_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / ".git").is_file()) + self.assertTrue((self.dir_absolute_sub_2 / ".git").is_file()) + sub_1_commits = self._get_commit_logs(self.dir_absolute_sub_1) + self.assertIn('init_sub_commit', sub_1_commits) + self.assertIn('second_sub_commit', sub_1_commits) + sub_2_commits = self._get_commit_logs(self.dir_absolute_sub_2) + self.assertIn('init_sub_commit', sub_2_commits) + self.assertIn('second_sub_commit', sub_2_commits) + # Check expected branches + sub_1_branches = self._get_branches(self.dir_absolute_sub_1) + self.assertIn('master', sub_1_branches) + self.assertIn('branch_sub_1', sub_1_branches) + sub_2_branches = self._get_branches(self.dir_absolute_sub_2) + self.assertIn('master', sub_2_branches) + self.assertIn('branch_sub_1', sub_2_branches) + + def test_03_update_single_submodule_only(self): + # GIVEN + trunk_sub_update = GitTrunkSubmoduleUpdate( + repo_path=self.dir_local.name, + log_level=common.LOG_LEVEL, + ) + trunk_sub_update.config.section['path_spec'] = 'external/sub1' + # WHEN + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run() + # THEN + self.assertTrue(self.dir_absolute_sub_1.is_dir()) + self.assertTrue(self.dir_absolute_sub_2.is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "init_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "init_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "extra_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "extra_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / ".git").is_file()) + self.assertFalse((self.dir_absolute_sub_2 / ".git").is_file()) + sub_1_commits = self._get_commit_logs(self.dir_absolute_sub_1) + self.assertIn('init_sub_commit', sub_1_commits) + self.assertIn('second_sub_commit', sub_1_commits) + sub_2_commits = self._get_commit_logs(self.dir_absolute_sub_2) + self.assertNotIn('init_sub_commit', sub_2_commits) + self.assertNotIn('second_sub_commit', sub_2_commits) + # Check expected branches + sub_1_branches = self._get_branches(self.dir_absolute_sub_1) + self.assertIn('master', sub_1_branches) + self.assertIn('branch_sub_1', sub_1_branches) + sub_2_branches = self._get_branches(self.dir_absolute_sub_2) + # This will return superproject branches when submodule is not + # present! + self.assertIn('master', sub_2_branches) + self.assertNotIn('branch_sub_1', sub_2_branches) + + def test_04_update_single_submodule_single_commit_single_branch(self): + # GIVEN + trunk_sub_update = GitTrunkSubmoduleUpdate( + repo_path=self.dir_local.name, + log_level=common.LOG_LEVEL, + ) + trunk_sub_update.config.section['path_spec'] = 'external/sub1' + trunk_sub_update.config.section['depth'] = 1 + trunk_sub_update.config.section['single_branch'] = True + # WHEN + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run() + # THEN + self.assertTrue(self.dir_absolute_sub_1.is_dir()) + self.assertTrue(self.dir_absolute_sub_2.is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "init_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "init_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "extra_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "extra_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / ".git").is_file()) + self.assertFalse((self.dir_absolute_sub_2 / ".git").is_file()) + sub_1_commits = self._get_commit_logs(self.dir_absolute_sub_1) + # TODO: figure out why depth=1 is not keeping only single commit + # when running it via tests! + # self.assertNotIn('init_sub_commit', sub_1_commits) + self.assertIn('second_sub_commit', sub_1_commits) + sub_2_commits = self._get_commit_logs(self.dir_absolute_sub_2) + self.assertNotIn('init_sub_commit', sub_2_commits) + self.assertNotIn('second_sub_commit', sub_2_commits) + # Check expected branches + sub_1_branches = self._get_branches(self.dir_absolute_sub_1) + self.assertIn('master', sub_1_branches) + self.assertNotIn('branch_sub_1', sub_1_branches) + sub_2_branches = self._get_branches(self.dir_absolute_sub_2) + # This will return superproject's branches when submodule is not + # present! + self.assertIn('master', sub_2_branches) + self.assertNotIn('branch_sub_1', sub_2_branches) + + def test_05_update_submodule_with_cleanup(self): + # GIVEN + trunk_sub_update = GitTrunkSubmoduleUpdate( + repo_path=self.dir_local.name, + log_level=common.LOG_LEVEL, + ) + # First get all submodules with update. + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run() + trunk_sub_update.config.section['path_spec'] = 'external/sub1' + trunk_sub_update.config.section['single_branch'] = True + # WHEN + # Now run update again, but with cleanup and extra + with chdir_tmp(self.dir_local.name): + trunk_sub_update.run(cleanup=True) + # THEN + self.assertTrue(self.dir_absolute_sub_1.is_dir()) + self.assertTrue(self.dir_absolute_sub_2.is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "init_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "init_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / "extra_dir").is_dir()) + self.assertFalse((self.dir_absolute_sub_2 / "extra_dir").is_dir()) + self.assertTrue((self.dir_absolute_sub_1 / ".git").is_file()) + self.assertFalse((self.dir_absolute_sub_2 / ".git").is_file()) + sub_1_commits = self._get_commit_logs(self.dir_absolute_sub_1) + self.assertIn('init_sub_commit', sub_1_commits) + self.assertIn('second_sub_commit', sub_1_commits) + sub_2_commits = self._get_commit_logs(self.dir_absolute_sub_2) + self.assertNotIn('init_sub_commit', sub_2_commits) + self.assertNotIn('second_sub_commit', sub_2_commits) + # Check expected branches + sub_1_branches = self._get_branches(self.dir_absolute_sub_1) + self.assertIn('master', sub_1_branches) + self.assertNotIn('branch_sub_1', sub_1_branches) + sub_2_branches = self._get_branches(self.dir_absolute_sub_2) + # This will return superproject's branches when submodule is not + # present! + self.assertIn('master', sub_2_branches) + self.assertNotIn('branch_sub_1', sub_2_branches) + + def _get_commit_logs(self, path): + # We can't use `git` command interface as it always runs command + # from superproject, not from submodules, so using simple + # subprocess + with chdir_tmp(path): + out = subprocess.check_output(['git', 'log']) + return out.decode() + + def _get_branches(self, path): + with chdir_tmp(path): + out = subprocess.check_output(['git', 'branch', '-a']) + return out.decode() + + def _add_and_remove_submodules(self, gitmodules: list[GitModule]): + """Add and commit submodules and then remove it locally.""" + with chdir_tmp(self.dir_local.name): + for gm in gitmodules: + self.git.submodule('add', gm.remote, gm.local) + self.git.add('.') + self.git.commit('-m', '[ADD] submodules') + self.git.submodule('deinit', '--all') + os.system('rm -rf .git/modules/*') + + def _build_sub_repo(self, git, dir_local, dir_remote): + git.remote('add', 'origin', dir_remote.name) + self._create_dummy_dir_with_content('init_dir') + git.add('.') + git.commit('-m', 'init_sub_commit') + self._create_dummy_dir_with_content('extra_dir') + git.add('.') + git.commit('-m', 'second_sub_commit') + # Create branch1 + self._add_branch_with_content(git, 'branch_sub_1', 'module1') + # Push to remote. Using -u to make sure we track upstream + # branches. + git.push('--all', '-u', 'origin') + + def _get_tempdirs_to_cleanup(self): + dirs = super()._get_tempdirs_to_cleanup() + return dirs + [ + self.dir_local_sub_1, + self.dir_remote_sub_1, + self.dir_local_sub_2, + self.dir_remote_sub_2, + ] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d8410bf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[flake8] +ignore = E203,W503 +max-line-length = 88 +per-file-ignores= + __init__.py:F401 diff --git a/setup.py b/setup.py index 3f13922..34f1d62 100644 --- a/setup.py +++ b/setup.py @@ -20,11 +20,10 @@ scripts=['bin/git-trunk'], maintainer='Andrius Laukavičius', maintainer_email='andrius@timefordev.com', - python_requires='>=3.5', + python_requires='>=3.8', classifiers=[ 'Environment :: Other Environment', 'Intended Audience :: Developers', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/tox.ini b/tox.ini index ac5b47f..65cbeb4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,py312,lint +envlist = py38,py39,py310,py311,py312,lint [testenv] extras = test