12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649 |
- #!/usr/bin/python
- #
- # Copyright 2008 Google Inc. All Rights Reserved.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """A wrapper script to manage a set of client modules in different SCM.
- This script is intended to be used to help basic management of client
- program sources residing in one or more Subversion modules, along with
- other modules it depends on, also in Subversion, but possibly on
- multiple respositories, making a wrapper system apparently necessary.
- Files
- .gclient : Current client configuration, written by 'config' command.
- Format is a Python script defining 'solutions', a list whose
- entries each are maps binding the strings "name" and "url"
- to strings specifying the name and location of the client
- module, as well as "custom_deps" to a map similar to the DEPS
- file below.
- .gclient_entries : A cache constructed by 'update' command. Format is a
- Python script defining 'entries', a list of the names
- of all modules in the client
- <module>/DEPS : Python script defining var 'deps' as a map from each requisite
- submodule name to a URL where it can be found (via one SCM)
- Hooks
- .gclient and DEPS files may optionally contain a list named "hooks" to
- allow custom actions to be performed based on files that have changed in the
- working copy as a result of a "sync"/"update" or "revert" operation. Hooks
- can also be run based on what files have been modified in the working copy
- with the "runhooks" operation. If any of these operation are run with
- --force, all known hooks will run regardless of the state of the working
- copy.
- Each item in a "hooks" list is a dict, containing these two keys:
- "pattern" The associated value is a string containing a regular
- expression. When a file whose pathname matches the expression
- is checked out, updated, or reverted, the hook's "action" will
- run.
- "action" A list describing a command to run along with its arguments, if
- any. An action command will run at most one time per gclient
- invocation, regardless of how many files matched the pattern.
- The action is executed in the same directory as the .gclient
- file. If the first item in the list is the string "python",
- the current Python interpreter (sys.executable) will be used
- to run the command.
- Example:
- hooks = [
- { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
- "action": ["python", "image_indexer.py", "--all"]},
- ]
- """
- __author__ = "darinf@gmail.com (Darin Fisher)"
- __version__ = "0.3.1"
- import errno
- import optparse
- import os
- import re
- import stat
- import subprocess
- import sys
- import time
- import urlparse
- import xml.dom.minidom
- import urllib
- def getText(nodelist):
- """
- Return the concatenated text for the children of a list of DOM nodes.
- """
- rc = []
- for node in nodelist:
- if node.nodeType == node.TEXT_NODE:
- rc.append(node.data)
- else:
- rc.append(getText(node.childNodes))
- return ''.join(rc)
- SVN_COMMAND = "svn"
- # default help text
- DEFAULT_USAGE_TEXT = (
- """usage: %prog <subcommand> [options] [--] [svn options/args...]
- a wrapper for managing a set of client modules in svn.
- Version """ + __version__ + """
- subcommands:
- cleanup
- config
- diff
- revert
- status
- sync
- update
- runhooks
- revinfo
- Options and extra arguments can be passed to invoked svn commands by
- appending them to the command line. Note that if the first such
- appended option starts with a dash (-) then the options must be
- preceded by -- to distinguish them from gclient options.
- For additional help on a subcommand or examples of usage, try
- %prog help <subcommand>
- %prog help files
- """)
- GENERIC_UPDATE_USAGE_TEXT = (
- """Perform a checkout/update of the modules specified by the gclient
- configuration; see 'help config'. Unless --revision is specified,
- then the latest revision of the root solutions is checked out, with
- dependent submodule versions updated according to DEPS files.
- If --revision is specified, then the given revision is used in place
- of the latest, either for a single solution or for all solutions.
- Unless the --force option is provided, solutions and modules whose
- local revision matches the one to update (i.e., they have not changed
- in the repository) are *not* modified.
- This a synonym for 'gclient %(alias)s'
- usage: gclient %(cmd)s [options] [--] [svn update options/args]
- Valid options:
- --force : force update even for unchanged modules
- --revision REV : update/checkout all solutions with specified revision
- --revision SOLUTION@REV : update given solution to specified revision
- --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
- --verbose : output additional diagnostics
- Examples:
- gclient %(cmd)s
- update files from SVN according to current configuration,
- *for modules which have changed since last update or sync*
- gclient %(cmd)s --force
- update files from SVN according to current configuration, for
- all modules (useful for recovering files deleted from local copy)
- """)
- COMMAND_USAGE_TEXT = {
- "cleanup":
- """Clean up all working copies, using 'svn cleanup' for each module.
- Additional options and args may be passed to 'svn cleanup'.
- usage: cleanup [options] [--] [svn cleanup args/options]
- Valid options:
- --verbose : output additional diagnostics
- """,
- "config": """Create a .gclient file in the current directory; this
- specifies the configuration for further commands. After update/sync,
- top-level DEPS files in each module are read to determine dependent
- modules to operate on as well. If optional [url] parameter is
- provided, then configuration is read from a specified Subversion server
- URL. Otherwise, a --spec option must be provided.
- usage: config [option | url] [safesync url]
- Valid options:
- --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
- *Note that due to Cygwin/Python brokenness, it
- probably can't contain any newlines.*
- Examples:
- gclient config https://gclient.googlecode.com/svn/trunk/gclient
- configure a new client to check out gclient.py tool sources
- gclient config --spec='solutions=[{"name":"gclient","""
- '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
- '"custom_deps":{}}]',
- "diff": """Display the differences between two revisions of modules.
- (Does 'svn diff' for each checked out module and dependences.)
- Additional args and options to 'svn diff' can be passed after
- gclient options.
- usage: diff [options] [--] [svn args/options]
- Valid options:
- --verbose : output additional diagnostics
- Examples:
- gclient diff
- simple 'svn diff' for configured client and dependences
- gclient diff -- -x -b
- use 'svn diff -x -b' to suppress whitespace-only differences
- gclient diff -- -r HEAD -x -b
- diff versus the latest version of each module
- """,
- "revert":
- """Revert every file in every managed directory in the client view.
- usage: revert
- """,
- "status":
- """Show the status of client and dependent modules, using 'svn diff'
- for each module. Additional options and args may be passed to 'svn diff'.
- usage: status [options] [--] [svn diff args/options]
- Valid options:
- --verbose : output additional diagnostics
- """,
- "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
- "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
- "help": """Describe the usage of this program or its subcommands.
- usage: help [options] [subcommand]
- Valid options:
- --verbose : output additional diagnostics
- """,
- "runhooks":
- """Runs hooks for files that have been modified in the local working copy,
- according to 'svn status'.
- usage: runhooks [options]
- Valid options:
- --force : runs all known hooks, regardless of the working
- copy status
- --verbose : output additional diagnostics
- """,
- "revinfo":
- """Outputs source path, server URL and revision information for every
- dependency in all solutions (no local checkout required).
- usage: revinfo [options]
- """,
- }
- # parameterized by (solution_name, solution_url, safesync_url)
- DEFAULT_CLIENT_FILE_TEXT = (
- """
- # An element of this array (a \"solution\") describes a repository directory
- # that will be checked out into your working copy. Each solution may
- # optionally define additional dependencies (via its DEPS file) to be
- # checked out alongside the solution's directory. A solution may also
- # specify custom dependencies (via the \"custom_deps\" property) that
- # override or augment the dependencies specified by the DEPS file.
- # If a \"safesync_url\" is specified, it is assumed to reference the location of
- # a text file which contains nothing but the last known good SCM revision to
- # sync against. It is fetched if specified and used unless --head is passed
- solutions = [
- { \"name\" : \"%s\",
- \"url\" : \"%s\",
- \"custom_deps\" : {
- # To use the trunk of a component instead of what's in DEPS:
- #\"component\": \"https://svnserver/component/trunk/\",
- # To exclude a component from your working copy:
- #\"data/really_large_component\": None,
- },
- \"safesync_url\": \"%s\"
- }
- ]
- """)
- ## Generic utils
- class Error(Exception):
- """gclient exception class."""
- pass
- class PrintableObject(object):
- def __str__(self):
- output = ''
- for i in dir(self):
- if i.startswith('__'):
- continue
- output += '%s = %s\n' % (i, str(getattr(self, i, '')))
- return output
- def FileRead(filename):
- content = None
- f = open(filename, "rU")
- try:
- content = f.read()
- finally:
- f.close()
- return content
- def FileWrite(filename, content):
- f = open(filename, "w")
- try:
- f.write(content)
- finally:
- f.close()
- def RemoveDirectory(*path):
- """Recursively removes a directory, even if it's marked read-only.
- Remove the directory located at *path, if it exists.
- shutil.rmtree() doesn't work on Windows if any of the files or directories
- are read-only, which svn repositories and some .svn files are. We need to
- be able to force the files to be writable (i.e., deletable) as we traverse
- the tree.
- Even with all this, Windows still sometimes fails to delete a file, citing
- a permission error (maybe something to do with antivirus scans or disk
- indexing). The best suggestion any of the user forums had was to wait a
- bit and try again, so we do that too. It's hand-waving, but sometimes it
- works. :/
- On POSIX systems, things are a little bit simpler. The modes of the files
- to be deleted doesn't matter, only the modes of the directories containing
- them are significant. As the directory tree is traversed, each directory
- has its mode set appropriately before descending into it. This should
- result in the entire tree being removed, with the possible exception of
- *path itself, because nothing attempts to change the mode of its parent.
- Doing so would be hazardous, as it's not a directory slated for removal.
- In the ordinary case, this is not a problem: for our purposes, the user
- will never lack write permission on *path's parent.
- """
- file_path = os.path.join(*path)
- if not os.path.exists(file_path):
- return
- if os.path.islink(file_path) or not os.path.isdir(file_path):
- raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
- has_win32api = False
- if sys.platform == 'win32':
- has_win32api = True
- # Some people don't have the APIs installed. In that case we'll do without.
- try:
- win32api = __import__('win32api')
- win32con = __import__('win32con')
- except ImportError:
- has_win32api = False
- else:
- # On POSIX systems, we need the x-bit set on the directory to access it,
- # the r-bit to see its contents, and the w-bit to remove files from it.
- # The actual modes of the files within the directory is irrelevant.
- os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
- for fn in os.listdir(file_path):
- fullpath = os.path.join(file_path, fn)
- # If fullpath is a symbolic link that points to a directory, isdir will
- # be True, but we don't want to descend into that as a directory, we just
- # want to remove the link. Check islink and treat links as ordinary files
- # would be treated regardless of what they reference.
- if os.path.islink(fullpath) or not os.path.isdir(fullpath):
- if sys.platform == 'win32':
- os.chmod(fullpath, stat.S_IWRITE)
- if has_win32api:
- win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
- try:
- os.remove(fullpath)
- except OSError, e:
- if e.errno != errno.EACCES or sys.platform != 'win32':
- raise
- print 'Failed to delete %s: trying again' % fullpath
- time.sleep(0.1)
- os.remove(fullpath)
- else:
- RemoveDirectory(fullpath)
- if sys.platform == 'win32':
- os.chmod(file_path, stat.S_IWRITE)
- if has_win32api:
- win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
- try:
- os.rmdir(file_path)
- except OSError, e:
- if e.errno != errno.EACCES or sys.platform != 'win32':
- raise
- print 'Failed to remove %s: trying again' % file_path
- time.sleep(0.1)
- os.rmdir(file_path)
- def SubprocessCall(command, in_directory, out, fail_status=None):
- """Runs command, a list, in directory in_directory.
- This function wraps SubprocessCallAndCapture, but does not perform the
- capturing functions. See that function for a more complete usage
- description.
- """
- # Call subprocess and capture nothing:
- SubprocessCallAndCapture(command, in_directory, out, fail_status)
- def SubprocessCallAndCapture(command, in_directory, out, fail_status=None,
- pattern=None, capture_list=None):
- """Runs command, a list, in directory in_directory.
- A message indicating what is being done, as well as the command's stdout,
- is printed to out.
- If a pattern is specified, any line in the output matching pattern will have
- its first match group appended to capture_list.
- If the command fails, as indicated by a nonzero exit status, gclient will
- exit with an exit status of fail_status. If fail_status is None (the
- default), gclient will raise an Error exception.
- """
- print >> out, ("\n________ running \'%s\' in \'%s\'"
- % (' '.join(command), in_directory))
- # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
- # executable, but shell=True makes subprocess on Linux fail when it's called
- # with a list because it only tries to execute the first item in the list.
- kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
- shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
- if pattern:
- compiled_pattern = re.compile(pattern)
- # Also, we need to forward stdout to prevent weird re-ordering of output.
- # This has to be done on a per byte basis to make sure it is not buffered:
- # normally buffering is done for each line, but if svn requests input, no
- # end-of-line character is output after the prompt and it would not show up.
- in_byte = kid.stdout.read(1)
- in_line = ""
- while in_byte:
- if in_byte != "\r":
- out.write(in_byte)
- in_line += in_byte
- if in_byte == "\n" and pattern:
- match = compiled_pattern.search(in_line[:-1])
- if match:
- capture_list.append(match.group(1))
- in_line = ""
- in_byte = kid.stdout.read(1)
- rv = kid.wait()
- if rv:
- msg = "failed to run command: %s" % " ".join(command)
- if fail_status != None:
- print >>sys.stderr, msg
- sys.exit(fail_status)
- raise Error(msg)
- def IsUsingGit(root, paths):
- """Returns True if we're using git to manage any of our checkouts.
- |entries| is a list of paths to check."""
- for path in paths:
- if os.path.exists(os.path.join(root, path, '.git')):
- return True
- return False
- # -----------------------------------------------------------------------------
- # SVN utils:
- def RunSVN(options, args, in_directory):
- """Runs svn, sending output to stdout.
- Args:
- args: A sequence of command line parameters to be passed to svn.
- in_directory: The directory where svn is to be run.
- Raises:
- Error: An error occurred while running the svn command.
- """
- c = [SVN_COMMAND]
- c.extend(args)
- SubprocessCall(c, in_directory, options.stdout)
- def CaptureSVN(options, args, in_directory):
- """Runs svn, capturing output sent to stdout as a string.
- Args:
- args: A sequence of command line parameters to be passed to svn.
- in_directory: The directory where svn is to be run.
- Returns:
- The output sent to stdout as a string.
- """
- c = [SVN_COMMAND]
- c.extend(args)
- # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
- # the svn.exe executable, but shell=True makes subprocess on Linux fail
- # when it's called with a list because it only tries to execute the
- # first string ("svn").
- return subprocess.Popen(c, cwd=in_directory, shell=(sys.platform == 'win32'),
- stdout=subprocess.PIPE).communicate()[0]
- def RunSVNAndGetFileList(options, args, in_directory, file_list):
- """Runs svn checkout, update, or status, output to stdout.
- The first item in args must be either "checkout", "update", or "status".
- svn's stdout is parsed to collect a list of files checked out or updated.
- These files are appended to file_list. svn's stdout is also printed to
- sys.stdout as in RunSVN.
- Args:
- args: A sequence of command line parameters to be passed to svn.
- in_directory: The directory where svn is to be run.
- Raises:
- Error: An error occurred while running the svn command.
- """
- command = [SVN_COMMAND]
- command.extend(args)
- # svn update and svn checkout use the same pattern: the first three columns
- # are for file status, property status, and lock status. This is followed
- # by two spaces, and then the path to the file.
- update_pattern = '^... (.*)$'
- # The first three columns of svn status are the same as for svn update and
- # svn checkout. The next three columns indicate addition-with-history,
- # switch, and remote lock status. This is followed by one space, and then
- # the path to the file.
- status_pattern = '^...... (.*)$'
- # args[0] must be a supported command. This will blow up if it's something
- # else, which is good. Note that the patterns are only effective when
- # these commands are used in their ordinary forms, the patterns are invalid
- # for "svn status --show-updates", for example.
- pattern = {
- 'checkout': update_pattern,
- 'status': status_pattern,
- 'update': update_pattern,
- }[args[0]]
- SubprocessCallAndCapture(command, in_directory, options.stdout,
- pattern=pattern, capture_list=file_list)
- def CaptureSVNInfo(options, relpath, in_directory):
- """Runs 'svn info' on an existing path.
- Args:
- relpath: The directory where the working copy resides relative to
- the directory given by in_directory.
- in_directory: The directory where svn is to be run.
- Returns:
- An object with fields corresponding to the output of 'svn info'
- """
- info = CaptureSVN(options, ["info", "--xml", relpath], in_directory)
- dom = xml.dom.minidom.parseString(info)
- # str() the getText() results because they may be returned as
- # Unicode, which interferes with the higher layers matching up
- # things in the deps dictionary.
- result = PrintableObject()
- result.root = str(getText(dom.getElementsByTagName('root')))
- result.url = str(getText(dom.getElementsByTagName('url')))
- result.uuid = str(getText(dom.getElementsByTagName('uuid')))
- result.revision = int(dom.getElementsByTagName('entry')[0].getAttribute(
- 'revision'))
- return result
- def CaptureSVNHeadRevision(options, url):
- """Get the head revision of a SVN repository.
- Returns:
- Int head revision
- """
- info = CaptureSVN(options, ["info", "--xml", url], os.getcwd())
- dom = xml.dom.minidom.parseString(info)
- return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
- class FileStatus:
- def __init__(self, path, text_status, props, locked, history, switched,
- repo_locked, out_of_date):
- self.path = path.strip()
- self.text_status = text_status
- self.props = props
- self.locked = locked
- self.history = history
- self.switched = switched
- self.repo_locked = repo_locked
- self.out_of_date = out_of_date
- def __str__(self):
- return (self.text_status + self.props + self.locked + self.history +
- self.switched + self.repo_locked + self.out_of_date +
- self.path)
- def CaptureSVNStatus(options, path):
- """Runs 'svn status' on an existing path.
- Args:
- path: The directory to run svn status.
- Returns:
- An array of FileStatus corresponding to the output of 'svn status'
- """
- info = CaptureSVN(options, ["status"], path)
- result = []
- if not info:
- return result
- for line in info.splitlines():
- if line:
- new_item = FileStatus(line[7:], line[0:1], line[1:2], line[2:3],
- line[3:4], line[4:5], line[5:6], line[6:7])
- result.append(new_item)
- return result
- ### SCM abstraction layer
- class SCMWrapper(object):
- """Add necessary glue between all the supported SCM.
- This is the abstraction layer to bind to different SCM. Since currently only
- subversion is supported, a lot of subersionism remains. This can be sorted out
- once another SCM is supported."""
- def __init__(self, url=None, root_dir=None, relpath=None,
- scm_name='svn'):
- # TODO(maruel): Deduce the SCM from the url.
- self.scm_name = scm_name
- self.url = url
- self._root_dir = root_dir
- if self._root_dir:
- self._root_dir = self._root_dir.replace('/', os.sep).strip()
- self.relpath = relpath
- if self.relpath:
- self.relpath = self.relpath.replace('/', os.sep).strip()
- def FullUrlForRelativeUrl(self, url):
- # Find the forth '/' and strip from there. A bit hackish.
- return '/'.join(self.url.split('/')[:4]) + url
- def RunCommand(self, command, options, args, file_list=None):
- # file_list will have all files that are modified appended to it.
- if file_list == None:
- file_list = []
- commands = {
- 'cleanup': self.cleanup,
- 'update': self.update,
- 'revert': self.revert,
- 'status': self.status,
- 'diff': self.diff,
- 'runhooks': self.status,
- }
- if not command in commands:
- raise Error('Unknown command %s' % command)
- return commands[command](options, args, file_list)
- def cleanup(self, options, args, file_list):
- """Cleanup working copy."""
- command = ['cleanup']
- command.extend(args)
- RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
- def diff(self, options, args, file_list):
- # NOTE: This function does not currently modify file_list.
- command = ['diff']
- command.extend(args)
- RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
- def update(self, options, args, file_list):
- """Runs SCM to update or transparently checkout the working copy.
- All updated files will be appended to file_list.
- Raises:
- Error: if can't get URL for relative path.
- """
- # Only update if git is not controlling the directory.
- git_path = os.path.join(self._root_dir, self.relpath, '.git')
- if options.path_exists(git_path):
- print >> options.stdout, (
- "________ found .git directory; skipping %s" % self.relpath)
- return
- if args:
- raise Error("Unsupported argument(s): %s" % ",".join(args))
- url = self.url
- components = url.split("@")
- revision = None
- forced_revision = False
- if options.revision:
- # Override the revision number.
- url = '%s@%s' % (components[0], str(options.revision))
- revision = int(options.revision)
- forced_revision = True
- elif len(components) == 2:
- revision = int(components[1])
- forced_revision = True
- rev_str = ""
- if revision:
- rev_str = ' at %d' % revision
- if not options.path_exists(os.path.join(self._root_dir, self.relpath)):
- # We need to checkout.
- command = ['checkout', url, os.path.join(self._root_dir, self.relpath)]
- RunSVNAndGetFileList(options, command, self._root_dir, file_list)
- # Get the existing scm url and the revision number of the current checkout.
- from_info = CaptureSVNInfo(options,
- os.path.join(self._root_dir, self.relpath, '.'),
- '.')
- if options.manually_grab_svn_rev:
- # Retrieve the current HEAD version because svn is slow at null updates.
- if not revision:
- from_info_live = CaptureSVNInfo(options, from_info.url, '.')
- revision = int(from_info_live.revision)
- rev_str = ' at %d' % revision
- if from_info.url != components[0]:
- to_info = CaptureSVNInfo(options, url, '.')
- if from_info.root != to_info.root:
- # We have different roots, so check if we can switch --relocate.
- # Subversion only permits this if the repository UUIDs match.
- if from_info.uuid != to_info.uuid:
- raise Error("Can't switch the checkout to %s; UUID don't match" % url)
- # Perform the switch --relocate, then rewrite the from_url
- # to reflect where we "are now." (This is the same way that
- # Subversion itself handles the metadata when switch --relocate
- # is used.) This makes the checks below for whether we
- # can update to a revision or have to switch to a different
- # branch work as expected.
- # TODO(maruel): TEST ME !
- command = ["switch", "--relocate", from_info.root, to_info.root,
- self.relpath]
- RunSVN(options, command, self._root_dir)
- from_info.url = from_info.url.replace(from_info.root, to_info.root)
- # If the provided url has a revision number that matches the revision
- # number of the existing directory, then we don't need to bother updating.
- if not options.force and from_info.revision == revision:
- if options.verbose or not forced_revision:
- print >>options.stdout, ("\n_____ %s%s" % (
- self.relpath, rev_str))
- return
- command = ["update", os.path.join(self._root_dir, self.relpath)]
- if revision:
- command.extend(['--revision', str(revision)])
- RunSVNAndGetFileList(options, command, self._root_dir, file_list)
- def revert(self, options, args, file_list):
- """Reverts local modifications. Subversion specific.
- All reverted files will be appended to file_list, even if Subversion
- doesn't know about them.
- """
- path = os.path.join(self._root_dir, self.relpath)
- if not os.path.isdir(path):
- # We can't revert path that doesn't exist.
- # TODO(maruel): Should we update instead?
- if options.verbose:
- print >>options.stdout, ("\n_____ %s is missing, can't revert" %
- self.relpath)
- return
- files = CaptureSVNStatus(options, path)
- # Batch the command.
- files_to_revert = []
- for file in files:
- file_path = os.path.join(path, file.path)
- print >>options.stdout, file_path
- # Unversioned file or unexpected unversioned file.
- if file.text_status in ('?', '~'):
- # Remove extraneous file. Also remove unexpected unversioned
- # directories. svn won't touch them but we want to delete these.
- file_list.append(file_path)
- try:
- os.remove(file_path)
- except EnvironmentError:
- RemoveDirectory(file_path)
- if file.text_status != '?':
- # For any other status, svn revert will work.
- file_list.append(file_path)
- files_to_revert.append(file.path)
- # Revert them all at once.
- if files_to_revert:
- accumulated_paths = []
- accumulated_length = 0
- command = ['revert']
- for p in files_to_revert:
- # Some shell have issues with command lines too long.
- if accumulated_length and accumulated_length + len(p) > 3072:
- RunSVN(options, command + accumulated_paths,
- os.path.join(self._root_dir, self.relpath))
- accumulated_paths = []
- accumulated_length = 0
- else:
- accumulated_paths.append(p)
- accumulated_length += len(p)
- if accumulated_paths:
- RunSVN(options, command + accumulated_paths,
- os.path.join(self._root_dir, self.relpath))
- def status(self, options, args, file_list):
- """Display status information."""
- command = ['status']
- command.extend(args)
- RunSVNAndGetFileList(options, command,
- os.path.join(self._root_dir, self.relpath), file_list)
- ## GClient implementation.
- class GClient(object):
- """Object that represent a gclient checkout."""
- supported_commands = [
- 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks'
- ]
- def __init__(self, root_dir, options):
- self._root_dir = root_dir
- self._options = options
- self._config_content = None
- self._config_dict = {}
- self._deps_hooks = []
- def SetConfig(self, content):
- self._config_dict = {}
- self._config_content = content
- exec(content, self._config_dict)
- def SaveConfig(self):
- FileWrite(os.path.join(self._root_dir, self._options.config_filename),
- self._config_content)
- def _LoadConfig(self):
- client_source = FileRead(os.path.join(self._root_dir,
- self._options.config_filename))
- self.SetConfig(client_source)
- def ConfigContent(self):
- return self._config_content
- def GetVar(self, key, default=None):
- return self._config_dict.get(key, default)
- @staticmethod
- def LoadCurrentConfig(options, from_dir=None):
- """Searches for and loads a .gclient file relative to the current working
- dir.
- Returns:
- A dict representing the contents of the .gclient file or an empty dict if
- the .gclient file doesn't exist.
- """
- if not from_dir:
- from_dir = os.curdir
- path = os.path.realpath(from_dir)
- while not options.path_exists(os.path.join(path, options.config_filename)):
- next = os.path.split(path)
- if not next[1]:
- return None
- path = next[0]
- client = options.gclient(path, options)
- client._LoadConfig()
- return client
- def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
- self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
- solution_name, solution_url, safesync_url
- ))
- def _SaveEntries(self, entries):
- """Creates a .gclient_entries file to record the list of unique checkouts.
- The .gclient_entries file lives in the same directory as .gclient.
- Args:
- entries: A sequence of solution names.
- """
- text = "entries = [\n"
- for entry in entries:
- text += " \"%s\",\n" % entry
- text += "]\n"
- FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
- text)
- def _ReadEntries(self):
- """Read the .gclient_entries file for the given client.
- Args:
- client: The client for which the entries file should be read.
- Returns:
- A sequence of solution names, which will be empty if there is the
- entries file hasn't been created yet.
- """
- scope = {}
- filename = os.path.join(self._root_dir, self._options.entries_filename)
- if not self._options.path_exists(filename):
- return []
- exec(FileRead(filename), scope)
- return scope["entries"]
- class FromImpl:
- """Used to implement the From syntax."""
- def __init__(self, module_name):
- self.module_name = module_name
- def __str__(self):
- return 'From("%s")' % self.module_name
- class _VarImpl:
- def __init__(self, custom_vars, local_scope):
- self._custom_vars = custom_vars
- self._local_scope = local_scope
- def Lookup(self, var_name):
- """Implements the Var syntax."""
- if var_name in self._custom_vars:
- return self._custom_vars[var_name]
- elif var_name in self._local_scope.get("vars", {}):
- return self._local_scope["vars"][var_name]
- raise Error("Var is not defined: %s" % var_name)
- def _ParseSolutionDeps(self, solution_name, solution_deps_content,
- custom_vars):
- """Parses the DEPS file for the specified solution.
- Args:
- solution_name: The name of the solution to query.
- solution_deps_content: Content of the DEPS file for the solution
- custom_vars: A dict of vars to override any vars defined in the DEPS file.
- Returns:
- A dict mapping module names (as relative paths) to URLs or an empty
- dict if the solution does not have a DEPS file.
- """
- # Skip empty
- if not solution_deps_content:
- return {}
- # Eval the content
- local_scope = {}
- var = self._VarImpl(custom_vars, local_scope)
- global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
- exec(solution_deps_content, global_scope, local_scope)
- deps = local_scope.get("deps", {})
- # load os specific dependencies if defined. these dependencies may
- # override or extend the values defined by the 'deps' member.
- if "deps_os" in local_scope:
- deps_os_choices = {
- "win32": "win",
- "win": "win",
- "cygwin": "win",
- "darwin": "mac",
- "mac": "mac",
- "unix": "unix",
- "linux": "unix",
- "linux2": "unix",
- }
- if self._options.deps_os is not None:
- deps_to_include = self._options.deps_os.split(",")
- if "all" in deps_to_include:
- deps_to_include = deps_os_choices.values()
- else:
- deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
- deps_to_include = set(deps_to_include)
- for deps_os_key in deps_to_include:
- os_deps = local_scope["deps_os"].get(deps_os_key, {})
- if len(deps_to_include) > 1:
- # Ignore any overrides when including deps for more than one
- # platform, so we collect the broadest set of dependencies available.
- # We may end up with the wrong revision of something for our
- # platform, but this is the best we can do.
- deps.update([x for x in os_deps.items() if not x[0] in deps])
- else:
- deps.update(os_deps)
- if 'hooks' in local_scope:
- self._deps_hooks.extend(local_scope['hooks'])
- # If use_relative_paths is set in the DEPS file, regenerate
- # the dictionary using paths relative to the directory containing
- # the DEPS file.
- if local_scope.get('use_relative_paths'):
- rel_deps = {}
- for d, url in deps.items():
- # normpath is required to allow DEPS to use .. in their
- # dependency local path.
- rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
- return rel_deps
- else:
- return deps
- def _ParseAllDeps(self, solution_urls, solution_deps_content):
- """Parse the complete list of dependencies for the client.
- Args:
- solution_urls: A dict mapping module names (as relative paths) to URLs
- corresponding to the solutions specified by the client. This parameter
- is passed as an optimization.
- solution_deps_content: A dict mapping module names to the content
- of their DEPS files
- Returns:
- A dict mapping module names (as relative paths) to URLs corresponding
- to the entire set of dependencies to checkout for the given client.
- Raises:
- Error: If a dependency conflicts with another dependency or of a solution.
- """
- deps = {}
- for solution in self.GetVar("solutions"):
- custom_vars = solution.get("custom_vars", {})
- solution_deps = self._ParseSolutionDeps(
- solution["name"],
- solution_deps_content[solution["name"]],
- custom_vars)
- # If a line is in custom_deps, but not in the solution, we want to append
- # this line to the solution.
- if "custom_deps" in solution:
- for d in solution["custom_deps"]:
- if d not in solution_deps:
- solution_deps[d] = solution["custom_deps"][d]
- for d in solution_deps:
- if "custom_deps" in solution and d in solution["custom_deps"]:
- # Dependency is overriden.
- url = solution["custom_deps"][d]
- if url is None:
- continue
- else:
- url = solution_deps[d]
- # if we have a From reference dependent on another solution, then
- # just skip the From reference. When we pull deps for the solution,
- # we will take care of this dependency.
- #
- # If multiple solutions all have the same From reference, then we
- # should only add one to our list of dependencies.
- if type(url) != str:
- if url.module_name in solution_urls:
- # Already parsed.
- continue
- if d in deps and type(deps[d]) != str:
- if url.module_name == deps[d].module_name:
- continue
- else:
- parsed_url = urlparse.urlparse(url)
- scheme = parsed_url[0]
- if not scheme:
- # A relative url. Fetch the real base.
- path = parsed_url[2]
- if path[0] != "/":
- raise Error(
- "relative DEPS entry \"%s\" must begin with a slash" % d)
- # Create a scm just to query the full url.
- scm = self._options.scm_wrapper(solution["url"], self._root_dir,
- None)
- url = scm.FullUrlForRelativeUrl(url)
- if d in deps and deps[d] != url:
- raise Error(
- "Solutions have conflicting versions of dependency \"%s\"" % d)
- if d in solution_urls and solution_urls[d] != url:
- raise Error(
- "Dependency \"%s\" conflicts with specified solution" % d)
- # Grab the dependency.
- deps[d] = url
- return deps
- def _RunHookAction(self, hook_dict):
- """Runs the action from a single hook.
- """
- command = hook_dict['action'][:]
- if command[0] == 'python':
- # If the hook specified "python" as the first item, the action is a
- # Python script. Run it by starting a new copy of the same
- # interpreter.
- command[0] = sys.executable
- # Use a discrete exit status code of 2 to indicate that a hook action
- # failed. Users of this script may wish to treat hook action failures
- # differently from VC failures.
- SubprocessCall(command, self._root_dir, self._options.stdout,
- fail_status=2)
- def _RunHooks(self, command, file_list, is_using_git):
- """Evaluates all hooks, running actions as needed.
- """
- # Hooks only run for these command types.
- if not command in ('update', 'revert', 'runhooks'):
- return
- # Get any hooks from the .gclient file.
- hooks = self.GetVar("hooks", [])
- # Add any hooks found in DEPS files.
- hooks.extend(self._deps_hooks)
- # If "--force" was specified, run all hooks regardless of what files have
- # changed. If the user is using git, then we don't know what files have
- # changed so we always run all hooks.
- if self._options.force or is_using_git:
- for hook_dict in hooks:
- self._RunHookAction(hook_dict)
- return
- # Run hooks on the basis of whether the files from the gclient operation
- # match each hook's pattern.
- for hook_dict in hooks:
- pattern = re.compile(hook_dict['pattern'])
- for file in file_list:
- if not pattern.search(file):
- continue
- self._RunHookAction(hook_dict)
- # The hook's action only runs once. Don't bother looking for any
- # more matches.
- break
- def RunOnDeps(self, command, args):
- """Runs a command on each dependency in a client and its dependencies.
- The module's dependencies are specified in its top-level DEPS files.
- Args:
- command: The command to use (e.g., 'status' or 'diff')
- args: list of str - extra arguments to add to the command line.
- Raises:
- Error: If the client has conflicting entries.
- """
- if not command in self.supported_commands:
- raise Error("'%s' is an unsupported command" % command)
- # Check for revision overrides.
- revision_overrides = {}
- for revision in self._options.revisions:
- if revision.find("@") == -1:
- raise Error(
- "Specify the full dependency when specifying a revision number.")
- revision_elem = revision.split("@")
- # Disallow conflicting revs
- if revision_overrides.has_key(revision_elem[0]) and \
- revision_overrides[revision_elem[0]] != revision_elem[1]:
- raise Error(
- "Conflicting revision numbers specified.")
- revision_overrides[revision_elem[0]] = revision_elem[1]
- solutions = self.GetVar("solutions")
- if not solutions:
- raise Error("No solution specified")
- # When running runhooks --force, there's no need to consult the SCM.
- # All known hooks are expected to run unconditionally regardless of working
- # copy state, so skip the SCM status check.
- run_scm = not (command == 'runhooks' and self._options.force)
- entries = {}
- entries_deps_content = {}
- file_list = []
- # Run on the base solutions first.
- for solution in solutions:
- name = solution["name"]
- if name in entries:
- raise Error("solution %s specified more than once" % name)
- url = solution["url"]
- entries[name] = url
- if run_scm:
- self._options.revision = revision_overrides.get(name)
- scm = self._options.scm_wrapper(url, self._root_dir, name)
- scm.RunCommand(command, self._options, args, file_list)
- self._options.revision = None
- try:
- deps_content = FileRead(os.path.join(self._root_dir, name,
- self._options.deps_file))
- except IOError, e:
- if e.errno != errno.ENOENT:
- raise
- deps_content = ""
- entries_deps_content[name] = deps_content
- # Process the dependencies next (sort alphanumerically to ensure that
- # containing directories get populated first and for readability)
- deps = self._ParseAllDeps(entries, entries_deps_content)
- deps_to_process = deps.keys()
- deps_to_process.sort()
- # First pass for direct dependencies.
- for d in deps_to_process:
- if type(deps[d]) == str:
- url = deps[d]
- entries[d] = url
- if run_scm:
- self._options.revision = revision_overrides.get(d)
- scm = self._options.scm_wrapper(url, self._root_dir, d)
- scm.RunCommand(command, self._options, args, file_list)
- self._options.revision = None
- # Second pass for inherited deps (via the From keyword)
- for d in deps_to_process:
- if type(deps[d]) != str:
- sub_deps = self._ParseSolutionDeps(
- deps[d].module_name,
- FileRead(os.path.join(self._root_dir,
- deps[d].module_name,
- self._options.deps_file)),
- {})
- url = sub_deps[d]
- entries[d] = url
- if run_scm:
- self._options.revision = revision_overrides.get(d)
- scm = self._options.scm_wrapper(url, self._root_dir, d)
- scm.RunCommand(command, self._options, args, file_list)
- self._options.revision = None
- is_using_git = IsUsingGit(self._root_dir, entries.keys())
- self._RunHooks(command, file_list, is_using_git)
- if command == 'update':
- # notify the user if there is an orphaned entry in their working copy.
- # TODO(darin): we should delete this directory manually if it doesn't
- # have any changes in it.
- prev_entries = self._ReadEntries()
- for entry in prev_entries:
- e_dir = os.path.join(self._root_dir, entry)
- if entry not in entries and self._options.path_exists(e_dir):
- if CaptureSVNStatus(self._options, e_dir):
- # There are modified files in this entry
- entries[entry] = None # Keep warning until removed.
- print >> self._options.stdout, (
- "\nWARNING: \"%s\" is no longer part of this client. "
- "It is recommended that you manually remove it.\n") % entry
- else:
- # Delete the entry
- print >> self._options.stdout, ("\n________ deleting \'%s\' " +
- "in \'%s\'") % (entry, self._root_dir)
- RemoveDirectory(e_dir)
- # record the current list of entries for next time
- self._SaveEntries(entries)
- def PrintRevInfo(self):
- """Output revision info mapping for the client and its dependencies. This
- allows the capture of a overall "revision" for the source tree that can
- be used to reproduce the same tree in the future. The actual output
- contains enough information (source paths, svn server urls and revisions)
- that it can be used either to generate external svn commands (without
- gclient) or as input to gclient's --rev option (with some massaging of
- the data).
- NOTE: Unlike RunOnDeps this does not require a local checkout and is run
- on the Pulse master. It MUST NOT execute hooks.
- Raises:
- Error: If the client has conflicting entries.
- """
- # Check for revision overrides.
- revision_overrides = {}
- for revision in self._options.revisions:
- if revision.find("@") < 0:
- raise Error(
- "Specify the full dependency when specifying a revision number.")
- revision_elem = revision.split("@")
- # Disallow conflicting revs
- if revision_overrides.has_key(revision_elem[0]) and \
- revision_overrides[revision_elem[0]] != revision_elem[1]:
- raise Error(
- "Conflicting revision numbers specified.")
- revision_overrides[revision_elem[0]] = revision_elem[1]
- solutions = self.GetVar("solutions")
- if not solutions:
- raise Error("No solution specified")
- entries = {}
- entries_deps_content = {}
- # Inner helper to generate base url and rev tuple (including honoring
- # |revision_overrides|)
- def GetURLAndRev(name, original_url):
- if original_url.find("@") < 0:
- if revision_overrides.has_key(name):
- return (original_url, int(revision_overrides[name]))
- else:
- # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
- return (original_url, CaptureSVNHeadRevision(self._options,
- original_url))
- else:
- url_components = original_url.split("@")
- if revision_overrides.has_key(name):
- return (url_components[0], int(revision_overrides[name]))
- else:
- return (url_components[0], int(url_components[1]))
- # Run on the base solutions first.
- for solution in solutions:
- name = solution["name"]
- if name in entries:
- raise Error("solution %s specified more than once" % name)
- (url, rev) = GetURLAndRev(name, solution["url"])
- entries[name] = "%s@%d" % (url, rev)
- # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
- entries_deps_content[name] = CaptureSVN(
- self._options,
- ["cat",
- "%s/%s@%d" % (url,
- self._options.deps_file,
- rev)],
- os.getcwd())
- # Process the dependencies next (sort alphanumerically to ensure that
- # containing directories get populated first and for readability)
- deps = self._ParseAllDeps(entries, entries_deps_content)
- deps_to_process = deps.keys()
- deps_to_process.sort()
- # First pass for direct dependencies.
- for d in deps_to_process:
- if type(deps[d]) == str:
- (url, rev) = GetURLAndRev(d, deps[d])
- entries[d] = "%s@%d" % (url, rev)
- # Second pass for inherited deps (via the From keyword)
- for d in deps_to_process:
- if type(deps[d]) != str:
- deps_parent_url = entries[deps[d].module_name]
- if deps_parent_url.find("@") < 0:
- raise Error("From %s missing revisioned url" % deps[d].module_name)
- deps_parent_url_components = deps_parent_url.split("@")
- # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
- deps_parent_content = CaptureSVN(
- self._options,
- ["cat",
- "%s/%s@%s" % (deps_parent_url_components[0],
- self._options.deps_file,
- deps_parent_url_components[1])],
- os.getcwd())
- sub_deps = self._ParseSolutionDeps(
- deps[d].module_name,
- FileRead(os.path.join(self._root_dir,
- deps[d].module_name,
- self._options.deps_file)),
- {})
- (url, rev) = GetURLAndRev(d, sub_deps[d])
- entries[d] = "%s@%d" % (url, rev)
- print ";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())])
- ## gclient commands.
- def DoCleanup(options, args):
- """Handle the cleanup subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- if options.verbose:
- # Print out the .gclient file. This is longer than if we just printed the
- # client dict, but more legible, and it might contain helpful comments.
- print >>options.stdout, client.ConfigContent()
- options.verbose = True
- return client.RunOnDeps('cleanup', args)
- def DoConfig(options, args):
- """Handle the config subcommand.
- Args:
- options: If options.spec set, a string providing contents of config file.
- args: The command line args. If spec is not set,
- then args[0] is a string URL to get for config file.
- Raises:
- Error: on usage error
- """
- if len(args) < 1 and not options.spec:
- raise Error("required argument missing; see 'gclient help config'")
- if options.path_exists(options.config_filename):
- raise Error("%s file already exists in the current directory" %
- options.config_filename)
- client = options.gclient('.', options)
- if options.spec:
- client.SetConfig(options.spec)
- else:
- # TODO(darin): it would be nice to be able to specify an alternate relpath
- # for the given URL.
- base_url = args[0]
- name = args[0].split("/")[-1]
- safesync_url = ""
- if len(args) > 1:
- safesync_url = args[1]
- client.SetDefaultConfig(name, base_url, safesync_url)
- client.SaveConfig()
- def DoHelp(options, args):
- """Handle the help subcommand giving help for another subcommand.
- Raises:
- Error: if the command is unknown.
- """
- if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
- print >>options.stdout, COMMAND_USAGE_TEXT[args[0]]
- else:
- raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
- def DoStatus(options, args):
- """Handle the status subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- if options.verbose:
- # Print out the .gclient file. This is longer than if we just printed the
- # client dict, but more legible, and it might contain helpful comments.
- print >>options.stdout, client.ConfigContent()
- options.verbose = True
- return client.RunOnDeps('status', args)
- def DoUpdate(options, args):
- """Handle the update and sync subcommands.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- if not options.head:
- solutions = client.GetVar('solutions')
- if solutions:
- for s in solutions:
- if s.get('safesync_url', ''):
- # rip through revisions and make sure we're not over-riding
- # something that was explicitly passed
- has_key = False
- for r in options.revisions:
- if r.split('@')[0] == s['name']:
- has_key = True
- break
- if not has_key:
- handle = urllib.urlopen(s['safesync_url'])
- rev = handle.read().strip()
- handle.close()
- if len(rev):
- options.revisions.append(s['name']+'@'+rev)
- if options.verbose:
- # Print out the .gclient file. This is longer than if we just printed the
- # client dict, but more legible, and it might contain helpful comments.
- print >>options.stdout, client.ConfigContent()
- return client.RunOnDeps('update', args)
- def DoDiff(options, args):
- """Handle the diff subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- if options.verbose:
- # Print out the .gclient file. This is longer than if we just printed the
- # client dict, but more legible, and it might contain helpful comments.
- print >>options.stdout, client.ConfigContent()
- options.verbose = True
- return client.RunOnDeps('diff', args)
- def DoRevert(options, args):
- """Handle the revert subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- return client.RunOnDeps('revert', args)
- def DoRunHooks(options, args):
- """Handle the runhooks subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- if options.verbose:
- # Print out the .gclient file. This is longer than if we just printed the
- # client dict, but more legible, and it might contain helpful comments.
- print >>options.stdout, client.ConfigContent()
- return client.RunOnDeps('runhooks', args)
- def DoRevInfo(options, args):
- """Handle the revinfo subcommand.
- Raises:
- Error: if client isn't configured properly.
- """
- client = options.gclient.LoadCurrentConfig(options)
- if not client:
- raise Error("client not configured; see 'gclient config'")
- client.PrintRevInfo()
- gclient_command_map = {
- "cleanup": DoCleanup,
- "config": DoConfig,
- "diff": DoDiff,
- "help": DoHelp,
- "status": DoStatus,
- "sync": DoUpdate,
- "update": DoUpdate,
- "revert": DoRevert,
- "runhooks": DoRunHooks,
- "revinfo" : DoRevInfo,
- }
- def DispatchCommand(command, options, args, command_map=None):
- """Dispatches the appropriate subcommand based on command line arguments."""
- if command_map is None:
- command_map = gclient_command_map
- if command in command_map:
- return command_map[command](options, args)
- else:
- raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
- def Main(argv):
- """Parse command line arguments and dispatch command."""
- option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
- version=__version__)
- option_parser.disable_interspersed_args()
- option_parser.add_option("", "--force", action="store_true", default=False,
- help=("(update/sync only) force update even "
- "for modules which haven't changed"))
- option_parser.add_option("", "--revision", action="append", dest="revisions",
- metavar="REV", default=[],
- help=("(update/sync only) sync to a specific "
- "revision, can be used multiple times for "
- "each solution, e.g. --revision=src@123, "
- "--revision=internal@32"))
- option_parser.add_option("", "--deps", default=None, dest="deps_os",
- metavar="OS_LIST",
- help=("(update/sync only) sync deps for the "
- "specified (comma-separated) platform(s); "
- "'all' will sync all platforms"))
- option_parser.add_option("", "--spec", default=None,
- help=("(config only) create a gclient file "
- "containing the provided string"))
- option_parser.add_option("", "--verbose", action="store_true", default=False,
- help="produce additional output for diagnostics")
- option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
- default=False,
- help="Skip svn up whenever possible by requesting "
- "actual HEAD revision from the repository")
- option_parser.add_option("", "--head", action="store_true", default=False,
- help=("skips any safesync_urls specified in "
- "configured solutions"))
- if len(argv) < 2:
- # Users don't need to be told to use the 'help' command.
- option_parser.print_help()
- return 1
- # Add manual support for --version as first argument.
- if argv[1] == '--version':
- option_parser.print_version()
- return 0
- # Add manual support for --help as first argument.
- if argv[1] == '--help':
- argv[1] = 'help'
- command = argv[1]
- options, args = option_parser.parse_args(argv[2:])
- if len(argv) < 3 and command == "help":
- option_parser.print_help()
- return 0
- # Files used for configuration and state saving.
- options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
- options.entries_filename = ".gclient_entries"
- options.deps_file = "DEPS"
- # These are overridded when testing. They are not externally visible.
- options.stdout = sys.stdout
- options.path_exists = os.path.exists
- options.gclient = GClient
- options.scm_wrapper = SCMWrapper
- options.platform = sys.platform
- return DispatchCommand(command, options, args)
- if "__main__" == __name__:
- try:
- result = Main(sys.argv)
- except Error, e:
- print "Error: %s" % str(e)
- result = 1
- sys.exit(result)
- # vim: ts=2:sw=2:tw=80:et:
|