diff --git a/gclient.py b/gclient.py index 725642e47..b7d8e77e7 100644 --- a/gclient.py +++ b/gclient.py @@ -139,7 +139,7 @@ class GClientKeywords(object): class Dependency(GClientKeywords, gclient_utils.WorkItem): """Object that represents a dependency checkout.""" - def __init__(self, parent, name, url, safesync_url, custom_deps, + def __init__(self, parent, name, url, safesync_url, managed, custom_deps, custom_vars, deps_file, should_process): # Warning: this function can be called from any thread. Both # self.dependencies and self.requirements are read and modified from @@ -157,6 +157,13 @@ class Dependency(GClientKeywords, gclient_utils.WorkItem): self.url = url # These are only set in .gclient and not in DEPS files. + # 'managed' determines whether or not this dependency is synced/updated by + # gclient after gclient checks it out initially. The difference between + # 'managed' and 'should_process' (defined below) is that the user specifies + # 'managed' via the --unmanaged command-line flag or a .gclient config, + # where 'should_process' is dynamically set by gclient if it goes over its + # recursion limit and controls gclient's behavior so it does not misbehave. + self.managed = managed self.custom_vars = custom_vars or {} self.custom_deps = custom_deps or {} self.deps_hooks = [] @@ -402,7 +409,7 @@ class Dependency(GClientKeywords, gclient_utils.WorkItem): 'Dependency %s specified more than once:\n %s\nvs\n %s' % (name, tree[name].hierarchy(), self.hierarchy())) self.dependencies.append(Dependency(self, name, url, None, None, None, - self.deps_file, should_process)) + None, self.deps_file, should_process)) logging.debug('Loaded: %s' % str(self)) # Arguments number differs from overridden method @@ -662,6 +669,7 @@ solutions = [ { "name" : "%(solution_name)s", "url" : "%(solution_url)s", "deps_file" : "%(deps_file)s", + "managed" : %(managed)s, "custom_deps" : { }, "safesync_url": "%(safesync_url)s", @@ -673,6 +681,7 @@ solutions = [ { "name" : "%(solution_name)s", "url" : "%(solution_url)s", "deps_file" : "%(deps_file)s", + "managed" : %(managed)s, "custom_deps" : { %(solution_deps)s }, "safesync_url": "%(safesync_url)s", @@ -689,8 +698,8 @@ solutions = [ # Do not change previous behavior. Only solution level and immediate DEPS # are processed. self._recursion_limit = 2 - Dependency.__init__(self, None, None, None, None, None, None, 'unused', - True) + Dependency.__init__(self, None, None, None, None, True, None, None, + 'unused', True) self._options = options if options.deps_os: enforced_os = options.deps_os.split(',') @@ -719,6 +728,7 @@ solutions = [ self.dependencies.append(Dependency( self, s['name'], s['url'], s.get('safesync_url', None), + s.get('managed', True), s.get('custom_deps', {}), s.get('custom_vars', {}), s.get('deps_file', 'DEPS'), @@ -748,12 +758,13 @@ solutions = [ return client def SetDefaultConfig(self, solution_name, deps_file, solution_url, - safesync_url): + safesync_url, managed=True): self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % { 'solution_name': solution_name, 'solution_url': solution_url, 'deps_file': deps_file, 'safesync_url' : safesync_url, + 'managed': managed, }) def _SaveEntries(self): @@ -799,13 +810,14 @@ solutions = [ # Do not check safesync_url if one or more --revision flag is specified. if not self._options.revisions: for s in self.dependencies: - if not s.safesync_url: - continue - handle = urllib.urlopen(s.safesync_url) - rev = handle.read().strip() - handle.close() - if len(rev): - self._options.revisions.append('%s@%s' % (s.name, rev)) + if not s.managed: + self._options.revisions.append('%s@unmanaged' % s.name) + elif s.safesync_url: + handle = urllib.urlopen(s.safesync_url) + rev = handle.read().strip() + handle.close() + if len(rev): + self._options.revisions.append('%s@%s' % (s.name, rev)) if not self._options.revisions: return revision_overrides solutions_names = [s.name for s in self.dependencies] @@ -929,6 +941,7 @@ solutions = [ 'solution_url': d.url, 'deps_file': d.deps_file, 'safesync_url' : d.safesync_url or '', + 'managed': d.managed, 'solution_deps': ''.join(custom_deps), } # Print the snapshot configuration file @@ -1048,6 +1061,11 @@ URL. parser.add_option('--deps-file', default='DEPS', help='overrides the default name for the DEPS file for the' 'main solutions and all sub-dependencies') + parser.add_option('--unmanaged', action='store_true', default=False, + help='overrides the default behavior to make it possible ' + 'to have the main solution untouched by gclient ' + '(gclient will check out unmanaged dependencies but ' + 'will never sync them)') parser.add_option('--git-deps', action='store_true', help='sets the deps file to ".DEPS.git" instead of "DEPS"') (options, args) = parser.parse_args(args) @@ -1073,7 +1091,8 @@ URL. safesync_url = '' if len(args) > 1: safesync_url = args[1] - client.SetDefaultConfig(name, deps_file, base_url, safesync_url) + client.SetDefaultConfig(name, deps_file, base_url, safesync_url, + managed=not options.unmanaged) client.SaveConfig() return 0 diff --git a/gclient_scm.py b/gclient_scm.py index 3c6e17305..ded662e27 100644 --- a/gclient_scm.py +++ b/gclient_scm.py @@ -174,9 +174,13 @@ class GitWrapper(SCMWrapper): url, deps_revision = gclient_utils.SplitUrlRevision(self.url) rev_str = "" revision = deps_revision + managed = True if options.revision: # Override the revision number. revision = str(options.revision) + if revision == 'unmanaged': + revision = None + managed = False if not revision: revision = default_rev @@ -218,6 +222,10 @@ class GitWrapper(SCMWrapper): print('') return + if not managed: + print ('________ unmanaged solution; skipping %s' % self.relpath) + return + if not os.path.exists(os.path.join(self.checkout_path, '.git')): raise gclient_utils.Error('\n____ %s%s\n' '\tPath is not a git repo. No .git dir.\n' @@ -735,14 +743,19 @@ class SVNWrapper(SCMWrapper): url, revision = gclient_utils.SplitUrlRevision(self.url) # Keep the original unpinned url for reference in case the repo is switched. base_url = url + managed = True if options.revision: # Override the revision number. revision = str(options.revision) if revision: - forced_revision = True - # Reconstruct the url. - url = '%s@%s' % (url, revision) - rev_str = ' at %s' % revision + if revision != 'unmanaged': + forced_revision = True + # Reconstruct the url. + url = '%s@%s' % (url, revision) + rev_str = ' at %s' % revision + else: + managed = False + revision = None else: forced_revision = False rev_str = '' @@ -754,6 +767,10 @@ class SVNWrapper(SCMWrapper): self._RunAndGetFileList(command, options, file_list, self._root_dir) return + if not managed: + print ('________ unmanaged solution; skipping %s' % self.relpath) + return + # Get the existing scm url and the revision number of the current checkout. try: from_info = scm.SVN.CaptureInfo(os.path.join(self.checkout_path, '.')) diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py index 2ea01fd00..21da096cf 100755 --- a/tests/gclient_scm_test.py +++ b/tests/gclient_scm_test.py @@ -442,8 +442,8 @@ class SVNWrapperTestCase(BaseTestCase): ('________ found .hg directory; skipping %s\n' % self.relpath)) -class GitWrapperTestCase(GCBaseTestCase, StdoutCheck, TestCaseUtils, - unittest.TestCase): +class BaseGitWrapperTestCase(GCBaseTestCase, StdoutCheck, TestCaseUtils, + unittest.TestCase): """This class doesn't use pymox.""" class OptionsObject(object): def __init__(self, verbose=False, revision=None): @@ -543,6 +543,8 @@ from :3 unittest.TestCase.tearDown(self) rmtree(self.root_dir) + +class ManagedGitWrapperTestCase(BaseGitWrapperTestCase): def testDir(self): members = [ 'FullUrlForRelativeUrl', 'GetRevisionDate', 'RunCommand', @@ -823,6 +825,64 @@ from :3 self.assertEquals(rev_info, '069c602044c5388d2d15c3f875b057c852003458') +class UnmanagedGitWrapperTestCase(BaseGitWrapperTestCase): + def testUpdateCheckout(self): + if not self.enabled: + return + options = self.Options(verbose=True) + root_dir = gclient_scm.os.path.realpath(tempfile.mkdtemp()) + relpath = 'foo' + base_path = join(root_dir, relpath) + url = join(self.base_path, '.git') + try: + scm = gclient_scm.CreateSCM(url=url, root_dir=root_dir, + relpath=relpath) + file_list = [] + options.revision = 'unmanaged' + scm.update(options, (), file_list) + self.assertEquals(len(file_list), 2) + self.assert_(gclient_scm.os.path.isfile(join(base_path, 'a'))) + self.assertEquals(scm.revinfo(options, (), None), + '069c602044c5388d2d15c3f875b057c852003458') + finally: + rmtree(root_dir) + msg1 = ( + "\n_____ foo at refs/heads/master\n\n" + "________ running 'git clone -b master --verbose %s %s' in '%s'\n" + "Initialized empty Git repository in %s\n") % ( + join(self.root_dir, '.', '.git'), + join(root_dir, 'foo'), + root_dir, + join(gclient_scm.os.path.realpath(root_dir), 'foo', '.git') + '/') + msg2 = ( + "\n_____ foo at refs/heads/master\n\n" + "________ running 'git clone -b master --verbose %s %s' in '%s'\n" + "Cloning into %s...\ndone.\n") % ( + join(self.root_dir, '.', '.git'), + join(root_dir, 'foo'), + root_dir, + join(gclient_scm.os.path.realpath(root_dir), 'foo')) + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = self._old_stdout + self.assertTrue(out in (msg1, msg2), (out, msg1, msg2)) + + def testUpdateUpdate(self): + if not self.enabled: + return + options = self.Options() + expected_file_list = [] + scm = gclient_scm.CreateSCM(url=self.url, root_dir=self.root_dir, + relpath=self.relpath) + file_list = [] + options.revision = 'unmanaged' + scm.update(options, (), file_list) + self.assertEquals(file_list, expected_file_list) + self.assertEquals(scm.revinfo(options, (), None), + '069c602044c5388d2d15c3f875b057c852003458') + self.checkstdout('________ unmanaged solution; skipping .\n') + + if __name__ == '__main__': if '-v' in sys.argv: logging.basicConfig( diff --git a/tests/gclient_smoketest.py b/tests/gclient_smoketest.py index 2a143953e..473a5116c 100755 --- a/tests/gclient_smoketest.py +++ b/tests/gclient_smoketest.py @@ -200,6 +200,7 @@ class GClientSmoke(GClientSmokeBase): ' { "name" : "src",\n' ' "url" : "%strunk/src",\n' ' "deps_file" : "DEPS",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' },\n' ' "safesync_url": "",\n' @@ -211,6 +212,7 @@ class GClientSmoke(GClientSmokeBase): ' { "name" : "src",\n' ' "url" : "%srepo_1",\n' ' "deps_file" : "DEPS",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' },\n' ' "safesync_url": "",\n' @@ -222,6 +224,7 @@ class GClientSmoke(GClientSmokeBase): ' { "name" : "foo",\n' ' "url" : "foo",\n' ' "deps_file" : "DEPS",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' },\n' ' "safesync_url": "faa",\n' @@ -233,6 +236,7 @@ class GClientSmoke(GClientSmokeBase): ' { "name" : "foo",\n' ' "url" : "foo",\n' ' "deps_file" : "blah",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' },\n' ' "safesync_url": "",\n' @@ -687,6 +691,7 @@ class GClientSmokeSVN(GClientSmokeBase): ' { "name" : "src",\n' ' "url" : "%(base)s/src",\n' ' "deps_file" : "DEPS",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' "foo/bar": None,\n' ' "invalid": None,\n' @@ -713,6 +718,7 @@ class GClientSmokeSVN(GClientSmokeBase): ' { "name" : "src",\n' ' "url" : "%(base)s/src",\n' ' "deps_file" : "DEPS.alt",\n' + ' "managed" : True,\n' ' "custom_deps" : {\n' ' "src/other2": \'%(base)s/other@2\',\n' ' },\n' diff --git a/tests/gclient_test.py b/tests/gclient_test.py index 86aa89384..d477b96f4 100755 --- a/tests/gclient_test.py +++ b/tests/gclient_test.py @@ -199,7 +199,7 @@ class GclientTest(trial_dir.TestCase): # Invalid urls causes pain when specifying requirements. Make sure it's # auto-fixed. d = gclient.Dependency( - None, 'name', 'proto://host/path/@revision', None, None, + None, 'name', 'proto://host/path/@revision', None, None, None, None, '', True) self.assertEquals('proto://host/path@revision', d.url) @@ -210,23 +210,25 @@ class GclientTest(trial_dir.TestCase): options, _ = parser.parse_args([]) obj = gclient.GClient('foo', options) obj.dependencies.append( - gclient.Dependency(obj, 'foo', 'url', None, None, None, 'DEPS', True)) + gclient.Dependency(obj, 'foo', 'url', None, None, None, None, 'DEPS', + True)) obj.dependencies.append( - gclient.Dependency(obj, 'bar', 'url', None, None, None, 'DEPS', True)) + gclient.Dependency(obj, 'bar', 'url', None, None, None, None, 'DEPS', + True)) obj.dependencies[0].dependencies.append( gclient.Dependency( - obj.dependencies[0], 'foo/dir1', 'url', None, None, None, 'DEPS', - True)) + obj.dependencies[0], 'foo/dir1', 'url', None, None, None, None, + 'DEPS', True)) obj.dependencies[0].dependencies.append( gclient.Dependency( obj.dependencies[0], 'foo/dir2', - gclient.GClientKeywords.FromImpl('bar'), None, None, None, 'DEPS', - True)) + gclient.GClientKeywords.FromImpl('bar'), None, None, None, None, + 'DEPS', True)) obj.dependencies[0].dependencies.append( gclient.Dependency( obj.dependencies[0], 'foo/dir3', - gclient.GClientKeywords.FileImpl('url'), None, None, None, 'DEPS', - True)) + gclient.GClientKeywords.FileImpl('url'), None, None, None, None, + 'DEPS', True)) obj.dependencies[0]._file_list.append('foo') self.assertEquals(434, len(str(obj)), '%d\n%s' % (len(str(obj)), str(obj)))