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.
17 GIT_ALTERNATES_PATH = os.path.join('.git', 'objects', 'info', 'alternates')
20 class InvalidRepoException(Exception):
21 def __init__(self, expected_repo, msg, *args):
22 Exception.__init__(self, msg % args)
23 self.expected_repo = expected_repo
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']
38 def CheckGitOutput(args):
39 """Run a git subcommand and capture its stdout a la subprocess.check_output.
41 args: list of arguments to 'git'
43 return log_tools.CheckOutput(GitCmd() + args)
47 """Return the svn command to execute for the host platform."""
48 if platform.IsWindows():
49 return ['cmd.exe', '/c', 'svn.bat']
54 def ValidateGitRepo(url, directory, clobber_mismatch=False, logger=None):
55 """Validates a git repository tracks a particular URL.
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
63 directory: Directory to look for.
64 clobber_mismatch: If True, will delete invalid directories instead of raising
68 logger = log_tools.GetConsoleLogger()
69 git_dir = os.path.join(directory, '.git')
70 if os.path.exists(git_dir):
72 if IsURLInRemoteRepoList(url, directory, include_fetch=True,
76 logger.warn('Local git repo (%s) does not track url (%s)',
79 logger.error('Invalid git repo: %s', directory)
81 if not clobber_mismatch:
82 raise InvalidRepoException(url, 'Invalid local git repo: %s', directory)
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',
92 logger.debug('Clobbering intended repository destination: %s', directory)
93 file_tools.RemoveDirectoryIfPresent(directory)
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.
102 destination: Directory to check out into.
103 revision: Pinned revision to check out. If None, do not check out a
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
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
114 logger = log_tools.GetConsoleLogger()
116 logger.debug('Clobbering source directory %s' % destination)
117 file_tools.RemoveDirectoryIfPresent(destination)
120 git_cache_url = GetGitCacheURL(git_cache, url)
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,
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,
134 GitSetRemoteRepo(url, destination, push_url=push_url,
137 logger.error('Git Repo (%s) does not track URL: %s',
139 raise InvalidRepoException(url, 'Could not sync git repo: %s',
142 # Make sure the push URL is set correctly as well.
143 if not IsURLInRemoteRepoList(push_url, destination, include_fetch=False,
145 GitSetRemoteRepo(url, destination, push_url=push_url)
148 if not os.path.exists(git_dir):
149 logger.info('Cloning %s...' % url)
151 file_tools.MakeDirectoryIfAbsent(destination)
152 clone_args = ['clone', '-n']
154 clone_args.extend(['--reference', git_cache_url])
156 log_tools.CheckCall(git + clone_args + [url, '.'],
157 logger=logger, cwd=destination)
160 GitSetRemoteRepo(url, destination, push_url=push_url, logger=logger)
162 # If a git cache URL is supplied, make sure it is setup as a git alternate.
164 git_alternates = [git_cache_url]
168 GitSetRepoAlternates(destination, git_alternates, append=False, logger=logger)
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 []
176 git + ['checkout', revision] + path,
177 logger=logger, cwd=destination)
180 def CleanGitWorkingDir(directory, reset=False, path=None, logger=None):
181 """Clean all or part of an existing git checkout.
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
189 repo_path = [path] if path else []
190 log_tools.CheckCall(GitCmd() + ['clean', '-dffx'] + repo_path,
191 logger=logger, cwd=directory)
193 log_tools.CheckCall(GitCmd() + ['reset', '--hard', 'HEAD'],
194 logger=logger, cwd=directory)
197 def PopulateGitCache(cache_dir, url_list, logger=None):
198 """Fetches a git repo that combines a list of git repos.
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.
206 cache_dir: Local directory where git cache will be populated.
207 url_list: List of URLs which cache_dir should be populated with.
210 file_tools.MakeDirectoryIfAbsent(cache_dir)
213 log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
214 logger=logger, cwd=cache_dir)
217 def GetGitCacheURL(cache_dir, url, logger=None):
218 """Converts a regular git URL to a git cache URL within a cache directory.
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.
225 Git Cache URL where a git repository can clone/fetch from.
227 # Make sure we are using absolute paths or else cache exists return relative.
228 cache_dir = os.path.abspath(cache_dir)
230 # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
232 if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
234 drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
235 cache_dir = drive + ':\\' + file_path.replace('/', '\\')
237 git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
240 logger=logger).strip()
242 # For windows, make sure the git cache URL is a posix path.
243 if platform.IsWindows():
244 git_url = git_url.replace('\\', '/')
248 def GitRevInfo(directory):
249 """Get the git revision information of a git checkout.
252 directory: Existing git working directory.
254 url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
256 rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
258 return url.strip(), rev.strip()
261 def SvnRevInfo(directory):
262 """Get the SVN revision information of an existing svn/gclient checkout.
265 directory: Directory where the svn repo is currently checked out
267 info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
270 for line in info.splitlines():
271 pieces = line.split(':', 1)
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))
283 def GetAuthenticatedGitURL(url):
284 """Returns the authenticated version of a git URL.
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.
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)
296 return urlsplit.geturl()
299 def GitRemoteRepoList(directory, include_fetch=True, include_push=True,
301 """Returns a list of remote git repos associated with a directory.
304 directory: Existing git working directory.
306 List of (repo_name, repo_url) for tracked remote repos.
308 remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
309 logger=logger, cwd=directory)
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))
319 return sorted(repo_set)
322 def GitSetRemoteRepo(url, directory, push_url=None,
323 repo_name='origin', logger=None):
324 """Sets the remotely tracked URL for a git repository.
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.
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)
342 log_tools.CheckCall(git + ['remote', 'set-url', '--push',
343 repo_name, push_url],
344 logger=logger, cwd=directory)
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.
352 url: URL to look for in remote repo list.
353 directory: Existing git working directory.
355 if try_authenticated_url:
356 valid_urls = (url, GetAuthenticatedGitURL(url))
360 remote_repo_list = GitRemoteRepoList(directory,
361 include_fetch=include_fetch,
362 include_push=include_push,
364 return len([repo_name for
365 repo_name, repo_url in remote_repo_list
366 if repo_url in valid_urls]) > 0
369 def GitGetRepoAlternates(directory):
370 """Gets the list of git alternates for a local git repo.
373 directory: Local git repository to get the git alternate for.
376 List of git alternates set for the local git repository.
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:
382 for line in f.readlines():
385 if posixpath.basename(line) == 'objects':
386 line = posixpath.dirname(line)
387 alternates_list.append(line)
389 return alternates_list
394 def GitSetRepoAlternates(directory, alternates_list, append=True, logger=None):
395 """Sets the list of git alternates for a local git repo.
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.
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)
410 original_alternates_list = GitGetRepoAlternates(directory)
412 alternates_list.extend(original_alternates_list)
413 alternates_list = sorted(set(alternates_list))
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))
419 with open(git_alternates_file, 'wb') as f: