Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(67)

Side by Side Diff: gclient.py

Issue 2917009: Reapply gclient.py refactor for the third time. (Closed)
Patch Set: Fix runhooks with hooks defined in From(File()) Created 10 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | gclient_utils.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Meta checkout manager supporting both Subversion and GIT. 6 """Meta checkout manager supporting both Subversion and GIT.
7 7
8 Files 8 Files
9 .gclient : Current client configuration, written by 'config' command. 9 .gclient : Current client configuration, written by 'config' command.
10 Format is a Python script defining 'solutions', a list whose 10 Format is a Python script defining 'solutions', a list whose
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
42 it will be removed from the list and the list will be extended 42 it will be removed from the list and the list will be extended
43 by the list of matching files. 43 by the list of matching files.
44 44
45 Example: 45 Example:
46 hooks = [ 46 hooks = [
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", 47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
48 "action": ["python", "image_indexer.py", "--all"]}, 48 "action": ["python", "image_indexer.py", "--all"]},
49 ] 49 ]
50 """ 50 """
51 51
52 __version__ = "0.4.1" 52 __version__ = "0.5"
53 53
54 import errno
55 import logging 54 import logging
56 import optparse 55 import optparse
57 import os 56 import os
58 import pprint 57 import pprint
59 import re 58 import re
60 import subprocess 59 import subprocess
61 import sys 60 import sys
62 import urlparse 61 import urlparse
63 import urllib 62 import urllib
64 63
(...skipping 25 matching lines...) Expand all
90 89
91 sub_target_name is an optional parameter if the module name in the other 90 sub_target_name is an optional parameter if the module name in the other
92 DEPS file is different. E.g., you might want to map src/net to net.""" 91 DEPS file is different. E.g., you might want to map src/net to net."""
93 self.module_name = module_name 92 self.module_name = module_name
94 self.sub_target_name = sub_target_name 93 self.sub_target_name = sub_target_name
95 94
96 def __str__(self): 95 def __str__(self):
97 return 'From(%s, %s)' % (repr(self.module_name), 96 return 'From(%s, %s)' % (repr(self.module_name),
98 repr(self.sub_target_name)) 97 repr(self.sub_target_name))
99 98
100 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
101 """Resolve the URL for this From entry."""
102 sub_deps_target_name = target_name
103 if self.sub_target_name:
104 sub_deps_target_name = self.sub_target_name
105 url = sub_deps[sub_deps_target_name]
106 if url.startswith('/'):
107 # If it's a relative URL, we need to resolve the URL relative to the
108 # sub deps base URL.
109 if not isinstance(sub_deps_base_url, basestring):
110 sub_deps_base_url = sub_deps_base_url.GetPath()
111 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
112 None)
113 url = scm.FullUrlForRelativeUrl(url)
114 return url
115
116 class FileImpl(object): 99 class FileImpl(object):
117 """Used to implement the File('') syntax which lets you sync a single file 100 """Used to implement the File('') syntax which lets you sync a single file
118 from a SVN repo.""" 101 from a SVN repo."""
119 102
120 def __init__(self, file_location): 103 def __init__(self, file_location):
121 self.file_location = file_location 104 self.file_location = file_location
122 105
123 def __str__(self): 106 def __str__(self):
124 return 'File("%s")' % self.file_location 107 return 'File("%s")' % self.file_location
125 108
(...skipping 27 matching lines...) Expand all
153 class Dependency(GClientKeywords): 136 class Dependency(GClientKeywords):
154 """Object that represents a dependency checkout.""" 137 """Object that represents a dependency checkout."""
155 DEPS_FILE = 'DEPS' 138 DEPS_FILE = 'DEPS'
156 139
157 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None, 140 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
158 custom_vars=None, deps_file=None): 141 custom_vars=None, deps_file=None):
159 GClientKeywords.__init__(self) 142 GClientKeywords.__init__(self)
160 self.parent = parent 143 self.parent = parent
161 self.name = name 144 self.name = name
162 self.url = url 145 self.url = url
146 self.parsed_url = None
163 # These 2 are only set in .gclient and not in DEPS files. 147 # These 2 are only set in .gclient and not in DEPS files.
164 self.safesync_url = safesync_url 148 self.safesync_url = safesync_url
165 self.custom_vars = custom_vars or {} 149 self.custom_vars = custom_vars or {}
166 self.custom_deps = custom_deps or {} 150 self.custom_deps = custom_deps or {}
167 self.deps_hooks = [] 151 self.deps_hooks = []
168 self.dependencies = [] 152 self.dependencies = []
169 self.deps_file = deps_file or self.DEPS_FILE 153 self.deps_file = deps_file or self.DEPS_FILE
154 # A cache of the files affected by the current operation, necessary for
155 # hooks.
156 self.file_list = []
170 self.deps_parsed = False 157 self.deps_parsed = False
171 self.direct_reference = False 158 self.direct_reference = False
172 159
173 # Sanity checks 160 # Sanity checks
174 if not self.name and self.parent: 161 if not self.name and self.parent:
175 raise gclient_utils.Error('Dependency without name') 162 raise gclient_utils.Error('Dependency without name')
163 if self.name in [d.name for d in self.tree(False)]:
164 raise gclient_utils.Error('Dependency %s specified more than once' %
165 self.name)
176 if not isinstance(self.url, 166 if not isinstance(self.url,
177 (basestring, self.FromImpl, self.FileImpl, None.__class__)): 167 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
178 raise gclient_utils.Error('dependency url must be either a string, None, ' 168 raise gclient_utils.Error('dependency url must be either a string, None, '
179 'File() or From() instead of %s' % 169 'File() or From() instead of %s' %
180 self.url.__class__.__name__) 170 self.url.__class__.__name__)
181 if '/' in self.deps_file or '\\' in self.deps_file: 171 if '/' in self.deps_file or '\\' in self.deps_file:
182 raise gclient_utils.Error('deps_file name must not be a path, just a ' 172 raise gclient_utils.Error('deps_file name must not be a path, just a '
183 'filename. %s' % self.deps_file) 173 'filename. %s' % self.deps_file)
184 174
175 def LateOverride(self, url):
176 overriden_url = self.get_custom_deps(self.name, url)
177 if overriden_url != url:
178 self.parsed_url = overriden_url
179 logging.debug('%s, %s was overriden to %s' % (self.name, url,
180 self.parsed_url))
181 elif isinstance(url, self.FromImpl):
182 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
183 if not len(ref) == 1:
184 raise Exception('Failed to find one reference to %s. %s' % (
185 url.module_name, ref))
186 ref = ref[0]
187 sub_target = url.sub_target_name or url
188 # Make sure the referenced dependency DEPS file is loaded and file the
189 # inner referenced dependency.
190 ref.ParseDepsFile(False)
191 found_dep = None
192 for d in ref.dependencies:
193 if d.name == sub_target:
194 found_dep = d
195 break
196 if not found_dep:
197 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
198 sub_target, ref.name, self.name))
199 # Call LateOverride() again.
200 self.parsed_url = found_dep.LateOverride(found_dep.url)
201 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
202 elif isinstance(url, basestring):
203 parsed_url = urlparse.urlparse(url)
204 if not parsed_url[0]:
205 # A relative url. Fetch the real base.
206 path = parsed_url[2]
207 if not path.startswith('/'):
208 raise gclient_utils.Error(
209 'relative DEPS entry \'%s\' must begin with a slash' % url)
210 # Create a scm just to query the full url.
211 parent_url = self.parent.parsed_url
212 if isinstance(parent_url, self.FileImpl):
213 parent_url = parent_url.file_location
214 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
215 self.parsed_url = scm.FullUrlForRelativeUrl(url)
216 else:
217 self.parsed_url = url
218 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
219 elif isinstance(url, self.FileImpl):
220 self.parsed_url = url
221 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
222 return self.parsed_url
223
185 def ParseDepsFile(self, direct_reference): 224 def ParseDepsFile(self, direct_reference):
186 """Parses the DEPS file for this dependency.""" 225 """Parses the DEPS file for this dependency."""
187 if direct_reference: 226 if direct_reference:
188 # Maybe it was referenced earlier by a From() keyword but it's now 227 # Maybe it was referenced earlier by a From() keyword but it's now
189 # directly referenced. 228 # directly referenced.
190 self.direct_reference = direct_reference 229 self.direct_reference = direct_reference
230 if self.deps_parsed:
231 return
191 self.deps_parsed = True 232 self.deps_parsed = True
192 filepath = os.path.join(self.root_dir(), self.name, self.deps_file) 233 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
193 if not os.path.isfile(filepath): 234 if not os.path.isfile(filepath):
194 return {} 235 return
195 deps_content = gclient_utils.FileRead(filepath) 236 deps_content = gclient_utils.FileRead(filepath)
196 237
197 # Eval the content. 238 # Eval the content.
198 # One thing is unintuitive, vars= {} must happen before Var() use. 239 # One thing is unintuitive, vars= {} must happen before Var() use.
199 local_scope = {} 240 local_scope = {}
200 var = self.VarImpl(self.custom_vars, local_scope) 241 var = self.VarImpl(self.custom_vars, local_scope)
201 global_scope = { 242 global_scope = {
202 'File': self.FileImpl, 243 'File': self.FileImpl,
203 'From': self.FromImpl, 244 'From': self.FromImpl,
204 'Var': var.Lookup, 245 'Var': var.Lookup,
(...skipping 30 matching lines...) Expand all
235 # the dictionary using paths relative to the directory containing 276 # the dictionary using paths relative to the directory containing
236 # the DEPS file. 277 # the DEPS file.
237 use_relative_paths = local_scope.get('use_relative_paths', False) 278 use_relative_paths = local_scope.get('use_relative_paths', False)
238 if use_relative_paths: 279 if use_relative_paths:
239 rel_deps = {} 280 rel_deps = {}
240 for d, url in deps.items(): 281 for d, url in deps.items():
241 # normpath is required to allow DEPS to use .. in their 282 # normpath is required to allow DEPS to use .. in their
242 # dependency local path. 283 # dependency local path.
243 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url 284 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
244 deps = rel_deps 285 deps = rel_deps
245 # TODO(maruel): Add these dependencies into self.dependencies.
246 return deps
247 286
248 def _ParseAllDeps(self, solution_urls): 287 # Convert the deps into real Dependency.
249 """Parse the complete list of dependencies for the client. 288 for name, url in deps.iteritems():
289 if name in [s.name for s in self.dependencies]:
290 raise
291 self.dependencies.append(Dependency(self, name, url))
292 # Sort by name.
293 self.dependencies.sort(key=lambda x: x.name)
294 logging.info('Loaded: %s' % str(self))
250 295
251 Args: 296 def RunCommandRecursively(self, options, revision_overrides,
252 solution_urls: A dict mapping module names (as relative paths) to URLs 297 command, args, pm):
253 corresponding to the solutions specified by the client. This parameter 298 """Runs 'command' before parsing the DEPS in case it's a initial checkout
254 is passed as an optimization. 299 or a revert."""
300 assert self.file_list == []
301 # When running runhooks, there's no need to consult the SCM.
302 # All known hooks are expected to run unconditionally regardless of working
303 # copy state, so skip the SCM status check.
304 run_scm = command not in ('runhooks', None)
305 self.LateOverride(self.url)
306 if run_scm and self.parsed_url:
307 if isinstance(self.parsed_url, self.FileImpl):
308 # Special support for single-file checkout.
309 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
310 options.revision = self.parsed_url.GetRevision()
311 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
312 self.root_dir(),
313 self.name)
314 scm.RunCommand('updatesingle', options,
315 args + [self.parsed_url.GetFilename()],
316 self.file_list)
317 else:
318 options.revision = revision_overrides.get(self.name)
319 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
320 scm.RunCommand(command, options, args, self.file_list)
321 self.file_list = [os.path.join(self.name, f.strip())
322 for f in self.file_list]
323 options.revision = None
324 if pm:
325 # The + 1 comes from the fact that .gclient is considered a step in
326 # itself, .i.e. this code is called one time for the .gclient. This is not
327 # conceptually correct but it simplifies code.
328 pm._total = len(self.tree(False)) + 1
329 pm.update()
330 if self.recursion_limit():
331 # Then we can parse the DEPS file.
332 self.ParseDepsFile(True)
333 if pm:
334 pm._total = len(self.tree(False)) + 1
335 pm.update(0)
336 # Parse the dependencies of this dependency.
337 for s in self.dependencies:
338 # TODO(maruel): All these can run concurrently! No need for threads,
339 # just buffer stdout&stderr on pipes and flush as they complete.
340 # Watch out for stdin.
341 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
255 342
256 Returns: 343 def RunHooksRecursively(self, options):
257 A dict mapping module names (as relative paths) to URLs corresponding 344 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
258 to the entire set of dependencies to checkout for the given client. 345 must have been called before to load the DEPS."""
346 # If "--force" was specified, run all hooks regardless of what files have
347 # changed.
348 if self.deps_hooks and self.direct_reference:
349 # TODO(maruel): If the user is using git or git-svn, then we don't know
350 # what files have changed so we always run all hooks. It'd be nice to fix
351 # that.
352 if (options.force or
353 isinstance(self.parsed_url, self.FileImpl) or
354 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
355 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
356 for hook_dict in self.deps_hooks:
357 self._RunHookAction(hook_dict, [])
358 else:
359 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
360 # Convert all absolute paths to relative.
361 for i in range(len(self.file_list)):
362 # It depends on the command being executed (like runhooks vs sync).
363 if not os.path.isabs(self.file_list[i]):
364 continue
259 365
260 Raises: 366 prefix = os.path.commonprefix([self.root_dir().lower(),
261 Error: If a dependency conflicts with another dependency or of a solution. 367 self.file_list[i].lower()])
262 """ 368 self.file_list[i] = self.file_list[i][len(prefix):]
263 deps = {}
264 for solution in self.dependencies:
265 solution_deps = solution.ParseDepsFile(True)
266 369
267 # If a line is in custom_deps, but not in the solution, we want to append 370 # Strip any leading path separators.
268 # this line to the solution. 371 while (self.file_list[i].startswith('\\') or
269 for d in solution.custom_deps: 372 self.file_list[i].startswith('/')):
270 if d not in solution_deps: 373 self.file_list[i] = self.file_list[i][1:]
271 solution_deps[d] = solution.custom_deps[d]
272 374
273 for d in solution_deps: 375 # Run hooks on the basis of whether the files from the gclient operation
274 if d in solution.custom_deps: 376 # match each hook's pattern.
275 # Dependency is overriden. 377 for hook_dict in self.deps_hooks:
276 url = solution.custom_deps[d] 378 pattern = re.compile(hook_dict['pattern'])
277 if url is None: 379 matching_file_list = [f for f in self.file_list if pattern.search(f)]
278 continue 380 if matching_file_list:
279 else: 381 self._RunHookAction(hook_dict, matching_file_list)
280 url = solution_deps[d] 382 if self.recursion_limit():
281 # if we have a From reference dependent on another solution, then 383 for s in self.dependencies:
282 # just skip the From reference. When we pull deps for the solution, 384 s.RunHooksRecursively(options)
283 # we will take care of this dependency.
284 #
285 # If multiple solutions all have the same From reference, then we
286 # should only add one to our list of dependencies.
287 if isinstance(url, self.FromImpl):
288 if url.module_name in solution_urls:
289 # Already parsed.
290 continue
291 if d in deps and type(deps[d]) != str:
292 if url.module_name == deps[d].module_name:
293 continue
294 elif isinstance(url, str):
295 parsed_url = urlparse.urlparse(url)
296 scheme = parsed_url[0]
297 if not scheme:
298 # A relative url. Fetch the real base.
299 path = parsed_url[2]
300 if path[0] != "/":
301 raise gclient_utils.Error(
302 "relative DEPS entry \"%s\" must begin with a slash" % d)
303 # Create a scm just to query the full url.
304 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(),
305 None)
306 url = scm.FullUrlForRelativeUrl(url)
307 if d in deps and deps[d] != url:
308 raise gclient_utils.Error(
309 "Solutions have conflicting versions of dependency \"%s\"" % d)
310 if d in solution_urls and solution_urls[d] != url:
311 raise gclient_utils.Error(
312 "Dependency \"%s\" conflicts with specified solution" % d)
313 # Grab the dependency.
314 deps[d] = url
315 return deps
316
317 def _RunHooks(self, command, file_list, is_using_git):
318 """Evaluates all hooks, running actions as needed.
319 """
320 # Hooks only run for these command types.
321 if not command in ('update', 'revert', 'runhooks'):
322 return
323
324 # Hooks only run when --nohooks is not specified
325 if self._options.nohooks:
326 return
327
328 # Get any hooks from the .gclient file.
329 hooks = self.deps_hooks[:]
330 # Add any hooks found in DEPS files.
331 for d in self.dependencies:
332 hooks.extend(d.deps_hooks)
333
334 # If "--force" was specified, run all hooks regardless of what files have
335 # changed. If the user is using git, then we don't know what files have
336 # changed so we always run all hooks.
337 if self._options.force or is_using_git:
338 for hook_dict in hooks:
339 self._RunHookAction(hook_dict, [])
340 return
341
342 # Run hooks on the basis of whether the files from the gclient operation
343 # match each hook's pattern.
344 for hook_dict in hooks:
345 pattern = re.compile(hook_dict['pattern'])
346 matching_file_list = [f for f in file_list if pattern.search(f)]
347 if matching_file_list:
348 self._RunHookAction(hook_dict, matching_file_list)
349 385
350 def _RunHookAction(self, hook_dict, matching_file_list): 386 def _RunHookAction(self, hook_dict, matching_file_list):
351 """Runs the action from a single hook.""" 387 """Runs the action from a single hook."""
352 logging.info(hook_dict) 388 logging.info(hook_dict)
353 logging.info(matching_file_list) 389 logging.info(matching_file_list)
354 command = hook_dict['action'][:] 390 command = hook_dict['action'][:]
355 if command[0] == 'python': 391 if command[0] == 'python':
356 # If the hook specified "python" as the first item, the action is a 392 # If the hook specified "python" as the first item, the action is a
357 # Python script. Run it by starting a new copy of the same 393 # Python script. Run it by starting a new copy of the same
358 # interpreter. 394 # interpreter.
(...skipping 23 matching lines...) Expand all
382 def get_custom_deps(self, name, url): 418 def get_custom_deps(self, name, url):
383 """Returns a custom deps if applicable.""" 419 """Returns a custom deps if applicable."""
384 if self.parent: 420 if self.parent:
385 url = self.parent.get_custom_deps(name, url) 421 url = self.parent.get_custom_deps(name, url)
386 # None is a valid return value to disable a dependency. 422 # None is a valid return value to disable a dependency.
387 return self.custom_deps.get(name, url) 423 return self.custom_deps.get(name, url)
388 424
389 def __str__(self): 425 def __str__(self):
390 out = [] 426 out = []
391 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars', 427 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
392 'deps_hooks'): 428 'deps_hooks', 'file_list'):
393 # 'deps_file' 429 # 'deps_file'
394 if self.__dict__[i]: 430 if self.__dict__[i]:
395 out.append('%s: %s' % (i, self.__dict__[i])) 431 out.append('%s: %s' % (i, self.__dict__[i]))
396 432
397 for d in self.dependencies: 433 for d in self.dependencies:
398 out.extend([' ' + x for x in str(d).splitlines()]) 434 out.extend([' ' + x for x in str(d).splitlines()])
399 out.append('') 435 out.append('')
400 return '\n'.join(out) 436 return '\n'.join(out)
401 437
402 def __repr__(self): 438 def __repr__(self):
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after
498 os.path.join(path, options.config_filename))) 534 os.path.join(path, options.config_filename)))
499 return client 535 return client
500 536
501 def SetDefaultConfig(self, solution_name, solution_url, safesync_url): 537 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
502 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % { 538 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
503 'solution_name': solution_name, 539 'solution_name': solution_name,
504 'solution_url': solution_url, 540 'solution_url': solution_url,
505 'safesync_url' : safesync_url, 541 'safesync_url' : safesync_url,
506 }) 542 })
507 543
508 def _SaveEntries(self, entries): 544 def _SaveEntries(self):
509 """Creates a .gclient_entries file to record the list of unique checkouts. 545 """Creates a .gclient_entries file to record the list of unique checkouts.
510 546
511 The .gclient_entries file lives in the same directory as .gclient. 547 The .gclient_entries file lives in the same directory as .gclient.
512 """ 548 """
513 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It 549 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
514 # makes testing a bit too fun. 550 # makes testing a bit too fun.
515 result = pprint.pformat(entries, 2) 551 result = 'entries = {\n'
516 if result.startswith('{\''): 552 for entry in self.tree(False):
517 result = '{ \'' + result[2:] 553 # Skip over File() dependencies as we can't version them.
518 text = 'entries = \\\n' + result + '\n' 554 if not isinstance(entry.parsed_url, self.FileImpl):
555 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
556 pprint.pformat(entry.parsed_url))
557 result += '}\n'
519 file_path = os.path.join(self.root_dir(), self._options.entries_filename) 558 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
520 gclient_utils.FileWrite(file_path, text) 559 logging.info(result)
560 gclient_utils.FileWrite(file_path, result)
521 561
522 def _ReadEntries(self): 562 def _ReadEntries(self):
523 """Read the .gclient_entries file for the given client. 563 """Read the .gclient_entries file for the given client.
524 564
525 Returns: 565 Returns:
526 A sequence of solution names, which will be empty if there is the 566 A sequence of solution names, which will be empty if there is the
527 entries file hasn't been created yet. 567 entries file hasn't been created yet.
528 """ 568 """
529 scope = {} 569 scope = {}
530 filename = os.path.join(self.root_dir(), self._options.entries_filename) 570 filename = os.path.join(self.root_dir(), self._options.entries_filename)
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
571 def RunOnDeps(self, command, args): 611 def RunOnDeps(self, command, args):
572 """Runs a command on each dependency in a client and its dependencies. 612 """Runs a command on each dependency in a client and its dependencies.
573 613
574 Args: 614 Args:
575 command: The command to use (e.g., 'status' or 'diff') 615 command: The command to use (e.g., 'status' or 'diff')
576 args: list of str - extra arguments to add to the command line. 616 args: list of str - extra arguments to add to the command line.
577 """ 617 """
578 if not self.dependencies: 618 if not self.dependencies:
579 raise gclient_utils.Error('No solution specified') 619 raise gclient_utils.Error('No solution specified')
580 revision_overrides = self._EnforceRevisions() 620 revision_overrides = self._EnforceRevisions()
581 621 pm = None
582 # When running runhooks --force, there's no need to consult the SCM.
583 # All known hooks are expected to run unconditionally regardless of working
584 # copy state, so skip the SCM status check.
585 run_scm = not (command == 'runhooks' and self._options.force)
586
587 entries = {}
588 file_list = []
589 # Run on the base solutions first.
590 for solution in self.dependencies:
591 name = solution.name
592 if name in entries:
593 raise gclient_utils.Error("solution %s specified more than once" % name)
594 url = solution.url
595 entries[name] = url
596 if run_scm and url:
597 self._options.revision = revision_overrides.get(name)
598 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
599 scm.RunCommand(command, self._options, args, file_list)
600 file_list = [os.path.join(name, f.strip()) for f in file_list]
601 self._options.revision = None
602
603 # Process the dependencies next (sort alphanumerically to ensure that
604 # containing directories get populated first and for readability)
605 deps = self._ParseAllDeps(entries)
606 deps_to_process = deps.keys()
607 deps_to_process.sort()
608
609 # First pass for direct dependencies.
610 if command == 'update' and not self._options.verbose: 622 if command == 'update' and not self._options.verbose:
611 pm = Progress('Syncing projects', len(deps_to_process)) 623 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
612 for d in deps_to_process: 624 self.RunCommandRecursively(self._options, revision_overrides,
613 if command == 'update' and not self._options.verbose: 625 command, args, pm)
614 pm.update() 626 if pm:
615 if type(deps[d]) == str:
616 url = deps[d]
617 entries[d] = url
618 if run_scm:
619 self._options.revision = revision_overrides.get(d)
620 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
621 scm.RunCommand(command, self._options, args, file_list)
622 self._options.revision = None
623 elif isinstance(deps[d], self.FileImpl):
624 if command in (None, 'cleanup', 'diff', 'pack', 'status'):
625 continue
626 file_dep = deps[d]
627 self._options.revision = file_dep.GetRevision()
628 if run_scm:
629 scm = gclient_scm.SVNWrapper(file_dep.GetPath(), self.root_dir(), d)
630 scm.RunCommand('updatesingle', self._options,
631 args + [file_dep.GetFilename()], file_list)
632
633 if command == 'update' and not self._options.verbose:
634 pm.end() 627 pm.end()
635 628
636 # Second pass for inherited deps (via the From keyword) 629 # Once all the dependencies have been processed, it's now safe to run the
637 for d in deps_to_process: 630 # hooks.
638 if isinstance(deps[d], self.FromImpl): 631 if not self._options.nohooks:
639 # Getting the URL from the sub_deps file can involve having to resolve 632 self.RunHooksRecursively(self._options)
640 # a File() or having to resolve a relative URL. To resolve relative
641 # URLs, we need to pass in the orignal sub deps URL.
642 sub_deps_base_url = deps[deps[d].module_name]
643 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
644 ).ParseDepsFile(False)
645 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
646 entries[d] = url
647 if run_scm:
648 self._options.revision = revision_overrides.get(d)
649 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
650 scm.RunCommand(command, self._options, args, file_list)
651 self._options.revision = None
652
653 # Convert all absolute paths to relative.
654 for i in range(len(file_list)):
655 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
656 # It depends on the command being executed (like runhooks vs sync).
657 if not os.path.isabs(file_list[i]):
658 continue
659
660 prefix = os.path.commonprefix([self.root_dir().lower(),
661 file_list[i].lower()])
662 file_list[i] = file_list[i][len(prefix):]
663
664 # Strip any leading path separators.
665 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
666 file_list[i] = file_list[i][1:]
667
668 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
669 self._RunHooks(command, file_list, is_using_git)
670 633
671 if command == 'update': 634 if command == 'update':
672 # Notify the user if there is an orphaned entry in their working copy. 635 # Notify the user if there is an orphaned entry in their working copy.
673 # Only delete the directory if there are no changes in it, and 636 # Only delete the directory if there are no changes in it, and
674 # delete_unversioned_trees is set to true. 637 # delete_unversioned_trees is set to true.
675 prev_entries = self._ReadEntries() 638 entries = [i.name for i in self.tree(False)]
676 for entry in prev_entries: 639 for entry, prev_url in self._ReadEntries().iteritems():
677 # Fix path separator on Windows. 640 # Fix path separator on Windows.
678 entry_fixed = entry.replace('/', os.path.sep) 641 entry_fixed = entry.replace('/', os.path.sep)
679 e_dir = os.path.join(self.root_dir(), entry_fixed) 642 e_dir = os.path.join(self.root_dir(), entry_fixed)
680 # Use entry and not entry_fixed there. 643 # Use entry and not entry_fixed there.
681 if entry not in entries and os.path.exists(e_dir): 644 if entry not in entries and os.path.exists(e_dir):
682 modified_files = False 645 file_list = []
683 if isinstance(prev_entries, list): 646 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
684 # old .gclient_entries format was list, now dict 647 scm.status(self._options, [], file_list)
685 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir) 648 modified_files = file_list != []
686 else:
687 file_list = []
688 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
689 entry_fixed)
690 scm.status(self._options, [], file_list)
691 modified_files = file_list != []
692 if not self._options.delete_unversioned_trees or modified_files: 649 if not self._options.delete_unversioned_trees or modified_files:
693 # There are modified files in this entry. Keep warning until 650 # There are modified files in this entry. Keep warning until
694 # removed. 651 # removed.
695 print(('\nWARNING: \'%s\' is no longer part of this client. ' 652 print(('\nWARNING: \'%s\' is no longer part of this client. '
696 'It is recommended that you manually remove it.\n') % 653 'It is recommended that you manually remove it.\n') %
697 entry_fixed) 654 entry_fixed)
698 else: 655 else:
699 # Delete the entry 656 # Delete the entry
700 print('\n________ deleting \'%s\' in \'%s\'' % ( 657 print('\n________ deleting \'%s\' in \'%s\'' % (
701 entry_fixed, self.root_dir())) 658 entry_fixed, self.root_dir()))
702 gclient_utils.RemoveDirectory(e_dir) 659 gclient_utils.RemoveDirectory(e_dir)
703 # record the current list of entries for next time 660 # record the current list of entries for next time
704 self._SaveEntries(entries) 661 self._SaveEntries()
705 return 0 662 return 0
706 663
707 def PrintRevInfo(self): 664 def PrintRevInfo(self):
708 """Output revision info mapping for the client and its dependencies. 665 """Output revision info mapping for the client and its dependencies.
709 666
710 This allows the capture of an overall "revision" for the source tree that 667 This allows the capture of an overall "revision" for the source tree that
711 can be used to reproduce the same tree in the future. It is only useful for 668 can be used to reproduce the same tree in the future. It is only useful for
712 "unpinned dependencies", i.e. DEPS/deps references without a svn revision 669 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
713 number or a git hash. A git branch name isn't "pinned" since the actual 670 number or a git hash. A git branch name isn't "pinned" since the actual
714 commit can change. 671 commit can change.
715 672
716 The --snapshot option allows creating a .gclient file to reproduce the tree. 673 The --snapshot option allows creating a .gclient file to reproduce the tree.
717 """ 674 """
718 if not self.dependencies: 675 if not self.dependencies:
719 raise gclient_utils.Error('No solution specified') 676 raise gclient_utils.Error('No solution specified')
677 # Load all the settings.
678 self.RunCommandRecursively(self._options, {}, None, [], None)
720 679
721 # Inner helper to generate base url and rev tuple
722 def GetURLAndRev(name, original_url): 680 def GetURLAndRev(name, original_url):
723 if not original_url: 681 """Returns the revision-qualified SCM url."""
682 if original_url is None:
724 return None 683 return None
684 if isinstance(original_url, self.FileImpl):
685 original_url = original_url.file_location
725 url, _ = gclient_utils.SplitUrlRevision(original_url) 686 url, _ = gclient_utils.SplitUrlRevision(original_url)
726 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name) 687 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
688 if not os.path.isdir(scm.checkout_path):
689 return None
727 return '%s@%s' % (url, scm.revinfo(self._options, [], None)) 690 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
728 691
729 # text of the snapshot gclient file
730 new_gclient = ""
731 # Dictionary of { path : SCM url } to ensure no duplicate solutions
732 solution_names = {}
733 entries = {}
734 # Run on the base solutions first.
735 for solution in self.dependencies:
736 # Dictionary of { path : SCM url } to describe the gclient checkout
737 name = solution.name
738 if name in solution_names:
739 raise gclient_utils.Error("solution %s specified more than once" % name)
740 url = GetURLAndRev(name, solution.url)
741 entries[name] = url
742 solution_names[name] = url
743
744 # Process the dependencies next (sort alphanumerically to ensure that
745 # containing directories get populated first and for readability)
746 deps = self._ParseAllDeps(entries)
747 deps_to_process = deps.keys()
748 deps_to_process.sort()
749
750 # First pass for direct dependencies.
751 for d in deps_to_process:
752 if type(deps[d]) == str:
753 entries[d] = GetURLAndRev(d, deps[d])
754 elif isinstance(deps[d], self.FileImpl):
755 entries[d] = GetURLAndRev(d, deps[d].file_location)
756
757 # Second pass for inherited deps (via the From keyword)
758 for d in deps_to_process:
759 if isinstance(deps[d], self.FromImpl):
760 deps_parent_url = entries[deps[d].module_name]
761 if deps_parent_url.find("@") < 0:
762 raise gclient_utils.Error("From %s missing revisioned url" %
763 deps[d].module_name)
764 sub_deps_base_url = deps[deps[d].module_name]
765 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
766 ).ParseDepsFile(False)
767 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
768 entries[d] = GetURLAndRev(d, url)
769
770 # Build the snapshot configuration string
771 if self._options.snapshot: 692 if self._options.snapshot:
772 url = entries.pop(name) 693 new_gclient = ''
773 694 # First level at .gclient
774 # Build the snapshot configuration string 695 for d in self.dependencies:
775 if self._options.snapshot: 696 entries = {}
776 url = entries.pop(name) 697 def GrabDeps(sol):
777 custom_deps = ''.join([' \"%s\": \"%s\",\n' % (x, entries[x]) 698 """Recursively grab dependencies."""
778 for x in sorted(entries.keys())]) 699 for i in sol.dependencies:
779 700 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
780 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % { 701 GrabDeps(i)
781 'solution_name': name, 702 GrabDeps(d)
782 'solution_url': url, 703 custom_deps = []
783 'safesync_url' : '', 704 for k in sorted(entries.keys()):
784 'solution_deps': custom_deps, 705 if entries[k]:
785 } 706 # Quotes aren't escaped...
707 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
708 else:
709 custom_deps.append(' \"%s\": None,\n' % k)
710 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
711 'solution_name': d.name,
712 'solution_url': d.url,
713 'safesync_url' : d.safesync_url or '',
714 'solution_deps': ''.join(custom_deps),
715 }
716 # Print the snapshot configuration file
717 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
786 else: 718 else:
787 print(';\n'.join(['%s: %s' % (x, entries[x]) 719 entries = sorted(self.tree(False), key=lambda i: i.name)
788 for x in sorted(entries.keys())])) 720 for entry in entries:
789 721 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
790 # Print the snapshot configuration file 722 line = '%s: %s' % (entry.name, entry_url)
791 if self._options.snapshot: 723 if not entry is entries[-1]:
792 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient} 724 line += ';'
793 snapclient = GClient(self.root_dir(), self._options) 725 print line
794 snapclient.SetConfig(config)
795 print(snapclient.config_content)
796 726
797 def ParseDepsFile(self, direct_reference): 727 def ParseDepsFile(self, direct_reference):
798 """No DEPS to parse for a .gclient file.""" 728 """No DEPS to parse for a .gclient file."""
799 self.direct_reference = direct_reference 729 self.direct_reference = direct_reference
800 self.deps_parsed = True 730 self.deps_parsed = True
801 731
802 def root_dir(self): 732 def root_dir(self):
803 """Root directory of gclient checkout.""" 733 """Root directory of gclient checkout."""
804 return self._root_dir 734 return self._root_dir
805 735
(...skipping 300 matching lines...) Expand 10 before | Expand all | Expand 10 after
1106 'unpinned dependencies', i.e. DEPS/deps references without a svn revision 1036 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1107 number or a git hash. A git branch name isn't 'pinned' since the actual 1037 number or a git hash. A git branch name isn't 'pinned' since the actual
1108 commit can change. 1038 commit can change.
1109 """ 1039 """
1110 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST', 1040 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1111 help='override deps for the specified (comma-separated) ' 1041 help='override deps for the specified (comma-separated) '
1112 'platform(s); \'all\' will process all deps_os ' 1042 'platform(s); \'all\' will process all deps_os '
1113 'references') 1043 'references')
1114 parser.add_option('-s', '--snapshot', action='store_true', 1044 parser.add_option('-s', '--snapshot', action='store_true',
1115 help='creates a snapshot .gclient file of the current ' 1045 help='creates a snapshot .gclient file of the current '
1116 'version of all repositories to reproduce the tree, ' 1046 'version of all repositories to reproduce the tree')
1117 'implies -a')
1118 (options, args) = parser.parse_args(args) 1047 (options, args) = parser.parse_args(args)
1119 client = GClient.LoadCurrentConfig(options) 1048 client = GClient.LoadCurrentConfig(options)
1120 if not client: 1049 if not client:
1121 raise gclient_utils.Error('client not configured; see \'gclient config\'') 1050 raise gclient_utils.Error('client not configured; see \'gclient config\'')
1122 client.PrintRevInfo() 1051 client.PrintRevInfo()
1123 return 0 1052 return 0
1124 1053
1125 1054
1126 def Command(name): 1055 def Command(name):
1127 return getattr(sys.modules[__name__], 'CMD' + name, None) 1056 return getattr(sys.modules[__name__], 'CMD' + name, None)
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
1205 return CMDhelp(parser, argv) 1134 return CMDhelp(parser, argv)
1206 except gclient_utils.Error, e: 1135 except gclient_utils.Error, e:
1207 print >> sys.stderr, 'Error: %s' % str(e) 1136 print >> sys.stderr, 'Error: %s' % str(e)
1208 return 1 1137 return 1
1209 1138
1210 1139
1211 if '__main__' == __name__: 1140 if '__main__' == __name__:
1212 sys.exit(Main(sys.argv[1:])) 1141 sys.exit(Main(sys.argv[1:]))
1213 1142
1214 # vim: ts=2:sw=2:tw=80:et: 1143 # vim: ts=2:sw=2:tw=80:et:
OLDNEW
« no previous file with comments | « no previous file | gclient_utils.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698