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