##// END OF EJS Templates
fix(svn): svn events fixes
super-admin -
r1261:0f8db01d default
parent child Browse files
Show More
@@ -0,0 +1,111 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
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
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import logging
19 import redis
20
21 from ..lib import rc_cache
22 from ..lib.ext_json import json
23
24
25 log = logging.getLogger(__name__)
26
27 redis_client = None
28
29
30 class RedisTxnClient:
31
32 def __init__(self, url):
33 self.url = url
34 self._create_client(url)
35
36 def _create_client(self, url):
37 connection_pool = redis.ConnectionPool.from_url(url)
38 self.writer_client = redis.StrictRedis(
39 connection_pool=connection_pool
40 )
41 self.reader_client = self.writer_client
42
43 def set(self, key, value):
44 self.writer_client.set(key, value)
45
46 def get(self, key):
47 return self.reader_client.get(key)
48
49 def delete(self, key):
50 self.writer_client.delete(key)
51
52
53 def get_redis_client(url=''):
54
55 global redis_client
56 if redis_client is not None:
57 return redis_client
58 if not url:
59 from vcsserver import CONFIG
60 url = CONFIG['vcs.svn.redis_conn']
61 redis_client = RedisTxnClient(url)
62 return redis_client
63
64
65 def get_txn_id_data_key(repo_path, svn_txn_id):
66 log.debug('svn-txn-id: %s, obtaining data path', svn_txn_id)
67 repo_key = rc_cache.utils.compute_key_from_params(repo_path)
68 final_key = f'{repo_key}.{svn_txn_id}.svn_txn_id'
69 log.debug('computed final key: %s', final_key)
70
71 return final_key
72
73
74 def store_txn_id_data(repo_path, svn_txn_id, data_dict):
75 log.debug('svn-txn-id: %s, storing data', svn_txn_id)
76
77 if not svn_txn_id:
78 log.warning('Cannot store txn_id because it is empty')
79 return
80
81 redis_conn = get_redis_client()
82
83 store_key = get_txn_id_data_key(repo_path, svn_txn_id)
84 store_data = json.dumps(data_dict)
85 redis_conn.set(store_key, store_data)
86
87
88 def get_txn_id_from_store(repo_path, svn_txn_id, rm_on_read=False):
89 """
90 Reads txn_id from store and if present returns the data for callback manager
91 """
92 log.debug('svn-txn-id: %s, retrieving data', svn_txn_id)
93 redis_conn = get_redis_client()
94
95 store_key = get_txn_id_data_key(repo_path, svn_txn_id)
96 data = {}
97 redis_conn.get(store_key)
98 raw_data = 'not-set'
99 try:
100 raw_data = redis_conn.get(store_key)
101 if not raw_data:
102 raise ValueError(f'Failed to get txn_id metadata, from store: {store_key}')
103 data = json.loads(raw_data)
104 except Exception:
105 log.exception('Failed to get txn_id metadata: %s', raw_data)
106
107 if rm_on_read:
108 log.debug('Cleaning up txn_id at %s', store_key)
109 redis_conn.delete(store_key)
110
111 return data
@@ -1,187 +1,191 b''
1 1 #
2 2
3 3 ; #################################
4 4 ; RHODECODE VCSSERVER CONFIGURATION
5 5 ; #################################
6 6
7 7 [server:main]
8 8 ; COMMON HOST/IP CONFIG
9 9 host = 0.0.0.0
10 10 port = 10010
11 11
12 12
13 13 ; ###########################
14 14 ; GUNICORN APPLICATION SERVER
15 15 ; ###########################
16 16
17 17 ; run with gunicorn --paste rhodecode.ini
18 18
19 19 ; Module to use, this setting shouldn't be changed
20 20 use = egg:gunicorn#main
21 21
22 22 [app:main]
23 23 ; The %(here)s variable will be replaced with the absolute path of parent directory
24 24 ; of this file
25 25 ; Each option in the app:main can be override by an environmental variable
26 26 ;
27 27 ;To override an option:
28 28 ;
29 29 ;RC_<KeyName>
30 30 ;Everything should be uppercase, . and - should be replaced by _.
31 31 ;For example, if you have these configuration settings:
32 32 ;rc_cache.repo_object.backend = foo
33 33 ;can be overridden by
34 34 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
35 35
36 36 use = egg:rhodecode-vcsserver
37 37
38 38
39 39 ; #############
40 40 ; DEBUG OPTIONS
41 41 ; #############
42 42
43 43 # During development the we want to have the debug toolbar enabled
44 44 pyramid.includes =
45 45 pyramid_debugtoolbar
46 46
47 47 debugtoolbar.hosts = 0.0.0.0/0
48 48 debugtoolbar.exclude_prefixes =
49 49 /css
50 50 /fonts
51 51 /images
52 52 /js
53 53
54 54 ; #################
55 55 ; END DEBUG OPTIONS
56 56 ; #################
57 57
58 58 ; Pyramid default locales, we need this to be set
59 59 #pyramid.default_locale_name = en
60 60
61 61 ; default locale used by VCS systems
62 62 #locale = en_US.UTF-8
63 63
64 64 ; path to binaries (hg,git,svn) for vcsserver, it should be set by the installer
65 65 ; at installation time, e.g /home/user/.rccontrol/vcsserver-1/profile/bin
66 66 ; or /usr/local/bin/rhodecode_bin/vcs_bin
67 67 core.binary_dir =
68 68
69 ; Redis connection settings for svn integrations logic
70 ; This connection string needs to be the same on ce and vcsserver
71 vcs.svn.redis_conn = redis://redis:6379/0
72
69 73 ; Custom exception store path, defaults to TMPDIR
70 74 ; This is used to store exception from RhodeCode in shared directory
71 75 #exception_tracker.store_path =
72 76
73 77 ; #############
74 78 ; DOGPILE CACHE
75 79 ; #############
76 80
77 81 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
78 82 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
79 83 #cache_dir = %(here)s/data
80 84
81 85 ; ***************************************
82 86 ; `repo_object` cache, default file based
83 87 ; ***************************************
84 88
85 89 ; `repo_object` cache settings for vcs methods for repositories
86 90 #rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
87 91
88 92 ; cache auto-expires after N seconds
89 93 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
90 94 #rc_cache.repo_object.expiration_time = 2592000
91 95
92 96 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
93 97 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache_repo_object.db
94 98
95 99 ; ***********************************************************
96 100 ; `repo_object` cache with redis backend
97 101 ; recommended for larger instance, and for better performance
98 102 ; ***********************************************************
99 103
100 104 ; `repo_object` cache settings for vcs methods for repositories
101 105 #rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack
102 106
103 107 ; cache auto-expires after N seconds
104 108 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
105 109 #rc_cache.repo_object.expiration_time = 2592000
106 110
107 111 ; redis_expiration_time needs to be greater then expiration_time
108 112 #rc_cache.repo_object.arguments.redis_expiration_time = 3592000
109 113
110 114 #rc_cache.repo_object.arguments.host = localhost
111 115 #rc_cache.repo_object.arguments.port = 6379
112 116 #rc_cache.repo_object.arguments.db = 5
113 117 #rc_cache.repo_object.arguments.socket_timeout = 30
114 118 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
115 119 #rc_cache.repo_object.arguments.distributed_lock = true
116 120
117 121 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
118 122 #rc_cache.repo_object.arguments.lock_auto_renewal = true
119 123
120 124 ; Statsd client config, this is used to send metrics to statsd
121 125 ; We recommend setting statsd_exported and scrape them using Promethues
122 126 #statsd.enabled = false
123 127 #statsd.statsd_host = 0.0.0.0
124 128 #statsd.statsd_port = 8125
125 129 #statsd.statsd_prefix =
126 130 #statsd.statsd_ipv6 = false
127 131
128 132 ; configure logging automatically at server startup set to false
129 133 ; to use the below custom logging config.
130 134 ; RC_LOGGING_FORMATTER
131 135 ; RC_LOGGING_LEVEL
132 136 ; env variables can control the settings for logging in case of autoconfigure
133 137
134 138 #logging.autoconfigure = true
135 139
136 140 ; specify your own custom logging config file to configure logging
137 141 #logging.logging_conf_file = /path/to/custom_logging.ini
138 142
139 143 ; #####################
140 144 ; LOGGING CONFIGURATION
141 145 ; #####################
142 146
143 147 [loggers]
144 148 keys = root, vcsserver
145 149
146 150 [handlers]
147 151 keys = console
148 152
149 153 [formatters]
150 154 keys = generic, json
151 155
152 156 ; #######
153 157 ; LOGGERS
154 158 ; #######
155 159 [logger_root]
156 160 level = NOTSET
157 161 handlers = console
158 162
159 163 [logger_vcsserver]
160 164 level = DEBUG
161 165 handlers =
162 166 qualname = vcsserver
163 167 propagate = 1
164 168
165 169 ; ########
166 170 ; HANDLERS
167 171 ; ########
168 172
169 173 [handler_console]
170 174 class = StreamHandler
171 175 args = (sys.stderr, )
172 176 level = DEBUG
173 177 ; To enable JSON formatted logs replace 'generic' with 'json'
174 178 ; This allows sending properly formatted logs to grafana loki or elasticsearch
175 179 formatter = generic
176 180
177 181 ; ##########
178 182 ; FORMATTERS
179 183 ; ##########
180 184
181 185 [formatter_generic]
182 186 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
183 187 datefmt = %Y-%m-%d %H:%M:%S
184 188
185 189 [formatter_json]
186 190 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
187 191 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
@@ -1,167 +1,171 b''
1 1 #
2 2
3 3 ; #################################
4 4 ; RHODECODE VCSSERVER CONFIGURATION
5 5 ; #################################
6 6
7 7 [server:main]
8 8 ; COMMON HOST/IP CONFIG
9 9 host = 0.0.0.0
10 10 port = 10010
11 11
12 12
13 13 ; ###########################
14 14 ; GUNICORN APPLICATION SERVER
15 15 ; ###########################
16 16
17 17 ; run with gunicorn --paste rhodecode.ini
18 18
19 19 ; Module to use, this setting shouldn't be changed
20 20 use = egg:gunicorn#main
21 21
22 22 [app:main]
23 23 ; The %(here)s variable will be replaced with the absolute path of parent directory
24 24 ; of this file
25 25 ; Each option in the app:main can be override by an environmental variable
26 26 ;
27 27 ;To override an option:
28 28 ;
29 29 ;RC_<KeyName>
30 30 ;Everything should be uppercase, . and - should be replaced by _.
31 31 ;For example, if you have these configuration settings:
32 32 ;rc_cache.repo_object.backend = foo
33 33 ;can be overridden by
34 34 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
35 35
36 36 use = egg:rhodecode-vcsserver
37 37
38 38 ; Pyramid default locales, we need this to be set
39 39 #pyramid.default_locale_name = en
40 40
41 41 ; default locale used by VCS systems
42 42 #locale = en_US.UTF-8
43 43
44 44 ; path to binaries (hg,git,svn) for vcsserver, it should be set by the installer
45 45 ; at installation time, e.g /home/user/.rccontrol/vcsserver-1/profile/bin
46 46 ; or /usr/local/bin/rhodecode_bin/vcs_bin
47 47 core.binary_dir =
48 48
49 ; Redis connection settings for svn integrations logic
50 ; This connection string needs to be the same on ce and vcsserver
51 vcs.svn.redis_conn = redis://redis:6379/0
52
49 53 ; Custom exception store path, defaults to TMPDIR
50 54 ; This is used to store exception from RhodeCode in shared directory
51 55 #exception_tracker.store_path =
52 56
53 57 ; #############
54 58 ; DOGPILE CACHE
55 59 ; #############
56 60
57 61 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
58 62 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
59 63 #cache_dir = %(here)s/data
60 64
61 65 ; ***************************************
62 66 ; `repo_object` cache, default file based
63 67 ; ***************************************
64 68
65 69 ; `repo_object` cache settings for vcs methods for repositories
66 70 #rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
67 71
68 72 ; cache auto-expires after N seconds
69 73 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
70 74 #rc_cache.repo_object.expiration_time = 2592000
71 75
72 76 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
73 77 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache_repo_object.db
74 78
75 79 ; ***********************************************************
76 80 ; `repo_object` cache with redis backend
77 81 ; recommended for larger instance, and for better performance
78 82 ; ***********************************************************
79 83
80 84 ; `repo_object` cache settings for vcs methods for repositories
81 85 #rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack
82 86
83 87 ; cache auto-expires after N seconds
84 88 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
85 89 #rc_cache.repo_object.expiration_time = 2592000
86 90
87 91 ; redis_expiration_time needs to be greater then expiration_time
88 92 #rc_cache.repo_object.arguments.redis_expiration_time = 3592000
89 93
90 94 #rc_cache.repo_object.arguments.host = localhost
91 95 #rc_cache.repo_object.arguments.port = 6379
92 96 #rc_cache.repo_object.arguments.db = 5
93 97 #rc_cache.repo_object.arguments.socket_timeout = 30
94 98 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
95 99 #rc_cache.repo_object.arguments.distributed_lock = true
96 100
97 101 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
98 102 #rc_cache.repo_object.arguments.lock_auto_renewal = true
99 103
100 104 ; Statsd client config, this is used to send metrics to statsd
101 105 ; We recommend setting statsd_exported and scrape them using Promethues
102 106 #statsd.enabled = false
103 107 #statsd.statsd_host = 0.0.0.0
104 108 #statsd.statsd_port = 8125
105 109 #statsd.statsd_prefix =
106 110 #statsd.statsd_ipv6 = false
107 111
108 112 ; configure logging automatically at server startup set to false
109 113 ; to use the below custom logging config.
110 114 ; RC_LOGGING_FORMATTER
111 115 ; RC_LOGGING_LEVEL
112 116 ; env variables can control the settings for logging in case of autoconfigure
113 117
114 118 #logging.autoconfigure = true
115 119
116 120 ; specify your own custom logging config file to configure logging
117 121 #logging.logging_conf_file = /path/to/custom_logging.ini
118 122
119 123 ; #####################
120 124 ; LOGGING CONFIGURATION
121 125 ; #####################
122 126
123 127 [loggers]
124 128 keys = root, vcsserver
125 129
126 130 [handlers]
127 131 keys = console
128 132
129 133 [formatters]
130 134 keys = generic, json
131 135
132 136 ; #######
133 137 ; LOGGERS
134 138 ; #######
135 139 [logger_root]
136 140 level = NOTSET
137 141 handlers = console
138 142
139 143 [logger_vcsserver]
140 144 level = INFO
141 145 handlers =
142 146 qualname = vcsserver
143 147 propagate = 1
144 148
145 149 ; ########
146 150 ; HANDLERS
147 151 ; ########
148 152
149 153 [handler_console]
150 154 class = StreamHandler
151 155 args = (sys.stderr, )
152 156 level = INFO
153 157 ; To enable JSON formatted logs replace 'generic' with 'json'
154 158 ; This allows sending properly formatted logs to grafana loki or elasticsearch
155 159 formatter = generic
156 160
157 161 ; ##########
158 162 ; FORMATTERS
159 163 ; ##########
160 164
161 165 [formatter_generic]
162 166 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
163 167 datefmt = %Y-%m-%d %H:%M:%S
164 168
165 169 [formatter_json]
166 170 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
167 171 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
@@ -1,230 +1,238 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18
19 19 import re
20 20 import os
21 21 import sys
22 22 import datetime
23 23 import logging
24 24 import pkg_resources
25 25
26 26 import vcsserver
27 27 import vcsserver.settings
28 28 from vcsserver.lib.str_utils import safe_bytes
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 HOOKS_DIR_MODE = 0o755
33 33 HOOKS_FILE_MODE = 0o755
34 34
35 35
36 36 def set_permissions_if_needed(path_to_check, perms: oct):
37 37 # Get current permissions
38 38 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
39 39
40 40 # Check if current permissions are lower than required
41 41 if current_permissions < int(perms):
42 42 # Change the permissions if they are lower than required
43 43 os.chmod(path_to_check, perms)
44 44
45 45
46 46 def get_git_hooks_path(repo_path, bare):
47 47 hooks_path = os.path.join(repo_path, 'hooks')
48 48 if not bare:
49 49 hooks_path = os.path.join(repo_path, '.git', 'hooks')
50 50
51 51 return hooks_path
52 52
53 53
54 54 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
55 55 """
56 56 Creates a RhodeCode hook inside a git repository
57 57
58 58 :param repo_path: path to repository
59 59 :param bare: defines if repository is considered a bare git repo
60 60 :param executable: binary executable to put in the hooks
61 61 :param force_create: Creates even if the same name hook exists
62 62 """
63 63 executable = executable or sys.executable
64 64 hooks_path = get_git_hooks_path(repo_path, bare)
65 65
66 66 # we always call it to ensure dir exists and it has a proper mode
67 67 if not os.path.exists(hooks_path):
68 68 # If it doesn't exist, create a new directory with the specified mode
69 69 os.makedirs(hooks_path, mode=HOOKS_DIR_MODE, exist_ok=True)
70 70 # If it exists, change the directory's mode to the specified mode
71 71 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
72 72
73 73 tmpl_post = pkg_resources.resource_string(
74 74 'vcsserver', '/'.join(
75 75 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
76 76 tmpl_pre = pkg_resources.resource_string(
77 77 'vcsserver', '/'.join(
78 78 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
79 79
80 80 path = '' # not used for now
81 81 timestamp = datetime.datetime.utcnow().isoformat()
82 82
83 83 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
84 84 log.debug('Installing git hook in repo %s', repo_path)
85 85 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
86 86 _rhodecode_hook = check_rhodecode_hook(_hook_file)
87 87
88 88 if _rhodecode_hook or force_create:
89 89 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
90 env_expand = str([
91 ('RC_INI_FILE', vcsserver.CONFIG['__file__']),
92 ('RC_CORE_BINARY_DIR', vcsserver.settings.BINARY_DIR),
93 ('RC_GIT_EXECUTABLE', vcsserver.settings.GIT_EXECUTABLE()),
94 ('RC_SVN_EXECUTABLE', vcsserver.settings.SVN_EXECUTABLE()),
95 ('RC_SVNLOOK_EXECUTABLE', vcsserver.settings.SVNLOOK_EXECUTABLE()),
96 ])
90 97 try:
91 98 with open(_hook_file, 'wb') as f:
99 template = template.replace(b'_OS_EXPAND_', safe_bytes(env_expand))
92 100 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
93 101 template = template.replace(b'_DATE_', safe_bytes(timestamp))
94 102 template = template.replace(b'_ENV_', safe_bytes(executable))
95 103 template = template.replace(b'_PATH_', safe_bytes(path))
96 104 f.write(template)
97 105 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
98 106 except OSError:
99 107 log.exception('error writing hook file %s', _hook_file)
100 108 else:
101 109 log.debug('skipping writing hook file')
102 110
103 111 return True
104 112
105 113
106 114 def get_svn_hooks_path(repo_path):
107 115 hooks_path = os.path.join(repo_path, 'hooks')
108 116
109 117 return hooks_path
110 118
111 119
112 120 def install_svn_hooks(repo_path, executable=None, force_create=False):
113 121 """
114 122 Creates RhodeCode hooks inside a svn repository
115 123
116 124 :param repo_path: path to repository
117 125 :param executable: binary executable to put in the hooks
118 126 :param force_create: Create even if same name hook exists
119 127 """
120 128 executable = executable or sys.executable
121 129 hooks_path = get_svn_hooks_path(repo_path)
122 130 if not os.path.isdir(hooks_path):
123 131 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
124 132
125 133 tmpl_post = pkg_resources.resource_string(
126 134 'vcsserver', '/'.join(
127 135 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
128 136 tmpl_pre = pkg_resources.resource_string(
129 137 'vcsserver', '/'.join(
130 138 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
131 139
132 140 path = '' # not used for now
133 141 timestamp = datetime.datetime.utcnow().isoformat()
134 142
135 143 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
136 144 log.debug('Installing svn hook in repo %s', repo_path)
137 145 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
138 146 _rhodecode_hook = check_rhodecode_hook(_hook_file)
139 147
140 148 if _rhodecode_hook or force_create:
141 149 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
142 150
143 151 env_expand = str([
152 ('RC_INI_FILE', vcsserver.CONFIG['__file__']),
144 153 ('RC_CORE_BINARY_DIR', vcsserver.settings.BINARY_DIR),
145 154 ('RC_GIT_EXECUTABLE', vcsserver.settings.GIT_EXECUTABLE()),
146 155 ('RC_SVN_EXECUTABLE', vcsserver.settings.SVN_EXECUTABLE()),
147 156 ('RC_SVNLOOK_EXECUTABLE', vcsserver.settings.SVNLOOK_EXECUTABLE()),
148
149 157 ])
150 158 try:
151 159 with open(_hook_file, 'wb') as f:
160 template = template.replace(b'_OS_EXPAND_', safe_bytes(env_expand))
152 161 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
153 162 template = template.replace(b'_DATE_', safe_bytes(timestamp))
154 template = template.replace(b'_OS_EXPAND_', safe_bytes(env_expand))
155 163 template = template.replace(b'_ENV_', safe_bytes(executable))
156 164 template = template.replace(b'_PATH_', safe_bytes(path))
157 165
158 166 f.write(template)
159 167 os.chmod(_hook_file, 0o755)
160 168 except OSError:
161 169 log.exception('error writing hook file %s', _hook_file)
162 170 else:
163 171 log.debug('skipping writing hook file')
164 172
165 173 return True
166 174
167 175
168 176 def get_version_from_hook(hook_path):
169 177 version = b''
170 178 hook_content = read_hook_content(hook_path)
171 179 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
172 180 if matches:
173 181 try:
174 182 version = matches.groups()[0]
175 183 log.debug('got version %s from hooks.', version)
176 184 except Exception:
177 185 log.exception("Exception while reading the hook version.")
178 186 return version.replace(b"'", b"")
179 187
180 188
181 189 def check_rhodecode_hook(hook_path):
182 190 """
183 191 Check if the hook was created by RhodeCode
184 192 """
185 193 if not os.path.exists(hook_path):
186 194 return True
187 195
188 196 log.debug('hook exists, checking if it is from RhodeCode')
189 197
190 198 version = get_version_from_hook(hook_path)
191 199 if version:
192 200 return True
193 201
194 202 return False
195 203
196 204
197 205 def read_hook_content(hook_path) -> bytes:
198 206 content = b''
199 207 if os.path.isfile(hook_path):
200 208 with open(hook_path, 'rb') as f:
201 209 content = f.read()
202 210 return content
203 211
204 212
205 213 def get_git_pre_hook_version(repo_path, bare):
206 214 hooks_path = get_git_hooks_path(repo_path, bare)
207 215 _hook_file = os.path.join(hooks_path, 'pre-receive')
208 216 version = get_version_from_hook(_hook_file)
209 217 return version
210 218
211 219
212 220 def get_git_post_hook_version(repo_path, bare):
213 221 hooks_path = get_git_hooks_path(repo_path, bare)
214 222 _hook_file = os.path.join(hooks_path, 'post-receive')
215 223 version = get_version_from_hook(_hook_file)
216 224 return version
217 225
218 226
219 227 def get_svn_pre_hook_version(repo_path):
220 228 hooks_path = get_svn_hooks_path(repo_path)
221 229 _hook_file = os.path.join(hooks_path, 'pre-commit')
222 230 version = get_version_from_hook(_hook_file)
223 231 return version
224 232
225 233
226 234 def get_svn_post_hook_version(repo_path):
227 235 hooks_path = get_svn_hooks_path(repo_path)
228 236 _hook_file = os.path.join(hooks_path, 'post-commit')
229 237 version = get_version_from_hook(_hook_file)
230 238 return version
@@ -1,51 +1,59 b''
1 1 #!_ENV_
2
2 3 import os
3 4 import sys
4 5 path_adjust = [_PATH_]
5 6
6 7 if path_adjust:
7 8 sys.path = path_adjust
8 9
10 # special trick to pass in some information from rc to hooks
11 # mod_dav strips ALL env vars and we can't even access things like PATH
12 for env_k, env_v in _OS_EXPAND_:
13 os.environ[env_k] = env_v
14
9 15 try:
10 16 from vcsserver import hooks
11 17 except ImportError:
12 18 if os.environ.get('RC_DEBUG_GIT_HOOK'):
13 19 import traceback
14 20 print(traceback.format_exc())
15 21 hooks = None
16 22
17 23
18 24 # TIMESTAMP: _DATE_
19 25 RC_HOOK_VER = '_TMPL_'
20 26
21 27
22 28 def main():
23 29 if hooks is None:
24 30 # exit with success if we cannot import vcsserver.hooks !!
25 31 # this allows simply push to this repo even without rhodecode
26 32 sys.exit(0)
27 33
28 34 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_GIT_HOOKS'):
29 35 sys.exit(0)
30 36
31 37 repo_path = os.getcwd()
32 38 push_data = sys.stdin.readlines()
33 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
39
34 40 # os.environ is modified here by a subprocess call that
35 41 # runs git and later git executes this hook.
36 42 # Environ gets some additional info from rhodecode system
37 43 # like IP or username from basic-auth
44
45 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
38 46 try:
39 47 result = hooks.git_post_receive(repo_path, push_data, os.environ)
40 48 sys.exit(result)
41 49 except Exception as error:
42 50 # TODO: johbo: Improve handling of this special case
43 51 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
44 52 raise
45 53 print(f'ERROR: {error}')
46 54 sys.exit(1)
47 55 sys.exit(0)
48 56
49 57
50 58 if __name__ == '__main__':
51 59 main()
@@ -1,51 +1,59 b''
1 1 #!_ENV_
2
2 3 import os
3 4 import sys
4 5 path_adjust = [_PATH_]
5 6
6 7 if path_adjust:
7 8 sys.path = path_adjust
8 9
10 # special trick to pass in some information from rc to hooks
11 # mod_dav strips ALL env vars and we can't even access things like PATH
12 for env_k, env_v in _OS_EXPAND_:
13 os.environ[env_k] = env_v
14
9 15 try:
10 16 from vcsserver import hooks
11 17 except ImportError:
12 18 if os.environ.get('RC_DEBUG_GIT_HOOK'):
13 19 import traceback
14 20 print(traceback.format_exc())
15 21 hooks = None
16 22
17 23
18 24 # TIMESTAMP: _DATE_
19 25 RC_HOOK_VER = '_TMPL_'
20 26
21 27
22 28 def main():
23 29 if hooks is None:
24 30 # exit with success if we cannot import vcsserver.hooks !!
25 31 # this allows simply push to this repo even without rhodecode
26 32 sys.exit(0)
27 33
28 34 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_GIT_HOOKS'):
29 35 sys.exit(0)
30 36
31 37 repo_path = os.getcwd()
32 38 push_data = sys.stdin.readlines()
33 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
39
34 40 # os.environ is modified here by a subprocess call that
35 41 # runs git and later git executes this hook.
36 42 # Environ gets some additional info from rhodecode system
37 43 # like IP or username from basic-auth
44
45 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
38 46 try:
39 47 result = hooks.git_pre_receive(repo_path, push_data, os.environ)
40 48 sys.exit(result)
41 49 except Exception as error:
42 50 # TODO: johbo: Improve handling of this special case
43 51 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
44 52 raise
45 53 print(f'ERROR: {error}')
46 54 sys.exit(1)
47 55 sys.exit(0)
48 56
49 57
50 58 if __name__ == '__main__':
51 59 main()
@@ -1,54 +1,54 b''
1 1 #!_ENV_
2 2
3 3 import os
4 4 import sys
5 5 path_adjust = [_PATH_]
6 6
7 7 if path_adjust:
8 8 sys.path = path_adjust
9 9
10 # special trick to pass in some information from rc to hooks
11 # mod_dav strips ALL env vars and we can't even access things like PATH
12 for env_k, env_v in _OS_EXPAND_:
13 os.environ[env_k] = env_v
14
10 15 try:
11 16 from vcsserver import hooks
12 17 except ImportError:
13 18 if os.environ.get('RC_DEBUG_SVN_HOOK'):
14 19 import traceback
15 20 print(traceback.format_exc())
16 21 hooks = None
17 22
18 23
19 24 # TIMESTAMP: _DATE_
20 25 RC_HOOK_VER = '_TMPL_'
21 26
22 27
23 # special trick to pass in some information from rc to hooks
24 # mod_dav strips ALL env vars and we can't even access things like PATH
25 for env_k, env_v in _OS_EXPAND_:
26 os.environ[env_k] = env_v
27
28 28 def main():
29 29 if hooks is None:
30 30 # exit with success if we cannot import vcsserver.hooks !!
31 31 # this allows simply push to this repo even without rhodecode
32 32 sys.exit(0)
33 33
34 34 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
35 35 sys.exit(0)
36 repo_path = os.getcwd()
36 cwd_repo_path = os.getcwd()
37 37 push_data = sys.argv[1:]
38 38
39 39 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
40 40
41 41 try:
42 result = hooks.svn_post_commit(repo_path, push_data, os.environ)
42 result = hooks.svn_post_commit(cwd_repo_path, push_data, os.environ)
43 43 sys.exit(result)
44 44 except Exception as error:
45 45 # TODO: johbo: Improve handling of this special case
46 46 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
47 47 raise
48 48 print(f'ERROR: {error}')
49 49 sys.exit(1)
50 50 sys.exit(0)
51 51
52 52
53 53 if __name__ == '__main__':
54 54 main()
@@ -1,58 +1,57 b''
1 1 #!_ENV_
2 2
3 3 import os
4 4 import sys
5 5 path_adjust = [_PATH_]
6 6
7 7 if path_adjust:
8 8 sys.path = path_adjust
9 9
10 # special trick to pass in some information from rc to hooks
11 # mod_dav strips ALL env vars and we can't even access things like PATH
12 for env_k, env_v in _OS_EXPAND_:
13 os.environ[env_k] = env_v
14
10 15 try:
11 16 from vcsserver import hooks
12 17 except ImportError:
13 18 if os.environ.get('RC_DEBUG_SVN_HOOK'):
14 19 import traceback
15 20 print(traceback.format_exc())
16 21 hooks = None
17 22
18 23
19 24 # TIMESTAMP: _DATE_
20 25 RC_HOOK_VER = '_TMPL_'
21 26
22 27
23 # special trick to pass in some information from rc to hooks
24 # mod_dav strips ALL env vars and we can't even access things like PATH
25 for env_k, env_v in _OS_EXPAND_:
26 os.environ[env_k] = env_v
27
28 28 def main():
29 29 if os.environ.get('SSH_READ_ONLY') == '1':
30 30 sys.stderr.write('Only read-only access is allowed')
31 31 sys.exit(1)
32 32
33 33 if hooks is None:
34 34 # exit with success if we cannot import vcsserver.hooks !!
35 35 # this allows simply push to this repo even without rhodecode
36 36 sys.exit(0)
37 37
38 38 if os.environ.get('RC_SKIP_HOOKS') or os.environ.get('RC_SKIP_SVN_HOOKS'):
39 39 sys.exit(0)
40 repo_path = os.getcwd()
40 cwd_repo_path = os.getcwd()
41 41 push_data = sys.argv[1:]
42 42
43 43 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
44
45 44 try:
46 result = hooks.svn_pre_commit(repo_path, push_data, os.environ)
45 result = hooks.svn_pre_commit(cwd_repo_path, push_data, os.environ)
47 46 sys.exit(result)
48 47 except Exception as error:
49 48 # TODO: johbo: Improve handling of this special case
50 49 if not getattr(error, '_vcs_kind', None) == 'repo_locked':
51 50 raise
52 51 print(f'ERROR: {error}')
53 52 sys.exit(1)
54 53 sys.exit(0)
55 54
56 55
57 56 if __name__ == '__main__':
58 57 main()
@@ -1,832 +1,822 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import os
20 20 import sys
21 21 import logging
22 22 import collections
23 23 import base64
24 24 import msgpack
25 25 import dataclasses
26 26 import pygit2
27 27
28 28 import http.client
29 29 from celery import Celery
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33
34 from vcsserver import exceptions, subprocessio, settings
34 35 from vcsserver.lib.ext_json import json
35 from vcsserver import exceptions, subprocessio, settings
36 36 from vcsserver.lib.str_utils import ascii_str, safe_str
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
37 38 from vcsserver.remote.git_remote import Repository
38 39
39 40 celery_app = Celery('__vcsserver__')
40 41 log = logging.getLogger(__name__)
41 42
42 43
43 44 class HooksHttpClient:
44 45 proto = 'msgpack.v1'
45 46 connection = None
46 47
47 48 def __init__(self, hooks_uri):
48 49 self.hooks_uri = hooks_uri
49 50
50 51 def __repr__(self):
51 52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52 53
53 54 def __call__(self, method, extras):
54 55 connection = http.client.HTTPConnection(self.hooks_uri)
55 56 # binary msgpack body
56 57 headers, body = self._serialize(method, extras)
57 58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58 59
59 60 try:
60 61 try:
61 62 connection.request('POST', '/', body, headers)
62 63 except Exception as error:
63 64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 65 raise
65 66
66 67 response = connection.getresponse()
67 68 try:
68 69 return msgpack.load(response)
69 70 except Exception:
70 71 response_data = response.read()
71 72 log.exception('Failed to decode hook response json data. '
72 73 'response_code:%s, raw_data:%s',
73 74 response.status, response_data)
74 75 raise
75 76 finally:
76 77 connection.close()
77 78
78 79 @classmethod
79 80 def _serialize(cls, hook_name, extras):
80 81 data = {
81 82 'method': hook_name,
82 83 'extras': extras
83 84 }
84 85 headers = {
85 86 "rc-hooks-protocol": cls.proto,
86 87 "Connection": "keep-alive"
87 88 }
88 89 return headers, msgpack.packb(data)
89 90
90 91
91 92 class HooksCeleryClient:
92 93 TASK_TIMEOUT = 60 # time in seconds
93 94
94 95 def __init__(self, queue, backend):
95 96 celery_app.config_from_object({
96 97 'broker_url': queue, 'result_backend': backend,
97 98 'broker_connection_retry_on_startup': True,
98 99 'task_serializer': 'json',
99 100 'accept_content': ['json', 'msgpack'],
100 101 'result_serializer': 'json',
101 102 'result_accept_content': ['json', 'msgpack']
102 103 })
103 104 self.celery_app = celery_app
104 105
105 106 def __call__(self, method, extras):
106 107 inquired_task = self.celery_app.signature(
107 108 f'rhodecode.lib.celerylib.tasks.{method}'
108 109 )
109 110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
110 111
111 112
112 113 class HooksShadowRepoClient:
113 114
114 115 def __call__(self, hook_name, extras):
115 116 return {'output': '', 'status': 0}
116 117
117 118
118 119 class RemoteMessageWriter:
119 120 """Writer base class."""
120 121 def write(self, message):
121 122 raise NotImplementedError()
122 123
123 124
124 125 class HgMessageWriter(RemoteMessageWriter):
125 126 """Writer that knows how to send messages to mercurial clients."""
126 127
127 128 def __init__(self, ui):
128 129 self.ui = ui
129 130
130 131 def write(self, message: str):
131 132 # TODO: Check why the quiet flag is set by default.
132 133 old = self.ui.quiet
133 134 self.ui.quiet = False
134 135 self.ui.status(message.encode('utf-8'))
135 136 self.ui.quiet = old
136 137
137 138
138 139 class GitMessageWriter(RemoteMessageWriter):
139 140 """Writer that knows how to send messages to git clients."""
140 141
141 142 def __init__(self, stdout=None):
142 143 self.stdout = stdout or sys.stdout
143 144
144 145 def write(self, message: str):
145 146 self.stdout.write(message)
146 147
147 148
148 149 class SvnMessageWriter(RemoteMessageWriter):
149 150 """Writer that knows how to send messages to svn clients."""
150 151
151 152 def __init__(self, stderr=None):
152 153 # SVN needs data sent to stderr for back-to-client messaging
153 154 self.stderr = stderr or sys.stderr
154 155
155 156 def write(self, message):
156 157 self.stderr.write(message)
157 158
158 159
159 160 def _handle_exception(result):
160 161 exception_class = result.get('exception')
161 162 exception_traceback = result.get('exception_traceback')
162 163 log.debug('Handling hook-call exception: %s', exception_class)
163 164
164 165 if exception_traceback:
165 166 log.error('Got traceback from remote call:%s', exception_traceback)
166 167
167 168 if exception_class == 'HTTPLockedRC':
168 169 raise exceptions.RepositoryLockedException()(*result['exception_args'])
169 170 elif exception_class == 'HTTPBranchProtected':
170 171 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
171 172 elif exception_class == 'RepositoryError':
172 173 raise exceptions.VcsException()(*result['exception_args'])
173 174 elif exception_class:
174 175 raise Exception(
175 176 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
176 177 )
177 178
178 179
179 180 def _get_hooks_client(extras):
180 181 hooks_uri = extras.get('hooks_uri')
181 182 task_queue = extras.get('task_queue')
182 183 task_backend = extras.get('task_backend')
183 184 is_shadow_repo = extras.get('is_shadow_repo')
184 185
185 186 if hooks_uri:
186 187 return HooksHttpClient(hooks_uri)
187 188 elif task_queue and task_backend:
188 189 return HooksCeleryClient(task_queue, task_backend)
189 190 elif is_shadow_repo:
190 191 return HooksShadowRepoClient()
191 192 else:
192 193 raise Exception("Hooks client not found!")
193 194
194 195
195 196 def _call_hook(hook_name, extras, writer):
196 197 hooks_client = _get_hooks_client(extras)
197 198 log.debug('Hooks, using client:%s', hooks_client)
198 199 result = hooks_client(hook_name, extras)
199 200 log.debug('Hooks got result: %s', result)
200 201 _handle_exception(result)
201 202 writer.write(result['output'])
202 203
203 204 return result['status']
204 205
205 206
206 207 def _extras_from_ui(ui):
207 208 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
208 209 if not hook_data:
209 210 # maybe it's inside environ ?
210 211 env_hook_data = os.environ.get('RC_SCM_DATA')
211 212 if env_hook_data:
212 213 hook_data = env_hook_data
213 214
214 215 extras = {}
215 216 if hook_data:
216 217 extras = json.loads(hook_data)
217 218 return extras
218 219
219 220
220 221 def _rev_range_hash(repo, node, check_heads=False):
221 222 from vcsserver.hgcompat import get_ctx
222 223
223 224 commits = []
224 225 revs = []
225 226 start = get_ctx(repo, node).rev()
226 227 end = len(repo)
227 228 for rev in range(start, end):
228 229 revs.append(rev)
229 230 ctx = get_ctx(repo, rev)
230 231 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
231 232 branch = safe_str(ctx.branch())
232 233 commits.append((commit_id, branch))
233 234
234 235 parent_heads = []
235 236 if check_heads:
236 237 parent_heads = _check_heads(repo, start, end, revs)
237 238 return commits, parent_heads
238 239
239 240
240 241 def _check_heads(repo, start, end, commits):
241 242 from vcsserver.hgcompat import get_ctx
242 243 changelog = repo.changelog
243 244 parents = set()
244 245
245 246 for new_rev in commits:
246 247 for p in changelog.parentrevs(new_rev):
247 248 if p == mercurial.node.nullrev:
248 249 continue
249 250 if p < start:
250 251 parents.add(p)
251 252
252 253 for p in parents:
253 254 branch = get_ctx(repo, p).branch()
254 255 # The heads descending from that parent, on the same branch
255 256 parent_heads = {p}
256 257 reachable = {p}
257 258 for x in range(p + 1, end):
258 259 if get_ctx(repo, x).branch() != branch:
259 260 continue
260 261 for pp in changelog.parentrevs(x):
261 262 if pp in reachable:
262 263 reachable.add(x)
263 264 parent_heads.discard(pp)
264 265 parent_heads.add(x)
265 266 # More than one head? Suggest merging
266 267 if len(parent_heads) > 1:
267 268 return list(parent_heads)
268 269
269 270 return []
270 271
271 272
272 273 def _get_git_env():
273 274 env = {}
274 275 for k, v in os.environ.items():
275 276 if k.startswith('GIT'):
276 277 env[k] = v
277 278
278 279 # serialized version
279 280 return [(k, v) for k, v in env.items()]
280 281
281 282
282 283 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
283 284 env = {}
284 285 for k, v in os.environ.items():
285 286 if k.startswith('HG'):
286 287 env[k] = v
287 288
288 289 env['HG_NODE'] = old_rev
289 290 env['HG_NODE_LAST'] = new_rev
290 291 env['HG_TXNID'] = txnid
291 292 env['HG_PENDING'] = repo_path
292 293
293 294 return [(k, v) for k, v in env.items()]
294 295
295 296
297 def _get_ini_settings(ini_file):
298 from vcsserver.http_main import sanitize_settings_and_apply_defaults
299 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
300
301 global_config = {'__file__': ini_file}
302 ini_settings = get_app_config_lightweight(ini_file)
303 sanitize_settings_and_apply_defaults(global_config, ini_settings)
304 configure_and_store_settings(global_config, ini_settings)
305
306 return ini_settings
307
308
296 309 def _fix_hooks_executables(ini_path=''):
297 310 """
298 311 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
299 312 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
300 313 because svn is not on PATH
301 314 """
302 from vcsserver.http_main import sanitize_settings_and_apply_defaults
303 from vcsserver.lib.config_utils import get_app_config_lightweight
304
315 # set defaults, in case we can't read from ini_file
305 316 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
306 317 if ini_path:
307
308 ini_settings = get_app_config_lightweight(ini_path)
309 ini_settings = sanitize_settings_and_apply_defaults({'__file__': ini_path}, ini_settings)
318 ini_settings = _get_ini_settings(ini_path)
310 319 core_binary_dir = ini_settings['core.binary_dir']
311 320
312 321 settings.BINARY_DIR = core_binary_dir
313 322
314 323
315 324 def repo_size(ui, repo, **kwargs):
316 325 extras = _extras_from_ui(ui)
317 326 return _call_hook('repo_size', extras, HgMessageWriter(ui))
318 327
319 328
320 329 def pre_pull(ui, repo, **kwargs):
321 330 extras = _extras_from_ui(ui)
322 331 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
323 332
324 333
325 334 def pre_pull_ssh(ui, repo, **kwargs):
326 335 extras = _extras_from_ui(ui)
327 336 if extras and extras.get('SSH'):
328 337 return pre_pull(ui, repo, **kwargs)
329 338 return 0
330 339
331 340
332 341 def post_pull(ui, repo, **kwargs):
333 342 extras = _extras_from_ui(ui)
334 343 return _call_hook('post_pull', extras, HgMessageWriter(ui))
335 344
336 345
337 346 def post_pull_ssh(ui, repo, **kwargs):
338 347 extras = _extras_from_ui(ui)
339 348 if extras and extras.get('SSH'):
340 349 return post_pull(ui, repo, **kwargs)
341 350 return 0
342 351
343 352
344 353 def pre_push(ui, repo, node=None, **kwargs):
345 354 """
346 355 Mercurial pre_push hook
347 356 """
348 357 extras = _extras_from_ui(ui)
349 358 detect_force_push = extras.get('detect_force_push')
350 359
351 360 rev_data = []
352 361 hook_type: str = safe_str(kwargs.get('hooktype'))
353 362
354 363 if node and hook_type == 'pretxnchangegroup':
355 364 branches = collections.defaultdict(list)
356 365 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
357 366 for commit_id, branch in commits:
358 367 branches[branch].append(commit_id)
359 368
360 369 for branch, commits in branches.items():
361 370 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
362 371 rev_data.append({
363 372 'total_commits': len(commits),
364 373 'old_rev': old_rev,
365 374 'new_rev': commits[-1],
366 375 'ref': '',
367 376 'type': 'branch',
368 377 'name': branch,
369 378 })
370 379
371 380 for push_ref in rev_data:
372 381 push_ref['multiple_heads'] = _heads
373 382
374 383 repo_path = os.path.join(
375 384 extras.get('repo_store', ''), extras.get('repository', ''))
376 385 push_ref['hg_env'] = _get_hg_env(
377 386 old_rev=push_ref['old_rev'],
378 387 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
379 388 repo_path=repo_path)
380 389
381 390 extras['hook_type'] = hook_type or 'pre_push'
382 391 extras['commit_ids'] = rev_data
383 392
384 393 return _call_hook('pre_push', extras, HgMessageWriter(ui))
385 394
386 395
387 396 def pre_push_ssh(ui, repo, node=None, **kwargs):
388 397 extras = _extras_from_ui(ui)
389 398 if extras.get('SSH'):
390 399 return pre_push(ui, repo, node, **kwargs)
391 400
392 401 return 0
393 402
394 403
395 404 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
396 405 """
397 406 Mercurial pre_push hook for SSH
398 407 """
399 408 extras = _extras_from_ui(ui)
400 409 if extras.get('SSH'):
401 410 permission = extras['SSH_PERMISSIONS']
402 411
403 412 if 'repository.write' == permission or 'repository.admin' == permission:
404 413 return 0
405 414
406 415 # non-zero ret code
407 416 return 1
408 417
409 418 return 0
410 419
411 420
412 421 def post_push(ui, repo, node, **kwargs):
413 422 """
414 423 Mercurial post_push hook
415 424 """
416 425 extras = _extras_from_ui(ui)
417 426
418 427 commit_ids = []
419 428 branches = []
420 429 bookmarks = []
421 430 tags = []
422 431 hook_type: str = safe_str(kwargs.get('hooktype'))
423 432
424 433 commits, _heads = _rev_range_hash(repo, node)
425 434 for commit_id, branch in commits:
426 435 commit_ids.append(commit_id)
427 436 if branch not in branches:
428 437 branches.append(branch)
429 438
430 439 if hasattr(ui, '_rc_pushkey_bookmarks'):
431 440 bookmarks = ui._rc_pushkey_bookmarks
432 441
433 442 extras['hook_type'] = hook_type or 'post_push'
434 443 extras['commit_ids'] = commit_ids
435 444
436 445 extras['new_refs'] = {
437 446 'branches': branches,
438 447 'bookmarks': bookmarks,
439 448 'tags': tags
440 449 }
441 450
442 451 return _call_hook('post_push', extras, HgMessageWriter(ui))
443 452
444 453
445 454 def post_push_ssh(ui, repo, node, **kwargs):
446 455 """
447 456 Mercurial post_push hook for SSH
448 457 """
449 458 if _extras_from_ui(ui).get('SSH'):
450 459 return post_push(ui, repo, node, **kwargs)
451 460 return 0
452 461
453 462
454 463 def key_push(ui, repo, **kwargs):
455 464 from vcsserver.hgcompat import get_ctx
456 465
457 466 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
458 467 # store new bookmarks in our UI object propagated later to post_push
459 468 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
460 469 return
461 470
462 471
463 472 # backward compat
464 473 log_pull_action = post_pull
465 474
466 475 # backward compat
467 476 log_push_action = post_push
468 477
469 478
470 479 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
471 480 """
472 481 Old hook name: keep here for backward compatibility.
473 482
474 483 This is only required when the installed git hooks are not upgraded.
475 484 """
476 485 pass
477 486
478 487
479 488 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
480 489 """
481 490 Old hook name: keep here for backward compatibility.
482 491
483 492 This is only required when the installed git hooks are not upgraded.
484 493 """
485 494 pass
486 495
487 496
488 497 @dataclasses.dataclass
489 498 class HookResponse:
490 499 status: int
491 500 output: str
492 501
493 502
494 503 def git_pre_pull(extras) -> HookResponse:
495 504 """
496 505 Pre pull hook.
497 506
498 507 :param extras: dictionary containing the keys defined in simplevcs
499 508 :type extras: dict
500 509
501 510 :return: status code of the hook. 0 for success.
502 511 :rtype: int
503 512 """
504 513
505 514 if 'pull' not in extras['hooks']:
506 515 return HookResponse(0, '')
507 516
508 517 stdout = io.StringIO()
509 518 try:
510 519 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
511 520
512 521 except Exception as error:
513 522 log.exception('Failed to call pre_pull hook')
514 523 status_code = 128
515 524 stdout.write(f'ERROR: {error}\n')
516 525
517 526 return HookResponse(status_code, stdout.getvalue())
518 527
519 528
520 529 def git_post_pull(extras) -> HookResponse:
521 530 """
522 531 Post pull hook.
523 532
524 533 :param extras: dictionary containing the keys defined in simplevcs
525 534 :type extras: dict
526 535
527 536 :return: status code of the hook. 0 for success.
528 537 :rtype: int
529 538 """
530 539 if 'pull' not in extras['hooks']:
531 540 return HookResponse(0, '')
532 541
533 542 stdout = io.StringIO()
534 543 try:
535 544 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
536 545 except Exception as error:
537 546 status = 128
538 547 stdout.write(f'ERROR: {error}\n')
539 548
540 549 return HookResponse(status, stdout.getvalue())
541 550
542 551
543 552 def _parse_git_ref_lines(revision_lines):
544 553 rev_data = []
545 554 for revision_line in revision_lines or []:
546 555 old_rev, new_rev, ref = revision_line.strip().split(' ')
547 556 ref_data = ref.split('/', 2)
548 557 if ref_data[1] in ('tags', 'heads'):
549 558 rev_data.append({
550 559 # NOTE(marcink):
551 560 # we're unable to tell total_commits for git at this point
552 561 # but we set the variable for consistency with GIT
553 562 'total_commits': -1,
554 563 'old_rev': old_rev,
555 564 'new_rev': new_rev,
556 565 'ref': ref,
557 566 'type': ref_data[1],
558 567 'name': ref_data[2],
559 568 })
560 569 return rev_data
561 570
562 571
563 572 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
564 573 """
565 574 Pre push hook.
566 575
567 576 :return: status code of the hook. 0 for success.
568 577 """
569 578 extras = json.loads(env['RC_SCM_DATA'])
570 579 rev_data = _parse_git_ref_lines(revision_lines)
571 580 if 'push' not in extras['hooks']:
572 581 return 0
573 _fix_hooks_executables()
582 _fix_hooks_executables(env.get('RC_INI_FILE'))
574 583
575 584 empty_commit_id = '0' * 40
576 585
577 586 detect_force_push = extras.get('detect_force_push')
578 587
579 588 for push_ref in rev_data:
580 589 # store our git-env which holds the temp store
581 590 push_ref['git_env'] = _get_git_env()
582 591 push_ref['pruned_sha'] = ''
583 592 if not detect_force_push:
584 593 # don't check for forced-push when we don't need to
585 594 continue
586 595
587 596 type_ = push_ref['type']
588 597 new_branch = push_ref['old_rev'] == empty_commit_id
589 598 delete_branch = push_ref['new_rev'] == empty_commit_id
590 599 if type_ == 'heads' and not (new_branch or delete_branch):
591 600 old_rev = push_ref['old_rev']
592 601 new_rev = push_ref['new_rev']
593 602 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
594 603 stdout, stderr = subprocessio.run_command(
595 604 cmd, env=os.environ.copy())
596 605 # means we're having some non-reachable objects, this forced push was used
597 606 if stdout:
598 607 push_ref['pruned_sha'] = stdout.splitlines()
599 608
600 609 extras['hook_type'] = 'pre_receive'
601 610 extras['commit_ids'] = rev_data
602 611
603 612 stdout = sys.stdout
604 613 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
605 614
606 615 return status_code
607 616
608 617
609 618 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
610 619 """
611 620 Post push hook.
612 621
613 622 :return: status code of the hook. 0 for success.
614 623 """
615 624 extras = json.loads(env['RC_SCM_DATA'])
616 625 if 'push' not in extras['hooks']:
617 626 return 0
618 627
619 _fix_hooks_executables()
628 _fix_hooks_executables(env.get('RC_INI_FILE'))
620 629
621 630 rev_data = _parse_git_ref_lines(revision_lines)
622 631
623 632 git_revs = []
624 633
625 634 # N.B.(skreft): it is ok to just call git, as git before calling a
626 635 # subcommand sets the PATH environment variable so that it point to the
627 636 # correct version of the git executable.
628 637 empty_commit_id = '0' * 40
629 638 branches = []
630 639 tags = []
631 640 for push_ref in rev_data:
632 641 type_ = push_ref['type']
633 642
634 643 if type_ == 'heads':
635 644 # starting new branch case
636 645 if push_ref['old_rev'] == empty_commit_id:
637 646 push_ref_name = push_ref['name']
638 647
639 648 if push_ref_name not in branches:
640 649 branches.append(push_ref_name)
641 650
642 651 need_head_set = ''
643 652 with Repository(os.getcwd()) as repo:
644 653 try:
645 654 repo.head
646 655 except pygit2.GitError:
647 656 need_head_set = f'refs/heads/{push_ref_name}'
648 657
649 658 if need_head_set:
650 659 repo.set_head(need_head_set)
651 660 print(f"Setting default branch to {push_ref_name}")
652 661
653 662 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
654 663 stdout, stderr = subprocessio.run_command(
655 664 cmd, env=os.environ.copy())
656 665 heads = safe_str(stdout)
657 666 heads = heads.replace(push_ref['ref'], '')
658 667 heads = ' '.join(head for head
659 668 in heads.splitlines() if head) or '.'
660 669 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
661 670 '--pretty=format:%H', '--', push_ref['new_rev'],
662 671 '--not', heads]
663 672 stdout, stderr = subprocessio.run_command(
664 673 cmd, env=os.environ.copy())
665 674 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
666 675
667 676 # delete branch case
668 677 elif push_ref['new_rev'] == empty_commit_id:
669 678 git_revs.append(f'delete_branch=>{push_ref["name"]}')
670 679 else:
671 680 if push_ref['name'] not in branches:
672 681 branches.append(push_ref['name'])
673 682
674 683 cmd = [settings.GIT_EXECUTABLE(), 'log',
675 684 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
676 685 '--reverse', '--pretty=format:%H']
677 686 stdout, stderr = subprocessio.run_command(
678 687 cmd, env=os.environ.copy())
679 688 # we get bytes from stdout, we need str to be consistent
680 689 log_revs = list(map(ascii_str, stdout.splitlines()))
681 690 git_revs.extend(log_revs)
682 691
683 692 # Pure pygit2 impl. but still 2-3x slower :/
684 693 # results = []
685 694 #
686 695 # with Repository(os.getcwd()) as repo:
687 696 # repo_new_rev = repo[push_ref['new_rev']]
688 697 # repo_old_rev = repo[push_ref['old_rev']]
689 698 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
690 699 #
691 700 # for commit in walker:
692 701 # if commit.id == repo_old_rev.id:
693 702 # break
694 703 # results.append(commit.id.hex)
695 704 # # reverse the order, can't use GIT_SORT_REVERSE
696 705 # log_revs = results[::-1]
697 706
698 707 elif type_ == 'tags':
699 708 if push_ref['name'] not in tags:
700 709 tags.append(push_ref['name'])
701 710 git_revs.append(f'tag=>{push_ref["name"]}')
702 711
703 712 extras['hook_type'] = 'post_receive'
704 713 extras['commit_ids'] = git_revs
705 714 extras['new_refs'] = {
706 715 'branches': branches,
707 716 'bookmarks': [],
708 717 'tags': tags,
709 718 }
710 719
711 720 stdout = sys.stdout
712 721
713 722 if 'repo_size' in extras['hooks']:
714 723 try:
715 724 _call_hook('repo_size', extras, GitMessageWriter(stdout))
716 725 except Exception:
717 726 pass
718 727
719 728 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
720 729 return status_code
721 730
722 731
723 def _get_extras_from_txn_id(path, txn_id):
724 _fix_hooks_executables()
725
726 extras = {}
727 try:
728 cmd = [settings.SVNLOOK_EXECUTABLE(), 'pget',
729 '-t', txn_id,
730 '--revprop', path, 'rc-scm-extras']
731 stdout, stderr = subprocessio.run_command(
732 cmd, env=os.environ.copy())
733 extras = json.loads(base64.urlsafe_b64decode(stdout))
734 except Exception:
735 log.exception('Failed to extract extras info from txn_id')
736
737 return extras
738
739
740 def _get_extras_from_commit_id(commit_id, path):
741 _fix_hooks_executables()
742
743 extras = {}
744 try:
745 cmd = [settings.SVNLOOK_EXECUTABLE(), 'pget',
746 '-r', commit_id,
747 '--revprop', path, 'rc-scm-extras']
748 stdout, stderr = subprocessio.run_command(
749 cmd, env=os.environ.copy())
750 extras = json.loads(base64.urlsafe_b64decode(stdout))
751 except Exception:
752 log.exception('Failed to extract extras info from commit_id')
753
732 def get_extras_from_txn_id(repo_path, txn_id):
733 extras = get_txn_id_from_store(repo_path, txn_id)
754 734 return extras
755 735
756 736
757 737 def svn_pre_commit(repo_path, commit_data, env):
758 738
759 739 path, txn_id = commit_data
760 740 branches = []
761 741 tags = []
762 742
763 743 if env.get('RC_SCM_DATA'):
764 744 extras = json.loads(env['RC_SCM_DATA'])
765 745 else:
746 ini_path = env.get('RC_INI_FILE')
747 if ini_path:
748 _get_ini_settings(ini_path)
766 749 # fallback method to read from TXN-ID stored data
767 extras = _get_extras_from_txn_id(path, txn_id)
750 extras = get_extras_from_txn_id(path, txn_id)
768 751
769 752 if not extras:
770 #TODO: temporary fix until svn txn-id changes are merged
753 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
754
755 if extras.get('rc_internal_commit'):
756 # special marker for internal commit, we don't call hooks client
771 757 return 0
772 raise ValueError('Failed to extract context data called extras for hook execution')
773 758
774 759 extras['hook_type'] = 'pre_commit'
775 760 extras['commit_ids'] = [txn_id]
776 761 extras['txn_id'] = txn_id
777 762 extras['new_refs'] = {
778 763 'total_commits': 1,
779 764 'branches': branches,
780 765 'bookmarks': [],
781 766 'tags': tags,
782 767 }
783 768
784 769 return _call_hook('pre_push', extras, SvnMessageWriter())
785 770
786 771
787 772 def svn_post_commit(repo_path, commit_data, env):
788 773 """
789 774 commit_data is path, rev, txn_id
790 775 """
791 776
792 777 if len(commit_data) == 3:
793 778 path, commit_id, txn_id = commit_data
794 779 elif len(commit_data) == 2:
795 780 log.error('Failed to extract txn_id from commit_data using legacy method. '
796 781 'Some functionality might be limited')
797 782 path, commit_id = commit_data
798 783 txn_id = None
799 784 else:
800 785 return 0
801 786
802 787 branches = []
803 788 tags = []
804 789
805 790 if env.get('RC_SCM_DATA'):
806 791 extras = json.loads(env['RC_SCM_DATA'])
807 792 else:
793 ini_path = env.get('RC_INI_FILE')
794 if ini_path:
795 _get_ini_settings(ini_path)
808 796 # fallback method to read from TXN-ID stored data
809 extras = _get_extras_from_commit_id(commit_id, path)
797 extras = get_extras_from_txn_id(path, txn_id)
810 798
811 if not extras:
812 #TODO: temporary fix until svn txn-id changes are merged
799 if not extras and txn_id:
800 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
801
802 if extras.get('rc_internal_commit'):
803 # special marker for internal commit, we don't call hooks client
813 804 return 0
814 raise ValueError('Failed to extract context data called extras for hook execution')
815 805
816 806 extras['hook_type'] = 'post_commit'
817 807 extras['commit_ids'] = [commit_id]
818 808 extras['txn_id'] = txn_id
819 809 extras['new_refs'] = {
820 810 'branches': branches,
821 811 'bookmarks': [],
822 812 'tags': tags,
823 813 'total_commits': 1,
824 814 }
825 815
826 816 if 'repo_size' in extras['hooks']:
827 817 try:
828 818 _call_hook('repo_size', extras, SvnMessageWriter())
829 819 except Exception:
830 820 pass
831 821
832 822 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,774 +1,763 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import os
20 20 import platform
21 21 import sys
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import tempfile
28 28 import psutil
29 29
30 30 from itertools import chain
31 31
32 32 import msgpack
33 33 import configparser
34 34
35 35 from pyramid.config import Configurator
36 36 from pyramid.wsgi import wsgiapp
37 37 from pyramid.response import Response
38 38
39 39 from vcsserver.base import BytesEnvelope, BinaryEnvelope
40 from vcsserver.lib.ext_json import json
40
41 41 from vcsserver.config.settings_maker import SettingsMaker
42 from vcsserver.lib.str_utils import safe_int
43 from vcsserver.lib.statsd_client import StatsdClient
42
44 43 from vcsserver.tweens.request_wrapper import get_headers_call_context
45 44
46 import vcsserver
47 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
45 from vcsserver import remote_wsgi, scm_app, hgpatches
46 from vcsserver.server import VcsServer
48 47 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
49 48 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
50 49 from vcsserver.echo_stub.echo_app import EchoApp
51 50 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
52 51 from vcsserver.lib.exc_tracking import store_exception, format_exc
53 from vcsserver.server import VcsServer
52 from vcsserver.lib.str_utils import safe_int
53 from vcsserver.lib.statsd_client import StatsdClient
54 from vcsserver.lib.ext_json import json
55 from vcsserver.lib.config_utils import configure_and_store_settings
56
54 57
55 58 strict_vcs = True
56 59
57 60 git_import_err = None
58 61 try:
59 62 from vcsserver.remote.git_remote import GitFactory, GitRemote
60 63 except ImportError as e:
61 64 GitFactory = None
62 65 GitRemote = None
63 66 git_import_err = e
64 67 if strict_vcs:
65 68 raise
66 69
67 70
68 71 hg_import_err = None
69 72 try:
70 73 from vcsserver.remote.hg_remote import MercurialFactory, HgRemote
71 74 except ImportError as e:
72 75 MercurialFactory = None
73 76 HgRemote = None
74 77 hg_import_err = e
75 78 if strict_vcs:
76 79 raise
77 80
78 81
79 82 svn_import_err = None
80 83 try:
81 84 from vcsserver.remote.svn_remote import SubversionFactory, SvnRemote
82 85 except ImportError as e:
83 86 SubversionFactory = None
84 87 SvnRemote = None
85 88 svn_import_err = e
86 89 if strict_vcs:
87 90 raise
88 91
89 92 log = logging.getLogger(__name__)
90 93
91 94 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
92 95 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
93 96
94 97 try:
95 98 locale.setlocale(locale.LC_ALL, '')
96 99 except locale.Error as e:
97 log.error(
98 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
100 log.error('LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
99 101 os.environ['LC_ALL'] = 'C'
100 102
101 103
102 104 def _is_request_chunked(environ):
103 105 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 106 return stream
105 107
106 108
107 109 def log_max_fd():
108 110 try:
109 111 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 112 log.info('Max file descriptors value: %s', maxfd)
111 113 except Exception:
112 114 pass
113 115
114 116
115 117 class VCS:
116 118 def __init__(self, locale_conf=None, cache_config=None):
117 119 self.locale = locale_conf
118 120 self.cache_config = cache_config
119 121 self._configure_locale()
120 122
121 123 log_max_fd()
122 124
123 125 if GitFactory and GitRemote:
124 126 git_factory = GitFactory()
125 127 self._git_remote = GitRemote(git_factory)
126 128 else:
127 129 log.error("Git client import failed: %s", git_import_err)
128 130
129 131 if MercurialFactory and HgRemote:
130 132 hg_factory = MercurialFactory()
131 133 self._hg_remote = HgRemote(hg_factory)
132 134 else:
133 135 log.error("Mercurial client import failed: %s", hg_import_err)
134 136
135 137 if SubversionFactory and SvnRemote:
136 138 svn_factory = SubversionFactory()
137 139
138 140 # hg factory is used for svn url validation
139 141 hg_factory = MercurialFactory()
140 142 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 143 else:
142 144 log.error("Subversion client import failed: %s", svn_import_err)
143 145
144 146 self._vcsserver = VcsServer()
145 147
146 148 def _configure_locale(self):
147 149 if self.locale:
148 150 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 151 else:
150 152 log.info('Configuring locale subsystem based on environment variables')
151 153 try:
152 154 # If self.locale is the empty string, then the locale
153 155 # module will use the environment variables. See the
154 156 # documentation of the package `locale`.
155 157 locale.setlocale(locale.LC_ALL, self.locale)
156 158
157 159 language_code, encoding = locale.getlocale()
158 160 log.info(
159 161 'Locale set to language code "%s" with encoding "%s".',
160 162 language_code, encoding)
161 163 except locale.Error:
162 164 log.exception('Cannot set locale, not configuring the locale system')
163 165
164 166
165 167 class WsgiProxy:
166 168 def __init__(self, wsgi):
167 169 self.wsgi = wsgi
168 170
169 171 def __call__(self, environ, start_response):
170 172 input_data = environ['wsgi.input'].read()
171 173 input_data = msgpack.unpackb(input_data)
172 174
173 175 error = None
174 176 try:
175 177 data, status, headers = self.wsgi.handle(
176 178 input_data['environment'], input_data['input_data'],
177 179 *input_data['args'], **input_data['kwargs'])
178 180 except Exception as e:
179 181 data, status, headers = [], None, None
180 182 error = {
181 183 'message': str(e),
182 184 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 185 }
184 186
185 187 start_response(200, {})
186 188 return self._iterator(error, status, headers, data)
187 189
188 190 def _iterator(self, error, status, headers, data):
189 191 initial_data = [
190 192 error,
191 193 status,
192 194 headers,
193 195 ]
194 196
195 197 for d in chain(initial_data, data):
196 198 yield msgpack.packb(d)
197 199
198 200
199 201 def not_found(request):
200 202 return {'status': '404 NOT FOUND'}
201 203
202 204
203 205 class VCSViewPredicate:
204 206 def __init__(self, val, config):
205 207 self.remotes = val
206 208
207 209 def text(self):
208 210 return f'vcs view method = {list(self.remotes.keys())}'
209 211
210 212 phash = text
211 213
212 214 def __call__(self, context, request):
213 215 """
214 216 View predicate that returns true if given backend is supported by
215 217 defined remotes.
216 218 """
217 219 backend = request.matchdict.get('backend')
218 220 return backend in self.remotes
219 221
220 222
221 223 class HTTPApplication:
222 224 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 225
224 226 remote_wsgi = remote_wsgi
225 227 _use_echo_app = False
226 228
227 229 def __init__(self, settings=None, global_config=None):
228 230
229 231 self.config = Configurator(settings=settings)
230 232 # Init our statsd at very start
231 233 self.config.registry.statsd = StatsdClient.statsd
232 234 self.config.registry.vcs_call_context = {}
233 235
234 236 self.global_config = global_config
235 237 self.config.include('vcsserver.lib.rc_cache')
236 238 self.config.include('vcsserver.lib.archive_cache')
237 239
238 240 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 241 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 242 self._remotes = {
241 243 'hg': vcs._hg_remote,
242 244 'git': vcs._git_remote,
243 245 'svn': vcs._svn_remote,
244 246 'server': vcs._vcsserver,
245 247 }
246 248 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 249 self._use_echo_app = True
248 250 log.warning("Using EchoApp for VCS operations.")
249 251 self.remote_wsgi = remote_wsgi_stub
250 252
251 self._configure_settings(global_config, settings)
253 configure_and_store_settings(global_config, settings)
252 254
253 255 self._configure()
254 256
255 def _configure_settings(self, global_config, app_settings):
256 """
257 Configure the settings module.
258 """
259 settings_merged = global_config.copy()
260 settings_merged.update(app_settings)
261
262 binary_dir = app_settings['core.binary_dir']
263
264 settings.BINARY_DIR = binary_dir
265
266 # Store the settings to make them available to other modules.
267 vcsserver.PYRAMID_SETTINGS = settings_merged
268 vcsserver.CONFIG = settings_merged
269
270 257 def _configure(self):
271 258 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
272 259
273 260 self.config.add_route('service', '/_service')
274 261 self.config.add_route('status', '/status')
275 262 self.config.add_route('hg_proxy', '/proxy/hg')
276 263 self.config.add_route('git_proxy', '/proxy/git')
277 264
278 265 # rpc methods
279 266 self.config.add_route('vcs', '/{backend}')
280 267
281 268 # streaming rpc remote methods
282 269 self.config.add_route('vcs_stream', '/{backend}/stream')
283 270
284 271 # vcs operations clone/push as streaming
285 272 self.config.add_route('stream_git', '/stream/git/*repo_name')
286 273 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
287 274
288 275 self.config.add_view(self.status_view, route_name='status', renderer='json')
289 276 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
290 277
291 278 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
292 279 self.config.add_view(self.git_proxy(), route_name='git_proxy')
293 280 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
294 281 vcs_view=self._remotes)
295 282 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
296 283 vcs_view=self._remotes)
297 284
298 285 self.config.add_view(self.hg_stream(), route_name='stream_hg')
299 286 self.config.add_view(self.git_stream(), route_name='stream_git')
300 287
301 288 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
302 289
303 290 self.config.add_notfound_view(not_found, renderer='json')
304 291
305 292 self.config.add_view(self.handle_vcs_exception, context=Exception)
306 293
307 294 self.config.add_tween(
308 295 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
309 296 )
310 297 self.config.add_request_method(
311 298 'vcsserver.lib.request_counter.get_request_counter',
312 299 'request_count')
313 300
314 301 def wsgi_app(self):
315 302 return self.config.make_wsgi_app()
316 303
317 304 def _vcs_view_params(self, request):
318 305 remote = self._remotes[request.matchdict['backend']]
319 306 payload = msgpack.unpackb(request.body, use_list=True)
320 307
321 308 method = payload.get('method')
322 309 params = payload['params']
323 310 wire = params.get('wire')
324 311 args = params.get('args')
325 312 kwargs = params.get('kwargs')
326 313 context_uid = None
327 314
328 315 request.registry.vcs_call_context = {
329 316 'method': method,
330 317 'repo_name': payload.get('_repo_name'),
331 318 }
332 319
333 320 if wire:
334 321 try:
335 322 wire['context'] = context_uid = uuid.UUID(wire['context'])
336 323 except KeyError:
337 324 pass
338 325 args.insert(0, wire)
339 326 repo_state_uid = wire.get('repo_state_uid') if wire else None
340 327
341 328 # NOTE(marcink): trading complexity for slight performance
342 329 if log.isEnabledFor(logging.DEBUG):
343 330 # also we SKIP printing out any of those methods args since they maybe excessive
344 331 just_args_methods = {
345 332 'commitctx': ('content', 'removed', 'updated'),
346 333 'commit': ('content', 'removed', 'updated')
347 334 }
348 335 if method in just_args_methods:
349 336 skip_args = just_args_methods[method]
350 337 call_args = ''
351 338 call_kwargs = {}
352 339 for k in kwargs:
353 340 if k in skip_args:
354 341 # replace our skip key with dummy
355 342 call_kwargs[k] = f'RemovedParam({k})'
356 343 else:
357 344 call_kwargs[k] = kwargs[k]
358 345 else:
359 346 call_args = args[1:]
360 347 call_kwargs = kwargs
361 348
362 349 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
363 350 method, call_args, call_kwargs, context_uid, repo_state_uid)
364 351
365 352 statsd = request.registry.statsd
366 353 if statsd:
367 354 statsd.incr(
368 355 'vcsserver_method_total', tags=[
369 356 f"method:{method}",
370 357 ])
371 358 return payload, remote, method, args, kwargs
372 359
373 360 def vcs_view(self, request):
374 361
375 362 payload, remote, method, args, kwargs = self._vcs_view_params(request)
376 363 payload_id = payload.get('id')
377 364
378 365 try:
379 366 resp = getattr(remote, method)(*args, **kwargs)
380 367 except Exception as e:
381 368 exc_info = list(sys.exc_info())
382 369 exc_type, exc_value, exc_traceback = exc_info
383 370
384 371 org_exc = getattr(e, '_org_exc', None)
385 372 org_exc_name = None
386 373 org_exc_tb = ''
387 374 if org_exc:
388 375 org_exc_name = org_exc.__class__.__name__
389 376 org_exc_tb = getattr(e, '_org_exc_tb', '')
390 377 # replace our "faked" exception with our org
391 378 exc_info[0] = org_exc.__class__
392 379 exc_info[1] = org_exc
393 380
394 381 should_store_exc = True
395 382 if org_exc:
396 383 def get_exc_fqn(_exc_obj):
397 384 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
398 385 return module_name + '.' + org_exc_name
399 386
400 387 exc_fqn = get_exc_fqn(org_exc)
401 388
402 389 if exc_fqn in ['mercurial.error.RepoLookupError',
403 390 'vcsserver.exceptions.RefNotFoundException']:
404 391 should_store_exc = False
405 392
406 393 if should_store_exc:
407 394 store_exception(id(exc_info), exc_info, request_path=request.path)
408 395
409 396 tb_info = format_exc(exc_info)
410 397
411 398 type_ = e.__class__.__name__
412 399 if type_ not in self.ALLOWED_EXCEPTIONS:
413 400 type_ = None
414 401
415 402 resp = {
416 403 'id': payload_id,
417 404 'error': {
418 405 'message': str(e),
419 406 'traceback': tb_info,
420 407 'org_exc': org_exc_name,
421 408 'org_exc_tb': org_exc_tb,
422 409 'type': type_
423 410 }
424 411 }
425 412
426 413 try:
427 414 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
428 415 except AttributeError:
429 416 pass
430 417 else:
431 418 resp = {
432 419 'id': payload_id,
433 420 'result': resp
434 421 }
435 422 log.debug('Serving data for method %s', method)
436 423 return resp
437 424
438 425 def vcs_stream_view(self, request):
439 426 payload, remote, method, args, kwargs = self._vcs_view_params(request)
440 427 # this method has a stream: marker we remove it here
441 428 method = method.split('stream:')[-1]
442 429 chunk_size = safe_int(payload.get('chunk_size')) or 4096
443 430
444 431 resp = getattr(remote, method)(*args, **kwargs)
445 432
446 433 def get_chunked_data(method_resp):
447 434 stream = io.BytesIO(method_resp)
448 435 while 1:
449 436 chunk = stream.read(chunk_size)
450 437 if not chunk:
451 438 break
452 439 yield chunk
453 440
454 441 response = Response(app_iter=get_chunked_data(resp))
455 442 response.content_type = 'application/octet-stream'
456 443
457 444 return response
458 445
459 446 def status_view(self, request):
460 447 import vcsserver
461 448 _platform_id = platform.uname()[1] or 'instance'
462 449
463 450 return {
464 451 "status": "OK",
465 452 "vcsserver_version": vcsserver.get_version(),
466 453 "platform": _platform_id,
467 454 "pid": os.getpid(),
468 455 }
469 456
470 457 def service_view(self, request):
471 458 import vcsserver
472 459
473 460 payload = msgpack.unpackb(request.body, use_list=True)
474 461 server_config, app_config = {}, {}
475 462
476 463 try:
477 464 path = self.global_config['__file__']
478 465 config = configparser.RawConfigParser()
479 466
480 467 config.read(path)
481 468
482 469 if config.has_section('server:main'):
483 470 server_config = dict(config.items('server:main'))
484 471 if config.has_section('app:main'):
485 472 app_config = dict(config.items('app:main'))
486 473
487 474 except Exception:
488 475 log.exception('Failed to read .ini file for display')
489 476
490 477 environ = list(os.environ.items())
491 478
492 479 resp = {
493 480 'id': payload.get('id'),
494 481 'result': dict(
495 482 version=vcsserver.get_version(),
496 483 config=server_config,
497 484 app_config=app_config,
498 485 environ=environ,
499 486 payload=payload,
500 487 )
501 488 }
502 489 return resp
503 490
504 491 def _msgpack_renderer_factory(self, info):
505 492
506 493 def _render(value, system):
507 494 bin_type = False
508 495 res = value.get('result')
509 496 if isinstance(res, BytesEnvelope):
510 497 log.debug('Result is wrapped in BytesEnvelope type')
511 498 bin_type = True
512 499 elif isinstance(res, BinaryEnvelope):
513 500 log.debug('Result is wrapped in BinaryEnvelope type')
514 501 value['result'] = res.val
515 502 bin_type = True
516 503
517 504 request = system.get('request')
518 505 if request is not None:
519 506 response = request.response
520 507 ct = response.content_type
521 508 if ct == response.default_content_type:
522 509 response.content_type = 'application/x-msgpack'
523 510 if bin_type:
524 511 response.content_type = 'application/x-msgpack-bin'
525 512
526 513 return msgpack.packb(value, use_bin_type=bin_type)
527 514 return _render
528 515
529 516 def set_env_from_config(self, environ, config):
530 517 dict_conf = {}
531 518 try:
532 519 for elem in config:
533 520 if elem[0] == 'rhodecode':
534 521 dict_conf = json.loads(elem[2])
535 522 break
536 523 except Exception:
537 524 log.exception('Failed to fetch SCM CONFIG')
538 525 return
539 526
540 527 username = dict_conf.get('username')
541 528 if username:
542 529 environ['REMOTE_USER'] = username
543 530 # mercurial specific, some extension api rely on this
544 531 environ['HGUSER'] = username
545 532
546 533 ip = dict_conf.get('ip')
547 534 if ip:
548 535 environ['REMOTE_HOST'] = ip
549 536
550 537 if _is_request_chunked(environ):
551 538 # set the compatibility flag for webob
552 539 environ['wsgi.input_terminated'] = True
553 540
554 541 def hg_proxy(self):
555 542 @wsgiapp
556 543 def _hg_proxy(environ, start_response):
557 544 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
558 545 return app(environ, start_response)
559 546 return _hg_proxy
560 547
561 548 def git_proxy(self):
562 549 @wsgiapp
563 550 def _git_proxy(environ, start_response):
564 551 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
565 552 return app(environ, start_response)
566 553 return _git_proxy
567 554
568 555 def hg_stream(self):
569 556 if self._use_echo_app:
570 557 @wsgiapp
571 558 def _hg_stream(environ, start_response):
572 559 app = EchoApp('fake_path', 'fake_name', None)
573 560 return app(environ, start_response)
574 561 return _hg_stream
575 562 else:
576 563 @wsgiapp
577 564 def _hg_stream(environ, start_response):
578 565 log.debug('http-app: handling hg stream')
579 566 call_context = get_headers_call_context(environ)
580 567
581 568 repo_path = call_context['repo_path']
582 569 repo_name = call_context['repo_name']
583 570 config = call_context['repo_config']
584 571
585 572 app = scm_app.create_hg_wsgi_app(
586 573 repo_path, repo_name, config)
587 574
588 575 # Consistent path information for hgweb
589 576 environ['PATH_INFO'] = call_context['path_info']
590 577 environ['REPO_NAME'] = repo_name
591 578 self.set_env_from_config(environ, config)
592 579
593 580 log.debug('http-app: starting app handler '
594 581 'with %s and process request', app)
595 582 return app(environ, ResponseFilter(start_response))
596 583 return _hg_stream
597 584
598 585 def git_stream(self):
599 586 if self._use_echo_app:
600 587 @wsgiapp
601 588 def _git_stream(environ, start_response):
602 589 app = EchoApp('fake_path', 'fake_name', None)
603 590 return app(environ, start_response)
604 591 return _git_stream
605 592 else:
606 593 @wsgiapp
607 594 def _git_stream(environ, start_response):
608 595 log.debug('http-app: handling git stream')
609 596
610 597 call_context = get_headers_call_context(environ)
611 598
612 599 repo_path = call_context['repo_path']
613 600 repo_name = call_context['repo_name']
614 601 config = call_context['repo_config']
615 602
616 603 environ['PATH_INFO'] = call_context['path_info']
617 604 self.set_env_from_config(environ, config)
618 605
619 606 content_type = environ.get('CONTENT_TYPE', '')
620 607
621 608 path = environ['PATH_INFO']
622 609 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
623 610 log.debug(
624 611 'LFS: Detecting if request `%s` is LFS server path based '
625 612 'on content type:`%s`, is_lfs:%s',
626 613 path, content_type, is_lfs_request)
627 614
628 615 if not is_lfs_request:
629 616 # fallback detection by path
630 617 if GIT_LFS_PROTO_PAT.match(path):
631 618 is_lfs_request = True
632 619 log.debug(
633 620 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
634 621 path, is_lfs_request)
635 622
636 623 if is_lfs_request:
637 624 app = scm_app.create_git_lfs_wsgi_app(
638 625 repo_path, repo_name, config)
639 626 else:
640 627 app = scm_app.create_git_wsgi_app(
641 628 repo_path, repo_name, config)
642 629
643 630 log.debug('http-app: starting app handler '
644 631 'with %s and process request', app)
645 632
646 633 return app(environ, start_response)
647 634
648 635 return _git_stream
649 636
650 637 def handle_vcs_exception(self, exception, request):
651 638 _vcs_kind = getattr(exception, '_vcs_kind', '')
652 639
653 640 if _vcs_kind == 'repo_locked':
654 641 headers_call_context = get_headers_call_context(request.environ)
655 642 status_code = safe_int(headers_call_context['locked_status_code'])
656 643
657 644 return HTTPRepoLocked(
658 645 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
659 646
660 647 elif _vcs_kind == 'repo_branch_protected':
661 648 # Get custom repo-branch-protected status code if present.
662 649 return HTTPRepoBranchProtected(
663 650 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
664 651
665 652 exc_info = request.exc_info
666 653 store_exception(id(exc_info), exc_info)
667 654
668 655 traceback_info = 'unavailable'
669 656 if request.exc_info:
670 657 traceback_info = format_exc(request.exc_info)
671 658
672 659 log.error(
673 660 'error occurred handling this request for path: %s, \n%s',
674 661 request.path, traceback_info)
675 662
676 663 statsd = request.registry.statsd
677 664 if statsd:
678 665 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
679 666 statsd.incr('vcsserver_exception_total',
680 667 tags=[f"type:{exc_type}"])
681 668 raise exception
682 669
683 670
684 671 class ResponseFilter:
685 672
686 673 def __init__(self, start_response):
687 674 self._start_response = start_response
688 675
689 676 def __call__(self, status, response_headers, exc_info=None):
690 677 headers = tuple(
691 678 (h, v) for h, v in response_headers
692 679 if not wsgiref.util.is_hop_by_hop(h))
693 680 return self._start_response(status, headers, exc_info)
694 681
695 682
696 683 def sanitize_settings_and_apply_defaults(global_config, settings):
697 684 _global_settings_maker = SettingsMaker(global_config)
698 685 settings_maker = SettingsMaker(settings)
699 686
700 687 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
701 688
702 689 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
703 690 settings_maker.enable_logging(logging_conf)
704 691
705 692 # Default includes, possible to change as a user
706 693 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
707 694 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
708 695
709 696 settings_maker.make_setting('__file__', global_config.get('__file__'))
710 697
711 698 settings_maker.make_setting('pyramid.default_locale_name', 'en')
712 699 settings_maker.make_setting('locale', 'en_US.UTF-8')
713 700
714 701 settings_maker.make_setting(
715 702 'core.binary_dir', '/usr/local/bin/rhodecode_bin/vcs_bin',
716 703 default_when_empty=True, parser='string:noquote')
717 704
705 settings_maker.make_setting('vcs.svn.redis_conn', 'redis://redis:6379/0')
706
718 707 temp_store = tempfile.gettempdir()
719 708 default_cache_dir = os.path.join(temp_store, 'rc_cache')
720 709 # save default, cache dir, and use it for all backends later.
721 710 default_cache_dir = settings_maker.make_setting(
722 711 'cache_dir',
723 712 default=default_cache_dir, default_when_empty=True,
724 713 parser='dir:ensured')
725 714
726 715 # exception store cache
727 716 settings_maker.make_setting(
728 717 'exception_tracker.store_path',
729 718 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
730 719 parser='dir:ensured'
731 720 )
732 721
733 722 # repo_object cache defaults
734 723 settings_maker.make_setting(
735 724 'rc_cache.repo_object.backend',
736 725 default='dogpile.cache.rc.file_namespace',
737 726 parser='string')
738 727 settings_maker.make_setting(
739 728 'rc_cache.repo_object.expiration_time',
740 729 default=30 * 24 * 60 * 60, # 30days
741 730 parser='int')
742 731 settings_maker.make_setting(
743 732 'rc_cache.repo_object.arguments.filename',
744 733 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
745 734 parser='string')
746 735
747 736 # statsd
748 737 settings_maker.make_setting('statsd.enabled', False, parser='bool')
749 738 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
750 739 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
751 740 settings_maker.make_setting('statsd.statsd_prefix', '')
752 741 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
753 742
754 743 settings_maker.env_expand()
755 744
756 745
757 746 def main(global_config, **settings):
758 747 start_time = time.time()
759 748 log.info('Pyramid app config starting')
760 749
761 750 if MercurialFactory:
762 751 hgpatches.patch_largefiles_capabilities()
763 752 hgpatches.patch_subrepo_type_mapping()
764 753
765 754 # Fill in and sanitize the defaults & do ENV expansion
766 755 sanitize_settings_and_apply_defaults(global_config, settings)
767 756
768 757 # init and bootstrap StatsdClient
769 758 StatsdClient.setup(settings)
770 759
771 760 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
772 761 total_time = time.time() - start_time
773 762 log.info('Pyramid app created and configured in %.2fs', total_time)
774 763 return pyramid_app
@@ -1,40 +1,58 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 import vcsserver
20 import vcsserver.settings
19 21
20 22
21 23 def get_config(ini_path, **kwargs):
22 24 import configparser
23 25 parser = configparser.ConfigParser(**kwargs)
24 26 parser.read(ini_path)
25 27 return parser
26 28
27 29
28 30 def get_app_config_lightweight(ini_path):
29 31 parser = get_config(ini_path)
30 32 parser.set('app:main', 'here', os.getcwd())
31 33 parser.set('app:main', '__file__', ini_path)
32 34 return dict(parser.items('app:main'))
33 35
34 36
35 37 def get_app_config(ini_path):
36 38 """
37 39 This loads the app context and provides a heavy type iniliaziation of config
38 40 """
39 41 from paste.deploy.loadwsgi import appconfig
40 42 return appconfig(f'config:{ini_path}', relative_to=os.getcwd())
43
44
45 def configure_and_store_settings(global_config, app_settings):
46 """
47 Configure the settings module.
48 """
49 settings_merged = global_config.copy()
50 settings_merged.update(app_settings)
51
52 binary_dir = app_settings['core.binary_dir']
53
54 vcsserver.settings.BINARY_DIR = binary_dir
55
56 # Store the settings to make them available to other modules.
57 vcsserver.PYRAMID_SETTINGS = settings_merged
58 vcsserver.CONFIG = settings_merged
@@ -1,954 +1,959 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18
19 19 import os
20 20 import subprocess
21 21 from urllib.error import URLError
22 22 import urllib.parse
23 23 import logging
24 24 import posixpath as vcspath
25 25 import io
26 26 import urllib.request
27 27 import urllib.parse
28 28 import urllib.error
29 29 import traceback
30 30
31
32 31 import svn.client # noqa
33 32 import svn.core # noqa
34 33 import svn.delta # noqa
35 34 import svn.diff # noqa
36 35 import svn.fs # noqa
37 36 import svn.repos # noqa
38 37
39 38 import rhodecode
40 39 from vcsserver import svn_diff, exceptions, subprocessio, settings
41 40 from vcsserver.base import (
42 41 RepoFactory,
43 42 raise_from_original,
44 43 ArchiveNode,
45 44 store_archive_in_cache,
46 45 BytesEnvelope,
47 46 BinaryEnvelope,
48 47 )
49 48 from vcsserver.exceptions import NoContentException
49 from vcsserver.vcs_base import RemoteBase
50 50 from vcsserver.lib.str_utils import safe_str, safe_bytes
51 51 from vcsserver.lib.type_utils import assert_bytes
52 from vcsserver.vcs_base import RemoteBase
53 52 from vcsserver.lib.svnremoterepo import svnremoterepo
53 from vcsserver.lib.svn_txn_utils import store_txn_id_data
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 svn_compatible_versions_map = {
59 59 'pre-1.4-compatible': '1.3',
60 60 'pre-1.5-compatible': '1.4',
61 61 'pre-1.6-compatible': '1.5',
62 62 'pre-1.8-compatible': '1.7',
63 63 'pre-1.9-compatible': '1.8',
64 64 }
65 65
66 66 current_compatible_version = '1.14'
67 67
68 68
69 69 def reraise_safe_exceptions(func):
70 70 """Decorator for converting svn exceptions to something neutral."""
71 71 def wrapper(*args, **kwargs):
72 72 try:
73 73 return func(*args, **kwargs)
74 74 except Exception as e:
75 75 if not hasattr(e, '_vcs_kind'):
76 76 log.exception("Unhandled exception in svn remote call")
77 77 raise_from_original(exceptions.UnhandledException(e), e)
78 78 raise
79 79 return wrapper
80 80
81 81
82 82 class SubversionFactory(RepoFactory):
83 83 repo_type = 'svn'
84 84
85 85 def _create_repo(self, wire, create, compatible_version):
86 86 path = svn.core.svn_path_canonicalize(wire['path'])
87 87 if create:
88 88 fs_config = {'compatible-version': current_compatible_version}
89 89 if compatible_version:
90 90
91 91 compatible_version_string = \
92 92 svn_compatible_versions_map.get(compatible_version) \
93 93 or compatible_version
94 94 fs_config['compatible-version'] = compatible_version_string
95 95
96 96 log.debug('Create SVN repo with config `%s`', fs_config)
97 97 repo = svn.repos.create(path, "", "", None, fs_config)
98 98 else:
99 99 repo = svn.repos.open(path)
100 100
101 101 log.debug('repository created: got SVN object: %s', repo)
102 102 return repo
103 103
104 104 def repo(self, wire, create=False, compatible_version=None):
105 105 """
106 106 Get a repository instance for the given path.
107 107 """
108 108 return self._create_repo(wire, create, compatible_version)
109 109
110 110
111 111 NODE_TYPE_MAPPING = {
112 112 svn.core.svn_node_file: 'file',
113 113 svn.core.svn_node_dir: 'dir',
114 114 }
115 115
116 116
117 117 class SvnRemote(RemoteBase):
118 118
119 119 def __init__(self, factory, hg_factory=None):
120 120 self._factory = factory
121 121
122 122 self._bulk_methods = {
123 123 # NOT supported in SVN ATM...
124 124 }
125 125 self._bulk_file_methods = {
126 126 "size": self.get_file_size,
127 127 "data": self.get_file_content,
128 128 "flags": self.get_node_type,
129 129 "is_binary": self.is_binary,
130 130 "md5": self.md5_hash
131 131 }
132 132
133 133 @reraise_safe_exceptions
134 134 def bulk_file_request(self, wire, commit_id, path, pre_load):
135 135 cache_on, context_uid, repo_id = self._cache_on(wire)
136 136 region = self._region(wire)
137 137
138 138 # since we use unified API, we need to cast from str to in for SVN
139 139 commit_id = int(commit_id)
140 140
141 141 @region.conditional_cache_on_arguments(condition=cache_on)
142 142 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
143 143 result = {}
144 144 for attr in pre_load:
145 145 try:
146 146 method = self._bulk_file_methods[attr]
147 147 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
148 148 result[attr] = method(wire, _commit_id, _path)
149 149 except KeyError as e:
150 150 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
151 151 return result
152 152
153 153 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
154 154
155 155 @reraise_safe_exceptions
156 156 def discover_svn_version(self):
157 157 try:
158 158 import svn.core
159 159 svn_ver = svn.core.SVN_VERSION
160 160 except ImportError:
161 161 svn_ver = None
162 162 return safe_str(svn_ver)
163 163
164 164 @reraise_safe_exceptions
165 165 def is_empty(self, wire):
166 166 try:
167 167 return self.lookup(wire, -1) == 0
168 168 except Exception:
169 169 log.exception("failed to read object_store")
170 170 return False
171 171
172 172 def check_url(self, url, config):
173 173
174 174 # uuid function gets only valid UUID from proper repo, else
175 175 # throws exception
176 176 username, password, src_url = self.get_url_and_credentials(url)
177 177 try:
178 178 svnremoterepo(safe_bytes(username), safe_bytes(password), safe_bytes(src_url)).svn().uuid
179 179 except Exception:
180 180 tb = traceback.format_exc()
181 181 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
182 182 raise URLError(f'"{url}" is not a valid Subversion source url.')
183 183 return True
184 184
185 185 def is_path_valid_repository(self, wire, path):
186 186 # NOTE(marcink): short circuit the check for SVN repo
187 187 # the repos.open might be expensive to check, but we have one cheap
188 188 # pre-condition that we can use, to check for 'format' file
189 189 if not os.path.isfile(os.path.join(path, 'format')):
190 190 return False
191 191
192 192 cache_on, context_uid, repo_id = self._cache_on(wire)
193 193 region = self._region(wire)
194 194
195 195 @region.conditional_cache_on_arguments(condition=cache_on)
196 196 def _assert_correct_path(_context_uid, _repo_id, fast_check):
197 197
198 198 try:
199 199 svn.repos.open(path)
200 200 except svn.core.SubversionException:
201 201 tb = traceback.format_exc()
202 202 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
203 203 return False
204 204 return True
205 205
206 206 return _assert_correct_path(context_uid, repo_id, True)
207 207
208 208 @reraise_safe_exceptions
209 209 def verify(self, wire,):
210 210 repo_path = wire['path']
211 211 if not self.is_path_valid_repository(wire, repo_path):
212 212 raise Exception(
213 213 f"Path {repo_path} is not a valid Subversion repository.")
214 214
215 215 cmd = ['svnadmin', 'info', repo_path]
216 216 stdout, stderr = subprocessio.run_command(cmd)
217 217 return stdout
218 218
219 219 @reraise_safe_exceptions
220 220 def lookup(self, wire, revision):
221 221 if revision not in [-1, None, 'HEAD']:
222 222 raise NotImplementedError
223 223 repo = self._factory.repo(wire)
224 224 fs_ptr = svn.repos.fs(repo)
225 225 head = svn.fs.youngest_rev(fs_ptr)
226 226 return head
227 227
228 228 @reraise_safe_exceptions
229 229 def lookup_interval(self, wire, start_ts, end_ts):
230 230 repo = self._factory.repo(wire)
231 231 fsobj = svn.repos.fs(repo)
232 232 start_rev = None
233 233 end_rev = None
234 234 if start_ts:
235 235 start_ts_svn = apr_time_t(start_ts)
236 236 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
237 237 else:
238 238 start_rev = 1
239 239 if end_ts:
240 240 end_ts_svn = apr_time_t(end_ts)
241 241 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
242 242 else:
243 243 end_rev = svn.fs.youngest_rev(fsobj)
244 244 return start_rev, end_rev
245 245
246 246 @reraise_safe_exceptions
247 247 def revision_properties(self, wire, revision):
248 248
249 249 cache_on, context_uid, repo_id = self._cache_on(wire)
250 250 region = self._region(wire)
251 251
252 252 @region.conditional_cache_on_arguments(condition=cache_on)
253 253 def _revision_properties(_repo_id, _revision):
254 254 repo = self._factory.repo(wire)
255 255 fs_ptr = svn.repos.fs(repo)
256 256 return svn.fs.revision_proplist(fs_ptr, revision)
257 257 return _revision_properties(repo_id, revision)
258 258
259 259 def revision_changes(self, wire, revision):
260 260
261 261 repo = self._factory.repo(wire)
262 262 fsobj = svn.repos.fs(repo)
263 263 rev_root = svn.fs.revision_root(fsobj, revision)
264 264
265 265 editor = svn.repos.ChangeCollector(fsobj, rev_root)
266 266 editor_ptr, editor_baton = svn.delta.make_editor(editor)
267 267 base_dir = ""
268 268 send_deltas = False
269 269 svn.repos.replay2(
270 270 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
271 271 editor_ptr, editor_baton, None)
272 272
273 273 added = []
274 274 changed = []
275 275 removed = []
276 276
277 277 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
278 278 for path, change in editor.changes.items():
279 279 # TODO: Decide what to do with directory nodes. Subversion can add
280 280 # empty directories.
281 281
282 282 if change.item_kind == svn.core.svn_node_dir:
283 283 continue
284 284 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
285 285 added.append(path)
286 286 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
287 287 svn.repos.CHANGE_ACTION_REPLACE]:
288 288 changed.append(path)
289 289 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
290 290 removed.append(path)
291 291 else:
292 292 raise NotImplementedError(
293 293 "Action {} not supported on path {}".format(
294 294 change.action, path))
295 295
296 296 changes = {
297 297 'added': added,
298 298 'changed': changed,
299 299 'removed': removed,
300 300 }
301 301 return changes
302 302
303 303 @reraise_safe_exceptions
304 304 def node_history(self, wire, path, revision, limit):
305 305 cache_on, context_uid, repo_id = self._cache_on(wire)
306 306 region = self._region(wire)
307 307
308 308 @region.conditional_cache_on_arguments(condition=cache_on)
309 309 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
310 310 cross_copies = False
311 311 repo = self._factory.repo(wire)
312 312 fsobj = svn.repos.fs(repo)
313 313 rev_root = svn.fs.revision_root(fsobj, revision)
314 314
315 315 history_revisions = []
316 316 history = svn.fs.node_history(rev_root, path)
317 317 history = svn.fs.history_prev(history, cross_copies)
318 318 while history:
319 319 __, node_revision = svn.fs.history_location(history)
320 320 history_revisions.append(node_revision)
321 321 if limit and len(history_revisions) >= limit:
322 322 break
323 323 history = svn.fs.history_prev(history, cross_copies)
324 324 return history_revisions
325 325 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
326 326
327 327 @reraise_safe_exceptions
328 328 def node_properties(self, wire, path, revision):
329 329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 330 region = self._region(wire)
331 331
332 332 @region.conditional_cache_on_arguments(condition=cache_on)
333 333 def _node_properties(_repo_id, _path, _revision):
334 334 repo = self._factory.repo(wire)
335 335 fsobj = svn.repos.fs(repo)
336 336 rev_root = svn.fs.revision_root(fsobj, revision)
337 337 return svn.fs.node_proplist(rev_root, path)
338 338 return _node_properties(repo_id, path, revision)
339 339
340 340 def file_annotate(self, wire, path, revision):
341 341 abs_path = 'file://' + urllib.request.pathname2url(
342 342 vcspath.join(wire['path'], path))
343 343 file_uri = svn.core.svn_path_canonicalize(abs_path)
344 344
345 345 start_rev = svn_opt_revision_value_t(0)
346 346 peg_rev = svn_opt_revision_value_t(revision)
347 347 end_rev = peg_rev
348 348
349 349 annotations = []
350 350
351 351 def receiver(line_no, revision, author, date, line, pool):
352 352 annotations.append((line_no, revision, line))
353 353
354 354 # TODO: Cannot use blame5, missing typemap function in the swig code
355 355 try:
356 356 svn.client.blame2(
357 357 file_uri, peg_rev, start_rev, end_rev,
358 358 receiver, svn.client.create_context())
359 359 except svn.core.SubversionException as exc:
360 360 log.exception("Error during blame operation.")
361 361 raise Exception(
362 362 f"Blame not supported or file does not exist at path {path}. "
363 363 f"Error {exc}.")
364 364
365 365 return BinaryEnvelope(annotations)
366 366
367 367 @reraise_safe_exceptions
368 368 def get_node_type(self, wire, revision=None, path=''):
369 369
370 370 cache_on, context_uid, repo_id = self._cache_on(wire)
371 371 region = self._region(wire)
372 372
373 373 @region.conditional_cache_on_arguments(condition=cache_on)
374 374 def _get_node_type(_repo_id, _revision, _path):
375 375 repo = self._factory.repo(wire)
376 376 fs_ptr = svn.repos.fs(repo)
377 377 if _revision is None:
378 378 _revision = svn.fs.youngest_rev(fs_ptr)
379 379 root = svn.fs.revision_root(fs_ptr, _revision)
380 380 node = svn.fs.check_path(root, path)
381 381 return NODE_TYPE_MAPPING.get(node, None)
382 382 return _get_node_type(repo_id, revision, path)
383 383
384 384 @reraise_safe_exceptions
385 385 def get_nodes(self, wire, revision=None, path=''):
386 386
387 387 cache_on, context_uid, repo_id = self._cache_on(wire)
388 388 region = self._region(wire)
389 389
390 390 @region.conditional_cache_on_arguments(condition=cache_on)
391 391 def _get_nodes(_repo_id, _path, _revision):
392 392 repo = self._factory.repo(wire)
393 393 fsobj = svn.repos.fs(repo)
394 394 if _revision is None:
395 395 _revision = svn.fs.youngest_rev(fsobj)
396 396 root = svn.fs.revision_root(fsobj, _revision)
397 397 entries = svn.fs.dir_entries(root, path)
398 398 result = []
399 399 for entry_path, entry_info in entries.items():
400 400 result.append(
401 401 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
402 402 return result
403 403 return _get_nodes(repo_id, path, revision)
404 404
405 405 @reraise_safe_exceptions
406 406 def get_file_content(self, wire, rev=None, path=''):
407 407 repo = self._factory.repo(wire)
408 408 fsobj = svn.repos.fs(repo)
409 409
410 410 if rev is None:
411 411 rev = svn.fs.youngest_rev(fsobj)
412 412
413 413 root = svn.fs.revision_root(fsobj, rev)
414 414 content = svn.core.Stream(svn.fs.file_contents(root, path))
415 415 return BytesEnvelope(content.read())
416 416
417 417 @reraise_safe_exceptions
418 418 def get_file_size(self, wire, revision=None, path=''):
419 419
420 420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 421 region = self._region(wire)
422 422
423 423 @region.conditional_cache_on_arguments(condition=cache_on)
424 424 def _get_file_size(_repo_id, _revision, _path):
425 425 repo = self._factory.repo(wire)
426 426 fsobj = svn.repos.fs(repo)
427 427 if _revision is None:
428 428 _revision = svn.fs.youngest_revision(fsobj)
429 429 root = svn.fs.revision_root(fsobj, _revision)
430 430 size = svn.fs.file_length(root, path)
431 431 return size
432 432 return _get_file_size(repo_id, revision, path)
433 433
434 434 def create_repository(self, wire, compatible_version=None):
435 435 log.info('Creating Subversion repository in path "%s"', wire['path'])
436 436 self._factory.repo(wire, create=True,
437 437 compatible_version=compatible_version)
438 438
439 439 def get_url_and_credentials(self, src_url) -> tuple[str, str, str]:
440 440 obj = urllib.parse.urlparse(src_url)
441 441 username = obj.username or ''
442 442 password = obj.password or ''
443 443 return username, password, src_url
444 444
445 445 def import_remote_repository(self, wire, src_url):
446 446 repo_path = wire['path']
447 447 if not self.is_path_valid_repository(wire, repo_path):
448 448 raise Exception(
449 449 f"Path {repo_path} is not a valid Subversion repository.")
450 450
451 451 username, password, src_url = self.get_url_and_credentials(src_url)
452 452 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
453 453 '--trust-server-cert-failures=unknown-ca']
454 454 if username and password:
455 455 rdump_cmd += ['--username', username, '--password', password]
456 456 rdump_cmd += [src_url]
457 457
458 458 rdump = subprocess.Popen(
459 459 rdump_cmd,
460 460 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
461 461 load = subprocess.Popen(
462 462 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
463 463
464 464 # TODO: johbo: This can be a very long operation, might be better
465 465 # to track some kind of status and provide an api to check if the
466 466 # import is done.
467 467 rdump.wait()
468 468 load.wait()
469 469
470 470 log.debug('Return process ended with code: %s', rdump.returncode)
471 471 if rdump.returncode != 0:
472 472 errors = rdump.stderr.read()
473 473 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
474 474
475 475 reason = 'UNKNOWN'
476 476 if b'svnrdump: E230001:' in errors:
477 477 reason = 'INVALID_CERTIFICATE'
478 478
479 479 if reason == 'UNKNOWN':
480 480 reason = f'UNKNOWN:{safe_str(errors)}'
481 481
482 482 raise Exception(
483 483 'Failed to dump the remote repository from {}. Reason:{}'.format(
484 484 src_url, reason))
485 485 if load.returncode != 0:
486 486 raise Exception(
487 487 f'Failed to load the dump of remote repository from {src_url}.')
488 488
489 489 def commit(self, wire, message, author, timestamp, updated, removed):
490 490
491 491 message = safe_bytes(message)
492 492 author = safe_bytes(author)
493 493
494 494 repo = self._factory.repo(wire)
495 495 fsobj = svn.repos.fs(repo)
496 496
497 497 rev = svn.fs.youngest_rev(fsobj)
498 498 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
499 499 txn_root = svn.fs.txn_root(txn)
500 500
501 501 for node in updated:
502 502 TxnNodeProcessor(node, txn_root).update()
503 503 for node in removed:
504 504 TxnNodeProcessor(node, txn_root).remove()
505 505
506 svn_txn_id = safe_str(svn.fs.svn_fs_txn_name(txn))
507 full_repo_path = wire['path']
508 txn_id_data = {'svn_txn_id': svn_txn_id, 'rc_internal_commit': True}
509
510 store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
506 511 commit_id = svn.repos.fs_commit_txn(repo, txn)
507 512
508 513 if timestamp:
509 514 apr_time = apr_time_t(timestamp)
510 515 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
511 516 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
512 517
513 518 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
514 519 return commit_id
515 520
516 521 @reraise_safe_exceptions
517 522 def diff(self, wire, rev1, rev2, path1=None, path2=None,
518 523 ignore_whitespace=False, context=3):
519 524
520 525 wire.update(cache=False)
521 526 repo = self._factory.repo(wire)
522 527 diff_creator = SvnDiffer(
523 528 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
524 529 try:
525 530 return BytesEnvelope(diff_creator.generate_diff())
526 531 except svn.core.SubversionException as e:
527 532 log.exception(
528 533 "Error during diff operation operation. "
529 534 "Path might not exist %s, %s", path1, path2)
530 535 return BytesEnvelope(b'')
531 536
532 537 @reraise_safe_exceptions
533 538 def is_large_file(self, wire, path):
534 539 return False
535 540
536 541 @reraise_safe_exceptions
537 542 def is_binary(self, wire, rev, path):
538 543 cache_on, context_uid, repo_id = self._cache_on(wire)
539 544 region = self._region(wire)
540 545
541 546 @region.conditional_cache_on_arguments(condition=cache_on)
542 547 def _is_binary(_repo_id, _rev, _path):
543 548 raw_bytes = self.get_file_content(wire, rev, path)
544 549 if not raw_bytes:
545 550 return False
546 551 return b'\0' in raw_bytes
547 552
548 553 return _is_binary(repo_id, rev, path)
549 554
550 555 @reraise_safe_exceptions
551 556 def md5_hash(self, wire, rev, path):
552 557 cache_on, context_uid, repo_id = self._cache_on(wire)
553 558 region = self._region(wire)
554 559
555 560 @region.conditional_cache_on_arguments(condition=cache_on)
556 561 def _md5_hash(_repo_id, _rev, _path):
557 562 return ''
558 563
559 564 return _md5_hash(repo_id, rev, path)
560 565
561 566 @reraise_safe_exceptions
562 567 def run_svn_command(self, wire, cmd, **opts):
563 568 path = wire.get('path', None)
564 569 debug_mode = rhodecode.ConfigGet().get_bool('debug')
565 570
566 571 if path and os.path.isdir(path):
567 572 opts['cwd'] = path
568 573
569 574 safe_call = opts.pop('_safe', False)
570 575
571 576 svnenv = os.environ.copy()
572 577 svnenv.update(opts.pop('extra_env', {}))
573 578
574 579 _opts = {'env': svnenv, 'shell': False}
575 580
576 581 try:
577 582 _opts.update(opts)
578 583 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
579 584
580 585 return b''.join(proc), b''.join(proc.stderr)
581 586 except OSError as err:
582 587 if safe_call:
583 588 return '', safe_str(err).strip()
584 589 else:
585 590 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
586 591 call_opts = {}
587 592 if debug_mode:
588 593 call_opts = _opts
589 594
590 595 tb_err = ("Couldn't run svn command ({}).\n"
591 596 "Original error was:{}\n"
592 597 "Call options:{}\n"
593 598 .format(cmd, err, call_opts))
594 599 log.exception(tb_err)
595 600 raise exceptions.VcsException()(tb_err)
596 601
597 602 @reraise_safe_exceptions
598 603 def install_hooks(self, wire, force=False):
599 604 from vcsserver.hook_utils import install_svn_hooks
600 605 repo_path = wire['path']
601 606 binary_dir = settings.BINARY_DIR
602 607 executable = None
603 608 if binary_dir:
604 609 executable = os.path.join(binary_dir, 'python3')
605 610 return install_svn_hooks(repo_path, force_create=force)
606 611
607 612 @reraise_safe_exceptions
608 613 def get_hooks_info(self, wire):
609 614 from vcsserver.hook_utils import (
610 615 get_svn_pre_hook_version, get_svn_post_hook_version)
611 616 repo_path = wire['path']
612 617 return {
613 618 'pre_version': get_svn_pre_hook_version(repo_path),
614 619 'post_version': get_svn_post_hook_version(repo_path),
615 620 }
616 621
617 622 @reraise_safe_exceptions
618 623 def set_head_ref(self, wire, head_name):
619 624 pass
620 625
621 626 @reraise_safe_exceptions
622 627 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
623 628 archive_dir_name, commit_id, cache_config):
624 629
625 630 def walk_tree(root, root_dir, _commit_id):
626 631 """
627 632 Special recursive svn repo walker
628 633 """
629 634 root_dir = safe_bytes(root_dir)
630 635
631 636 filemode_default = 0o100644
632 637 filemode_executable = 0o100755
633 638
634 639 file_iter = svn.fs.dir_entries(root, root_dir)
635 640 for f_name in file_iter:
636 641 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
637 642
638 643 if f_type == 'dir':
639 644 # return only DIR, and then all entries in that dir
640 645 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
641 646 new_root = os.path.join(root_dir, f_name)
642 647 yield from walk_tree(root, new_root, _commit_id)
643 648 else:
644 649
645 650 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
646 651 prop_list = svn.fs.node_proplist(root, f_path)
647 652
648 653 f_mode = filemode_default
649 654 if prop_list.get('svn:executable'):
650 655 f_mode = filemode_executable
651 656
652 657 f_is_link = False
653 658 if prop_list.get('svn:special'):
654 659 f_is_link = True
655 660
656 661 data = {
657 662 'is_link': f_is_link,
658 663 'mode': f_mode,
659 664 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
660 665 }
661 666
662 667 yield f_path, data, f_type
663 668
664 669 def file_walker(_commit_id, path):
665 670 repo = self._factory.repo(wire)
666 671 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
667 672
668 673 def no_content():
669 674 raise NoContentException()
670 675
671 676 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
672 677 file_path = f_name
673 678
674 679 if f_type == 'dir':
675 680 mode = f_data['mode']
676 681 yield ArchiveNode(file_path, mode, False, no_content)
677 682 else:
678 683 mode = f_data['mode']
679 684 is_link = f_data['is_link']
680 685 data_stream = f_data['content_stream']
681 686 yield ArchiveNode(file_path, mode, is_link, data_stream)
682 687
683 688 return store_archive_in_cache(
684 689 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
685 690
686 691
687 692 class SvnDiffer:
688 693 """
689 694 Utility to create diffs based on difflib and the Subversion api
690 695 """
691 696
692 697 binary_content = False
693 698
694 699 def __init__(
695 700 self, repo, src_rev, src_path, tgt_rev, tgt_path,
696 701 ignore_whitespace, context):
697 702 self.repo = repo
698 703 self.ignore_whitespace = ignore_whitespace
699 704 self.context = context
700 705
701 706 fsobj = svn.repos.fs(repo)
702 707
703 708 self.tgt_rev = tgt_rev
704 709 self.tgt_path = tgt_path or ''
705 710 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
706 711 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
707 712
708 713 self.src_rev = src_rev
709 714 self.src_path = src_path or self.tgt_path
710 715 self.src_root = svn.fs.revision_root(fsobj, src_rev)
711 716 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
712 717
713 718 self._validate()
714 719
715 720 def _validate(self):
716 721 if (self.tgt_kind != svn.core.svn_node_none and
717 722 self.src_kind != svn.core.svn_node_none and
718 723 self.src_kind != self.tgt_kind):
719 724 # TODO: johbo: proper error handling
720 725 raise Exception(
721 726 "Source and target are not compatible for diff generation. "
722 727 "Source type: %s, target type: %s" %
723 728 (self.src_kind, self.tgt_kind))
724 729
725 730 def generate_diff(self) -> bytes:
726 731 buf = io.BytesIO()
727 732 if self.tgt_kind == svn.core.svn_node_dir:
728 733 self._generate_dir_diff(buf)
729 734 else:
730 735 self._generate_file_diff(buf)
731 736 return buf.getvalue()
732 737
733 738 def _generate_dir_diff(self, buf: io.BytesIO):
734 739 editor = DiffChangeEditor()
735 740 editor_ptr, editor_baton = svn.delta.make_editor(editor)
736 741 svn.repos.dir_delta2(
737 742 self.src_root,
738 743 self.src_path,
739 744 '', # src_entry
740 745 self.tgt_root,
741 746 self.tgt_path,
742 747 editor_ptr, editor_baton,
743 748 authorization_callback_allow_all,
744 749 False, # text_deltas
745 750 svn.core.svn_depth_infinity, # depth
746 751 False, # entry_props
747 752 False, # ignore_ancestry
748 753 )
749 754
750 755 for path, __, change in sorted(editor.changes):
751 756 self._generate_node_diff(
752 757 buf, change, path, self.tgt_path, path, self.src_path)
753 758
754 759 def _generate_file_diff(self, buf: io.BytesIO):
755 760 change = None
756 761 if self.src_kind == svn.core.svn_node_none:
757 762 change = "add"
758 763 elif self.tgt_kind == svn.core.svn_node_none:
759 764 change = "delete"
760 765 tgt_base, tgt_path = vcspath.split(self.tgt_path)
761 766 src_base, src_path = vcspath.split(self.src_path)
762 767 self._generate_node_diff(
763 768 buf, change, tgt_path, tgt_base, src_path, src_base)
764 769
765 770 def _generate_node_diff(
766 771 self, buf: io.BytesIO, change, tgt_path, tgt_base, src_path, src_base):
767 772
768 773 tgt_path_bytes = safe_bytes(tgt_path)
769 774 tgt_path = safe_str(tgt_path)
770 775
771 776 src_path_bytes = safe_bytes(src_path)
772 777 src_path = safe_str(src_path)
773 778
774 779 if self.src_rev == self.tgt_rev and tgt_base == src_base:
775 780 # makes consistent behaviour with git/hg to return empty diff if
776 781 # we compare same revisions
777 782 return
778 783
779 784 tgt_full_path = vcspath.join(tgt_base, tgt_path)
780 785 src_full_path = vcspath.join(src_base, src_path)
781 786
782 787 self.binary_content = False
783 788 mime_type = self._get_mime_type(tgt_full_path)
784 789
785 790 if mime_type and not mime_type.startswith(b'text'):
786 791 self.binary_content = True
787 792 buf.write(b"=" * 67 + b'\n')
788 793 buf.write(b"Cannot display: file marked as a binary type.\n")
789 794 buf.write(b"svn:mime-type = %s\n" % mime_type)
790 795 buf.write(b"Index: %b\n" % tgt_path_bytes)
791 796 buf.write(b"=" * 67 + b'\n')
792 797 buf.write(b"diff --git a/%b b/%b\n" % (tgt_path_bytes, tgt_path_bytes))
793 798
794 799 if change == 'add':
795 800 # TODO: johbo: SVN is missing a zero here compared to git
796 801 buf.write(b"new file mode 10644\n")
797 802
798 803 # TODO(marcink): intro to binary detection of svn patches
799 804 # if self.binary_content:
800 805 # buf.write(b'GIT binary patch\n')
801 806
802 807 buf.write(b"--- /dev/null\t(revision 0)\n")
803 808 src_lines = []
804 809 else:
805 810 if change == 'delete':
806 811 buf.write(b"deleted file mode 10644\n")
807 812
808 813 # TODO(marcink): intro to binary detection of svn patches
809 814 # if self.binary_content:
810 815 # buf.write('GIT binary patch\n')
811 816
812 817 buf.write(b"--- a/%b\t(revision %d)\n" % (src_path_bytes, self.src_rev))
813 818 src_lines = self._svn_readlines(self.src_root, src_full_path)
814 819
815 820 if change == 'delete':
816 821 buf.write(b"+++ /dev/null\t(revision %d)\n" % self.tgt_rev)
817 822 tgt_lines = []
818 823 else:
819 824 buf.write(b"+++ b/%b\t(revision %d)\n" % (tgt_path_bytes, self.tgt_rev))
820 825 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
821 826
822 827 # we made our diff header, time to generate the diff content into our buffer
823 828
824 829 if not self.binary_content:
825 830 udiff = svn_diff.unified_diff(
826 831 src_lines, tgt_lines, context=self.context,
827 832 ignore_blank_lines=self.ignore_whitespace,
828 833 ignore_case=False,
829 834 ignore_space_changes=self.ignore_whitespace)
830 835
831 836 buf.writelines(udiff)
832 837
833 838 def _get_mime_type(self, path) -> bytes:
834 839 try:
835 840 mime_type = svn.fs.node_prop(
836 841 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
837 842 except svn.core.SubversionException:
838 843 mime_type = svn.fs.node_prop(
839 844 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
840 845 return mime_type
841 846
842 847 def _svn_readlines(self, fs_root, node_path):
843 848 if self.binary_content:
844 849 return []
845 850 node_kind = svn.fs.check_path(fs_root, node_path)
846 851 if node_kind not in (
847 852 svn.core.svn_node_file, svn.core.svn_node_symlink):
848 853 return []
849 854 content = svn.core.Stream(
850 855 svn.fs.file_contents(fs_root, node_path)).read()
851 856
852 857 return content.splitlines(True)
853 858
854 859
855 860 class DiffChangeEditor(svn.delta.Editor):
856 861 """
857 862 Records changes between two given revisions
858 863 """
859 864
860 865 def __init__(self):
861 866 self.changes = []
862 867
863 868 def delete_entry(self, path, revision, parent_baton, pool=None):
864 869 self.changes.append((path, None, 'delete'))
865 870
866 871 def add_file(
867 872 self, path, parent_baton, copyfrom_path, copyfrom_revision,
868 873 file_pool=None):
869 874 self.changes.append((path, 'file', 'add'))
870 875
871 876 def open_file(self, path, parent_baton, base_revision, file_pool=None):
872 877 self.changes.append((path, 'file', 'change'))
873 878
874 879
875 880 def authorization_callback_allow_all(root, path, pool):
876 881 return True
877 882
878 883
879 884 class TxnNodeProcessor:
880 885 """
881 886 Utility to process the change of one node within a transaction root.
882 887
883 888 It encapsulates the knowledge of how to add, update or remove
884 889 a node for a given transaction root. The purpose is to support the method
885 890 `SvnRemote.commit`.
886 891 """
887 892
888 893 def __init__(self, node, txn_root):
889 894 assert_bytes(node['path'])
890 895
891 896 self.node = node
892 897 self.txn_root = txn_root
893 898
894 899 def update(self):
895 900 self._ensure_parent_dirs()
896 901 self._add_file_if_node_does_not_exist()
897 902 self._update_file_content()
898 903 self._update_file_properties()
899 904
900 905 def remove(self):
901 906 svn.fs.delete(self.txn_root, self.node['path'])
902 907 # TODO: Clean up directory if empty
903 908
904 909 def _ensure_parent_dirs(self):
905 910 curdir = vcspath.dirname(self.node['path'])
906 911 dirs_to_create = []
907 912 while not self._svn_path_exists(curdir):
908 913 dirs_to_create.append(curdir)
909 914 curdir = vcspath.dirname(curdir)
910 915
911 916 for curdir in reversed(dirs_to_create):
912 917 log.debug('Creating missing directory "%s"', curdir)
913 918 svn.fs.make_dir(self.txn_root, curdir)
914 919
915 920 def _svn_path_exists(self, path):
916 921 path_status = svn.fs.check_path(self.txn_root, path)
917 922 return path_status != svn.core.svn_node_none
918 923
919 924 def _add_file_if_node_does_not_exist(self):
920 925 kind = svn.fs.check_path(self.txn_root, self.node['path'])
921 926 if kind == svn.core.svn_node_none:
922 927 svn.fs.make_file(self.txn_root, self.node['path'])
923 928
924 929 def _update_file_content(self):
925 930 assert_bytes(self.node['content'])
926 931
927 932 handler, baton = svn.fs.apply_textdelta(
928 933 self.txn_root, self.node['path'], None, None)
929 934 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
930 935
931 936 def _update_file_properties(self):
932 937 properties = self.node.get('properties', {})
933 938 for key, value in properties.items():
934 939 svn.fs.change_node_prop(
935 940 self.txn_root, self.node['path'], safe_bytes(key), safe_bytes(value))
936 941
937 942
938 943 def apr_time_t(timestamp):
939 944 """
940 945 Convert a Python timestamp into APR timestamp type apr_time_t
941 946 """
942 947 return int(timestamp * 1E6)
943 948
944 949
945 950 def svn_opt_revision_value_t(num):
946 951 """
947 952 Put `num` into a `svn_opt_revision_value_t` structure.
948 953 """
949 954 value = svn.core.svn_opt_revision_value_t()
950 955 value.number = num
951 956 revision = svn.core.svn_opt_revision_t()
952 957 revision.kind = svn.core.svn_opt_revision_number
953 958 revision.value = value
954 959 return revision
General Comments 0
You need to be logged in to leave comments. Login now