diff --git a/gclient.py b/gclient.py index 407b9ad4e..506439630 100755 --- a/gclient.py +++ b/gclient.py @@ -73,6 +73,7 @@ __version__ = '0.7' import copy +import json import logging import optparse import os @@ -301,6 +302,9 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): # This is the scm used to checkout self.url. It may be used by dependencies # to get the datetime of the revision we checked out. self._used_scm = None + # The actual revision we ended up getting, or None if that information is + # unavailable + self._got_revision = None if not self.name and self.parent: raise gclient_utils.Error('Dependency without name') @@ -624,7 +628,8 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): command, options, parsed_url, self.parent.name, revision_overrides) self._used_scm = gclient_scm.CreateSCM( parsed_url, self.root.root_dir, self.name) - self._used_scm.RunCommand(command, options, args, file_list) + self._got_revision = self._used_scm.RunCommand(command, options, args, + file_list) if file_list: file_list = [os.path.join(self.name, f.strip()) for f in file_list] @@ -854,6 +859,11 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): """SCMWrapper instance for this dependency or None if not processed yet.""" return self._used_scm + @property + @gclient_utils.lockedmethod + def got_revision(self): + return self._got_revision + @property def file_list_and_children(self): result = list(self.file_list) @@ -1523,6 +1533,21 @@ def CMDstatus(parser, args): all modules (useful for recovering files deleted from local copy) gclient sync --revision src@31000 update src directory to r31000 + +JSON output format: +If the --output-json option is specified, the following document structure will +be emitted to the provided file. 'null' entries may occur for subprojects which +are present in the gclient solution, but were not processed (due to custom_deps, +os_deps, etc.) + +{ + "solutions" : { + "": { # is the posix-normalized path to the solution. + "revision": [||null], + "scm": ["svn"|"git"|null], + } + } +} """) def CMDsync(parser, args): """Checkout/update all modules.""" @@ -1573,6 +1598,9 @@ def CMDsync(parser, args): 'actual HEAD revision from the repository') parser.add_option('--upstream', action='store_true', help='Make repo state match upstream branch.') + parser.add_option('--output-json', + help='Output a json document to this path containing ' + 'summary information about the sync.') (options, args) = parser.parse_args(args) client = GClient.LoadCurrentConfig(options) @@ -1587,7 +1615,18 @@ def CMDsync(parser, args): # 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(client.config_content) - return client.RunOnDeps('update', args) + ret = client.RunOnDeps('update', args) + if options.output_json: + slns = {} + for d in client.subtree(True): + normed = d.name.replace('\\', '/').rstrip('/') + '/' + slns[normed] = { + 'revision': d.got_revision, + 'scm': d.used_scm.name if d.used_scm else None, + } + with open(options.output_json, 'wb') as f: + json.dump({'solutions': slns}, f) + return ret CMDupdate = CMDsync diff --git a/gclient_scm.py b/gclient_scm.py index acb31ef15..c9af57ce5 100644 --- a/gclient_scm.py +++ b/gclient_scm.py @@ -190,6 +190,7 @@ class GitFilter(object): class GitWrapper(SCMWrapper): """Wrapper for Git""" + name = 'git' cache_dir = None # If a given cache is used in a solution more than once, prevent multiple @@ -363,13 +364,13 @@ class GitWrapper(SCMWrapper): # Make the output a little prettier. It's nice to have some whitespace # between projects when cloning. print('') - return + return self._Capture(['rev-parse', '--verify', 'HEAD']) if not managed: self._UpdateBranchHeads(options, fetch=False) self.UpdateSubmoduleConfig() print ('________ unmanaged solution; skipping %s' % self.relpath) - return + return self._Capture(['rev-parse', '--verify', 'HEAD']) if not os.path.exists(os.path.join(self.checkout_path, '.git')): raise gclient_utils.Error('\n____ %s%s\n' @@ -406,7 +407,7 @@ class GitWrapper(SCMWrapper): self._PossiblySwitchCache(url, options) if return_early: - return + return self._Capture(['rev-parse', '--verify', 'HEAD']) cur_branch = self._GetCurrentBranch() @@ -595,6 +596,8 @@ class GitWrapper(SCMWrapper): print('\n_____ removing unversioned directory %s' % path) gclient_utils.rmtree(full_path) + return self._Capture(['rev-parse', '--verify', 'HEAD']) + def revert(self, options, _args, file_list): """Reverts local modifications. @@ -1088,6 +1091,7 @@ class GitWrapper(SCMWrapper): class SVNWrapper(SCMWrapper): """ Wrapper for SVN """ + name = 'svn' @staticmethod def BinaryExists(): @@ -1202,11 +1206,11 @@ class SVNWrapper(SCMWrapper): command = ['checkout', url, self.checkout_path] command = self._AddAdditionalUpdateFlags(command, options, revision) self._RunAndGetFileList(command, options, file_list, self._root_dir) - return + return self.Svnversion() if not managed: print ('________ unmanaged solution; skipping %s' % self.relpath) - return + return self.Svnversion() if 'URL' not in from_info: raise gclient_utils.Error( @@ -1294,7 +1298,7 @@ class SVNWrapper(SCMWrapper): command = ['checkout', url, self.checkout_path] command = self._AddAdditionalUpdateFlags(command, options, revision) self._RunAndGetFileList(command, options, file_list, self._root_dir) - return + return self.Svnversion() # 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. @@ -1316,6 +1320,7 @@ class SVNWrapper(SCMWrapper): and not os.path.islink(full_path)): print('\n_____ removing unversioned directory %s' % status[1]) gclient_utils.rmtree(full_path) + return self.Svnversion() def updatesingle(self, options, args, file_list): filename = args.pop() @@ -1442,6 +1447,11 @@ class SVNWrapper(SCMWrapper): gclient_utils.CheckCallAndFilterAndHeader(['svn'] + args, always=options.verbose, **kwargs) + def Svnversion(self): + """Runs the lowest checked out revision in the current project.""" + info = scm.SVN.CaptureLocalInfo([], os.path.join(self.checkout_path, '.')) + return info['Revision'] + def _RunAndGetFileList(self, args, options, file_list, cwd=None): """Runs a commands that goes to stdout and grabs the file listed.""" cwd = cwd or self.checkout_path diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py index 465d172fd..fa16be676 100755 --- a/tests/gclient_scm_test.py +++ b/tests/gclient_scm_test.py @@ -104,9 +104,11 @@ class SVNWrapperTestCase(BaseTestCase): 'GetCheckoutRoot', 'GetRevisionDate', 'GetUsableRev', + 'Svnversion', 'RunCommand', 'cleanup', 'diff', + 'name', 'nag_max', 'nag_timer', 'pack', @@ -198,6 +200,9 @@ class SVNWrapperTestCase(BaseTestCase): cwd=self.root_dir, file_list=files_list) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -228,6 +233,8 @@ class SVNWrapperTestCase(BaseTestCase): cwd=self.root_dir, file_list=files_list) gclient_scm.gclient_utils.rmtree(self.base_path) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -352,6 +359,8 @@ class SVNWrapperTestCase(BaseTestCase): ['checkout', self.url, self.base_path, '--force', '--ignore-externals'], cwd=self.root_dir, file_list=files_list) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -386,6 +395,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.scm.SVN.Capture(['--version', '--quiet'], None ).AndReturn('1.5.1') + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + additional_args = [] if options.manually_grab_svn_rev: additional_args = ['--revision', str(file_info['Revision'])] @@ -425,6 +437,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.scm.SVN._CaptureInfo([file_info['URL']], None ).AndReturn(file_info) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() files_list = [] scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, @@ -466,6 +481,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.os.path.islink(join(self.base_path, 'dir')).AndReturn(False) gclient_scm.gclient_utils.rmtree(join(self.base_path, 'dir')) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -515,6 +533,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.scm.SVN._CaptureInfo([file_info['URL']], None ).AndReturn(file_info) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -585,6 +606,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.scm.SVN._CaptureInfo([file_info['URL']], None ).AndReturn(file_info) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -618,6 +642,9 @@ class SVNWrapperTestCase(BaseTestCase): gclient_scm.scm.SVN._CaptureInfo([file_info['URL']], None ).AndReturn(file_info) + gclient_scm.scm.SVN._CaptureInfo([], self.base_path+'/.' + ).AndReturn({'Revision': 100}) + self.mox.ReplayAll() scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, relpath=self.relpath) @@ -799,6 +826,7 @@ class ManagedGitWrapperTestCase(BaseGitWrapperTestCase): 'cache_locks', 'cleanup', 'diff', + 'name', 'nag_max', 'nag_timer', 'pack',