##// END OF EJS Templates
fix(core): don't rely on pkgutil to extract the version info of project
super-admin -
r1188:dc28b397 default
parent child Browse files
Show More
@@ -1,28 +1,34 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 pkgutil
18 import os
19
19
20
20
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip().decode()
21 def get_version():
22 here = os.path.abspath(os.path.dirname(__file__))
23 ver_file = os.path.join(here, "VERSION")
24 with open(ver_file, "rt") as f:
25 version = f.read().strip()
26
27 return version
22
28
23 # link to config for pyramid
29 # link to config for pyramid
24 CONFIG = {}
30 CONFIG = {}
25
31
26 # Populated with the settings dictionary from application init in
32 # Populated with the settings dictionary from application init in
27 #
33 #
28 PYRAMID_SETTINGS = {}
34 PYRAMID_SETTINGS = {}
@@ -1,220 +1,220 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 re
18 import re
19 import os
19 import os
20 import sys
20 import sys
21 import datetime
21 import datetime
22 import logging
22 import logging
23 import pkg_resources
23 import pkg_resources
24
24
25 import vcsserver
25 import vcsserver
26 from vcsserver.str_utils import safe_bytes
26 from vcsserver.str_utils import safe_bytes
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30 HOOKS_DIR_MODE = 0o755
30 HOOKS_DIR_MODE = 0o755
31 HOOKS_FILE_MODE = 0o755
31 HOOKS_FILE_MODE = 0o755
32
32
33
33
34 def set_permissions_if_needed(path_to_check, perms: oct):
34 def set_permissions_if_needed(path_to_check, perms: oct):
35 # Get current permissions
35 # Get current permissions
36 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
36 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
37
37
38 # Check if current permissions are lower than required
38 # Check if current permissions are lower than required
39 if current_permissions < int(perms):
39 if current_permissions < int(perms):
40 # Change the permissions if they are lower than required
40 # Change the permissions if they are lower than required
41 os.chmod(path_to_check, perms)
41 os.chmod(path_to_check, perms)
42
42
43
43
44 def get_git_hooks_path(repo_path, bare):
44 def get_git_hooks_path(repo_path, bare):
45 hooks_path = os.path.join(repo_path, 'hooks')
45 hooks_path = os.path.join(repo_path, 'hooks')
46 if not bare:
46 if not bare:
47 hooks_path = os.path.join(repo_path, '.git', 'hooks')
47 hooks_path = os.path.join(repo_path, '.git', 'hooks')
48
48
49 return hooks_path
49 return hooks_path
50
50
51
51
52 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
52 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
53 """
53 """
54 Creates a RhodeCode hook inside a git repository
54 Creates a RhodeCode hook inside a git repository
55
55
56 :param repo_path: path to repository
56 :param repo_path: path to repository
57 :param bare: defines if repository is considered a bare git repo
57 :param bare: defines if repository is considered a bare git repo
58 :param executable: binary executable to put in the hooks
58 :param executable: binary executable to put in the hooks
59 :param force_create: Creates even if the same name hook exists
59 :param force_create: Creates even if the same name hook exists
60 """
60 """
61 executable = executable or sys.executable
61 executable = executable or sys.executable
62 hooks_path = get_git_hooks_path(repo_path, bare)
62 hooks_path = get_git_hooks_path(repo_path, bare)
63
63
64 # we always call it to ensure dir exists and it has a proper mode
64 # we always call it to ensure dir exists and it has a proper mode
65 if not os.path.exists(hooks_path):
65 if not os.path.exists(hooks_path):
66 # If it doesn't exist, create a new directory with the specified mode
66 # 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)
67 os.makedirs(hooks_path, mode=HOOKS_DIR_MODE, exist_ok=True)
68 # If it exists, change the directory's mode to the specified mode
68 # If it exists, change the directory's mode to the specified mode
69 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
69 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
70
70
71 tmpl_post = pkg_resources.resource_string(
71 tmpl_post = pkg_resources.resource_string(
72 'vcsserver', '/'.join(
72 'vcsserver', '/'.join(
73 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
73 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
74 tmpl_pre = pkg_resources.resource_string(
74 tmpl_pre = pkg_resources.resource_string(
75 'vcsserver', '/'.join(
75 'vcsserver', '/'.join(
76 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
76 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
77
77
78 path = '' # not used for now
78 path = '' # not used for now
79 timestamp = datetime.datetime.utcnow().isoformat()
79 timestamp = datetime.datetime.utcnow().isoformat()
80
80
81 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
81 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
82 log.debug('Installing git hook in repo %s', repo_path)
82 log.debug('Installing git hook in repo %s', repo_path)
83 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
83 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
84 _rhodecode_hook = check_rhodecode_hook(_hook_file)
84 _rhodecode_hook = check_rhodecode_hook(_hook_file)
85
85
86 if _rhodecode_hook or force_create:
86 if _rhodecode_hook or force_create:
87 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
87 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
88 try:
88 try:
89 with open(_hook_file, 'wb') as f:
89 with open(_hook_file, 'wb') as f:
90 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
90 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
91 template = template.replace(b'_DATE_', safe_bytes(timestamp))
91 template = template.replace(b'_DATE_', safe_bytes(timestamp))
92 template = template.replace(b'_ENV_', safe_bytes(executable))
92 template = template.replace(b'_ENV_', safe_bytes(executable))
93 template = template.replace(b'_PATH_', safe_bytes(path))
93 template = template.replace(b'_PATH_', safe_bytes(path))
94 f.write(template)
94 f.write(template)
95 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
95 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
96 except OSError:
96 except OSError:
97 log.exception('error writing hook file %s', _hook_file)
97 log.exception('error writing hook file %s', _hook_file)
98 else:
98 else:
99 log.debug('skipping writing hook file')
99 log.debug('skipping writing hook file')
100
100
101 return True
101 return True
102
102
103
103
104 def get_svn_hooks_path(repo_path):
104 def get_svn_hooks_path(repo_path):
105 hooks_path = os.path.join(repo_path, 'hooks')
105 hooks_path = os.path.join(repo_path, 'hooks')
106
106
107 return hooks_path
107 return hooks_path
108
108
109
109
110 def install_svn_hooks(repo_path, executable=None, force_create=False):
110 def install_svn_hooks(repo_path, executable=None, force_create=False):
111 """
111 """
112 Creates RhodeCode hooks inside a svn repository
112 Creates RhodeCode hooks inside a svn repository
113
113
114 :param repo_path: path to repository
114 :param repo_path: path to repository
115 :param executable: binary executable to put in the hooks
115 :param executable: binary executable to put in the hooks
116 :param force_create: Create even if same name hook exists
116 :param force_create: Create even if same name hook exists
117 """
117 """
118 executable = executable or sys.executable
118 executable = executable or sys.executable
119 hooks_path = get_svn_hooks_path(repo_path)
119 hooks_path = get_svn_hooks_path(repo_path)
120 if not os.path.isdir(hooks_path):
120 if not os.path.isdir(hooks_path):
121 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
121 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
122
122
123 tmpl_post = pkg_resources.resource_string(
123 tmpl_post = pkg_resources.resource_string(
124 'vcsserver', '/'.join(
124 'vcsserver', '/'.join(
125 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
125 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
126 tmpl_pre = pkg_resources.resource_string(
126 tmpl_pre = pkg_resources.resource_string(
127 'vcsserver', '/'.join(
127 'vcsserver', '/'.join(
128 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
128 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
129
129
130 path = '' # not used for now
130 path = '' # not used for now
131 timestamp = datetime.datetime.utcnow().isoformat()
131 timestamp = datetime.datetime.utcnow().isoformat()
132
132
133 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
133 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
134 log.debug('Installing svn hook in repo %s', repo_path)
134 log.debug('Installing svn hook in repo %s', repo_path)
135 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
135 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
136 _rhodecode_hook = check_rhodecode_hook(_hook_file)
136 _rhodecode_hook = check_rhodecode_hook(_hook_file)
137
137
138 if _rhodecode_hook or force_create:
138 if _rhodecode_hook or force_create:
139 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
139 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
140
140
141 try:
141 try:
142 with open(_hook_file, 'wb') as f:
142 with open(_hook_file, 'wb') as f:
143 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
143 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
144 template = template.replace(b'_DATE_', safe_bytes(timestamp))
144 template = template.replace(b'_DATE_', safe_bytes(timestamp))
145 template = template.replace(b'_ENV_', safe_bytes(executable))
145 template = template.replace(b'_ENV_', safe_bytes(executable))
146 template = template.replace(b'_PATH_', safe_bytes(path))
146 template = template.replace(b'_PATH_', safe_bytes(path))
147
147
148 f.write(template)
148 f.write(template)
149 os.chmod(_hook_file, 0o755)
149 os.chmod(_hook_file, 0o755)
150 except OSError:
150 except OSError:
151 log.exception('error writing hook file %s', _hook_file)
151 log.exception('error writing hook file %s', _hook_file)
152 else:
152 else:
153 log.debug('skipping writing hook file')
153 log.debug('skipping writing hook file')
154
154
155 return True
155 return True
156
156
157
157
158 def get_version_from_hook(hook_path):
158 def get_version_from_hook(hook_path):
159 version = b''
159 version = b''
160 hook_content = read_hook_content(hook_path)
160 hook_content = read_hook_content(hook_path)
161 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
161 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
162 if matches:
162 if matches:
163 try:
163 try:
164 version = matches.groups()[0]
164 version = matches.groups()[0]
165 log.debug('got version %s from hooks.', version)
165 log.debug('got version %s from hooks.', version)
166 except Exception:
166 except Exception:
167 log.exception("Exception while reading the hook version.")
167 log.exception("Exception while reading the hook version.")
168 return version.replace(b"'", b"")
168 return version.replace(b"'", b"")
169
169
170
170
171 def check_rhodecode_hook(hook_path):
171 def check_rhodecode_hook(hook_path):
172 """
172 """
173 Check if the hook was created by RhodeCode
173 Check if the hook was created by RhodeCode
174 """
174 """
175 if not os.path.exists(hook_path):
175 if not os.path.exists(hook_path):
176 return True
176 return True
177
177
178 log.debug('hook exists, checking if it is from RhodeCode')
178 log.debug('hook exists, checking if it is from RhodeCode')
179
179
180 version = get_version_from_hook(hook_path)
180 version = get_version_from_hook(hook_path)
181 if version:
181 if version:
182 return True
182 return True
183
183
184 return False
184 return False
185
185
186
186
187 def read_hook_content(hook_path) -> bytes:
187 def read_hook_content(hook_path) -> bytes:
188 content = b''
188 content = b''
189 if os.path.isfile(hook_path):
189 if os.path.isfile(hook_path):
190 with open(hook_path, 'rb') as f:
190 with open(hook_path, 'rb') as f:
191 content = f.read()
191 content = f.read()
192 return content
192 return content
193
193
194
194
195 def get_git_pre_hook_version(repo_path, bare):
195 def get_git_pre_hook_version(repo_path, bare):
196 hooks_path = get_git_hooks_path(repo_path, bare)
196 hooks_path = get_git_hooks_path(repo_path, bare)
197 _hook_file = os.path.join(hooks_path, 'pre-receive')
197 _hook_file = os.path.join(hooks_path, 'pre-receive')
198 version = get_version_from_hook(_hook_file)
198 version = get_version_from_hook(_hook_file)
199 return version
199 return version
200
200
201
201
202 def get_git_post_hook_version(repo_path, bare):
202 def get_git_post_hook_version(repo_path, bare):
203 hooks_path = get_git_hooks_path(repo_path, bare)
203 hooks_path = get_git_hooks_path(repo_path, bare)
204 _hook_file = os.path.join(hooks_path, 'post-receive')
204 _hook_file = os.path.join(hooks_path, 'post-receive')
205 version = get_version_from_hook(_hook_file)
205 version = get_version_from_hook(_hook_file)
206 return version
206 return version
207
207
208
208
209 def get_svn_pre_hook_version(repo_path):
209 def get_svn_pre_hook_version(repo_path):
210 hooks_path = get_svn_hooks_path(repo_path)
210 hooks_path = get_svn_hooks_path(repo_path)
211 _hook_file = os.path.join(hooks_path, 'pre-commit')
211 _hook_file = os.path.join(hooks_path, 'pre-commit')
212 version = get_version_from_hook(_hook_file)
212 version = get_version_from_hook(_hook_file)
213 return version
213 return version
214
214
215
215
216 def get_svn_post_hook_version(repo_path):
216 def get_svn_post_hook_version(repo_path):
217 hooks_path = get_svn_hooks_path(repo_path)
217 hooks_path = get_svn_hooks_path(repo_path)
218 _hook_file = os.path.join(hooks_path, 'post-commit')
218 _hook_file = os.path.join(hooks_path, 'post-commit')
219 version = get_version_from_hook(_hook_file)
219 version = get_version_from_hook(_hook_file)
220 return version
220 return version
@@ -1,775 +1,775 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 git_path = app_settings.get('git_path', None)
263 if git_path:
263 if git_path:
264 settings.GIT_EXECUTABLE = git_path
264 settings.GIT_EXECUTABLE = git_path
265 binary_dir = app_settings.get('core.binary_dir', None)
265 binary_dir = app_settings.get('core.binary_dir', None)
266 if binary_dir:
266 if binary_dir:
267 settings.BINARY_DIR = binary_dir
267 settings.BINARY_DIR = binary_dir
268
268
269 # Store the settings to make them available to other modules.
269 # Store the settings to make them available to other modules.
270 vcsserver.PYRAMID_SETTINGS = settings_merged
270 vcsserver.PYRAMID_SETTINGS = settings_merged
271 vcsserver.CONFIG = settings_merged
271 vcsserver.CONFIG = settings_merged
272
272
273 def _configure(self):
273 def _configure(self):
274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
275
275
276 self.config.add_route('service', '/_service')
276 self.config.add_route('service', '/_service')
277 self.config.add_route('status', '/status')
277 self.config.add_route('status', '/status')
278 self.config.add_route('hg_proxy', '/proxy/hg')
278 self.config.add_route('hg_proxy', '/proxy/hg')
279 self.config.add_route('git_proxy', '/proxy/git')
279 self.config.add_route('git_proxy', '/proxy/git')
280
280
281 # rpc methods
281 # rpc methods
282 self.config.add_route('vcs', '/{backend}')
282 self.config.add_route('vcs', '/{backend}')
283
283
284 # streaming rpc remote methods
284 # streaming rpc remote methods
285 self.config.add_route('vcs_stream', '/{backend}/stream')
285 self.config.add_route('vcs_stream', '/{backend}/stream')
286
286
287 # vcs operations clone/push as streaming
287 # vcs operations clone/push as streaming
288 self.config.add_route('stream_git', '/stream/git/*repo_name')
288 self.config.add_route('stream_git', '/stream/git/*repo_name')
289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
290
290
291 self.config.add_view(self.status_view, route_name='status', renderer='json')
291 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')
292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
293
293
294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
297 vcs_view=self._remotes)
297 vcs_view=self._remotes)
298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
299 vcs_view=self._remotes)
299 vcs_view=self._remotes)
300
300
301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
302 self.config.add_view(self.git_stream(), route_name='stream_git')
302 self.config.add_view(self.git_stream(), route_name='stream_git')
303
303
304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
305
305
306 self.config.add_notfound_view(not_found, renderer='json')
306 self.config.add_notfound_view(not_found, renderer='json')
307
307
308 self.config.add_view(self.handle_vcs_exception, context=Exception)
308 self.config.add_view(self.handle_vcs_exception, context=Exception)
309
309
310 self.config.add_tween(
310 self.config.add_tween(
311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
312 )
312 )
313 self.config.add_request_method(
313 self.config.add_request_method(
314 'vcsserver.lib.request_counter.get_request_counter',
314 'vcsserver.lib.request_counter.get_request_counter',
315 'request_count')
315 'request_count')
316
316
317 def wsgi_app(self):
317 def wsgi_app(self):
318 return self.config.make_wsgi_app()
318 return self.config.make_wsgi_app()
319
319
320 def _vcs_view_params(self, request):
320 def _vcs_view_params(self, request):
321 remote = self._remotes[request.matchdict['backend']]
321 remote = self._remotes[request.matchdict['backend']]
322 payload = msgpack.unpackb(request.body, use_list=True)
322 payload = msgpack.unpackb(request.body, use_list=True)
323
323
324 method = payload.get('method')
324 method = payload.get('method')
325 params = payload['params']
325 params = payload['params']
326 wire = params.get('wire')
326 wire = params.get('wire')
327 args = params.get('args')
327 args = params.get('args')
328 kwargs = params.get('kwargs')
328 kwargs = params.get('kwargs')
329 context_uid = None
329 context_uid = None
330
330
331 request.registry.vcs_call_context = {
331 request.registry.vcs_call_context = {
332 'method': method,
332 'method': method,
333 'repo_name': payload.get('_repo_name'),
333 'repo_name': payload.get('_repo_name'),
334 }
334 }
335
335
336 if wire:
336 if wire:
337 try:
337 try:
338 wire['context'] = context_uid = uuid.UUID(wire['context'])
338 wire['context'] = context_uid = uuid.UUID(wire['context'])
339 except KeyError:
339 except KeyError:
340 pass
340 pass
341 args.insert(0, wire)
341 args.insert(0, wire)
342 repo_state_uid = wire.get('repo_state_uid') if wire else None
342 repo_state_uid = wire.get('repo_state_uid') if wire else None
343
343
344 # NOTE(marcink): trading complexity for slight performance
344 # NOTE(marcink): trading complexity for slight performance
345 if log.isEnabledFor(logging.DEBUG):
345 if log.isEnabledFor(logging.DEBUG):
346 # also we SKIP printing out any of those methods args since they maybe excessive
346 # also we SKIP printing out any of those methods args since they maybe excessive
347 just_args_methods = {
347 just_args_methods = {
348 'commitctx': ('content', 'removed', 'updated'),
348 'commitctx': ('content', 'removed', 'updated'),
349 'commit': ('content', 'removed', 'updated')
349 'commit': ('content', 'removed', 'updated')
350 }
350 }
351 if method in just_args_methods:
351 if method in just_args_methods:
352 skip_args = just_args_methods[method]
352 skip_args = just_args_methods[method]
353 call_args = ''
353 call_args = ''
354 call_kwargs = {}
354 call_kwargs = {}
355 for k in kwargs:
355 for k in kwargs:
356 if k in skip_args:
356 if k in skip_args:
357 # replace our skip key with dummy
357 # replace our skip key with dummy
358 call_kwargs[k] = f'RemovedParam({k})'
358 call_kwargs[k] = f'RemovedParam({k})'
359 else:
359 else:
360 call_kwargs[k] = kwargs[k]
360 call_kwargs[k] = kwargs[k]
361 else:
361 else:
362 call_args = args[1:]
362 call_args = args[1:]
363 call_kwargs = kwargs
363 call_kwargs = kwargs
364
364
365 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
365 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)
366 method, call_args, call_kwargs, context_uid, repo_state_uid)
367
367
368 statsd = request.registry.statsd
368 statsd = request.registry.statsd
369 if statsd:
369 if statsd:
370 statsd.incr(
370 statsd.incr(
371 'vcsserver_method_total', tags=[
371 'vcsserver_method_total', tags=[
372 f"method:{method}",
372 f"method:{method}",
373 ])
373 ])
374 return payload, remote, method, args, kwargs
374 return payload, remote, method, args, kwargs
375
375
376 def vcs_view(self, request):
376 def vcs_view(self, request):
377
377
378 payload, remote, method, args, kwargs = self._vcs_view_params(request)
378 payload, remote, method, args, kwargs = self._vcs_view_params(request)
379 payload_id = payload.get('id')
379 payload_id = payload.get('id')
380
380
381 try:
381 try:
382 resp = getattr(remote, method)(*args, **kwargs)
382 resp = getattr(remote, method)(*args, **kwargs)
383 except Exception as e:
383 except Exception as e:
384 exc_info = list(sys.exc_info())
384 exc_info = list(sys.exc_info())
385 exc_type, exc_value, exc_traceback = exc_info
385 exc_type, exc_value, exc_traceback = exc_info
386
386
387 org_exc = getattr(e, '_org_exc', None)
387 org_exc = getattr(e, '_org_exc', None)
388 org_exc_name = None
388 org_exc_name = None
389 org_exc_tb = ''
389 org_exc_tb = ''
390 if org_exc:
390 if org_exc:
391 org_exc_name = org_exc.__class__.__name__
391 org_exc_name = org_exc.__class__.__name__
392 org_exc_tb = getattr(e, '_org_exc_tb', '')
392 org_exc_tb = getattr(e, '_org_exc_tb', '')
393 # replace our "faked" exception with our org
393 # replace our "faked" exception with our org
394 exc_info[0] = org_exc.__class__
394 exc_info[0] = org_exc.__class__
395 exc_info[1] = org_exc
395 exc_info[1] = org_exc
396
396
397 should_store_exc = True
397 should_store_exc = True
398 if org_exc:
398 if org_exc:
399 def get_exc_fqn(_exc_obj):
399 def get_exc_fqn(_exc_obj):
400 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
400 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
401 return module_name + '.' + org_exc_name
401 return module_name + '.' + org_exc_name
402
402
403 exc_fqn = get_exc_fqn(org_exc)
403 exc_fqn = get_exc_fqn(org_exc)
404
404
405 if exc_fqn in ['mercurial.error.RepoLookupError',
405 if exc_fqn in ['mercurial.error.RepoLookupError',
406 'vcsserver.exceptions.RefNotFoundException']:
406 'vcsserver.exceptions.RefNotFoundException']:
407 should_store_exc = False
407 should_store_exc = False
408
408
409 if should_store_exc:
409 if should_store_exc:
410 store_exception(id(exc_info), exc_info, request_path=request.path)
410 store_exception(id(exc_info), exc_info, request_path=request.path)
411
411
412 tb_info = format_exc(exc_info)
412 tb_info = format_exc(exc_info)
413
413
414 type_ = e.__class__.__name__
414 type_ = e.__class__.__name__
415 if type_ not in self.ALLOWED_EXCEPTIONS:
415 if type_ not in self.ALLOWED_EXCEPTIONS:
416 type_ = None
416 type_ = None
417
417
418 resp = {
418 resp = {
419 'id': payload_id,
419 'id': payload_id,
420 'error': {
420 'error': {
421 'message': str(e),
421 'message': str(e),
422 'traceback': tb_info,
422 'traceback': tb_info,
423 'org_exc': org_exc_name,
423 'org_exc': org_exc_name,
424 'org_exc_tb': org_exc_tb,
424 'org_exc_tb': org_exc_tb,
425 'type': type_
425 'type': type_
426 }
426 }
427 }
427 }
428
428
429 try:
429 try:
430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
431 except AttributeError:
431 except AttributeError:
432 pass
432 pass
433 else:
433 else:
434 resp = {
434 resp = {
435 'id': payload_id,
435 'id': payload_id,
436 'result': resp
436 'result': resp
437 }
437 }
438 log.debug('Serving data for method %s', method)
438 log.debug('Serving data for method %s', method)
439 return resp
439 return resp
440
440
441 def vcs_stream_view(self, request):
441 def vcs_stream_view(self, request):
442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
443 # this method has a stream: marker we remove it here
443 # this method has a stream: marker we remove it here
444 method = method.split('stream:')[-1]
444 method = method.split('stream:')[-1]
445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
446
446
447 resp = getattr(remote, method)(*args, **kwargs)
447 resp = getattr(remote, method)(*args, **kwargs)
448
448
449 def get_chunked_data(method_resp):
449 def get_chunked_data(method_resp):
450 stream = io.BytesIO(method_resp)
450 stream = io.BytesIO(method_resp)
451 while 1:
451 while 1:
452 chunk = stream.read(chunk_size)
452 chunk = stream.read(chunk_size)
453 if not chunk:
453 if not chunk:
454 break
454 break
455 yield chunk
455 yield chunk
456
456
457 response = Response(app_iter=get_chunked_data(resp))
457 response = Response(app_iter=get_chunked_data(resp))
458 response.content_type = 'application/octet-stream'
458 response.content_type = 'application/octet-stream'
459
459
460 return response
460 return response
461
461
462 def status_view(self, request):
462 def status_view(self, request):
463 import vcsserver
463 import vcsserver
464 _platform_id = platform.uname()[1] or 'instance'
464 _platform_id = platform.uname()[1] or 'instance'
465
465
466 return {
466 return {
467 "status": "OK",
467 "status": "OK",
468 "vcsserver_version": vcsserver.__version__,
468 "vcsserver_version": vcsserver.get_version(),
469 "platform": _platform_id,
469 "platform": _platform_id,
470 "pid": os.getpid(),
470 "pid": os.getpid(),
471 }
471 }
472
472
473 def service_view(self, request):
473 def service_view(self, request):
474 import vcsserver
474 import vcsserver
475
475
476 payload = msgpack.unpackb(request.body, use_list=True)
476 payload = msgpack.unpackb(request.body, use_list=True)
477 server_config, app_config = {}, {}
477 server_config, app_config = {}, {}
478
478
479 try:
479 try:
480 path = self.global_config['__file__']
480 path = self.global_config['__file__']
481 config = configparser.RawConfigParser()
481 config = configparser.RawConfigParser()
482
482
483 config.read(path)
483 config.read(path)
484
484
485 if config.has_section('server:main'):
485 if config.has_section('server:main'):
486 server_config = dict(config.items('server:main'))
486 server_config = dict(config.items('server:main'))
487 if config.has_section('app:main'):
487 if config.has_section('app:main'):
488 app_config = dict(config.items('app:main'))
488 app_config = dict(config.items('app:main'))
489
489
490 except Exception:
490 except Exception:
491 log.exception('Failed to read .ini file for display')
491 log.exception('Failed to read .ini file for display')
492
492
493 environ = list(os.environ.items())
493 environ = list(os.environ.items())
494
494
495 resp = {
495 resp = {
496 'id': payload.get('id'),
496 'id': payload.get('id'),
497 'result': dict(
497 'result': dict(
498 version=vcsserver.__version__,
498 version=vcsserver.get_version(),
499 config=server_config,
499 config=server_config,
500 app_config=app_config,
500 app_config=app_config,
501 environ=environ,
501 environ=environ,
502 payload=payload,
502 payload=payload,
503 )
503 )
504 }
504 }
505 return resp
505 return resp
506
506
507 def _msgpack_renderer_factory(self, info):
507 def _msgpack_renderer_factory(self, info):
508
508
509 def _render(value, system):
509 def _render(value, system):
510 bin_type = False
510 bin_type = False
511 res = value.get('result')
511 res = value.get('result')
512 if isinstance(res, BytesEnvelope):
512 if isinstance(res, BytesEnvelope):
513 log.debug('Result is wrapped in BytesEnvelope type')
513 log.debug('Result is wrapped in BytesEnvelope type')
514 bin_type = True
514 bin_type = True
515 elif isinstance(res, BinaryEnvelope):
515 elif isinstance(res, BinaryEnvelope):
516 log.debug('Result is wrapped in BinaryEnvelope type')
516 log.debug('Result is wrapped in BinaryEnvelope type')
517 value['result'] = res.val
517 value['result'] = res.val
518 bin_type = True
518 bin_type = True
519
519
520 request = system.get('request')
520 request = system.get('request')
521 if request is not None:
521 if request is not None:
522 response = request.response
522 response = request.response
523 ct = response.content_type
523 ct = response.content_type
524 if ct == response.default_content_type:
524 if ct == response.default_content_type:
525 response.content_type = 'application/x-msgpack'
525 response.content_type = 'application/x-msgpack'
526 if bin_type:
526 if bin_type:
527 response.content_type = 'application/x-msgpack-bin'
527 response.content_type = 'application/x-msgpack-bin'
528
528
529 return msgpack.packb(value, use_bin_type=bin_type)
529 return msgpack.packb(value, use_bin_type=bin_type)
530 return _render
530 return _render
531
531
532 def set_env_from_config(self, environ, config):
532 def set_env_from_config(self, environ, config):
533 dict_conf = {}
533 dict_conf = {}
534 try:
534 try:
535 for elem in config:
535 for elem in config:
536 if elem[0] == 'rhodecode':
536 if elem[0] == 'rhodecode':
537 dict_conf = json.loads(elem[2])
537 dict_conf = json.loads(elem[2])
538 break
538 break
539 except Exception:
539 except Exception:
540 log.exception('Failed to fetch SCM CONFIG')
540 log.exception('Failed to fetch SCM CONFIG')
541 return
541 return
542
542
543 username = dict_conf.get('username')
543 username = dict_conf.get('username')
544 if username:
544 if username:
545 environ['REMOTE_USER'] = username
545 environ['REMOTE_USER'] = username
546 # mercurial specific, some extension api rely on this
546 # mercurial specific, some extension api rely on this
547 environ['HGUSER'] = username
547 environ['HGUSER'] = username
548
548
549 ip = dict_conf.get('ip')
549 ip = dict_conf.get('ip')
550 if ip:
550 if ip:
551 environ['REMOTE_HOST'] = ip
551 environ['REMOTE_HOST'] = ip
552
552
553 if _is_request_chunked(environ):
553 if _is_request_chunked(environ):
554 # set the compatibility flag for webob
554 # set the compatibility flag for webob
555 environ['wsgi.input_terminated'] = True
555 environ['wsgi.input_terminated'] = True
556
556
557 def hg_proxy(self):
557 def hg_proxy(self):
558 @wsgiapp
558 @wsgiapp
559 def _hg_proxy(environ, start_response):
559 def _hg_proxy(environ, start_response):
560 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
560 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
561 return app(environ, start_response)
561 return app(environ, start_response)
562 return _hg_proxy
562 return _hg_proxy
563
563
564 def git_proxy(self):
564 def git_proxy(self):
565 @wsgiapp
565 @wsgiapp
566 def _git_proxy(environ, start_response):
566 def _git_proxy(environ, start_response):
567 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
567 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
568 return app(environ, start_response)
568 return app(environ, start_response)
569 return _git_proxy
569 return _git_proxy
570
570
571 def hg_stream(self):
571 def hg_stream(self):
572 if self._use_echo_app:
572 if self._use_echo_app:
573 @wsgiapp
573 @wsgiapp
574 def _hg_stream(environ, start_response):
574 def _hg_stream(environ, start_response):
575 app = EchoApp('fake_path', 'fake_name', None)
575 app = EchoApp('fake_path', 'fake_name', None)
576 return app(environ, start_response)
576 return app(environ, start_response)
577 return _hg_stream
577 return _hg_stream
578 else:
578 else:
579 @wsgiapp
579 @wsgiapp
580 def _hg_stream(environ, start_response):
580 def _hg_stream(environ, start_response):
581 log.debug('http-app: handling hg stream')
581 log.debug('http-app: handling hg stream')
582 call_context = get_headers_call_context(environ)
582 call_context = get_headers_call_context(environ)
583
583
584 repo_path = call_context['repo_path']
584 repo_path = call_context['repo_path']
585 repo_name = call_context['repo_name']
585 repo_name = call_context['repo_name']
586 config = call_context['repo_config']
586 config = call_context['repo_config']
587
587
588 app = scm_app.create_hg_wsgi_app(
588 app = scm_app.create_hg_wsgi_app(
589 repo_path, repo_name, config)
589 repo_path, repo_name, config)
590
590
591 # Consistent path information for hgweb
591 # Consistent path information for hgweb
592 environ['PATH_INFO'] = call_context['path_info']
592 environ['PATH_INFO'] = call_context['path_info']
593 environ['REPO_NAME'] = repo_name
593 environ['REPO_NAME'] = repo_name
594 self.set_env_from_config(environ, config)
594 self.set_env_from_config(environ, config)
595
595
596 log.debug('http-app: starting app handler '
596 log.debug('http-app: starting app handler '
597 'with %s and process request', app)
597 'with %s and process request', app)
598 return app(environ, ResponseFilter(start_response))
598 return app(environ, ResponseFilter(start_response))
599 return _hg_stream
599 return _hg_stream
600
600
601 def git_stream(self):
601 def git_stream(self):
602 if self._use_echo_app:
602 if self._use_echo_app:
603 @wsgiapp
603 @wsgiapp
604 def _git_stream(environ, start_response):
604 def _git_stream(environ, start_response):
605 app = EchoApp('fake_path', 'fake_name', None)
605 app = EchoApp('fake_path', 'fake_name', None)
606 return app(environ, start_response)
606 return app(environ, start_response)
607 return _git_stream
607 return _git_stream
608 else:
608 else:
609 @wsgiapp
609 @wsgiapp
610 def _git_stream(environ, start_response):
610 def _git_stream(environ, start_response):
611 log.debug('http-app: handling git stream')
611 log.debug('http-app: handling git stream')
612
612
613 call_context = get_headers_call_context(environ)
613 call_context = get_headers_call_context(environ)
614
614
615 repo_path = call_context['repo_path']
615 repo_path = call_context['repo_path']
616 repo_name = call_context['repo_name']
616 repo_name = call_context['repo_name']
617 config = call_context['repo_config']
617 config = call_context['repo_config']
618
618
619 environ['PATH_INFO'] = call_context['path_info']
619 environ['PATH_INFO'] = call_context['path_info']
620 self.set_env_from_config(environ, config)
620 self.set_env_from_config(environ, config)
621
621
622 content_type = environ.get('CONTENT_TYPE', '')
622 content_type = environ.get('CONTENT_TYPE', '')
623
623
624 path = environ['PATH_INFO']
624 path = environ['PATH_INFO']
625 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
625 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
626 log.debug(
626 log.debug(
627 'LFS: Detecting if request `%s` is LFS server path based '
627 'LFS: Detecting if request `%s` is LFS server path based '
628 'on content type:`%s`, is_lfs:%s',
628 'on content type:`%s`, is_lfs:%s',
629 path, content_type, is_lfs_request)
629 path, content_type, is_lfs_request)
630
630
631 if not is_lfs_request:
631 if not is_lfs_request:
632 # fallback detection by path
632 # fallback detection by path
633 if GIT_LFS_PROTO_PAT.match(path):
633 if GIT_LFS_PROTO_PAT.match(path):
634 is_lfs_request = True
634 is_lfs_request = True
635 log.debug(
635 log.debug(
636 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
636 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
637 path, is_lfs_request)
637 path, is_lfs_request)
638
638
639 if is_lfs_request:
639 if is_lfs_request:
640 app = scm_app.create_git_lfs_wsgi_app(
640 app = scm_app.create_git_lfs_wsgi_app(
641 repo_path, repo_name, config)
641 repo_path, repo_name, config)
642 else:
642 else:
643 app = scm_app.create_git_wsgi_app(
643 app = scm_app.create_git_wsgi_app(
644 repo_path, repo_name, config)
644 repo_path, repo_name, config)
645
645
646 log.debug('http-app: starting app handler '
646 log.debug('http-app: starting app handler '
647 'with %s and process request', app)
647 'with %s and process request', app)
648
648
649 return app(environ, start_response)
649 return app(environ, start_response)
650
650
651 return _git_stream
651 return _git_stream
652
652
653 def handle_vcs_exception(self, exception, request):
653 def handle_vcs_exception(self, exception, request):
654 _vcs_kind = getattr(exception, '_vcs_kind', '')
654 _vcs_kind = getattr(exception, '_vcs_kind', '')
655
655
656 if _vcs_kind == 'repo_locked':
656 if _vcs_kind == 'repo_locked':
657 headers_call_context = get_headers_call_context(request.environ)
657 headers_call_context = get_headers_call_context(request.environ)
658 status_code = safe_int(headers_call_context['locked_status_code'])
658 status_code = safe_int(headers_call_context['locked_status_code'])
659
659
660 return HTTPRepoLocked(
660 return HTTPRepoLocked(
661 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
661 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
662
662
663 elif _vcs_kind == 'repo_branch_protected':
663 elif _vcs_kind == 'repo_branch_protected':
664 # Get custom repo-branch-protected status code if present.
664 # Get custom repo-branch-protected status code if present.
665 return HTTPRepoBranchProtected(
665 return HTTPRepoBranchProtected(
666 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
666 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
667
667
668 exc_info = request.exc_info
668 exc_info = request.exc_info
669 store_exception(id(exc_info), exc_info)
669 store_exception(id(exc_info), exc_info)
670
670
671 traceback_info = 'unavailable'
671 traceback_info = 'unavailable'
672 if request.exc_info:
672 if request.exc_info:
673 traceback_info = format_exc(request.exc_info)
673 traceback_info = format_exc(request.exc_info)
674
674
675 log.error(
675 log.error(
676 'error occurred handling this request for path: %s, \n%s',
676 'error occurred handling this request for path: %s, \n%s',
677 request.path, traceback_info)
677 request.path, traceback_info)
678
678
679 statsd = request.registry.statsd
679 statsd = request.registry.statsd
680 if statsd:
680 if statsd:
681 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
681 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
682 statsd.incr('vcsserver_exception_total',
682 statsd.incr('vcsserver_exception_total',
683 tags=[f"type:{exc_type}"])
683 tags=[f"type:{exc_type}"])
684 raise exception
684 raise exception
685
685
686
686
687 class ResponseFilter:
687 class ResponseFilter:
688
688
689 def __init__(self, start_response):
689 def __init__(self, start_response):
690 self._start_response = start_response
690 self._start_response = start_response
691
691
692 def __call__(self, status, response_headers, exc_info=None):
692 def __call__(self, status, response_headers, exc_info=None):
693 headers = tuple(
693 headers = tuple(
694 (h, v) for h, v in response_headers
694 (h, v) for h, v in response_headers
695 if not wsgiref.util.is_hop_by_hop(h))
695 if not wsgiref.util.is_hop_by_hop(h))
696 return self._start_response(status, headers, exc_info)
696 return self._start_response(status, headers, exc_info)
697
697
698
698
699 def sanitize_settings_and_apply_defaults(global_config, settings):
699 def sanitize_settings_and_apply_defaults(global_config, settings):
700 _global_settings_maker = SettingsMaker(global_config)
700 _global_settings_maker = SettingsMaker(global_config)
701 settings_maker = SettingsMaker(settings)
701 settings_maker = SettingsMaker(settings)
702
702
703 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
703 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
704
704
705 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
705 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
706 settings_maker.enable_logging(logging_conf)
706 settings_maker.enable_logging(logging_conf)
707
707
708 # Default includes, possible to change as a user
708 # Default includes, possible to change as a user
709 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
709 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
710 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
710 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
711
711
712 settings_maker.make_setting('__file__', global_config.get('__file__'))
712 settings_maker.make_setting('__file__', global_config.get('__file__'))
713
713
714 settings_maker.make_setting('pyramid.default_locale_name', 'en')
714 settings_maker.make_setting('pyramid.default_locale_name', 'en')
715 settings_maker.make_setting('locale', 'en_US.UTF-8')
715 settings_maker.make_setting('locale', 'en_US.UTF-8')
716
716
717 settings_maker.make_setting('core.binary_dir', '')
717 settings_maker.make_setting('core.binary_dir', '')
718
718
719 temp_store = tempfile.gettempdir()
719 temp_store = tempfile.gettempdir()
720 default_cache_dir = os.path.join(temp_store, 'rc_cache')
720 default_cache_dir = os.path.join(temp_store, 'rc_cache')
721 # save default, cache dir, and use it for all backends later.
721 # save default, cache dir, and use it for all backends later.
722 default_cache_dir = settings_maker.make_setting(
722 default_cache_dir = settings_maker.make_setting(
723 'cache_dir',
723 'cache_dir',
724 default=default_cache_dir, default_when_empty=True,
724 default=default_cache_dir, default_when_empty=True,
725 parser='dir:ensured')
725 parser='dir:ensured')
726
726
727 # exception store cache
727 # exception store cache
728 settings_maker.make_setting(
728 settings_maker.make_setting(
729 'exception_tracker.store_path',
729 'exception_tracker.store_path',
730 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
730 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
731 parser='dir:ensured'
731 parser='dir:ensured'
732 )
732 )
733
733
734 # repo_object cache defaults
734 # repo_object cache defaults
735 settings_maker.make_setting(
735 settings_maker.make_setting(
736 'rc_cache.repo_object.backend',
736 'rc_cache.repo_object.backend',
737 default='dogpile.cache.rc.file_namespace',
737 default='dogpile.cache.rc.file_namespace',
738 parser='string')
738 parser='string')
739 settings_maker.make_setting(
739 settings_maker.make_setting(
740 'rc_cache.repo_object.expiration_time',
740 'rc_cache.repo_object.expiration_time',
741 default=30 * 24 * 60 * 60, # 30days
741 default=30 * 24 * 60 * 60, # 30days
742 parser='int')
742 parser='int')
743 settings_maker.make_setting(
743 settings_maker.make_setting(
744 'rc_cache.repo_object.arguments.filename',
744 'rc_cache.repo_object.arguments.filename',
745 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
745 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
746 parser='string')
746 parser='string')
747
747
748 # statsd
748 # statsd
749 settings_maker.make_setting('statsd.enabled', False, parser='bool')
749 settings_maker.make_setting('statsd.enabled', False, parser='bool')
750 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
750 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
751 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
751 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
752 settings_maker.make_setting('statsd.statsd_prefix', '')
752 settings_maker.make_setting('statsd.statsd_prefix', '')
753 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
753 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
754
754
755 settings_maker.env_expand()
755 settings_maker.env_expand()
756
756
757
757
758 def main(global_config, **settings):
758 def main(global_config, **settings):
759 start_time = time.time()
759 start_time = time.time()
760 log.info('Pyramid app config starting')
760 log.info('Pyramid app config starting')
761
761
762 if MercurialFactory:
762 if MercurialFactory:
763 hgpatches.patch_largefiles_capabilities()
763 hgpatches.patch_largefiles_capabilities()
764 hgpatches.patch_subrepo_type_mapping()
764 hgpatches.patch_subrepo_type_mapping()
765
765
766 # Fill in and sanitize the defaults & do ENV expansion
766 # Fill in and sanitize the defaults & do ENV expansion
767 sanitize_settings_and_apply_defaults(global_config, settings)
767 sanitize_settings_and_apply_defaults(global_config, settings)
768
768
769 # init and bootstrap StatsdClient
769 # init and bootstrap StatsdClient
770 StatsdClient.setup(settings)
770 StatsdClient.setup(settings)
771
771
772 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
772 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
773 total_time = time.time() - start_time
773 total_time = time.time() - start_time
774 log.info('Pyramid app created and configured in %.2fs', total_time)
774 log.info('Pyramid app created and configured in %.2fs', total_time)
775 return pyramid_app
775 return pyramid_app
@@ -1,1213 +1,1213 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 import binascii
17 import binascii
18 import io
18 import io
19 import logging
19 import logging
20 import stat
20 import stat
21 import sys
21 import sys
22 import urllib.request
22 import urllib.request
23 import urllib.parse
23 import urllib.parse
24 import hashlib
24 import hashlib
25
25
26 from hgext import largefiles, rebase, purge
26 from hgext import largefiles, rebase, purge
27
27
28 from mercurial import commands
28 from mercurial import commands
29 from mercurial import unionrepo
29 from mercurial import unionrepo
30 from mercurial import verify
30 from mercurial import verify
31 from mercurial import repair
31 from mercurial import repair
32 from mercurial.error import AmbiguousPrefixLookupError
32 from mercurial.error import AmbiguousPrefixLookupError
33
33
34 import vcsserver
34 import vcsserver
35 from vcsserver import exceptions
35 from vcsserver import exceptions
36 from vcsserver.base import (
36 from vcsserver.base import (
37 RepoFactory,
37 RepoFactory,
38 obfuscate_qs,
38 obfuscate_qs,
39 raise_from_original,
39 raise_from_original,
40 store_archive_in_cache,
40 store_archive_in_cache,
41 ArchiveNode,
41 ArchiveNode,
42 BytesEnvelope,
42 BytesEnvelope,
43 BinaryEnvelope,
43 BinaryEnvelope,
44 )
44 )
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 archival,
46 archival,
47 bin,
47 bin,
48 clone,
48 clone,
49 config as hgconfig,
49 config as hgconfig,
50 diffopts,
50 diffopts,
51 hex,
51 hex,
52 get_ctx,
52 get_ctx,
53 hg_url as url_parser,
53 hg_url as url_parser,
54 httpbasicauthhandler,
54 httpbasicauthhandler,
55 httpdigestauthhandler,
55 httpdigestauthhandler,
56 makepeer,
56 makepeer,
57 instance,
57 instance,
58 match,
58 match,
59 memctx,
59 memctx,
60 exchange,
60 exchange,
61 memfilectx,
61 memfilectx,
62 nullrev,
62 nullrev,
63 hg_merge,
63 hg_merge,
64 patch,
64 patch,
65 peer,
65 peer,
66 revrange,
66 revrange,
67 ui,
67 ui,
68 hg_tag,
68 hg_tag,
69 Abort,
69 Abort,
70 LookupError,
70 LookupError,
71 RepoError,
71 RepoError,
72 RepoLookupError,
72 RepoLookupError,
73 InterventionRequired,
73 InterventionRequired,
74 RequirementError,
74 RequirementError,
75 alwaysmatcher,
75 alwaysmatcher,
76 patternmatcher,
76 patternmatcher,
77 hgutil,
77 hgutil,
78 hgext_strip,
78 hgext_strip,
79 )
79 )
80 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
80 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
81 from vcsserver.vcs_base import RemoteBase
81 from vcsserver.vcs_base import RemoteBase
82 from vcsserver.config import hooks as hooks_config
82 from vcsserver.config import hooks as hooks_config
83 from vcsserver.lib.exc_tracking import format_exc
83 from vcsserver.lib.exc_tracking import format_exc
84
84
85 log = logging.getLogger(__name__)
85 log = logging.getLogger(__name__)
86
86
87
87
88 def make_ui_from_config(repo_config):
88 def make_ui_from_config(repo_config):
89
89
90 class LoggingUI(ui.ui):
90 class LoggingUI(ui.ui):
91
91
92 def status(self, *msg, **opts):
92 def status(self, *msg, **opts):
93 str_msg = map(safe_str, msg)
93 str_msg = map(safe_str, msg)
94 log.info(' '.join(str_msg).rstrip('\n'))
94 log.info(' '.join(str_msg).rstrip('\n'))
95 #super(LoggingUI, self).status(*msg, **opts)
95 #super(LoggingUI, self).status(*msg, **opts)
96
96
97 def warn(self, *msg, **opts):
97 def warn(self, *msg, **opts):
98 str_msg = map(safe_str, msg)
98 str_msg = map(safe_str, msg)
99 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
99 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
100 #super(LoggingUI, self).warn(*msg, **opts)
100 #super(LoggingUI, self).warn(*msg, **opts)
101
101
102 def error(self, *msg, **opts):
102 def error(self, *msg, **opts):
103 str_msg = map(safe_str, msg)
103 str_msg = map(safe_str, msg)
104 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
104 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
105 #super(LoggingUI, self).error(*msg, **opts)
105 #super(LoggingUI, self).error(*msg, **opts)
106
106
107 def note(self, *msg, **opts):
107 def note(self, *msg, **opts):
108 str_msg = map(safe_str, msg)
108 str_msg = map(safe_str, msg)
109 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
109 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
110 #super(LoggingUI, self).note(*msg, **opts)
110 #super(LoggingUI, self).note(*msg, **opts)
111
111
112 def debug(self, *msg, **opts):
112 def debug(self, *msg, **opts):
113 str_msg = map(safe_str, msg)
113 str_msg = map(safe_str, msg)
114 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
114 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
115 #super(LoggingUI, self).debug(*msg, **opts)
115 #super(LoggingUI, self).debug(*msg, **opts)
116
116
117 baseui = LoggingUI()
117 baseui = LoggingUI()
118
118
119 # clean the baseui object
119 # clean the baseui object
120 baseui._ocfg = hgconfig.config()
120 baseui._ocfg = hgconfig.config()
121 baseui._ucfg = hgconfig.config()
121 baseui._ucfg = hgconfig.config()
122 baseui._tcfg = hgconfig.config()
122 baseui._tcfg = hgconfig.config()
123
123
124 for section, option, value in repo_config:
124 for section, option, value in repo_config:
125 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
125 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
126
126
127 # make our hgweb quiet so it doesn't print output
127 # make our hgweb quiet so it doesn't print output
128 baseui.setconfig(b'ui', b'quiet', b'true')
128 baseui.setconfig(b'ui', b'quiet', b'true')
129
129
130 baseui.setconfig(b'ui', b'paginate', b'never')
130 baseui.setconfig(b'ui', b'paginate', b'never')
131 # for better Error reporting of Mercurial
131 # for better Error reporting of Mercurial
132 baseui.setconfig(b'ui', b'message-output', b'stderr')
132 baseui.setconfig(b'ui', b'message-output', b'stderr')
133
133
134 # force mercurial to only use 1 thread, otherwise it may try to set a
134 # force mercurial to only use 1 thread, otherwise it may try to set a
135 # signal in a non-main thread, thus generating a ValueError.
135 # signal in a non-main thread, thus generating a ValueError.
136 baseui.setconfig(b'worker', b'numcpus', 1)
136 baseui.setconfig(b'worker', b'numcpus', 1)
137
137
138 # If there is no config for the largefiles extension, we explicitly disable
138 # If there is no config for the largefiles extension, we explicitly disable
139 # it here. This overrides settings from repositories hgrc file. Recent
139 # it here. This overrides settings from repositories hgrc file. Recent
140 # mercurial versions enable largefiles in hgrc on clone from largefile
140 # mercurial versions enable largefiles in hgrc on clone from largefile
141 # repo.
141 # repo.
142 if not baseui.hasconfig(b'extensions', b'largefiles'):
142 if not baseui.hasconfig(b'extensions', b'largefiles'):
143 log.debug('Explicitly disable largefiles extension for repo.')
143 log.debug('Explicitly disable largefiles extension for repo.')
144 baseui.setconfig(b'extensions', b'largefiles', b'!')
144 baseui.setconfig(b'extensions', b'largefiles', b'!')
145
145
146 return baseui
146 return baseui
147
147
148
148
149 def reraise_safe_exceptions(func):
149 def reraise_safe_exceptions(func):
150 """Decorator for converting mercurial exceptions to something neutral."""
150 """Decorator for converting mercurial exceptions to something neutral."""
151
151
152 def wrapper(*args, **kwargs):
152 def wrapper(*args, **kwargs):
153 try:
153 try:
154 return func(*args, **kwargs)
154 return func(*args, **kwargs)
155 except (Abort, InterventionRequired) as e:
155 except (Abort, InterventionRequired) as e:
156 raise_from_original(exceptions.AbortException(e), e)
156 raise_from_original(exceptions.AbortException(e), e)
157 except RepoLookupError as e:
157 except RepoLookupError as e:
158 raise_from_original(exceptions.LookupException(e), e)
158 raise_from_original(exceptions.LookupException(e), e)
159 except RequirementError as e:
159 except RequirementError as e:
160 raise_from_original(exceptions.RequirementException(e), e)
160 raise_from_original(exceptions.RequirementException(e), e)
161 except RepoError as e:
161 except RepoError as e:
162 raise_from_original(exceptions.VcsException(e), e)
162 raise_from_original(exceptions.VcsException(e), e)
163 except LookupError as e:
163 except LookupError as e:
164 raise_from_original(exceptions.LookupException(e), e)
164 raise_from_original(exceptions.LookupException(e), e)
165 except Exception as e:
165 except Exception as e:
166 if not hasattr(e, '_vcs_kind'):
166 if not hasattr(e, '_vcs_kind'):
167 log.exception("Unhandled exception in hg remote call")
167 log.exception("Unhandled exception in hg remote call")
168 raise_from_original(exceptions.UnhandledException(e), e)
168 raise_from_original(exceptions.UnhandledException(e), e)
169
169
170 raise
170 raise
171 return wrapper
171 return wrapper
172
172
173
173
174 class MercurialFactory(RepoFactory):
174 class MercurialFactory(RepoFactory):
175 repo_type = 'hg'
175 repo_type = 'hg'
176
176
177 def _create_config(self, config, hooks=True):
177 def _create_config(self, config, hooks=True):
178 if not hooks:
178 if not hooks:
179
179
180 hooks_to_clean = {
180 hooks_to_clean = {
181
181
182 hooks_config.HOOK_REPO_SIZE,
182 hooks_config.HOOK_REPO_SIZE,
183 hooks_config.HOOK_PRE_PULL,
183 hooks_config.HOOK_PRE_PULL,
184 hooks_config.HOOK_PULL,
184 hooks_config.HOOK_PULL,
185
185
186 hooks_config.HOOK_PRE_PUSH,
186 hooks_config.HOOK_PRE_PUSH,
187 # TODO: what about PRETXT, this was disabled in pre 5.0.0
187 # TODO: what about PRETXT, this was disabled in pre 5.0.0
188 hooks_config.HOOK_PRETX_PUSH,
188 hooks_config.HOOK_PRETX_PUSH,
189
189
190 }
190 }
191 new_config = []
191 new_config = []
192 for section, option, value in config:
192 for section, option, value in config:
193 if section == 'hooks' and option in hooks_to_clean:
193 if section == 'hooks' and option in hooks_to_clean:
194 continue
194 continue
195 new_config.append((section, option, value))
195 new_config.append((section, option, value))
196 config = new_config
196 config = new_config
197
197
198 baseui = make_ui_from_config(config)
198 baseui = make_ui_from_config(config)
199 return baseui
199 return baseui
200
200
201 def _create_repo(self, wire, create):
201 def _create_repo(self, wire, create):
202 baseui = self._create_config(wire["config"])
202 baseui = self._create_config(wire["config"])
203 repo = instance(baseui, safe_bytes(wire["path"]), create)
203 repo = instance(baseui, safe_bytes(wire["path"]), create)
204 log.debug('repository created: got HG object: %s', repo)
204 log.debug('repository created: got HG object: %s', repo)
205 return repo
205 return repo
206
206
207 def repo(self, wire, create=False):
207 def repo(self, wire, create=False):
208 """
208 """
209 Get a repository instance for the given path.
209 Get a repository instance for the given path.
210 """
210 """
211 return self._create_repo(wire, create)
211 return self._create_repo(wire, create)
212
212
213
213
214 def patch_ui_message_output(baseui):
214 def patch_ui_message_output(baseui):
215 baseui.setconfig(b'ui', b'quiet', b'false')
215 baseui.setconfig(b'ui', b'quiet', b'false')
216 output = io.BytesIO()
216 output = io.BytesIO()
217
217
218 def write(data, **unused_kwargs):
218 def write(data, **unused_kwargs):
219 output.write(data)
219 output.write(data)
220
220
221 baseui.status = write
221 baseui.status = write
222 baseui.write = write
222 baseui.write = write
223 baseui.warn = write
223 baseui.warn = write
224 baseui.debug = write
224 baseui.debug = write
225
225
226 return baseui, output
226 return baseui, output
227
227
228
228
229 def get_obfuscated_url(url_obj):
229 def get_obfuscated_url(url_obj):
230 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
230 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
231 url_obj.query = obfuscate_qs(url_obj.query)
231 url_obj.query = obfuscate_qs(url_obj.query)
232 obfuscated_uri = str(url_obj)
232 obfuscated_uri = str(url_obj)
233 return obfuscated_uri
233 return obfuscated_uri
234
234
235
235
236 def normalize_url_for_hg(url: str):
236 def normalize_url_for_hg(url: str):
237 _proto = None
237 _proto = None
238
238
239 if '+' in url[:url.find('://')]:
239 if '+' in url[:url.find('://')]:
240 _proto = url[0:url.find('+')]
240 _proto = url[0:url.find('+')]
241 url = url[url.find('+') + 1:]
241 url = url[url.find('+') + 1:]
242 return url, _proto
242 return url, _proto
243
243
244
244
245 class HgRemote(RemoteBase):
245 class HgRemote(RemoteBase):
246
246
247 def __init__(self, factory):
247 def __init__(self, factory):
248 self._factory = factory
248 self._factory = factory
249 self._bulk_methods = {
249 self._bulk_methods = {
250 "affected_files": self.ctx_files,
250 "affected_files": self.ctx_files,
251 "author": self.ctx_user,
251 "author": self.ctx_user,
252 "branch": self.ctx_branch,
252 "branch": self.ctx_branch,
253 "children": self.ctx_children,
253 "children": self.ctx_children,
254 "date": self.ctx_date,
254 "date": self.ctx_date,
255 "message": self.ctx_description,
255 "message": self.ctx_description,
256 "parents": self.ctx_parents,
256 "parents": self.ctx_parents,
257 "status": self.ctx_status,
257 "status": self.ctx_status,
258 "obsolete": self.ctx_obsolete,
258 "obsolete": self.ctx_obsolete,
259 "phase": self.ctx_phase,
259 "phase": self.ctx_phase,
260 "hidden": self.ctx_hidden,
260 "hidden": self.ctx_hidden,
261 "_file_paths": self.ctx_list,
261 "_file_paths": self.ctx_list,
262 }
262 }
263 self._bulk_file_methods = {
263 self._bulk_file_methods = {
264 "size": self.fctx_size,
264 "size": self.fctx_size,
265 "data": self.fctx_node_data,
265 "data": self.fctx_node_data,
266 "flags": self.fctx_flags,
266 "flags": self.fctx_flags,
267 "is_binary": self.is_binary,
267 "is_binary": self.is_binary,
268 "md5": self.md5_hash,
268 "md5": self.md5_hash,
269 }
269 }
270
270
271 def _get_ctx(self, repo, ref):
271 def _get_ctx(self, repo, ref):
272 return get_ctx(repo, ref)
272 return get_ctx(repo, ref)
273
273
274 @reraise_safe_exceptions
274 @reraise_safe_exceptions
275 def discover_hg_version(self):
275 def discover_hg_version(self):
276 from mercurial import util
276 from mercurial import util
277 return safe_str(util.version())
277 return safe_str(util.version())
278
278
279 @reraise_safe_exceptions
279 @reraise_safe_exceptions
280 def is_empty(self, wire):
280 def is_empty(self, wire):
281 repo = self._factory.repo(wire)
281 repo = self._factory.repo(wire)
282
282
283 try:
283 try:
284 return len(repo) == 0
284 return len(repo) == 0
285 except Exception:
285 except Exception:
286 log.exception("failed to read object_store")
286 log.exception("failed to read object_store")
287 return False
287 return False
288
288
289 @reraise_safe_exceptions
289 @reraise_safe_exceptions
290 def bookmarks(self, wire):
290 def bookmarks(self, wire):
291 cache_on, context_uid, repo_id = self._cache_on(wire)
291 cache_on, context_uid, repo_id = self._cache_on(wire)
292 region = self._region(wire)
292 region = self._region(wire)
293
293
294 @region.conditional_cache_on_arguments(condition=cache_on)
294 @region.conditional_cache_on_arguments(condition=cache_on)
295 def _bookmarks(_context_uid, _repo_id):
295 def _bookmarks(_context_uid, _repo_id):
296 repo = self._factory.repo(wire)
296 repo = self._factory.repo(wire)
297 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
297 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
298
298
299 return _bookmarks(context_uid, repo_id)
299 return _bookmarks(context_uid, repo_id)
300
300
301 @reraise_safe_exceptions
301 @reraise_safe_exceptions
302 def branches(self, wire, normal, closed):
302 def branches(self, wire, normal, closed):
303 cache_on, context_uid, repo_id = self._cache_on(wire)
303 cache_on, context_uid, repo_id = self._cache_on(wire)
304 region = self._region(wire)
304 region = self._region(wire)
305
305
306 @region.conditional_cache_on_arguments(condition=cache_on)
306 @region.conditional_cache_on_arguments(condition=cache_on)
307 def _branches(_context_uid, _repo_id, _normal, _closed):
307 def _branches(_context_uid, _repo_id, _normal, _closed):
308 repo = self._factory.repo(wire)
308 repo = self._factory.repo(wire)
309 iter_branches = repo.branchmap().iterbranches()
309 iter_branches = repo.branchmap().iterbranches()
310 bt = {}
310 bt = {}
311 for branch_name, _heads, tip_node, is_closed in iter_branches:
311 for branch_name, _heads, tip_node, is_closed in iter_branches:
312 if normal and not is_closed:
312 if normal and not is_closed:
313 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
313 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
314 if closed and is_closed:
314 if closed and is_closed:
315 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
315 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
316
316
317 return bt
317 return bt
318
318
319 return _branches(context_uid, repo_id, normal, closed)
319 return _branches(context_uid, repo_id, normal, closed)
320
320
321 @reraise_safe_exceptions
321 @reraise_safe_exceptions
322 def bulk_request(self, wire, commit_id, pre_load):
322 def bulk_request(self, wire, commit_id, pre_load):
323 cache_on, context_uid, repo_id = self._cache_on(wire)
323 cache_on, context_uid, repo_id = self._cache_on(wire)
324 region = self._region(wire)
324 region = self._region(wire)
325
325
326 @region.conditional_cache_on_arguments(condition=cache_on)
326 @region.conditional_cache_on_arguments(condition=cache_on)
327 def _bulk_request(_repo_id, _commit_id, _pre_load):
327 def _bulk_request(_repo_id, _commit_id, _pre_load):
328 result = {}
328 result = {}
329 for attr in pre_load:
329 for attr in pre_load:
330 try:
330 try:
331 method = self._bulk_methods[attr]
331 method = self._bulk_methods[attr]
332 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
332 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
333 result[attr] = method(wire, commit_id)
333 result[attr] = method(wire, commit_id)
334 except KeyError as e:
334 except KeyError as e:
335 raise exceptions.VcsException(e)(
335 raise exceptions.VcsException(e)(
336 f'Unknown bulk attribute: "{attr}"')
336 f'Unknown bulk attribute: "{attr}"')
337 return result
337 return result
338
338
339 return _bulk_request(repo_id, commit_id, sorted(pre_load))
339 return _bulk_request(repo_id, commit_id, sorted(pre_load))
340
340
341 @reraise_safe_exceptions
341 @reraise_safe_exceptions
342 def ctx_branch(self, wire, commit_id):
342 def ctx_branch(self, wire, commit_id):
343 cache_on, context_uid, repo_id = self._cache_on(wire)
343 cache_on, context_uid, repo_id = self._cache_on(wire)
344 region = self._region(wire)
344 region = self._region(wire)
345
345
346 @region.conditional_cache_on_arguments(condition=cache_on)
346 @region.conditional_cache_on_arguments(condition=cache_on)
347 def _ctx_branch(_repo_id, _commit_id):
347 def _ctx_branch(_repo_id, _commit_id):
348 repo = self._factory.repo(wire)
348 repo = self._factory.repo(wire)
349 ctx = self._get_ctx(repo, commit_id)
349 ctx = self._get_ctx(repo, commit_id)
350 return ctx.branch()
350 return ctx.branch()
351 return _ctx_branch(repo_id, commit_id)
351 return _ctx_branch(repo_id, commit_id)
352
352
353 @reraise_safe_exceptions
353 @reraise_safe_exceptions
354 def ctx_date(self, wire, commit_id):
354 def ctx_date(self, wire, commit_id):
355 cache_on, context_uid, repo_id = self._cache_on(wire)
355 cache_on, context_uid, repo_id = self._cache_on(wire)
356 region = self._region(wire)
356 region = self._region(wire)
357
357
358 @region.conditional_cache_on_arguments(condition=cache_on)
358 @region.conditional_cache_on_arguments(condition=cache_on)
359 def _ctx_date(_repo_id, _commit_id):
359 def _ctx_date(_repo_id, _commit_id):
360 repo = self._factory.repo(wire)
360 repo = self._factory.repo(wire)
361 ctx = self._get_ctx(repo, commit_id)
361 ctx = self._get_ctx(repo, commit_id)
362 return ctx.date()
362 return ctx.date()
363 return _ctx_date(repo_id, commit_id)
363 return _ctx_date(repo_id, commit_id)
364
364
365 @reraise_safe_exceptions
365 @reraise_safe_exceptions
366 def ctx_description(self, wire, revision):
366 def ctx_description(self, wire, revision):
367 repo = self._factory.repo(wire)
367 repo = self._factory.repo(wire)
368 ctx = self._get_ctx(repo, revision)
368 ctx = self._get_ctx(repo, revision)
369 return ctx.description()
369 return ctx.description()
370
370
371 @reraise_safe_exceptions
371 @reraise_safe_exceptions
372 def ctx_files(self, wire, commit_id):
372 def ctx_files(self, wire, commit_id):
373 cache_on, context_uid, repo_id = self._cache_on(wire)
373 cache_on, context_uid, repo_id = self._cache_on(wire)
374 region = self._region(wire)
374 region = self._region(wire)
375
375
376 @region.conditional_cache_on_arguments(condition=cache_on)
376 @region.conditional_cache_on_arguments(condition=cache_on)
377 def _ctx_files(_repo_id, _commit_id):
377 def _ctx_files(_repo_id, _commit_id):
378 repo = self._factory.repo(wire)
378 repo = self._factory.repo(wire)
379 ctx = self._get_ctx(repo, commit_id)
379 ctx = self._get_ctx(repo, commit_id)
380 return ctx.files()
380 return ctx.files()
381
381
382 return _ctx_files(repo_id, commit_id)
382 return _ctx_files(repo_id, commit_id)
383
383
384 @reraise_safe_exceptions
384 @reraise_safe_exceptions
385 def ctx_list(self, path, revision):
385 def ctx_list(self, path, revision):
386 repo = self._factory.repo(path)
386 repo = self._factory.repo(path)
387 ctx = self._get_ctx(repo, revision)
387 ctx = self._get_ctx(repo, revision)
388 return list(ctx)
388 return list(ctx)
389
389
390 @reraise_safe_exceptions
390 @reraise_safe_exceptions
391 def ctx_parents(self, wire, commit_id):
391 def ctx_parents(self, wire, commit_id):
392 cache_on, context_uid, repo_id = self._cache_on(wire)
392 cache_on, context_uid, repo_id = self._cache_on(wire)
393 region = self._region(wire)
393 region = self._region(wire)
394
394
395 @region.conditional_cache_on_arguments(condition=cache_on)
395 @region.conditional_cache_on_arguments(condition=cache_on)
396 def _ctx_parents(_repo_id, _commit_id):
396 def _ctx_parents(_repo_id, _commit_id):
397 repo = self._factory.repo(wire)
397 repo = self._factory.repo(wire)
398 ctx = self._get_ctx(repo, commit_id)
398 ctx = self._get_ctx(repo, commit_id)
399 return [parent.hex() for parent in ctx.parents()
399 return [parent.hex() for parent in ctx.parents()
400 if not (parent.hidden() or parent.obsolete())]
400 if not (parent.hidden() or parent.obsolete())]
401
401
402 return _ctx_parents(repo_id, commit_id)
402 return _ctx_parents(repo_id, commit_id)
403
403
404 @reraise_safe_exceptions
404 @reraise_safe_exceptions
405 def ctx_children(self, wire, commit_id):
405 def ctx_children(self, wire, commit_id):
406 cache_on, context_uid, repo_id = self._cache_on(wire)
406 cache_on, context_uid, repo_id = self._cache_on(wire)
407 region = self._region(wire)
407 region = self._region(wire)
408
408
409 @region.conditional_cache_on_arguments(condition=cache_on)
409 @region.conditional_cache_on_arguments(condition=cache_on)
410 def _ctx_children(_repo_id, _commit_id):
410 def _ctx_children(_repo_id, _commit_id):
411 repo = self._factory.repo(wire)
411 repo = self._factory.repo(wire)
412 ctx = self._get_ctx(repo, commit_id)
412 ctx = self._get_ctx(repo, commit_id)
413 return [child.hex() for child in ctx.children()
413 return [child.hex() for child in ctx.children()
414 if not (child.hidden() or child.obsolete())]
414 if not (child.hidden() or child.obsolete())]
415
415
416 return _ctx_children(repo_id, commit_id)
416 return _ctx_children(repo_id, commit_id)
417
417
418 @reraise_safe_exceptions
418 @reraise_safe_exceptions
419 def ctx_phase(self, wire, commit_id):
419 def ctx_phase(self, wire, commit_id):
420 cache_on, context_uid, repo_id = self._cache_on(wire)
420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 region = self._region(wire)
421 region = self._region(wire)
422
422
423 @region.conditional_cache_on_arguments(condition=cache_on)
423 @region.conditional_cache_on_arguments(condition=cache_on)
424 def _ctx_phase(_context_uid, _repo_id, _commit_id):
424 def _ctx_phase(_context_uid, _repo_id, _commit_id):
425 repo = self._factory.repo(wire)
425 repo = self._factory.repo(wire)
426 ctx = self._get_ctx(repo, commit_id)
426 ctx = self._get_ctx(repo, commit_id)
427 # public=0, draft=1, secret=3
427 # public=0, draft=1, secret=3
428 return ctx.phase()
428 return ctx.phase()
429 return _ctx_phase(context_uid, repo_id, commit_id)
429 return _ctx_phase(context_uid, repo_id, commit_id)
430
430
431 @reraise_safe_exceptions
431 @reraise_safe_exceptions
432 def ctx_obsolete(self, wire, commit_id):
432 def ctx_obsolete(self, wire, commit_id):
433 cache_on, context_uid, repo_id = self._cache_on(wire)
433 cache_on, context_uid, repo_id = self._cache_on(wire)
434 region = self._region(wire)
434 region = self._region(wire)
435
435
436 @region.conditional_cache_on_arguments(condition=cache_on)
436 @region.conditional_cache_on_arguments(condition=cache_on)
437 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
437 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
438 repo = self._factory.repo(wire)
438 repo = self._factory.repo(wire)
439 ctx = self._get_ctx(repo, commit_id)
439 ctx = self._get_ctx(repo, commit_id)
440 return ctx.obsolete()
440 return ctx.obsolete()
441 return _ctx_obsolete(context_uid, repo_id, commit_id)
441 return _ctx_obsolete(context_uid, repo_id, commit_id)
442
442
443 @reraise_safe_exceptions
443 @reraise_safe_exceptions
444 def ctx_hidden(self, wire, commit_id):
444 def ctx_hidden(self, wire, commit_id):
445 cache_on, context_uid, repo_id = self._cache_on(wire)
445 cache_on, context_uid, repo_id = self._cache_on(wire)
446 region = self._region(wire)
446 region = self._region(wire)
447
447
448 @region.conditional_cache_on_arguments(condition=cache_on)
448 @region.conditional_cache_on_arguments(condition=cache_on)
449 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
449 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
450 repo = self._factory.repo(wire)
450 repo = self._factory.repo(wire)
451 ctx = self._get_ctx(repo, commit_id)
451 ctx = self._get_ctx(repo, commit_id)
452 return ctx.hidden()
452 return ctx.hidden()
453 return _ctx_hidden(context_uid, repo_id, commit_id)
453 return _ctx_hidden(context_uid, repo_id, commit_id)
454
454
455 @reraise_safe_exceptions
455 @reraise_safe_exceptions
456 def ctx_substate(self, wire, revision):
456 def ctx_substate(self, wire, revision):
457 repo = self._factory.repo(wire)
457 repo = self._factory.repo(wire)
458 ctx = self._get_ctx(repo, revision)
458 ctx = self._get_ctx(repo, revision)
459 return ctx.substate
459 return ctx.substate
460
460
461 @reraise_safe_exceptions
461 @reraise_safe_exceptions
462 def ctx_status(self, wire, revision):
462 def ctx_status(self, wire, revision):
463 repo = self._factory.repo(wire)
463 repo = self._factory.repo(wire)
464 ctx = self._get_ctx(repo, revision)
464 ctx = self._get_ctx(repo, revision)
465 status = repo[ctx.p1().node()].status(other=ctx.node())
465 status = repo[ctx.p1().node()].status(other=ctx.node())
466 # object of status (odd, custom named tuple in mercurial) is not
466 # object of status (odd, custom named tuple in mercurial) is not
467 # correctly serializable, we make it a list, as the underling
467 # correctly serializable, we make it a list, as the underling
468 # API expects this to be a list
468 # API expects this to be a list
469 return list(status)
469 return list(status)
470
470
471 @reraise_safe_exceptions
471 @reraise_safe_exceptions
472 def ctx_user(self, wire, revision):
472 def ctx_user(self, wire, revision):
473 repo = self._factory.repo(wire)
473 repo = self._factory.repo(wire)
474 ctx = self._get_ctx(repo, revision)
474 ctx = self._get_ctx(repo, revision)
475 return ctx.user()
475 return ctx.user()
476
476
477 @reraise_safe_exceptions
477 @reraise_safe_exceptions
478 def check_url(self, url, config):
478 def check_url(self, url, config):
479 url, _proto = normalize_url_for_hg(url)
479 url, _proto = normalize_url_for_hg(url)
480 url_obj = url_parser(safe_bytes(url))
480 url_obj = url_parser(safe_bytes(url))
481
481
482 test_uri = safe_str(url_obj.authinfo()[0])
482 test_uri = safe_str(url_obj.authinfo()[0])
483 authinfo = url_obj.authinfo()[1]
483 authinfo = url_obj.authinfo()[1]
484 obfuscated_uri = get_obfuscated_url(url_obj)
484 obfuscated_uri = get_obfuscated_url(url_obj)
485 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
485 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
486
486
487 handlers = []
487 handlers = []
488 if authinfo:
488 if authinfo:
489 # create a password manager
489 # create a password manager
490 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
490 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
491 passmgr.add_password(*authinfo)
491 passmgr.add_password(*authinfo)
492
492
493 handlers.extend((httpbasicauthhandler(passmgr),
493 handlers.extend((httpbasicauthhandler(passmgr),
494 httpdigestauthhandler(passmgr)))
494 httpdigestauthhandler(passmgr)))
495
495
496 o = urllib.request.build_opener(*handlers)
496 o = urllib.request.build_opener(*handlers)
497 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
497 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
498 ('Accept', 'application/mercurial-0.1')]
498 ('Accept', 'application/mercurial-0.1')]
499
499
500 q = {"cmd": 'between'}
500 q = {"cmd": 'between'}
501 q.update({'pairs': "{}-{}".format('0' * 40, '0' * 40)})
501 q.update({'pairs': "{}-{}".format('0' * 40, '0' * 40)})
502 qs = f'?{urllib.parse.urlencode(q)}'
502 qs = f'?{urllib.parse.urlencode(q)}'
503 cu = f"{test_uri}{qs}"
503 cu = f"{test_uri}{qs}"
504
504
505 try:
505 try:
506 req = urllib.request.Request(cu, None, {})
506 req = urllib.request.Request(cu, None, {})
507 log.debug("Trying to open URL %s", obfuscated_uri)
507 log.debug("Trying to open URL %s", obfuscated_uri)
508 resp = o.open(req)
508 resp = o.open(req)
509 if resp.code != 200:
509 if resp.code != 200:
510 raise exceptions.URLError()('Return Code is not 200')
510 raise exceptions.URLError()('Return Code is not 200')
511 except Exception as e:
511 except Exception as e:
512 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
512 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
513 # means it cannot be cloned
513 # means it cannot be cloned
514 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
514 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
515
515
516 # now check if it's a proper hg repo, but don't do it for svn
516 # now check if it's a proper hg repo, but don't do it for svn
517 try:
517 try:
518 if _proto == 'svn':
518 if _proto == 'svn':
519 pass
519 pass
520 else:
520 else:
521 # check for pure hg repos
521 # check for pure hg repos
522 log.debug(
522 log.debug(
523 "Verifying if URL is a Mercurial repository: %s", obfuscated_uri)
523 "Verifying if URL is a Mercurial repository: %s", obfuscated_uri)
524 ui = make_ui_from_config(config)
524 ui = make_ui_from_config(config)
525 peer_checker = makepeer(ui, safe_bytes(url))
525 peer_checker = makepeer(ui, safe_bytes(url))
526 peer_checker.lookup(b'tip')
526 peer_checker.lookup(b'tip')
527 except Exception as e:
527 except Exception as e:
528 log.warning("URL is not a valid Mercurial repository: %s",
528 log.warning("URL is not a valid Mercurial repository: %s",
529 obfuscated_uri)
529 obfuscated_uri)
530 raise exceptions.URLError(e)(
530 raise exceptions.URLError(e)(
531 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
531 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
532
532
533 log.info("URL is a valid Mercurial repository: %s", obfuscated_uri)
533 log.info("URL is a valid Mercurial repository: %s", obfuscated_uri)
534 return True
534 return True
535
535
536 @reraise_safe_exceptions
536 @reraise_safe_exceptions
537 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
537 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
538 repo = self._factory.repo(wire)
538 repo = self._factory.repo(wire)
539
539
540 if file_filter:
540 if file_filter:
541 # unpack the file-filter
541 # unpack the file-filter
542 repo_path, node_path = file_filter
542 repo_path, node_path = file_filter
543 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
543 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
544 else:
544 else:
545 match_filter = file_filter
545 match_filter = file_filter
546 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
546 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
547
547
548 try:
548 try:
549 diff_iter = patch.diff(
549 diff_iter = patch.diff(
550 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
550 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
551 return BytesEnvelope(b"".join(diff_iter))
551 return BytesEnvelope(b"".join(diff_iter))
552 except RepoLookupError as e:
552 except RepoLookupError as e:
553 raise exceptions.LookupException(e)()
553 raise exceptions.LookupException(e)()
554
554
555 @reraise_safe_exceptions
555 @reraise_safe_exceptions
556 def node_history(self, wire, revision, path, limit):
556 def node_history(self, wire, revision, path, limit):
557 cache_on, context_uid, repo_id = self._cache_on(wire)
557 cache_on, context_uid, repo_id = self._cache_on(wire)
558 region = self._region(wire)
558 region = self._region(wire)
559
559
560 @region.conditional_cache_on_arguments(condition=cache_on)
560 @region.conditional_cache_on_arguments(condition=cache_on)
561 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
561 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
562 repo = self._factory.repo(wire)
562 repo = self._factory.repo(wire)
563
563
564 ctx = self._get_ctx(repo, revision)
564 ctx = self._get_ctx(repo, revision)
565 fctx = ctx.filectx(safe_bytes(path))
565 fctx = ctx.filectx(safe_bytes(path))
566
566
567 def history_iter():
567 def history_iter():
568 limit_rev = fctx.rev()
568 limit_rev = fctx.rev()
569
569
570 for fctx_candidate in reversed(list(fctx.filelog())):
570 for fctx_candidate in reversed(list(fctx.filelog())):
571 f_obj = fctx.filectx(fctx_candidate)
571 f_obj = fctx.filectx(fctx_candidate)
572
572
573 # NOTE: This can be problematic...we can hide ONLY history node resulting in empty history
573 # NOTE: This can be problematic...we can hide ONLY history node resulting in empty history
574 _ctx = f_obj.changectx()
574 _ctx = f_obj.changectx()
575 if _ctx.hidden() or _ctx.obsolete():
575 if _ctx.hidden() or _ctx.obsolete():
576 continue
576 continue
577
577
578 if limit_rev >= f_obj.rev():
578 if limit_rev >= f_obj.rev():
579 yield f_obj
579 yield f_obj
580
580
581 history = []
581 history = []
582 for cnt, obj in enumerate(history_iter()):
582 for cnt, obj in enumerate(history_iter()):
583 if limit and cnt >= limit:
583 if limit and cnt >= limit:
584 break
584 break
585 history.append(hex(obj.node()))
585 history.append(hex(obj.node()))
586
586
587 return [x for x in history]
587 return [x for x in history]
588 return _node_history(context_uid, repo_id, revision, path, limit)
588 return _node_history(context_uid, repo_id, revision, path, limit)
589
589
590 @reraise_safe_exceptions
590 @reraise_safe_exceptions
591 def node_history_until(self, wire, revision, path, limit):
591 def node_history_until(self, wire, revision, path, limit):
592 cache_on, context_uid, repo_id = self._cache_on(wire)
592 cache_on, context_uid, repo_id = self._cache_on(wire)
593 region = self._region(wire)
593 region = self._region(wire)
594
594
595 @region.conditional_cache_on_arguments(condition=cache_on)
595 @region.conditional_cache_on_arguments(condition=cache_on)
596 def _node_history_until(_context_uid, _repo_id):
596 def _node_history_until(_context_uid, _repo_id):
597 repo = self._factory.repo(wire)
597 repo = self._factory.repo(wire)
598 ctx = self._get_ctx(repo, revision)
598 ctx = self._get_ctx(repo, revision)
599 fctx = ctx.filectx(safe_bytes(path))
599 fctx = ctx.filectx(safe_bytes(path))
600
600
601 file_log = list(fctx.filelog())
601 file_log = list(fctx.filelog())
602 if limit:
602 if limit:
603 # Limit to the last n items
603 # Limit to the last n items
604 file_log = file_log[-limit:]
604 file_log = file_log[-limit:]
605
605
606 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
606 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
607 return _node_history_until(context_uid, repo_id, revision, path, limit)
607 return _node_history_until(context_uid, repo_id, revision, path, limit)
608
608
609 @reraise_safe_exceptions
609 @reraise_safe_exceptions
610 def bulk_file_request(self, wire, commit_id, path, pre_load):
610 def bulk_file_request(self, wire, commit_id, path, pre_load):
611 cache_on, context_uid, repo_id = self._cache_on(wire)
611 cache_on, context_uid, repo_id = self._cache_on(wire)
612 region = self._region(wire)
612 region = self._region(wire)
613
613
614 @region.conditional_cache_on_arguments(condition=cache_on)
614 @region.conditional_cache_on_arguments(condition=cache_on)
615 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
615 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
616 result = {}
616 result = {}
617 for attr in pre_load:
617 for attr in pre_load:
618 try:
618 try:
619 method = self._bulk_file_methods[attr]
619 method = self._bulk_file_methods[attr]
620 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
620 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
621 result[attr] = method(wire, _commit_id, _path)
621 result[attr] = method(wire, _commit_id, _path)
622 except KeyError as e:
622 except KeyError as e:
623 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
623 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
624 return result
624 return result
625
625
626 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
626 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
627
627
628 @reraise_safe_exceptions
628 @reraise_safe_exceptions
629 def fctx_annotate(self, wire, revision, path):
629 def fctx_annotate(self, wire, revision, path):
630 repo = self._factory.repo(wire)
630 repo = self._factory.repo(wire)
631 ctx = self._get_ctx(repo, revision)
631 ctx = self._get_ctx(repo, revision)
632 fctx = ctx.filectx(safe_bytes(path))
632 fctx = ctx.filectx(safe_bytes(path))
633
633
634 result = []
634 result = []
635 for i, annotate_obj in enumerate(fctx.annotate(), 1):
635 for i, annotate_obj in enumerate(fctx.annotate(), 1):
636 ln_no = i
636 ln_no = i
637 sha = hex(annotate_obj.fctx.node())
637 sha = hex(annotate_obj.fctx.node())
638 content = annotate_obj.text
638 content = annotate_obj.text
639 result.append((ln_no, ascii_str(sha), content))
639 result.append((ln_no, ascii_str(sha), content))
640 return BinaryEnvelope(result)
640 return BinaryEnvelope(result)
641
641
642 @reraise_safe_exceptions
642 @reraise_safe_exceptions
643 def fctx_node_data(self, wire, revision, path):
643 def fctx_node_data(self, wire, revision, path):
644 repo = self._factory.repo(wire)
644 repo = self._factory.repo(wire)
645 ctx = self._get_ctx(repo, revision)
645 ctx = self._get_ctx(repo, revision)
646 fctx = ctx.filectx(safe_bytes(path))
646 fctx = ctx.filectx(safe_bytes(path))
647 return BytesEnvelope(fctx.data())
647 return BytesEnvelope(fctx.data())
648
648
649 @reraise_safe_exceptions
649 @reraise_safe_exceptions
650 def fctx_flags(self, wire, commit_id, path):
650 def fctx_flags(self, wire, commit_id, path):
651 cache_on, context_uid, repo_id = self._cache_on(wire)
651 cache_on, context_uid, repo_id = self._cache_on(wire)
652 region = self._region(wire)
652 region = self._region(wire)
653
653
654 @region.conditional_cache_on_arguments(condition=cache_on)
654 @region.conditional_cache_on_arguments(condition=cache_on)
655 def _fctx_flags(_repo_id, _commit_id, _path):
655 def _fctx_flags(_repo_id, _commit_id, _path):
656 repo = self._factory.repo(wire)
656 repo = self._factory.repo(wire)
657 ctx = self._get_ctx(repo, commit_id)
657 ctx = self._get_ctx(repo, commit_id)
658 fctx = ctx.filectx(safe_bytes(path))
658 fctx = ctx.filectx(safe_bytes(path))
659 return fctx.flags()
659 return fctx.flags()
660
660
661 return _fctx_flags(repo_id, commit_id, path)
661 return _fctx_flags(repo_id, commit_id, path)
662
662
663 @reraise_safe_exceptions
663 @reraise_safe_exceptions
664 def fctx_size(self, wire, commit_id, path):
664 def fctx_size(self, wire, commit_id, path):
665 cache_on, context_uid, repo_id = self._cache_on(wire)
665 cache_on, context_uid, repo_id = self._cache_on(wire)
666 region = self._region(wire)
666 region = self._region(wire)
667
667
668 @region.conditional_cache_on_arguments(condition=cache_on)
668 @region.conditional_cache_on_arguments(condition=cache_on)
669 def _fctx_size(_repo_id, _revision, _path):
669 def _fctx_size(_repo_id, _revision, _path):
670 repo = self._factory.repo(wire)
670 repo = self._factory.repo(wire)
671 ctx = self._get_ctx(repo, commit_id)
671 ctx = self._get_ctx(repo, commit_id)
672 fctx = ctx.filectx(safe_bytes(path))
672 fctx = ctx.filectx(safe_bytes(path))
673 return fctx.size()
673 return fctx.size()
674 return _fctx_size(repo_id, commit_id, path)
674 return _fctx_size(repo_id, commit_id, path)
675
675
676 @reraise_safe_exceptions
676 @reraise_safe_exceptions
677 def get_all_commit_ids(self, wire, name):
677 def get_all_commit_ids(self, wire, name):
678 cache_on, context_uid, repo_id = self._cache_on(wire)
678 cache_on, context_uid, repo_id = self._cache_on(wire)
679 region = self._region(wire)
679 region = self._region(wire)
680
680
681 @region.conditional_cache_on_arguments(condition=cache_on)
681 @region.conditional_cache_on_arguments(condition=cache_on)
682 def _get_all_commit_ids(_context_uid, _repo_id, _name):
682 def _get_all_commit_ids(_context_uid, _repo_id, _name):
683 repo = self._factory.repo(wire)
683 repo = self._factory.repo(wire)
684 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
684 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
685 return revs
685 return revs
686 return _get_all_commit_ids(context_uid, repo_id, name)
686 return _get_all_commit_ids(context_uid, repo_id, name)
687
687
688 @reraise_safe_exceptions
688 @reraise_safe_exceptions
689 def get_config_value(self, wire, section, name, untrusted=False):
689 def get_config_value(self, wire, section, name, untrusted=False):
690 repo = self._factory.repo(wire)
690 repo = self._factory.repo(wire)
691 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
691 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
692
692
693 @reraise_safe_exceptions
693 @reraise_safe_exceptions
694 def is_large_file(self, wire, commit_id, path):
694 def is_large_file(self, wire, commit_id, path):
695 cache_on, context_uid, repo_id = self._cache_on(wire)
695 cache_on, context_uid, repo_id = self._cache_on(wire)
696 region = self._region(wire)
696 region = self._region(wire)
697
697
698 @region.conditional_cache_on_arguments(condition=cache_on)
698 @region.conditional_cache_on_arguments(condition=cache_on)
699 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
699 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
700 return largefiles.lfutil.isstandin(safe_bytes(path))
700 return largefiles.lfutil.isstandin(safe_bytes(path))
701
701
702 return _is_large_file(context_uid, repo_id, commit_id, path)
702 return _is_large_file(context_uid, repo_id, commit_id, path)
703
703
704 @reraise_safe_exceptions
704 @reraise_safe_exceptions
705 def is_binary(self, wire, revision, path):
705 def is_binary(self, wire, revision, path):
706 cache_on, context_uid, repo_id = self._cache_on(wire)
706 cache_on, context_uid, repo_id = self._cache_on(wire)
707 region = self._region(wire)
707 region = self._region(wire)
708
708
709 @region.conditional_cache_on_arguments(condition=cache_on)
709 @region.conditional_cache_on_arguments(condition=cache_on)
710 def _is_binary(_repo_id, _sha, _path):
710 def _is_binary(_repo_id, _sha, _path):
711 repo = self._factory.repo(wire)
711 repo = self._factory.repo(wire)
712 ctx = self._get_ctx(repo, revision)
712 ctx = self._get_ctx(repo, revision)
713 fctx = ctx.filectx(safe_bytes(path))
713 fctx = ctx.filectx(safe_bytes(path))
714 return fctx.isbinary()
714 return fctx.isbinary()
715
715
716 return _is_binary(repo_id, revision, path)
716 return _is_binary(repo_id, revision, path)
717
717
718 @reraise_safe_exceptions
718 @reraise_safe_exceptions
719 def md5_hash(self, wire, revision, path):
719 def md5_hash(self, wire, revision, path):
720 cache_on, context_uid, repo_id = self._cache_on(wire)
720 cache_on, context_uid, repo_id = self._cache_on(wire)
721 region = self._region(wire)
721 region = self._region(wire)
722
722
723 @region.conditional_cache_on_arguments(condition=cache_on)
723 @region.conditional_cache_on_arguments(condition=cache_on)
724 def _md5_hash(_repo_id, _sha, _path):
724 def _md5_hash(_repo_id, _sha, _path):
725 repo = self._factory.repo(wire)
725 repo = self._factory.repo(wire)
726 ctx = self._get_ctx(repo, revision)
726 ctx = self._get_ctx(repo, revision)
727 fctx = ctx.filectx(safe_bytes(path))
727 fctx = ctx.filectx(safe_bytes(path))
728 return hashlib.md5(fctx.data()).hexdigest()
728 return hashlib.md5(fctx.data()).hexdigest()
729
729
730 return _md5_hash(repo_id, revision, path)
730 return _md5_hash(repo_id, revision, path)
731
731
732 @reraise_safe_exceptions
732 @reraise_safe_exceptions
733 def in_largefiles_store(self, wire, sha):
733 def in_largefiles_store(self, wire, sha):
734 repo = self._factory.repo(wire)
734 repo = self._factory.repo(wire)
735 return largefiles.lfutil.instore(repo, sha)
735 return largefiles.lfutil.instore(repo, sha)
736
736
737 @reraise_safe_exceptions
737 @reraise_safe_exceptions
738 def in_user_cache(self, wire, sha):
738 def in_user_cache(self, wire, sha):
739 repo = self._factory.repo(wire)
739 repo = self._factory.repo(wire)
740 return largefiles.lfutil.inusercache(repo.ui, sha)
740 return largefiles.lfutil.inusercache(repo.ui, sha)
741
741
742 @reraise_safe_exceptions
742 @reraise_safe_exceptions
743 def store_path(self, wire, sha):
743 def store_path(self, wire, sha):
744 repo = self._factory.repo(wire)
744 repo = self._factory.repo(wire)
745 return largefiles.lfutil.storepath(repo, sha)
745 return largefiles.lfutil.storepath(repo, sha)
746
746
747 @reraise_safe_exceptions
747 @reraise_safe_exceptions
748 def link(self, wire, sha, path):
748 def link(self, wire, sha, path):
749 repo = self._factory.repo(wire)
749 repo = self._factory.repo(wire)
750 largefiles.lfutil.link(
750 largefiles.lfutil.link(
751 largefiles.lfutil.usercachepath(repo.ui, sha), path)
751 largefiles.lfutil.usercachepath(repo.ui, sha), path)
752
752
753 @reraise_safe_exceptions
753 @reraise_safe_exceptions
754 def localrepository(self, wire, create=False):
754 def localrepository(self, wire, create=False):
755 self._factory.repo(wire, create=create)
755 self._factory.repo(wire, create=create)
756
756
757 @reraise_safe_exceptions
757 @reraise_safe_exceptions
758 def lookup(self, wire, revision, both):
758 def lookup(self, wire, revision, both):
759 cache_on, context_uid, repo_id = self._cache_on(wire)
759 cache_on, context_uid, repo_id = self._cache_on(wire)
760 region = self._region(wire)
760 region = self._region(wire)
761
761
762 @region.conditional_cache_on_arguments(condition=cache_on)
762 @region.conditional_cache_on_arguments(condition=cache_on)
763 def _lookup(_context_uid, _repo_id, _revision, _both):
763 def _lookup(_context_uid, _repo_id, _revision, _both):
764 repo = self._factory.repo(wire)
764 repo = self._factory.repo(wire)
765 rev = _revision
765 rev = _revision
766 if isinstance(rev, int):
766 if isinstance(rev, int):
767 # NOTE(marcink):
767 # NOTE(marcink):
768 # since Mercurial doesn't support negative indexes properly
768 # since Mercurial doesn't support negative indexes properly
769 # we need to shift accordingly by one to get proper index, e.g
769 # we need to shift accordingly by one to get proper index, e.g
770 # repo[-1] => repo[-2]
770 # repo[-1] => repo[-2]
771 # repo[0] => repo[-1]
771 # repo[0] => repo[-1]
772 if rev <= 0:
772 if rev <= 0:
773 rev = rev + -1
773 rev = rev + -1
774 try:
774 try:
775 ctx = self._get_ctx(repo, rev)
775 ctx = self._get_ctx(repo, rev)
776 except AmbiguousPrefixLookupError:
776 except AmbiguousPrefixLookupError:
777 e = RepoLookupError(rev)
777 e = RepoLookupError(rev)
778 e._org_exc_tb = format_exc(sys.exc_info())
778 e._org_exc_tb = format_exc(sys.exc_info())
779 raise exceptions.LookupException(e)(rev)
779 raise exceptions.LookupException(e)(rev)
780 except (TypeError, RepoLookupError, binascii.Error) as e:
780 except (TypeError, RepoLookupError, binascii.Error) as e:
781 e._org_exc_tb = format_exc(sys.exc_info())
781 e._org_exc_tb = format_exc(sys.exc_info())
782 raise exceptions.LookupException(e)(rev)
782 raise exceptions.LookupException(e)(rev)
783 except LookupError as e:
783 except LookupError as e:
784 e._org_exc_tb = format_exc(sys.exc_info())
784 e._org_exc_tb = format_exc(sys.exc_info())
785 raise exceptions.LookupException(e)(e.name)
785 raise exceptions.LookupException(e)(e.name)
786
786
787 if not both:
787 if not both:
788 return ctx.hex()
788 return ctx.hex()
789
789
790 ctx = repo[ctx.hex()]
790 ctx = repo[ctx.hex()]
791 return ctx.hex(), ctx.rev()
791 return ctx.hex(), ctx.rev()
792
792
793 return _lookup(context_uid, repo_id, revision, both)
793 return _lookup(context_uid, repo_id, revision, both)
794
794
795 @reraise_safe_exceptions
795 @reraise_safe_exceptions
796 def sync_push(self, wire, url):
796 def sync_push(self, wire, url):
797 if not self.check_url(url, wire['config']):
797 if not self.check_url(url, wire['config']):
798 return
798 return
799
799
800 repo = self._factory.repo(wire)
800 repo = self._factory.repo(wire)
801
801
802 # Disable any prompts for this repo
802 # Disable any prompts for this repo
803 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
803 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
804
804
805 bookmarks = list(dict(repo._bookmarks).keys())
805 bookmarks = list(dict(repo._bookmarks).keys())
806 remote = peer(repo, {}, safe_bytes(url))
806 remote = peer(repo, {}, safe_bytes(url))
807 # Disable any prompts for this remote
807 # Disable any prompts for this remote
808 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
808 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
809
809
810 return exchange.push(
810 return exchange.push(
811 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
811 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
812
812
813 @reraise_safe_exceptions
813 @reraise_safe_exceptions
814 def revision(self, wire, rev):
814 def revision(self, wire, rev):
815 repo = self._factory.repo(wire)
815 repo = self._factory.repo(wire)
816 ctx = self._get_ctx(repo, rev)
816 ctx = self._get_ctx(repo, rev)
817 return ctx.rev()
817 return ctx.rev()
818
818
819 @reraise_safe_exceptions
819 @reraise_safe_exceptions
820 def rev_range(self, wire, commit_filter):
820 def rev_range(self, wire, commit_filter):
821 cache_on, context_uid, repo_id = self._cache_on(wire)
821 cache_on, context_uid, repo_id = self._cache_on(wire)
822 region = self._region(wire)
822 region = self._region(wire)
823
823
824 @region.conditional_cache_on_arguments(condition=cache_on)
824 @region.conditional_cache_on_arguments(condition=cache_on)
825 def _rev_range(_context_uid, _repo_id, _filter):
825 def _rev_range(_context_uid, _repo_id, _filter):
826 repo = self._factory.repo(wire)
826 repo = self._factory.repo(wire)
827 revisions = [
827 revisions = [
828 ascii_str(repo[rev].hex())
828 ascii_str(repo[rev].hex())
829 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
829 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
830 ]
830 ]
831 return revisions
831 return revisions
832
832
833 return _rev_range(context_uid, repo_id, sorted(commit_filter))
833 return _rev_range(context_uid, repo_id, sorted(commit_filter))
834
834
835 @reraise_safe_exceptions
835 @reraise_safe_exceptions
836 def rev_range_hash(self, wire, node):
836 def rev_range_hash(self, wire, node):
837 repo = self._factory.repo(wire)
837 repo = self._factory.repo(wire)
838
838
839 def get_revs(repo, rev_opt):
839 def get_revs(repo, rev_opt):
840 if rev_opt:
840 if rev_opt:
841 revs = revrange(repo, rev_opt)
841 revs = revrange(repo, rev_opt)
842 if len(revs) == 0:
842 if len(revs) == 0:
843 return (nullrev, nullrev)
843 return (nullrev, nullrev)
844 return max(revs), min(revs)
844 return max(revs), min(revs)
845 else:
845 else:
846 return len(repo) - 1, 0
846 return len(repo) - 1, 0
847
847
848 stop, start = get_revs(repo, [node + ':'])
848 stop, start = get_revs(repo, [node + ':'])
849 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
849 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
850 return revs
850 return revs
851
851
852 @reraise_safe_exceptions
852 @reraise_safe_exceptions
853 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
853 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
854 org_path = safe_bytes(wire["path"])
854 org_path = safe_bytes(wire["path"])
855 other_path = safe_bytes(kwargs.pop('other_path', ''))
855 other_path = safe_bytes(kwargs.pop('other_path', ''))
856
856
857 # case when we want to compare two independent repositories
857 # case when we want to compare two independent repositories
858 if other_path and other_path != wire["path"]:
858 if other_path and other_path != wire["path"]:
859 baseui = self._factory._create_config(wire["config"])
859 baseui = self._factory._create_config(wire["config"])
860 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
860 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
861 else:
861 else:
862 repo = self._factory.repo(wire)
862 repo = self._factory.repo(wire)
863 return list(repo.revs(rev_spec, *args))
863 return list(repo.revs(rev_spec, *args))
864
864
865 @reraise_safe_exceptions
865 @reraise_safe_exceptions
866 def verify(self, wire,):
866 def verify(self, wire,):
867 repo = self._factory.repo(wire)
867 repo = self._factory.repo(wire)
868 baseui = self._factory._create_config(wire['config'])
868 baseui = self._factory._create_config(wire['config'])
869
869
870 baseui, output = patch_ui_message_output(baseui)
870 baseui, output = patch_ui_message_output(baseui)
871
871
872 repo.ui = baseui
872 repo.ui = baseui
873 verify.verify(repo)
873 verify.verify(repo)
874 return output.getvalue()
874 return output.getvalue()
875
875
876 @reraise_safe_exceptions
876 @reraise_safe_exceptions
877 def hg_update_cache(self, wire,):
877 def hg_update_cache(self, wire,):
878 repo = self._factory.repo(wire)
878 repo = self._factory.repo(wire)
879 baseui = self._factory._create_config(wire['config'])
879 baseui = self._factory._create_config(wire['config'])
880 baseui, output = patch_ui_message_output(baseui)
880 baseui, output = patch_ui_message_output(baseui)
881
881
882 repo.ui = baseui
882 repo.ui = baseui
883 with repo.wlock(), repo.lock():
883 with repo.wlock(), repo.lock():
884 repo.updatecaches(full=True)
884 repo.updatecaches(full=True)
885
885
886 return output.getvalue()
886 return output.getvalue()
887
887
888 @reraise_safe_exceptions
888 @reraise_safe_exceptions
889 def hg_rebuild_fn_cache(self, wire,):
889 def hg_rebuild_fn_cache(self, wire,):
890 repo = self._factory.repo(wire)
890 repo = self._factory.repo(wire)
891 baseui = self._factory._create_config(wire['config'])
891 baseui = self._factory._create_config(wire['config'])
892 baseui, output = patch_ui_message_output(baseui)
892 baseui, output = patch_ui_message_output(baseui)
893
893
894 repo.ui = baseui
894 repo.ui = baseui
895
895
896 repair.rebuildfncache(baseui, repo)
896 repair.rebuildfncache(baseui, repo)
897
897
898 return output.getvalue()
898 return output.getvalue()
899
899
900 @reraise_safe_exceptions
900 @reraise_safe_exceptions
901 def tags(self, wire):
901 def tags(self, wire):
902 cache_on, context_uid, repo_id = self._cache_on(wire)
902 cache_on, context_uid, repo_id = self._cache_on(wire)
903 region = self._region(wire)
903 region = self._region(wire)
904
904
905 @region.conditional_cache_on_arguments(condition=cache_on)
905 @region.conditional_cache_on_arguments(condition=cache_on)
906 def _tags(_context_uid, _repo_id):
906 def _tags(_context_uid, _repo_id):
907 repo = self._factory.repo(wire)
907 repo = self._factory.repo(wire)
908 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
908 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
909
909
910 return _tags(context_uid, repo_id)
910 return _tags(context_uid, repo_id)
911
911
912 @reraise_safe_exceptions
912 @reraise_safe_exceptions
913 def update(self, wire, node='', clean=False):
913 def update(self, wire, node='', clean=False):
914 repo = self._factory.repo(wire)
914 repo = self._factory.repo(wire)
915 baseui = self._factory._create_config(wire['config'])
915 baseui = self._factory._create_config(wire['config'])
916 node = safe_bytes(node)
916 node = safe_bytes(node)
917
917
918 commands.update(baseui, repo, node=node, clean=clean)
918 commands.update(baseui, repo, node=node, clean=clean)
919
919
920 @reraise_safe_exceptions
920 @reraise_safe_exceptions
921 def identify(self, wire):
921 def identify(self, wire):
922 repo = self._factory.repo(wire)
922 repo = self._factory.repo(wire)
923 baseui = self._factory._create_config(wire['config'])
923 baseui = self._factory._create_config(wire['config'])
924 output = io.BytesIO()
924 output = io.BytesIO()
925 baseui.write = output.write
925 baseui.write = output.write
926 # This is required to get a full node id
926 # This is required to get a full node id
927 baseui.debugflag = True
927 baseui.debugflag = True
928 commands.identify(baseui, repo, id=True)
928 commands.identify(baseui, repo, id=True)
929
929
930 return output.getvalue()
930 return output.getvalue()
931
931
932 @reraise_safe_exceptions
932 @reraise_safe_exceptions
933 def heads(self, wire, branch=None):
933 def heads(self, wire, branch=None):
934 repo = self._factory.repo(wire)
934 repo = self._factory.repo(wire)
935 baseui = self._factory._create_config(wire['config'])
935 baseui = self._factory._create_config(wire['config'])
936 output = io.BytesIO()
936 output = io.BytesIO()
937
937
938 def write(data, **unused_kwargs):
938 def write(data, **unused_kwargs):
939 output.write(data)
939 output.write(data)
940
940
941 baseui.write = write
941 baseui.write = write
942 if branch:
942 if branch:
943 args = [safe_bytes(branch)]
943 args = [safe_bytes(branch)]
944 else:
944 else:
945 args = []
945 args = []
946 commands.heads(baseui, repo, template=b'{node} ', *args)
946 commands.heads(baseui, repo, template=b'{node} ', *args)
947
947
948 return output.getvalue()
948 return output.getvalue()
949
949
950 @reraise_safe_exceptions
950 @reraise_safe_exceptions
951 def ancestor(self, wire, revision1, revision2):
951 def ancestor(self, wire, revision1, revision2):
952 repo = self._factory.repo(wire)
952 repo = self._factory.repo(wire)
953 changelog = repo.changelog
953 changelog = repo.changelog
954 lookup = repo.lookup
954 lookup = repo.lookup
955 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
955 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
956 return hex(a)
956 return hex(a)
957
957
958 @reraise_safe_exceptions
958 @reraise_safe_exceptions
959 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
959 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
960 baseui = self._factory._create_config(wire["config"], hooks=hooks)
960 baseui = self._factory._create_config(wire["config"], hooks=hooks)
961 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
961 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
962
962
963 @reraise_safe_exceptions
963 @reraise_safe_exceptions
964 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
964 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
965
965
966 repo = self._factory.repo(wire)
966 repo = self._factory.repo(wire)
967 baseui = self._factory._create_config(wire['config'])
967 baseui = self._factory._create_config(wire['config'])
968 publishing = baseui.configbool(b'phases', b'publish')
968 publishing = baseui.configbool(b'phases', b'publish')
969
969
970 def _filectxfn(_repo, ctx, path: bytes):
970 def _filectxfn(_repo, ctx, path: bytes):
971 """
971 """
972 Marks given path as added/changed/removed in a given _repo. This is
972 Marks given path as added/changed/removed in a given _repo. This is
973 for internal mercurial commit function.
973 for internal mercurial commit function.
974 """
974 """
975
975
976 # check if this path is removed
976 # check if this path is removed
977 if safe_str(path) in removed:
977 if safe_str(path) in removed:
978 # returning None is a way to mark node for removal
978 # returning None is a way to mark node for removal
979 return None
979 return None
980
980
981 # check if this path is added
981 # check if this path is added
982 for node in updated:
982 for node in updated:
983 if safe_bytes(node['path']) == path:
983 if safe_bytes(node['path']) == path:
984 return memfilectx(
984 return memfilectx(
985 _repo,
985 _repo,
986 changectx=ctx,
986 changectx=ctx,
987 path=safe_bytes(node['path']),
987 path=safe_bytes(node['path']),
988 data=safe_bytes(node['content']),
988 data=safe_bytes(node['content']),
989 islink=False,
989 islink=False,
990 isexec=bool(node['mode'] & stat.S_IXUSR),
990 isexec=bool(node['mode'] & stat.S_IXUSR),
991 copysource=False)
991 copysource=False)
992 abort_exc = exceptions.AbortException()
992 abort_exc = exceptions.AbortException()
993 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
993 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
994
994
995 if publishing:
995 if publishing:
996 new_commit_phase = b'public'
996 new_commit_phase = b'public'
997 else:
997 else:
998 new_commit_phase = b'draft'
998 new_commit_phase = b'draft'
999 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
999 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
1000 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
1000 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
1001 commit_ctx = memctx(
1001 commit_ctx = memctx(
1002 repo=repo,
1002 repo=repo,
1003 parents=parents,
1003 parents=parents,
1004 text=safe_bytes(message),
1004 text=safe_bytes(message),
1005 files=[safe_bytes(x) for x in files],
1005 files=[safe_bytes(x) for x in files],
1006 filectxfn=_filectxfn,
1006 filectxfn=_filectxfn,
1007 user=safe_bytes(user),
1007 user=safe_bytes(user),
1008 date=(commit_time, commit_timezone),
1008 date=(commit_time, commit_timezone),
1009 extra=kwargs)
1009 extra=kwargs)
1010
1010
1011 n = repo.commitctx(commit_ctx)
1011 n = repo.commitctx(commit_ctx)
1012 new_id = hex(n)
1012 new_id = hex(n)
1013
1013
1014 return new_id
1014 return new_id
1015
1015
1016 @reraise_safe_exceptions
1016 @reraise_safe_exceptions
1017 def pull(self, wire, url, commit_ids=None):
1017 def pull(self, wire, url, commit_ids=None):
1018 repo = self._factory.repo(wire)
1018 repo = self._factory.repo(wire)
1019 # Disable any prompts for this repo
1019 # Disable any prompts for this repo
1020 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1020 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1021
1021
1022 remote = peer(repo, {}, safe_bytes(url))
1022 remote = peer(repo, {}, safe_bytes(url))
1023 # Disable any prompts for this remote
1023 # Disable any prompts for this remote
1024 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1024 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1025
1025
1026 if commit_ids:
1026 if commit_ids:
1027 commit_ids = [bin(commit_id) for commit_id in commit_ids]
1027 commit_ids = [bin(commit_id) for commit_id in commit_ids]
1028
1028
1029 return exchange.pull(
1029 return exchange.pull(
1030 repo, remote, heads=commit_ids, force=None).cgresult
1030 repo, remote, heads=commit_ids, force=None).cgresult
1031
1031
1032 @reraise_safe_exceptions
1032 @reraise_safe_exceptions
1033 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
1033 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
1034 repo = self._factory.repo(wire)
1034 repo = self._factory.repo(wire)
1035 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1035 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1036
1036
1037 source = safe_bytes(source)
1037 source = safe_bytes(source)
1038
1038
1039 # Mercurial internally has a lot of logic that checks ONLY if
1039 # Mercurial internally has a lot of logic that checks ONLY if
1040 # option is defined, we just pass those if they are defined then
1040 # option is defined, we just pass those if they are defined then
1041 opts = {}
1041 opts = {}
1042
1042
1043 if bookmark:
1043 if bookmark:
1044 opts['bookmark'] = [safe_bytes(x) for x in bookmark] \
1044 opts['bookmark'] = [safe_bytes(x) for x in bookmark] \
1045 if isinstance(bookmark, list) else safe_bytes(bookmark)
1045 if isinstance(bookmark, list) else safe_bytes(bookmark)
1046
1046
1047 if branch:
1047 if branch:
1048 opts['branch'] = [safe_bytes(x) for x in branch] \
1048 opts['branch'] = [safe_bytes(x) for x in branch] \
1049 if isinstance(branch, list) else safe_bytes(branch)
1049 if isinstance(branch, list) else safe_bytes(branch)
1050
1050
1051 if revision:
1051 if revision:
1052 opts['rev'] = [safe_bytes(x) for x in revision] \
1052 opts['rev'] = [safe_bytes(x) for x in revision] \
1053 if isinstance(revision, list) else safe_bytes(revision)
1053 if isinstance(revision, list) else safe_bytes(revision)
1054
1054
1055 commands.pull(baseui, repo, source, **opts)
1055 commands.pull(baseui, repo, source, **opts)
1056
1056
1057 @reraise_safe_exceptions
1057 @reraise_safe_exceptions
1058 def push(self, wire, revisions, dest_path, hooks: bool = True, push_branches: bool = False):
1058 def push(self, wire, revisions, dest_path, hooks: bool = True, push_branches: bool = False):
1059 repo = self._factory.repo(wire)
1059 repo = self._factory.repo(wire)
1060 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1060 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1061
1061
1062 revisions = [safe_bytes(x) for x in revisions] \
1062 revisions = [safe_bytes(x) for x in revisions] \
1063 if isinstance(revisions, list) else safe_bytes(revisions)
1063 if isinstance(revisions, list) else safe_bytes(revisions)
1064
1064
1065 commands.push(baseui, repo, safe_bytes(dest_path),
1065 commands.push(baseui, repo, safe_bytes(dest_path),
1066 rev=revisions,
1066 rev=revisions,
1067 new_branch=push_branches)
1067 new_branch=push_branches)
1068
1068
1069 @reraise_safe_exceptions
1069 @reraise_safe_exceptions
1070 def strip(self, wire, revision, update, backup):
1070 def strip(self, wire, revision, update, backup):
1071 repo = self._factory.repo(wire)
1071 repo = self._factory.repo(wire)
1072 ctx = self._get_ctx(repo, revision)
1072 ctx = self._get_ctx(repo, revision)
1073 hgext_strip.strip(
1073 hgext_strip.strip(
1074 repo.baseui, repo, ctx.node(), update=update, backup=backup)
1074 repo.baseui, repo, ctx.node(), update=update, backup=backup)
1075
1075
1076 @reraise_safe_exceptions
1076 @reraise_safe_exceptions
1077 def get_unresolved_files(self, wire):
1077 def get_unresolved_files(self, wire):
1078 repo = self._factory.repo(wire)
1078 repo = self._factory.repo(wire)
1079
1079
1080 log.debug('Calculating unresolved files for repo: %s', repo)
1080 log.debug('Calculating unresolved files for repo: %s', repo)
1081 output = io.BytesIO()
1081 output = io.BytesIO()
1082
1082
1083 def write(data, **unused_kwargs):
1083 def write(data, **unused_kwargs):
1084 output.write(data)
1084 output.write(data)
1085
1085
1086 baseui = self._factory._create_config(wire['config'])
1086 baseui = self._factory._create_config(wire['config'])
1087 baseui.write = write
1087 baseui.write = write
1088
1088
1089 commands.resolve(baseui, repo, list=True)
1089 commands.resolve(baseui, repo, list=True)
1090 unresolved = output.getvalue().splitlines(0)
1090 unresolved = output.getvalue().splitlines(0)
1091 return unresolved
1091 return unresolved
1092
1092
1093 @reraise_safe_exceptions
1093 @reraise_safe_exceptions
1094 def merge(self, wire, revision):
1094 def merge(self, wire, revision):
1095 repo = self._factory.repo(wire)
1095 repo = self._factory.repo(wire)
1096 baseui = self._factory._create_config(wire['config'])
1096 baseui = self._factory._create_config(wire['config'])
1097 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1097 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1098
1098
1099 # In case of sub repositories are used mercurial prompts the user in
1099 # In case of sub repositories are used mercurial prompts the user in
1100 # case of merge conflicts or different sub repository sources. By
1100 # case of merge conflicts or different sub repository sources. By
1101 # setting the interactive flag to `False` mercurial doesn't prompt the
1101 # setting the interactive flag to `False` mercurial doesn't prompt the
1102 # used but instead uses a default value.
1102 # used but instead uses a default value.
1103 repo.ui.setconfig(b'ui', b'interactive', False)
1103 repo.ui.setconfig(b'ui', b'interactive', False)
1104 commands.merge(baseui, repo, rev=safe_bytes(revision))
1104 commands.merge(baseui, repo, rev=safe_bytes(revision))
1105
1105
1106 @reraise_safe_exceptions
1106 @reraise_safe_exceptions
1107 def merge_state(self, wire):
1107 def merge_state(self, wire):
1108 repo = self._factory.repo(wire)
1108 repo = self._factory.repo(wire)
1109 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1109 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1110
1110
1111 # In case of sub repositories are used mercurial prompts the user in
1111 # In case of sub repositories are used mercurial prompts the user in
1112 # case of merge conflicts or different sub repository sources. By
1112 # case of merge conflicts or different sub repository sources. By
1113 # setting the interactive flag to `False` mercurial doesn't prompt the
1113 # setting the interactive flag to `False` mercurial doesn't prompt the
1114 # used but instead uses a default value.
1114 # used but instead uses a default value.
1115 repo.ui.setconfig(b'ui', b'interactive', False)
1115 repo.ui.setconfig(b'ui', b'interactive', False)
1116 ms = hg_merge.mergestate(repo)
1116 ms = hg_merge.mergestate(repo)
1117 return [x for x in ms.unresolved()]
1117 return [x for x in ms.unresolved()]
1118
1118
1119 @reraise_safe_exceptions
1119 @reraise_safe_exceptions
1120 def commit(self, wire, message, username, close_branch=False):
1120 def commit(self, wire, message, username, close_branch=False):
1121 repo = self._factory.repo(wire)
1121 repo = self._factory.repo(wire)
1122 baseui = self._factory._create_config(wire['config'])
1122 baseui = self._factory._create_config(wire['config'])
1123 repo.ui.setconfig(b'ui', b'username', safe_bytes(username))
1123 repo.ui.setconfig(b'ui', b'username', safe_bytes(username))
1124 commands.commit(baseui, repo, message=safe_bytes(message), close_branch=close_branch)
1124 commands.commit(baseui, repo, message=safe_bytes(message), close_branch=close_branch)
1125
1125
1126 @reraise_safe_exceptions
1126 @reraise_safe_exceptions
1127 def rebase(self, wire, source='', dest='', abort=False):
1127 def rebase(self, wire, source='', dest='', abort=False):
1128
1128
1129 repo = self._factory.repo(wire)
1129 repo = self._factory.repo(wire)
1130 baseui = self._factory._create_config(wire['config'])
1130 baseui = self._factory._create_config(wire['config'])
1131 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1131 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1132 # In case of sub repositories are used mercurial prompts the user in
1132 # In case of sub repositories are used mercurial prompts the user in
1133 # case of merge conflicts or different sub repository sources. By
1133 # case of merge conflicts or different sub repository sources. By
1134 # setting the interactive flag to `False` mercurial doesn't prompt the
1134 # setting the interactive flag to `False` mercurial doesn't prompt the
1135 # used but instead uses a default value.
1135 # used but instead uses a default value.
1136 repo.ui.setconfig(b'ui', b'interactive', False)
1136 repo.ui.setconfig(b'ui', b'interactive', False)
1137
1137
1138 rebase_kws = dict(
1138 rebase_kws = dict(
1139 keep=not abort,
1139 keep=not abort,
1140 abort=abort
1140 abort=abort
1141 )
1141 )
1142
1142
1143 if source:
1143 if source:
1144 source = repo[source]
1144 source = repo[source]
1145 rebase_kws['base'] = [source.hex()]
1145 rebase_kws['base'] = [source.hex()]
1146 if dest:
1146 if dest:
1147 dest = repo[dest]
1147 dest = repo[dest]
1148 rebase_kws['dest'] = dest.hex()
1148 rebase_kws['dest'] = dest.hex()
1149
1149
1150 rebase.rebase(baseui, repo, **rebase_kws)
1150 rebase.rebase(baseui, repo, **rebase_kws)
1151
1151
1152 @reraise_safe_exceptions
1152 @reraise_safe_exceptions
1153 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1153 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1154 repo = self._factory.repo(wire)
1154 repo = self._factory.repo(wire)
1155 ctx = self._get_ctx(repo, revision)
1155 ctx = self._get_ctx(repo, revision)
1156 node = ctx.node()
1156 node = ctx.node()
1157
1157
1158 date = (tag_time, tag_timezone)
1158 date = (tag_time, tag_timezone)
1159 try:
1159 try:
1160 hg_tag.tag(repo, safe_bytes(name), node, safe_bytes(message), local, safe_bytes(user), date)
1160 hg_tag.tag(repo, safe_bytes(name), node, safe_bytes(message), local, safe_bytes(user), date)
1161 except Abort as e:
1161 except Abort as e:
1162 log.exception("Tag operation aborted")
1162 log.exception("Tag operation aborted")
1163 # Exception can contain unicode which we convert
1163 # Exception can contain unicode which we convert
1164 raise exceptions.AbortException(e)(repr(e))
1164 raise exceptions.AbortException(e)(repr(e))
1165
1165
1166 @reraise_safe_exceptions
1166 @reraise_safe_exceptions
1167 def bookmark(self, wire, bookmark, revision=''):
1167 def bookmark(self, wire, bookmark, revision=''):
1168 repo = self._factory.repo(wire)
1168 repo = self._factory.repo(wire)
1169 baseui = self._factory._create_config(wire['config'])
1169 baseui = self._factory._create_config(wire['config'])
1170 revision = revision or ''
1170 revision = revision or ''
1171 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1171 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1172
1172
1173 @reraise_safe_exceptions
1173 @reraise_safe_exceptions
1174 def install_hooks(self, wire, force=False):
1174 def install_hooks(self, wire, force=False):
1175 # we don't need any special hooks for Mercurial
1175 # we don't need any special hooks for Mercurial
1176 pass
1176 pass
1177
1177
1178 @reraise_safe_exceptions
1178 @reraise_safe_exceptions
1179 def get_hooks_info(self, wire):
1179 def get_hooks_info(self, wire):
1180 return {
1180 return {
1181 'pre_version': vcsserver.__version__,
1181 'pre_version': vcsserver.get_version(),
1182 'post_version': vcsserver.__version__,
1182 'post_version': vcsserver.get_version(),
1183 }
1183 }
1184
1184
1185 @reraise_safe_exceptions
1185 @reraise_safe_exceptions
1186 def set_head_ref(self, wire, head_name):
1186 def set_head_ref(self, wire, head_name):
1187 pass
1187 pass
1188
1188
1189 @reraise_safe_exceptions
1189 @reraise_safe_exceptions
1190 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1190 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1191 archive_dir_name, commit_id, cache_config):
1191 archive_dir_name, commit_id, cache_config):
1192
1192
1193 def file_walker(_commit_id, path):
1193 def file_walker(_commit_id, path):
1194 repo = self._factory.repo(wire)
1194 repo = self._factory.repo(wire)
1195 ctx = repo[_commit_id]
1195 ctx = repo[_commit_id]
1196 is_root = path in ['', '/']
1196 is_root = path in ['', '/']
1197 if is_root:
1197 if is_root:
1198 matcher = alwaysmatcher(badfn=None)
1198 matcher = alwaysmatcher(badfn=None)
1199 else:
1199 else:
1200 matcher = patternmatcher('', [(b'glob', safe_bytes(path)+b'/**', b'')], badfn=None)
1200 matcher = patternmatcher('', [(b'glob', safe_bytes(path)+b'/**', b'')], badfn=None)
1201 file_iter = ctx.manifest().walk(matcher)
1201 file_iter = ctx.manifest().walk(matcher)
1202
1202
1203 for fn in file_iter:
1203 for fn in file_iter:
1204 file_path = fn
1204 file_path = fn
1205 flags = ctx.flags(fn)
1205 flags = ctx.flags(fn)
1206 mode = b'x' in flags and 0o755 or 0o644
1206 mode = b'x' in flags and 0o755 or 0o644
1207 is_link = b'l' in flags
1207 is_link = b'l' in flags
1208
1208
1209 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1209 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1210
1210
1211 return store_archive_in_cache(
1211 return store_archive_in_cache(
1212 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1212 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1213
1213
@@ -1,289 +1,289 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 os
18 import os
19 import sys
19 import sys
20 import stat
20 import stat
21 import pytest
21 import pytest
22 import vcsserver
22 import vcsserver
23 import tempfile
23 import tempfile
24 from vcsserver import hook_utils
24 from vcsserver import hook_utils
25 from vcsserver.hook_utils import set_permissions_if_needed, HOOKS_DIR_MODE, HOOKS_FILE_MODE
25 from vcsserver.hook_utils import set_permissions_if_needed, HOOKS_DIR_MODE, HOOKS_FILE_MODE
26 from vcsserver.tests.fixture import no_newline_id_generator
26 from vcsserver.tests.fixture import no_newline_id_generator
27 from vcsserver.str_utils import safe_bytes
27 from vcsserver.str_utils import safe_bytes
28 from vcsserver.utils import AttributeDict
28 from vcsserver.utils import AttributeDict
29
29
30
30
31 class TestCheckRhodecodeHook:
31 class TestCheckRhodecodeHook:
32
32
33 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
33 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
34 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
34 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
35 with open(hook, 'wb') as f:
35 with open(hook, 'wb') as f:
36 f.write(b'dummy test')
36 f.write(b'dummy test')
37 result = hook_utils.check_rhodecode_hook(hook)
37 result = hook_utils.check_rhodecode_hook(hook)
38 assert result is False
38 assert result is False
39
39
40 def test_returns_true_when_no_hook_file_found(self, tmpdir):
40 def test_returns_true_when_no_hook_file_found(self, tmpdir):
41 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
41 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
42 result = hook_utils.check_rhodecode_hook(hook)
42 result = hook_utils.check_rhodecode_hook(hook)
43 assert result
43 assert result
44
44
45 @pytest.mark.parametrize("file_content, expected_result", [
45 @pytest.mark.parametrize("file_content, expected_result", [
46 ("RC_HOOK_VER = '3.3.3'\n", True),
46 ("RC_HOOK_VER = '3.3.3'\n", True),
47 ("RC_HOOK = '3.3.3'\n", False),
47 ("RC_HOOK = '3.3.3'\n", False),
48 ], ids=no_newline_id_generator)
48 ], ids=no_newline_id_generator)
49 def test_signatures(self, file_content, expected_result, tmpdir):
49 def test_signatures(self, file_content, expected_result, tmpdir):
50 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
50 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
51 with open(hook, 'wb') as f:
51 with open(hook, 'wb') as f:
52 f.write(safe_bytes(file_content))
52 f.write(safe_bytes(file_content))
53
53
54 result = hook_utils.check_rhodecode_hook(hook)
54 result = hook_utils.check_rhodecode_hook(hook)
55
55
56 assert result is expected_result
56 assert result is expected_result
57
57
58
58
59 class BaseInstallHooks:
59 class BaseInstallHooks:
60 HOOK_FILES = ()
60 HOOK_FILES = ()
61
61
62 def _check_hook_file_dir_mode(self, file_path):
62 def _check_hook_file_dir_mode(self, file_path):
63 dir_path = os.path.dirname(file_path)
63 dir_path = os.path.dirname(file_path)
64 assert os.path.exists(dir_path), f'dir {file_path} missing'
64 assert os.path.exists(dir_path), f'dir {file_path} missing'
65 stat_info = os.stat(dir_path)
65 stat_info = os.stat(dir_path)
66
66
67 file_mode = stat.S_IMODE(stat_info.st_mode)
67 file_mode = stat.S_IMODE(stat_info.st_mode)
68 expected_mode = int(HOOKS_DIR_MODE)
68 expected_mode = int(HOOKS_DIR_MODE)
69 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {dir_path}'
69 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {dir_path}'
70
70
71 def _check_hook_file_mode(self, file_path):
71 def _check_hook_file_mode(self, file_path):
72 assert os.path.exists(file_path), f'path {file_path} missing'
72 assert os.path.exists(file_path), f'path {file_path} missing'
73 stat_info = os.stat(file_path)
73 stat_info = os.stat(file_path)
74
74
75 file_mode = stat.S_IMODE(stat_info.st_mode)
75 file_mode = stat.S_IMODE(stat_info.st_mode)
76 expected_mode = int(HOOKS_FILE_MODE)
76 expected_mode = int(HOOKS_FILE_MODE)
77 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {file_path}'
77 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {file_path}'
78
78
79 def _check_hook_file_content(self, file_path, executable):
79 def _check_hook_file_content(self, file_path, executable):
80 executable = executable or sys.executable
80 executable = executable or sys.executable
81 with open(file_path, 'rt') as hook_file:
81 with open(file_path, 'rt') as hook_file:
82 content = hook_file.read()
82 content = hook_file.read()
83
83
84 expected_env = '#!{}'.format(executable)
84 expected_env = '#!{}'.format(executable)
85 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(vcsserver.__version__)
85 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(vcsserver.get_version())
86 assert content.strip().startswith(expected_env)
86 assert content.strip().startswith(expected_env)
87 assert expected_rc_version in content
87 assert expected_rc_version in content
88
88
89 def _create_fake_hook(self, file_path, content):
89 def _create_fake_hook(self, file_path, content):
90 with open(file_path, 'w') as hook_file:
90 with open(file_path, 'w') as hook_file:
91 hook_file.write(content)
91 hook_file.write(content)
92
92
93 def create_dummy_repo(self, repo_type):
93 def create_dummy_repo(self, repo_type):
94 tmpdir = tempfile.mkdtemp()
94 tmpdir = tempfile.mkdtemp()
95 repo = AttributeDict()
95 repo = AttributeDict()
96 if repo_type == 'git':
96 if repo_type == 'git':
97 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
97 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
98 os.makedirs(repo.path)
98 os.makedirs(repo.path)
99 os.makedirs(os.path.join(repo.path, 'hooks'))
99 os.makedirs(os.path.join(repo.path, 'hooks'))
100 repo.bare = True
100 repo.bare = True
101
101
102 elif repo_type == 'svn':
102 elif repo_type == 'svn':
103 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
103 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
104 os.makedirs(repo.path)
104 os.makedirs(repo.path)
105 os.makedirs(os.path.join(repo.path, 'hooks'))
105 os.makedirs(os.path.join(repo.path, 'hooks'))
106
106
107 return repo
107 return repo
108
108
109 def check_hooks(self, repo_path, repo_bare=True):
109 def check_hooks(self, repo_path, repo_bare=True):
110 for file_name in self.HOOK_FILES:
110 for file_name in self.HOOK_FILES:
111 if repo_bare:
111 if repo_bare:
112 file_path = os.path.join(repo_path, 'hooks', file_name)
112 file_path = os.path.join(repo_path, 'hooks', file_name)
113 else:
113 else:
114 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
114 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
115
115
116 self._check_hook_file_dir_mode(file_path)
116 self._check_hook_file_dir_mode(file_path)
117 self._check_hook_file_mode(file_path)
117 self._check_hook_file_mode(file_path)
118 self._check_hook_file_content(file_path, sys.executable)
118 self._check_hook_file_content(file_path, sys.executable)
119
119
120
120
121 class TestInstallGitHooks(BaseInstallHooks):
121 class TestInstallGitHooks(BaseInstallHooks):
122 HOOK_FILES = ('pre-receive', 'post-receive')
122 HOOK_FILES = ('pre-receive', 'post-receive')
123
123
124 def test_hooks_are_installed(self):
124 def test_hooks_are_installed(self):
125 repo = self.create_dummy_repo('git')
125 repo = self.create_dummy_repo('git')
126 result = hook_utils.install_git_hooks(repo.path, repo.bare)
126 result = hook_utils.install_git_hooks(repo.path, repo.bare)
127 assert result
127 assert result
128 self.check_hooks(repo.path, repo.bare)
128 self.check_hooks(repo.path, repo.bare)
129
129
130 def test_hooks_are_replaced(self):
130 def test_hooks_are_replaced(self):
131 repo = self.create_dummy_repo('git')
131 repo = self.create_dummy_repo('git')
132 hooks_path = os.path.join(repo.path, 'hooks')
132 hooks_path = os.path.join(repo.path, 'hooks')
133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 self._create_fake_hook(
134 self._create_fake_hook(
135 file_path, content="RC_HOOK_VER = 'abcde'\n")
135 file_path, content="RC_HOOK_VER = 'abcde'\n")
136
136
137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 assert result
138 assert result
139 self.check_hooks(repo.path, repo.bare)
139 self.check_hooks(repo.path, repo.bare)
140
140
141 def test_non_rc_hooks_are_not_replaced(self):
141 def test_non_rc_hooks_are_not_replaced(self):
142 repo = self.create_dummy_repo('git')
142 repo = self.create_dummy_repo('git')
143 hooks_path = os.path.join(repo.path, 'hooks')
143 hooks_path = os.path.join(repo.path, 'hooks')
144 non_rc_content = 'echo "non rc hook"\n'
144 non_rc_content = 'echo "non rc hook"\n'
145 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
145 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
146 self._create_fake_hook(
146 self._create_fake_hook(
147 file_path, content=non_rc_content)
147 file_path, content=non_rc_content)
148
148
149 result = hook_utils.install_git_hooks(repo.path, repo.bare)
149 result = hook_utils.install_git_hooks(repo.path, repo.bare)
150 assert result
150 assert result
151
151
152 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
152 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
153 with open(file_path, 'rt') as hook_file:
153 with open(file_path, 'rt') as hook_file:
154 content = hook_file.read()
154 content = hook_file.read()
155 assert content == non_rc_content
155 assert content == non_rc_content
156
156
157 def test_non_rc_hooks_are_replaced_with_force_flag(self):
157 def test_non_rc_hooks_are_replaced_with_force_flag(self):
158 repo = self.create_dummy_repo('git')
158 repo = self.create_dummy_repo('git')
159 hooks_path = os.path.join(repo.path, 'hooks')
159 hooks_path = os.path.join(repo.path, 'hooks')
160 non_rc_content = 'echo "non rc hook"\n'
160 non_rc_content = 'echo "non rc hook"\n'
161 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
161 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
162 self._create_fake_hook(
162 self._create_fake_hook(
163 file_path, content=non_rc_content)
163 file_path, content=non_rc_content)
164
164
165 result = hook_utils.install_git_hooks(
165 result = hook_utils.install_git_hooks(
166 repo.path, repo.bare, force_create=True)
166 repo.path, repo.bare, force_create=True)
167 assert result
167 assert result
168 self.check_hooks(repo.path, repo.bare)
168 self.check_hooks(repo.path, repo.bare)
169
169
170
170
171 class TestInstallSvnHooks(BaseInstallHooks):
171 class TestInstallSvnHooks(BaseInstallHooks):
172 HOOK_FILES = ('pre-commit', 'post-commit')
172 HOOK_FILES = ('pre-commit', 'post-commit')
173
173
174 def test_hooks_are_installed(self):
174 def test_hooks_are_installed(self):
175 repo = self.create_dummy_repo('svn')
175 repo = self.create_dummy_repo('svn')
176 result = hook_utils.install_svn_hooks(repo.path)
176 result = hook_utils.install_svn_hooks(repo.path)
177 assert result
177 assert result
178 self.check_hooks(repo.path)
178 self.check_hooks(repo.path)
179
179
180 def test_hooks_are_replaced(self):
180 def test_hooks_are_replaced(self):
181 repo = self.create_dummy_repo('svn')
181 repo = self.create_dummy_repo('svn')
182 hooks_path = os.path.join(repo.path, 'hooks')
182 hooks_path = os.path.join(repo.path, 'hooks')
183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 self._create_fake_hook(
184 self._create_fake_hook(
185 file_path, content="RC_HOOK_VER = 'abcde'\n")
185 file_path, content="RC_HOOK_VER = 'abcde'\n")
186
186
187 result = hook_utils.install_svn_hooks(repo.path)
187 result = hook_utils.install_svn_hooks(repo.path)
188 assert result
188 assert result
189 self.check_hooks(repo.path)
189 self.check_hooks(repo.path)
190
190
191 def test_non_rc_hooks_are_not_replaced(self):
191 def test_non_rc_hooks_are_not_replaced(self):
192 repo = self.create_dummy_repo('svn')
192 repo = self.create_dummy_repo('svn')
193 hooks_path = os.path.join(repo.path, 'hooks')
193 hooks_path = os.path.join(repo.path, 'hooks')
194 non_rc_content = 'echo "non rc hook"\n'
194 non_rc_content = 'echo "non rc hook"\n'
195 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
195 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
196 self._create_fake_hook(
196 self._create_fake_hook(
197 file_path, content=non_rc_content)
197 file_path, content=non_rc_content)
198
198
199 result = hook_utils.install_svn_hooks(repo.path)
199 result = hook_utils.install_svn_hooks(repo.path)
200 assert result
200 assert result
201
201
202 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
202 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
203 with open(file_path, 'rt') as hook_file:
203 with open(file_path, 'rt') as hook_file:
204 content = hook_file.read()
204 content = hook_file.read()
205 assert content == non_rc_content
205 assert content == non_rc_content
206
206
207 def test_non_rc_hooks_are_replaced_with_force_flag(self):
207 def test_non_rc_hooks_are_replaced_with_force_flag(self):
208 repo = self.create_dummy_repo('svn')
208 repo = self.create_dummy_repo('svn')
209 hooks_path = os.path.join(repo.path, 'hooks')
209 hooks_path = os.path.join(repo.path, 'hooks')
210 non_rc_content = 'echo "non rc hook"\n'
210 non_rc_content = 'echo "non rc hook"\n'
211 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
211 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
212 self._create_fake_hook(
212 self._create_fake_hook(
213 file_path, content=non_rc_content)
213 file_path, content=non_rc_content)
214
214
215 result = hook_utils.install_svn_hooks(
215 result = hook_utils.install_svn_hooks(
216 repo.path, force_create=True)
216 repo.path, force_create=True)
217 assert result
217 assert result
218 self.check_hooks(repo.path, )
218 self.check_hooks(repo.path, )
219
219
220
220
221 def create_test_file(filename):
221 def create_test_file(filename):
222 """Utility function to create a test file."""
222 """Utility function to create a test file."""
223 with open(filename, 'w') as f:
223 with open(filename, 'w') as f:
224 f.write("Test file")
224 f.write("Test file")
225
225
226
226
227 def remove_test_file(filename):
227 def remove_test_file(filename):
228 """Utility function to remove a test file."""
228 """Utility function to remove a test file."""
229 if os.path.exists(filename):
229 if os.path.exists(filename):
230 os.remove(filename)
230 os.remove(filename)
231
231
232
232
233 @pytest.fixture
233 @pytest.fixture
234 def test_file():
234 def test_file():
235 filename = 'test_file.txt'
235 filename = 'test_file.txt'
236 create_test_file(filename)
236 create_test_file(filename)
237 yield filename
237 yield filename
238 remove_test_file(filename)
238 remove_test_file(filename)
239
239
240
240
241 def test_increase_permissions(test_file):
241 def test_increase_permissions(test_file):
242 # Set initial lower permissions
242 # Set initial lower permissions
243 initial_perms = 0o644
243 initial_perms = 0o644
244 os.chmod(test_file, initial_perms)
244 os.chmod(test_file, initial_perms)
245
245
246 # Set higher permissions
246 # Set higher permissions
247 new_perms = 0o666
247 new_perms = 0o666
248 set_permissions_if_needed(test_file, new_perms)
248 set_permissions_if_needed(test_file, new_perms)
249
249
250 # Check if permissions were updated
250 # Check if permissions were updated
251 assert (os.stat(test_file).st_mode & 0o777) == new_perms
251 assert (os.stat(test_file).st_mode & 0o777) == new_perms
252
252
253
253
254 def test_no_permission_change_needed(test_file):
254 def test_no_permission_change_needed(test_file):
255 # Set initial permissions
255 # Set initial permissions
256 initial_perms = 0o666
256 initial_perms = 0o666
257 os.chmod(test_file, initial_perms)
257 os.chmod(test_file, initial_perms)
258
258
259 # Attempt to set the same permissions
259 # Attempt to set the same permissions
260 set_permissions_if_needed(test_file, initial_perms)
260 set_permissions_if_needed(test_file, initial_perms)
261
261
262 # Check if permissions were unchanged
262 # Check if permissions were unchanged
263 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
263 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
264
264
265
265
266 def test_no_permission_reduction(test_file):
266 def test_no_permission_reduction(test_file):
267 # Set initial higher permissions
267 # Set initial higher permissions
268 initial_perms = 0o666
268 initial_perms = 0o666
269 os.chmod(test_file, initial_perms)
269 os.chmod(test_file, initial_perms)
270
270
271 # Attempt to set lower permissions
271 # Attempt to set lower permissions
272 lower_perms = 0o644
272 lower_perms = 0o644
273 set_permissions_if_needed(test_file, lower_perms)
273 set_permissions_if_needed(test_file, lower_perms)
274
274
275 # Check if permissions were not reduced
275 # Check if permissions were not reduced
276 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
276 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
277
277
278
278
279 def test_no_permission_reduction_when_on_777(test_file):
279 def test_no_permission_reduction_when_on_777(test_file):
280 # Set initial higher permissions
280 # Set initial higher permissions
281 initial_perms = 0o777
281 initial_perms = 0o777
282 os.chmod(test_file, initial_perms)
282 os.chmod(test_file, initial_perms)
283
283
284 # Attempt to set lower permissions
284 # Attempt to set lower permissions
285 lower_perms = 0o755
285 lower_perms = 0o755
286 set_permissions_if_needed(test_file, lower_perms)
286 set_permissions_if_needed(test_file, lower_perms)
287
287
288 # Check if permissions were not reduced
288 # Check if permissions were not reduced
289 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
289 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
@@ -1,123 +1,123 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 import base64
17 import base64
18 import logging
18 import logging
19 import time
19 import time
20
20
21 import msgpack
21 import msgpack
22
22
23 import vcsserver
23 import vcsserver
24 from vcsserver.str_utils import safe_str
24 from vcsserver.str_utils import safe_str
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 def get_access_path(environ):
29 def get_access_path(environ):
30 path = environ.get('PATH_INFO')
30 path = environ.get('PATH_INFO')
31 return path
31 return path
32
32
33
33
34 def get_user_agent(environ):
34 def get_user_agent(environ):
35 return environ.get('HTTP_USER_AGENT')
35 return environ.get('HTTP_USER_AGENT')
36
36
37
37
38 def get_call_context(request) -> dict:
38 def get_call_context(request) -> dict:
39 cc = {}
39 cc = {}
40 registry = request.registry
40 registry = request.registry
41 if hasattr(registry, 'vcs_call_context'):
41 if hasattr(registry, 'vcs_call_context'):
42 cc.update({
42 cc.update({
43 'X-RC-Method': registry.vcs_call_context.get('method'),
43 'X-RC-Method': registry.vcs_call_context.get('method'),
44 'X-RC-Repo-Name': registry.vcs_call_context.get('repo_name')
44 'X-RC-Repo-Name': registry.vcs_call_context.get('repo_name')
45 })
45 })
46
46
47 return cc
47 return cc
48
48
49
49
50 def get_headers_call_context(environ, strict=True):
50 def get_headers_call_context(environ, strict=True):
51 if 'HTTP_X_RC_VCS_STREAM_CALL_CONTEXT' in environ:
51 if 'HTTP_X_RC_VCS_STREAM_CALL_CONTEXT' in environ:
52 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
52 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
53 return msgpack.unpackb(packed_cc)
53 return msgpack.unpackb(packed_cc)
54 elif strict:
54 elif strict:
55 raise ValueError('Expected header HTTP_X_RC_VCS_STREAM_CALL_CONTEXT not found')
55 raise ValueError('Expected header HTTP_X_RC_VCS_STREAM_CALL_CONTEXT not found')
56
56
57
57
58 class RequestWrapperTween:
58 class RequestWrapperTween:
59 def __init__(self, handler, registry):
59 def __init__(self, handler, registry):
60 self.handler = handler
60 self.handler = handler
61 self.registry = registry
61 self.registry = registry
62
62
63 # one-time configuration code goes here
63 # one-time configuration code goes here
64
64
65 def __call__(self, request):
65 def __call__(self, request):
66 start = time.time()
66 start = time.time()
67 log.debug('Starting request time measurement')
67 log.debug('Starting request time measurement')
68 response = None
68 response = None
69
69
70 try:
70 try:
71 response = self.handler(request)
71 response = self.handler(request)
72 finally:
72 finally:
73 ua = get_user_agent(request.environ)
73 ua = get_user_agent(request.environ)
74 call_context = get_call_context(request)
74 call_context = get_call_context(request)
75 vcs_method = call_context.get('X-RC-Method', '_NO_VCS_METHOD')
75 vcs_method = call_context.get('X-RC-Method', '_NO_VCS_METHOD')
76 repo_name = call_context.get('X-RC-Repo-Name', '')
76 repo_name = call_context.get('X-RC-Repo-Name', '')
77
77
78 count = request.request_count()
78 count = request.request_count()
79 _ver_ = vcsserver.__version__
79 _ver_ = vcsserver.get_version()
80 _path = safe_str(get_access_path(request.environ))
80 _path = safe_str(get_access_path(request.environ))
81
81
82 ip = '127.0.0.1'
82 ip = '127.0.0.1'
83 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
83 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
84 resp_code = getattr(response, 'status_code', 'UNDEFINED')
84 resp_code = getattr(response, 'status_code', 'UNDEFINED')
85
85
86 _view_path = f"{repo_name}@{_path}/{vcs_method}"
86 _view_path = f"{repo_name}@{_path}/{vcs_method}"
87
87
88 total = time.time() - start
88 total = time.time() - start
89
89
90 log.info(
90 log.info(
91 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
91 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
92 count, ip, request.environ.get('REQUEST_METHOD'),
92 count, ip, request.environ.get('REQUEST_METHOD'),
93 _view_path, total, ua, _ver_,
93 _view_path, total, ua, _ver_,
94 extra={"time": total, "ver": _ver_, "code": resp_code,
94 extra={"time": total, "ver": _ver_, "code": resp_code,
95 "path": _path, "view_name": match_route, "user_agent": ua,
95 "path": _path, "view_name": match_route, "user_agent": ua,
96 "vcs_method": vcs_method, "repo_name": repo_name}
96 "vcs_method": vcs_method, "repo_name": repo_name}
97 )
97 )
98
98
99 statsd = request.registry.statsd
99 statsd = request.registry.statsd
100 if statsd:
100 if statsd:
101 match_route = request.matched_route.name if request.matched_route else _path
101 match_route = request.matched_route.name if request.matched_route else _path
102 elapsed_time_ms = round(1000.0 * total) # use ms only
102 elapsed_time_ms = round(1000.0 * total) # use ms only
103 statsd.timing(
103 statsd.timing(
104 "vcsserver_req_timing.histogram", elapsed_time_ms,
104 "vcsserver_req_timing.histogram", elapsed_time_ms,
105 tags=[
105 tags=[
106 f"view_name:{match_route}",
106 f"view_name:{match_route}",
107 f"code:{resp_code}"
107 f"code:{resp_code}"
108 ],
108 ],
109 use_decimals=False
109 use_decimals=False
110 )
110 )
111 statsd.incr(
111 statsd.incr(
112 "vcsserver_req_total", tags=[
112 "vcsserver_req_total", tags=[
113 f"view_name:{match_route}",
113 f"view_name:{match_route}",
114 f"code:{resp_code}"
114 f"code:{resp_code}"
115 ])
115 ])
116
116
117 return response
117 return response
118
118
119
119
120 def includeme(config):
120 def includeme(config):
121 config.add_tween(
121 config.add_tween(
122 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
122 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
123 )
123 )
General Comments 0
You need to be logged in to leave comments. Login now