forked from oppia/oppia
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpre_commit_hook.py
More file actions
executable file
·244 lines (203 loc) · 8.71 KB
/
pre_commit_hook.py
File metadata and controls
executable file
·244 lines (203 loc) · 8.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/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.
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
# TODO(#15567): The order can be fixed after Literal in utils.py is loaded
# from typing instead of typing_extensions, this will be possible after
# we migrate to Python 3.8.
sys.path.append(os.getcwd())
from scripts import common # isort:skip # pylint: disable=wrong-import-position
from core import utils # isort:skip # pylint: disable=wrong-import-position
FECONF_FILEPATH = os.path.join('core', 'feconf.py')
CONSTANTS_FILEPATH = os.path.join('.', 'assets', 'constants.ts')
RELEASE_CONSTANTS_FILEPATH = os.path.join(
'.', 'assets', 'release_constants.json')
KEYS_UPDATED_IN_FECONF = [
b'INCOMING_EMAILS_DOMAIN_NAME', b'ADMIN_EMAIL_ADDRESS',
b'SYSTEM_EMAIL_ADDRESS', b'NOREPLY_EMAIL_ADDRESS', b'CAN_SEND_EMAILS',
b'CAN_SEND_EDITOR_ROLE_EMAILS', b'CAN_SEND_FEEDBACK_MESSAGE_EMAILS',
b'CAN_SEND_SUBSCRIPTION_EMAILS', b'DEFAULT_EMAIL_UPDATES_PREFERENCE',
b'REQUIRE_EMAIL_ON_MODERATOR_ACTION', b'EMAIL_SERVICE_PROVIDER',
b'SYSTEM_EMAIL_NAME', b'MAILGUN_DOMAIN_NAME']
KEYS_UPDATED_IN_CONSTANTS = [
b'SITE_FEEDBACK_FORM_URL', b'FIREBASE_CONFIG_API_KEY',
b'FIREBASE_CONFIG_APP_ID', b'FIREBASE_CONFIG_AUTH_DOMAIN',
b'FIREBASE_CONFIG_MESSAGING_SENDER_ID', b'FIREBASE_CONFIG_PROJECT_ID',
b'FIREBASE_CONFIG_STORAGE_BUCKET', b'FIREBASE_CONFIG_GOOGLE_CLIENT_ID']
def install_hook():
"""Installs the pre_commit_hook script and makes it executable.
It ensures that oppia/ is the root folder.
Raises:
ValueError. If chmod command fails.
"""
oppia_dir = os.getcwd()
hooks_dir = os.path.join(oppia_dir, '.git', 'hooks')
pre_commit_file = os.path.join(hooks_dir, 'pre-commit')
chmod_cmd = ['chmod', '+x', pre_commit_file]
file_is_symlink = os.path.islink(pre_commit_file)
file_exists = os.path.exists(pre_commit_file)
if file_is_symlink and file_exists:
print('Symlink already exists')
else:
# This is needed, because otherwise some systems symlink/copy the .pyc
# file instead of the .py file.
this_file = __file__.replace('pyc', 'py')
# If its a broken symlink, delete it.
if file_is_symlink and not file_exists:
os.unlink(pre_commit_file)
print('Removing broken symlink')
try:
os.symlink(os.path.abspath(this_file), pre_commit_file)
print('Created symlink in .git/hooks directory')
# Raises AttributeError on windows, OSError added as failsafe.
except (OSError, AttributeError):
shutil.copy(this_file, pre_commit_file)
print('Copied file to .git/hooks directory')
print('Making pre-commit hook file executable ...')
if not common.is_windows_os():
_, err_chmod_cmd = start_subprocess_for_result(chmod_cmd)
if not err_chmod_cmd:
print('pre-commit hook file is now executable!')
else:
raise ValueError(err_chmod_cmd)
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():
"""Checks whether the diff includes package-lock.json.
Returns:
bool. Whether the diff includes package-lock.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(b'\n')
return b'package-lock.json' in files_changed
else:
raise ValueError(err)
def does_current_folder_contain_have_package_lock_file():
"""Checks whether package-lock.json exists in the current folder.
Returns:
bool. Whether the current folder includes package-lock.json.
"""
return os.path.isfile('package-lock.json')
def check_changes(filetype):
"""Checks if diff in feconf or constants file includes
changes made for release.
Args:
filetype: str. The file to check - feconf or constants.
Returns:
bool. Whether the diff includes changes made for release.
"""
if filetype == 'feconf':
filepath = FECONF_FILEPATH
keys_to_check = [b'%s = ' % key for key in KEYS_UPDATED_IN_FECONF]
elif filetype == 'constants':
filepath = CONSTANTS_FILEPATH
keys_to_check = [b'"%s": ' % key for key in KEYS_UPDATED_IN_CONSTANTS]
else:
return True
diff_output = subprocess.check_output([
'git', 'diff', filepath])[:-1].split(b'\n')
for line in diff_output:
if (line.startswith(b'-') or line.startswith(b'+')) and any(
key in line for key in keys_to_check):
return False
return True
def check_changes_in_config():
"""Checks whether feconf and assets have changes made for release
deployment.
Raises:
Exception. There are deployment changes in feconf or constants filepath.
"""
if not check_changes('feconf'):
raise Exception(
'Changes to %s made for deployment cannot be committed.' % (
FECONF_FILEPATH))
if not check_changes('constants'):
raise Exception(
'Changes to %s made for deployment cannot be committed.' % (
CONSTANTS_FILEPATH))
def check_changes_in_gcloud_path():
"""Checks that the gcloud path in common.py matches with the path in
release_constants.json.
Raises:
Exception. The gcloud path in common.py does not match with the path
in release_constants.json.
"""
with utils.open_file(RELEASE_CONSTANTS_FILEPATH, 'r') as f:
release_constants_gcloud_path = json.loads(f.read())['GCLOUD_PATH']
if not (
os.path.exists(release_constants_gcloud_path) and
os.path.samefile(release_constants_gcloud_path, common.GCLOUD_PATH)
):
raise Exception(
'The gcloud path in common.py: %s should match the path in '
'release_constants.json: %s. Please fix.' % (
common.GCLOUD_PATH, release_constants_gcloud_path))
def main(args=None):
"""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(args=args)
if args.install:
install_hook()
return
print('Running pre-commit check for feconf and constants ...')
check_changes_in_config()
print('Running pre-commit check for gcloud path changes...')
check_changes_in_gcloud_path()
print('Running pre-commit check for package-lock.json ...')
if does_diff_include_package_lock_file() and (
does_current_folder_contain_have_package_lock_file()):
# The following message is necessary since there git commit aborts
# quietly when the status is non-zero.
print('-----------COMMIT ABORTED-----------')
print(
'Oppia utilize Yarn to manage node packages. Please delete '
'package-lock.json, revert the changes in package.json, and use '
'yarn to add, update, or delete the packages. For more information '
'on how to use yarn, see https://yarnpkg.com/en/docs/usage.'
)
sys.exit(1)
return
# The 'no coverage' pragma is used as this line is un-testable. This is because
# it will only be called when pre_commit_hook.py is used as a script.
if __name__ == '__main__': # pragma: no cover
main()