diff --git a/gclient.py b/gclient.py index deb6d8d29..b1dc49a82 100644 --- a/gclient.py +++ b/gclient.py @@ -429,6 +429,29 @@ class GClient(object): def __str__(self): return 'From("%s")' % self.module_name + class FileImpl: + """Used to implement the File('') syntax which lets you sync a single file + from an SVN repo.""" + + def __init__(self, file_location): + self.file_location = file_location + + def __str__(self): + return 'File("%s")' % self.file_location + + def GetPath(self): + return os.path.split(self.file_location)[0] + + def GetFilename(self): + rev_tokens = self.file_location.split('@') + return os.path.split(rev_tokens[0])[1] + + def GetRevision(self): + rev_tokens = self.file_location.split('@') + if len(rev_tokens) > 1: + return rev_tokens[1] + return None + class _VarImpl: def __init__(self, custom_vars, local_scope): self._custom_vars = custom_vars @@ -461,7 +484,12 @@ class GClient(object): # Eval the content local_scope = {} var = self._VarImpl(custom_vars, local_scope) - global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}} + global_scope = { + "File": self.FileImpl, + "From": self.FromImpl, + "Var": var.Lookup, + "deps_os": {}, + } exec(solution_deps_content, global_scope, local_scope) deps = local_scope.get("deps", {}) @@ -560,14 +588,14 @@ class GClient(object): # # 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 isinstance(url, self.FromImpl): 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: + elif isinstance(url, str): parsed_url = urlparse.urlparse(url) scheme = parsed_url[0] if not scheme: @@ -728,12 +756,20 @@ class GClient(object): scm = gclient_scm.CreateSCM(url, self._root_dir, d) scm.RunCommand(command, self._options, args, file_list) self._options.revision = None + elif isinstance(deps[d], self.FileImpl): + file = deps[d] + self._options.revision = file.GetRevision() + if run_scm: + scm = gclient_scm.CreateSCM(file.GetPath(), self._root_dir, d) + scm.RunCommand("updatesingle", self._options, + args + [file.GetFilename()], file_list) + if command == 'update' and not self._options.verbose: pm.end() # Second pass for inherited deps (via the From keyword) for d in deps_to_process: - if type(deps[d]) != str: + if isinstance(deps[d], self.FromImpl): filename = os.path.join(self._root_dir, deps[d].module_name, self._options.deps_file) @@ -885,7 +921,7 @@ class GClient(object): # Second pass for inherited deps (via the From keyword) for d in deps_to_process: - if type(deps[d]) != str: + if isinstance(deps[d], self.FromImpl): deps_parent_url = entries[deps[d].module_name] if deps_parent_url.find("@") < 0: raise gclient_utils.Error("From %s missing revisioned url" % diff --git a/gclient_scm.py b/gclient_scm.py index 4363763e7..3a3bd1a7b 100644 --- a/gclient_scm.py +++ b/gclient_scm.py @@ -97,8 +97,8 @@ class SCMWrapper(object): if file_list is None: file_list = [] - commands = ['cleanup', 'export', 'update', 'revert', 'revinfo', - 'status', 'diff', 'pack', 'runhooks'] + commands = ['cleanup', 'export', 'update', 'updatesingle', 'revert', + 'revinfo', 'status', 'diff', 'pack', 'runhooks'] if not command in commands: raise gclient_utils.Error('Unknown command %s' % command) @@ -746,6 +746,20 @@ class SVNWrapper(SCMWrapper): command.extend(['--revision', str(revision)]) scm.SVN.RunAndGetFileList(options, command, self._root_dir, file_list) + def updatesingle(self, options, args, file_list): + checkout_path = os.path.join(self._root_dir, self.relpath) + filename = args.pop() + if not os.path.exists(checkout_path): + # Create an empty checkout and then update the one file we want. Future + # operations will only apply to the one file we checked out. + command = ["checkout", "--depth", "empty", self.url, checkout_path] + scm.SVN.Run(command, self._root_dir) + command = ["update", filename] + scm.SVN.RunAndGetFileList(options, command, checkout_path, file_list) + # After the initial checkout, we can use update as if it were any other + # dep. + self.update(options, args, file_list) + def revert(self, options, args, file_list): """Reverts local modifications. Subversion specific. diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py index 9771dc55b..46e553605 100755 --- a/tests/gclient_scm_test.py +++ b/tests/gclient_scm_test.py @@ -60,7 +60,8 @@ class SVNWrapperTestCase(BaseTestCase): members = [ 'FullUrlForRelativeUrl', 'RunCommand', 'cleanup', 'diff', 'export', 'pack', 'relpath', 'revert', - 'revinfo', 'runhooks', 'scm_name', 'status', 'update', 'url', + 'revinfo', 'runhooks', 'scm_name', 'status', 'update', + 'updatesingle', 'url', ] # If you add a member, be sure to add the relevant test! @@ -266,6 +267,61 @@ class SVNWrapperTestCase(BaseTestCase): relpath=self.relpath) scm.update(options, (), files_list) + def testUpdateSingleCheckout(self): + options = self.Options(verbose=True) + base_path = gclient_scm.os.path.join(self.root_dir, self.relpath) + file_info = { + 'URL': self.url, + 'Revision': 42, + } + # When checking out a single file, we issue an svn checkout and svn update. + gclient_scm.os.path.exists(base_path).AndReturn(False) + files_list = self.mox.CreateMockAnything() + gclient_scm.scm.SVN.Run( + ['checkout', '--depth', 'empty', self.url, base_path], self.root_dir) + gclient_scm.scm.SVN.RunAndGetFileList(options, ['update', 'DEPS'], + gclient_scm.os.path.join(self.root_dir, self.relpath), files_list) + + # Now we fall back on scm.update(). + gclient_scm.os.path.exists(gclient_scm.os.path.join(base_path, '.git') + ).AndReturn(False) + gclient_scm.os.path.exists(base_path).AndReturn(True) + gclient_scm.scm.SVN.CaptureInfo( + gclient_scm.os.path.join(base_path, "."), '.' + ).AndReturn(file_info) + gclient_scm.scm.SVN.CaptureInfo(file_info['URL'], '.').AndReturn(file_info) + print("\n_____ %s at 42" % self.relpath) + + self.mox.ReplayAll() + scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, + relpath=self.relpath) + scm.updatesingle(options, ['DEPS'], files_list) + + def testUpdateSingleUpdate(self): + options = self.Options(verbose=True) + base_path = gclient_scm.os.path.join(self.root_dir, self.relpath) + file_info = { + 'URL': self.url, + 'Revision': 42, + } + gclient_scm.os.path.exists(base_path).AndReturn(True) + + # Now we fall back on scm.update(). + files_list = self.mox.CreateMockAnything() + gclient_scm.os.path.exists(gclient_scm.os.path.join(base_path, '.git') + ).AndReturn(False) + gclient_scm.os.path.exists(base_path).AndReturn(True) + gclient_scm.scm.SVN.CaptureInfo( + gclient_scm.os.path.join(base_path, "."), '.' + ).AndReturn(file_info) + gclient_scm.scm.SVN.CaptureInfo(file_info['URL'], '.').AndReturn(file_info) + print("\n_____ %s at 42" % self.relpath) + + self.mox.ReplayAll() + scm = self._scm_wrapper(url=self.url, root_dir=self.root_dir, + relpath=self.relpath) + scm.updatesingle(options, ['DEPS'], files_list) + def testUpdateGit(self): options = self.Options(verbose=True) file_path = gclient_scm.os.path.join(self.root_dir, self.relpath, '.git') diff --git a/tests/gclient_test.py b/tests/gclient_test.py index df8c3ff0a..8a1fcf58f 100755 --- a/tests/gclient_test.py +++ b/tests/gclient_test.py @@ -213,9 +213,6 @@ class TestDoRunHooks(GenericCommandTestCase): class TestDoUpdate(GenericCommandTestCase): - def Options(self, verbose=False, *args, **kwargs): - return self.OptionsObject(self, verbose=verbose, *args, **kwargs) - def ReturnValue(self, command, function, return_value): options = self.Options() gclient.GClient.LoadCurrentConfig(options).AndReturn(gclient.GClient) @@ -302,7 +299,7 @@ class TestDoRevert(GenericCommandTestCase): class GClientClassTestCase(GclientTestCase): def testDir(self): members = [ - 'ConfigContent', 'FromImpl', 'GetVar', 'LoadCurrentConfig', + 'ConfigContent', 'FileImpl', 'FromImpl', 'GetVar', 'LoadCurrentConfig', 'RunOnDeps', 'SaveConfig', 'SetConfig', 'SetDefaultConfig', 'supported_commands', 'PrintRevInfo', ] @@ -1033,6 +1030,48 @@ deps = { # TODO(maruel): Test me! pass + def testFileImpl(self): + # Fake .gclient file. + name = "testFileImpl" + gclient_config = ( + "solutions = [ { 'name': '%s'," + "'url': '%s', } ]" % (name, self.url) + ) + # Fake DEPS file. + target = "chromium_deps" + deps_content = ( + "deps = {" + " '%s': File('%s/DEPS') }" % (target, self.url) + ) + + gclient.gclient_scm.CreateSCM(self.url, self.root_dir, name).AndReturn( + gclient.gclient_scm.CreateSCM) + options = self.Options() + gclient.gclient_scm.CreateSCM.RunCommand('update', options, self.args, []) + gclient.gclient_utils.FileRead( + gclient.os.path.join(self.root_dir, name, options.deps_file) + ).AndReturn(deps_content) + gclient.os.path.exists( + gclient.os.path.join(self.root_dir, name, '.git') + ).AndReturn(False) + gclient.os.path.exists( + gclient.os.path.join(self.root_dir, options.entries_filename) + ).AndReturn(False) + + # This is where gclient tries to do the initial checkout. + gclient.gclient_scm.CreateSCM(self.url, self.root_dir, target).AndReturn( + gclient.gclient_scm.CreateSCM) + gclient.gclient_scm.CreateSCM.RunCommand('updatesingle', options, + self.args + ["DEPS"], []) + gclient.gclient_utils.FileWrite( + gclient.os.path.join(self.root_dir, options.entries_filename), + "entries = \\\n{'%s': '%s'}\n" % (name, self.url)) + + self.mox.ReplayAll() + client = self._gclient_gclient(self.root_dir, options) + client.SetConfig(gclient_config) + client.RunOnDeps('update', self.args) + def test_PrintRevInfo(self): # TODO(aharper): no test yet for revinfo, lock it down once we've verified # implementation for Pulse plugin