1 #!/usr/bin/env python2 2 # 3 # Copyright 2010 Google Inc. All Rights Reserved. 4 """Module for transferring files between various types of repositories.""" 5 6 from __future__ import print_function 7 8 __author__ = 'asharif (at] google.com (Ahmad Sharif)' 9 10 import argparse 11 import datetime 12 import json 13 import os 14 import re 15 import socket 16 import sys 17 import tempfile 18 19 from automation.clients.helper import perforce 20 from cros_utils import command_executer 21 from cros_utils import logger 22 from cros_utils import misc 23 24 # pylint: disable=anomalous-backslash-in-string 25 26 27 def GetCanonicalMappings(mappings): 28 canonical_mappings = [] 29 for mapping in mappings: 30 remote_path, local_path = mapping.split() 31 if local_path.endswith('/') and not remote_path.endswith('/'): 32 local_path = os.path.join(local_path, os.path.basename(remote_path)) 33 remote_path = remote_path.lstrip('/').split('/', 1)[1] 34 canonical_mappings.append(perforce.PathMapping(remote_path, local_path)) 35 return canonical_mappings 36 37 38 def SplitMapping(mapping): 39 parts = mapping.split() 40 assert len(parts) <= 2, 'Mapping %s invalid' % mapping 41 remote_path = parts[0] 42 if len(parts) == 2: 43 local_path = parts[1] 44 else: 45 local_path = '.' 46 return remote_path, local_path 47 48 49 class Repo(object): 50 """Basic repository base class.""" 51 52 def __init__(self, no_create_tmp_dir=False): 53 self.repo_type = None 54 self.address = None 55 self.mappings = None 56 self.revision = None 57 self.ignores = ['.gitignore', '.p4config', 'README.google'] 58 if no_create_tmp_dir: 59 self._root_dir = None 60 else: 61 self._root_dir = tempfile.mkdtemp() 62 self._ce = command_executer.GetCommandExecuter() 63 self._logger = logger.GetLogger() 64 65 def PullSources(self): 66 """Pull all sources into an internal dir.""" 67 pass 68 69 def SetupForPush(self): 70 """Setup a repository for pushing later.""" 71 pass 72 73 def PushSources(self, commit_message=None, dry_run=False, message_file=None): 74 """Push to the external repo with the commit message.""" 75 pass 76 77 def _RsyncExcludingRepoDirs(self, source_dir, dest_dir): 78 for f in os.listdir(source_dir): 79 if f in ['.git', '.svn', '.p4config']: 80 continue 81 dest_file = os.path.join(dest_dir, f) 82 source_file = os.path.join(source_dir, f) 83 if os.path.exists(dest_file): 84 command = 'rm -rf %s' % dest_file 85 self._ce.RunCommand(command) 86 command = 'rsync -a %s %s' % (source_file, dest_dir) 87 self._ce.RunCommand(command) 88 return 0 89 90 def MapSources(self, dest_dir): 91 """Copy sources from the internal dir to root_dir.""" 92 return self._RsyncExcludingRepoDirs(self._root_dir, dest_dir) 93 94 def GetRoot(self): 95 return self._root_dir 96 97 def SetRoot(self, directory): 98 self._root_dir = directory 99 100 def CleanupRoot(self): 101 command = 'rm -rf %s' % self._root_dir 102 return self._ce.RunCommand(command) 103 104 def __str__(self): 105 return '\n'.join( 106 str(s) for s in [self.repo_type, self.address, self.mappings]) 107 108 109 # Note - this type of repo is used only for "readonly", in other words, this 110 # only serves as a incoming repo. 111 class FileRepo(Repo): 112 """Class for file repositories.""" 113 114 def __init__(self, address, ignores=None): 115 Repo.__init__(self, no_create_tmp_dir=True) 116 self.repo_type = 'file' 117 self.address = address 118 self.mappings = None 119 self.branch = None 120 self.revision = '{0} (as of "{1}")'.format(address, datetime.datetime.now()) 121 self.gerrit = None 122 self._root_dir = self.address 123 if ignores: 124 self.ignores += ignores 125 126 def CleanupRoot(self): 127 """Override to prevent deletion.""" 128 pass 129 130 131 class P4Repo(Repo): 132 """Class for P4 repositories.""" 133 134 def __init__(self, address, mappings, revision=None): 135 Repo.__init__(self) 136 self.repo_type = 'p4' 137 self.address = address 138 self.mappings = mappings 139 self.revision = revision 140 141 def PullSources(self): 142 client_name = socket.gethostname() 143 client_name += tempfile.mkstemp()[1].replace('/', '-') 144 mappings = self.mappings 145 p4view = perforce.View('depot2', GetCanonicalMappings(mappings)) 146 p4client = perforce.CommandsFactory( 147 self._root_dir, p4view, name=client_name) 148 command = p4client.SetupAndDo(p4client.Sync(self.revision)) 149 ret = self._ce.RunCommand(command) 150 assert ret == 0, 'Could not setup client.' 151 command = p4client.InCheckoutDir(p4client.SaveCurrentCLNumber()) 152 ret, o, _ = self._ce.RunCommandWOutput(command) 153 assert ret == 0, 'Could not get version from client.' 154 self.revision = re.search('^\d+$', o.strip(), re.MULTILINE).group(0) 155 command = p4client.InCheckoutDir(p4client.Remove()) 156 ret = self._ce.RunCommand(command) 157 assert ret == 0, 'Could not delete client.' 158 return 0 159 160 161 class SvnRepo(Repo): 162 """Class for svn repositories.""" 163 164 def __init__(self, address, mappings): 165 Repo.__init__(self) 166 self.repo_type = 'svn' 167 self.address = address 168 self.mappings = mappings 169 170 def PullSources(self): 171 with misc.WorkingDirectory(self._root_dir): 172 for mapping in self.mappings: 173 remote_path, local_path = SplitMapping(mapping) 174 command = 'svn co %s/%s %s' % (self.address, remote_path, local_path) 175 ret = self._ce.RunCommand(command) 176 if ret: 177 return ret 178 179 self.revision = '' 180 for mapping in self.mappings: 181 remote_path, local_path = SplitMapping(mapping) 182 command = 'cd %s && svnversion -c .' % (local_path) 183 ret, o, _ = self._ce.RunCommandWOutput(command) 184 self.revision += o.strip().split(':')[-1] 185 if ret: 186 return ret 187 return 0 188 189 190 class GitRepo(Repo): 191 """Class for git repositories.""" 192 193 def __init__(self, address, branch, mappings=None, ignores=None, gerrit=None): 194 Repo.__init__(self) 195 self.repo_type = 'git' 196 self.address = address 197 self.branch = branch or 'master' 198 if ignores: 199 self.ignores += ignores 200 self.mappings = mappings 201 self.gerrit = gerrit 202 203 def _CloneSources(self): 204 with misc.WorkingDirectory(self._root_dir): 205 command = 'git clone %s .' % (self.address) 206 return self._ce.RunCommand(command) 207 208 def PullSources(self): 209 with misc.WorkingDirectory(self._root_dir): 210 ret = self._CloneSources() 211 if ret: 212 return ret 213 214 command = 'git checkout %s' % self.branch 215 ret = self._ce.RunCommand(command) 216 if ret: 217 return ret 218 219 command = 'git describe --always' 220 ret, o, _ = self._ce.RunCommandWOutput(command) 221 self.revision = o.strip() 222 return ret 223 224 def SetupForPush(self): 225 with misc.WorkingDirectory(self._root_dir): 226 ret = self._CloneSources() 227 logger.GetLogger().LogFatalIf( 228 ret, 'Could not clone git repo %s.' % self.address) 229 230 command = 'git branch -a | grep -wq %s' % self.branch 231 ret = self._ce.RunCommand(command) 232 233 if ret == 0: 234 if self.branch != 'master': 235 command = ('git branch --track %s remotes/origin/%s' % (self.branch, 236 self.branch)) 237 else: 238 command = 'pwd' 239 command += '&& git checkout %s' % self.branch 240 else: 241 command = 'git symbolic-ref HEAD refs/heads/%s' % self.branch 242 command += '&& rm -rf *' 243 ret = self._ce.RunCommand(command) 244 return ret 245 246 def CommitLocally(self, commit_message=None, message_file=None): 247 with misc.WorkingDirectory(self._root_dir): 248 command = 'pwd' 249 for ignore in self.ignores: 250 command += '&& echo \'%s\' >> .git/info/exclude' % ignore 251 command += '&& git add -Av .' 252 if message_file: 253 message_arg = '-F %s' % message_file 254 elif commit_message: 255 message_arg = '-m \'%s\'' % commit_message 256 else: 257 raise RuntimeError('No commit message given!') 258 command += '&& git commit -v %s' % message_arg 259 return self._ce.RunCommand(command) 260 261 def PushSources(self, commit_message=None, dry_run=False, message_file=None): 262 ret = self.CommitLocally(commit_message, message_file) 263 if ret: 264 return ret 265 push_args = '' 266 if dry_run: 267 push_args += ' -n ' 268 with misc.WorkingDirectory(self._root_dir): 269 if self.gerrit: 270 label = 'somelabel' 271 command = 'git remote add %s %s' % (label, self.address) 272 command += ('&& git push %s %s HEAD:refs/for/master' % (push_args, 273 label)) 274 else: 275 command = 'git push -v %s origin %s:%s' % (push_args, self.branch, 276 self.branch) 277 ret = self._ce.RunCommand(command) 278 return ret 279 280 def MapSources(self, root_dir): 281 if not self.mappings: 282 self._RsyncExcludingRepoDirs(self._root_dir, root_dir) 283 return 284 with misc.WorkingDirectory(self._root_dir): 285 for mapping in self.mappings: 286 remote_path, local_path = SplitMapping(mapping) 287 remote_path.rstrip('...') 288 local_path.rstrip('...') 289 full_local_path = os.path.join(root_dir, local_path) 290 ret = self._RsyncExcludingRepoDirs(remote_path, full_local_path) 291 if ret: 292 return ret 293 return 0 294 295 296 class RepoReader(object): 297 """Class for reading repositories.""" 298 299 def __init__(self, filename): 300 self.filename = filename 301 self.main_dict = {} 302 self.input_repos = [] 303 self.output_repos = [] 304 305 def ParseFile(self): 306 with open(self.filename) as f: 307 self.main_dict = json.load(f) 308 self.CreateReposFromDict(self.main_dict) 309 return [self.input_repos, self.output_repos] 310 311 def CreateReposFromDict(self, main_dict): 312 for key, repo_list in main_dict.items(): 313 for repo_dict in repo_list: 314 repo = self.CreateRepoFromDict(repo_dict) 315 if key == 'input': 316 self.input_repos.append(repo) 317 elif key == 'output': 318 self.output_repos.append(repo) 319 else: 320 logger.GetLogger().LogFatal('Unknown key: %s found' % key) 321 322 def CreateRepoFromDict(self, repo_dict): 323 repo_type = repo_dict.get('type', None) 324 repo_address = repo_dict.get('address', None) 325 repo_mappings = repo_dict.get('mappings', None) 326 repo_ignores = repo_dict.get('ignores', None) 327 repo_branch = repo_dict.get('branch', None) 328 gerrit = repo_dict.get('gerrit', None) 329 revision = repo_dict.get('revision', None) 330 331 if repo_type == 'p4': 332 repo = P4Repo(repo_address, repo_mappings, revision=revision) 333 elif repo_type == 'svn': 334 repo = SvnRepo(repo_address, repo_mappings) 335 elif repo_type == 'git': 336 repo = GitRepo( 337 repo_address, 338 repo_branch, 339 mappings=repo_mappings, 340 ignores=repo_ignores, 341 gerrit=gerrit) 342 elif repo_type == 'file': 343 repo = FileRepo(repo_address) 344 else: 345 logger.GetLogger().LogFatal('Unknown repo type: %s' % repo_type) 346 return repo 347 348 349 @logger.HandleUncaughtExceptions 350 def Main(argv): 351 parser = argparse.ArgumentParser() 352 parser.add_argument( 353 '-i', 354 '--input_file', 355 dest='input_file', 356 help='The input file that contains repo descriptions.') 357 358 parser.add_argument( 359 '-n', 360 '--dry_run', 361 dest='dry_run', 362 action='store_true', 363 default=False, 364 help='Do a dry run of the push.') 365 366 parser.add_argument( 367 '-F', 368 '--message_file', 369 dest='message_file', 370 default=None, 371 help=('Use contents of the log file as the commit ' 372 'message.')) 373 374 options = parser.parse_args(argv) 375 if not options.input_file: 376 parser.print_help() 377 return 1 378 rr = RepoReader(options.input_file) 379 [input_repos, output_repos] = rr.ParseFile() 380 381 # Make sure FileRepo is not used as output destination. 382 for output_repo in output_repos: 383 if output_repo.repo_type == 'file': 384 logger.GetLogger().LogFatal( 385 'FileRepo is only supported as an input repo.') 386 387 for output_repo in output_repos: 388 ret = output_repo.SetupForPush() 389 if ret: 390 return ret 391 392 input_revisions = [] 393 for input_repo in input_repos: 394 ret = input_repo.PullSources() 395 if ret: 396 return ret 397 input_revisions.append(input_repo.revision) 398 399 for input_repo in input_repos: 400 for output_repo in output_repos: 401 ret = input_repo.MapSources(output_repo.GetRoot()) 402 if ret: 403 return ret 404 405 commit_message = 'Synced repos to: %s' % ','.join(input_revisions) 406 for output_repo in output_repos: 407 ret = output_repo.PushSources( 408 commit_message=commit_message, 409 dry_run=options.dry_run, 410 message_file=options.message_file) 411 if ret: 412 return ret 413 414 if not options.dry_run: 415 for output_repo in output_repos: 416 output_repo.CleanupRoot() 417 for input_repo in input_repos: 418 input_repo.CleanupRoot() 419 420 return ret 421 422 423 if __name__ == '__main__': 424 retval = Main(sys.argv[1:]) 425 sys.exit(retval) 426