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