Reland "Make gclient ready for the Blink (DEPS to main project)"

Reland crrev.com/743083002, which was reverted in crrev.com/796053002
due to some test flakiness, probably related with an old version of Git on
the bots. Relanding now that the infra has been updated to Trusty (plus
adding some de-flake precautions).

Original CL Description:
Make gclient ready for the Blink (DEPS to main project) transition

This CL makes gclient understand correctly whether a git project is
being moved from DEPS to an upper project and vice-versa.
The driving use case for this is the upcoming Blink merge, where
third_party/Webkit will be removed from DEPS (and .gitignore) and will
become part of the main project.

At present state, gclient leaves the .git folder around when a project
is removed from DEPS, and that causes many problems.

Furthermore this CL solves the performance problem of bisecting across
the merge point. The subproject's (Blink) .git/ folder is moved to a
backup location (in the main checkout root) and is restored when moving
backwards, avoiding a re-fetch when bisecting across the merge point.

BUG=431469

Review URL: https://codereview.chromium.org/910913003

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294082 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/gclient.py b/gclient.py
index f6db225..22a4eb9 100755
--- a/gclient.py
+++ b/gclient.py
@@ -1542,23 +1542,18 @@
         # Fix path separator on Windows.
         entry_fixed = entry.replace('/', os.path.sep)
         e_dir = os.path.join(self.root_dir, entry_fixed)
-
-        def _IsParentOfAny(parent, path_list):
-          parent_plus_slash = parent + '/'
-          return any(
-              path[:len(parent_plus_slash)] == parent_plus_slash
-              for path in path_list)
-
         # Use entry and not entry_fixed there.
         if (entry not in entries and
             (not any(path.startswith(entry + '/') for path in entries)) and
             os.path.exists(e_dir)):
+          # The entry has been removed from DEPS.
           scm = gclient_scm.CreateSCM(
               prev_url, self.root_dir, entry_fixed, self.outbuf)
 
           # Check to see if this directory is now part of a higher-up checkout.
           # The directory might be part of a git OR svn checkout.
           scm_root = None
+          scm_class = None
           for scm_class in (gclient_scm.scm.GIT, gclient_scm.scm.SVN):
             try:
               scm_root = scm_class.GetCheckoutRoot(scm.checkout_path)
@@ -1571,9 +1566,45 @@
                             'determine whether it is part of a higher-level '
                             'checkout, so not removing.' % entry)
             continue
+
+          # This is to handle the case of third_party/WebKit migrating from
+          # being a DEPS entry to being part of the main project.
+          # If the subproject is a Git project, we need to remove its .git
+          # folder. Otherwise git operations on that folder will have different
+          # effects depending on the current working directory.
+          if scm_class == gclient_scm.scm.GIT and (
+              os.path.abspath(scm_root) == os.path.abspath(e_dir)):
+            e_par_dir = os.path.join(e_dir, os.pardir)
+            if scm_class.IsInsideWorkTree(e_par_dir):
+              par_scm_root = scm_class.GetCheckoutRoot(e_par_dir)
+              # rel_e_dir : relative path of entry w.r.t. its parent repo.
+              rel_e_dir = os.path.relpath(e_dir, par_scm_root)
+              if scm_class.IsDirectoryVersioned(par_scm_root, rel_e_dir):
+                save_dir = scm.GetGitBackupDirPath()
+                # Remove any eventual stale backup dir for the same project.
+                if os.path.exists(save_dir):
+                  gclient_utils.rmtree(save_dir)
+                os.rename(os.path.join(e_dir, '.git'), save_dir)
+                # When switching between the two states (entry/ is a subproject
+                # -> entry/ is part of the outer project), it is very likely
+                # that some files are changed in the checkout, unless we are
+                # jumping *exactly* across the commit which changed just DEPS.
+                # In such case we want to cleanup any eventual stale files
+                # (coming from the old subproject) in order to end up with a
+                # clean checkout.
+                scm_class.CleanupDir(par_scm_root, rel_e_dir)
+                assert not os.path.exists(os.path.join(e_dir, '.git'))
+                print(('\nWARNING: \'%s\' has been moved from DEPS to a higher '
+                       'level checkout. The git folder containing all the local'
+                       ' branches has been saved to %s.\n'
+                       'If you don\'t care about its state you can safely '
+                       'remove that folder to free up space.') %
+                      (entry, save_dir))
+                continue
+
           if scm_root in full_entries:
-            logging.info('%s is part of a higher level checkout, not '
-                         'removing.', scm.GetCheckoutRoot())
+            logging.info('%s is part of a higher level checkout, not removing',
+                         scm.GetCheckoutRoot())
             continue
 
           file_list = []