From c80c33b95b03345db9c32cb629a4015ad627f54a Mon Sep 17 00:00:00 2001 From: Michael Tietz Date: Mon, 13 Nov 2023 07:00:13 +0100 Subject: [PATCH] Add patches direct in yaml --- README.rst | 2 ++ git_aggregator/command.py | 31 ++++++++++++++++++ git_aggregator/config.py | 12 +++++++ git_aggregator/patch.py | 66 +++++++++++++++++++++++++++++++++++++++ git_aggregator/repo.py | 42 ++++++++++++++----------- 5 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 git_aggregator/command.py create mode 100644 git_aggregator/patch.py diff --git a/README.rst b/README.rst index a5d9173..66da357 100644 --- a/README.rst +++ b/README.rst @@ -353,11 +353,13 @@ Contributors * Simone Orsi (camptocamp_) * Artem Kostyuk * Jan Verbeek +* Michael Tietz (MT_Software_) .. _ACSONE: https://www.acsone.eu .. _Tecnativa: https://www.tecnativa.com .. _camptocamp: https://www.camptocamp.com .. _LasLabs: https://laslabs.com +.. _MT_Software: https://github.com/mt-software-de Maintainer ---------- diff --git a/git_aggregator/command.py b/git_aggregator/command.py new file mode 100644 index 0000000..cb2e522 --- /dev/null +++ b/git_aggregator/command.py @@ -0,0 +1,31 @@ +# © 2015 ACSONE SA/NV +# Copyright 2023 Michael Tietz (MT Software) +# License AGPLv3 (http://www.gnu.org/licenses/agpl-3.0-standalone.html) +# Parts of the code comes from ANYBOX +# https://github.com/anybox/anybox.recipe.odoo +import logging +import subprocess + +from ._compat import console_to_str + +logger = logging.getLogger(__name__) + + +class CommandExecutor: + def __init__(self, cwd): + self.cwd = cwd + + def log_call(self, cmd, callwith=subprocess.check_call, + log_level=logging.DEBUG, **kw): + """Wrap a subprocess call with logging + :param meth: the calling method to use. + """ + logger.log(log_level, "%s> call %r", self.cwd, cmd) + try: + ret = callwith(cmd, **kw) + except Exception: + logger.error("%s> error calling %r", self.cwd, cmd) + raise + if callwith == subprocess.check_output: + ret = console_to_str(ret) + return ret diff --git a/git_aggregator/config.py b/git_aggregator/config.py index 1f95b66..3dc8c99 100644 --- a/git_aggregator/config.py +++ b/git_aggregator/config.py @@ -9,10 +9,21 @@ from ._compat import string_types from .exception import ConfigException +from .patch import Patches log = logging.getLogger(__name__) +def update_patches(repo_dict, repo_data): + """Check and update repo_dict with patch files""" + patches_data = repo_data.get("patches") + patches = repo_dict.setdefault("patches", Patches()) + if not patches_data: + return + for patch in patches_data: + patches += Patches.prepare_patches(patch, repo_dict.get("cwd")) + + def get_repos(config, force=False): """Return a :py:obj:`list` list of repos from config file. :param config: the repos config in :py:class:`dict` format. @@ -126,6 +137,7 @@ def get_repos(config, force=False): cmds = [cmds] commands = cmds repo_dict['shell_command_after'] = commands + update_patches(repo_dict, repo_data) repo_list.append(repo_dict) return repo_list diff --git a/git_aggregator/patch.py b/git_aggregator/patch.py new file mode 100644 index 0000000..f0c544f --- /dev/null +++ b/git_aggregator/patch.py @@ -0,0 +1,66 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPLv3 (http://www.gnu.org/licenses/agpl-3.0-standalone.html) +import logging +import subprocess +from pathlib import Path + +from .command import CommandExecutor + +logger = logging.getLogger(__name__) + + +class Patch(CommandExecutor): + is_local = False + + def __init__(self, path, cwd): + super().__init__(cwd) + self.path = path + path = Path(path) + if path.exists(): + self.is_local = True + + def retrive_data(self): + path = self.path + if self.is_local: + patch_path = Path(path).absolute() + path = f"FILE:{str(patch_path)}" + cmd = [ + "curl", + path, + ] + if logger.getEffectiveLevel() != logging.DEBUG: + cmd.append('-s') + return self.log_call( + cmd, + callwith=subprocess.Popen, + stdout=subprocess.PIPE + ) + + def apply(self): + res = self.retrive_data() + cmd = [ + "git", + "am", + ] + if logger.getEffectiveLevel() != logging.DEBUG: + cmd.append('--quiet') + self.log_call(cmd, cwd=self.cwd, stdin=res.stdout) + + +class Patches(list): + """List of patches""" + @staticmethod + def prepare_patches(path, cwd): + _path = Path(path) + patches = Patches() + if not _path.exists() or _path.is_file(): + patches.append(Patch(path, cwd)) + elif _path.is_dir(): + for fpath in _path.iterdir(): + if fpath.is_file(): + patches.append(Patch(str(fpath), cwd)) + return patches + + def apply(self): + for patch in self: + patch.apply() diff --git a/git_aggregator/repo.py b/git_aggregator/repo.py index 0656b89..8df05a4 100644 --- a/git_aggregator/repo.py +++ b/git_aggregator/repo.py @@ -10,6 +10,7 @@ import requests from ._compat import console_to_str +from .command import CommandExecutor from .exception import DirtyException, GitAggregatorException FETCH_DEFAULTS = ("depth", "shallow-since", "shallow-exclude") @@ -30,13 +31,13 @@ def ishex(s): return True -class Repo: +class Repo(CommandExecutor): _git_version = None def __init__(self, cwd, remotes, merges, target, shell_command_after=None, fetch_all=False, defaults=None, - force=False): + force=False, patches=None): """Initialize a git repository aggregator :param cwd: path to the directory where to initialize the repository @@ -56,7 +57,7 @@ def __init__(self, cwd, remotes, merges, target, :param bool force: When ``False``, it will stop if repo is dirty. """ - self.cwd = cwd + super().__init__(cwd) self.remotes = remotes if fetch_all is True: self.fetch_all = frozenset(r["name"] for r in remotes) @@ -67,6 +68,7 @@ def __init__(self, cwd, remotes, merges, target, self.shell_command_after = shell_command_after or [] self.defaults = defaults or dict() self.force = force + self.patches = patches @property def git_version(self): @@ -148,21 +150,6 @@ def query_remote_ref(self, remote, ref): return 'HEAD', sha return None, ref - def log_call(self, cmd, callwith=subprocess.check_call, - log_level=logging.DEBUG, **kw): - """Wrap a subprocess call with logging - :param meth: the calling method to use. - """ - logger.log(log_level, "%s> call %r", self.cwd, cmd) - try: - ret = callwith(cmd, **kw) - except Exception: - logger.error("%s> error calling %r", self.cwd, cmd) - raise - if callwith == subprocess.check_output: - ret = console_to_str(ret) - return ret - def aggregate(self): """ Aggregate all merges into the target branch If the target_dir doesn't exist, create an empty git repo otherwise @@ -187,6 +174,7 @@ def aggregate(self): self._reset_to(origin["remote"], origin["ref"]) for merge in merges: self._merge(merge) + self.patches.apply() self._execute_shell_command_after() logger.info('End aggregation of %s', self.cwd) @@ -313,6 +301,24 @@ def _merge(self, merge): cmd += self._fetch_options(merge) + (merge["remote"], merge["ref"]) self.log_call(cmd, cwd=self.cwd) + def _patch(self, patch_path): + cmd = ( + "patch", + "-p1", + "--no-backup-if-mismatch", + "-t", + "-i", + str(patch_path.resolve()), + ) + if logger.getEffectiveLevel() != logging.DEBUG: + cmd += ('--quiet',) + self.log_call(cmd, cwd=self.cwd) + self.log_call(("git", "add", "."), cwd=self.cwd) + self.log_call( + ("git", "commit", "-am", "Applied patch %s" % str(patch_path)), + cwd=self.cwd, + ) + def _get_remotes(self): lines = self.log_call( ['git', 'remote', '-v'],