[gclient] increase firepower on case insensitive filesystems.

BUG=693030

Change-Id: I01ae922ce48f40e28ebdb9bcc08d5964f6bc676c
Reviewed-on: https://chromium-review.googlesource.com/444308
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Aaron Gable <agable@chromium.org>
Reviewed-by: Sergey Berezin <sergeyberezin@chromium.org>
Reviewed-by: Daniel Jacques <dnj@chromium.org>
diff --git a/gclient_scm.py b/gclient_scm.py
index b4da536..c6d7699 100644
--- a/gclient_scm.py
+++ b/gclient_scm.py
@@ -293,15 +293,35 @@
         cwd=self.checkout_path,
         filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
 
-  def _FetchAndReset(self, revision, file_list, options):
-    """Equivalent to git fetch; git reset."""
+  def _Scrub(self, target, options):
+    """Scrubs out all changes in the local repo, back to the state of target."""
     quiet = []
     if not options.verbose:
       quiet = ['--quiet']
+    self._Run(['reset', '--hard', target] + quiet, options)
+    if options.force and options.delete_unversioned_trees:
+      # where `target` is a commit that contains both upper and lower case
+      # versions of the same file on a case insensitive filesystem, we are
+      # actually in a broken state here. The index will have both 'a' and 'A',
+      # but only one of them will exist on the disk. To progress, we delete
+      # everything that status thinks is modified.
+      for line in self._Capture(['status', '--porcelain']).splitlines():
+        # --porcelain (v1) looks like:
+        # XY filename
+        try:
+          filename = line[3:]
+          self.Print('_____ Deleting residual after reset: %r.' % filename)
+          gclient_utils.rm_file_or_tree(
+            os.path.join(self.checkout_path, line[3:]))
+        except OSError:
+          pass
+
+  def _FetchAndReset(self, revision, file_list, options):
+    """Equivalent to git fetch; git reset."""
     self._UpdateBranchHeads(options, fetch=False)
 
     self._Fetch(options, prune=True, quiet=options.verbose)
-    self._Run(['reset', '--hard', revision] + quiet, options)
+    self._Scrub(revision, options)
     if file_list is not None:
       files = self._Capture(['ls-files']).splitlines()
       file_list.extend([os.path.join(self.checkout_path, f) for f in files])
@@ -406,7 +426,7 @@
       gclient_utils.safe_makedirs(self.checkout_path)
       os.rename(backup_dir, target_dir)
       # Reset to a clean state
-      self._Run(['reset', '--hard', 'HEAD'], options)
+      self._Scrub('HEAD', options)
 
     if (not os.path.exists(self.checkout_path) or
         (os.path.isdir(self.checkout_path) and
@@ -527,11 +547,17 @@
       target = 'HEAD'
       if options.upstream and upstream_branch:
         target = upstream_branch
-      self._Run(['reset', '--hard', target], options)
+      self._Scrub(target, options)
 
     if current_type == 'detached':
       # case 0
-      self._CheckClean(revision)
+      # We just did a Scrub, this is as clean as it's going to get. In
+      # particular if HEAD is a commit that contains two versions of the same
+      # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
+      # to actually "Clean" the checkout; that commit is uncheckoutable on this
+      # system. The best we can do is carry forward to the checkout step.
+      if not (options.force or options.reset):
+        self._CheckClean(revision)
       self._CheckDetachedHead(revision, options)
       if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
         self.Print('Up-to-date; skipping checkout.')
@@ -736,7 +762,7 @@
     if file_list is not None:
       files = self._Capture(['diff', deps_revision, '--name-only']).split()
 
-    self._Run(['reset', '--hard', deps_revision], options)
+    self._Scrub(deps_revision, options)
     self._Run(['clean', '-f', '-d'], options)
 
     if file_list is not None:
@@ -961,7 +987,7 @@
               'WARNING: destroys any uncommitted work in your current branch!'
               ' (y)es / (q)uit / (s)how : ', options)
           if re.match(r'yes|y', rebase_action, re.I):
-            self._Run(['reset', '--hard', 'HEAD'], options)
+            self._Scrub('HEAD', options)
             # Should this be recursive?
             rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
             break
@@ -1032,7 +1058,7 @@
       os.path.isdir(os.path.join(g, "rebase-merge")) or
       os.path.isdir(os.path.join(g, "rebase-apply")))
 
-  def _CheckClean(self, revision):
+  def _CheckClean(self, revision, fixup=False):
     lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
     if os.path.exists(lockfile):
       raise gclient_utils.Error(