xref: /OpenGrok/tools/src/main/python/opengrok_tools/scm/repository.py (revision f321caad0acb8beb39015f207bbccce81033d22b)
1#
2# CDDL HEADER START
3#
4# The contents of this file are subject to the terms of the
5# Common Development and Distribution License (the "License").
6# You may not use this file except in compliance with the License.
7#
8# See LICENSE.txt included in this distribution for the specific
9# language governing permissions and limitations under the License.
10#
11# When distributing Covered Code, include this CDDL HEADER in each
12# file and include the License file at LICENSE.txt.
13# If applicable, add the following below this CDDL HEADER, with the
14# fields enclosed by brackets "[]" replaced with your own identifying
15# information: Portions Copyright [yyyy] [name of copyright owner]
16#
17# CDDL HEADER END
18#
19
20#
21# Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
22# Portions Copyright (c) 2020, Krystof Tulinger <k.tulinger@seznam.cz>
23#
24
25import abc
26import os
27
28from ..utils.command import Command
29
30
31class RepositoryException(Exception):
32    """
33    Exception returned when repository operation failed.
34    """
35    pass
36
37
38class Repository:
39    """
40    abstract class wrapper for Source Code Management repository
41    """
42
43    __metaclass__ = abc.ABCMeta
44
45    SYNC_COMMAND_SECTION = 'sync'
46    INCOMING_COMMAND_SECTION = 'incoming'
47    COMMAND_PROPERTY = 'command'
48
49    def __init__(self, name, logger, path, project, configured_commands, env, hooks, timeout):
50        self.name = name
51        self.command = None
52        self.logger = logger
53        self.path = path
54        self.project = project
55        self.timeout = timeout
56        self.configured_commands = configured_commands
57        if env:
58            self.env = env
59        else:
60            self.env = {}
61
62    def __str__(self):
63        return self.path
64
65    def get_command(self, cmd, **kwargs):
66        """
67        :param cmd: command
68        :param kwargs: dictionary of command attributes
69        :return: Command object ready for execution.
70        """
71        kwargs['timeout'] = self.timeout
72        return Command(cmd, **kwargs)
73
74    def sync(self):
75        if self.is_command_overridden(self.configured_commands, self.SYNC_COMMAND_SECTION):
76            return self._run_custom_sync_command(
77                self.listify(self.configured_commands[self.SYNC_COMMAND_SECTION])
78            )
79        return self.reposync()
80
81    @abc.abstractmethod
82    def reposync(self):
83        """
84        Synchronize the repository by running sync command specific for
85        given repository type.
86
87        This method definition has to be overriden by given repository class.
88
89        Return 1 on failure, 0 on success.
90        """
91        raise NotImplementedError()
92
93    def incoming(self):
94        """
95        Check if there are any incoming changes.
96
97        Return True if so, False otherwise.
98        """
99        if self.is_command_overridden(self.configured_commands, self.INCOMING_COMMAND_SECTION):
100            return self._run_custom_incoming_command(
101                self.listify(self.configured_commands[self.INCOMING_COMMAND_SECTION])
102            )
103        return self.incoming_check()
104
105    def incoming_check(self):
106        """
107        Check if there are any incoming changes.
108        Normally this method definition is overridden, unless the repository
109        type has no way how to check for incoming changes.
110
111        :return True if so, False otherwise.
112        """
113        return True
114
115    def strip_outgoing(self):
116        """
117        Strip any outgoing changes.
118        Normally this method definition is overridden, unless the repository
119        type has no way how to check for outgoing changes or cannot strip them.
120
121        :return True if any changes were stripped, False otherwise.
122        """
123        return False
124
125    def _run_custom_sync_command(self, command):
126        """
127        Execute the custom sync command.
128
129        :param command: the command
130        :return: 0 on success execution, 1 otherwise
131        """
132        status, output = self._run_command(command)
133        log_handler = self.logger.info if status == 0 else self.logger.warning
134        log_handler("output of '{}':".format(command))
135        log_handler(output)
136        return status
137
138    def _run_custom_incoming_command(self, command):
139        """
140        Execute the custom incoming command.
141
142        :param command: the command
143        :return: true when there are changes, false otherwise
144        """
145        status, output = self._run_command(command)
146        if status != 0:
147            self.logger.error("output of '{}':".format(command))
148            self.logger.error(output)
149            raise RepositoryException('failed to check for incoming in repository {}'.format(self))
150        return len(output.strip()) > 0
151
152    def _run_command(self, command):
153        """
154        Execute the command.
155
156        :param command: the command
157        :return: tuple of (status, output)
158                    - status: 0 on success execution, non-zero otherwise
159                    - output: command output as string
160        """
161        cmd = self.get_command(command, work_dir=self.path,
162                               env_vars=self.env, logger=self.logger)
163        cmd.execute()
164        if cmd.getretcode() != 0 or cmd.getstate() != Command.FINISHED:
165            cmd.log_error("failed to perform command {}".format(command))
166            status = cmd.getretcode()
167            if status == 0 and cmd.getstate() != Command.FINISHED:
168                status = 1
169            return status, '\n'.join(filter(None, [
170                cmd.getoutputstr(),
171                cmd.geterroutputstr()
172            ]))
173        return 0, cmd.getoutputstr()
174
175    @staticmethod
176    def _repository_command(configured_commands, default=lambda: None):
177        """
178        Get the repository command, or use default supplier.
179
180        :param configured_commands: commands section from configuration
181                                    for this repository type
182        :param default: the supplier of default command
183        :return: the repository command
184        """
185        if isinstance(configured_commands, str):
186            return configured_commands
187        elif isinstance(configured_commands, dict) and \
188                configured_commands.get('command'):  # COMMAND_PROPERTY
189            return configured_commands['command']
190
191        return default()
192
193    @staticmethod
194    def listify(object):
195        if isinstance(object, list) or isinstance(object, tuple):
196            return object
197        return [object]
198
199    @staticmethod
200    def is_command_overridden(config, command):
201        """
202        Determine if command key is overridden in the configuration.
203
204        :param config: configuration
205        :param command: the command
206        :return: true if overridden, false otherwise
207        """
208        return isinstance(config, dict) and config.get(command) is not None
209
210    def _check_command(self):
211        """
212        Could be overridden in given repository class to provide different check.
213        :return: True if self.command is a file, False otherwise.
214        """
215        if self.command and not os.path.isfile(self.command):
216            self.logger.error("path for '{}' is not a file: {}".
217                              format(self.name, self.command))
218            return False
219
220        return True
221
222    def check_command(self):
223        """
224        Check the validity of the command. Does not check the command if
225        the sync/incoming is overridden.
226        :return: True if self.command is valid, False otherwise.
227        """
228
229        if isinstance(self.configured_commands, dict):
230            for key in self.configured_commands.keys():
231                if key not in [self.SYNC_COMMAND_SECTION,
232                               self.INCOMING_COMMAND_SECTION,
233                               self.COMMAND_PROPERTY]:
234                    self.logger.error("Unknown property '{}' for '{}'".
235                                      format(key, self.name))
236                    return False
237
238        if self.command and not os.path.exists(self.command):
239            self.logger.error("path for '{}' does not exist: {}".
240                              format(self.name, self.command))
241            return False
242
243        return self._check_command()
244
245    def top_level(self):
246        """
247        :return: Whether to terminate the synchronization processing at the top level.
248        """
249        return False
250