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.
16 class InvalidRepoException(Exception):
17 def __init__(self, expected_repo, msg, *args):
18 Exception.__init__(self, msg % args)
19 self.expected_repo = expected_repo
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']
34 def CheckGitOutput(args):
35 """Run a git subcommand and capture its stdout a la subprocess.check_output.
37 args: list of arguments to 'git'
39 return log_tools.CheckOutput(GitCmd() + args)
43 """Return the svn command to execute for the host platform."""
44 if platform.IsWindows():
45 return ['cmd.exe', '/c', 'svn.bat']
50 def ValidateGitRepo(url, directory, clobber_mismatch=False):
51 """Validates a git repository tracks a particular URL.
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
59 directory: Directory to look for.
60 clobber_mismatch: If True, will delete invalid directories instead of raising
63 git_dir = os.path.join(directory, '.git')
64 if os.path.exists(git_dir):
66 if IsURLInRemoteRepoList(url, directory, include_fetch=True,
70 logging.warn('Local git repo (%s) does not track url (%s)',
73 logging.error('Invalid git repo: %s', directory)
75 if not clobber_mismatch:
76 raise InvalidRepoException(url, 'Invalid local git repo: %s', directory)
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',
86 logging.debug('Clobbering intended repository destination: %s', directory)
87 file_tools.RemoveDirectoryIfPresent(directory)
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.
96 destination: Directory to check out into.
97 revision: Pinned revision to check out. If None, do not check out a
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
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
110 logging.debug('Clobbering source directory %s' % destination)
111 file_tools.RemoveDirectoryIfPresent(destination)
114 fetch_url = GetGitCacheURL(git_cache, url)
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,
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,
127 include_push=False)):
128 GitSetRemoteRepo(fetch_url, destination, push_url=push_url)
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',
136 if not os.path.exists(git_dir):
137 logging.info('Cloning %s...' % url)
138 clone_args = ['clone', '-n']
140 clone_args.append('-s')
142 file_tools.MakeDirectoryIfAbsent(destination)
143 log_tools.CheckCall(git + clone_args + [fetch_url, '.'], cwd=destination)
146 GitSetRemoteRepo(fetch_url, destination, push_url=push_url)
149 log_tools.CheckCall(git + ['clean', '-dffx'], cwd=destination)
150 log_tools.CheckCall(git + ['reset', '--hard', 'HEAD'], cwd=destination)
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 []
158 git + ['checkout'] + checkout_flags + [revision] + path,
162 def CleanGitWorkingDir(directory, path):
163 """Clean a particular path of an existing git checkout.
166 directory: Directory where the git repo is currently checked out
167 path: path to clean, relative to the repo directory
169 log_tools.CheckCall(GitCmd() + ['clean', '-f', path], cwd=directory)
172 def PopulateGitCache(cache_dir, url_list):
173 """Fetches a git repo that combines a list of git repos.
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.
181 cache_dir: Local directory where git cache will be populated.
182 url_list: List of URLs which cache_dir should be populated with.
185 file_tools.MakeDirectoryIfAbsent(cache_dir)
188 log_tools.CheckCall(git + ['cache', 'populate', '-c', '.', url],
192 def GetGitCacheURL(cache_dir, url):
193 """Converts a regular git URL to a git cache URL within a cache directory.
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.
200 Git Cache URL where a git repository can clone/fetch from.
202 # Make sure we are using absolute paths or else cache exists return relative.
203 cache_dir = os.path.abspath(cache_dir)
205 # For CygWin, we must first convert the cache_dir name to a non-cygwin path.
207 if platform.IsCygWin() and cache_dir.startswith('/cygdrive/'):
209 drive, file_path = cache_dir[len('/cygdrive/'):].split('/', 1)
210 cache_dir = drive + ':\\' + file_path.replace('/', '\\')
212 git_url = log_tools.CheckOutput(GitCmd() + ['cache', 'exists',
216 # For cygwin paths, convert forward slashes to backslashes to mimic URLs.
218 git_url = git_url.replace('\\', '/')
222 def GitRevInfo(directory):
223 """Get the git revision information of a git checkout.
226 directory: Existing git working directory.
228 url = log_tools.CheckOutput(GitCmd() + ['ls-remote', '--get-url', 'origin'],
230 rev = log_tools.CheckOutput(GitCmd() + ['rev-parse', 'HEAD'],
232 return url.strip(), rev.strip()
235 def SvnRevInfo(directory):
236 """Get the SVN revision information of an existing svn/gclient checkout.
239 directory: Directory where the svn repo is currently checked out
241 info = log_tools.CheckOutput(SvnCmd() + ['info'], cwd=directory)
244 for line in info.splitlines():
245 pieces = line.split(':', 1)
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))
257 def GetAuthenticatedGitURL(url):
258 """Returns the authenticated version of a git URL.
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.
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)
270 return urlsplit.geturl()
273 def GitRemoteRepoList(directory, include_fetch=True, include_push=True):
274 """Returns a list of remote git repos associated with a directory.
277 directory: Existing git working directory.
279 List of (repo_name, repo_url) for tracked remote repos.
281 remote_repos = log_tools.CheckOutput(GitCmd() + ['remote', '-v'],
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))
292 return sorted(repo_set)
295 def GitSetRemoteRepo(url, directory, push_url=None, repo_name='origin'):
296 """Sets the remotely tracked URL for a git repository.
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.
306 log_tools.CheckCall(git + ['remote', 'set-url', repo_name, url],
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],
314 log_tools.CheckCall(git + ['remote', 'set-url', '--push',
315 repo_name, push_url],
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.
324 url: URL to look for in remote repo list.
325 directory: Existing git working directory.
327 if try_authenticated_url:
328 valid_urls = (url, GetAuthenticatedGitURL(url))
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