diff --git a/gclient.py b/gclient.py index 9a678220f..28f590d90 100644 --- a/gclient.py +++ b/gclient.py @@ -339,6 +339,40 @@ class Dependency(GClientKeywords, gclient_utils.WorkItem): def run(self, revision_overrides, command, args, work_queue, options): """Runs 'command' before parsing the DEPS in case it's a initial checkout or a revert.""" + + def maybeGetParentRevision(options): + """If we are performing an update and --transitive is set, set the + revision to the parent's revision. If we have an explicit revision + do nothing.""" + if command == 'update' and options.transitive and not options.revision: + _, revision = gclient_utils.SplitUrlRevision(self.parsed_url) + if not revision: + options.revision = revision_overrides.get(self.parent.name) + if options.verbose and options.revision: + print("Using parent's revision date: %s" % options.revision) + # If the parent has a revision override, then it must have been + # converted to date format. + assert (not options.revision or + gclient_utils.IsDateRevision(options.revision)) + + def maybeConvertToDateRevision(options): + """If we are performing an update and --transitive is set, convert the + revision to a date-revision (if necessary). Instead of having + -r 101 replace the revision with the time stamp of 101 (e.g. + "{2011-18-04}"). + This way dependencies are upgraded to the revision they had at the + check-in of revision 101.""" + if (command == 'update' and + options.transitive and + options.revision and + not gclient_utils.IsDateRevision(options.revision)): + revision_date = scm.GetRevisionDate(options.revision) + revision = gclient_utils.MakeDateRevision(revision_date) + if options.verbose: + print("Updating revision override from %s to %s." % + (options.revision, revision)) + revision_overrides[self.name] = revision + assert self._file_list == [] if not self.should_process: return @@ -362,8 +396,10 @@ class Dependency(GClientKeywords, gclient_utils.WorkItem): # Create a shallow copy to mutate revision. options = copy.copy(options) options.revision = revision_overrides.get(self.name) + maybeGetParentRevision(options) scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name) scm.RunCommand(command, options, args, self._file_list) + maybeConvertToDateRevision(options) self._file_list = [os.path.join(self.name, f.strip()) for f in self._file_list] self.processed = True @@ -1043,6 +1079,11 @@ def CMDsync(parser, args): 'has multiple solutions configured and will work even ' 'if the src@ part is skipped. Note that specifying ' '--revision means your safesync_url gets ignored.') + parser.add_option('-t', '--transitive', action='store_true', + help='When a revision is specified (in the DEPS file or ' + 'with the command-line flag), transitively update ' + 'the dependencies to the date of the given revision. ' + 'Only supported for SVN repositories.') parser.add_option('-H', '--head', action='store_true', help='skips any safesync_urls specified in ' 'configured solutions and sync to head instead') diff --git a/gclient_scm.py b/gclient_scm.py index 6dc4da7f6..99461d13f 100644 --- a/gclient_scm.py +++ b/gclient_scm.py @@ -126,6 +126,13 @@ class SCMWrapper(object): class GitWrapper(SCMWrapper): """Wrapper for Git""" + def GetRevisionDate(self, revision): + """Returns the given revision's date in ISO-8601 format (which contains the + time zone).""" + # TODO(floitsch): get the time-stamp of the given revision and not just the + # time-stamp of the currently checked out revision. + return self._Capture(['log', '-n', '1', '--format=%ai']) + @staticmethod def cleanup(options, args, file_list): """'Cleanup' the repo. @@ -186,6 +193,14 @@ class GitWrapper(SCMWrapper): if not revision: revision = default_rev + if gclient_utils.IsDateRevision(revision): + # Date-revisions only work on git-repositories if the reflog hasn't + # expired yet. Use rev-list to get the corresponding revision. + # git rev-list -n 1 --before='time-stamp' branchname + if options.transitive: + print('Warning: --transitive only works for SVN repositories.') + revision = default_rev + rev_str = ' at %s' % revision files = [] @@ -668,6 +683,13 @@ class GitWrapper(SCMWrapper): class SVNWrapper(SCMWrapper): """ Wrapper for SVN """ + def GetRevisionDate(self, revision): + """Returns the given revision's date in ISO-8601 format (which contains the + time zone).""" + date = scm.SVN.Capture(['propget', '--revprop', 'svn:date', '-r', revision, + os.path.join(self.checkout_path, '.')]) + return date.strip() + def cleanup(self, options, args, file_list): """Cleanup working copy.""" self._Run(['cleanup'] + args, options) diff --git a/gclient_utils.py b/gclient_utils.py index 995322389..81a1f667e 100644 --- a/gclient_utils.py +++ b/gclient_utils.py @@ -112,6 +112,17 @@ def SplitUrlRevision(url): return tuple(components) +def IsDateRevision(revision): + """Returns true if the given revision is of the form "{ ... }".""" + return bool(revision and re.match(r'^\{.+\}$', str(revision))) + + +def MakeDateRevision(date): + """Returns a revision representing the latest revision before the given + date.""" + return "{" + date + "}" + + def SyntaxErrorToError(filename, e): """Raises a gclient_utils.Error exception with the human readable message""" try: diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py index d7d93b678..a11c4d7cb 100755 --- a/tests/gclient_scm_test.py +++ b/tests/gclient_scm_test.py @@ -82,7 +82,7 @@ class SVNWrapperTestCase(BaseTestCase): def testDir(self): members = [ - 'FullUrlForRelativeUrl', 'RunCommand', + 'FullUrlForRelativeUrl', 'GetRevisionDate', 'RunCommand', 'cleanup', 'diff', 'export', 'pack', 'relpath', 'revert', 'revinfo', 'runhooks', 'status', 'update', 'updatesingle', 'url', @@ -543,7 +543,7 @@ from :3 def testDir(self): members = [ - 'FullUrlForRelativeUrl', 'RunCommand', + 'FullUrlForRelativeUrl', 'GetRevisionDate', 'RunCommand', 'cleanup', 'diff', 'export', 'pack', 'relpath', 'revert', 'revinfo', 'runhooks', 'status', 'update', 'url', ] diff --git a/tests/gclient_smoketest.py b/tests/gclient_smoketest.py index acd0ab566..408cd66b0 100755 --- a/tests/gclient_smoketest.py +++ b/tests/gclient_smoketest.py @@ -327,6 +327,43 @@ class GClientSmokeSVN(GClientSmokeBase): tree['src/svn_hooked1'] = 'svn_hooked1' self.assertTree(tree) + def testSyncTransitive(self): + # TODO(maruel): safesync. + if not self.enabled: + return + self.gclient(['config', self.svn_base + 'trunk/src/']) + + # Make sure we can populate a new repository with --transitive. + self.parseGclient( + ['sync', '--transitive', '--revision', 'src@1', '--deps', 'mac', + '--jobs', '1'], + ['running', 'running', 'running', 'running']) + tree = self.mangle_svn_tree( + ('trunk/src@1', 'src'), + ('trunk/third_party/foo@1', 'src/third_party/fpp'), + ('trunk/other@1', 'src/other'), + ('trunk/third_party/foo@1', 'src/third_party/prout')) + + # Get up to date, so we can test synching back. + self.gclient(['sync', '--deps', 'mac', '--jobs', '1']) + + # Manually remove svn_hooked1 before synching to make sure it's not + # recreated. + os.remove(join(self.root_dir, 'src', 'svn_hooked1')) + + self.parseGclient( + ['sync', '--transitive', '--revision', 'src@1', '--deps', 'mac', + '--delete_unversioned_trees', '--jobs', '1'], + ['running', 'running', 'running', 'running', 'deleting']) + tree = self.mangle_svn_tree( + ('trunk/src@1', 'src'), + ('trunk/third_party/foo@1', 'src/third_party/fpp'), + ('trunk/other@1', 'src/other'), + ('trunk/third_party/foo@1', 'src/third_party/prout')) + tree['src/file/other/DEPS'] = ( + self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS']) + self.assertTree(tree) + def testSyncIgnoredSolutionName(self): """TODO(maruel): This will become an error soon.""" if not self.enabled: diff --git a/tests/gclient_utils_test.py b/tests/gclient_utils_test.py index f0d833bd3..59d5eb7d0 100755 --- a/tests/gclient_utils_test.py +++ b/tests/gclient_utils_test.py @@ -28,8 +28,8 @@ class GclientUtilsUnittest(GclientUtilBase): 'CheckCall', 'CheckCallError', 'CheckCallAndFilter', 'CheckCallAndFilterAndHeader', 'Error', 'ExecutionQueue', 'FileRead', 'FileWrite', 'FindFileUpwards', 'FindGclientRoot', - 'GetGClientRootAndEntries', 'MakeFileAutoFlush', - 'MakeFileAnnotated', 'PathDifference', 'Popen', + 'GetGClientRootAndEntries', 'IsDateRevision', 'MakeDateRevision', + 'MakeFileAutoFlush', 'MakeFileAnnotated', 'PathDifference', 'Popen', 'PrintableObject', 'RemoveDirectory', 'SoftClone', 'SplitUrlRevision', 'SyntaxErrorToError', 'WorkItem', 'errno', 'hack_subprocess', 'logging', 'os', 'Queue', 're', 'rmtree',