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