aboutsummaryrefslogtreecommitdiff
path: root/repo_to_repo.py
diff options
context:
space:
mode:
Diffstat (limited to 'repo_to_repo.py')
-rwxr-xr-xrepo_to_repo.py421
1 files changed, 421 insertions, 0 deletions
diff --git a/repo_to_repo.py b/repo_to_repo.py
new file mode 100755
index 00000000..3b3b9bc4
--- /dev/null
+++ b/repo_to_repo.py
@@ -0,0 +1,421 @@
+#!/usr/bin/python2
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+"""Module for transferring files between various types of repositories."""
+
+from __future__ import print_function
+
+__author__ = 'asharif@google.com (Ahmad Sharif)'
+
+import argparse
+import datetime
+import json
+import os
+import re
+import socket
+import sys
+import tempfile
+
+from automation.clients.helper import perforce
+from cros_utils import command_executer
+from cros_utils import logger
+from cros_utils import misc
+
+# pylint: disable=anomalous-backslash-in-string
+
+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):
+ """Basic repository base class."""
+
+ 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 SetRoot(self, directory):
+ self._root_dir = directory
+
+ 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):
+ """Class for file repositories."""
+
+ 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
+ if ignores:
+ self.ignores += ignores
+
+ def CleanupRoot(self):
+ """Override to prevent deletion."""
+ pass
+
+
+class P4Repo(Repo):
+ """Class for P4 repositories."""
+
+
+ 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):
+ """Class for svn repositories."""
+
+ 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):
+ """Class for git repositories."""
+
+ 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 RuntimeError('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):
+ """Class for reading repositories."""
+
+ 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 = json.load(f)
+ 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 = argparse.ArgumentParser()
+ parser.add_argument('-i',
+ '--input_file',
+ dest='input_file',
+ help='The input file that contains repo descriptions.')
+
+ parser.add_argument('-n',
+ '--dry_run',
+ dest='dry_run',
+ action='store_true',
+ default=False,
+ help='Do a dry run of the push.')
+
+ parser.add_argument('-F',
+ '--message_file',
+ dest='message_file',
+ default=None,
+ help=('Use contents of the log file as the commit '
+ 'message.'))
+
+ options = parser.parse_args(argv)
+ 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[1:])
+ sys.exit(retval)