Skip to content

Commit

Permalink
[ADD] submodule-update: init (#10)
Browse files Browse the repository at this point in the history
* [ADD] submodule-update: init

New sub-command submodule-update to make it more convenient to manage
submodule updates.

* [REM] drop support for py37

It is now end of life and there are extra issues supporting it.
  • Loading branch information
oerp-odoo authored May 20, 2024
1 parent 261807f commit ab10f3c
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
@@ -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
########################
Expand All @@ -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:

Expand Down Expand Up @@ -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
=======

Expand Down
38 changes: 38 additions & 0 deletions bin/git-trunk
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import sys
import abc
import argparse
from distutils.util import strtobool
Expand All @@ -12,6 +13,7 @@ from git_trunk.git_trunk_commands import (
GitTrunkRelease,
GitTrunkRefresh,
GitTrunkSquash,
GitTrunkSubmoduleUpdate,
)
from git_trunk import __version__

Expand Down Expand Up @@ -107,6 +109,10 @@ class ArgAdderStr(BaseArgAdder):
)


class ArgAdderInt(ArgAdderStr):
pass


class ArgAdderBool(BaseArgAdder):
"""Class to add bool type argument on parser."""

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand All @@ -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."""
Expand Down Expand Up @@ -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."""

Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions git_trunk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("git_trunk")
except PackageNotFoundError:
__version__ = 'unknown'
77 changes: 75 additions & 2 deletions git_trunk/git_trunk_commands.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
22 changes: 22 additions & 0 deletions git_trunk/git_trunk_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"
),
}
}

Expand Down
9 changes: 1 addition & 8 deletions git_trunk/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
9 changes: 8 additions & 1 deletion git_trunk/tests/test_git_trunk_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
FINISH_SECTION,
RELEASE_SECTION,
SQUASH_SECTION,
SUBMODULE_UPDATE_SECTION,
)
from . import common

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
},
}
)

Expand Down
Loading

0 comments on commit ab10f3c

Please sign in to comment.