##// END OF EJS Templates
fix(svn-hooks): fixed problem with svn subprocess execution fixes RCCE-62
super-admin -
r1214:77e2f888 default
parent child Browse files
Show More
@@ -1,220 +1,230 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18 import re
19 import re
19 import os
20 import os
20 import sys
21 import sys
21 import datetime
22 import datetime
22 import logging
23 import logging
23 import pkg_resources
24 import pkg_resources
24
25
25 import vcsserver
26 import vcsserver
27 import vcsserver.settings
26 from vcsserver.str_utils import safe_bytes
28 from vcsserver.str_utils import safe_bytes
27
29
28 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
29
31
30 HOOKS_DIR_MODE = 0o755
32 HOOKS_DIR_MODE = 0o755
31 HOOKS_FILE_MODE = 0o755
33 HOOKS_FILE_MODE = 0o755
32
34
33
35
34 def set_permissions_if_needed(path_to_check, perms: oct):
36 def set_permissions_if_needed(path_to_check, perms: oct):
35 # Get current permissions
37 # Get current permissions
36 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
38 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
37
39
38 # Check if current permissions are lower than required
40 # Check if current permissions are lower than required
39 if current_permissions < int(perms):
41 if current_permissions < int(perms):
40 # Change the permissions if they are lower than required
42 # Change the permissions if they are lower than required
41 os.chmod(path_to_check, perms)
43 os.chmod(path_to_check, perms)
42
44
43
45
44 def get_git_hooks_path(repo_path, bare):
46 def get_git_hooks_path(repo_path, bare):
45 hooks_path = os.path.join(repo_path, 'hooks')
47 hooks_path = os.path.join(repo_path, 'hooks')
46 if not bare:
48 if not bare:
47 hooks_path = os.path.join(repo_path, '.git', 'hooks')
49 hooks_path = os.path.join(repo_path, '.git', 'hooks')
48
50
49 return hooks_path
51 return hooks_path
50
52
51
53
52 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
54 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
53 """
55 """
54 Creates a RhodeCode hook inside a git repository
56 Creates a RhodeCode hook inside a git repository
55
57
56 :param repo_path: path to repository
58 :param repo_path: path to repository
57 :param bare: defines if repository is considered a bare git repo
59 :param bare: defines if repository is considered a bare git repo
58 :param executable: binary executable to put in the hooks
60 :param executable: binary executable to put in the hooks
59 :param force_create: Creates even if the same name hook exists
61 :param force_create: Creates even if the same name hook exists
60 """
62 """
61 executable = executable or sys.executable
63 executable = executable or sys.executable
62 hooks_path = get_git_hooks_path(repo_path, bare)
64 hooks_path = get_git_hooks_path(repo_path, bare)
63
65
64 # we always call it to ensure dir exists and it has a proper mode
66 # we always call it to ensure dir exists and it has a proper mode
65 if not os.path.exists(hooks_path):
67 if not os.path.exists(hooks_path):
66 # If it doesn't exist, create a new directory with the specified mode
68 # If it doesn't exist, create a new directory with the specified mode
67 os.makedirs(hooks_path, mode=HOOKS_DIR_MODE, exist_ok=True)
69 os.makedirs(hooks_path, mode=HOOKS_DIR_MODE, exist_ok=True)
68 # If it exists, change the directory's mode to the specified mode
70 # If it exists, change the directory's mode to the specified mode
69 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
71 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
70
72
71 tmpl_post = pkg_resources.resource_string(
73 tmpl_post = pkg_resources.resource_string(
72 'vcsserver', '/'.join(
74 'vcsserver', '/'.join(
73 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
75 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
74 tmpl_pre = pkg_resources.resource_string(
76 tmpl_pre = pkg_resources.resource_string(
75 'vcsserver', '/'.join(
77 'vcsserver', '/'.join(
76 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
78 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
77
79
78 path = '' # not used for now
80 path = '' # not used for now
79 timestamp = datetime.datetime.utcnow().isoformat()
81 timestamp = datetime.datetime.utcnow().isoformat()
80
82
81 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
83 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
82 log.debug('Installing git hook in repo %s', repo_path)
84 log.debug('Installing git hook in repo %s', repo_path)
83 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
85 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
84 _rhodecode_hook = check_rhodecode_hook(_hook_file)
86 _rhodecode_hook = check_rhodecode_hook(_hook_file)
85
87
86 if _rhodecode_hook or force_create:
88 if _rhodecode_hook or force_create:
87 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
89 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
88 try:
90 try:
89 with open(_hook_file, 'wb') as f:
91 with open(_hook_file, 'wb') as f:
90 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
92 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
91 template = template.replace(b'_DATE_', safe_bytes(timestamp))
93 template = template.replace(b'_DATE_', safe_bytes(timestamp))
92 template = template.replace(b'_ENV_', safe_bytes(executable))
94 template = template.replace(b'_ENV_', safe_bytes(executable))
93 template = template.replace(b'_PATH_', safe_bytes(path))
95 template = template.replace(b'_PATH_', safe_bytes(path))
94 f.write(template)
96 f.write(template)
95 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
97 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
96 except OSError:
98 except OSError:
97 log.exception('error writing hook file %s', _hook_file)
99 log.exception('error writing hook file %s', _hook_file)
98 else:
100 else:
99 log.debug('skipping writing hook file')
101 log.debug('skipping writing hook file')
100
102
101 return True
103 return True
102
104
103
105
104 def get_svn_hooks_path(repo_path):
106 def get_svn_hooks_path(repo_path):
105 hooks_path = os.path.join(repo_path, 'hooks')
107 hooks_path = os.path.join(repo_path, 'hooks')
106
108
107 return hooks_path
109 return hooks_path
108
110
109
111
110 def install_svn_hooks(repo_path, executable=None, force_create=False):
112 def install_svn_hooks(repo_path, executable=None, force_create=False):
111 """
113 """
112 Creates RhodeCode hooks inside a svn repository
114 Creates RhodeCode hooks inside a svn repository
113
115
114 :param repo_path: path to repository
116 :param repo_path: path to repository
115 :param executable: binary executable to put in the hooks
117 :param executable: binary executable to put in the hooks
116 :param force_create: Create even if same name hook exists
118 :param force_create: Create even if same name hook exists
117 """
119 """
118 executable = executable or sys.executable
120 executable = executable or sys.executable
119 hooks_path = get_svn_hooks_path(repo_path)
121 hooks_path = get_svn_hooks_path(repo_path)
120 if not os.path.isdir(hooks_path):
122 if not os.path.isdir(hooks_path):
121 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
123 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
122
124
123 tmpl_post = pkg_resources.resource_string(
125 tmpl_post = pkg_resources.resource_string(
124 'vcsserver', '/'.join(
126 'vcsserver', '/'.join(
125 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
127 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
126 tmpl_pre = pkg_resources.resource_string(
128 tmpl_pre = pkg_resources.resource_string(
127 'vcsserver', '/'.join(
129 'vcsserver', '/'.join(
128 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
130 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
129
131
130 path = '' # not used for now
132 path = '' # not used for now
131 timestamp = datetime.datetime.utcnow().isoformat()
133 timestamp = datetime.datetime.utcnow().isoformat()
132
134
133 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
135 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
134 log.debug('Installing svn hook in repo %s', repo_path)
136 log.debug('Installing svn hook in repo %s', repo_path)
135 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
137 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
136 _rhodecode_hook = check_rhodecode_hook(_hook_file)
138 _rhodecode_hook = check_rhodecode_hook(_hook_file)
137
139
138 if _rhodecode_hook or force_create:
140 if _rhodecode_hook or force_create:
139 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
141 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
140
142
143 env_expand = str([
144 ('RC_CORE_BINARY_DIR', vcsserver.settings.BINARY_DIR),
145 ('RC_GIT_EXECUTABLE', vcsserver.settings.GIT_EXECUTABLE),
146 ('RC_SVN_EXECUTABLE', vcsserver.settings.SVN_EXECUTABLE),
147 ('RC_SVNLOOK_EXECUTABLE', vcsserver.settings.SVNLOOK_EXECUTABLE),
148
149 ])
141 try:
150 try:
142 with open(_hook_file, 'wb') as f:
151 with open(_hook_file, 'wb') as f:
143 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
152 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
144 template = template.replace(b'_DATE_', safe_bytes(timestamp))
153 template = template.replace(b'_DATE_', safe_bytes(timestamp))
154 template = template.replace(b'_OS_EXPAND_', safe_bytes(env_expand))
145 template = template.replace(b'_ENV_', safe_bytes(executable))
155 template = template.replace(b'_ENV_', safe_bytes(executable))
146 template = template.replace(b'_PATH_', safe_bytes(path))
156 template = template.replace(b'_PATH_', safe_bytes(path))
147
157
148 f.write(template)
158 f.write(template)
149 os.chmod(_hook_file, 0o755)
159 os.chmod(_hook_file, 0o755)
150 except OSError:
160 except OSError:
151 log.exception('error writing hook file %s', _hook_file)
161 log.exception('error writing hook file %s', _hook_file)
152 else:
162 else:
153 log.debug('skipping writing hook file')
163 log.debug('skipping writing hook file')
154
164
155 return True
165 return True
156
166
157
167
158 def get_version_from_hook(hook_path):
168 def get_version_from_hook(hook_path):
159 version = b''
169 version = b''
160 hook_content = read_hook_content(hook_path)
170 hook_content = read_hook_content(hook_path)
161 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
171 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
162 if matches:
172 if matches:
163 try:
173 try:
164 version = matches.groups()[0]
174 version = matches.groups()[0]
165 log.debug('got version %s from hooks.', version)
175 log.debug('got version %s from hooks.', version)
166 except Exception:
176 except Exception:
167 log.exception("Exception while reading the hook version.")
177 log.exception("Exception while reading the hook version.")
168 return version.replace(b"'", b"")
178 return version.replace(b"'", b"")
169
179
170
180
171 def check_rhodecode_hook(hook_path):
181 def check_rhodecode_hook(hook_path):
172 """
182 """
173 Check if the hook was created by RhodeCode
183 Check if the hook was created by RhodeCode
174 """
184 """
175 if not os.path.exists(hook_path):
185 if not os.path.exists(hook_path):
176 return True
186 return True
177
187
178 log.debug('hook exists, checking if it is from RhodeCode')
188 log.debug('hook exists, checking if it is from RhodeCode')
179
189
180 version = get_version_from_hook(hook_path)
190 version = get_version_from_hook(hook_path)
181 if version:
191 if version:
182 return True
192 return True
183
193
184 return False
194 return False
185
195
186
196
187 def read_hook_content(hook_path) -> bytes:
197 def read_hook_content(hook_path) -> bytes:
188 content = b''
198 content = b''
189 if os.path.isfile(hook_path):
199 if os.path.isfile(hook_path):
190 with open(hook_path, 'rb') as f:
200 with open(hook_path, 'rb') as f:
191 content = f.read()
201 content = f.read()
192 return content
202 return content
193
203
194
204
195 def get_git_pre_hook_version(repo_path, bare):
205 def get_git_pre_hook_version(repo_path, bare):
196 hooks_path = get_git_hooks_path(repo_path, bare)
206 hooks_path = get_git_hooks_path(repo_path, bare)
197 _hook_file = os.path.join(hooks_path, 'pre-receive')
207 _hook_file = os.path.join(hooks_path, 'pre-receive')
198 version = get_version_from_hook(_hook_file)
208 version = get_version_from_hook(_hook_file)
199 return version
209 return version
200
210
201
211
202 def get_git_post_hook_version(repo_path, bare):
212 def get_git_post_hook_version(repo_path, bare):
203 hooks_path = get_git_hooks_path(repo_path, bare)
213 hooks_path = get_git_hooks_path(repo_path, bare)
204 _hook_file = os.path.join(hooks_path, 'post-receive')
214 _hook_file = os.path.join(hooks_path, 'post-receive')
205 version = get_version_from_hook(_hook_file)
215 version = get_version_from_hook(_hook_file)
206 return version
216 return version
207
217
208
218
209 def get_svn_pre_hook_version(repo_path):
219 def get_svn_pre_hook_version(repo_path):
210 hooks_path = get_svn_hooks_path(repo_path)
220 hooks_path = get_svn_hooks_path(repo_path)
211 _hook_file = os.path.join(hooks_path, 'pre-commit')
221 _hook_file = os.path.join(hooks_path, 'pre-commit')
212 version = get_version_from_hook(_hook_file)
222 version = get_version_from_hook(_hook_file)
213 return version
223 return version
214
224
215
225
216 def get_svn_post_hook_version(repo_path):
226 def get_svn_post_hook_version(repo_path):
217 hooks_path = get_svn_hooks_path(repo_path)
227 hooks_path = get_svn_hooks_path(repo_path)
218 _hook_file = os.path.join(hooks_path, 'post-commit')
228 _hook_file = os.path.join(hooks_path, 'post-commit')
219 version = get_version_from_hook(_hook_file)
229 version = get_version_from_hook(_hook_file)
220 return version
230 return version
@@ -1,50 +1,54 b''
1 #!_ENV_
1 #!_ENV_
2
2
3 import os
3 import os
4 import sys
4 import sys
5 path_adjust = [_PATH_]
5 path_adjust = [_PATH_]
6
6
7 if path_adjust:
7 if path_adjust:
8 sys.path = path_adjust
8 sys.path = path_adjust
9
9
10 try:
10 try:
11 from vcsserver import hooks
11 from vcsserver import hooks
12 except ImportError:
12 except ImportError:
13 if os.environ.get('RC_DEBUG_SVN_HOOK'):
13 if os.environ.get('RC_DEBUG_SVN_HOOK'):
14 import traceback
14 import traceback
15 print(traceback.format_exc())
15 print(traceback.format_exc())
16 hooks = None
16 hooks = None
17
17
18
18
19 # TIMESTAMP: _DATE_
19 # TIMESTAMP: _DATE_
20 RC_HOOK_VER = '_TMPL_'
20 RC_HOOK_VER = '_TMPL_'
21
21
22
22
23 # special trick to pass in some information from rc to hooks
24 # mod_dav strips ALL env vars and we can't even access things like PATH
25 for env_k, env_v in _OS_EXPAND_:
26 os.environ[env_k] = env_v
27
23 def main():
28 def main():
24 if hooks is None:
29 if hooks is None:
25 # exit with success if we cannot import vcsserver.hooks !!
30 # exit with success if we cannot import vcsserver.hooks !!
26 # this allows simply push to this repo even without rhodecode
31 # this allows simply push to this repo even without rhodecode
27 sys.exit(0)
32 sys.exit(0)
28
33
29 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
34 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
30 sys.exit(0)
35 sys.exit(0)
31 repo_path = os.getcwd()
36 repo_path = os.getcwd()
32 push_data = sys.argv[1:]
37 push_data = sys.argv[1:]
33
38
34 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
39 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
35
40
36 try:
41 try:
37 result = hooks.svn_post_commit(repo_path, push_data, os.environ)
42 result = hooks.svn_post_commit(repo_path, push_data, os.environ)
38 sys.exit(result)
43 sys.exit(result)
39 except Exception as error:
44 except Exception as error:
40 # TODO: johbo: Improve handling of this special case
45 # TODO: johbo: Improve handling of this special case
41 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
46 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
42 raise
47 raise
43 print(f'ERROR: {error}')
48 print(f'ERROR: {error}')
44 sys.exit(1)
49 sys.exit(1)
45 sys.exit(0)
50 sys.exit(0)
46
51
47
52
48
49 if __name__ == '__main__':
53 if __name__ == '__main__':
50 main()
54 main()
@@ -1,52 +1,58 b''
1 #!_ENV_
1 #!_ENV_
2
2
3 import os
3 import os
4 import sys
4 import sys
5 path_adjust = [_PATH_]
5 path_adjust = [_PATH_]
6
6
7 if path_adjust:
7 if path_adjust:
8 sys.path = path_adjust
8 sys.path = path_adjust
9
9
10 try:
10 try:
11 from vcsserver import hooks
11 from vcsserver import hooks
12 except ImportError:
12 except ImportError:
13 if os.environ.get('RC_DEBUG_SVN_HOOK'):
13 if os.environ.get('RC_DEBUG_SVN_HOOK'):
14 import traceback
14 import traceback
15 print(traceback.format_exc())
15 print(traceback.format_exc())
16 hooks = None
16 hooks = None
17
17
18
18
19 # TIMESTAMP: _DATE_
19 # TIMESTAMP: _DATE_
20 RC_HOOK_VER = '_TMPL_'
20 RC_HOOK_VER = '_TMPL_'
21
21
22
22
23 # special trick to pass in some information from rc to hooks
24 # mod_dav strips ALL env vars and we can't even access things like PATH
25 for env_k, env_v in _OS_EXPAND_:
26 os.environ[env_k] = env_v
27
23 def main():
28 def main():
24 if os.environ.get('SSH_READ_ONLY') == '1':
29 if os.environ.get('SSH_READ_ONLY') == '1':
25 sys.stderr.write('Only read-only access is allowed')
30 sys.stderr.write('Only read-only access is allowed')
26 sys.exit(1)
31 sys.exit(1)
27
32
28 if hooks is None:
33 if hooks is None:
29 # exit with success if we cannot import vcsserver.hooks !!
34 # exit with success if we cannot import vcsserver.hooks !!
30 # this allows simply push to this repo even without rhodecode
35 # this allows simply push to this repo even without rhodecode
31 sys.exit(0)
36 sys.exit(0)
37
32 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
38 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
33 sys.exit(0)
39 sys.exit(0)
34 repo_path = os.getcwd()
40 repo_path = os.getcwd()
35 push_data = sys.argv[1:]
41 push_data = sys.argv[1:]
36
42
37 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
43 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
38
44
39 try:
45 try:
40 result = hooks.svn_pre_commit(repo_path, push_data, os.environ)
46 result = hooks.svn_pre_commit(repo_path, push_data, os.environ)
41 sys.exit(result)
47 sys.exit(result)
42 except Exception as error:
48 except Exception as error:
43 # TODO: johbo: Improve handling of this special case
49 # TODO: johbo: Improve handling of this special case
44 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
50 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
45 raise
51 raise
46 print(f'ERROR: {error}')
52 print(f'ERROR: {error}')
47 sys.exit(1)
53 sys.exit(1)
48 sys.exit(0)
54 sys.exit(0)
49
55
50
56
51 if __name__ == '__main__':
57 if __name__ == '__main__':
52 main()
58 main()
@@ -1,795 +1,818 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import os
19 import os
20 import sys
20 import sys
21 import logging
21 import logging
22 import collections
22 import collections
23 import base64
23 import base64
24 import msgpack
24 import msgpack
25 import dataclasses
25 import dataclasses
26 import pygit2
26 import pygit2
27
27
28 import http.client
28 import http.client
29 from celery import Celery
29 from celery import Celery
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33
33
34 import vcsserver.settings
34 from vcsserver.lib.rc_json import json
35 from vcsserver.lib.rc_json import json
35 from vcsserver import exceptions, subprocessio, settings
36 from vcsserver import exceptions, subprocessio, settings
36 from vcsserver.str_utils import ascii_str, safe_str
37 from vcsserver.str_utils import ascii_str, safe_str
37 from vcsserver.remote.git_remote import Repository
38 from vcsserver.remote.git_remote import Repository
38
39
39 celery_app = Celery('__vcsserver__')
40 celery_app = Celery('__vcsserver__')
40 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
41
42
42
43
43 class HooksHttpClient:
44 class HooksHttpClient:
44 proto = 'msgpack.v1'
45 proto = 'msgpack.v1'
45 connection = None
46 connection = None
46
47
47 def __init__(self, hooks_uri):
48 def __init__(self, hooks_uri):
48 self.hooks_uri = hooks_uri
49 self.hooks_uri = hooks_uri
49
50
50 def __repr__(self):
51 def __repr__(self):
51 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52
53
53 def __call__(self, method, extras):
54 def __call__(self, method, extras):
54 connection = http.client.HTTPConnection(self.hooks_uri)
55 connection = http.client.HTTPConnection(self.hooks_uri)
55 # binary msgpack body
56 # binary msgpack body
56 headers, body = self._serialize(method, extras)
57 headers, body = self._serialize(method, extras)
57 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58
59
59 try:
60 try:
60 try:
61 try:
61 connection.request('POST', '/', body, headers)
62 connection.request('POST', '/', body, headers)
62 except Exception as error:
63 except Exception as error:
63 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 raise
65 raise
65
66
66 response = connection.getresponse()
67 response = connection.getresponse()
67 try:
68 try:
68 return msgpack.load(response)
69 return msgpack.load(response)
69 except Exception:
70 except Exception:
70 response_data = response.read()
71 response_data = response.read()
71 log.exception('Failed to decode hook response json data. '
72 log.exception('Failed to decode hook response json data. '
72 'response_code:%s, raw_data:%s',
73 'response_code:%s, raw_data:%s',
73 response.status, response_data)
74 response.status, response_data)
74 raise
75 raise
75 finally:
76 finally:
76 connection.close()
77 connection.close()
77
78
78 @classmethod
79 @classmethod
79 def _serialize(cls, hook_name, extras):
80 def _serialize(cls, hook_name, extras):
80 data = {
81 data = {
81 'method': hook_name,
82 'method': hook_name,
82 'extras': extras
83 'extras': extras
83 }
84 }
84 headers = {
85 headers = {
85 "rc-hooks-protocol": cls.proto,
86 "rc-hooks-protocol": cls.proto,
86 "Connection": "keep-alive"
87 "Connection": "keep-alive"
87 }
88 }
88 return headers, msgpack.packb(data)
89 return headers, msgpack.packb(data)
89
90
90
91
91 class HooksCeleryClient:
92 class HooksCeleryClient:
92 TASK_TIMEOUT = 60 # time in seconds
93 TASK_TIMEOUT = 60 # time in seconds
93
94
94 def __init__(self, queue, backend):
95 def __init__(self, queue, backend):
95 celery_app.config_from_object({
96 celery_app.config_from_object({
96 'broker_url': queue, 'result_backend': backend,
97 'broker_url': queue, 'result_backend': backend,
97 'broker_connection_retry_on_startup': True,
98 'broker_connection_retry_on_startup': True,
98 'task_serializer': 'msgpack',
99 'task_serializer': 'msgpack',
99 'accept_content': ['json', 'msgpack'],
100 'accept_content': ['json', 'msgpack'],
100 'result_serializer': 'msgpack',
101 'result_serializer': 'msgpack',
101 'result_accept_content': ['json', 'msgpack']
102 'result_accept_content': ['json', 'msgpack']
102 })
103 })
103 self.celery_app = celery_app
104 self.celery_app = celery_app
104
105
105 def __call__(self, method, extras):
106 def __call__(self, method, extras):
106 inquired_task = self.celery_app.signature(
107 inquired_task = self.celery_app.signature(
107 f'rhodecode.lib.celerylib.tasks.{method}'
108 f'rhodecode.lib.celerylib.tasks.{method}'
108 )
109 )
109 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
110
111
111
112
112 class HooksShadowRepoClient:
113 class HooksShadowRepoClient:
113
114
114 def __call__(self, hook_name, extras):
115 def __call__(self, hook_name, extras):
115 return {'output': '', 'status': 0}
116 return {'output': '', 'status': 0}
116
117
117
118
118 class RemoteMessageWriter:
119 class RemoteMessageWriter:
119 """Writer base class."""
120 """Writer base class."""
120 def write(self, message):
121 def write(self, message):
121 raise NotImplementedError()
122 raise NotImplementedError()
122
123
123
124
124 class HgMessageWriter(RemoteMessageWriter):
125 class HgMessageWriter(RemoteMessageWriter):
125 """Writer that knows how to send messages to mercurial clients."""
126 """Writer that knows how to send messages to mercurial clients."""
126
127
127 def __init__(self, ui):
128 def __init__(self, ui):
128 self.ui = ui
129 self.ui = ui
129
130
130 def write(self, message: str):
131 def write(self, message: str):
131 # TODO: Check why the quiet flag is set by default.
132 # TODO: Check why the quiet flag is set by default.
132 old = self.ui.quiet
133 old = self.ui.quiet
133 self.ui.quiet = False
134 self.ui.quiet = False
134 self.ui.status(message.encode('utf-8'))
135 self.ui.status(message.encode('utf-8'))
135 self.ui.quiet = old
136 self.ui.quiet = old
136
137
137
138
138 class GitMessageWriter(RemoteMessageWriter):
139 class GitMessageWriter(RemoteMessageWriter):
139 """Writer that knows how to send messages to git clients."""
140 """Writer that knows how to send messages to git clients."""
140
141
141 def __init__(self, stdout=None):
142 def __init__(self, stdout=None):
142 self.stdout = stdout or sys.stdout
143 self.stdout = stdout or sys.stdout
143
144
144 def write(self, message: str):
145 def write(self, message: str):
145 self.stdout.write(message)
146 self.stdout.write(message)
146
147
147
148
148 class SvnMessageWriter(RemoteMessageWriter):
149 class SvnMessageWriter(RemoteMessageWriter):
149 """Writer that knows how to send messages to svn clients."""
150 """Writer that knows how to send messages to svn clients."""
150
151
151 def __init__(self, stderr=None):
152 def __init__(self, stderr=None):
152 # SVN needs data sent to stderr for back-to-client messaging
153 # SVN needs data sent to stderr for back-to-client messaging
153 self.stderr = stderr or sys.stderr
154 self.stderr = stderr or sys.stderr
154
155
155 def write(self, message):
156 def write(self, message):
156 self.stderr.write(message.encode('utf-8'))
157 self.stderr.write(message)
157
158
158
159
159 def _handle_exception(result):
160 def _handle_exception(result):
160 exception_class = result.get('exception')
161 exception_class = result.get('exception')
161 exception_traceback = result.get('exception_traceback')
162 exception_traceback = result.get('exception_traceback')
162 log.debug('Handling hook-call exception: %s', exception_class)
163 log.debug('Handling hook-call exception: %s', exception_class)
163
164
164 if exception_traceback:
165 if exception_traceback:
165 log.error('Got traceback from remote call:%s', exception_traceback)
166 log.error('Got traceback from remote call:%s', exception_traceback)
166
167
167 if exception_class == 'HTTPLockedRC':
168 if exception_class == 'HTTPLockedRC':
168 raise exceptions.RepositoryLockedException()(*result['exception_args'])
169 raise exceptions.RepositoryLockedException()(*result['exception_args'])
169 elif exception_class == 'HTTPBranchProtected':
170 elif exception_class == 'HTTPBranchProtected':
170 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
171 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
171 elif exception_class == 'RepositoryError':
172 elif exception_class == 'RepositoryError':
172 raise exceptions.VcsException()(*result['exception_args'])
173 raise exceptions.VcsException()(*result['exception_args'])
173 elif exception_class:
174 elif exception_class:
174 raise Exception(
175 raise Exception(
175 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
176 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
176 )
177 )
177
178
178
179
179 def _get_hooks_client(extras):
180 def _get_hooks_client(extras):
180 hooks_uri = extras.get('hooks_uri')
181 hooks_uri = extras.get('hooks_uri')
181 task_queue = extras.get('task_queue')
182 task_queue = extras.get('task_queue')
182 task_backend = extras.get('task_backend')
183 task_backend = extras.get('task_backend')
183 is_shadow_repo = extras.get('is_shadow_repo')
184 is_shadow_repo = extras.get('is_shadow_repo')
184
185
185 if hooks_uri:
186 if hooks_uri:
186 return HooksHttpClient(hooks_uri)
187 return HooksHttpClient(hooks_uri)
187 elif task_queue and task_backend:
188 elif task_queue and task_backend:
188 return HooksCeleryClient(task_queue, task_backend)
189 return HooksCeleryClient(task_queue, task_backend)
189 elif is_shadow_repo:
190 elif is_shadow_repo:
190 return HooksShadowRepoClient()
191 return HooksShadowRepoClient()
191 else:
192 else:
192 raise Exception("Hooks client not found!")
193 raise Exception("Hooks client not found!")
193
194
194
195
195 def _call_hook(hook_name, extras, writer):
196 def _call_hook(hook_name, extras, writer):
196 hooks_client = _get_hooks_client(extras)
197 hooks_client = _get_hooks_client(extras)
197 log.debug('Hooks, using client:%s', hooks_client)
198 log.debug('Hooks, using client:%s', hooks_client)
198 result = hooks_client(hook_name, extras)
199 result = hooks_client(hook_name, extras)
199 log.debug('Hooks got result: %s', result)
200 log.debug('Hooks got result: %s', result)
200 _handle_exception(result)
201 _handle_exception(result)
201 writer.write(result['output'])
202 writer.write(result['output'])
202
203
203 return result['status']
204 return result['status']
204
205
205
206
206 def _extras_from_ui(ui):
207 def _extras_from_ui(ui):
207 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
208 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
208 if not hook_data:
209 if not hook_data:
209 # maybe it's inside environ ?
210 # maybe it's inside environ ?
210 env_hook_data = os.environ.get('RC_SCM_DATA')
211 env_hook_data = os.environ.get('RC_SCM_DATA')
211 if env_hook_data:
212 if env_hook_data:
212 hook_data = env_hook_data
213 hook_data = env_hook_data
213
214
214 extras = {}
215 extras = {}
215 if hook_data:
216 if hook_data:
216 extras = json.loads(hook_data)
217 extras = json.loads(hook_data)
217 return extras
218 return extras
218
219
219
220
220 def _rev_range_hash(repo, node, check_heads=False):
221 def _rev_range_hash(repo, node, check_heads=False):
221 from vcsserver.hgcompat import get_ctx
222 from vcsserver.hgcompat import get_ctx
222
223
223 commits = []
224 commits = []
224 revs = []
225 revs = []
225 start = get_ctx(repo, node).rev()
226 start = get_ctx(repo, node).rev()
226 end = len(repo)
227 end = len(repo)
227 for rev in range(start, end):
228 for rev in range(start, end):
228 revs.append(rev)
229 revs.append(rev)
229 ctx = get_ctx(repo, rev)
230 ctx = get_ctx(repo, rev)
230 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
231 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
231 branch = safe_str(ctx.branch())
232 branch = safe_str(ctx.branch())
232 commits.append((commit_id, branch))
233 commits.append((commit_id, branch))
233
234
234 parent_heads = []
235 parent_heads = []
235 if check_heads:
236 if check_heads:
236 parent_heads = _check_heads(repo, start, end, revs)
237 parent_heads = _check_heads(repo, start, end, revs)
237 return commits, parent_heads
238 return commits, parent_heads
238
239
239
240
240 def _check_heads(repo, start, end, commits):
241 def _check_heads(repo, start, end, commits):
241 from vcsserver.hgcompat import get_ctx
242 from vcsserver.hgcompat import get_ctx
242 changelog = repo.changelog
243 changelog = repo.changelog
243 parents = set()
244 parents = set()
244
245
245 for new_rev in commits:
246 for new_rev in commits:
246 for p in changelog.parentrevs(new_rev):
247 for p in changelog.parentrevs(new_rev):
247 if p == mercurial.node.nullrev:
248 if p == mercurial.node.nullrev:
248 continue
249 continue
249 if p < start:
250 if p < start:
250 parents.add(p)
251 parents.add(p)
251
252
252 for p in parents:
253 for p in parents:
253 branch = get_ctx(repo, p).branch()
254 branch = get_ctx(repo, p).branch()
254 # The heads descending from that parent, on the same branch
255 # The heads descending from that parent, on the same branch
255 parent_heads = {p}
256 parent_heads = {p}
256 reachable = {p}
257 reachable = {p}
257 for x in range(p + 1, end):
258 for x in range(p + 1, end):
258 if get_ctx(repo, x).branch() != branch:
259 if get_ctx(repo, x).branch() != branch:
259 continue
260 continue
260 for pp in changelog.parentrevs(x):
261 for pp in changelog.parentrevs(x):
261 if pp in reachable:
262 if pp in reachable:
262 reachable.add(x)
263 reachable.add(x)
263 parent_heads.discard(pp)
264 parent_heads.discard(pp)
264 parent_heads.add(x)
265 parent_heads.add(x)
265 # More than one head? Suggest merging
266 # More than one head? Suggest merging
266 if len(parent_heads) > 1:
267 if len(parent_heads) > 1:
267 return list(parent_heads)
268 return list(parent_heads)
268
269
269 return []
270 return []
270
271
271
272
272 def _get_git_env():
273 def _get_git_env():
273 env = {}
274 env = {}
274 for k, v in os.environ.items():
275 for k, v in os.environ.items():
275 if k.startswith('GIT'):
276 if k.startswith('GIT'):
276 env[k] = v
277 env[k] = v
277
278
278 # serialized version
279 # serialized version
279 return [(k, v) for k, v in env.items()]
280 return [(k, v) for k, v in env.items()]
280
281
281
282
282 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
283 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
283 env = {}
284 env = {}
284 for k, v in os.environ.items():
285 for k, v in os.environ.items():
285 if k.startswith('HG'):
286 if k.startswith('HG'):
286 env[k] = v
287 env[k] = v
287
288
288 env['HG_NODE'] = old_rev
289 env['HG_NODE'] = old_rev
289 env['HG_NODE_LAST'] = new_rev
290 env['HG_NODE_LAST'] = new_rev
290 env['HG_TXNID'] = txnid
291 env['HG_TXNID'] = txnid
291 env['HG_PENDING'] = repo_path
292 env['HG_PENDING'] = repo_path
292
293
293 return [(k, v) for k, v in env.items()]
294 return [(k, v) for k, v in env.items()]
294
295
295
296
297 def _fix_hooks_executables():
298 """
299 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
300 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
301 because svn is not on PATH
302 """
303 vcsserver.settings.BINARY_DIR = (
304 os.environ.get('RC_BINARY_DIR') or vcsserver.settings.BINARY_DIR)
305 vcsserver.settings.GIT_EXECUTABLE = (
306 os.environ.get('RC_GIT_EXECUTABLE') or vcsserver.settings.GIT_EXECUTABLE)
307 vcsserver.settings.SVN_EXECUTABLE = (
308 os.environ.get('RC_SVN_EXECUTABLE') or vcsserver.settings.SVN_EXECUTABLE)
309 vcsserver.settings.SVNLOOK_EXECUTABLE = (
310 os.environ.get('RC_SVNLOOK_EXECUTABLE') or vcsserver.settings.SVNLOOK_EXECUTABLE)
311
312
296 def repo_size(ui, repo, **kwargs):
313 def repo_size(ui, repo, **kwargs):
297 extras = _extras_from_ui(ui)
314 extras = _extras_from_ui(ui)
298 return _call_hook('repo_size', extras, HgMessageWriter(ui))
315 return _call_hook('repo_size', extras, HgMessageWriter(ui))
299
316
300
317
301 def pre_pull(ui, repo, **kwargs):
318 def pre_pull(ui, repo, **kwargs):
302 extras = _extras_from_ui(ui)
319 extras = _extras_from_ui(ui)
303 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
320 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
304
321
305
322
306 def pre_pull_ssh(ui, repo, **kwargs):
323 def pre_pull_ssh(ui, repo, **kwargs):
307 extras = _extras_from_ui(ui)
324 extras = _extras_from_ui(ui)
308 if extras and extras.get('SSH'):
325 if extras and extras.get('SSH'):
309 return pre_pull(ui, repo, **kwargs)
326 return pre_pull(ui, repo, **kwargs)
310 return 0
327 return 0
311
328
312
329
313 def post_pull(ui, repo, **kwargs):
330 def post_pull(ui, repo, **kwargs):
314 extras = _extras_from_ui(ui)
331 extras = _extras_from_ui(ui)
315 return _call_hook('post_pull', extras, HgMessageWriter(ui))
332 return _call_hook('post_pull', extras, HgMessageWriter(ui))
316
333
317
334
318 def post_pull_ssh(ui, repo, **kwargs):
335 def post_pull_ssh(ui, repo, **kwargs):
319 extras = _extras_from_ui(ui)
336 extras = _extras_from_ui(ui)
320 if extras and extras.get('SSH'):
337 if extras and extras.get('SSH'):
321 return post_pull(ui, repo, **kwargs)
338 return post_pull(ui, repo, **kwargs)
322 return 0
339 return 0
323
340
324
341
325 def pre_push(ui, repo, node=None, **kwargs):
342 def pre_push(ui, repo, node=None, **kwargs):
326 """
343 """
327 Mercurial pre_push hook
344 Mercurial pre_push hook
328 """
345 """
329 extras = _extras_from_ui(ui)
346 extras = _extras_from_ui(ui)
330 detect_force_push = extras.get('detect_force_push')
347 detect_force_push = extras.get('detect_force_push')
331
348
332 rev_data = []
349 rev_data = []
333 hook_type: str = safe_str(kwargs.get('hooktype'))
350 hook_type: str = safe_str(kwargs.get('hooktype'))
334
351
335 if node and hook_type == 'pretxnchangegroup':
352 if node and hook_type == 'pretxnchangegroup':
336 branches = collections.defaultdict(list)
353 branches = collections.defaultdict(list)
337 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
354 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
338 for commit_id, branch in commits:
355 for commit_id, branch in commits:
339 branches[branch].append(commit_id)
356 branches[branch].append(commit_id)
340
357
341 for branch, commits in branches.items():
358 for branch, commits in branches.items():
342 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
359 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
343 rev_data.append({
360 rev_data.append({
344 'total_commits': len(commits),
361 'total_commits': len(commits),
345 'old_rev': old_rev,
362 'old_rev': old_rev,
346 'new_rev': commits[-1],
363 'new_rev': commits[-1],
347 'ref': '',
364 'ref': '',
348 'type': 'branch',
365 'type': 'branch',
349 'name': branch,
366 'name': branch,
350 })
367 })
351
368
352 for push_ref in rev_data:
369 for push_ref in rev_data:
353 push_ref['multiple_heads'] = _heads
370 push_ref['multiple_heads'] = _heads
354
371
355 repo_path = os.path.join(
372 repo_path = os.path.join(
356 extras.get('repo_store', ''), extras.get('repository', ''))
373 extras.get('repo_store', ''), extras.get('repository', ''))
357 push_ref['hg_env'] = _get_hg_env(
374 push_ref['hg_env'] = _get_hg_env(
358 old_rev=push_ref['old_rev'],
375 old_rev=push_ref['old_rev'],
359 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
376 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
360 repo_path=repo_path)
377 repo_path=repo_path)
361
378
362 extras['hook_type'] = hook_type or 'pre_push'
379 extras['hook_type'] = hook_type or 'pre_push'
363 extras['commit_ids'] = rev_data
380 extras['commit_ids'] = rev_data
364
381
365 return _call_hook('pre_push', extras, HgMessageWriter(ui))
382 return _call_hook('pre_push', extras, HgMessageWriter(ui))
366
383
367
384
368 def pre_push_ssh(ui, repo, node=None, **kwargs):
385 def pre_push_ssh(ui, repo, node=None, **kwargs):
369 extras = _extras_from_ui(ui)
386 extras = _extras_from_ui(ui)
370 if extras.get('SSH'):
387 if extras.get('SSH'):
371 return pre_push(ui, repo, node, **kwargs)
388 return pre_push(ui, repo, node, **kwargs)
372
389
373 return 0
390 return 0
374
391
375
392
376 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
393 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
377 """
394 """
378 Mercurial pre_push hook for SSH
395 Mercurial pre_push hook for SSH
379 """
396 """
380 extras = _extras_from_ui(ui)
397 extras = _extras_from_ui(ui)
381 if extras.get('SSH'):
398 if extras.get('SSH'):
382 permission = extras['SSH_PERMISSIONS']
399 permission = extras['SSH_PERMISSIONS']
383
400
384 if 'repository.write' == permission or 'repository.admin' == permission:
401 if 'repository.write' == permission or 'repository.admin' == permission:
385 return 0
402 return 0
386
403
387 # non-zero ret code
404 # non-zero ret code
388 return 1
405 return 1
389
406
390 return 0
407 return 0
391
408
392
409
393 def post_push(ui, repo, node, **kwargs):
410 def post_push(ui, repo, node, **kwargs):
394 """
411 """
395 Mercurial post_push hook
412 Mercurial post_push hook
396 """
413 """
397 extras = _extras_from_ui(ui)
414 extras = _extras_from_ui(ui)
398
415
399 commit_ids = []
416 commit_ids = []
400 branches = []
417 branches = []
401 bookmarks = []
418 bookmarks = []
402 tags = []
419 tags = []
403 hook_type: str = safe_str(kwargs.get('hooktype'))
420 hook_type: str = safe_str(kwargs.get('hooktype'))
404
421
405 commits, _heads = _rev_range_hash(repo, node)
422 commits, _heads = _rev_range_hash(repo, node)
406 for commit_id, branch in commits:
423 for commit_id, branch in commits:
407 commit_ids.append(commit_id)
424 commit_ids.append(commit_id)
408 if branch not in branches:
425 if branch not in branches:
409 branches.append(branch)
426 branches.append(branch)
410
427
411 if hasattr(ui, '_rc_pushkey_bookmarks'):
428 if hasattr(ui, '_rc_pushkey_bookmarks'):
412 bookmarks = ui._rc_pushkey_bookmarks
429 bookmarks = ui._rc_pushkey_bookmarks
413
430
414 extras['hook_type'] = hook_type or 'post_push'
431 extras['hook_type'] = hook_type or 'post_push'
415 extras['commit_ids'] = commit_ids
432 extras['commit_ids'] = commit_ids
416
433
417 extras['new_refs'] = {
434 extras['new_refs'] = {
418 'branches': branches,
435 'branches': branches,
419 'bookmarks': bookmarks,
436 'bookmarks': bookmarks,
420 'tags': tags
437 'tags': tags
421 }
438 }
422
439
423 return _call_hook('post_push', extras, HgMessageWriter(ui))
440 return _call_hook('post_push', extras, HgMessageWriter(ui))
424
441
425
442
426 def post_push_ssh(ui, repo, node, **kwargs):
443 def post_push_ssh(ui, repo, node, **kwargs):
427 """
444 """
428 Mercurial post_push hook for SSH
445 Mercurial post_push hook for SSH
429 """
446 """
430 if _extras_from_ui(ui).get('SSH'):
447 if _extras_from_ui(ui).get('SSH'):
431 return post_push(ui, repo, node, **kwargs)
448 return post_push(ui, repo, node, **kwargs)
432 return 0
449 return 0
433
450
434
451
435 def key_push(ui, repo, **kwargs):
452 def key_push(ui, repo, **kwargs):
436 from vcsserver.hgcompat import get_ctx
453 from vcsserver.hgcompat import get_ctx
437
454
438 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
455 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
439 # store new bookmarks in our UI object propagated later to post_push
456 # store new bookmarks in our UI object propagated later to post_push
440 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
457 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
441 return
458 return
442
459
443
460
444 # backward compat
461 # backward compat
445 log_pull_action = post_pull
462 log_pull_action = post_pull
446
463
447 # backward compat
464 # backward compat
448 log_push_action = post_push
465 log_push_action = post_push
449
466
450
467
451 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
468 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
452 """
469 """
453 Old hook name: keep here for backward compatibility.
470 Old hook name: keep here for backward compatibility.
454
471
455 This is only required when the installed git hooks are not upgraded.
472 This is only required when the installed git hooks are not upgraded.
456 """
473 """
457 pass
474 pass
458
475
459
476
460 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
477 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
461 """
478 """
462 Old hook name: keep here for backward compatibility.
479 Old hook name: keep here for backward compatibility.
463
480
464 This is only required when the installed git hooks are not upgraded.
481 This is only required when the installed git hooks are not upgraded.
465 """
482 """
466 pass
483 pass
467
484
468
485
469 @dataclasses.dataclass
486 @dataclasses.dataclass
470 class HookResponse:
487 class HookResponse:
471 status: int
488 status: int
472 output: str
489 output: str
473
490
474
491
475 def git_pre_pull(extras) -> HookResponse:
492 def git_pre_pull(extras) -> HookResponse:
476 """
493 """
477 Pre pull hook.
494 Pre pull hook.
478
495
479 :param extras: dictionary containing the keys defined in simplevcs
496 :param extras: dictionary containing the keys defined in simplevcs
480 :type extras: dict
497 :type extras: dict
481
498
482 :return: status code of the hook. 0 for success.
499 :return: status code of the hook. 0 for success.
483 :rtype: int
500 :rtype: int
484 """
501 """
485
502
486 if 'pull' not in extras['hooks']:
503 if 'pull' not in extras['hooks']:
487 return HookResponse(0, '')
504 return HookResponse(0, '')
488
505
489 stdout = io.StringIO()
506 stdout = io.StringIO()
490 try:
507 try:
491 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
508 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
492
509
493 except Exception as error:
510 except Exception as error:
494 log.exception('Failed to call pre_pull hook')
511 log.exception('Failed to call pre_pull hook')
495 status_code = 128
512 status_code = 128
496 stdout.write(f'ERROR: {error}\n')
513 stdout.write(f'ERROR: {error}\n')
497
514
498 return HookResponse(status_code, stdout.getvalue())
515 return HookResponse(status_code, stdout.getvalue())
499
516
500
517
501 def git_post_pull(extras) -> HookResponse:
518 def git_post_pull(extras) -> HookResponse:
502 """
519 """
503 Post pull hook.
520 Post pull hook.
504
521
505 :param extras: dictionary containing the keys defined in simplevcs
522 :param extras: dictionary containing the keys defined in simplevcs
506 :type extras: dict
523 :type extras: dict
507
524
508 :return: status code of the hook. 0 for success.
525 :return: status code of the hook. 0 for success.
509 :rtype: int
526 :rtype: int
510 """
527 """
511 if 'pull' not in extras['hooks']:
528 if 'pull' not in extras['hooks']:
512 return HookResponse(0, '')
529 return HookResponse(0, '')
513
530
514 stdout = io.StringIO()
531 stdout = io.StringIO()
515 try:
532 try:
516 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
533 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
517 except Exception as error:
534 except Exception as error:
518 status = 128
535 status = 128
519 stdout.write(f'ERROR: {error}\n')
536 stdout.write(f'ERROR: {error}\n')
520
537
521 return HookResponse(status, stdout.getvalue())
538 return HookResponse(status, stdout.getvalue())
522
539
523
540
524 def _parse_git_ref_lines(revision_lines):
541 def _parse_git_ref_lines(revision_lines):
525 rev_data = []
542 rev_data = []
526 for revision_line in revision_lines or []:
543 for revision_line in revision_lines or []:
527 old_rev, new_rev, ref = revision_line.strip().split(' ')
544 old_rev, new_rev, ref = revision_line.strip().split(' ')
528 ref_data = ref.split('/', 2)
545 ref_data = ref.split('/', 2)
529 if ref_data[1] in ('tags', 'heads'):
546 if ref_data[1] in ('tags', 'heads'):
530 rev_data.append({
547 rev_data.append({
531 # NOTE(marcink):
548 # NOTE(marcink):
532 # we're unable to tell total_commits for git at this point
549 # we're unable to tell total_commits for git at this point
533 # but we set the variable for consistency with GIT
550 # but we set the variable for consistency with GIT
534 'total_commits': -1,
551 'total_commits': -1,
535 'old_rev': old_rev,
552 'old_rev': old_rev,
536 'new_rev': new_rev,
553 'new_rev': new_rev,
537 'ref': ref,
554 'ref': ref,
538 'type': ref_data[1],
555 'type': ref_data[1],
539 'name': ref_data[2],
556 'name': ref_data[2],
540 })
557 })
541 return rev_data
558 return rev_data
542
559
543
560
544 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
561 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
545 """
562 """
546 Pre push hook.
563 Pre push hook.
547
564
548 :return: status code of the hook. 0 for success.
565 :return: status code of the hook. 0 for success.
549 """
566 """
550 extras = json.loads(env['RC_SCM_DATA'])
567 extras = json.loads(env['RC_SCM_DATA'])
551 rev_data = _parse_git_ref_lines(revision_lines)
568 rev_data = _parse_git_ref_lines(revision_lines)
552 if 'push' not in extras['hooks']:
569 if 'push' not in extras['hooks']:
553 return 0
570 return 0
554 empty_commit_id = '0' * 40
571 empty_commit_id = '0' * 40
555
572
556 detect_force_push = extras.get('detect_force_push')
573 detect_force_push = extras.get('detect_force_push')
557
574 _fix_hooks_executables()
558 for push_ref in rev_data:
575 for push_ref in rev_data:
559 # store our git-env which holds the temp store
576 # store our git-env which holds the temp store
560 push_ref['git_env'] = _get_git_env()
577 push_ref['git_env'] = _get_git_env()
561 push_ref['pruned_sha'] = ''
578 push_ref['pruned_sha'] = ''
562 if not detect_force_push:
579 if not detect_force_push:
563 # don't check for forced-push when we don't need to
580 # don't check for forced-push when we don't need to
564 continue
581 continue
565
582
566 type_ = push_ref['type']
583 type_ = push_ref['type']
567 new_branch = push_ref['old_rev'] == empty_commit_id
584 new_branch = push_ref['old_rev'] == empty_commit_id
568 delete_branch = push_ref['new_rev'] == empty_commit_id
585 delete_branch = push_ref['new_rev'] == empty_commit_id
569 if type_ == 'heads' and not (new_branch or delete_branch):
586 if type_ == 'heads' and not (new_branch or delete_branch):
570 old_rev = push_ref['old_rev']
587 old_rev = push_ref['old_rev']
571 new_rev = push_ref['new_rev']
588 new_rev = push_ref['new_rev']
572 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
589 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
573 stdout, stderr = subprocessio.run_command(
590 stdout, stderr = subprocessio.run_command(
574 cmd, env=os.environ.copy())
591 cmd, env=os.environ.copy())
575 # means we're having some non-reachable objects, this forced push was used
592 # means we're having some non-reachable objects, this forced push was used
576 if stdout:
593 if stdout:
577 push_ref['pruned_sha'] = stdout.splitlines()
594 push_ref['pruned_sha'] = stdout.splitlines()
578
595
579 extras['hook_type'] = 'pre_receive'
596 extras['hook_type'] = 'pre_receive'
580 extras['commit_ids'] = rev_data
597 extras['commit_ids'] = rev_data
581
598
582 stdout = sys.stdout
599 stdout = sys.stdout
583 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
600 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
584
601
585 return status_code
602 return status_code
586
603
587
604
588 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
605 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
589 """
606 """
590 Post push hook.
607 Post push hook.
591
608
592 :return: status code of the hook. 0 for success.
609 :return: status code of the hook. 0 for success.
593 """
610 """
594 extras = json.loads(env['RC_SCM_DATA'])
611 extras = json.loads(env['RC_SCM_DATA'])
595 if 'push' not in extras['hooks']:
612 if 'push' not in extras['hooks']:
596 return 0
613 return 0
614 _fix_hooks_executables()
597
615
598 rev_data = _parse_git_ref_lines(revision_lines)
616 rev_data = _parse_git_ref_lines(revision_lines)
599
617
600 git_revs = []
618 git_revs = []
601
619
602 # N.B.(skreft): it is ok to just call git, as git before calling a
620 # N.B.(skreft): it is ok to just call git, as git before calling a
603 # subcommand sets the PATH environment variable so that it point to the
621 # subcommand sets the PATH environment variable so that it point to the
604 # correct version of the git executable.
622 # correct version of the git executable.
605 empty_commit_id = '0' * 40
623 empty_commit_id = '0' * 40
606 branches = []
624 branches = []
607 tags = []
625 tags = []
608 for push_ref in rev_data:
626 for push_ref in rev_data:
609 type_ = push_ref['type']
627 type_ = push_ref['type']
610
628
611 if type_ == 'heads':
629 if type_ == 'heads':
612 # starting new branch case
630 # starting new branch case
613 if push_ref['old_rev'] == empty_commit_id:
631 if push_ref['old_rev'] == empty_commit_id:
614 push_ref_name = push_ref['name']
632 push_ref_name = push_ref['name']
615
633
616 if push_ref_name not in branches:
634 if push_ref_name not in branches:
617 branches.append(push_ref_name)
635 branches.append(push_ref_name)
618
636
619 need_head_set = ''
637 need_head_set = ''
620 with Repository(os.getcwd()) as repo:
638 with Repository(os.getcwd()) as repo:
621 try:
639 try:
622 repo.head
640 repo.head
623 except pygit2.GitError:
641 except pygit2.GitError:
624 need_head_set = f'refs/heads/{push_ref_name}'
642 need_head_set = f'refs/heads/{push_ref_name}'
625
643
626 if need_head_set:
644 if need_head_set:
627 repo.set_head(need_head_set)
645 repo.set_head(need_head_set)
628 print(f"Setting default branch to {push_ref_name}")
646 print(f"Setting default branch to {push_ref_name}")
629
647
630 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
648 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
631 stdout, stderr = subprocessio.run_command(
649 stdout, stderr = subprocessio.run_command(
632 cmd, env=os.environ.copy())
650 cmd, env=os.environ.copy())
633 heads = safe_str(stdout)
651 heads = safe_str(stdout)
634 heads = heads.replace(push_ref['ref'], '')
652 heads = heads.replace(push_ref['ref'], '')
635 heads = ' '.join(head for head
653 heads = ' '.join(head for head
636 in heads.splitlines() if head) or '.'
654 in heads.splitlines() if head) or '.'
637 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
655 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
638 '--pretty=format:%H', '--', push_ref['new_rev'],
656 '--pretty=format:%H', '--', push_ref['new_rev'],
639 '--not', heads]
657 '--not', heads]
640 stdout, stderr = subprocessio.run_command(
658 stdout, stderr = subprocessio.run_command(
641 cmd, env=os.environ.copy())
659 cmd, env=os.environ.copy())
642 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
660 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
643
661
644 # delete branch case
662 # delete branch case
645 elif push_ref['new_rev'] == empty_commit_id:
663 elif push_ref['new_rev'] == empty_commit_id:
646 git_revs.append(f'delete_branch=>{push_ref["name"]}')
664 git_revs.append(f'delete_branch=>{push_ref["name"]}')
647 else:
665 else:
648 if push_ref['name'] not in branches:
666 if push_ref['name'] not in branches:
649 branches.append(push_ref['name'])
667 branches.append(push_ref['name'])
650
668
651 cmd = [settings.GIT_EXECUTABLE, 'log',
669 cmd = [settings.GIT_EXECUTABLE, 'log',
652 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
670 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
653 '--reverse', '--pretty=format:%H']
671 '--reverse', '--pretty=format:%H']
654 stdout, stderr = subprocessio.run_command(
672 stdout, stderr = subprocessio.run_command(
655 cmd, env=os.environ.copy())
673 cmd, env=os.environ.copy())
656 # we get bytes from stdout, we need str to be consistent
674 # we get bytes from stdout, we need str to be consistent
657 log_revs = list(map(ascii_str, stdout.splitlines()))
675 log_revs = list(map(ascii_str, stdout.splitlines()))
658 git_revs.extend(log_revs)
676 git_revs.extend(log_revs)
659
677
660 # Pure pygit2 impl. but still 2-3x slower :/
678 # Pure pygit2 impl. but still 2-3x slower :/
661 # results = []
679 # results = []
662 #
680 #
663 # with Repository(os.getcwd()) as repo:
681 # with Repository(os.getcwd()) as repo:
664 # repo_new_rev = repo[push_ref['new_rev']]
682 # repo_new_rev = repo[push_ref['new_rev']]
665 # repo_old_rev = repo[push_ref['old_rev']]
683 # repo_old_rev = repo[push_ref['old_rev']]
666 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
684 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
667 #
685 #
668 # for commit in walker:
686 # for commit in walker:
669 # if commit.id == repo_old_rev.id:
687 # if commit.id == repo_old_rev.id:
670 # break
688 # break
671 # results.append(commit.id.hex)
689 # results.append(commit.id.hex)
672 # # reverse the order, can't use GIT_SORT_REVERSE
690 # # reverse the order, can't use GIT_SORT_REVERSE
673 # log_revs = results[::-1]
691 # log_revs = results[::-1]
674
692
675 elif type_ == 'tags':
693 elif type_ == 'tags':
676 if push_ref['name'] not in tags:
694 if push_ref['name'] not in tags:
677 tags.append(push_ref['name'])
695 tags.append(push_ref['name'])
678 git_revs.append(f'tag=>{push_ref["name"]}')
696 git_revs.append(f'tag=>{push_ref["name"]}')
679
697
680 extras['hook_type'] = 'post_receive'
698 extras['hook_type'] = 'post_receive'
681 extras['commit_ids'] = git_revs
699 extras['commit_ids'] = git_revs
682 extras['new_refs'] = {
700 extras['new_refs'] = {
683 'branches': branches,
701 'branches': branches,
684 'bookmarks': [],
702 'bookmarks': [],
685 'tags': tags,
703 'tags': tags,
686 }
704 }
687
705
688 stdout = sys.stdout
706 stdout = sys.stdout
689
707
690 if 'repo_size' in extras['hooks']:
708 if 'repo_size' in extras['hooks']:
691 try:
709 try:
692 _call_hook('repo_size', extras, GitMessageWriter(stdout))
710 _call_hook('repo_size', extras, GitMessageWriter(stdout))
693 except Exception:
711 except Exception:
694 pass
712 pass
695
713
696 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
714 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
697 return status_code
715 return status_code
698
716
699
717
700 def _get_extras_from_txn_id(path, txn_id):
718 def _get_extras_from_txn_id(path, txn_id):
701 extras = {}
719 extras = {}
702 try:
720 try:
703 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
721 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
704 '-t', txn_id,
722 '-t', txn_id,
705 '--revprop', path, 'rc-scm-extras']
723 '--revprop', path, 'rc-scm-extras']
706 stdout, stderr = subprocessio.run_command(
724 stdout, stderr = subprocessio.run_command(
707 cmd, env=os.environ.copy())
725 cmd, env=os.environ.copy())
708 extras = json.loads(base64.urlsafe_b64decode(stdout))
726 extras = json.loads(base64.urlsafe_b64decode(stdout))
709 except Exception:
727 except Exception:
710 log.exception('Failed to extract extras info from txn_id')
728 log.exception('Failed to extract extras info from txn_id')
711
729
712 return extras
730 return extras
713
731
714
732
715 def _get_extras_from_commit_id(commit_id, path):
733 def _get_extras_from_commit_id(commit_id, path):
716 extras = {}
734 extras = {}
717 try:
735 try:
718 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
736 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
719 '-r', commit_id,
737 '-r', commit_id,
720 '--revprop', path, 'rc-scm-extras']
738 '--revprop', path, 'rc-scm-extras']
721 stdout, stderr = subprocessio.run_command(
739 stdout, stderr = subprocessio.run_command(
722 cmd, env=os.environ.copy())
740 cmd, env=os.environ.copy())
723 extras = json.loads(base64.urlsafe_b64decode(stdout))
741 extras = json.loads(base64.urlsafe_b64decode(stdout))
724 except Exception:
742 except Exception:
725 log.exception('Failed to extract extras info from commit_id')
743 log.exception('Failed to extract extras info from commit_id')
726
744
727 return extras
745 return extras
728
746
729
747
730 def svn_pre_commit(repo_path, commit_data, env):
748 def svn_pre_commit(repo_path, commit_data, env):
731 path, txn_id = commit_data
749 path, txn_id = commit_data
732 branches = []
750 branches = []
733 tags = []
751 tags = []
734
752
753 _fix_hooks_executables()
735 if env.get('RC_SCM_DATA'):
754 if env.get('RC_SCM_DATA'):
736 extras = json.loads(env['RC_SCM_DATA'])
755 extras = json.loads(env['RC_SCM_DATA'])
737 else:
756 else:
738 # fallback method to read from TXN-ID stored data
757 # fallback method to read from TXN-ID stored data
739 extras = _get_extras_from_txn_id(path, txn_id)
758 extras = _get_extras_from_txn_id(path, txn_id)
740 if not extras:
759 if not extras:
741 return 0
760 return 0
742
761
743 extras['hook_type'] = 'pre_commit'
762 extras['hook_type'] = 'pre_commit'
744 extras['commit_ids'] = [txn_id]
763 extras['commit_ids'] = [txn_id]
745 extras['txn_id'] = txn_id
764 extras['txn_id'] = txn_id
746 extras['new_refs'] = {
765 extras['new_refs'] = {
747 'total_commits': 1,
766 'total_commits': 1,
748 'branches': branches,
767 'branches': branches,
749 'bookmarks': [],
768 'bookmarks': [],
750 'tags': tags,
769 'tags': tags,
751 }
770 }
752
771
753 return _call_hook('pre_push', extras, SvnMessageWriter())
772 return _call_hook('pre_push', extras, SvnMessageWriter())
754
773
755
774
756 def svn_post_commit(repo_path, commit_data, env):
775 def svn_post_commit(repo_path, commit_data, env):
757 """
776 """
758 commit_data is path, rev, txn_id
777 commit_data is path, rev, txn_id
759 """
778 """
779
760 if len(commit_data) == 3:
780 if len(commit_data) == 3:
761 path, commit_id, txn_id = commit_data
781 path, commit_id, txn_id = commit_data
762 elif len(commit_data) == 2:
782 elif len(commit_data) == 2:
763 log.error('Failed to extract txn_id from commit_data using legacy method. '
783 log.error('Failed to extract txn_id from commit_data using legacy method. '
764 'Some functionality might be limited')
784 'Some functionality might be limited')
765 path, commit_id = commit_data
785 path, commit_id = commit_data
766 txn_id = None
786 txn_id = None
787 else:
788 return 0
767
789
768 branches = []
790 branches = []
769 tags = []
791 tags = []
770
792
793 _fix_hooks_executables()
771 if env.get('RC_SCM_DATA'):
794 if env.get('RC_SCM_DATA'):
772 extras = json.loads(env['RC_SCM_DATA'])
795 extras = json.loads(env['RC_SCM_DATA'])
773 else:
796 else:
774 # fallback method to read from TXN-ID stored data
797 # fallback method to read from TXN-ID stored data
775 extras = _get_extras_from_commit_id(commit_id, path)
798 extras = _get_extras_from_commit_id(commit_id, path)
776 if not extras:
799 if not extras:
777 return 0
800 return 0
778
801
779 extras['hook_type'] = 'post_commit'
802 extras['hook_type'] = 'post_commit'
780 extras['commit_ids'] = [commit_id]
803 extras['commit_ids'] = [commit_id]
781 extras['txn_id'] = txn_id
804 extras['txn_id'] = txn_id
782 extras['new_refs'] = {
805 extras['new_refs'] = {
783 'branches': branches,
806 'branches': branches,
784 'bookmarks': [],
807 'bookmarks': [],
785 'tags': tags,
808 'tags': tags,
786 'total_commits': 1,
809 'total_commits': 1,
787 }
810 }
788
811
789 if 'repo_size' in extras['hooks']:
812 if 'repo_size' in extras['hooks']:
790 try:
813 try:
791 _call_hook('repo_size', extras, SvnMessageWriter())
814 _call_hook('repo_size', extras, SvnMessageWriter())
792 except Exception:
815 except Exception:
793 pass
816 pass
794
817
795 return _call_hook('post_push', extras, SvnMessageWriter())
818 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,775 +1,777 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import os
19 import os
20 import platform
20 import platform
21 import sys
21 import sys
22 import locale
22 import locale
23 import logging
23 import logging
24 import uuid
24 import uuid
25 import time
25 import time
26 import wsgiref.util
26 import wsgiref.util
27 import tempfile
27 import tempfile
28 import psutil
28 import psutil
29
29
30 from itertools import chain
30 from itertools import chain
31
31
32 import msgpack
32 import msgpack
33 import configparser
33 import configparser
34
34
35 from pyramid.config import Configurator
35 from pyramid.config import Configurator
36 from pyramid.wsgi import wsgiapp
36 from pyramid.wsgi import wsgiapp
37 from pyramid.response import Response
37 from pyramid.response import Response
38
38
39 from vcsserver.base import BytesEnvelope, BinaryEnvelope
39 from vcsserver.base import BytesEnvelope, BinaryEnvelope
40 from vcsserver.lib.rc_json import json
40 from vcsserver.lib.rc_json import json
41 from vcsserver.config.settings_maker import SettingsMaker
41 from vcsserver.config.settings_maker import SettingsMaker
42 from vcsserver.str_utils import safe_int
42 from vcsserver.str_utils import safe_int
43 from vcsserver.lib.statsd_client import StatsdClient
43 from vcsserver.lib.statsd_client import StatsdClient
44 from vcsserver.tweens.request_wrapper import get_headers_call_context
44 from vcsserver.tweens.request_wrapper import get_headers_call_context
45
45
46 import vcsserver
46 import vcsserver
47 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
47 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
48 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
48 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
49 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
49 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
50 from vcsserver.echo_stub.echo_app import EchoApp
50 from vcsserver.echo_stub.echo_app import EchoApp
51 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
51 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
52 from vcsserver.lib.exc_tracking import store_exception, format_exc
52 from vcsserver.lib.exc_tracking import store_exception, format_exc
53 from vcsserver.server import VcsServer
53 from vcsserver.server import VcsServer
54
54
55 strict_vcs = True
55 strict_vcs = True
56
56
57 git_import_err = None
57 git_import_err = None
58 try:
58 try:
59 from vcsserver.remote.git_remote import GitFactory, GitRemote
59 from vcsserver.remote.git_remote import GitFactory, GitRemote
60 except ImportError as e:
60 except ImportError as e:
61 GitFactory = None
61 GitFactory = None
62 GitRemote = None
62 GitRemote = None
63 git_import_err = e
63 git_import_err = e
64 if strict_vcs:
64 if strict_vcs:
65 raise
65 raise
66
66
67
67
68 hg_import_err = None
68 hg_import_err = None
69 try:
69 try:
70 from vcsserver.remote.hg_remote import MercurialFactory, HgRemote
70 from vcsserver.remote.hg_remote import MercurialFactory, HgRemote
71 except ImportError as e:
71 except ImportError as e:
72 MercurialFactory = None
72 MercurialFactory = None
73 HgRemote = None
73 HgRemote = None
74 hg_import_err = e
74 hg_import_err = e
75 if strict_vcs:
75 if strict_vcs:
76 raise
76 raise
77
77
78
78
79 svn_import_err = None
79 svn_import_err = None
80 try:
80 try:
81 from vcsserver.remote.svn_remote import SubversionFactory, SvnRemote
81 from vcsserver.remote.svn_remote import SubversionFactory, SvnRemote
82 except ImportError as e:
82 except ImportError as e:
83 SubversionFactory = None
83 SubversionFactory = None
84 SvnRemote = None
84 SvnRemote = None
85 svn_import_err = e
85 svn_import_err = e
86 if strict_vcs:
86 if strict_vcs:
87 raise
87 raise
88
88
89 log = logging.getLogger(__name__)
89 log = logging.getLogger(__name__)
90
90
91 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
91 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
92 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
92 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
93
93
94 try:
94 try:
95 locale.setlocale(locale.LC_ALL, '')
95 locale.setlocale(locale.LC_ALL, '')
96 except locale.Error as e:
96 except locale.Error as e:
97 log.error(
97 log.error(
98 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
98 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
99 os.environ['LC_ALL'] = 'C'
99 os.environ['LC_ALL'] = 'C'
100
100
101
101
102 def _is_request_chunked(environ):
102 def _is_request_chunked(environ):
103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 return stream
104 return stream
105
105
106
106
107 def log_max_fd():
107 def log_max_fd():
108 try:
108 try:
109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 log.info('Max file descriptors value: %s', maxfd)
110 log.info('Max file descriptors value: %s', maxfd)
111 except Exception:
111 except Exception:
112 pass
112 pass
113
113
114
114
115 class VCS:
115 class VCS:
116 def __init__(self, locale_conf=None, cache_config=None):
116 def __init__(self, locale_conf=None, cache_config=None):
117 self.locale = locale_conf
117 self.locale = locale_conf
118 self.cache_config = cache_config
118 self.cache_config = cache_config
119 self._configure_locale()
119 self._configure_locale()
120
120
121 log_max_fd()
121 log_max_fd()
122
122
123 if GitFactory and GitRemote:
123 if GitFactory and GitRemote:
124 git_factory = GitFactory()
124 git_factory = GitFactory()
125 self._git_remote = GitRemote(git_factory)
125 self._git_remote = GitRemote(git_factory)
126 else:
126 else:
127 log.error("Git client import failed: %s", git_import_err)
127 log.error("Git client import failed: %s", git_import_err)
128
128
129 if MercurialFactory and HgRemote:
129 if MercurialFactory and HgRemote:
130 hg_factory = MercurialFactory()
130 hg_factory = MercurialFactory()
131 self._hg_remote = HgRemote(hg_factory)
131 self._hg_remote = HgRemote(hg_factory)
132 else:
132 else:
133 log.error("Mercurial client import failed: %s", hg_import_err)
133 log.error("Mercurial client import failed: %s", hg_import_err)
134
134
135 if SubversionFactory and SvnRemote:
135 if SubversionFactory and SvnRemote:
136 svn_factory = SubversionFactory()
136 svn_factory = SubversionFactory()
137
137
138 # hg factory is used for svn url validation
138 # hg factory is used for svn url validation
139 hg_factory = MercurialFactory()
139 hg_factory = MercurialFactory()
140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 else:
141 else:
142 log.error("Subversion client import failed: %s", svn_import_err)
142 log.error("Subversion client import failed: %s", svn_import_err)
143
143
144 self._vcsserver = VcsServer()
144 self._vcsserver = VcsServer()
145
145
146 def _configure_locale(self):
146 def _configure_locale(self):
147 if self.locale:
147 if self.locale:
148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 else:
149 else:
150 log.info('Configuring locale subsystem based on environment variables')
150 log.info('Configuring locale subsystem based on environment variables')
151 try:
151 try:
152 # If self.locale is the empty string, then the locale
152 # If self.locale is the empty string, then the locale
153 # module will use the environment variables. See the
153 # module will use the environment variables. See the
154 # documentation of the package `locale`.
154 # documentation of the package `locale`.
155 locale.setlocale(locale.LC_ALL, self.locale)
155 locale.setlocale(locale.LC_ALL, self.locale)
156
156
157 language_code, encoding = locale.getlocale()
157 language_code, encoding = locale.getlocale()
158 log.info(
158 log.info(
159 'Locale set to language code "%s" with encoding "%s".',
159 'Locale set to language code "%s" with encoding "%s".',
160 language_code, encoding)
160 language_code, encoding)
161 except locale.Error:
161 except locale.Error:
162 log.exception('Cannot set locale, not configuring the locale system')
162 log.exception('Cannot set locale, not configuring the locale system')
163
163
164
164
165 class WsgiProxy:
165 class WsgiProxy:
166 def __init__(self, wsgi):
166 def __init__(self, wsgi):
167 self.wsgi = wsgi
167 self.wsgi = wsgi
168
168
169 def __call__(self, environ, start_response):
169 def __call__(self, environ, start_response):
170 input_data = environ['wsgi.input'].read()
170 input_data = environ['wsgi.input'].read()
171 input_data = msgpack.unpackb(input_data)
171 input_data = msgpack.unpackb(input_data)
172
172
173 error = None
173 error = None
174 try:
174 try:
175 data, status, headers = self.wsgi.handle(
175 data, status, headers = self.wsgi.handle(
176 input_data['environment'], input_data['input_data'],
176 input_data['environment'], input_data['input_data'],
177 *input_data['args'], **input_data['kwargs'])
177 *input_data['args'], **input_data['kwargs'])
178 except Exception as e:
178 except Exception as e:
179 data, status, headers = [], None, None
179 data, status, headers = [], None, None
180 error = {
180 error = {
181 'message': str(e),
181 'message': str(e),
182 '_vcs_kind': getattr(e, '_vcs_kind', None)
182 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 }
183 }
184
184
185 start_response(200, {})
185 start_response(200, {})
186 return self._iterator(error, status, headers, data)
186 return self._iterator(error, status, headers, data)
187
187
188 def _iterator(self, error, status, headers, data):
188 def _iterator(self, error, status, headers, data):
189 initial_data = [
189 initial_data = [
190 error,
190 error,
191 status,
191 status,
192 headers,
192 headers,
193 ]
193 ]
194
194
195 for d in chain(initial_data, data):
195 for d in chain(initial_data, data):
196 yield msgpack.packb(d)
196 yield msgpack.packb(d)
197
197
198
198
199 def not_found(request):
199 def not_found(request):
200 return {'status': '404 NOT FOUND'}
200 return {'status': '404 NOT FOUND'}
201
201
202
202
203 class VCSViewPredicate:
203 class VCSViewPredicate:
204 def __init__(self, val, config):
204 def __init__(self, val, config):
205 self.remotes = val
205 self.remotes = val
206
206
207 def text(self):
207 def text(self):
208 return f'vcs view method = {list(self.remotes.keys())}'
208 return f'vcs view method = {list(self.remotes.keys())}'
209
209
210 phash = text
210 phash = text
211
211
212 def __call__(self, context, request):
212 def __call__(self, context, request):
213 """
213 """
214 View predicate that returns true if given backend is supported by
214 View predicate that returns true if given backend is supported by
215 defined remotes.
215 defined remotes.
216 """
216 """
217 backend = request.matchdict.get('backend')
217 backend = request.matchdict.get('backend')
218 return backend in self.remotes
218 return backend in self.remotes
219
219
220
220
221 class HTTPApplication:
221 class HTTPApplication:
222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223
223
224 remote_wsgi = remote_wsgi
224 remote_wsgi = remote_wsgi
225 _use_echo_app = False
225 _use_echo_app = False
226
226
227 def __init__(self, settings=None, global_config=None):
227 def __init__(self, settings=None, global_config=None):
228
228
229 self.config = Configurator(settings=settings)
229 self.config = Configurator(settings=settings)
230 # Init our statsd at very start
230 # Init our statsd at very start
231 self.config.registry.statsd = StatsdClient.statsd
231 self.config.registry.statsd = StatsdClient.statsd
232 self.config.registry.vcs_call_context = {}
232 self.config.registry.vcs_call_context = {}
233
233
234 self.global_config = global_config
234 self.global_config = global_config
235 self.config.include('vcsserver.lib.rc_cache')
235 self.config.include('vcsserver.lib.rc_cache')
236 self.config.include('vcsserver.lib.rc_cache.archive_cache')
236 self.config.include('vcsserver.lib.rc_cache.archive_cache')
237
237
238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 self._remotes = {
240 self._remotes = {
241 'hg': vcs._hg_remote,
241 'hg': vcs._hg_remote,
242 'git': vcs._git_remote,
242 'git': vcs._git_remote,
243 'svn': vcs._svn_remote,
243 'svn': vcs._svn_remote,
244 'server': vcs._vcsserver,
244 'server': vcs._vcsserver,
245 }
245 }
246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 self._use_echo_app = True
247 self._use_echo_app = True
248 log.warning("Using EchoApp for VCS operations.")
248 log.warning("Using EchoApp for VCS operations.")
249 self.remote_wsgi = remote_wsgi_stub
249 self.remote_wsgi = remote_wsgi_stub
250
250
251 self._configure_settings(global_config, settings)
251 self._configure_settings(global_config, settings)
252
252
253 self._configure()
253 self._configure()
254
254
255 def _configure_settings(self, global_config, app_settings):
255 def _configure_settings(self, global_config, app_settings):
256 """
256 """
257 Configure the settings module.
257 Configure the settings module.
258 """
258 """
259 settings_merged = global_config.copy()
259 settings_merged = global_config.copy()
260 settings_merged.update(app_settings)
260 settings_merged.update(app_settings)
261
261
262 git_path = app_settings.get('git_path', None)
262 binary_dir = app_settings['core.binary_dir']
263 if git_path:
263
264 settings.GIT_EXECUTABLE = git_path
265 binary_dir = app_settings.get('core.binary_dir', None)
266 if binary_dir:
267 settings.BINARY_DIR = binary_dir
264 settings.BINARY_DIR = binary_dir
268
265
266 # from core.binary dir we set executable paths
267 settings.GIT_EXECUTABLE = os.path.join(binary_dir, settings.GIT_EXECUTABLE)
268 settings.SVN_EXECUTABLE = os.path.join(binary_dir, settings.SVN_EXECUTABLE)
269 settings.SVNLOOK_EXECUTABLE = os.path.join(binary_dir, settings.SVNLOOK_EXECUTABLE)
270
269 # Store the settings to make them available to other modules.
271 # Store the settings to make them available to other modules.
270 vcsserver.PYRAMID_SETTINGS = settings_merged
272 vcsserver.PYRAMID_SETTINGS = settings_merged
271 vcsserver.CONFIG = settings_merged
273 vcsserver.CONFIG = settings_merged
272
274
273 def _configure(self):
275 def _configure(self):
274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
276 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
275
277
276 self.config.add_route('service', '/_service')
278 self.config.add_route('service', '/_service')
277 self.config.add_route('status', '/status')
279 self.config.add_route('status', '/status')
278 self.config.add_route('hg_proxy', '/proxy/hg')
280 self.config.add_route('hg_proxy', '/proxy/hg')
279 self.config.add_route('git_proxy', '/proxy/git')
281 self.config.add_route('git_proxy', '/proxy/git')
280
282
281 # rpc methods
283 # rpc methods
282 self.config.add_route('vcs', '/{backend}')
284 self.config.add_route('vcs', '/{backend}')
283
285
284 # streaming rpc remote methods
286 # streaming rpc remote methods
285 self.config.add_route('vcs_stream', '/{backend}/stream')
287 self.config.add_route('vcs_stream', '/{backend}/stream')
286
288
287 # vcs operations clone/push as streaming
289 # vcs operations clone/push as streaming
288 self.config.add_route('stream_git', '/stream/git/*repo_name')
290 self.config.add_route('stream_git', '/stream/git/*repo_name')
289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
291 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
290
292
291 self.config.add_view(self.status_view, route_name='status', renderer='json')
293 self.config.add_view(self.status_view, route_name='status', renderer='json')
292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
294 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
293
295
294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
296 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
297 self.config.add_view(self.git_proxy(), route_name='git_proxy')
296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
298 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
297 vcs_view=self._remotes)
299 vcs_view=self._remotes)
298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
300 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
299 vcs_view=self._remotes)
301 vcs_view=self._remotes)
300
302
301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
303 self.config.add_view(self.hg_stream(), route_name='stream_hg')
302 self.config.add_view(self.git_stream(), route_name='stream_git')
304 self.config.add_view(self.git_stream(), route_name='stream_git')
303
305
304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
306 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
305
307
306 self.config.add_notfound_view(not_found, renderer='json')
308 self.config.add_notfound_view(not_found, renderer='json')
307
309
308 self.config.add_view(self.handle_vcs_exception, context=Exception)
310 self.config.add_view(self.handle_vcs_exception, context=Exception)
309
311
310 self.config.add_tween(
312 self.config.add_tween(
311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
313 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
312 )
314 )
313 self.config.add_request_method(
315 self.config.add_request_method(
314 'vcsserver.lib.request_counter.get_request_counter',
316 'vcsserver.lib.request_counter.get_request_counter',
315 'request_count')
317 'request_count')
316
318
317 def wsgi_app(self):
319 def wsgi_app(self):
318 return self.config.make_wsgi_app()
320 return self.config.make_wsgi_app()
319
321
320 def _vcs_view_params(self, request):
322 def _vcs_view_params(self, request):
321 remote = self._remotes[request.matchdict['backend']]
323 remote = self._remotes[request.matchdict['backend']]
322 payload = msgpack.unpackb(request.body, use_list=True)
324 payload = msgpack.unpackb(request.body, use_list=True)
323
325
324 method = payload.get('method')
326 method = payload.get('method')
325 params = payload['params']
327 params = payload['params']
326 wire = params.get('wire')
328 wire = params.get('wire')
327 args = params.get('args')
329 args = params.get('args')
328 kwargs = params.get('kwargs')
330 kwargs = params.get('kwargs')
329 context_uid = None
331 context_uid = None
330
332
331 request.registry.vcs_call_context = {
333 request.registry.vcs_call_context = {
332 'method': method,
334 'method': method,
333 'repo_name': payload.get('_repo_name'),
335 'repo_name': payload.get('_repo_name'),
334 }
336 }
335
337
336 if wire:
338 if wire:
337 try:
339 try:
338 wire['context'] = context_uid = uuid.UUID(wire['context'])
340 wire['context'] = context_uid = uuid.UUID(wire['context'])
339 except KeyError:
341 except KeyError:
340 pass
342 pass
341 args.insert(0, wire)
343 args.insert(0, wire)
342 repo_state_uid = wire.get('repo_state_uid') if wire else None
344 repo_state_uid = wire.get('repo_state_uid') if wire else None
343
345
344 # NOTE(marcink): trading complexity for slight performance
346 # NOTE(marcink): trading complexity for slight performance
345 if log.isEnabledFor(logging.DEBUG):
347 if log.isEnabledFor(logging.DEBUG):
346 # also we SKIP printing out any of those methods args since they maybe excessive
348 # also we SKIP printing out any of those methods args since they maybe excessive
347 just_args_methods = {
349 just_args_methods = {
348 'commitctx': ('content', 'removed', 'updated'),
350 'commitctx': ('content', 'removed', 'updated'),
349 'commit': ('content', 'removed', 'updated')
351 'commit': ('content', 'removed', 'updated')
350 }
352 }
351 if method in just_args_methods:
353 if method in just_args_methods:
352 skip_args = just_args_methods[method]
354 skip_args = just_args_methods[method]
353 call_args = ''
355 call_args = ''
354 call_kwargs = {}
356 call_kwargs = {}
355 for k in kwargs:
357 for k in kwargs:
356 if k in skip_args:
358 if k in skip_args:
357 # replace our skip key with dummy
359 # replace our skip key with dummy
358 call_kwargs[k] = f'RemovedParam({k})'
360 call_kwargs[k] = f'RemovedParam({k})'
359 else:
361 else:
360 call_kwargs[k] = kwargs[k]
362 call_kwargs[k] = kwargs[k]
361 else:
363 else:
362 call_args = args[1:]
364 call_args = args[1:]
363 call_kwargs = kwargs
365 call_kwargs = kwargs
364
366
365 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
367 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
366 method, call_args, call_kwargs, context_uid, repo_state_uid)
368 method, call_args, call_kwargs, context_uid, repo_state_uid)
367
369
368 statsd = request.registry.statsd
370 statsd = request.registry.statsd
369 if statsd:
371 if statsd:
370 statsd.incr(
372 statsd.incr(
371 'vcsserver_method_total', tags=[
373 'vcsserver_method_total', tags=[
372 f"method:{method}",
374 f"method:{method}",
373 ])
375 ])
374 return payload, remote, method, args, kwargs
376 return payload, remote, method, args, kwargs
375
377
376 def vcs_view(self, request):
378 def vcs_view(self, request):
377
379
378 payload, remote, method, args, kwargs = self._vcs_view_params(request)
380 payload, remote, method, args, kwargs = self._vcs_view_params(request)
379 payload_id = payload.get('id')
381 payload_id = payload.get('id')
380
382
381 try:
383 try:
382 resp = getattr(remote, method)(*args, **kwargs)
384 resp = getattr(remote, method)(*args, **kwargs)
383 except Exception as e:
385 except Exception as e:
384 exc_info = list(sys.exc_info())
386 exc_info = list(sys.exc_info())
385 exc_type, exc_value, exc_traceback = exc_info
387 exc_type, exc_value, exc_traceback = exc_info
386
388
387 org_exc = getattr(e, '_org_exc', None)
389 org_exc = getattr(e, '_org_exc', None)
388 org_exc_name = None
390 org_exc_name = None
389 org_exc_tb = ''
391 org_exc_tb = ''
390 if org_exc:
392 if org_exc:
391 org_exc_name = org_exc.__class__.__name__
393 org_exc_name = org_exc.__class__.__name__
392 org_exc_tb = getattr(e, '_org_exc_tb', '')
394 org_exc_tb = getattr(e, '_org_exc_tb', '')
393 # replace our "faked" exception with our org
395 # replace our "faked" exception with our org
394 exc_info[0] = org_exc.__class__
396 exc_info[0] = org_exc.__class__
395 exc_info[1] = org_exc
397 exc_info[1] = org_exc
396
398
397 should_store_exc = True
399 should_store_exc = True
398 if org_exc:
400 if org_exc:
399 def get_exc_fqn(_exc_obj):
401 def get_exc_fqn(_exc_obj):
400 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
402 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
401 return module_name + '.' + org_exc_name
403 return module_name + '.' + org_exc_name
402
404
403 exc_fqn = get_exc_fqn(org_exc)
405 exc_fqn = get_exc_fqn(org_exc)
404
406
405 if exc_fqn in ['mercurial.error.RepoLookupError',
407 if exc_fqn in ['mercurial.error.RepoLookupError',
406 'vcsserver.exceptions.RefNotFoundException']:
408 'vcsserver.exceptions.RefNotFoundException']:
407 should_store_exc = False
409 should_store_exc = False
408
410
409 if should_store_exc:
411 if should_store_exc:
410 store_exception(id(exc_info), exc_info, request_path=request.path)
412 store_exception(id(exc_info), exc_info, request_path=request.path)
411
413
412 tb_info = format_exc(exc_info)
414 tb_info = format_exc(exc_info)
413
415
414 type_ = e.__class__.__name__
416 type_ = e.__class__.__name__
415 if type_ not in self.ALLOWED_EXCEPTIONS:
417 if type_ not in self.ALLOWED_EXCEPTIONS:
416 type_ = None
418 type_ = None
417
419
418 resp = {
420 resp = {
419 'id': payload_id,
421 'id': payload_id,
420 'error': {
422 'error': {
421 'message': str(e),
423 'message': str(e),
422 'traceback': tb_info,
424 'traceback': tb_info,
423 'org_exc': org_exc_name,
425 'org_exc': org_exc_name,
424 'org_exc_tb': org_exc_tb,
426 'org_exc_tb': org_exc_tb,
425 'type': type_
427 'type': type_
426 }
428 }
427 }
429 }
428
430
429 try:
431 try:
430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
432 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
431 except AttributeError:
433 except AttributeError:
432 pass
434 pass
433 else:
435 else:
434 resp = {
436 resp = {
435 'id': payload_id,
437 'id': payload_id,
436 'result': resp
438 'result': resp
437 }
439 }
438 log.debug('Serving data for method %s', method)
440 log.debug('Serving data for method %s', method)
439 return resp
441 return resp
440
442
441 def vcs_stream_view(self, request):
443 def vcs_stream_view(self, request):
442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
444 payload, remote, method, args, kwargs = self._vcs_view_params(request)
443 # this method has a stream: marker we remove it here
445 # this method has a stream: marker we remove it here
444 method = method.split('stream:')[-1]
446 method = method.split('stream:')[-1]
445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
447 chunk_size = safe_int(payload.get('chunk_size')) or 4096
446
448
447 resp = getattr(remote, method)(*args, **kwargs)
449 resp = getattr(remote, method)(*args, **kwargs)
448
450
449 def get_chunked_data(method_resp):
451 def get_chunked_data(method_resp):
450 stream = io.BytesIO(method_resp)
452 stream = io.BytesIO(method_resp)
451 while 1:
453 while 1:
452 chunk = stream.read(chunk_size)
454 chunk = stream.read(chunk_size)
453 if not chunk:
455 if not chunk:
454 break
456 break
455 yield chunk
457 yield chunk
456
458
457 response = Response(app_iter=get_chunked_data(resp))
459 response = Response(app_iter=get_chunked_data(resp))
458 response.content_type = 'application/octet-stream'
460 response.content_type = 'application/octet-stream'
459
461
460 return response
462 return response
461
463
462 def status_view(self, request):
464 def status_view(self, request):
463 import vcsserver
465 import vcsserver
464 _platform_id = platform.uname()[1] or 'instance'
466 _platform_id = platform.uname()[1] or 'instance'
465
467
466 return {
468 return {
467 "status": "OK",
469 "status": "OK",
468 "vcsserver_version": vcsserver.get_version(),
470 "vcsserver_version": vcsserver.get_version(),
469 "platform": _platform_id,
471 "platform": _platform_id,
470 "pid": os.getpid(),
472 "pid": os.getpid(),
471 }
473 }
472
474
473 def service_view(self, request):
475 def service_view(self, request):
474 import vcsserver
476 import vcsserver
475
477
476 payload = msgpack.unpackb(request.body, use_list=True)
478 payload = msgpack.unpackb(request.body, use_list=True)
477 server_config, app_config = {}, {}
479 server_config, app_config = {}, {}
478
480
479 try:
481 try:
480 path = self.global_config['__file__']
482 path = self.global_config['__file__']
481 config = configparser.RawConfigParser()
483 config = configparser.RawConfigParser()
482
484
483 config.read(path)
485 config.read(path)
484
486
485 if config.has_section('server:main'):
487 if config.has_section('server:main'):
486 server_config = dict(config.items('server:main'))
488 server_config = dict(config.items('server:main'))
487 if config.has_section('app:main'):
489 if config.has_section('app:main'):
488 app_config = dict(config.items('app:main'))
490 app_config = dict(config.items('app:main'))
489
491
490 except Exception:
492 except Exception:
491 log.exception('Failed to read .ini file for display')
493 log.exception('Failed to read .ini file for display')
492
494
493 environ = list(os.environ.items())
495 environ = list(os.environ.items())
494
496
495 resp = {
497 resp = {
496 'id': payload.get('id'),
498 'id': payload.get('id'),
497 'result': dict(
499 'result': dict(
498 version=vcsserver.get_version(),
500 version=vcsserver.get_version(),
499 config=server_config,
501 config=server_config,
500 app_config=app_config,
502 app_config=app_config,
501 environ=environ,
503 environ=environ,
502 payload=payload,
504 payload=payload,
503 )
505 )
504 }
506 }
505 return resp
507 return resp
506
508
507 def _msgpack_renderer_factory(self, info):
509 def _msgpack_renderer_factory(self, info):
508
510
509 def _render(value, system):
511 def _render(value, system):
510 bin_type = False
512 bin_type = False
511 res = value.get('result')
513 res = value.get('result')
512 if isinstance(res, BytesEnvelope):
514 if isinstance(res, BytesEnvelope):
513 log.debug('Result is wrapped in BytesEnvelope type')
515 log.debug('Result is wrapped in BytesEnvelope type')
514 bin_type = True
516 bin_type = True
515 elif isinstance(res, BinaryEnvelope):
517 elif isinstance(res, BinaryEnvelope):
516 log.debug('Result is wrapped in BinaryEnvelope type')
518 log.debug('Result is wrapped in BinaryEnvelope type')
517 value['result'] = res.val
519 value['result'] = res.val
518 bin_type = True
520 bin_type = True
519
521
520 request = system.get('request')
522 request = system.get('request')
521 if request is not None:
523 if request is not None:
522 response = request.response
524 response = request.response
523 ct = response.content_type
525 ct = response.content_type
524 if ct == response.default_content_type:
526 if ct == response.default_content_type:
525 response.content_type = 'application/x-msgpack'
527 response.content_type = 'application/x-msgpack'
526 if bin_type:
528 if bin_type:
527 response.content_type = 'application/x-msgpack-bin'
529 response.content_type = 'application/x-msgpack-bin'
528
530
529 return msgpack.packb(value, use_bin_type=bin_type)
531 return msgpack.packb(value, use_bin_type=bin_type)
530 return _render
532 return _render
531
533
532 def set_env_from_config(self, environ, config):
534 def set_env_from_config(self, environ, config):
533 dict_conf = {}
535 dict_conf = {}
534 try:
536 try:
535 for elem in config:
537 for elem in config:
536 if elem[0] == 'rhodecode':
538 if elem[0] == 'rhodecode':
537 dict_conf = json.loads(elem[2])
539 dict_conf = json.loads(elem[2])
538 break
540 break
539 except Exception:
541 except Exception:
540 log.exception('Failed to fetch SCM CONFIG')
542 log.exception('Failed to fetch SCM CONFIG')
541 return
543 return
542
544
543 username = dict_conf.get('username')
545 username = dict_conf.get('username')
544 if username:
546 if username:
545 environ['REMOTE_USER'] = username
547 environ['REMOTE_USER'] = username
546 # mercurial specific, some extension api rely on this
548 # mercurial specific, some extension api rely on this
547 environ['HGUSER'] = username
549 environ['HGUSER'] = username
548
550
549 ip = dict_conf.get('ip')
551 ip = dict_conf.get('ip')
550 if ip:
552 if ip:
551 environ['REMOTE_HOST'] = ip
553 environ['REMOTE_HOST'] = ip
552
554
553 if _is_request_chunked(environ):
555 if _is_request_chunked(environ):
554 # set the compatibility flag for webob
556 # set the compatibility flag for webob
555 environ['wsgi.input_terminated'] = True
557 environ['wsgi.input_terminated'] = True
556
558
557 def hg_proxy(self):
559 def hg_proxy(self):
558 @wsgiapp
560 @wsgiapp
559 def _hg_proxy(environ, start_response):
561 def _hg_proxy(environ, start_response):
560 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
562 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
561 return app(environ, start_response)
563 return app(environ, start_response)
562 return _hg_proxy
564 return _hg_proxy
563
565
564 def git_proxy(self):
566 def git_proxy(self):
565 @wsgiapp
567 @wsgiapp
566 def _git_proxy(environ, start_response):
568 def _git_proxy(environ, start_response):
567 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
569 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
568 return app(environ, start_response)
570 return app(environ, start_response)
569 return _git_proxy
571 return _git_proxy
570
572
571 def hg_stream(self):
573 def hg_stream(self):
572 if self._use_echo_app:
574 if self._use_echo_app:
573 @wsgiapp
575 @wsgiapp
574 def _hg_stream(environ, start_response):
576 def _hg_stream(environ, start_response):
575 app = EchoApp('fake_path', 'fake_name', None)
577 app = EchoApp('fake_path', 'fake_name', None)
576 return app(environ, start_response)
578 return app(environ, start_response)
577 return _hg_stream
579 return _hg_stream
578 else:
580 else:
579 @wsgiapp
581 @wsgiapp
580 def _hg_stream(environ, start_response):
582 def _hg_stream(environ, start_response):
581 log.debug('http-app: handling hg stream')
583 log.debug('http-app: handling hg stream')
582 call_context = get_headers_call_context(environ)
584 call_context = get_headers_call_context(environ)
583
585
584 repo_path = call_context['repo_path']
586 repo_path = call_context['repo_path']
585 repo_name = call_context['repo_name']
587 repo_name = call_context['repo_name']
586 config = call_context['repo_config']
588 config = call_context['repo_config']
587
589
588 app = scm_app.create_hg_wsgi_app(
590 app = scm_app.create_hg_wsgi_app(
589 repo_path, repo_name, config)
591 repo_path, repo_name, config)
590
592
591 # Consistent path information for hgweb
593 # Consistent path information for hgweb
592 environ['PATH_INFO'] = call_context['path_info']
594 environ['PATH_INFO'] = call_context['path_info']
593 environ['REPO_NAME'] = repo_name
595 environ['REPO_NAME'] = repo_name
594 self.set_env_from_config(environ, config)
596 self.set_env_from_config(environ, config)
595
597
596 log.debug('http-app: starting app handler '
598 log.debug('http-app: starting app handler '
597 'with %s and process request', app)
599 'with %s and process request', app)
598 return app(environ, ResponseFilter(start_response))
600 return app(environ, ResponseFilter(start_response))
599 return _hg_stream
601 return _hg_stream
600
602
601 def git_stream(self):
603 def git_stream(self):
602 if self._use_echo_app:
604 if self._use_echo_app:
603 @wsgiapp
605 @wsgiapp
604 def _git_stream(environ, start_response):
606 def _git_stream(environ, start_response):
605 app = EchoApp('fake_path', 'fake_name', None)
607 app = EchoApp('fake_path', 'fake_name', None)
606 return app(environ, start_response)
608 return app(environ, start_response)
607 return _git_stream
609 return _git_stream
608 else:
610 else:
609 @wsgiapp
611 @wsgiapp
610 def _git_stream(environ, start_response):
612 def _git_stream(environ, start_response):
611 log.debug('http-app: handling git stream')
613 log.debug('http-app: handling git stream')
612
614
613 call_context = get_headers_call_context(environ)
615 call_context = get_headers_call_context(environ)
614
616
615 repo_path = call_context['repo_path']
617 repo_path = call_context['repo_path']
616 repo_name = call_context['repo_name']
618 repo_name = call_context['repo_name']
617 config = call_context['repo_config']
619 config = call_context['repo_config']
618
620
619 environ['PATH_INFO'] = call_context['path_info']
621 environ['PATH_INFO'] = call_context['path_info']
620 self.set_env_from_config(environ, config)
622 self.set_env_from_config(environ, config)
621
623
622 content_type = environ.get('CONTENT_TYPE', '')
624 content_type = environ.get('CONTENT_TYPE', '')
623
625
624 path = environ['PATH_INFO']
626 path = environ['PATH_INFO']
625 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
627 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
626 log.debug(
628 log.debug(
627 'LFS: Detecting if request `%s` is LFS server path based '
629 'LFS: Detecting if request `%s` is LFS server path based '
628 'on content type:`%s`, is_lfs:%s',
630 'on content type:`%s`, is_lfs:%s',
629 path, content_type, is_lfs_request)
631 path, content_type, is_lfs_request)
630
632
631 if not is_lfs_request:
633 if not is_lfs_request:
632 # fallback detection by path
634 # fallback detection by path
633 if GIT_LFS_PROTO_PAT.match(path):
635 if GIT_LFS_PROTO_PAT.match(path):
634 is_lfs_request = True
636 is_lfs_request = True
635 log.debug(
637 log.debug(
636 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
638 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
637 path, is_lfs_request)
639 path, is_lfs_request)
638
640
639 if is_lfs_request:
641 if is_lfs_request:
640 app = scm_app.create_git_lfs_wsgi_app(
642 app = scm_app.create_git_lfs_wsgi_app(
641 repo_path, repo_name, config)
643 repo_path, repo_name, config)
642 else:
644 else:
643 app = scm_app.create_git_wsgi_app(
645 app = scm_app.create_git_wsgi_app(
644 repo_path, repo_name, config)
646 repo_path, repo_name, config)
645
647
646 log.debug('http-app: starting app handler '
648 log.debug('http-app: starting app handler '
647 'with %s and process request', app)
649 'with %s and process request', app)
648
650
649 return app(environ, start_response)
651 return app(environ, start_response)
650
652
651 return _git_stream
653 return _git_stream
652
654
653 def handle_vcs_exception(self, exception, request):
655 def handle_vcs_exception(self, exception, request):
654 _vcs_kind = getattr(exception, '_vcs_kind', '')
656 _vcs_kind = getattr(exception, '_vcs_kind', '')
655
657
656 if _vcs_kind == 'repo_locked':
658 if _vcs_kind == 'repo_locked':
657 headers_call_context = get_headers_call_context(request.environ)
659 headers_call_context = get_headers_call_context(request.environ)
658 status_code = safe_int(headers_call_context['locked_status_code'])
660 status_code = safe_int(headers_call_context['locked_status_code'])
659
661
660 return HTTPRepoLocked(
662 return HTTPRepoLocked(
661 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
663 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
662
664
663 elif _vcs_kind == 'repo_branch_protected':
665 elif _vcs_kind == 'repo_branch_protected':
664 # Get custom repo-branch-protected status code if present.
666 # Get custom repo-branch-protected status code if present.
665 return HTTPRepoBranchProtected(
667 return HTTPRepoBranchProtected(
666 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
668 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
667
669
668 exc_info = request.exc_info
670 exc_info = request.exc_info
669 store_exception(id(exc_info), exc_info)
671 store_exception(id(exc_info), exc_info)
670
672
671 traceback_info = 'unavailable'
673 traceback_info = 'unavailable'
672 if request.exc_info:
674 if request.exc_info:
673 traceback_info = format_exc(request.exc_info)
675 traceback_info = format_exc(request.exc_info)
674
676
675 log.error(
677 log.error(
676 'error occurred handling this request for path: %s, \n%s',
678 'error occurred handling this request for path: %s, \n%s',
677 request.path, traceback_info)
679 request.path, traceback_info)
678
680
679 statsd = request.registry.statsd
681 statsd = request.registry.statsd
680 if statsd:
682 if statsd:
681 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
683 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
682 statsd.incr('vcsserver_exception_total',
684 statsd.incr('vcsserver_exception_total',
683 tags=[f"type:{exc_type}"])
685 tags=[f"type:{exc_type}"])
684 raise exception
686 raise exception
685
687
686
688
687 class ResponseFilter:
689 class ResponseFilter:
688
690
689 def __init__(self, start_response):
691 def __init__(self, start_response):
690 self._start_response = start_response
692 self._start_response = start_response
691
693
692 def __call__(self, status, response_headers, exc_info=None):
694 def __call__(self, status, response_headers, exc_info=None):
693 headers = tuple(
695 headers = tuple(
694 (h, v) for h, v in response_headers
696 (h, v) for h, v in response_headers
695 if not wsgiref.util.is_hop_by_hop(h))
697 if not wsgiref.util.is_hop_by_hop(h))
696 return self._start_response(status, headers, exc_info)
698 return self._start_response(status, headers, exc_info)
697
699
698
700
699 def sanitize_settings_and_apply_defaults(global_config, settings):
701 def sanitize_settings_and_apply_defaults(global_config, settings):
700 _global_settings_maker = SettingsMaker(global_config)
702 _global_settings_maker = SettingsMaker(global_config)
701 settings_maker = SettingsMaker(settings)
703 settings_maker = SettingsMaker(settings)
702
704
703 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
705 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
704
706
705 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
707 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
706 settings_maker.enable_logging(logging_conf)
708 settings_maker.enable_logging(logging_conf)
707
709
708 # Default includes, possible to change as a user
710 # Default includes, possible to change as a user
709 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
711 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
710 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
712 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
711
713
712 settings_maker.make_setting('__file__', global_config.get('__file__'))
714 settings_maker.make_setting('__file__', global_config.get('__file__'))
713
715
714 settings_maker.make_setting('pyramid.default_locale_name', 'en')
716 settings_maker.make_setting('pyramid.default_locale_name', 'en')
715 settings_maker.make_setting('locale', 'en_US.UTF-8')
717 settings_maker.make_setting('locale', 'en_US.UTF-8')
716
718
717 settings_maker.make_setting('core.binary_dir', '')
719 settings_maker.make_setting('core.binary_dir', '/usr/local/bin/rhodecode_bin/vcs_bin')
718
720
719 temp_store = tempfile.gettempdir()
721 temp_store = tempfile.gettempdir()
720 default_cache_dir = os.path.join(temp_store, 'rc_cache')
722 default_cache_dir = os.path.join(temp_store, 'rc_cache')
721 # save default, cache dir, and use it for all backends later.
723 # save default, cache dir, and use it for all backends later.
722 default_cache_dir = settings_maker.make_setting(
724 default_cache_dir = settings_maker.make_setting(
723 'cache_dir',
725 'cache_dir',
724 default=default_cache_dir, default_when_empty=True,
726 default=default_cache_dir, default_when_empty=True,
725 parser='dir:ensured')
727 parser='dir:ensured')
726
728
727 # exception store cache
729 # exception store cache
728 settings_maker.make_setting(
730 settings_maker.make_setting(
729 'exception_tracker.store_path',
731 'exception_tracker.store_path',
730 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
732 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
731 parser='dir:ensured'
733 parser='dir:ensured'
732 )
734 )
733
735
734 # repo_object cache defaults
736 # repo_object cache defaults
735 settings_maker.make_setting(
737 settings_maker.make_setting(
736 'rc_cache.repo_object.backend',
738 'rc_cache.repo_object.backend',
737 default='dogpile.cache.rc.file_namespace',
739 default='dogpile.cache.rc.file_namespace',
738 parser='string')
740 parser='string')
739 settings_maker.make_setting(
741 settings_maker.make_setting(
740 'rc_cache.repo_object.expiration_time',
742 'rc_cache.repo_object.expiration_time',
741 default=30 * 24 * 60 * 60, # 30days
743 default=30 * 24 * 60 * 60, # 30days
742 parser='int')
744 parser='int')
743 settings_maker.make_setting(
745 settings_maker.make_setting(
744 'rc_cache.repo_object.arguments.filename',
746 'rc_cache.repo_object.arguments.filename',
745 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
747 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
746 parser='string')
748 parser='string')
747
749
748 # statsd
750 # statsd
749 settings_maker.make_setting('statsd.enabled', False, parser='bool')
751 settings_maker.make_setting('statsd.enabled', False, parser='bool')
750 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
752 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
751 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
753 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
752 settings_maker.make_setting('statsd.statsd_prefix', '')
754 settings_maker.make_setting('statsd.statsd_prefix', '')
753 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
755 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
754
756
755 settings_maker.env_expand()
757 settings_maker.env_expand()
756
758
757
759
758 def main(global_config, **settings):
760 def main(global_config, **settings):
759 start_time = time.time()
761 start_time = time.time()
760 log.info('Pyramid app config starting')
762 log.info('Pyramid app config starting')
761
763
762 if MercurialFactory:
764 if MercurialFactory:
763 hgpatches.patch_largefiles_capabilities()
765 hgpatches.patch_largefiles_capabilities()
764 hgpatches.patch_subrepo_type_mapping()
766 hgpatches.patch_subrepo_type_mapping()
765
767
766 # Fill in and sanitize the defaults & do ENV expansion
768 # Fill in and sanitize the defaults & do ENV expansion
767 sanitize_settings_and_apply_defaults(global_config, settings)
769 sanitize_settings_and_apply_defaults(global_config, settings)
768
770
769 # init and bootstrap StatsdClient
771 # init and bootstrap StatsdClient
770 StatsdClient.setup(settings)
772 StatsdClient.setup(settings)
771
773
772 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
774 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
773 total_time = time.time() - start_time
775 total_time = time.time() - start_time
774 log.info('Pyramid app created and configured in %.2fs', total_time)
776 log.info('Pyramid app created and configured in %.2fs', total_time)
775 return pyramid_app
777 return pyramid_app
General Comments 0
You need to be logged in to leave comments. Login now