bisect-kit: delete unexpected files in source tree

cros_mark_as_stable may make the tree dirty. We should recover the tree
state otherwise further bisections may fail forever.

BUG=b:131459909, b:131803654, b:139040320
TEST=switch_cros_localbuild.py R75-12107.0.0 twice

Change-Id: I1d7d271786f5c52c946be06f1df84f497585142e
Reviewed-on: https://chromium-review.googlesource.com/1782358
Tested-by: Kuang-che Wu <kcwu@chromium.org>
Commit-Ready: Kuang-che Wu <kcwu@chromium.org>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Chung-yih Wang <cywang@google.com>
diff --git a/bisect_kit/git_util.py b/bisect_kit/git_util.py
index 59d9565..d40f69a 100644
--- a/bisect_kit/git_util.py
+++ b/bisect_kit/git_util.py
@@ -8,6 +8,7 @@
 import logging
 import os
 import re
+import shutil
 import subprocess
 import time
 
@@ -406,6 +407,68 @@
   return result or None
 
 
+def reset_hard(git_repo):
+  """Restore modified and deleted files.
+
+  This is simply wrapper of "git reset --hard".
+
+  Args:
+    git_repo: path of git repo.
+  """
+  util.check_call('git', 'reset', '--hard', cwd=git_repo)
+
+
+def list_untracked(git_repo, excludes=None):
+  """List untracked files and directories.
+
+  Args:
+    git_repo: path of git repo.
+    excludes: files and/or directories to ignore, relative to git_repo
+
+  Returns:
+    list of paths, relative to git_repo
+  """
+  exclude_flags = []
+  if excludes:
+    for exclude in excludes:
+      assert not os.path.isabs(exclude), 'should be relative'
+      exclude_flags += ['--exclude', '/' + re.escape(exclude)]
+
+  result = []
+  for path in util.check_output(
+      'git',
+      'ls-files',
+      '--others',
+      '--exclude-standard',
+      *exclude_flags,
+      cwd=git_repo).splitlines():
+    # Remove the trailing slash, which means directory.
+    path = path.rstrip('/')
+    result.append(path)
+  return result
+
+
+def distclean(git_repo, excludes=None):
+  """Clean up git repo directory.
+
+  Restore modified and deleted files. Delete untracked files.
+
+  Args:
+    git_repo: path of git repo.
+    excludes: files and/or directories to ignore, relative to git_repo
+  """
+  reset_hard(git_repo)
+
+  # Delete untracked files.
+  for untracked in list_untracked(git_repo, excludes=excludes):
+    path = os.path.join(git_repo, untracked)
+    logger.debug('delete untracked: %s', path)
+    if os.path.isdir(path):
+      shutil.rmtree(path)
+    else:
+      os.unlink(path)
+
+
 def get_history(git_repo,
                 path,
                 branch=None,