Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / native_client / pynacl / repo_tools.py
1 #!/usr/bin/python
2 # Copyright (c) 2013 The Native Client Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 import logging
7 import os
8 import posixpath
9 import subprocess
10 import sys
11 import urlparse
12
13 import file_tools
14 import log_tools
15 import platform
16
17 GIT_ALTERNATES_PATH = os.path.join('.git', 'objects', 'info', 'alternates')
18
19
20 class InvalidRepoException(Exception):
21   def __init__(self, expected_repo, msg, *args):
22     Exception.__init__(self, msg % args)
23     self.expected_repo = expected_repo
24
25
26 def GitCmd():
27   """Return the git command to execute for the host platform."""
28   if platform.IsWindows():
29     # On windows, we want to use the depot_tools version of git, which has
30     # git.bat as an entry point. When running through the msys command
31     # prompt, subprocess does not handle batch files. Explicitly invoking
32     # cmd.exe to be sure we run the correct git in this case.
33     return ['cmd.exe', '/c', 'git.bat']
34   else:
35     return ['git']
36
37
38 def CheckGitOutput(args):
39   """Run a git subcommand and capture its stdout a la subprocess.check_output.
40   Args:
41     args: list of arguments to 'git'
42   """
43   return log_tools.CheckOutput(GitCmd() + args)
44
45
46 def SvnCmd():
47   """Return the svn command to execute for the host platform."""
48   if platform.IsWindows():
49     return ['cmd.exe', '/c', 'svn.bat']
50   else:
51     return ['svn']
52
53
54 def ValidateGitRepo(url, directory, clobber_mismatch=False, logger=None):
55   """Validates a git repository tracks a particular URL.
56
57   Given a git directory, this function will validate if the git directory
58   actually tracks an expected URL. If the directory does not exist nothing
59   will be done.
60
61   Args:
62   url: URL to look for.
63   directory: Directory to look for.
64   clobber_mismatch: If True, will delete invalid directories instead of raising
65                     an exception.
66   """
67   if logger is None:
68     logger = log_tools.GetConsoleLogger()
69   git_dir = os.path.join(directory, '.git')
70   if os.path.exists(git_dir):
71     try:
72       if IsURLInRemoteRepoList(url, directory, include_fetch=True,
73                                include_push=False):
74         return
75
76       logger.warn('Local git repo (%s) does not track url (%s)',
77                    directory, url)
78     except:
79       logger.error('Invalid git repo: %s', directory)
80
81     if not clobber_mismatch:
82       raise InvalidRepoException(url, 'Invalid local git repo: %s', directory)
83     else:
84       logger.debug('Clobbering invalid git repo %s' % directory)
85       file_tools.RemoveDirectoryIfPresent(directory)
86   elif os.path.exists(directory) and len(os.listdir(directory)) != 0:
87     if not clobber_mismatch:
88       raise InvalidRepoException(url,
89                                  'Invalid non-empty repository destination %s',
90                                  directory)
91     else:
92       logger.debug('Clobbering intended repository destination: %s', directory)
93       file_tools.RemoveDirectoryIfPresent(directory)
94
95
96 def SyncGitRepo(url, destination, revision, reclone=False, pathspec=None,
97                 git_cache=None, push_url=None, logger=None):
98   """Sync an individual git repo.
99
100   Args:
101   url: URL to sync
102   destination: Directory to check out into.
103   revision: Pinned revision to check out. If None, do not check out a
104             pinned revision.
105   reclone: If True, delete the destination directory and re-clone the repo.
106   pathspec: If not None, add the path to the git checkout command, which
107             causes it to just update the working tree without switching
108             branches.
109   git_cache: If set, assumes URL has been populated within the git cache
110              directory specified and sets the fetch URL to be from the
111              git_cache.
112   """
113   if logger is None:
114     logger = log_tools.GetConsoleLogger()
115   if reclone:
116     logger.debug('Clobbering source directory %s' % destination)
117     file_tools.RemoveDirectoryIfPresent(destination)
118
119   if git_cache:
120     git_cache_url = GetGitCacheURL(git_cache, url)
121   else:
122     git_cache_url = None
123
124   # If the destination is a git repository, validate the tracked origin.
125   git_dir = os.path.join(destination, '.git')
126   if os.path.exists(git_dir):
127     if not IsURLInRemoteRepoList(url, destination, include_fetch=True,
128                                  include_push=False):
129       # If the git cache URL is being tracked instead of the fetch URL, we
130       # can safely redirect it to the fetch URL instead.
131       if git_cache_url and IsURLInRemoteRepoList(git_cache_url, destination,
132                                                  include_fetch=True,
133                                                  include_push=False):
134         GitSetRemoteRepo(url, destination, push_url=push_url,
135                          logger=logger)
136       else:
137         logger.error('Git Repo (%s) does not track URL: %s',
138                       destination, url)
139         raise InvalidRepoException(url, 'Could not sync git repo: %s',
140                                    destination)
141
142       # Make sure the push URL is set correctly as well.
143       if not IsURLInRemoteRepoList(push_url, destination, include_fetch=False,
144                                    include_push=True):
145         GitSetRemoteRepo(url, destination, push_url=push_url)
146
147   git = GitCmd()
148   if not os.path.exists(git_dir):
149     logger.info('Cloning %s...' % url)
150
151     file_tools.MakeDirectoryIfAbsent(destination)
152     clone_args = ['clone', '-n']
153     if git_cache_url:
154       clone_args.extend(['--reference', git_cache_url])
155
156     log_tools.CheckCall(git + clone_args + [url, '.'],
157                         logger=logger, cwd=destination)
158
159     if url != push_url:
160       GitSetRemoteRepo(url, destination, push_url=push_url, logger=logger)
161
162   # If a git cache URL is supplied, make sure it is setup as a git alternate.
163   if git_cache_url:
164     git_alternates = [git_cache_url]
165   else:
166     git_alternates = []
167
168   GitSetRepoAlternates(destination, git_alternates, append=False, logger=logger)
169
170   if revision is not None:
171     logger.info('Checking out pinned revision...')
172     log_tools.CheckCall(git + ['fetch', '--all'],
173                         logger=logger, cwd=destination)
174     path = [pathspec] if pathspec else []
175     log_tools.CheckCall(
176         git + ['checkout', revision] + path,
177         logger=logger, cwd=destination)
178
179
180 def CleanGitWorkingDir(directory, reset=False, path=None, logger=None):
181   """Clean all or part of an existing git checkout.
182
183      Args:
184      directory: Directory where the git repo is currently checked out
185      reset: If True, also reset the working directory to HEAD
186      path: path to clean, relative to the repo directory. If None,
187            clean the whole working directory
188   """
189   repo_path = [path] if path else []
190   log_tools.CheckCall(GitCmd() + ['clean', '-dffx'] + repo_path,
191                       logger=logger, cwd=directory)
192   if reset:
193     log_tools.CheckCall(GitCmd() + ['reset', '--hard', 'HEAD'],
194                         logger=logger, cwd=directory)
195
196
197 def PopulateGitCache(cache_dir, url_list, logger=None):
198   """Fetches a git repo that combines a list of git repos.
199
200   This is an interface to the "git cache" command found within depot_tools.
201   You can populate a cache directory then obtain the local cache url using
202   GetGitCacheURL(). It is best to sync with the shared option so that the
203   cloned repository shares the same git objects.
204
205   Args:
206     cache_dir: Local directory where git cache will be populated.
207     url_list: List of URLs which cache_dir should be populated with.
208   """
209   if url_list:
210     file_tools.MakeDirectoryIfAbsent(cache_dir)
211     git = GitCmd()
212     for url in url_list:
213       log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
214                           logger=logger, cwd=cache_dir)
215
216
217 def GetGitCacheURL(cache_dir, url, logger=None):
218   """Converts a regular git URL to a git cache URL within a cache directory.
219
220   Args:
221     url: original Git URL that is already populated within the cache directory.
222     cache_dir: Git cache directory that has already populated the URL.
223
224   Returns:
225     Git Cache URL where a git repository can clone/fetch from.
226   """
227   # Make sure we are using absolute paths or else cache exists return relative.
228   cache_dir = os.path.abspath(cache_dir)
229
230   # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
231   cygwin_path = False
232   if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
233     cygwin_path = True
234     drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
235     cache_dir = drive + ':\\' + file_path.replace('/', '\\')
236
237   git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
238                                               '-c', cache_dir,
239                                               url],
240                                   logger=logger).strip()
241
242   # For windows, make sure the git cache URL is a posix path.
243   if platform.IsWindows():
244     git_url = git_url.replace('\\', '/')
245   return git_url
246
247
248 def GitRevInfo(directory):
249   """Get the git revision information of a git checkout.
250
251   Args:
252     directory: Existing git working directory.
253 """
254   url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
255                               cwd=directory)
256   rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
257                               cwd=directory)
258   return url.strip(), rev.strip()
259
260
261 def SvnRevInfo(directory):
262   """Get the SVN revision information of an existing svn/gclient checkout.
263
264   Args:
265      directory: Directory where the svn repo is currently checked out
266   """
267   info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
268   url = ''
269   rev = ''
270   for line in info.splitlines():
271     pieces = line.split(':', 1)
272     if len(pieces) != 2:
273       continue
274     if pieces[0] == 'URL':
275       url = pieces[1].strip()
276     elif pieces[0] == 'Revision':
277       rev = pieces[1].strip()
278   if not url or not rev:
279     raise RuntimeError('Missing svn info url: %s and rev: %s' % (url, rev))
280   return url, rev
281
282
283 def GetAuthenticatedGitURL(url):
284   """Returns the authenticated version of a git URL.
285
286   In chromium, there is a special URL that is the "authenticated" version. The
287   URLs are identical but the authenticated one has special privileges.
288   """
289   urlsplit = urlparse.urlsplit(url)
290   if urlsplit.scheme in ('https', 'http'):
291     urldict = urlsplit._asdict()
292     urldict['scheme'] = 'https'
293     urldict['path'] = '/a' + urlsplit.path
294     urlsplit = urlparse.SplitResult(**urldict)
295
296   return urlsplit.geturl()
297
298
299 def GitRemoteRepoList(directory, include_fetch=True, include_push=True,
300                       logger=None):
301   """Returns a list of remote git repos associated with a directory.
302
303   Args:
304       directory: Existing git working directory.
305   Returns:
306       List of (repo_name, repo_url) for tracked remote repos.
307   """
308   remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
309                                        logger=logger, cwd=directory)
310
311   repo_set = set()
312   for remote_repo_line in remote_repos.splitlines():
313     repo_name, repo_url, repo_type = remote_repo_line.split()
314     if include_fetch and repo_type == '(fetch)':
315       repo_set.add((repo_name, repo_url))
316     elif include_push and repo_type == '(push)':
317       repo_set.add((repo_name, repo_url))
318
319   return sorted(repo_set)
320
321
322 def GitSetRemoteRepo(url, directory, push_url=None,
323                      repo_name='origin', logger=None):
324   """Sets the remotely tracked URL for a git repository.
325
326   Args:
327       url: Remote git URL to set.
328       directory: Local git repository to set tracked repo for.
329       push_url: If specified, uses a different URL for pushing.
330       repo_name: set the URL for a particular remote repo name.
331   """
332   git = GitCmd()
333   try:
334     log_tools.CheckCall(git + ['remote', 'set-url', repo_name, url],
335                         logger=logger, cwd=directory)
336   except subprocess.CalledProcessError:
337     # If setting the URL failed, repo_name may be new. Try adding the URL.
338     log_tools.CheckCall(git + ['remote', 'add', repo_name, url],
339                         logger=logger, cwd=directory)
340
341   if push_url:
342     log_tools.CheckCall(git + ['remote', 'set-url', '--push',
343                                repo_name, push_url],
344                         logger=logger, cwd=directory)
345
346
347 def IsURLInRemoteRepoList(url, directory, include_fetch=True, include_push=True,
348                           try_authenticated_url=True, logger=None):
349   """Returns whether or not a url is a remote repo in a local git directory.
350
351   Args:
352       url: URL to look for in remote repo list.
353       directory: Existing git working directory.
354   """
355   if try_authenticated_url:
356     valid_urls = (url, GetAuthenticatedGitURL(url))
357   else:
358     valid_urls = (url,)
359
360   remote_repo_list = GitRemoteRepoList(directory,
361                                        include_fetch=include_fetch,
362                                        include_push=include_push,
363                                        logger=logger)
364   return len([repo_name for
365               repo_name, repo_url in remote_repo_list
366               if repo_url in valid_urls]) > 0
367
368
369 def GitGetRepoAlternates(directory):
370   """Gets the list of git alternates for a local git repo.
371
372   Args:
373       directory: Local git repository to get the git alternate for.
374
375   Returns:
376       List of git alternates set for the local git repository.
377   """
378   git_alternates_file = os.path.join(directory, GIT_ALTERNATES_PATH)
379   if os.path.isfile(git_alternates_file):
380     with open(git_alternates_file, 'rt') as f:
381       alternates_list = []
382       for line in f.readlines():
383         line = line.strip()
384         if line:
385           if posixpath.basename(line) == 'objects':
386             line = posixpath.dirname(line)
387           alternates_list.append(line)
388
389       return alternates_list
390
391   return []
392
393
394 def GitSetRepoAlternates(directory, alternates_list, append=True, logger=None):
395   """Sets the list of git alternates for a local git repo.
396
397   Args:
398       directory: Local git repository.
399       alternates_list: List of local git repositories for the git alternates.
400       append: If True, will append the list to currently set list of alternates.
401   """
402   if logger is None:
403     logger = log_tools.GetConsoleLogger()
404   git_alternates_file = os.path.join(directory, GIT_ALTERNATES_PATH)
405   git_alternates_dir = os.path.dirname(git_alternates_file)
406   if not os.path.isdir(git_alternates_dir):
407     raise InvalidRepoException(directory,
408                                'Invalid local git repo: %s', directory)
409
410   original_alternates_list = GitGetRepoAlternates(directory)
411   if append:
412     alternates_list.extend(original_alternates_list)
413     alternates_list = sorted(set(alternates_list))
414
415   if set(original_alternates_list) != set(alternates_list):
416     lines = [posixpath.join(line, 'objects') + '\n' for line in alternates_list]
417     logger.info('Setting git alternates:\n\t%s', '\t'.join(lines))
418
419     with open(git_alternates_file, 'wb') as f:
420       f.writelines(lines)