Repository: oppia/oppia

Issue:
Package-lock.json files gets updated automatically and interrupts developers workflow 
Package-lock.json files updates automatically after running scripts/starts.sh and adds `options: true` for dependencies in some operating system and user's commit's this changes unknowingly.

Is this an expected behavior (options: true) if not, can we do something to avoid this?

Example: [PR1](https://github.com/oppia/oppia/pull/6585/files#r281972417), [PR2](https://github.com/oppia/oppia/pull/6714/files#diff-32607347f8126e6534ebc7ebaec4853dL3148)

**Additional context:**

Found this as the reason for this unexpected changes: npm/npm#17722


Discussion:
@NishealJ, do you have any thoughts on this?
- Is it expected behavior? If not, how can we prevent this to happen? Currently, it really prevents me from switching branch, pushing changes, etc. 

![image](https://user-images.githubusercontent.com/16653571/57755289-29708c00-770e-11e9-8501-863c0ebffc05.png)

Hi @DubeySandeep ,
Yes this seems to be an expected behavior to me, I also faced this issue.
I'll try to find ways to prevent this, Also I read about this [here](https://stackoverflow.com/questions/54411377/package-lock-json-file-package-with-optional-true)
@NishealJ, Thanks for the update, I'm assigning this to you! 
FYI I remember reading something that might be related when trying to debug #6769. Adding it here in case it might be useful: https://github.com/npm/npm/issues/19393#issuecomment-359848898
@seanlip What should be our policy regarding reviewing PRs with changes to _package-lock.json_ without changes to _package.json_? Tell the developer not to commit changes to the _package-lock.json_?
I think we should do a checkout of package-lock.json (so, yep, basically what you said).

The churn in package-lock.json unfortunately does not seem resolvable because the fsevents dependency is required on Mac but removed on Linux. This seems to be an open issue on npm that has a very long comment thread and that still has not been resolved (see @DubeySandeep's link in the original comment on this issue). 

Resolving patch:
diff --git a/scripts/pre_commit_hook.py b/scripts/pre_commit_hook.py
new file mode 100644
--- /dev/null
+++ b/scripts/pre_commit_hook.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+#
+# Copyright 2019 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the 'License');
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS-IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Pre-commit hook that checks files added/modified in a commit.
+
+To install the hook manually simply execute this script from the oppia root dir
+with the `--install` flag.
+To bypass the validation upon `git commit` use the following command:
+`git commit --no-verify --am "<Your Commit Message>"`
+
+This hook works only on Unix like systems as of now.
+On Vagrant under Windows it will still copy the hook to the .git/hooks dir
+but it will have no effect.
+"""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+
+
+def _install_hook():
+    """Installs the pre_commit_hook script. It ensures that oppia/ is the
+    root folder.
+    """
+    oppia_dir = os.getcwd()
+    hooks_dir = os.path.join(oppia_dir, '.git', 'hooks')
+    pre_commit_file = os.path.join(hooks_dir, 'pre-commit')
+    if os.path.islink(pre_commit_file):
+        print 'Symlink already exists'
+        return
+    try:
+        os.symlink(os.path.abspath(__file__), pre_commit_file)
+        print 'Created symlink in .git/hooks directory'
+    # Raises AttributeError on windows, OSError added as failsafe.
+    except (OSError, AttributeError):
+        shutil.copy(__file__, pre_commit_file)
+        print 'Copied file to .git/hooks directory'
+
+
+def _start_subprocess_for_result(cmd):
+    """Starts subprocess and returns (stdout, stderr)."""
+    task = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    out, err = task.communicate()
+    return out, err
+
+
+def _does_diff_include_package_lock_file_and_no_package_file():
+    """Checks whether the diff includes package-lock.json and
+        no package.json.
+
+    Returns:
+        bool. Whether the diff includes package-lock.json and
+            no package.json.
+
+    Raises:
+        ValueError if git command fails.
+    """
+
+    git_cmd = ['git', 'diff', '--name-only', '--cached']
+    out, err = _start_subprocess_for_result(git_cmd)
+
+    if not err:
+        files_changed = out.split('\n')
+        return 'package-lock.json' in files_changed and (
+            'package.json' not in files_changed)
+    else:
+        raise ValueError(err)
+
+
+def _revert_changes_in_package_lock_file():
+    """Reverts the changes made in package-lock.json file.
+
+    Raises:
+        ValueError if any git command fails.
+    """
+
+    git_unstage_cmd = ['git', 'reset', '--', 'package-lock.json']
+    git_revert_cmd = ['git', 'checkout', '--', 'package-lock.json']
+
+    _, err_unstage_cmd = (
+        _start_subprocess_for_result(git_unstage_cmd))
+
+    if not err_unstage_cmd:
+        _, err_revert_cmd = (
+            _start_subprocess_for_result(git_revert_cmd))
+
+        if not err_revert_cmd:
+            print 'Changes to package-lock.json successfully reverted!'
+        else:
+            raise ValueError(err_revert_cmd)
+    else:
+        raise ValueError(err_unstage_cmd)
+
+
+def main():
+    """Main method for pre-commit hook that checks files added/modified
+    in a commit.
+    """
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--install', action='store_true', default=False,
+                        help='Install pre_commit_hook to the .git/hooks dir')
+    args = parser.parse_args()
+    if args.install:
+        _install_hook()
+        sys.exit(0)
+
+    print 'Running pre-commit check for package-lock.json ...'
+    if _does_diff_include_package_lock_file_and_no_package_file():
+        print ('This commit only changes package-lock.json without '
+               'any change in package.json. Therefore changes in '
+               'package-lock.json will be automatically reverted.')
+        print 'Reverting changes in package-lock.json ...'
+        _revert_changes_in_package_lock_file()
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/pre_push_hook.py b/scripts/pre_push_hook.py
--- a/scripts/pre_push_hook.py
+++ b/scripts/pre_push_hook.py
@@ -27,8 +27,6 @@
 but it will have no effect.
 """
 
-# Pylint has issues with the import order of argparse.
-# pylint: disable=wrong-import-order
 import argparse
 import collections
 import os
@@ -37,8 +35,6 @@
 import subprocess
 import sys
 
-# pylint: enable=wrong-import-order
-
 
 GitRef = collections.namedtuple(
     'GitRef', ['local_ref', 'local_sha1', 'remote_ref', 'remote_sha1'])


Test patch: