| #!/usr/bin/python |
| # |
| # Copyright 2010 Google Inc. All Rights Reserved. |
| |
| __author__ = 'asharif@google.com (Ahmad Sharif)' |
| |
| import datetime |
| import optparse |
| import os |
| import re |
| import socket |
| import sys |
| import tempfile |
| |
| from automation.clients.helper import perforce |
| from utils import command_executer |
| from utils import logger |
| from utils import misc |
| |
| |
| def GetCanonicalMappings(mappings): |
| canonical_mappings = [] |
| for mapping in mappings: |
| remote_path, local_path = mapping.split() |
| if local_path.endswith('/') and not remote_path.endswith('/'): |
| local_path = os.path.join(local_path, os.path.basename(remote_path)) |
| remote_path = remote_path.lstrip('/').split('/', 1)[1] |
| canonical_mappings.append(perforce.PathMapping(remote_path, local_path)) |
| return canonical_mappings |
| |
| |
| def SplitMapping(mapping): |
| parts = mapping.split() |
| assert len(parts) <= 2, 'Mapping %s invalid' % mapping |
| remote_path = parts[0] |
| if len(parts) == 2: |
| local_path = parts[1] |
| else: |
| local_path = '.' |
| return remote_path, local_path |
| |
| |
| class Repo(object): |
| |
| def __init__(self, no_create_tmp_dir=False): |
| self.repo_type = None |
| self.address = None |
| self.mappings = None |
| self.revision = None |
| self.ignores = ['.gitignore', '.p4config', 'README.google'] |
| if no_create_tmp_dir: |
| self._root_dir = None |
| else: |
| self._root_dir = tempfile.mkdtemp() |
| self._ce = command_executer.GetCommandExecuter() |
| self._logger = logger.GetLogger() |
| |
| def PullSources(self): |
| """Pull all sources into an internal dir.""" |
| pass |
| |
| def SetupForPush(self): |
| """Setup a repository for pushing later.""" |
| pass |
| |
| def PushSources(self, commit_message=None, dry_run=False, message_file=None): |
| """Push to the external repo with the commit message.""" |
| pass |
| |
| def _RsyncExcludingRepoDirs(self, source_dir, dest_dir): |
| for f in os.listdir(source_dir): |
| if f in ['.git', '.svn', '.p4config']: |
| continue |
| dest_file = os.path.join(dest_dir, f) |
| source_file = os.path.join(source_dir, f) |
| if os.path.exists(dest_file): |
| command = 'rm -rf %s' % dest_file |
| self._ce.RunCommand(command) |
| command = 'rsync -a %s %s' % (source_file, dest_dir) |
| self._ce.RunCommand(command) |
| return 0 |
| |
| def MapSources(self, dest_dir): |
| """Copy sources from the internal dir to root_dir.""" |
| return self._RsyncExcludingRepoDirs(self._root_dir, dest_dir) |
| |
| def GetRoot(self): |
| return self._root_dir |
| |
| def CleanupRoot(self): |
| command = 'rm -rf %s' % self._root_dir |
| return self._ce.RunCommand(command) |
| |
| def __str__(self): |
| return '\n'.join(str(s) |
| for s in [self.repo_type, self.address, self.mappings]) |
| |
| |
| # Note - this type of repo is used only for "readonly", in other words, this |
| # only serves as a incoming repo. |
| class FileRepo(Repo): |
| |
| def __init__(self, address, ignores=None): |
| Repo.__init__(self, no_create_tmp_dir=True) |
| self.repo_type = 'file' |
| self.address = address |
| self.mappings = None |
| self.branch = None |
| self.revision = '{0} (as of "{1}")'.format(address, datetime.datetime.now()) |
| self.gerrit = None |
| self._root_dir = self.address |
| |
| def CleanupRoot(self): |
| """Override to prevent deletion.""" |
| pass |
| |
| |
| class P4Repo(Repo): |
| |
| def __init__(self, address, mappings, revision=None): |
| Repo.__init__(self) |
| self.repo_type = 'p4' |
| self.address = address |
| self.mappings = mappings |
| self.revision = revision |
| |
| def PullSources(self): |
| client_name = socket.gethostname() |
| client_name += tempfile.mkstemp()[1].replace('/', '-') |
| mappings = self.mappings |
| p4view = perforce.View('depot2', GetCanonicalMappings(mappings)) |
| p4client = perforce.CommandsFactory(self._root_dir, |
| p4view, |
| name=client_name) |
| command = p4client.SetupAndDo(p4client.Sync(self.revision)) |
| ret = self._ce.RunCommand(command) |
| assert ret == 0, 'Could not setup client.' |
| command = p4client.InCheckoutDir(p4client.SaveCurrentCLNumber()) |
| ret, o, _ = self._ce.RunCommandWOutput(command) |
| assert ret == 0, 'Could not get version from client.' |
| self.revision = re.search('^\d+$', o.strip(), re.MULTILINE).group(0) |
| command = p4client.InCheckoutDir(p4client.Remove()) |
| ret = self._ce.RunCommand(command) |
| assert ret == 0, 'Could not delete client.' |
| return 0 |
| |
| |
| class SvnRepo(Repo): |
| |
| def __init__(self, address, mappings): |
| Repo.__init__(self) |
| self.repo_type = 'svn' |
| self.address = address |
| self.mappings = mappings |
| |
| def PullSources(self): |
| with misc.WorkingDirectory(self._root_dir): |
| for mapping in self.mappings: |
| remote_path, local_path = SplitMapping(mapping) |
| command = 'svn co %s/%s %s' % (self.address, remote_path, local_path) |
| ret = self._ce.RunCommand(command) |
| if ret: |
| return ret |
| |
| self.revision = '' |
| for mapping in self.mappings: |
| remote_path, local_path = SplitMapping(mapping) |
| command = 'cd %s && svnversion -c .' % (local_path) |
| ret, o, _ = self._ce.RunCommandWOutput(command) |
| self.revision += o.strip().split(':')[-1] |
| if ret: |
| return ret |
| return 0 |
| |
| |
| class GitRepo(Repo): |
| |
| def __init__(self, address, branch, mappings=None, ignores=None, gerrit=None): |
| Repo.__init__(self) |
| self.repo_type = 'git' |
| self.address = address |
| self.branch = branch or 'master' |
| if ignores: |
| self.ignores += ignores |
| self.mappings = mappings |
| self.gerrit = gerrit |
| |
| def _CloneSources(self): |
| with misc.WorkingDirectory(self._root_dir): |
| command = 'git clone %s .' % (self.address) |
| return self._ce.RunCommand(command) |
| |
| def PullSources(self): |
| with misc.WorkingDirectory(self._root_dir): |
| ret = self._CloneSources() |
| if ret: |
| return ret |
| |
| command = 'git checkout %s' % self.branch |
| ret = self._ce.RunCommand(command) |
| if ret: |
| return ret |
| |
| command = 'git describe --always' |
| ret, o, _ = self._ce.RunCommandWOutput(command) |
| self.revision = o.strip() |
| return ret |
| |
| def SetupForPush(self): |
| with misc.WorkingDirectory(self._root_dir): |
| ret = self._CloneSources() |
| logger.GetLogger().LogFatalIf(ret, 'Could not clone git repo %s.' % |
| self.address) |
| |
| command = 'git branch -a | grep -wq %s' % self.branch |
| ret = self._ce.RunCommand(command) |
| |
| if ret == 0: |
| if self.branch != 'master': |
| command = ('git branch --track %s remotes/origin/%s' % |
| (self.branch, self.branch)) |
| else: |
| command = 'pwd' |
| command += '&& git checkout %s' % self.branch |
| else: |
| command = 'git symbolic-ref HEAD refs/heads/%s' % self.branch |
| command += '&& rm -rf *' |
| ret = self._ce.RunCommand(command) |
| return ret |
| |
| def CommitLocally(self, commit_message=None, message_file=None): |
| with misc.WorkingDirectory(self._root_dir): |
| command = 'pwd' |
| for ignore in self.ignores: |
| command += '&& echo \'%s\' >> .git/info/exclude' % ignore |
| command += '&& git add -Av .' |
| if message_file: |
| message_arg = '-F %s' % message_file |
| elif commit_message: |
| message_arg = '-m \'%s\'' % commit_message |
| else: |
| raise Exception('No commit message given!') |
| command += '&& git commit -v %s' % message_arg |
| return self._ce.RunCommand(command) |
| |
| def PushSources(self, commit_message=None, dry_run=False, message_file=None): |
| ret = self.CommitLocally(commit_message, message_file) |
| if ret: |
| return ret |
| push_args = '' |
| if dry_run: |
| push_args += ' -n ' |
| with misc.WorkingDirectory(self._root_dir): |
| if self.gerrit: |
| label = 'somelabel' |
| command = 'git remote add %s %s' % (label, self.address) |
| command += ('&& git push %s %s HEAD:refs/for/master' % |
| (push_args, label)) |
| else: |
| command = 'git push -v %s origin %s:%s' % (push_args, self.branch, |
| self.branch) |
| ret = self._ce.RunCommand(command) |
| return ret |
| |
| def MapSources(self, root_dir): |
| if not self.mappings: |
| self._RsyncExcludingRepoDirs(self._root_dir, root_dir) |
| return |
| with misc.WorkingDirectory(self._root_dir): |
| for mapping in self.mappings: |
| remote_path, local_path = SplitMapping(mapping) |
| remote_path.rstrip('...') |
| local_path.rstrip('...') |
| full_local_path = os.path.join(root_dir, local_path) |
| ret = self._RsyncExcludingRepoDirs(remote_path, full_local_path) |
| if ret: |
| return ret |
| return 0 |
| |
| |
| class RepoReader(object): |
| |
| def __init__(self, filename): |
| self.filename = filename |
| self.main_dict = {} |
| self.input_repos = [] |
| self.output_repos = [] |
| |
| def ParseFile(self): |
| with open(self.filename) as f: |
| self.main_dict = eval(f.read()) |
| self.CreateReposFromDict(self.main_dict) |
| return [self.input_repos, self.output_repos] |
| |
| def CreateReposFromDict(self, main_dict): |
| for key, repo_list in main_dict.items(): |
| for repo_dict in repo_list: |
| repo = self.CreateRepoFromDict(repo_dict) |
| if key == 'input': |
| self.input_repos.append(repo) |
| elif key == 'output': |
| self.output_repos.append(repo) |
| else: |
| logger.GetLogger().LogFatal('Unknown key: %s found' % key) |
| |
| def CreateRepoFromDict(self, repo_dict): |
| repo_type = repo_dict.get('type', None) |
| repo_address = repo_dict.get('address', None) |
| repo_mappings = repo_dict.get('mappings', None) |
| repo_ignores = repo_dict.get('ignores', None) |
| repo_branch = repo_dict.get('branch', None) |
| gerrit = repo_dict.get('gerrit', None) |
| revision = repo_dict.get('revision', None) |
| |
| if repo_type == 'p4': |
| repo = P4Repo(repo_address, repo_mappings, revision=revision) |
| elif repo_type == 'svn': |
| repo = SvnRepo(repo_address, repo_mappings) |
| elif repo_type == 'git': |
| repo = GitRepo(repo_address, |
| repo_branch, |
| mappings=repo_mappings, |
| ignores=repo_ignores, |
| gerrit=gerrit) |
| elif repo_type == 'file': |
| repo = FileRepo(repo_address) |
| else: |
| logger.GetLogger().LogFatal('Unknown repo type: %s' % repo_type) |
| return repo |
| |
| |
| @logger.HandleUncaughtExceptions |
| def Main(argv): |
| parser = optparse.OptionParser() |
| parser.add_option('-i', |
| '--input_file', |
| dest='input_file', |
| help='The input file that contains repo descriptions.') |
| |
| parser.add_option('-n', |
| '--dry_run', |
| dest='dry_run', |
| action='store_true', |
| default=False, |
| help='Do a dry run of the push.') |
| |
| parser.add_option('-F', |
| '--message_file', |
| dest='message_file', |
| default=None, |
| help='Use contents of the log file as the commit message.') |
| |
| options = parser.parse_args(argv)[0] |
| if not options.input_file: |
| parser.print_help() |
| return 1 |
| rr = RepoReader(options.input_file) |
| [input_repos, output_repos] = rr.ParseFile() |
| |
| # Make sure FileRepo is not used as output destination. |
| for output_repo in output_repos: |
| if output_repo.repo_type == 'file': |
| logger.GetLogger().LogFatal( |
| 'FileRepo is only supported as an input repo.') |
| |
| for output_repo in output_repos: |
| ret = output_repo.SetupForPush() |
| if ret: |
| return ret |
| |
| input_revisions = [] |
| for input_repo in input_repos: |
| ret = input_repo.PullSources() |
| if ret: |
| return ret |
| input_revisions.append(input_repo.revision) |
| |
| for input_repo in input_repos: |
| for output_repo in output_repos: |
| ret = input_repo.MapSources(output_repo.GetRoot()) |
| if ret: |
| return ret |
| |
| commit_message = 'Synced repos to: %s' % ','.join(input_revisions) |
| for output_repo in output_repos: |
| ret = output_repo.PushSources(commit_message=commit_message, |
| dry_run=options.dry_run, |
| message_file=options.message_file) |
| if ret: |
| return ret |
| |
| if not options.dry_run: |
| for output_repo in output_repos: |
| output_repo.CleanupRoot() |
| for input_repo in input_repos: |
| input_repo.CleanupRoot() |
| |
| return ret |
| |
| |
| if __name__ == '__main__': |
| retval = Main(sys.argv) |
| sys.exit(retval) |