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