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

Side by Side Diff: depot_tools/gclient.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 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 | « depot_tools/gclient.bat ('k') | depot_tools/git-cl.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/python
2 #
3 # Copyright 2008 Google Inc. All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """A wrapper script to manage a set of client modules in different SCM.
18
19 This script is intended to be used to help basic management of client
20 program sources residing in one or more Subversion modules, along with
21 other modules it depends on, also in Subversion, but possibly on
22 multiple respositories, making a wrapper system apparently necessary.
23
24 Files
25 .gclient : Current client configuration, written by 'config' command.
26 Format is a Python script defining 'solutions', a list whose
27 entries each are maps binding the strings "name" and "url"
28 to strings specifying the name and location of the client
29 module, as well as "custom_deps" to a map similar to the DEPS
30 file below.
31 .gclient_entries : A cache constructed by 'update' command. Format is a
32 Python script defining 'entries', a list of the names
33 of all modules in the client
34 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
35 submodule name to a URL where it can be found (via one SCM)
36
37 Hooks
38 .gclient and DEPS files may optionally contain a list named "hooks" to
39 allow custom actions to be performed based on files that have changed in the
40 working copy as a result of a "sync"/"update" or "revert" operation. Hooks
41 can also be run based on what files have been modified in the working copy
42 with the "runhooks" operation. If any of these operation are run with
43 --force, all known hooks will run regardless of the state of the working
44 copy.
45
46 Each item in a "hooks" list is a dict, containing these two keys:
47 "pattern" The associated value is a string containing a regular
48 expression. When a file whose pathname matches the expression
49 is checked out, updated, or reverted, the hook's "action" will
50 run.
51 "action" A list describing a command to run along with its arguments, if
52 any. An action command will run at most one time per gclient
53 invocation, regardless of how many files matched the pattern.
54 The action is executed in the same directory as the .gclient
55 file. If the first item in the list is the string "python",
56 the current Python interpreter (sys.executable) will be used
57 to run the command.
58
59 Example:
60 hooks = [
61 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
62 "action": ["python", "image_indexer.py", "--all"]},
63 ]
64 """
65
66 __author__ = "darinf@gmail.com (Darin Fisher)"
67 __version__ = "0.3.1"
68
69 import errno
70 import optparse
71 import os
72 import re
73 import stat
74 import subprocess
75 import sys
76 import time
77 import urlparse
78 import xml.dom.minidom
79 import urllib
80
81 def getText(nodelist):
82 """
83 Return the concatenated text for the children of a list of DOM nodes.
84 """
85 rc = []
86 for node in nodelist:
87 if node.nodeType == node.TEXT_NODE:
88 rc.append(node.data)
89 else:
90 rc.append(getText(node.childNodes))
91 return ''.join(rc)
92
93
94 SVN_COMMAND = "svn"
95
96
97 # default help text
98 DEFAULT_USAGE_TEXT = (
99 """usage: %prog <subcommand> [options] [--] [svn options/args...]
100 a wrapper for managing a set of client modules in svn.
101 Version """ + __version__ + """
102
103 subcommands:
104 cleanup
105 config
106 diff
107 revert
108 status
109 sync
110 update
111 runhooks
112 revinfo
113
114 Options and extra arguments can be passed to invoked svn commands by
115 appending them to the command line. Note that if the first such
116 appended option starts with a dash (-) then the options must be
117 preceded by -- to distinguish them from gclient options.
118
119 For additional help on a subcommand or examples of usage, try
120 %prog help <subcommand>
121 %prog help files
122 """)
123
124 GENERIC_UPDATE_USAGE_TEXT = (
125 """Perform a checkout/update of the modules specified by the gclient
126 configuration; see 'help config'. Unless --revision is specified,
127 then the latest revision of the root solutions is checked out, with
128 dependent submodule versions updated according to DEPS files.
129 If --revision is specified, then the given revision is used in place
130 of the latest, either for a single solution or for all solutions.
131 Unless the --force option is provided, solutions and modules whose
132 local revision matches the one to update (i.e., they have not changed
133 in the repository) are *not* modified.
134 This a synonym for 'gclient %(alias)s'
135
136 usage: gclient %(cmd)s [options] [--] [svn update options/args]
137
138 Valid options:
139 --force : force update even for unchanged modules
140 --revision REV : update/checkout all solutions with specified revision
141 --revision SOLUTION@REV : update given solution to specified revision
142 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
143 --verbose : output additional diagnostics
144
145 Examples:
146 gclient %(cmd)s
147 update files from SVN according to current configuration,
148 *for modules which have changed since last update or sync*
149 gclient %(cmd)s --force
150 update files from SVN according to current configuration, for
151 all modules (useful for recovering files deleted from local copy)
152 """)
153
154 COMMAND_USAGE_TEXT = {
155 "cleanup":
156 """Clean up all working copies, using 'svn cleanup' for each module.
157 Additional options and args may be passed to 'svn cleanup'.
158
159 usage: cleanup [options] [--] [svn cleanup args/options]
160
161 Valid options:
162 --verbose : output additional diagnostics
163 """,
164 "config": """Create a .gclient file in the current directory; this
165 specifies the configuration for further commands. After update/sync,
166 top-level DEPS files in each module are read to determine dependent
167 modules to operate on as well. If optional [url] parameter is
168 provided, then configuration is read from a specified Subversion server
169 URL. Otherwise, a --spec option must be provided.
170
171 usage: config [option | url] [safesync url]
172
173 Valid options:
174 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
175 *Note that due to Cygwin/Python brokenness, it
176 probably can't contain any newlines.*
177
178 Examples:
179 gclient config https://gclient.googlecode.com/svn/trunk/gclient
180 configure a new client to check out gclient.py tool sources
181 gclient config --spec='solutions=[{"name":"gclient","""
182 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
183 '"custom_deps":{}}]',
184 "diff": """Display the differences between two revisions of modules.
185 (Does 'svn diff' for each checked out module and dependences.)
186 Additional args and options to 'svn diff' can be passed after
187 gclient options.
188
189 usage: diff [options] [--] [svn args/options]
190
191 Valid options:
192 --verbose : output additional diagnostics
193
194 Examples:
195 gclient diff
196 simple 'svn diff' for configured client and dependences
197 gclient diff -- -x -b
198 use 'svn diff -x -b' to suppress whitespace-only differences
199 gclient diff -- -r HEAD -x -b
200 diff versus the latest version of each module
201 """,
202 "revert":
203 """Revert every file in every managed directory in the client view.
204
205 usage: revert
206 """,
207 "status":
208 """Show the status of client and dependent modules, using 'svn diff'
209 for each module. Additional options and args may be passed to 'svn diff'.
210
211 usage: status [options] [--] [svn diff args/options]
212
213 Valid options:
214 --verbose : output additional diagnostics
215 """,
216 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
217 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
218 "help": """Describe the usage of this program or its subcommands.
219
220 usage: help [options] [subcommand]
221
222 Valid options:
223 --verbose : output additional diagnostics
224 """,
225 "runhooks":
226 """Runs hooks for files that have been modified in the local working copy,
227 according to 'svn status'.
228
229 usage: runhooks [options]
230
231 Valid options:
232 --force : runs all known hooks, regardless of the working
233 copy status
234 --verbose : output additional diagnostics
235 """,
236 "revinfo":
237 """Outputs source path, server URL and revision information for every
238 dependency in all solutions (no local checkout required).
239
240 usage: revinfo [options]
241 """,
242 }
243
244 # parameterized by (solution_name, solution_url, safesync_url)
245 DEFAULT_CLIENT_FILE_TEXT = (
246 """
247 # An element of this array (a \"solution\") describes a repository directory
248 # that will be checked out into your working copy. Each solution may
249 # optionally define additional dependencies (via its DEPS file) to be
250 # checked out alongside the solution's directory. A solution may also
251 # specify custom dependencies (via the \"custom_deps\" property) that
252 # override or augment the dependencies specified by the DEPS file.
253 # If a \"safesync_url\" is specified, it is assumed to reference the location of
254 # a text file which contains nothing but the last known good SCM revision to
255 # sync against. It is fetched if specified and used unless --head is passed
256 solutions = [
257 { \"name\" : \"%s\",
258 \"url\" : \"%s\",
259 \"custom_deps\" : {
260 # To use the trunk of a component instead of what's in DEPS:
261 #\"component\": \"https://svnserver/component/trunk/\",
262 # To exclude a component from your working copy:
263 #\"data/really_large_component\": None,
264 },
265 \"safesync_url\": \"%s\"
266 }
267 ]
268 """)
269
270
271 ## Generic utils
272
273
274 class Error(Exception):
275 """gclient exception class."""
276 pass
277
278 class PrintableObject(object):
279 def __str__(self):
280 output = ''
281 for i in dir(self):
282 if i.startswith('__'):
283 continue
284 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
285 return output
286
287
288 def FileRead(filename):
289 content = None
290 f = open(filename, "rU")
291 try:
292 content = f.read()
293 finally:
294 f.close()
295 return content
296
297
298 def FileWrite(filename, content):
299 f = open(filename, "w")
300 try:
301 f.write(content)
302 finally:
303 f.close()
304
305
306 def RemoveDirectory(*path):
307 """Recursively removes a directory, even if it's marked read-only.
308
309 Remove the directory located at *path, if it exists.
310
311 shutil.rmtree() doesn't work on Windows if any of the files or directories
312 are read-only, which svn repositories and some .svn files are. We need to
313 be able to force the files to be writable (i.e., deletable) as we traverse
314 the tree.
315
316 Even with all this, Windows still sometimes fails to delete a file, citing
317 a permission error (maybe something to do with antivirus scans or disk
318 indexing). The best suggestion any of the user forums had was to wait a
319 bit and try again, so we do that too. It's hand-waving, but sometimes it
320 works. :/
321
322 On POSIX systems, things are a little bit simpler. The modes of the files
323 to be deleted doesn't matter, only the modes of the directories containing
324 them are significant. As the directory tree is traversed, each directory
325 has its mode set appropriately before descending into it. This should
326 result in the entire tree being removed, with the possible exception of
327 *path itself, because nothing attempts to change the mode of its parent.
328 Doing so would be hazardous, as it's not a directory slated for removal.
329 In the ordinary case, this is not a problem: for our purposes, the user
330 will never lack write permission on *path's parent.
331 """
332 file_path = os.path.join(*path)
333 if not os.path.exists(file_path):
334 return
335
336 if os.path.islink(file_path) or not os.path.isdir(file_path):
337 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
338
339 has_win32api = False
340 if sys.platform == 'win32':
341 has_win32api = True
342 # Some people don't have the APIs installed. In that case we'll do without.
343 try:
344 win32api = __import__('win32api')
345 win32con = __import__('win32con')
346 except ImportError:
347 has_win32api = False
348 else:
349 # On POSIX systems, we need the x-bit set on the directory to access it,
350 # the r-bit to see its contents, and the w-bit to remove files from it.
351 # The actual modes of the files within the directory is irrelevant.
352 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
353 for fn in os.listdir(file_path):
354 fullpath = os.path.join(file_path, fn)
355
356 # If fullpath is a symbolic link that points to a directory, isdir will
357 # be True, but we don't want to descend into that as a directory, we just
358 # want to remove the link. Check islink and treat links as ordinary files
359 # would be treated regardless of what they reference.
360 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
361 if sys.platform == 'win32':
362 os.chmod(fullpath, stat.S_IWRITE)
363 if has_win32api:
364 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
365 try:
366 os.remove(fullpath)
367 except OSError, e:
368 if e.errno != errno.EACCES or sys.platform != 'win32':
369 raise
370 print 'Failed to delete %s: trying again' % fullpath
371 time.sleep(0.1)
372 os.remove(fullpath)
373 else:
374 RemoveDirectory(fullpath)
375
376 if sys.platform == 'win32':
377 os.chmod(file_path, stat.S_IWRITE)
378 if has_win32api:
379 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
380 try:
381 os.rmdir(file_path)
382 except OSError, e:
383 if e.errno != errno.EACCES or sys.platform != 'win32':
384 raise
385 print 'Failed to remove %s: trying again' % file_path
386 time.sleep(0.1)
387 os.rmdir(file_path)
388
389
390 def SubprocessCall(command, in_directory, out, fail_status=None):
391 """Runs command, a list, in directory in_directory.
392
393 This function wraps SubprocessCallAndCapture, but does not perform the
394 capturing functions. See that function for a more complete usage
395 description.
396 """
397 # Call subprocess and capture nothing:
398 SubprocessCallAndCapture(command, in_directory, out, fail_status)
399
400
401 def SubprocessCallAndCapture(command, in_directory, out, fail_status=None,
402 pattern=None, capture_list=None):
403 """Runs command, a list, in directory in_directory.
404
405 A message indicating what is being done, as well as the command's stdout,
406 is printed to out.
407
408 If a pattern is specified, any line in the output matching pattern will have
409 its first match group appended to capture_list.
410
411 If the command fails, as indicated by a nonzero exit status, gclient will
412 exit with an exit status of fail_status. If fail_status is None (the
413 default), gclient will raise an Error exception.
414 """
415
416 print >> out, ("\n________ running \'%s\' in \'%s\'"
417 % (' '.join(command), in_directory))
418
419 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
420 # executable, but shell=True makes subprocess on Linux fail when it's called
421 # with a list because it only tries to execute the first item in the list.
422 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
423 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
424
425 if pattern:
426 compiled_pattern = re.compile(pattern)
427
428 # Also, we need to forward stdout to prevent weird re-ordering of output.
429 # This has to be done on a per byte basis to make sure it is not buffered:
430 # normally buffering is done for each line, but if svn requests input, no
431 # end-of-line character is output after the prompt and it would not show up.
432 in_byte = kid.stdout.read(1)
433 in_line = ""
434 while in_byte:
435 if in_byte != "\r":
436 out.write(in_byte)
437 in_line += in_byte
438 if in_byte == "\n" and pattern:
439 match = compiled_pattern.search(in_line[:-1])
440 if match:
441 capture_list.append(match.group(1))
442 in_line = ""
443 in_byte = kid.stdout.read(1)
444 rv = kid.wait()
445
446 if rv:
447 msg = "failed to run command: %s" % " ".join(command)
448
449 if fail_status != None:
450 print >>sys.stderr, msg
451 sys.exit(fail_status)
452
453 raise Error(msg)
454
455
456 def IsUsingGit(root, paths):
457 """Returns True if we're using git to manage any of our checkouts.
458 |entries| is a list of paths to check."""
459 for path in paths:
460 if os.path.exists(os.path.join(root, path, '.git')):
461 return True
462 return False
463
464 # -----------------------------------------------------------------------------
465 # SVN utils:
466
467
468 def RunSVN(options, args, in_directory):
469 """Runs svn, sending output to stdout.
470
471 Args:
472 args: A sequence of command line parameters to be passed to svn.
473 in_directory: The directory where svn is to be run.
474
475 Raises:
476 Error: An error occurred while running the svn command.
477 """
478 c = [SVN_COMMAND]
479 c.extend(args)
480
481 SubprocessCall(c, in_directory, options.stdout)
482
483
484 def CaptureSVN(options, args, in_directory):
485 """Runs svn, capturing output sent to stdout as a string.
486
487 Args:
488 args: A sequence of command line parameters to be passed to svn.
489 in_directory: The directory where svn is to be run.
490
491 Returns:
492 The output sent to stdout as a string.
493 """
494 c = [SVN_COMMAND]
495 c.extend(args)
496
497 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
498 # the svn.exe executable, but shell=True makes subprocess on Linux fail
499 # when it's called with a list because it only tries to execute the
500 # first string ("svn").
501 return subprocess.Popen(c, cwd=in_directory, shell=(sys.platform == 'win32'),
502 stdout=subprocess.PIPE).communicate()[0]
503
504
505 def RunSVNAndGetFileList(options, args, in_directory, file_list):
506 """Runs svn checkout, update, or status, output to stdout.
507
508 The first item in args must be either "checkout", "update", or "status".
509
510 svn's stdout is parsed to collect a list of files checked out or updated.
511 These files are appended to file_list. svn's stdout is also printed to
512 sys.stdout as in RunSVN.
513
514 Args:
515 args: A sequence of command line parameters to be passed to svn.
516 in_directory: The directory where svn is to be run.
517
518 Raises:
519 Error: An error occurred while running the svn command.
520 """
521 command = [SVN_COMMAND]
522 command.extend(args)
523
524 # svn update and svn checkout use the same pattern: the first three columns
525 # are for file status, property status, and lock status. This is followed
526 # by two spaces, and then the path to the file.
527 update_pattern = '^... (.*)$'
528
529 # The first three columns of svn status are the same as for svn update and
530 # svn checkout. The next three columns indicate addition-with-history,
531 # switch, and remote lock status. This is followed by one space, and then
532 # the path to the file.
533 status_pattern = '^...... (.*)$'
534
535 # args[0] must be a supported command. This will blow up if it's something
536 # else, which is good. Note that the patterns are only effective when
537 # these commands are used in their ordinary forms, the patterns are invalid
538 # for "svn status --show-updates", for example.
539 pattern = {
540 'checkout': update_pattern,
541 'status': status_pattern,
542 'update': update_pattern,
543 }[args[0]]
544
545 SubprocessCallAndCapture(command, in_directory, options.stdout,
546 pattern=pattern, capture_list=file_list)
547
548
549 def CaptureSVNInfo(options, relpath, in_directory):
550 """Runs 'svn info' on an existing path.
551
552 Args:
553 relpath: The directory where the working copy resides relative to
554 the directory given by in_directory.
555 in_directory: The directory where svn is to be run.
556
557 Returns:
558 An object with fields corresponding to the output of 'svn info'
559 """
560 info = CaptureSVN(options, ["info", "--xml", relpath], in_directory)
561 dom = xml.dom.minidom.parseString(info)
562
563 # str() the getText() results because they may be returned as
564 # Unicode, which interferes with the higher layers matching up
565 # things in the deps dictionary.
566 result = PrintableObject()
567 result.root = str(getText(dom.getElementsByTagName('root')))
568 result.url = str(getText(dom.getElementsByTagName('url')))
569 result.uuid = str(getText(dom.getElementsByTagName('uuid')))
570 result.revision = int(dom.getElementsByTagName('entry')[0].getAttribute(
571 'revision'))
572 return result
573
574
575 def CaptureSVNHeadRevision(options, url):
576 """Get the head revision of a SVN repository.
577
578 Returns:
579 Int head revision
580 """
581 info = CaptureSVN(options, ["info", "--xml", url], os.getcwd())
582 dom = xml.dom.minidom.parseString(info)
583 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
584
585
586 class FileStatus:
587 def __init__(self, path, text_status, props, locked, history, switched,
588 repo_locked, out_of_date):
589 self.path = path.strip()
590 self.text_status = text_status
591 self.props = props
592 self.locked = locked
593 self.history = history
594 self.switched = switched
595 self.repo_locked = repo_locked
596 self.out_of_date = out_of_date
597
598 def __str__(self):
599 return (self.text_status + self.props + self.locked + self.history +
600 self.switched + self.repo_locked + self.out_of_date +
601 self.path)
602
603
604 def CaptureSVNStatus(options, path):
605 """Runs 'svn status' on an existing path.
606
607 Args:
608 path: The directory to run svn status.
609
610 Returns:
611 An array of FileStatus corresponding to the output of 'svn status'
612 """
613 info = CaptureSVN(options, ["status"], path)
614 result = []
615 if not info:
616 return result
617 for line in info.splitlines():
618 if line:
619 new_item = FileStatus(line[7:], line[0:1], line[1:2], line[2:3],
620 line[3:4], line[4:5], line[5:6], line[6:7])
621 result.append(new_item)
622 return result
623
624
625 ### SCM abstraction layer
626
627
628 class SCMWrapper(object):
629 """Add necessary glue between all the supported SCM.
630
631 This is the abstraction layer to bind to different SCM. Since currently only
632 subversion is supported, a lot of subersionism remains. This can be sorted out
633 once another SCM is supported."""
634 def __init__(self, url=None, root_dir=None, relpath=None,
635 scm_name='svn'):
636 # TODO(maruel): Deduce the SCM from the url.
637 self.scm_name = scm_name
638 self.url = url
639 self._root_dir = root_dir
640 if self._root_dir:
641 self._root_dir = self._root_dir.replace('/', os.sep).strip()
642 self.relpath = relpath
643 if self.relpath:
644 self.relpath = self.relpath.replace('/', os.sep).strip()
645
646 def FullUrlForRelativeUrl(self, url):
647 # Find the forth '/' and strip from there. A bit hackish.
648 return '/'.join(self.url.split('/')[:4]) + url
649
650 def RunCommand(self, command, options, args, file_list=None):
651 # file_list will have all files that are modified appended to it.
652
653 if file_list == None:
654 file_list = []
655
656 commands = {
657 'cleanup': self.cleanup,
658 'update': self.update,
659 'revert': self.revert,
660 'status': self.status,
661 'diff': self.diff,
662 'runhooks': self.status,
663 }
664
665 if not command in commands:
666 raise Error('Unknown command %s' % command)
667
668 return commands[command](options, args, file_list)
669
670 def cleanup(self, options, args, file_list):
671 """Cleanup working copy."""
672 command = ['cleanup']
673 command.extend(args)
674 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
675
676 def diff(self, options, args, file_list):
677 # NOTE: This function does not currently modify file_list.
678 command = ['diff']
679 command.extend(args)
680 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
681
682 def update(self, options, args, file_list):
683 """Runs SCM to update or transparently checkout the working copy.
684
685 All updated files will be appended to file_list.
686
687 Raises:
688 Error: if can't get URL for relative path.
689 """
690 # Only update if git is not controlling the directory.
691 git_path = os.path.join(self._root_dir, self.relpath, '.git')
692 if options.path_exists(git_path):
693 print >> options.stdout, (
694 "________ found .git directory; skipping %s" % self.relpath)
695 return
696
697 if args:
698 raise Error("Unsupported argument(s): %s" % ",".join(args))
699
700 url = self.url
701 components = url.split("@")
702 revision = None
703 forced_revision = False
704 if options.revision:
705 # Override the revision number.
706 url = '%s@%s' % (components[0], str(options.revision))
707 revision = int(options.revision)
708 forced_revision = True
709 elif len(components) == 2:
710 revision = int(components[1])
711 forced_revision = True
712
713 rev_str = ""
714 if revision:
715 rev_str = ' at %d' % revision
716
717 if not options.path_exists(os.path.join(self._root_dir, self.relpath)):
718 # We need to checkout.
719 command = ['checkout', url, os.path.join(self._root_dir, self.relpath)]
720 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
721
722 # Get the existing scm url and the revision number of the current checkout.
723 from_info = CaptureSVNInfo(options,
724 os.path.join(self._root_dir, self.relpath, '.'),
725 '.')
726
727 if options.manually_grab_svn_rev:
728 # Retrieve the current HEAD version because svn is slow at null updates.
729 if not revision:
730 from_info_live = CaptureSVNInfo(options, from_info.url, '.')
731 revision = int(from_info_live.revision)
732 rev_str = ' at %d' % revision
733
734 if from_info.url != components[0]:
735 to_info = CaptureSVNInfo(options, url, '.')
736 if from_info.root != to_info.root:
737 # We have different roots, so check if we can switch --relocate.
738 # Subversion only permits this if the repository UUIDs match.
739 if from_info.uuid != to_info.uuid:
740 raise Error("Can't switch the checkout to %s; UUID don't match" % url)
741
742 # Perform the switch --relocate, then rewrite the from_url
743 # to reflect where we "are now." (This is the same way that
744 # Subversion itself handles the metadata when switch --relocate
745 # is used.) This makes the checks below for whether we
746 # can update to a revision or have to switch to a different
747 # branch work as expected.
748 # TODO(maruel): TEST ME !
749 command = ["switch", "--relocate", from_info.root, to_info.root,
750 self.relpath]
751 RunSVN(options, command, self._root_dir)
752 from_info.url = from_info.url.replace(from_info.root, to_info.root)
753
754 # If the provided url has a revision number that matches the revision
755 # number of the existing directory, then we don't need to bother updating.
756 if not options.force and from_info.revision == revision:
757 if options.verbose or not forced_revision:
758 print >>options.stdout, ("\n_____ %s%s" % (
759 self.relpath, rev_str))
760 return
761
762 command = ["update", os.path.join(self._root_dir, self.relpath)]
763 if revision:
764 command.extend(['--revision', str(revision)])
765 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
766
767 def revert(self, options, args, file_list):
768 """Reverts local modifications. Subversion specific.
769
770 All reverted files will be appended to file_list, even if Subversion
771 doesn't know about them.
772 """
773 path = os.path.join(self._root_dir, self.relpath)
774 if not os.path.isdir(path):
775 # We can't revert path that doesn't exist.
776 # TODO(maruel): Should we update instead?
777 if options.verbose:
778 print >>options.stdout, ("\n_____ %s is missing, can't revert" %
779 self.relpath)
780 return
781
782 files = CaptureSVNStatus(options, path)
783 # Batch the command.
784 files_to_revert = []
785 for file in files:
786 file_path = os.path.join(path, file.path)
787 print >>options.stdout, file_path
788 # Unversioned file or unexpected unversioned file.
789 if file.text_status in ('?', '~'):
790 # Remove extraneous file. Also remove unexpected unversioned
791 # directories. svn won't touch them but we want to delete these.
792 file_list.append(file_path)
793 try:
794 os.remove(file_path)
795 except EnvironmentError:
796 RemoveDirectory(file_path)
797
798 if file.text_status != '?':
799 # For any other status, svn revert will work.
800 file_list.append(file_path)
801 files_to_revert.append(file.path)
802
803 # Revert them all at once.
804 if files_to_revert:
805 accumulated_paths = []
806 accumulated_length = 0
807 command = ['revert']
808 for p in files_to_revert:
809 # Some shell have issues with command lines too long.
810 if accumulated_length and accumulated_length + len(p) > 3072:
811 RunSVN(options, command + accumulated_paths,
812 os.path.join(self._root_dir, self.relpath))
813 accumulated_paths = []
814 accumulated_length = 0
815 else:
816 accumulated_paths.append(p)
817 accumulated_length += len(p)
818 if accumulated_paths:
819 RunSVN(options, command + accumulated_paths,
820 os.path.join(self._root_dir, self.relpath))
821
822 def status(self, options, args, file_list):
823 """Display status information."""
824 command = ['status']
825 command.extend(args)
826 RunSVNAndGetFileList(options, command,
827 os.path.join(self._root_dir, self.relpath), file_list)
828
829
830 ## GClient implementation.
831
832
833 class GClient(object):
834 """Object that represent a gclient checkout."""
835
836 supported_commands = [
837 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks'
838 ]
839
840 def __init__(self, root_dir, options):
841 self._root_dir = root_dir
842 self._options = options
843 self._config_content = None
844 self._config_dict = {}
845 self._deps_hooks = []
846
847 def SetConfig(self, content):
848 self._config_dict = {}
849 self._config_content = content
850 exec(content, self._config_dict)
851
852 def SaveConfig(self):
853 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
854 self._config_content)
855
856 def _LoadConfig(self):
857 client_source = FileRead(os.path.join(self._root_dir,
858 self._options.config_filename))
859 self.SetConfig(client_source)
860
861 def ConfigContent(self):
862 return self._config_content
863
864 def GetVar(self, key, default=None):
865 return self._config_dict.get(key, default)
866
867 @staticmethod
868 def LoadCurrentConfig(options, from_dir=None):
869 """Searches for and loads a .gclient file relative to the current working
870 dir.
871
872 Returns:
873 A dict representing the contents of the .gclient file or an empty dict if
874 the .gclient file doesn't exist.
875 """
876 if not from_dir:
877 from_dir = os.curdir
878 path = os.path.realpath(from_dir)
879 while not options.path_exists(os.path.join(path, options.config_filename)):
880 next = os.path.split(path)
881 if not next[1]:
882 return None
883 path = next[0]
884 client = options.gclient(path, options)
885 client._LoadConfig()
886 return client
887
888 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
889 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
890 solution_name, solution_url, safesync_url
891 ))
892
893 def _SaveEntries(self, entries):
894 """Creates a .gclient_entries file to record the list of unique checkouts.
895
896 The .gclient_entries file lives in the same directory as .gclient.
897
898 Args:
899 entries: A sequence of solution names.
900 """
901 text = "entries = [\n"
902 for entry in entries:
903 text += " \"%s\",\n" % entry
904 text += "]\n"
905 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
906 text)
907
908 def _ReadEntries(self):
909 """Read the .gclient_entries file for the given client.
910
911 Args:
912 client: The client for which the entries file should be read.
913
914 Returns:
915 A sequence of solution names, which will be empty if there is the
916 entries file hasn't been created yet.
917 """
918 scope = {}
919 filename = os.path.join(self._root_dir, self._options.entries_filename)
920 if not self._options.path_exists(filename):
921 return []
922 exec(FileRead(filename), scope)
923 return scope["entries"]
924
925 class FromImpl:
926 """Used to implement the From syntax."""
927
928 def __init__(self, module_name):
929 self.module_name = module_name
930
931 def __str__(self):
932 return 'From("%s")' % self.module_name
933
934 class _VarImpl:
935 def __init__(self, custom_vars, local_scope):
936 self._custom_vars = custom_vars
937 self._local_scope = local_scope
938
939 def Lookup(self, var_name):
940 """Implements the Var syntax."""
941 if var_name in self._custom_vars:
942 return self._custom_vars[var_name]
943 elif var_name in self._local_scope.get("vars", {}):
944 return self._local_scope["vars"][var_name]
945 raise Error("Var is not defined: %s" % var_name)
946
947 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
948 custom_vars):
949 """Parses the DEPS file for the specified solution.
950
951 Args:
952 solution_name: The name of the solution to query.
953 solution_deps_content: Content of the DEPS file for the solution
954 custom_vars: A dict of vars to override any vars defined in the DEPS file.
955
956 Returns:
957 A dict mapping module names (as relative paths) to URLs or an empty
958 dict if the solution does not have a DEPS file.
959 """
960 # Skip empty
961 if not solution_deps_content:
962 return {}
963 # Eval the content
964 local_scope = {}
965 var = self._VarImpl(custom_vars, local_scope)
966 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
967 exec(solution_deps_content, global_scope, local_scope)
968 deps = local_scope.get("deps", {})
969
970 # load os specific dependencies if defined. these dependencies may
971 # override or extend the values defined by the 'deps' member.
972 if "deps_os" in local_scope:
973 deps_os_choices = {
974 "win32": "win",
975 "win": "win",
976 "cygwin": "win",
977 "darwin": "mac",
978 "mac": "mac",
979 "unix": "unix",
980 "linux": "unix",
981 "linux2": "unix",
982 }
983
984 if self._options.deps_os is not None:
985 deps_to_include = self._options.deps_os.split(",")
986 if "all" in deps_to_include:
987 deps_to_include = deps_os_choices.values()
988 else:
989 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
990
991 deps_to_include = set(deps_to_include)
992 for deps_os_key in deps_to_include:
993 os_deps = local_scope["deps_os"].get(deps_os_key, {})
994 if len(deps_to_include) > 1:
995 # Ignore any overrides when including deps for more than one
996 # platform, so we collect the broadest set of dependencies available.
997 # We may end up with the wrong revision of something for our
998 # platform, but this is the best we can do.
999 deps.update([x for x in os_deps.items() if not x[0] in deps])
1000 else:
1001 deps.update(os_deps)
1002
1003 if 'hooks' in local_scope:
1004 self._deps_hooks.extend(local_scope['hooks'])
1005
1006 # If use_relative_paths is set in the DEPS file, regenerate
1007 # the dictionary using paths relative to the directory containing
1008 # the DEPS file.
1009 if local_scope.get('use_relative_paths'):
1010 rel_deps = {}
1011 for d, url in deps.items():
1012 # normpath is required to allow DEPS to use .. in their
1013 # dependency local path.
1014 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1015 return rel_deps
1016 else:
1017 return deps
1018
1019 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1020 """Parse the complete list of dependencies for the client.
1021
1022 Args:
1023 solution_urls: A dict mapping module names (as relative paths) to URLs
1024 corresponding to the solutions specified by the client. This parameter
1025 is passed as an optimization.
1026 solution_deps_content: A dict mapping module names to the content
1027 of their DEPS files
1028
1029 Returns:
1030 A dict mapping module names (as relative paths) to URLs corresponding
1031 to the entire set of dependencies to checkout for the given client.
1032
1033 Raises:
1034 Error: If a dependency conflicts with another dependency or of a solution.
1035 """
1036 deps = {}
1037 for solution in self.GetVar("solutions"):
1038 custom_vars = solution.get("custom_vars", {})
1039 solution_deps = self._ParseSolutionDeps(
1040 solution["name"],
1041 solution_deps_content[solution["name"]],
1042 custom_vars)
1043
1044 # If a line is in custom_deps, but not in the solution, we want to append
1045 # this line to the solution.
1046 if "custom_deps" in solution:
1047 for d in solution["custom_deps"]:
1048 if d not in solution_deps:
1049 solution_deps[d] = solution["custom_deps"][d]
1050
1051 for d in solution_deps:
1052 if "custom_deps" in solution and d in solution["custom_deps"]:
1053 # Dependency is overriden.
1054 url = solution["custom_deps"][d]
1055 if url is None:
1056 continue
1057 else:
1058 url = solution_deps[d]
1059 # if we have a From reference dependent on another solution, then
1060 # just skip the From reference. When we pull deps for the solution,
1061 # we will take care of this dependency.
1062 #
1063 # If multiple solutions all have the same From reference, then we
1064 # should only add one to our list of dependencies.
1065 if type(url) != str:
1066 if url.module_name in solution_urls:
1067 # Already parsed.
1068 continue
1069 if d in deps and type(deps[d]) != str:
1070 if url.module_name == deps[d].module_name:
1071 continue
1072 else:
1073 parsed_url = urlparse.urlparse(url)
1074 scheme = parsed_url[0]
1075 if not scheme:
1076 # A relative url. Fetch the real base.
1077 path = parsed_url[2]
1078 if path[0] != "/":
1079 raise Error(
1080 "relative DEPS entry \"%s\" must begin with a slash" % d)
1081 # Create a scm just to query the full url.
1082 scm = self._options.scm_wrapper(solution["url"], self._root_dir,
1083 None)
1084 url = scm.FullUrlForRelativeUrl(url)
1085 if d in deps and deps[d] != url:
1086 raise Error(
1087 "Solutions have conflicting versions of dependency \"%s\"" % d)
1088 if d in solution_urls and solution_urls[d] != url:
1089 raise Error(
1090 "Dependency \"%s\" conflicts with specified solution" % d)
1091 # Grab the dependency.
1092 deps[d] = url
1093 return deps
1094
1095 def _RunHookAction(self, hook_dict):
1096 """Runs the action from a single hook.
1097 """
1098 command = hook_dict['action'][:]
1099 if command[0] == 'python':
1100 # If the hook specified "python" as the first item, the action is a
1101 # Python script. Run it by starting a new copy of the same
1102 # interpreter.
1103 command[0] = sys.executable
1104
1105 # Use a discrete exit status code of 2 to indicate that a hook action
1106 # failed. Users of this script may wish to treat hook action failures
1107 # differently from VC failures.
1108 SubprocessCall(command, self._root_dir, self._options.stdout,
1109 fail_status=2)
1110
1111 def _RunHooks(self, command, file_list, is_using_git):
1112 """Evaluates all hooks, running actions as needed.
1113 """
1114 # Hooks only run for these command types.
1115 if not command in ('update', 'revert', 'runhooks'):
1116 return
1117
1118 # Get any hooks from the .gclient file.
1119 hooks = self.GetVar("hooks", [])
1120 # Add any hooks found in DEPS files.
1121 hooks.extend(self._deps_hooks)
1122
1123 # If "--force" was specified, run all hooks regardless of what files have
1124 # changed. If the user is using git, then we don't know what files have
1125 # changed so we always run all hooks.
1126 if self._options.force or is_using_git:
1127 for hook_dict in hooks:
1128 self._RunHookAction(hook_dict)
1129 return
1130
1131 # Run hooks on the basis of whether the files from the gclient operation
1132 # match each hook's pattern.
1133 for hook_dict in hooks:
1134 pattern = re.compile(hook_dict['pattern'])
1135 for file in file_list:
1136 if not pattern.search(file):
1137 continue
1138
1139 self._RunHookAction(hook_dict)
1140
1141 # The hook's action only runs once. Don't bother looking for any
1142 # more matches.
1143 break
1144
1145 def RunOnDeps(self, command, args):
1146 """Runs a command on each dependency in a client and its dependencies.
1147
1148 The module's dependencies are specified in its top-level DEPS files.
1149
1150 Args:
1151 command: The command to use (e.g., 'status' or 'diff')
1152 args: list of str - extra arguments to add to the command line.
1153
1154 Raises:
1155 Error: If the client has conflicting entries.
1156 """
1157 if not command in self.supported_commands:
1158 raise Error("'%s' is an unsupported command" % command)
1159
1160 # Check for revision overrides.
1161 revision_overrides = {}
1162 for revision in self._options.revisions:
1163 if revision.find("@") == -1:
1164 raise Error(
1165 "Specify the full dependency when specifying a revision number.")
1166 revision_elem = revision.split("@")
1167 # Disallow conflicting revs
1168 if revision_overrides.has_key(revision_elem[0]) and \
1169 revision_overrides[revision_elem[0]] != revision_elem[1]:
1170 raise Error(
1171 "Conflicting revision numbers specified.")
1172 revision_overrides[revision_elem[0]] = revision_elem[1]
1173
1174 solutions = self.GetVar("solutions")
1175 if not solutions:
1176 raise Error("No solution specified")
1177
1178 # When running runhooks --force, there's no need to consult the SCM.
1179 # All known hooks are expected to run unconditionally regardless of working
1180 # copy state, so skip the SCM status check.
1181 run_scm = not (command == 'runhooks' and self._options.force)
1182
1183 entries = {}
1184 entries_deps_content = {}
1185 file_list = []
1186 # Run on the base solutions first.
1187 for solution in solutions:
1188 name = solution["name"]
1189 if name in entries:
1190 raise Error("solution %s specified more than once" % name)
1191 url = solution["url"]
1192 entries[name] = url
1193 if run_scm:
1194 self._options.revision = revision_overrides.get(name)
1195 scm = self._options.scm_wrapper(url, self._root_dir, name)
1196 scm.RunCommand(command, self._options, args, file_list)
1197 self._options.revision = None
1198 try:
1199 deps_content = FileRead(os.path.join(self._root_dir, name,
1200 self._options.deps_file))
1201 except IOError, e:
1202 if e.errno != errno.ENOENT:
1203 raise
1204 deps_content = ""
1205 entries_deps_content[name] = deps_content
1206
1207 # Process the dependencies next (sort alphanumerically to ensure that
1208 # containing directories get populated first and for readability)
1209 deps = self._ParseAllDeps(entries, entries_deps_content)
1210 deps_to_process = deps.keys()
1211 deps_to_process.sort()
1212
1213 # First pass for direct dependencies.
1214 for d in deps_to_process:
1215 if type(deps[d]) == str:
1216 url = deps[d]
1217 entries[d] = url
1218 if run_scm:
1219 self._options.revision = revision_overrides.get(d)
1220 scm = self._options.scm_wrapper(url, self._root_dir, d)
1221 scm.RunCommand(command, self._options, args, file_list)
1222 self._options.revision = None
1223
1224 # Second pass for inherited deps (via the From keyword)
1225 for d in deps_to_process:
1226 if type(deps[d]) != str:
1227 sub_deps = self._ParseSolutionDeps(
1228 deps[d].module_name,
1229 FileRead(os.path.join(self._root_dir,
1230 deps[d].module_name,
1231 self._options.deps_file)),
1232 {})
1233 url = sub_deps[d]
1234 entries[d] = url
1235 if run_scm:
1236 self._options.revision = revision_overrides.get(d)
1237 scm = self._options.scm_wrapper(url, self._root_dir, d)
1238 scm.RunCommand(command, self._options, args, file_list)
1239 self._options.revision = None
1240
1241 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1242 self._RunHooks(command, file_list, is_using_git)
1243
1244 if command == 'update':
1245 # notify the user if there is an orphaned entry in their working copy.
1246 # TODO(darin): we should delete this directory manually if it doesn't
1247 # have any changes in it.
1248 prev_entries = self._ReadEntries()
1249 for entry in prev_entries:
1250 e_dir = os.path.join(self._root_dir, entry)
1251 if entry not in entries and self._options.path_exists(e_dir):
1252 if CaptureSVNStatus(self._options, e_dir):
1253 # There are modified files in this entry
1254 entries[entry] = None # Keep warning until removed.
1255 print >> self._options.stdout, (
1256 "\nWARNING: \"%s\" is no longer part of this client. "
1257 "It is recommended that you manually remove it.\n") % entry
1258 else:
1259 # Delete the entry
1260 print >> self._options.stdout, ("\n________ deleting \'%s\' " +
1261 "in \'%s\'") % (entry, self._root_dir)
1262 RemoveDirectory(e_dir)
1263 # record the current list of entries for next time
1264 self._SaveEntries(entries)
1265
1266 def PrintRevInfo(self):
1267 """Output revision info mapping for the client and its dependencies. This
1268 allows the capture of a overall "revision" for the source tree that can
1269 be used to reproduce the same tree in the future. The actual output
1270 contains enough information (source paths, svn server urls and revisions)
1271 that it can be used either to generate external svn commands (without
1272 gclient) or as input to gclient's --rev option (with some massaging of
1273 the data).
1274
1275 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1276 on the Pulse master. It MUST NOT execute hooks.
1277
1278 Raises:
1279 Error: If the client has conflicting entries.
1280 """
1281 # Check for revision overrides.
1282 revision_overrides = {}
1283 for revision in self._options.revisions:
1284 if revision.find("@") < 0:
1285 raise Error(
1286 "Specify the full dependency when specifying a revision number.")
1287 revision_elem = revision.split("@")
1288 # Disallow conflicting revs
1289 if revision_overrides.has_key(revision_elem[0]) and \
1290 revision_overrides[revision_elem[0]] != revision_elem[1]:
1291 raise Error(
1292 "Conflicting revision numbers specified.")
1293 revision_overrides[revision_elem[0]] = revision_elem[1]
1294
1295 solutions = self.GetVar("solutions")
1296 if not solutions:
1297 raise Error("No solution specified")
1298
1299 entries = {}
1300 entries_deps_content = {}
1301
1302 # Inner helper to generate base url and rev tuple (including honoring
1303 # |revision_overrides|)
1304 def GetURLAndRev(name, original_url):
1305 if original_url.find("@") < 0:
1306 if revision_overrides.has_key(name):
1307 return (original_url, int(revision_overrides[name]))
1308 else:
1309 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1310 return (original_url, CaptureSVNHeadRevision(self._options,
1311 original_url))
1312 else:
1313 url_components = original_url.split("@")
1314 if revision_overrides.has_key(name):
1315 return (url_components[0], int(revision_overrides[name]))
1316 else:
1317 return (url_components[0], int(url_components[1]))
1318
1319 # Run on the base solutions first.
1320 for solution in solutions:
1321 name = solution["name"]
1322 if name in entries:
1323 raise Error("solution %s specified more than once" % name)
1324 (url, rev) = GetURLAndRev(name, solution["url"])
1325 entries[name] = "%s@%d" % (url, rev)
1326 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1327 entries_deps_content[name] = CaptureSVN(
1328 self._options,
1329 ["cat",
1330 "%s/%s@%d" % (url,
1331 self._options.deps_file,
1332 rev)],
1333 os.getcwd())
1334
1335 # Process the dependencies next (sort alphanumerically to ensure that
1336 # containing directories get populated first and for readability)
1337 deps = self._ParseAllDeps(entries, entries_deps_content)
1338 deps_to_process = deps.keys()
1339 deps_to_process.sort()
1340
1341 # First pass for direct dependencies.
1342 for d in deps_to_process:
1343 if type(deps[d]) == str:
1344 (url, rev) = GetURLAndRev(d, deps[d])
1345 entries[d] = "%s@%d" % (url, rev)
1346
1347 # Second pass for inherited deps (via the From keyword)
1348 for d in deps_to_process:
1349 if type(deps[d]) != str:
1350 deps_parent_url = entries[deps[d].module_name]
1351 if deps_parent_url.find("@") < 0:
1352 raise Error("From %s missing revisioned url" % deps[d].module_name)
1353 deps_parent_url_components = deps_parent_url.split("@")
1354 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1355 deps_parent_content = CaptureSVN(
1356 self._options,
1357 ["cat",
1358 "%s/%s@%s" % (deps_parent_url_components[0],
1359 self._options.deps_file,
1360 deps_parent_url_components[1])],
1361 os.getcwd())
1362 sub_deps = self._ParseSolutionDeps(
1363 deps[d].module_name,
1364 FileRead(os.path.join(self._root_dir,
1365 deps[d].module_name,
1366 self._options.deps_file)),
1367 {})
1368 (url, rev) = GetURLAndRev(d, sub_deps[d])
1369 entries[d] = "%s@%d" % (url, rev)
1370
1371 print ";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())])
1372
1373
1374 ## gclient commands.
1375
1376
1377 def DoCleanup(options, args):
1378 """Handle the cleanup subcommand.
1379
1380 Raises:
1381 Error: if client isn't configured properly.
1382 """
1383 client = options.gclient.LoadCurrentConfig(options)
1384 if not client:
1385 raise Error("client not configured; see 'gclient config'")
1386 if options.verbose:
1387 # Print out the .gclient file. This is longer than if we just printed the
1388 # client dict, but more legible, and it might contain helpful comments.
1389 print >>options.stdout, client.ConfigContent()
1390 options.verbose = True
1391 return client.RunOnDeps('cleanup', args)
1392
1393
1394 def DoConfig(options, args):
1395 """Handle the config subcommand.
1396
1397 Args:
1398 options: If options.spec set, a string providing contents of config file.
1399 args: The command line args. If spec is not set,
1400 then args[0] is a string URL to get for config file.
1401
1402 Raises:
1403 Error: on usage error
1404 """
1405 if len(args) < 1 and not options.spec:
1406 raise Error("required argument missing; see 'gclient help config'")
1407 if options.path_exists(options.config_filename):
1408 raise Error("%s file already exists in the current directory" %
1409 options.config_filename)
1410 client = options.gclient('.', options)
1411 if options.spec:
1412 client.SetConfig(options.spec)
1413 else:
1414 # TODO(darin): it would be nice to be able to specify an alternate relpath
1415 # for the given URL.
1416 base_url = args[0]
1417 name = args[0].split("/")[-1]
1418 safesync_url = ""
1419 if len(args) > 1:
1420 safesync_url = args[1]
1421 client.SetDefaultConfig(name, base_url, safesync_url)
1422 client.SaveConfig()
1423
1424
1425 def DoHelp(options, args):
1426 """Handle the help subcommand giving help for another subcommand.
1427
1428 Raises:
1429 Error: if the command is unknown.
1430 """
1431 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
1432 print >>options.stdout, COMMAND_USAGE_TEXT[args[0]]
1433 else:
1434 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1435
1436
1437 def DoStatus(options, args):
1438 """Handle the status subcommand.
1439
1440 Raises:
1441 Error: if client isn't configured properly.
1442 """
1443 client = options.gclient.LoadCurrentConfig(options)
1444 if not client:
1445 raise Error("client not configured; see 'gclient config'")
1446 if options.verbose:
1447 # Print out the .gclient file. This is longer than if we just printed the
1448 # client dict, but more legible, and it might contain helpful comments.
1449 print >>options.stdout, client.ConfigContent()
1450 options.verbose = True
1451 return client.RunOnDeps('status', args)
1452
1453
1454 def DoUpdate(options, args):
1455 """Handle the update and sync subcommands.
1456
1457 Raises:
1458 Error: if client isn't configured properly.
1459 """
1460 client = options.gclient.LoadCurrentConfig(options)
1461
1462 if not client:
1463 raise Error("client not configured; see 'gclient config'")
1464
1465 if not options.head:
1466 solutions = client.GetVar('solutions')
1467 if solutions:
1468 for s in solutions:
1469 if s.get('safesync_url', ''):
1470 # rip through revisions and make sure we're not over-riding
1471 # something that was explicitly passed
1472 has_key = False
1473 for r in options.revisions:
1474 if r.split('@')[0] == s['name']:
1475 has_key = True
1476 break
1477
1478 if not has_key:
1479 handle = urllib.urlopen(s['safesync_url'])
1480 rev = handle.read().strip()
1481 handle.close()
1482 if len(rev):
1483 options.revisions.append(s['name']+'@'+rev)
1484
1485 if options.verbose:
1486 # Print out the .gclient file. This is longer than if we just printed the
1487 # client dict, but more legible, and it might contain helpful comments.
1488 print >>options.stdout, client.ConfigContent()
1489 return client.RunOnDeps('update', args)
1490
1491
1492 def DoDiff(options, args):
1493 """Handle the diff subcommand.
1494
1495 Raises:
1496 Error: if client isn't configured properly.
1497 """
1498 client = options.gclient.LoadCurrentConfig(options)
1499 if not client:
1500 raise Error("client not configured; see 'gclient config'")
1501 if options.verbose:
1502 # Print out the .gclient file. This is longer than if we just printed the
1503 # client dict, but more legible, and it might contain helpful comments.
1504 print >>options.stdout, client.ConfigContent()
1505 options.verbose = True
1506 return client.RunOnDeps('diff', args)
1507
1508
1509 def DoRevert(options, args):
1510 """Handle the revert subcommand.
1511
1512 Raises:
1513 Error: if client isn't configured properly.
1514 """
1515 client = options.gclient.LoadCurrentConfig(options)
1516 if not client:
1517 raise Error("client not configured; see 'gclient config'")
1518 return client.RunOnDeps('revert', args)
1519
1520
1521 def DoRunHooks(options, args):
1522 """Handle the runhooks subcommand.
1523
1524 Raises:
1525 Error: if client isn't configured properly.
1526 """
1527 client = options.gclient.LoadCurrentConfig(options)
1528 if not client:
1529 raise Error("client not configured; see 'gclient config'")
1530 if options.verbose:
1531 # Print out the .gclient file. This is longer than if we just printed the
1532 # client dict, but more legible, and it might contain helpful comments.
1533 print >>options.stdout, client.ConfigContent()
1534 return client.RunOnDeps('runhooks', args)
1535
1536
1537 def DoRevInfo(options, args):
1538 """Handle the revinfo subcommand.
1539
1540 Raises:
1541 Error: if client isn't configured properly.
1542 """
1543 client = options.gclient.LoadCurrentConfig(options)
1544 if not client:
1545 raise Error("client not configured; see 'gclient config'")
1546 client.PrintRevInfo()
1547
1548
1549 gclient_command_map = {
1550 "cleanup": DoCleanup,
1551 "config": DoConfig,
1552 "diff": DoDiff,
1553 "help": DoHelp,
1554 "status": DoStatus,
1555 "sync": DoUpdate,
1556 "update": DoUpdate,
1557 "revert": DoRevert,
1558 "runhooks": DoRunHooks,
1559 "revinfo" : DoRevInfo,
1560 }
1561
1562
1563 def DispatchCommand(command, options, args, command_map=None):
1564 """Dispatches the appropriate subcommand based on command line arguments."""
1565 if command_map is None:
1566 command_map = gclient_command_map
1567
1568 if command in command_map:
1569 return command_map[command](options, args)
1570 else:
1571 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1572
1573
1574 def Main(argv):
1575 """Parse command line arguments and dispatch command."""
1576
1577 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1578 version=__version__)
1579 option_parser.disable_interspersed_args()
1580 option_parser.add_option("", "--force", action="store_true", default=False,
1581 help=("(update/sync only) force update even "
1582 "for modules which haven't changed"))
1583 option_parser.add_option("", "--revision", action="append", dest="revisions",
1584 metavar="REV", default=[],
1585 help=("(update/sync only) sync to a specific "
1586 "revision, can be used multiple times for "
1587 "each solution, e.g. --revision=src@123, "
1588 "--revision=internal@32"))
1589 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1590 metavar="OS_LIST",
1591 help=("(update/sync only) sync deps for the "
1592 "specified (comma-separated) platform(s); "
1593 "'all' will sync all platforms"))
1594 option_parser.add_option("", "--spec", default=None,
1595 help=("(config only) create a gclient file "
1596 "containing the provided string"))
1597 option_parser.add_option("", "--verbose", action="store_true", default=False,
1598 help="produce additional output for diagnostics")
1599 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1600 default=False,
1601 help="Skip svn up whenever possible by requesting "
1602 "actual HEAD revision from the repository")
1603 option_parser.add_option("", "--head", action="store_true", default=False,
1604 help=("skips any safesync_urls specified in "
1605 "configured solutions"))
1606
1607 if len(argv) < 2:
1608 # Users don't need to be told to use the 'help' command.
1609 option_parser.print_help()
1610 return 1
1611 # Add manual support for --version as first argument.
1612 if argv[1] == '--version':
1613 option_parser.print_version()
1614 return 0
1615
1616 # Add manual support for --help as first argument.
1617 if argv[1] == '--help':
1618 argv[1] = 'help'
1619
1620 command = argv[1]
1621 options, args = option_parser.parse_args(argv[2:])
1622
1623 if len(argv) < 3 and command == "help":
1624 option_parser.print_help()
1625 return 0
1626
1627 # Files used for configuration and state saving.
1628 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1629 options.entries_filename = ".gclient_entries"
1630 options.deps_file = "DEPS"
1631
1632 # These are overridded when testing. They are not externally visible.
1633 options.stdout = sys.stdout
1634 options.path_exists = os.path.exists
1635 options.gclient = GClient
1636 options.scm_wrapper = SCMWrapper
1637 options.platform = sys.platform
1638 return DispatchCommand(command, options, args)
1639
1640
1641 if "__main__" == __name__:
1642 try:
1643 result = Main(sys.argv)
1644 except Error, e:
1645 print "Error: %s" % str(e)
1646 result = 1
1647 sys.exit(result)
1648
1649 # vim: ts=2:sw=2:tw=80:et:
OLDNEW
« no previous file with comments | « depot_tools/gclient.bat ('k') | depot_tools/git-cl.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698