OLD | NEW |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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: |
OLD | NEW |