[git-freeze] Ignore submodules when freezing.

It turns out we are already parsing the full output of `git status`,
so we may as well use that. This output already ignores submodules.

This also updates the freeze implementation to avoid unnecessary
calls to `git commit` when we already know if they are needed or
not.

Note that `git stash` (which `git freeze` is emulating) already
ignores submodules as well.

Ignoring submodules while freezing should make `git rebase-update`
work again inside of repos which have dirty submodules.

R=gavinmak,jojwang

Change-Id: Ib3a7784b6e7e7c19687203c1f4c32422a49bf8e3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4680288
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
Auto-Submit: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Joanna Wang <jojwang@chromium.org>
diff --git a/git_common.py b/git_common.py
index 0483536..7fe6c05 100644
--- a/git_common.py
+++ b/git_common.py
@@ -5,9 +5,6 @@
 # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
 # Derived from https://gist.github.com/aljungberg/626518
 
-from __future__ import print_function
-from __future__ import unicode_literals
-
 import multiprocessing.pool
 import sys
 import threading
@@ -44,12 +41,6 @@
 from io import BytesIO
 
 
-if sys.version_info.major == 2:
-  # On Python 3, BrokenPipeError is raised instead.
-  # pylint:disable=redefined-builtin
-  BrokenPipeError = IOError
-
-
 ROOT = os.path.abspath(os.path.dirname(__file__))
 IS_WIN = sys.platform == 'win32'
 TEST_MODE = False
@@ -450,12 +441,27 @@
 
   root_path = repo_root()
 
+  # unindexed tracks all the files which are unindexed but we want to add to
+  # the `FREEZE.unindexed` commit.
+  unindexed = []
+
+  # will be set to true if there are any indexed files to commit.
+  have_indexed_files = False
+
   for f, s in status():
     if is_unmerged(s):
       die("Cannot freeze unmerged changes!")
-    if limit_mb > 0:
-      if s.lstat == '?':
-        untracked_bytes += os.lstat(os.path.join(root_path, f)).st_size
+    if s.lstat not in ' ?':
+      # This covers all changes to indexed files.
+      # lstat = ' ' means that the file is tracked and modified, but wasn't
+      # added yet.
+      # lstat = '?' means that the file is untracked.
+      have_indexed_files = True
+    else:
+      unindexed.append(f.encode('utf-8'))
+    if s.lstat == '?' and limit_mb > 0:
+      untracked_bytes += os.lstat(os.path.join(root_path, f)).st_size
+
   if limit_mb > 0 and untracked_bytes > limit_mb * MB:
     die("""\
       You appear to have too much untracked+unignored data in your git
@@ -476,23 +482,29 @@
       Where <new_limit> is an integer threshold in megabytes.""",
       untracked_bytes / (MB * 1.0), limit_mb, key)
 
-  try:
-    run('commit', '--no-verify', '-m', FREEZE + '.indexed')
-    took_action = True
-  except subprocess2.CalledProcessError:
-    pass
+  if have_indexed_files:
+    try:
+      run('commit', '--no-verify', '-m', f'{FREEZE}.indexed')
+      took_action = True
+    except subprocess2.CalledProcessError:
+      pass
 
   add_errors = False
-  try:
-    run('add', '-A', '--ignore-errors')
-  except subprocess2.CalledProcessError:
-    add_errors = True
+  if unindexed:
+    try:
+      run('add',
+          '--pathspec-from-file',
+          '-',
+          '--ignore-errors',
+          indata=b'\n'.join(unindexed))
+    except subprocess2.CalledProcessError:
+      add_errors = True
 
-  try:
-    run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
-    took_action = True
-  except subprocess2.CalledProcessError:
-    pass
+    try:
+      run('commit', '--no-verify', '-m', f'{FREEZE}.unindexed')
+      took_action = True
+    except subprocess2.CalledProcessError:
+      pass
 
   ret = []
   if add_errors: