d8e22f76d2d26f10933046af1e463668a335cae9
[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 subprocess
9 import sys
10 import urlparse
11
12 import file_tools
13 import log_tools
14 import platform
15
16 class InvalidRepoException(Exception):
17   def __init__(self, expected_repo, msg, *args):
18     Exception.__init__(self, msg % args)
19     self.expected_repo = expected_repo
20
21
22 def GitCmd():
23   """Return the git command to execute for the host platform."""
24   if platform.IsWindows():
25     # On windows, we want to use the depot_tools version of git, which has
26     # git.bat as an entry point. When running through the msys command
27     # prompt, subprocess does not handle batch files. Explicitly invoking
28     # cmd.exe to be sure we run the correct git in this case.
29     return ['cmd.exe', '/c', 'git.bat']
30   else:
31     return ['git']
32
33
34 def CheckGitOutput(args):
35   """Run a git subcommand and capture its stdout a la subprocess.check_output.
36   Args:
37     args: list of arguments to 'git'
38   """
39   return log_tools.CheckOutput(GitCmd() + args)
40
41
42 def SvnCmd():
43   """Return the svn command to execute for the host platform."""
44   if platform.IsWindows():
45     return ['cmd.exe', '/c', 'svn.bat']
46   else:
47     return ['svn']
48
49
50 def ValidateGitRepo(url, directory, clobber_mismatch=False):
51   """Validates a git repository tracks a particular URL.
52
53   Given a git directory, this function will validate if the git directory
54   actually tracks an expected URL. If the directory does not exist nothing
55   will be done.
56
57   Args:
58   url: URL to look for.
59   directory: Directory to look for.
60   clobber_mismatch: If True, will delete invalid directories instead of raising
61                     an exception.
62   """
63   git_dir = os.path.join(directory, '.git')
64   if os.path.exists(git_dir):
65     try:
66       if IsURLInRemoteRepoList(url, directory, include_fetch=True,
67                                include_push=False):
68         return
69
70       logging.warn('Local git repo (%s) does not track url (%s)',
71                    directory, url)
72     except:
73       logging.error('Invalid git repo: %s', directory)
74
75     if not clobber_mismatch:
76       raise InvalidRepoException(url, 'Invalid local git repo: %s', directory)
77     else:
78       logging.debug('Clobbering invalid git repo %s' % directory)
79       file_tools.RemoveDirectoryIfPresent(directory)
80   elif os.path.exists(directory) and len(os.listdir(directory)) != 0:
81     if not clobber_mismatch:
82       raise InvalidRepoException(url,
83                                  'Invalid non-empty repository destination %s',
84                                  directory)
85     else:
86       logging.debug('Clobbering intended repository destination: %s', directory)
87       file_tools.RemoveDirectoryIfPresent(directory)
88
89
90 def SyncGitRepo(url, destination, revision, reclone=False, clean=False,
91                 pathspec=None, git_cache=None, push_url=None):
92   """Sync an individual git repo.
93
94   Args:
95   url: URL to sync
96   destination: Directory to check out into.
97   revision: Pinned revision to check out. If None, do not check out a
98             pinned revision.
99   reclone: If True, delete the destination directory and re-clone the repo.
100   clean: If True, discard local changes and untracked files.
101          Otherwise the checkout will fail if there are uncommitted changes.
102   pathspec: If not None, add the path to the git checkout command, which
103             causes it to just update the working tree without switching
104             branches.
105   git_cache: If set, assumes URL has been populated within the git cache
106              directory specified and sets the fetch URL to be from the
107              git_cache.
108   """
109   if reclone:
110     logging.debug('Clobbering source directory %s' % destination)
111     file_tools.RemoveDirectoryIfPresent(destination)
112
113   if git_cache:
114     fetch_url = GetGitCacheURL(git_cache, url)
115   else:
116     fetch_url = url
117
118   # If the destination is a git repository, validate the tracked origin.
119   git_dir = os.path.join(destination, '.git')
120   if os.path.exists(git_dir):
121     if not IsURLInRemoteRepoList(fetch_url, destination, include_fetch=True,
122                                  include_push=False):
123       # If the original URL is being tracked instead of the fetch URL, we
124       # can safely redirect it to the fetch URL instead.
125       if (fetch_url != url and IsURLInRemoteRepoList(url, destination,
126                                                      include_fetch=True,
127                                                      include_push=False)):
128         GitSetRemoteRepo(fetch_url, destination, push_url=push_url)
129       else:
130         logging.error('Git Repo (%s) does not track URL: %s',
131                       destination, fetch_url)
132         raise InvalidRepoException(fetch_url, 'Could not sync git repo: %s',
133                                    destination)
134
135   git = GitCmd()
136   if not os.path.exists(git_dir):
137     logging.info('Cloning %s...' % url)
138     clone_args = ['clone', '-n']
139     if git_cache:
140       clone_args.append('-s')
141
142     file_tools.MakeDirectoryIfAbsent(destination)
143     log_tools.CheckCall(git + clone_args + [fetch_url, '.'], cwd=destination)
144
145     if fetch_url != url:
146       GitSetRemoteRepo(fetch_url, destination, push_url=push_url)
147
148   elif clean:
149     log_tools.CheckCall(git + ['clean', '-dffx'], cwd=destination)
150     log_tools.CheckCall(git + ['reset', '--hard', 'HEAD'], cwd=destination)
151
152   if revision is not None:
153     logging.info('Checking out pinned revision...')
154     log_tools.CheckCall(git + ['fetch', '--all'], cwd=destination)
155     checkout_flags = ['-f'] if clean else []
156     path = [pathspec] if pathspec else []
157     log_tools.CheckCall(
158         git + ['checkout'] + checkout_flags + [revision] + path,
159         cwd=destination)
160
161
162 def CleanGitWorkingDir(directory, path):
163   """Clean a particular path of an existing git checkout.
164
165      Args:
166      directory: Directory where the git repo is currently checked out
167      path: path to clean, relative to the repo directory
168   """
169   log_tools.CheckCall(GitCmd() + ['clean', '-f', path], cwd=directory)
170
171
172 def PopulateGitCache(cache_dir, url_list):
173   """Fetches a git repo that combines a list of git repos.
174
175   This is an interface to the "git cache" command found within depot_tools.
176   You can populate a cache directory then obtain the local cache url using
177   GetGitCacheURL(). It is best to sync with the shared option so that the
178   cloned repository shares the same git objects.
179
180   Args:
181     cache_dir: Local directory where git cache will be populated.
182     url_list: List of URLs which cache_dir should be populated with.
183   """
184   if url_list:
185     file_tools.MakeDirectoryIfAbsent(cache_dir)
186     git = GitCmd()
187     for url in url_list:
188       log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
189                           cwd=cache_dir)
190
191
192 def GetGitCacheURL(cache_dir, url):
193   """Converts a regular git URL to a git cache URL within a cache directory.
194
195   Args:
196     url: original Git URL that is already populated within the cache directory.
197     cache_dir: Git cache directory that has already populated the URL.
198
199   Returns:
200     Git Cache URL where a git repository can clone/fetch from.
201   """
202   # Make sure we are using absolute paths or else cache exists return relative.
203   cache_dir = os.path.abspath(cache_dir)
204
205   # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
206   cygwin_path = False
207   if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
208     cygwin_path = True
209     drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
210     cache_dir = drive + ':\\' + file_path.replace('/', '\\')
211
212   git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
213                                               '-c', cache_dir,
214                                               url]).strip()
215
216   # For cygwin paths, convert forward slashes to backslashes to mimic URLs.
217   if cygwin_path:
218     git_url = git_url.replace('\\', '/')
219   return git_url
220
221
222 def GitRevInfo(directory):
223   """Get the git revision information of a git checkout.
224
225   Args:
226     directory: Existing git working directory.
227 """
228   url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
229                               cwd=directory)
230   rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
231                               cwd=directory)
232   return url.strip(), rev.strip()
233
234
235 def SvnRevInfo(directory):
236   """Get the SVN revision information of an existing svn/gclient checkout.
237
238   Args:
239      directory: Directory where the svn repo is currently checked out
240   """
241   info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
242   url = ''
243   rev = ''
244   for line in info.splitlines():
245     pieces = line.split(':', 1)
246     if len(pieces) != 2:
247       continue
248     if pieces[0] == 'URL':
249       url = pieces[1].strip()
250     elif pieces[0] == 'Revision':
251       rev = pieces[1].strip()
252   if not url or not rev:
253     raise RuntimeError('Missing svn info url: %s and rev: %s' % (url, rev))
254   return url, rev
255
256
257 def GetAuthenticatedGitURL(url):
258   """Returns the authenticated version of a git URL.
259
260   In chromium, there is a special URL that is the "authenticated" version. The
261   URLs are identical but the authenticated one has special privileges.
262   """
263   urlsplit = urlparse.urlsplit(url)
264   if urlsplit.scheme in ('https', 'http'):
265     urldict = urlsplit._asdict()
266     urldict['scheme'] = 'https'
267     urldict['path'] = '/a' + urlsplit.path
268     urlsplit = urlparse.SplitResult(**urldict)
269
270   return urlsplit.geturl()
271
272
273 def GitRemoteRepoList(directory, include_fetch=True, include_push=True):
274   """Returns a list of remote git repos associated with a directory.
275
276   Args:
277       directory: Existing git working directory.
278   Returns:
279       List of (repo_name, repo_url) for tracked remote repos.
280   """
281   remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
282                                        cwd=directory)
283
284   repo_set = set()
285   for remote_repo_line in remote_repos.splitlines():
286     repo_name, repo_url, repo_type = remote_repo_line.split()
287     if include_fetch and repo_type == '(fetch)':
288       repo_set.add((repo_name, repo_url))
289     elif include_push and repo_type == '(push)':
290       repo_set.add((repo_name, repo_url))
291
292   return sorted(repo_set)
293
294
295 def GitSetRemoteRepo(url, directory, push_url=None, repo_name='origin'):
296   """Sets the remotely tracked URL for a git repository.
297
298   Args:
299       url: Remote git URL to set.
300       directory: Local git repository to set tracked repo for.
301       push_url: If specified, uses a different URL for pushing.
302       repo_name: set the URL for a particular remote repo name.
303   """
304   git = GitCmd()
305   try:
306     log_tools.CheckCall(git + ['remote', 'set-url', repo_name, url],
307                         cwd=directory)
308   except subprocess.CalledProcessError:
309     # If setting the URL failed, repo_name may be new. Try adding the URL.
310     log_tools.CheckCall(git + ['remote', 'add', repo_name, url],
311                         cwd=directory)
312
313   if push_url:
314     log_tools.CheckCall(git + ['remote', 'set-url', '--push',
315                                repo_name, push_url],
316                         cwd=directory)
317
318
319 def IsURLInRemoteRepoList(url, directory, include_fetch=True, include_push=True,
320                           try_authenticated_url=True):
321   """Returns whether or not a url is a remote repo in a local git directory.
322
323   Args:
324       url: URL to look for in remote repo list.
325       directory: Existing git working directory.
326   """
327   if try_authenticated_url:
328     valid_urls = (url, GetAuthenticatedGitURL(url))
329   else:
330     valid_urls = (url,)
331
332   remote_repo_list = GitRemoteRepoList(directory,
333                                        include_fetch=include_fetch,
334                                        include_push=include_push)
335   return len([repo_name for
336               repo_name, repo_url in remote_repo_list
337               if repo_url in valid_urls]) > 0