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
38
39def git_executable():
40  """Find the git executable.
41
42  Returns:
43      A string suitable for passing to subprocess functions, or None.
44  """
45  envgit = os.environ.get('GIT_EXECUTABLE')
46  searchlist = ['git', 'git.exe', 'git.bat']
47  if envgit:
48    searchlist.insert(0, envgit)
49  with open(os.devnull, 'w') as devnull:
50    for git in searchlist:
51      try:
52        subprocess.call([git, '--version'], stdout=devnull)
53      except (OSError,):
54        continue
55      return git
56  return None
57
58
59DEFAULT_DEPS_PATH = os.path.normpath(
60  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
61
62
63def usage(deps_file_path = None):
64  sys.stderr.write(
65    'Usage: run to grab dependencies, with optional platform support:\n')
66  sys.stderr.write('  python %s' % __file__)
67  if deps_file_path:
68    for deps_os in parse_file_to_dict(deps_file_path)['deps_os']:
69      sys.stderr.write(' [%s]' % deps_os)
70  else:
71    sys.stderr.write(' [DEPS_OS...]')
72  sys.stderr.write('\n\n')
73  sys.stderr.write(__doc__)
74
75
76def git_repository_sync_is_disabled(git, directory):
77  try:
78    disable = subprocess.check_output(
79      [git, 'config', 'sync-deps.disable'], cwd=directory)
80    return disable.lower().strip() in ['true', '1', 'yes', 'on']
81  except subprocess.CalledProcessError:
82    return False
83
84
85def is_git_toplevel(git, directory):
86  """Return true iff the directory is the top level of a Git repository.
87
88  Args:
89    git (string) the git executable
90
91    directory (string) the path into which the repository
92              is expected to be checked out.
93  """
94  try:
95    toplevel = subprocess.check_output(
96      [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
97    return os.path.realpath(directory) == os.path.realpath(toplevel)
98  except subprocess.CalledProcessError:
99    return False
100
101
102def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
103  """Checkout (and clone if needed) a Git repository.
104
105  Args:
106    git (string) the git executable
107
108    repo (string) the location of the repository, suitable
109         for passing to `git clone`.
110
111    checkoutable (string) a tag, branch, or commit, suitable for
112                 passing to `git checkout`
113
114    directory (string) the path into which the repository
115              should be checked out.
116
117    verbose (boolean)
118
119  Raises an exception if any calls to git fail.
120  """
121  if not os.path.isdir(directory):
122    subprocess.check_call(
123      [git, 'clone', '--quiet', repo, directory])
124
125  if not is_git_toplevel(git, directory):
126    # if the directory exists, but isn't a git repo, you will modify
127    # the parent repostory, which isn't what you want.
128    sys.stdout.write('%s\n  IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
129    return
130
131  # Check to see if this repo is disabled.  Quick return.
132  if git_repository_sync_is_disabled(git, directory):
133    sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
134    return
135
136  if 0 == subprocess.call(
137      [git, 'checkout', '--quiet', checkoutable], cwd=directory):
138    # if this succeeds, skip slow `git fetch`.
139    if verbose:
140      sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))
141    return
142
143  subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
144
145  if 0 != subprocess.call(
146      [git, 'checkout', '--quiet', checkoutable], cwd=directory):
147      subprocess.check_call(
148          [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
149      subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
150      subprocess.check_call([git, 'checkout', '--quiet'], cwd=directory)
151
152  if verbose:
153    sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))  # Success.
154
155
156def parse_file_to_dict(path):
157  dictionary = {}
158  execfile(path, dictionary)
159  return dictionary
160
161
162def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
163  """Grab dependencies, with optional platform support.
164
165  Args:
166    deps_file_path (string) Path to the DEPS file.
167
168    command_line_os_requests (list of strings) Can be empty list.
169        List of strings that should each be a key in the deps_os
170        dictionary in the DEPS file.
171
172  Raises git Exceptions.
173  """
174  git = git_executable()
175  assert git
176
177  deps_file_directory = os.path.dirname(deps_file_path)
178  deps_file = parse_file_to_dict(deps_file_path)
179  dependencies = deps_file['deps'].copy()
180  os_specific_dependencies = deps_file.get('deps_os', [])
181  for os_name in command_line_os_requests:
182    # Add OS-specific dependencies
183    if os_name in os_specific_dependencies:
184      dependencies.update(os_specific_dependencies[os_name])
185  list_of_arg_lists = []
186  for directory in dependencies:
187    if '@' in dependencies[directory]:
188      repo, checkoutable = dependencies[directory].split('@', 1)
189    else:
190      raise Exception("please specify commit or tag")
191
192    relative_directory = os.path.join(deps_file_directory, directory)
193
194    list_of_arg_lists.append(
195      (git, repo, checkoutable, relative_directory, verbose))
196
197  multithread(git_checkout_to_directory, list_of_arg_lists)
198
199  for directory in deps_file.get('recursedeps', []):
200    recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
201    git_sync_deps(recursive_path, command_line_os_requests, verbose)
202
203
204def multithread(function, list_of_arg_lists):
205  # for args in list_of_arg_lists:
206  #   function(*args)
207  # return
208  threads = []
209  for args in list_of_arg_lists:
210    thread = threading.Thread(None, function, None, args)
211    thread.start()
212    threads.append(thread)
213  for thread in threads:
214    thread.join()
215
216
217def main(argv):
218  deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
219  verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
220
221  if '--help' in argv or '-h' in argv:
222    usage(deps_file_path)
223    return 1
224
225  git_sync_deps(deps_file_path, argv, verbose)
226  return 0
227
228
229if __name__ == '__main__':
230  exit(main(sys.argv[1:]))
231