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, clean=False,
97 pathspec=None, 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 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
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
116 logger = log_tools.GetConsoleLogger()
118 logger.debug('Clobbering source directory %s' % destination)
119 file_tools.RemoveDirectoryIfPresent(destination)
122 git_cache_url = GetGitCacheURL(git_cache, url)
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,
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,
136 GitSetRemoteRepo(url, destination, push_url=push_url,
139 logger.error('Git Repo (%s) does not track URL: %s',
141 raise InvalidRepoException(url, 'Could not sync git repo: %s',
144 # Make sure the push URL is set correctly as well.
145 if not IsURLInRemoteRepoList(push_url, destination, include_fetch=False,
147 GitSetRemoteRepo(url, destination, push_url=push_url)
150 if not os.path.exists(git_dir):
151 logger.info('Cloning %s...' % url)
153 file_tools.MakeDirectoryIfAbsent(destination)
154 clone_args = ['clone', '-n']
156 clone_args.extend(['--reference', git_cache_url])
158 log_tools.CheckCall(git + clone_args + [url, '.'],
159 logger=logger, cwd=destination)
162 GitSetRemoteRepo(url, destination, push_url=push_url, logger=logger)
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)
169 # If a git cache URL is supplied, make sure it is setup as a git alternate.
171 git_alternates = [git_cache_url]
175 GitSetRepoAlternates(destination, git_alternates, append=False, logger=logger)
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 []
184 git + ['checkout'] + checkout_flags + [revision] + path,
185 logger=logger, cwd=destination)
188 def CleanGitWorkingDir(directory, path, logger=None):
189 """Clean a particular path of an existing git checkout.
192 directory: Directory where the git repo is currently checked out
193 path: path to clean, relative to the repo directory
195 log_tools.CheckCall(GitCmd() + ['clean', '-f', path],
196 logger=logger, cwd=directory)
199 def PopulateGitCache(cache_dir, url_list, logger=None):
200 """Fetches a git repo that combines a list of git repos.
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.
208 cache_dir: Local directory where git cache will be populated.
209 url_list: List of URLs which cache_dir should be populated with.
212 file_tools.MakeDirectoryIfAbsent(cache_dir)
215 log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
216 logger=logger, cwd=cache_dir)
219 def GetGitCacheURL(cache_dir, url, logger=None):
220 """Converts a regular git URL to a git cache URL within a cache directory.
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.
227 Git Cache URL where a git repository can clone/fetch from.
229 # Make sure we are using absolute paths or else cache exists return relative.
230 cache_dir = os.path.abspath(cache_dir)
232 # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
234 if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
236 drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
237 cache_dir = drive + ':\\' + file_path.replace('/', '\\')
239 git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
242 logger=logger).strip()
244 # For windows, make sure the git cache URL is a posix path.
245 if platform.IsWindows():
246 git_url = git_url.replace('\\', '/')
250 def GitRevInfo(directory):
251 """Get the git revision information of a git checkout.
254 directory: Existing git working directory.
256 url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
258 rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
260 return url.strip(), rev.strip()
263 def SvnRevInfo(directory):
264 """Get the SVN revision information of an existing svn/gclient checkout.
267 directory: Directory where the svn repo is currently checked out
269 info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
272 for line in info.splitlines():
273 pieces = line.split(':', 1)
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))
285 def GetAuthenticatedGitURL(url):
286 """Returns the authenticated version of a git URL.
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.
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)
298 return urlsplit.geturl()
301 def GitRemoteRepoList(directory, include_fetch=True, include_push=True,
303 """Returns a list of remote git repos associated with a directory.
306 directory: Existing git working directory.
308 List of (repo_name, repo_url) for tracked remote repos.
310 remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
311 logger=logger, cwd=directory)
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))
321 return sorted(repo_set)
324 def GitSetRemoteRepo(url, directory, push_url=None,
325 repo_name='origin', logger=None):
326 """Sets the remotely tracked URL for a git repository.
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.
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)
344 log_tools.CheckCall(git + ['remote', 'set-url', '--push',
345 repo_name, push_url],
346 logger=logger, cwd=directory)
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.
354 url: URL to look for in remote repo list.
355 directory: Existing git working directory.
357 if try_authenticated_url:
358 valid_urls = (url, GetAuthenticatedGitURL(url))
362 remote_repo_list = GitRemoteRepoList(directory,
363 include_fetch=include_fetch,
364 include_push=include_push,
366 return len([repo_name for
367 repo_name, repo_url in remote_repo_list
368 if repo_url in valid_urls]) > 0
371 def GitGetRepoAlternates(directory):
372 """Gets the list of git alternates for a local git repo.
375 directory: Local git repository to get the git alternate for.
378 List of git alternates set for the local git repository.
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:
384 for line in f.readlines():
387 if posixpath.basename(line) == 'objects':
388 line = posixpath.dirname(line)
389 alternates_list.append(line)
391 return alternates_list
396 def GitSetRepoAlternates(directory, alternates_list, append=True, logger=None):
397 """Sets the list of git alternates for a local git repo.
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.
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)
412 original_alternates_list = GitGetRepoAlternates(directory)
414 alternates_list.extend(original_alternates_list)
415 alternates_list = sorted(set(alternates_list))
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))
421 with open(git_alternates_file, 'wb') as f: