1#!/usr/bin/python
2# Copyright 2014 Google Inc.
3#
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7
8"""Parse a DEPS file and git checkout all of the dependencies.
9
10Args:
11  An optional list of deps_os values.
12
13Environment Variables:
14  GIT_EXECUTABLE: path to "git" binary; if unset, will look for one of
15  ['git', 'git.exe', 'git.bat'] in your default path.
16
17  GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
18  will use the file ../DEPS relative to this script's directory.
19
20  GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
21
22Git Config:
23  To disable syncing of a single repository:
24      cd path/to/repository
25      git config sync-deps.disable true
26
27  To re-enable sync:
28      cd path/to/repository
29      git config --unset sync-deps.disable
30"""
31
32
33import os
34import subprocess
35import sys
36import threading
37
38from git_utils import git_executable
39
40
41DEFAULT_DEPS_PATH = os.path.normpath(
42  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
43
44
45def usage(deps_file_path = None):
46  sys.stderr.write(
47    'Usage: run to grab dependencies, with optional platform support:\n')
48  sys.stderr.write('  python %s' % __file__)
49  if deps_file_path:
50    for deps_os in parse_file_to_dict(deps_file_path)['deps_os']:
51      sys.stderr.write(' [%s]' % deps_os)
52  else:
53    sys.stderr.write(' [DEPS_OS...]')
54  sys.stderr.write('\n\n')
55  sys.stderr.write(__doc__)
56
57
58def git_repository_sync_is_disabled(git, directory):
59  try:
60    disable = subprocess.check_output(
61      [git, 'config', 'sync-deps.disable'], cwd=directory)
62    return disable.lower().strip() in ['true', '1', 'yes', 'on']
63  except subprocess.CalledProcessError:
64    return False
65
66
67def is_git_toplevel(git, directory):
68  """Return true iff the directory is the top level of a Git repository.
69
70  Args:
71    git (string) the git executable
72
73    directory (string) the path into which the repository
74              is expected to be checked out.
75  """
76  try:
77    toplevel = subprocess.check_output(
78      [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
79    return os.path.realpath(directory) == os.path.realpath(toplevel)
80  except subprocess.CalledProcessError:
81    return False
82
83
84def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
85  """Checkout (and clone if needed) a Git repository.
86
87  Args:
88    git (string) the git executable
89
90    repo (string) the location of the repository, suitable
91         for passing to `git clone`.
92
93    checkoutable (string) a tag, branch, or commit, suitable for
94                 passing to `git checkout`
95
96    directory (string) the path into which the repository
97              should be checked out.
98
99    verbose (boolean)
100
101  Raises an exception if any calls to git fail.
102  """
103  if not os.path.isdir(directory):
104    subprocess.check_call(
105      [git, 'clone', '--quiet', repo, directory])
106
107  if not is_git_toplevel(git, directory):
108    # if the directory exists, but isn't a git repo, you will modify
109    # the parent repostory, which isn't what you want.
110    sys.stdout.write('%s\n  IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
111    return
112
113  # Check to see if this repo is disabled.  Quick return.
114  if git_repository_sync_is_disabled(git, directory):
115    sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
116    return
117
118  subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
119
120  subprocess.check_call(
121    [git, 'checkout', '--quiet', checkoutable], cwd=directory)
122
123  if verbose:
124    sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))  # Success.
125
126
127def parse_file_to_dict(path):
128  dictionary = {}
129  execfile(path, dictionary)
130  return dictionary
131
132
133class DepsError(Exception):
134  """Raised if deps_os is a bad key.
135  """
136  pass
137
138
139def git_sync_deps(deps_file_path, deps_os_list, verbose):
140  """Grab dependencies, with optional platform support.
141
142  Args:
143    deps_file_path (string) Path to the DEPS file.
144
145    deps_os_list (list of strings) Can be empty list.  List of
146                 strings that should each be a key in the deps_os
147                 dictionary in the DEPS file.
148
149  Raises DepsError exception and git Exceptions.
150  """
151  git = git_executable()
152  assert git
153
154  deps_file_directory = os.path.dirname(deps_file_path)
155  deps = parse_file_to_dict(deps_file_path)
156  dependencies = deps['deps'].copy()
157  for deps_os in deps_os_list:
158    # Add OS-specific dependencies
159    if deps_os not in deps['deps_os']:
160      raise DepsError(
161        'Argument "%s" not found within deps_os keys %r' %
162        (deps_os, deps['deps_os'].keys()))
163    for dep in deps['deps_os'][deps_os]:
164      dependencies[dep] = deps['deps_os'][deps_os][dep]
165  list_of_arg_lists = []
166  for directory in dependencies:
167    if '@' in dependencies[directory]:
168      repo, checkoutable = dependencies[directory].split('@', 1)
169    else:
170      repo, checkoutable = dependencies[directory], 'origin/master'
171
172    relative_directory = os.path.join(deps_file_directory, directory)
173
174    list_of_arg_lists.append(
175      (git, repo, checkoutable, relative_directory, verbose))
176
177  multithread(git_checkout_to_directory, list_of_arg_lists)
178
179
180def multithread(function, list_of_arg_lists):
181  # for args in list_of_arg_lists:
182  #   function(*args)
183  # return
184  threads = []
185  for args in list_of_arg_lists:
186    thread = threading.Thread(None, function, None, args)
187    thread.start()
188    threads.append(thread)
189  for thread in threads:
190    thread.join()
191
192
193def main(argv):
194  deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
195  verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
196  try:
197    git_sync_deps(deps_file_path, argv, verbose)
198    return 0
199  except DepsError:
200    usage(deps_file_path)
201    return 1
202
203
204if __name__ == '__main__':
205  exit(main(sys.argv[1:]))
206