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