Upstream version 9.38.198.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, clean=False,
97                 pathspec=None, 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   clean: If True, discard local changes and untracked files.
107          Otherwise the checkout will fail if there are uncommitted changes.
108   pathspec: If not None, add the path to the git checkout command, which
109             causes it to just update the working tree without switching
110             branches.
111   git_cache: If set, assumes URL has been populated within the git cache
112              directory specified and sets the fetch URL to be from the
113              git_cache.
114   """
115   if logger is None:
116     logger = log_tools.GetConsoleLogger()
117   if reclone:
118     logger.debug('Clobbering source directory %s' % destination)
119     file_tools.RemoveDirectoryIfPresent(destination)
120
121   if git_cache:
122     git_cache_url = GetGitCacheURL(git_cache, url)
123   else:
124     git_cache_url = None
125
126   # If the destination is a git repository, validate the tracked origin.
127   git_dir = os.path.join(destination, '.git')
128   if os.path.exists(git_dir):
129     if not IsURLInRemoteRepoList(url, destination, include_fetch=True,
130                                  include_push=False):
131       # If the git cache URL is being tracked instead of the fetch URL, we
132       # can safely redirect it to the fetch URL instead.
133       if git_cache_url and IsURLInRemoteRepoList(git_cache_url, destination,
134                                                  include_fetch=True,
135                                                  include_push=False):
136         GitSetRemoteRepo(url, destination, push_url=push_url,
137                          logger=logger)
138       else:
139         logger.error('Git Repo (%s) does not track URL: %s',
140                       destination, url)
141         raise InvalidRepoException(url, 'Could not sync git repo: %s',
142                                    destination)
143
144       # Make sure the push URL is set correctly as well.
145       if not IsURLInRemoteRepoList(push_url, destination, include_fetch=False,
146                                    include_push=True):
147         GitSetRemoteRepo(url, destination, push_url=push_url)
148
149   git = GitCmd()
150   if not os.path.exists(git_dir):
151     logger.info('Cloning %s...' % url)
152
153     file_tools.MakeDirectoryIfAbsent(destination)
154     clone_args = ['clone', '-n']
155     if git_cache_url:
156       clone_args.extend(['--reference', git_cache_url])
157
158     log_tools.CheckCall(git + clone_args + [url, '.'],
159                         logger=logger, cwd=destination)
160
161     if url != push_url:
162       GitSetRemoteRepo(url, destination, push_url=push_url, logger=logger)
163   elif clean:
164     log_tools.CheckCall(git + ['clean', '-dffx'],
165                         logger=logger, cwd=destination)
166     log_tools.CheckCall(git + ['reset', '--hard', 'HEAD'],
167                         logger=logger, cwd=destination)
168
169   # If a git cache URL is supplied, make sure it is setup as a git alternate.
170   if git_cache_url:
171     git_alternates = [git_cache_url]
172   else:
173     git_alternates = []
174
175   GitSetRepoAlternates(destination, git_alternates, append=False, logger=logger)
176
177   if revision is not None:
178     logger.info('Checking out pinned revision...')
179     log_tools.CheckCall(git + ['fetch', '--all'],
180                         logger=logger, cwd=destination)
181     checkout_flags = ['-f'] if clean else []
182     path = [pathspec] if pathspec else []
183     log_tools.CheckCall(
184         git + ['checkout'] + checkout_flags + [revision] + path,
185         logger=logger, cwd=destination)
186
187
188 def CleanGitWorkingDir(directory, path, logger=None):
189   """Clean a particular path of an existing git checkout.
190
191      Args:
192      directory: Directory where the git repo is currently checked out
193      path: path to clean, relative to the repo directory
194   """
195   log_tools.CheckCall(GitCmd() + ['clean', '-f', path],
196                       logger=logger, cwd=directory)
197
198
199 def PopulateGitCache(cache_dir, url_list, logger=None):
200   """Fetches a git repo that combines a list of git repos.
201
202   This is an interface to the "git cache" command found within depot_tools.
203   You can populate a cache directory then obtain the local cache url using
204   GetGitCacheURL(). It is best to sync with the shared option so that the
205   cloned repository shares the same git objects.
206
207   Args:
208     cache_dir: Local directory where git cache will be populated.
209     url_list: List of URLs which cache_dir should be populated with.
210   """
211   if url_list:
212     file_tools.MakeDirectoryIfAbsent(cache_dir)
213     git = GitCmd()
214     for url in url_list:
215       log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
216                           logger=logger, cwd=cache_dir)
217
218
219 def GetGitCacheURL(cache_dir, url, logger=None):
220   """Converts a regular git URL to a git cache URL within a cache directory.
221
222   Args:
223     url: original Git URL that is already populated within the cache directory.
224     cache_dir: Git cache directory that has already populated the URL.
225
226   Returns:
227     Git Cache URL where a git repository can clone/fetch from.
228   """
229   # Make sure we are using absolute paths or else cache exists return relative.
230   cache_dir = os.path.abspath(cache_dir)
231
232   # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
233   cygwin_path = False
234   if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
235     cygwin_path = True
236     drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
237     cache_dir = drive + ':\\' + file_path.replace('/', '\\')
238
239   git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
240                                               '-c', cache_dir,
241                                               url],
242                                   logger=logger).strip()
243
244   # For windows, make sure the git cache URL is a posix path.
245   if platform.IsWindows():
246     git_url = git_url.replace('\\', '/')
247   return git_url
248
249
250 def GitRevInfo(directory):
251   """Get the git revision information of a git checkout.
252
253   Args:
254     directory: Existing git working directory.
255 """
256   url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
257                               cwd=directory)
258   rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
259                               cwd=directory)
260   return url.strip(), rev.strip()
261
262
263 def SvnRevInfo(directory):
264   """Get the SVN revision information of an existing svn/gclient checkout.
265
266   Args:
267      directory: Directory where the svn repo is currently checked out
268   """
269   info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
270   url = ''
271   rev = ''
272   for line in info.splitlines():
273     pieces = line.split(':', 1)
274     if len(pieces) != 2:
275       continue
276     if pieces[0] == 'URL':
277       url = pieces[1].strip()
278     elif pieces[0] == 'Revision':
279       rev = pieces[1].strip()
280   if not url or not rev:
281     raise RuntimeError('Missing svn info url: %s and rev: %s' % (url, rev))
282   return url, rev
283
284
285 def GetAuthenticatedGitURL(url):
286   """Returns the authenticated version of a git URL.
287
288   In chromium, there is a special URL that is the "authenticated" version. The
289   URLs are identical but the authenticated one has special privileges.
290   """
291   urlsplit = urlparse.urlsplit(url)
292   if urlsplit.scheme in ('https', 'http'):
293     urldict = urlsplit._asdict()
294     urldict['scheme'] = 'https'
295     urldict['path'] = '/a' + urlsplit.path
296     urlsplit = urlparse.SplitResult(**urldict)
297
298   return urlsplit.geturl()
299
300
301 def GitRemoteRepoList(directory, include_fetch=True, include_push=True,
302                       logger=None):
303   """Returns a list of remote git repos associated with a directory.
304
305   Args:
306       directory: Existing git working directory.
307   Returns:
308       List of (repo_name, repo_url) for tracked remote repos.
309   """
310   remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
311                                        logger=logger, cwd=directory)
312
313   repo_set = set()
314   for remote_repo_line in remote_repos.splitlines():
315     repo_name, repo_url, repo_type = remote_repo_line.split()
316     if include_fetch and repo_type == '(fetch)':
317       repo_set.add((repo_name, repo_url))
318     elif include_push and repo_type == '(push)':
319       repo_set.add((repo_name, repo_url))
320
321   return sorted(repo_set)
322
323
324 def GitSetRemoteRepo(url, directory, push_url=None,
325                      repo_name='origin', logger=None):
326   """Sets the remotely tracked URL for a git repository.
327
328   Args:
329       url: Remote git URL to set.
330       directory: Local git repository to set tracked repo for.
331       push_url: If specified, uses a different URL for pushing.
332       repo_name: set the URL for a particular remote repo name.
333   """
334   git = GitCmd()
335   try:
336     log_tools.CheckCall(git + ['remote', 'set-url', repo_name, url],
337                         logger=logger, cwd=directory)
338   except subprocess.CalledProcessError:
339     # If setting the URL failed, repo_name may be new. Try adding the URL.
340     log_tools.CheckCall(git + ['remote', 'add', repo_name, url],
341                         logger=logger, cwd=directory)
342
343   if push_url:
344     log_tools.CheckCall(git + ['remote', 'set-url', '--push',
345                                repo_name, push_url],
346                         logger=logger, cwd=directory)
347
348
349 def IsURLInRemoteRepoList(url, directory, include_fetch=True, include_push=True,
350                           try_authenticated_url=True, logger=None):
351   """Returns whether or not a url is a remote repo in a local git directory.
352
353   Args:
354       url: URL to look for in remote repo list.
355       directory: Existing git working directory.
356   """
357   if try_authenticated_url:
358     valid_urls = (url, GetAuthenticatedGitURL(url))
359   else:
360     valid_urls = (url,)
361
362   remote_repo_list = GitRemoteRepoList(directory,
363                                        include_fetch=include_fetch,
364                                        include_push=include_push,
365                                        logger=logger)
366   return len([repo_name for
367               repo_name, repo_url in remote_repo_list
368               if repo_url in valid_urls]) > 0
369
370
371 def GitGetRepoAlternates(directory):
372   """Gets the list of git alternates for a local git repo.
373
374   Args:
375       directory: Local git repository to get the git alternate for.
376
377   Returns:
378       List of git alternates set for the local git repository.
379   """
380   git_alternates_file = os.path.join(directory, GIT_ALTERNATES_PATH)
381   if os.path.isfile(git_alternates_file):
382     with open(git_alternates_file, 'rt') as f:
383       alternates_list = []
384       for line in f.readlines():
385         line = line.strip()
386         if line:
387           if posixpath.basename(line) == 'objects':
388             line = posixpath.dirname(line)
389           alternates_list.append(line)
390
391       return alternates_list
392
393   return []
394
395
396 def GitSetRepoAlternates(directory, alternates_list, append=True, logger=None):
397   """Sets the list of git alternates for a local git repo.
398
399   Args:
400       directory: Local git repository.
401       alternates_list: List of local git repositories for the git alternates.
402       append: If True, will append the list to currently set list of alternates.
403   """
404   if logger is None:
405     logger = log_tools.GetConsoleLogger()
406   git_alternates_file = os.path.join(directory, GIT_ALTERNATES_PATH)
407   git_alternates_dir = os.path.dirname(git_alternates_file)
408   if not os.path.isdir(git_alternates_dir):
409     raise InvalidRepoException(directory,
410                                'Invalid local git repo: %s', directory)
411
412   original_alternates_list = GitGetRepoAlternates(directory)
413   if append:
414     alternates_list.extend(original_alternates_list)
415     alternates_list = sorted(set(alternates_list))
416
417   if set(original_alternates_list) != set(alternates_list):
418     lines = [posixpath.join(line, 'objects') + '\n' for line in alternates_list]
419     logger.info('Setting git alternates:\n\t%s', '\t'.join(lines))
420
421     with open(git_alternates_file, 'wb') as f:
422       f.writelines(lines)