##// END OF EJS Templates
scm: added md5 methods to be calculated on vcsserver instead of RhodeCode side
super-admin -
r1074:ccf62cdf python3
parent child Browse files
Show More
@@ -1,208 +1,207 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 os
19 19 import time
20 20 import logging
21 21 import functools
22 22 import decorator
23 23
24 24 from dogpile.cache import CacheRegion
25 25
26 26 from vcsserver.str_utils import safe_bytes
27 27 from vcsserver.utils import sha1
28 28 from vcsserver.lib.rc_cache import region_meta
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RhodeCodeCacheRegion(CacheRegion):
34 34
35 35 def conditional_cache_on_arguments(
36 36 self, namespace=None,
37 37 expiration_time=None,
38 38 should_cache_fn=None,
39 39 to_str=str,
40 40 function_key_generator=None,
41 41 condition=True):
42 42 """
43 43 Custom conditional decorator, that will not touch any dogpile internals if
44 44 condition isn't meet. This works a bit different than should_cache_fn
45 45 And it's faster in cases we don't ever want to compute cached values
46 46 """
47 47 expiration_time_is_callable = callable(expiration_time)
48 48
49 49 if function_key_generator is None:
50 50 function_key_generator = self.function_key_generator
51 51
52 52 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
53 53
54 54 if not condition:
55 55 log.debug('Calling un-cached method:%s', user_func.__name__)
56 56 start = time.time()
57 57 result = user_func(*arg, **kw)
58 58 total = time.time() - start
59 59 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
60 60 return result
61 61
62 62 key = key_generator(*arg, **kw)
63 63
64 64 timeout = expiration_time() if expiration_time_is_callable \
65 65 else expiration_time
66 66
67 67 log.debug('Calling cached method:`%s`', user_func.__name__)
68 68 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
69 69
70 70 def cache_decorator(user_func):
71 71 if to_str is str:
72 72 # backwards compatible
73 73 key_generator = function_key_generator(namespace, user_func)
74 74 else:
75 75 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
76 76
77 77 def refresh(*arg, **kw):
78 78 """
79 79 Like invalidate, but regenerates the value instead
80 80 """
81 81 key = key_generator(*arg, **kw)
82 82 value = user_func(*arg, **kw)
83 83 self.set(key, value)
84 84 return value
85 85
86 86 def invalidate(*arg, **kw):
87 87 key = key_generator(*arg, **kw)
88 88 self.delete(key)
89 89
90 90 def set_(value, *arg, **kw):
91 91 key = key_generator(*arg, **kw)
92 92 self.set(key, value)
93 93
94 94 def get(*arg, **kw):
95 95 key = key_generator(*arg, **kw)
96 96 return self.get(key)
97 97
98 98 user_func.set = set_
99 99 user_func.invalidate = invalidate
100 100 user_func.get = get
101 101 user_func.refresh = refresh
102 102 user_func.key_generator = key_generator
103 103 user_func.original = user_func
104 104
105 105 # Use `decorate` to preserve the signature of :param:`user_func`.
106 106 return decorator.decorate(user_func, functools.partial(
107 107 get_or_create_for_user_func, key_generator))
108 108
109 109 return cache_decorator
110 110
111 111
112 112 def make_region(*arg, **kw):
113 113 return RhodeCodeCacheRegion(*arg, **kw)
114 114
115 115
116 116 def get_default_cache_settings(settings, prefixes=None):
117 117 prefixes = prefixes or []
118 118 cache_settings = {}
119 119 for key in settings.keys():
120 120 for prefix in prefixes:
121 121 if key.startswith(prefix):
122 122 name = key.split(prefix)[1].strip()
123 123 val = settings[key]
124 124 if isinstance(val, str):
125 125 val = val.strip()
126 126 cache_settings[name] = val
127 127 return cache_settings
128 128
129 129
130 130 def compute_key_from_params(*args):
131 131 """
132 132 Helper to compute key from given params to be used in cache manager
133 133 """
134 134 return sha1(safe_bytes("_".join(map(str, args))))
135 135
136 136
137 137 def backend_key_generator(backend):
138 138 """
139 139 Special wrapper that also sends over the backend to the key generator
140 140 """
141 141 def wrapper(namespace, fn):
142 142 return key_generator(backend, namespace, fn)
143 143 return wrapper
144 144
145 145
146 146 def key_generator(backend, namespace, fn):
147 147 fname = fn.__name__
148 148
149 149 def generate_key(*args):
150 150 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
151 151 namespace_pref = namespace or 'default_namespace'
152 152 arg_key = compute_key_from_params(*args)
153 153 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
154 154
155 155 return final_key
156 156
157 157 return generate_key
158 158
159 159
160 160 def get_or_create_region(region_name, region_namespace=None):
161 161 from vcsserver.lib.rc_cache.backends import FileNamespaceBackend
162 162 region_obj = region_meta.dogpile_cache_regions.get(region_name)
163 163 if not region_obj:
164 raise EnvironmentError(
165 'Region `{}` not in configured: {}.'.format(
166 region_name, list(region_meta.dogpile_cache_regions.keys())))
164 reg_keys = list(region_meta.dogpile_cache_regions.keys())
165 raise EnvironmentError(f'Region `{region_name}` not in configured: {reg_keys}.')
167 166
168 region_uid_name = '{}:{}'.format(region_name, region_namespace)
167 region_uid_name = f'{region_name}:{region_namespace}'
169 168 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
170 169 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
171 170 if region_exist:
172 171 log.debug('Using already configured region: %s', region_namespace)
173 172 return region_exist
174 173 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
175 174 expiration_time = region_obj.expiration_time
176 175
177 176 if not os.path.isdir(cache_dir):
178 177 os.makedirs(cache_dir)
179 178 new_region = make_region(
180 179 name=region_uid_name,
181 180 function_key_generator=backend_key_generator(region_obj.actual_backend)
182 181 )
183 182 namespace_filename = os.path.join(
184 cache_dir, "{}.cache.dbm".format(region_namespace))
183 cache_dir, f"{region_namespace}.cache.dbm")
185 184 # special type that allows 1db per namespace
186 185 new_region.configure(
187 186 backend='dogpile.cache.rc.file_namespace',
188 187 expiration_time=expiration_time,
189 188 arguments={"filename": namespace_filename}
190 189 )
191 190
192 191 # create and save in region caches
193 192 log.debug('configuring new region: %s', region_uid_name)
194 193 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
195 194
196 195 return region_obj
197 196
198 197
199 198 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
200 199 region = get_or_create_region(cache_region, cache_namespace_uid)
201 200 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
202 201 num_delete_keys = len(cache_keys)
203 202 if invalidate:
204 203 region.invalidate(hard=False)
205 204 else:
206 205 if num_delete_keys:
207 206 region.delete_multi(cache_keys)
208 207 return num_delete_keys
@@ -1,1332 +1,1343 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 collections
19 19 import logging
20 20 import os
21 21 import posixpath as vcspath
22 22 import re
23 23 import stat
24 24 import traceback
25 25 import urllib.request, urllib.parse, urllib.error
26 26 import urllib.request, urllib.error, urllib.parse
27 27 from functools import wraps
28 28
29 29 import more_itertools
30 30 import pygit2
31 31 from pygit2 import Repository as LibGit2Repo
32 32 from pygit2 import index as LibGit2Index
33 33 from dulwich import index, objects
34 34 from dulwich.client import HttpGitClient, LocalGitClient
35 35 from dulwich.errors import (
36 36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 37 MissingCommitError, ObjectMissing, HangupException,
38 38 UnexpectedCommandError)
39 39 from dulwich.repo import Repo as DulwichRepo
40 40 from dulwich.server import update_server_info
41 41
42 42 from vcsserver import exceptions, settings, subprocessio
43 43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes
44 44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
45 45 from vcsserver.hgcompat import (
46 46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 47 from vcsserver.git_lfs.lib import LFSOidStore
48 48 from vcsserver.vcs_base import RemoteBase
49 49
50 50 DIR_STAT = stat.S_IFDIR
51 51 FILE_MODE = stat.S_IFMT
52 52 GIT_LINK = objects.S_IFGITLINK
53 53 PEELED_REF_MARKER = b'^{}'
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def reraise_safe_exceptions(func):
60 60 """Converts Dulwich exceptions to something neutral."""
61 61
62 62 @wraps(func)
63 63 def wrapper(*args, **kwargs):
64 64 try:
65 65 return func(*args, **kwargs)
66 66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 67 exc = exceptions.LookupException(org_exc=e)
68 68 raise exc(safe_str(e))
69 69 except (HangupException, UnexpectedCommandError) as e:
70 70 exc = exceptions.VcsException(org_exc=e)
71 71 raise exc(safe_str(e))
72 72 except Exception as e:
73 73 # NOTE(marcink): becuase of how dulwich handles some exceptions
74 74 # (KeyError on empty repos), we cannot track this and catch all
75 75 # exceptions, it's an exceptions from other handlers
76 76 #if not hasattr(e, '_vcs_kind'):
77 77 #log.exception("Unhandled exception in git remote call")
78 78 #raise_from_original(exceptions.UnhandledException)
79 79 raise
80 80 return wrapper
81 81
82 82
83 83 class Repo(DulwichRepo):
84 84 """
85 85 A wrapper for dulwich Repo class.
86 86
87 87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 88 "Too many open files" error. We need to close all opened file descriptors
89 89 once the repo object is destroyed.
90 90 """
91 91 def __del__(self):
92 92 if hasattr(self, 'object_store'):
93 93 self.close()
94 94
95 95
96 96 class Repository(LibGit2Repo):
97 97
98 98 def __enter__(self):
99 99 return self
100 100
101 101 def __exit__(self, exc_type, exc_val, exc_tb):
102 102 self.free()
103 103
104 104
105 105 class GitFactory(RepoFactory):
106 106 repo_type = 'git'
107 107
108 108 def _create_repo(self, wire, create, use_libgit2=False):
109 109 if use_libgit2:
110 110 return Repository(wire['path'])
111 111 else:
112 112 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 113 return Repo(repo_path)
114 114
115 115 def repo(self, wire, create=False, use_libgit2=False):
116 116 """
117 117 Get a repository instance for the given path.
118 118 """
119 119 return self._create_repo(wire, create, use_libgit2)
120 120
121 121 def repo_libgit2(self, wire):
122 122 return self.repo(wire, use_libgit2=True)
123 123
124 124
125 125 class GitRemote(RemoteBase):
126 126
127 127 def __init__(self, factory):
128 128 self._factory = factory
129 129 self._bulk_methods = {
130 130 "date": self.date,
131 131 "author": self.author,
132 132 "branch": self.branch,
133 133 "message": self.message,
134 134 "parents": self.parents,
135 135 "_commit": self.revision,
136 136 }
137 137
138 138 def _wire_to_config(self, wire):
139 139 if 'config' in wire:
140 140 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
141 141 return {}
142 142
143 143 def _remote_conf(self, config):
144 144 params = [
145 145 '-c', 'core.askpass=""',
146 146 ]
147 147 ssl_cert_dir = config.get('vcs_ssl_dir')
148 148 if ssl_cert_dir:
149 149 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
150 150 return params
151 151
152 152 @reraise_safe_exceptions
153 153 def discover_git_version(self):
154 154 stdout, _ = self.run_git_command(
155 155 {}, ['--version'], _bare=True, _safe=True)
156 156 prefix = b'git version'
157 157 if stdout.startswith(prefix):
158 158 stdout = stdout[len(prefix):]
159 159 return safe_str(stdout.strip())
160 160
161 161 @reraise_safe_exceptions
162 162 def is_empty(self, wire):
163 163 repo_init = self._factory.repo_libgit2(wire)
164 164 with repo_init as repo:
165 165
166 166 try:
167 167 has_head = repo.head.name
168 168 if has_head:
169 169 return False
170 170
171 171 # NOTE(marcink): check again using more expensive method
172 172 return repo.is_empty
173 173 except Exception:
174 174 pass
175 175
176 176 return True
177 177
178 178 @reraise_safe_exceptions
179 179 def assert_correct_path(self, wire):
180 180 cache_on, context_uid, repo_id = self._cache_on(wire)
181 181 region = self._region(wire)
182 182
183 183 @region.conditional_cache_on_arguments(condition=cache_on)
184 184 def _assert_correct_path(_context_uid, _repo_id):
185 185 try:
186 186 repo_init = self._factory.repo_libgit2(wire)
187 187 with repo_init as repo:
188 188 pass
189 189 except pygit2.GitError:
190 190 path = wire.get('path')
191 191 tb = traceback.format_exc()
192 192 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
193 193 return False
194 194
195 195 return True
196 196 return _assert_correct_path(context_uid, repo_id)
197 197
198 198 @reraise_safe_exceptions
199 199 def bare(self, wire):
200 200 repo_init = self._factory.repo_libgit2(wire)
201 201 with repo_init as repo:
202 202 return repo.is_bare
203 203
204 204 @reraise_safe_exceptions
205 205 def blob_as_pretty_string(self, wire, sha):
206 206 repo_init = self._factory.repo_libgit2(wire)
207 207 with repo_init as repo:
208 208 blob_obj = repo[sha]
209 209 blob = blob_obj.data
210 210 return blob
211 211
212 212 @reraise_safe_exceptions
213 213 def blob_raw_length(self, wire, sha):
214 214 cache_on, context_uid, repo_id = self._cache_on(wire)
215 215 region = self._region(wire)
216 216
217 217 @region.conditional_cache_on_arguments(condition=cache_on)
218 218 def _blob_raw_length(_repo_id, _sha):
219 219
220 220 repo_init = self._factory.repo_libgit2(wire)
221 221 with repo_init as repo:
222 222 blob = repo[sha]
223 223 return blob.size
224 224
225 225 return _blob_raw_length(repo_id, sha)
226 226
227 227 def _parse_lfs_pointer(self, raw_content):
228 228 spec_string = b'version https://git-lfs.github.com/spec'
229 229 if raw_content and raw_content.startswith(spec_string):
230 230
231 231 pattern = re.compile(rb"""
232 232 (?:\n)?
233 233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
234 234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
235 235 ^size[ ](?P<oid_size>[0-9]+)\n
236 236 (?:\n)?
237 237 """, re.VERBOSE | re.MULTILINE)
238 238 match = pattern.match(raw_content)
239 239 if match:
240 240 return match.groupdict()
241 241
242 242 return {}
243 243
244 244 @reraise_safe_exceptions
245 245 def is_large_file(self, wire, commit_id):
246 246 cache_on, context_uid, repo_id = self._cache_on(wire)
247 247 region = self._region(wire)
248 248
249 249 @region.conditional_cache_on_arguments(condition=cache_on)
250 250 def _is_large_file(_repo_id, _sha):
251 251 repo_init = self._factory.repo_libgit2(wire)
252 252 with repo_init as repo:
253 253 blob = repo[commit_id]
254 254 if blob.is_binary:
255 255 return {}
256 256
257 257 return self._parse_lfs_pointer(blob.data)
258 258
259 259 return _is_large_file(repo_id, commit_id)
260 260
261 261 @reraise_safe_exceptions
262 262 def is_binary(self, wire, tree_id):
263 263 cache_on, context_uid, repo_id = self._cache_on(wire)
264 264 region = self._region(wire)
265 265
266 266 @region.conditional_cache_on_arguments(condition=cache_on)
267 267 def _is_binary(_repo_id, _tree_id):
268 268 repo_init = self._factory.repo_libgit2(wire)
269 269 with repo_init as repo:
270 270 blob_obj = repo[tree_id]
271 271 return blob_obj.is_binary
272 272
273 273 return _is_binary(repo_id, tree_id)
274 274
275 275 @reraise_safe_exceptions
276 def md5_hash(self, wire, tree_id):
277 cache_on, context_uid, repo_id = self._cache_on(wire)
278 region = self._region(wire)
279
280 @region.conditional_cache_on_arguments(condition=cache_on)
281 def _md5_hash(_repo_id, _tree_id):
282 return ''
283
284 return _md5_hash(repo_id, tree_id)
285
286 @reraise_safe_exceptions
276 287 def in_largefiles_store(self, wire, oid):
277 288 conf = self._wire_to_config(wire)
278 289 repo_init = self._factory.repo_libgit2(wire)
279 290 with repo_init as repo:
280 291 repo_name = repo.path
281 292
282 293 store_location = conf.get('vcs_git_lfs_store_location')
283 294 if store_location:
284 295
285 296 store = LFSOidStore(
286 297 oid=oid, repo=repo_name, store_location=store_location)
287 298 return store.has_oid()
288 299
289 300 return False
290 301
291 302 @reraise_safe_exceptions
292 303 def store_path(self, wire, oid):
293 304 conf = self._wire_to_config(wire)
294 305 repo_init = self._factory.repo_libgit2(wire)
295 306 with repo_init as repo:
296 307 repo_name = repo.path
297 308
298 309 store_location = conf.get('vcs_git_lfs_store_location')
299 310 if store_location:
300 311 store = LFSOidStore(
301 312 oid=oid, repo=repo_name, store_location=store_location)
302 313 return store.oid_path
303 314 raise ValueError('Unable to fetch oid with path {}'.format(oid))
304 315
305 316 @reraise_safe_exceptions
306 317 def bulk_request(self, wire, rev, pre_load):
307 318 cache_on, context_uid, repo_id = self._cache_on(wire)
308 319 region = self._region(wire)
309 320
310 321 @region.conditional_cache_on_arguments(condition=cache_on)
311 322 def _bulk_request(_repo_id, _rev, _pre_load):
312 323 result = {}
313 324 for attr in pre_load:
314 325 try:
315 326 method = self._bulk_methods[attr]
316 327 args = [wire, rev]
317 328 result[attr] = method(*args)
318 329 except KeyError as e:
319 330 raise exceptions.VcsException(e)(
320 331 "Unknown bulk attribute: %s" % attr)
321 332 return result
322 333
323 334 return _bulk_request(repo_id, rev, sorted(pre_load))
324 335
325 336 def _build_opener(self, url):
326 337 handlers = []
327 338 url_obj = url_parser(url)
328 339 _, authinfo = url_obj.authinfo()
329 340
330 341 if authinfo:
331 342 # create a password manager
332 343 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
333 344 passmgr.add_password(*authinfo)
334 345
335 346 handlers.extend((httpbasicauthhandler(passmgr),
336 347 httpdigestauthhandler(passmgr)))
337 348
338 349 return urllib.request.build_opener(*handlers)
339 350
340 351 def _type_id_to_name(self, type_id: int):
341 352 return {
342 353 1: 'commit',
343 354 2: 'tree',
344 355 3: 'blob',
345 356 4: 'tag'
346 357 }[type_id]
347 358
348 359 @reraise_safe_exceptions
349 360 def check_url(self, url, config):
350 361 url_obj = url_parser(url)
351 362 test_uri, _ = url_obj.authinfo()
352 363 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
353 364 url_obj.query = obfuscate_qs(url_obj.query)
354 365 cleaned_uri = str(url_obj)
355 366 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
356 367
357 368 if not test_uri.endswith('info/refs'):
358 369 test_uri = test_uri.rstrip('/') + '/info/refs'
359 370
360 371 o = self._build_opener(url)
361 372 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
362 373
363 374 q = {"service": 'git-upload-pack'}
364 375 qs = '?%s' % urllib.parse.urlencode(q)
365 376 cu = "%s%s" % (test_uri, qs)
366 377 req = urllib.request.Request(cu, None, {})
367 378
368 379 try:
369 380 log.debug("Trying to open URL %s", cleaned_uri)
370 381 resp = o.open(req)
371 382 if resp.code != 200:
372 383 raise exceptions.URLError()('Return Code is not 200')
373 384 except Exception as e:
374 385 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
375 386 # means it cannot be cloned
376 387 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
377 388
378 389 # now detect if it's proper git repo
379 390 gitdata = resp.read()
380 391 if 'service=git-upload-pack' in gitdata:
381 392 pass
382 393 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
383 394 # old style git can return some other format !
384 395 pass
385 396 else:
386 397 raise exceptions.URLError()(
387 398 "url [%s] does not look like an git" % (cleaned_uri,))
388 399
389 400 return True
390 401
391 402 @reraise_safe_exceptions
392 403 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
393 404 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
394 405 remote_refs = self.pull(wire, url, apply_refs=False)
395 406 repo = self._factory.repo(wire)
396 407 if isinstance(valid_refs, list):
397 408 valid_refs = tuple(valid_refs)
398 409
399 410 for k in remote_refs:
400 411 # only parse heads/tags and skip so called deferred tags
401 412 if k.startswith(valid_refs) and not k.endswith(deferred):
402 413 repo[k] = remote_refs[k]
403 414
404 415 if update_after_clone:
405 416 # we want to checkout HEAD
406 417 repo["HEAD"] = remote_refs["HEAD"]
407 418 index.build_index_from_tree(repo.path, repo.index_path(),
408 419 repo.object_store, repo["HEAD"].tree)
409 420
410 421 @reraise_safe_exceptions
411 422 def branch(self, wire, commit_id):
412 423 cache_on, context_uid, repo_id = self._cache_on(wire)
413 424 region = self._region(wire)
414 425 @region.conditional_cache_on_arguments(condition=cache_on)
415 426 def _branch(_context_uid, _repo_id, _commit_id):
416 427 regex = re.compile('^refs/heads')
417 428
418 429 def filter_with(ref):
419 430 return regex.match(ref[0]) and ref[1] == _commit_id
420 431
421 432 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
422 433 return [x[0].split('refs/heads/')[-1] for x in branches]
423 434
424 435 return _branch(context_uid, repo_id, commit_id)
425 436
426 437 @reraise_safe_exceptions
427 438 def commit_branches(self, wire, commit_id):
428 439 cache_on, context_uid, repo_id = self._cache_on(wire)
429 440 region = self._region(wire)
430 441 @region.conditional_cache_on_arguments(condition=cache_on)
431 442 def _commit_branches(_context_uid, _repo_id, _commit_id):
432 443 repo_init = self._factory.repo_libgit2(wire)
433 444 with repo_init as repo:
434 445 branches = [x for x in repo.branches.with_commit(_commit_id)]
435 446 return branches
436 447
437 448 return _commit_branches(context_uid, repo_id, commit_id)
438 449
439 450 @reraise_safe_exceptions
440 451 def add_object(self, wire, content):
441 452 repo_init = self._factory.repo_libgit2(wire)
442 453 with repo_init as repo:
443 454 blob = objects.Blob()
444 455 blob.set_raw_string(content)
445 456 repo.object_store.add_object(blob)
446 457 return blob.id
447 458
448 459 # TODO: this is quite complex, check if that can be simplified
449 460 @reraise_safe_exceptions
450 461 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
451 462 # Defines the root tree
452 463 class _Root(object):
453 464 def __repr__(self):
454 465 return 'ROOT TREE'
455 466 ROOT = _Root()
456 467
457 468 repo = self._factory.repo(wire)
458 469 object_store = repo.object_store
459 470
460 471 # Create tree and populates it with blobs
461 472
462 473 if commit_tree and repo[commit_tree]:
463 474 git_commit = repo[commit_data['parents'][0]]
464 475 commit_tree = repo[git_commit.tree] # root tree
465 476 else:
466 477 commit_tree = objects.Tree()
467 478
468 479 for node in updated:
469 480 # Compute subdirs if needed
470 481 dirpath, nodename = vcspath.split(node['path'])
471 482 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
472 483 parent = commit_tree
473 484 ancestors = [('', parent)]
474 485
475 486 # Tries to dig for the deepest existing tree
476 487 while dirnames:
477 488 curdir = dirnames.pop(0)
478 489 try:
479 490 dir_id = parent[curdir][1]
480 491 except KeyError:
481 492 # put curdir back into dirnames and stops
482 493 dirnames.insert(0, curdir)
483 494 break
484 495 else:
485 496 # If found, updates parent
486 497 parent = repo[dir_id]
487 498 ancestors.append((curdir, parent))
488 499 # Now parent is deepest existing tree and we need to create
489 500 # subtrees for dirnames (in reverse order)
490 501 # [this only applies for nodes from added]
491 502 new_trees = []
492 503
493 504 blob = objects.Blob.from_string(node['content'])
494 505
495 506 if dirnames:
496 507 # If there are trees which should be created we need to build
497 508 # them now (in reverse order)
498 509 reversed_dirnames = list(reversed(dirnames))
499 510 curtree = objects.Tree()
500 511 curtree[node['node_path']] = node['mode'], blob.id
501 512 new_trees.append(curtree)
502 513 for dirname in reversed_dirnames[:-1]:
503 514 newtree = objects.Tree()
504 515 newtree[dirname] = (DIR_STAT, curtree.id)
505 516 new_trees.append(newtree)
506 517 curtree = newtree
507 518 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
508 519 else:
509 520 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
510 521
511 522 new_trees.append(parent)
512 523 # Update ancestors
513 524 reversed_ancestors = reversed(
514 525 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
515 526 for parent, tree, path in reversed_ancestors:
516 527 parent[path] = (DIR_STAT, tree.id)
517 528 object_store.add_object(tree)
518 529
519 530 object_store.add_object(blob)
520 531 for tree in new_trees:
521 532 object_store.add_object(tree)
522 533
523 534 for node_path in removed:
524 535 paths = node_path.split('/')
525 536 tree = commit_tree # start with top-level
526 537 trees = [{'tree': tree, 'path': ROOT}]
527 538 # Traverse deep into the forest...
528 539 # resolve final tree by iterating the path.
529 540 # e.g a/b/c.txt will get
530 541 # - root as tree then
531 542 # - 'a' as tree,
532 543 # - 'b' as tree,
533 544 # - stop at c as blob.
534 545 for path in paths:
535 546 try:
536 547 obj = repo[tree[path][1]]
537 548 if isinstance(obj, objects.Tree):
538 549 trees.append({'tree': obj, 'path': path})
539 550 tree = obj
540 551 except KeyError:
541 552 break
542 553 #PROBLEM:
543 554 """
544 555 We're not editing same reference tree object
545 556 """
546 557 # Cut down the blob and all rotten trees on the way back...
547 558 for path, tree_data in reversed(list(zip(paths, trees))):
548 559 tree = tree_data['tree']
549 560 tree.__delitem__(path)
550 561 # This operation edits the tree, we need to mark new commit back
551 562
552 563 if len(tree) > 0:
553 564 # This tree still has elements - don't remove it or any
554 565 # of it's parents
555 566 break
556 567
557 568 object_store.add_object(commit_tree)
558 569
559 570 # Create commit
560 571 commit = objects.Commit()
561 572 commit.tree = commit_tree.id
562 573 bytes_keys = [
563 574 'author',
564 575 'committer',
565 576 'message',
566 577 'encoding'
567 578 ]
568 579
569 580 for k, v in commit_data.items():
570 581 if k in bytes_keys:
571 582 v = safe_bytes(v)
572 583 setattr(commit, k, v)
573 584
574 585 object_store.add_object(commit)
575 586
576 587 self.create_branch(wire, branch, safe_str(commit.id))
577 588
578 589 # dulwich set-ref
579 590 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
580 591
581 592 return commit.id
582 593
583 594 @reraise_safe_exceptions
584 595 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
585 596 if url != 'default' and '://' not in url:
586 597 client = LocalGitClient(url)
587 598 else:
588 599 url_obj = url_parser(url)
589 600 o = self._build_opener(url)
590 601 url, _ = url_obj.authinfo()
591 602 client = HttpGitClient(base_url=url, opener=o)
592 603 repo = self._factory.repo(wire)
593 604
594 605 determine_wants = repo.object_store.determine_wants_all
595 606 if refs:
596 607 def determine_wants_requested(references):
597 608 return [references[r] for r in references if r in refs]
598 609 determine_wants = determine_wants_requested
599 610
600 611 try:
601 612 remote_refs = client.fetch(
602 613 path=url, target=repo, determine_wants=determine_wants)
603 614 except NotGitRepository as e:
604 615 log.warning(
605 616 'Trying to fetch from "%s" failed, not a Git repository.', url)
606 617 # Exception can contain unicode which we convert
607 618 raise exceptions.AbortException(e)(repr(e))
608 619
609 620 # mikhail: client.fetch() returns all the remote refs, but fetches only
610 621 # refs filtered by `determine_wants` function. We need to filter result
611 622 # as well
612 623 if refs:
613 624 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
614 625
615 626 if apply_refs:
616 627 # TODO: johbo: Needs proper test coverage with a git repository
617 628 # that contains a tag object, so that we would end up with
618 629 # a peeled ref at this point.
619 630 for k in remote_refs:
620 631 if k.endswith(PEELED_REF_MARKER):
621 632 log.debug("Skipping peeled reference %s", k)
622 633 continue
623 634 repo[k] = remote_refs[k]
624 635
625 636 if refs and not update_after:
626 637 # mikhail: explicitly set the head to the last ref.
627 638 repo["HEAD"] = remote_refs[refs[-1]]
628 639
629 640 if update_after:
630 641 # we want to checkout HEAD
631 642 repo["HEAD"] = remote_refs["HEAD"]
632 643 index.build_index_from_tree(repo.path, repo.index_path(),
633 644 repo.object_store, repo["HEAD"].tree)
634 645 return remote_refs
635 646
636 647 @reraise_safe_exceptions
637 648 def sync_fetch(self, wire, url, refs=None, all_refs=False):
638 649 repo = self._factory.repo(wire)
639 650 if refs and not isinstance(refs, (list, tuple)):
640 651 refs = [refs]
641 652
642 653 config = self._wire_to_config(wire)
643 654 # get all remote refs we'll use to fetch later
644 655 cmd = ['ls-remote']
645 656 if not all_refs:
646 657 cmd += ['--heads', '--tags']
647 658 cmd += [url]
648 659 output, __ = self.run_git_command(
649 660 wire, cmd, fail_on_stderr=False,
650 661 _copts=self._remote_conf(config),
651 662 extra_env={'GIT_TERMINAL_PROMPT': '0'})
652 663
653 664 remote_refs = collections.OrderedDict()
654 665 fetch_refs = []
655 666
656 667 for ref_line in output.splitlines():
657 668 sha, ref = ref_line.split(b'\t')
658 669 sha = sha.strip()
659 670 if ref in remote_refs:
660 671 # duplicate, skip
661 672 continue
662 673 if ref.endswith(PEELED_REF_MARKER):
663 674 log.debug("Skipping peeled reference %s", ref)
664 675 continue
665 676 # don't sync HEAD
666 677 if ref in [b'HEAD']:
667 678 continue
668 679
669 680 remote_refs[ref] = sha
670 681
671 682 if refs and sha in refs:
672 683 # we filter fetch using our specified refs
673 684 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
674 685 elif not refs:
675 686 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
676 687 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
677 688
678 689 if fetch_refs:
679 690 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
680 691 fetch_refs_chunks = list(chunk)
681 692 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
682 693 self.run_git_command(
683 694 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
684 695 fail_on_stderr=False,
685 696 _copts=self._remote_conf(config),
686 697 extra_env={'GIT_TERMINAL_PROMPT': '0'})
687 698
688 699 return remote_refs
689 700
690 701 @reraise_safe_exceptions
691 702 def sync_push(self, wire, url, refs=None):
692 703 if not self.check_url(url, wire):
693 704 return
694 705 config = self._wire_to_config(wire)
695 706 self._factory.repo(wire)
696 707 self.run_git_command(
697 708 wire, ['push', url, '--mirror'], fail_on_stderr=False,
698 709 _copts=self._remote_conf(config),
699 710 extra_env={'GIT_TERMINAL_PROMPT': '0'})
700 711
701 712 @reraise_safe_exceptions
702 713 def get_remote_refs(self, wire, url):
703 714 repo = Repo(url)
704 715 return repo.get_refs()
705 716
706 717 @reraise_safe_exceptions
707 718 def get_description(self, wire):
708 719 repo = self._factory.repo(wire)
709 720 return repo.get_description()
710 721
711 722 @reraise_safe_exceptions
712 723 def get_missing_revs(self, wire, rev1, rev2, path2):
713 724 repo = self._factory.repo(wire)
714 725 LocalGitClient(thin_packs=False).fetch(path2, repo)
715 726
716 727 wire_remote = wire.copy()
717 728 wire_remote['path'] = path2
718 729 repo_remote = self._factory.repo(wire_remote)
719 730 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
720 731
721 732 revs = [
722 733 x.commit.id
723 734 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
724 735 return revs
725 736
726 737 @reraise_safe_exceptions
727 738 def get_object(self, wire, sha, maybe_unreachable=False):
728 739 cache_on, context_uid, repo_id = self._cache_on(wire)
729 740 region = self._region(wire)
730 741
731 742 @region.conditional_cache_on_arguments(condition=cache_on)
732 743 def _get_object(_context_uid, _repo_id, _sha):
733 744 repo_init = self._factory.repo_libgit2(wire)
734 745 with repo_init as repo:
735 746
736 747 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
737 748 try:
738 749 commit = repo.revparse_single(sha)
739 750 except KeyError:
740 751 # NOTE(marcink): KeyError doesn't give us any meaningful information
741 752 # here, we instead give something more explicit
742 753 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
743 754 raise exceptions.LookupException(e)(missing_commit_err)
744 755 except ValueError as e:
745 756 raise exceptions.LookupException(e)(missing_commit_err)
746 757
747 758 is_tag = False
748 759 if isinstance(commit, pygit2.Tag):
749 760 commit = repo.get(commit.target)
750 761 is_tag = True
751 762
752 763 check_dangling = True
753 764 if is_tag:
754 765 check_dangling = False
755 766
756 767 if check_dangling and maybe_unreachable:
757 768 check_dangling = False
758 769
759 770 # we used a reference and it parsed means we're not having a dangling commit
760 771 if sha != commit.hex:
761 772 check_dangling = False
762 773
763 774 if check_dangling:
764 775 # check for dangling commit
765 776 for branch in repo.branches.with_commit(commit.hex):
766 777 if branch:
767 778 break
768 779 else:
769 780 # NOTE(marcink): Empty error doesn't give us any meaningful information
770 781 # here, we instead give something more explicit
771 782 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
772 783 raise exceptions.LookupException(e)(missing_commit_err)
773 784
774 785 commit_id = commit.hex
775 786 type_id = commit.type
776 787
777 788 return {
778 789 'id': commit_id,
779 790 'type': self._type_id_to_name(type_id),
780 791 'commit_id': commit_id,
781 792 'idx': 0
782 793 }
783 794
784 795 return _get_object(context_uid, repo_id, sha)
785 796
786 797 @reraise_safe_exceptions
787 798 def get_refs(self, wire):
788 799 cache_on, context_uid, repo_id = self._cache_on(wire)
789 800 region = self._region(wire)
790 801
791 802 @region.conditional_cache_on_arguments(condition=cache_on)
792 803 def _get_refs(_context_uid, _repo_id):
793 804
794 805 repo_init = self._factory.repo_libgit2(wire)
795 806 with repo_init as repo:
796 807 regex = re.compile('^refs/(heads|tags)/')
797 808 return {x.name: x.target.hex for x in
798 809 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
799 810
800 811 return _get_refs(context_uid, repo_id)
801 812
802 813 @reraise_safe_exceptions
803 814 def get_branch_pointers(self, wire):
804 815 cache_on, context_uid, repo_id = self._cache_on(wire)
805 816 region = self._region(wire)
806 817
807 818 @region.conditional_cache_on_arguments(condition=cache_on)
808 819 def _get_branch_pointers(_context_uid, _repo_id):
809 820
810 821 repo_init = self._factory.repo_libgit2(wire)
811 822 regex = re.compile('^refs/heads')
812 823 with repo_init as repo:
813 824 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
814 825 return {x.target.hex: x.shorthand for x in branches}
815 826
816 827 return _get_branch_pointers(context_uid, repo_id)
817 828
818 829 @reraise_safe_exceptions
819 830 def head(self, wire, show_exc=True):
820 831 cache_on, context_uid, repo_id = self._cache_on(wire)
821 832 region = self._region(wire)
822 833
823 834 @region.conditional_cache_on_arguments(condition=cache_on)
824 835 def _head(_context_uid, _repo_id, _show_exc):
825 836 repo_init = self._factory.repo_libgit2(wire)
826 837 with repo_init as repo:
827 838 try:
828 839 return repo.head.peel().hex
829 840 except Exception:
830 841 if show_exc:
831 842 raise
832 843 return _head(context_uid, repo_id, show_exc)
833 844
834 845 @reraise_safe_exceptions
835 846 def init(self, wire):
836 847 repo_path = safe_str(wire['path'])
837 848 self.repo = Repo.init(repo_path)
838 849
839 850 @reraise_safe_exceptions
840 851 def init_bare(self, wire):
841 852 repo_path = safe_str(wire['path'])
842 853 self.repo = Repo.init_bare(repo_path)
843 854
844 855 @reraise_safe_exceptions
845 856 def revision(self, wire, rev):
846 857
847 858 cache_on, context_uid, repo_id = self._cache_on(wire)
848 859 region = self._region(wire)
849 860
850 861 @region.conditional_cache_on_arguments(condition=cache_on)
851 862 def _revision(_context_uid, _repo_id, _rev):
852 863 repo_init = self._factory.repo_libgit2(wire)
853 864 with repo_init as repo:
854 865 commit = repo[rev]
855 866 obj_data = {
856 867 'id': commit.id.hex,
857 868 }
858 869 # tree objects itself don't have tree_id attribute
859 870 if hasattr(commit, 'tree_id'):
860 871 obj_data['tree'] = commit.tree_id.hex
861 872
862 873 return obj_data
863 874 return _revision(context_uid, repo_id, rev)
864 875
865 876 @reraise_safe_exceptions
866 877 def date(self, wire, commit_id):
867 878 cache_on, context_uid, repo_id = self._cache_on(wire)
868 879 region = self._region(wire)
869 880
870 881 @region.conditional_cache_on_arguments(condition=cache_on)
871 882 def _date(_repo_id, _commit_id):
872 883 repo_init = self._factory.repo_libgit2(wire)
873 884 with repo_init as repo:
874 885 commit = repo[commit_id]
875 886
876 887 if hasattr(commit, 'commit_time'):
877 888 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
878 889 else:
879 890 commit = commit.get_object()
880 891 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
881 892
882 893 # TODO(marcink): check dulwich difference of offset vs timezone
883 894 return [commit_time, commit_time_offset]
884 895 return _date(repo_id, commit_id)
885 896
886 897 @reraise_safe_exceptions
887 898 def author(self, wire, commit_id):
888 899 cache_on, context_uid, repo_id = self._cache_on(wire)
889 900 region = self._region(wire)
890 901
891 902 @region.conditional_cache_on_arguments(condition=cache_on)
892 903 def _author(_repo_id, _commit_id):
893 904 repo_init = self._factory.repo_libgit2(wire)
894 905 with repo_init as repo:
895 906 commit = repo[commit_id]
896 907
897 908 if hasattr(commit, 'author'):
898 909 author = commit.author
899 910 else:
900 911 author = commit.get_object().author
901 912
902 913 if author.email:
903 914 return "{} <{}>".format(author.name, author.email)
904 915
905 916 try:
906 917 return "{}".format(author.name)
907 918 except Exception:
908 919 return "{}".format(safe_str(author.raw_name))
909 920
910 921 return _author(repo_id, commit_id)
911 922
912 923 @reraise_safe_exceptions
913 924 def message(self, wire, commit_id):
914 925 cache_on, context_uid, repo_id = self._cache_on(wire)
915 926 region = self._region(wire)
916 927 @region.conditional_cache_on_arguments(condition=cache_on)
917 928 def _message(_repo_id, _commit_id):
918 929 repo_init = self._factory.repo_libgit2(wire)
919 930 with repo_init as repo:
920 931 commit = repo[commit_id]
921 932 return commit.message
922 933 return _message(repo_id, commit_id)
923 934
924 935 @reraise_safe_exceptions
925 936 def parents(self, wire, commit_id):
926 937 cache_on, context_uid, repo_id = self._cache_on(wire)
927 938 region = self._region(wire)
928 939
929 940 @region.conditional_cache_on_arguments(condition=cache_on)
930 941 def _parents(_repo_id, _commit_id):
931 942 repo_init = self._factory.repo_libgit2(wire)
932 943 with repo_init as repo:
933 944 commit = repo[commit_id]
934 945 if hasattr(commit, 'parent_ids'):
935 946 parent_ids = commit.parent_ids
936 947 else:
937 948 parent_ids = commit.get_object().parent_ids
938 949
939 950 return [x.hex for x in parent_ids]
940 951 return _parents(repo_id, commit_id)
941 952
942 953 @reraise_safe_exceptions
943 954 def children(self, wire, commit_id):
944 955 cache_on, context_uid, repo_id = self._cache_on(wire)
945 956 region = self._region(wire)
946 957
947 958 head = self.head(wire)
948 959
949 960 @region.conditional_cache_on_arguments(condition=cache_on)
950 961 def _children(_repo_id, _commit_id):
951 962
952 963 output, __ = self.run_git_command(
953 964 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
954 965
955 966 child_ids = []
956 967 pat = re.compile(r'^{}'.format(commit_id))
957 968 for line in output.splitlines():
958 969 line = safe_str(line)
959 970 if pat.match(line):
960 971 found_ids = line.split(' ')[1:]
961 972 child_ids.extend(found_ids)
962 973 break
963 974
964 975 return child_ids
965 976 return _children(repo_id, commit_id)
966 977
967 978 @reraise_safe_exceptions
968 979 def set_refs(self, wire, key, value):
969 980 repo_init = self._factory.repo_libgit2(wire)
970 981 with repo_init as repo:
971 982 repo.references.create(key, value, force=True)
972 983
973 984 @reraise_safe_exceptions
974 985 def create_branch(self, wire, branch_name, commit_id, force=False):
975 986 repo_init = self._factory.repo_libgit2(wire)
976 987 with repo_init as repo:
977 988 commit = repo[commit_id]
978 989
979 990 if force:
980 991 repo.branches.local.create(branch_name, commit, force=force)
981 992 elif not repo.branches.get(branch_name):
982 993 # create only if that branch isn't existing
983 994 repo.branches.local.create(branch_name, commit, force=force)
984 995
985 996 @reraise_safe_exceptions
986 997 def remove_ref(self, wire, key):
987 998 repo_init = self._factory.repo_libgit2(wire)
988 999 with repo_init as repo:
989 1000 repo.references.delete(key)
990 1001
991 1002 @reraise_safe_exceptions
992 1003 def tag_remove(self, wire, tag_name):
993 1004 repo_init = self._factory.repo_libgit2(wire)
994 1005 with repo_init as repo:
995 1006 key = 'refs/tags/{}'.format(tag_name)
996 1007 repo.references.delete(key)
997 1008
998 1009 @reraise_safe_exceptions
999 1010 def tree_changes(self, wire, source_id, target_id):
1000 1011 # TODO(marcink): remove this seems it's only used by tests
1001 1012 repo = self._factory.repo(wire)
1002 1013 source = repo[source_id].tree if source_id else None
1003 1014 target = repo[target_id].tree
1004 1015 result = repo.object_store.tree_changes(source, target)
1005 1016 return list(result)
1006 1017
1007 1018 @reraise_safe_exceptions
1008 1019 def tree_and_type_for_path(self, wire, commit_id, path):
1009 1020
1010 1021 cache_on, context_uid, repo_id = self._cache_on(wire)
1011 1022 region = self._region(wire)
1012 1023
1013 1024 @region.conditional_cache_on_arguments(condition=cache_on)
1014 1025 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1015 1026 repo_init = self._factory.repo_libgit2(wire)
1016 1027
1017 1028 with repo_init as repo:
1018 1029 commit = repo[commit_id]
1019 1030 try:
1020 1031 tree = commit.tree[path]
1021 1032 except KeyError:
1022 1033 return None, None, None
1023 1034
1024 1035 return tree.id.hex, tree.type_str, tree.filemode
1025 1036 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1026 1037
1027 1038 @reraise_safe_exceptions
1028 1039 def tree_items(self, wire, tree_id):
1029 1040 cache_on, context_uid, repo_id = self._cache_on(wire)
1030 1041 region = self._region(wire)
1031 1042
1032 1043 @region.conditional_cache_on_arguments(condition=cache_on)
1033 1044 def _tree_items(_repo_id, _tree_id):
1034 1045
1035 1046 repo_init = self._factory.repo_libgit2(wire)
1036 1047 with repo_init as repo:
1037 1048 try:
1038 1049 tree = repo[tree_id]
1039 1050 except KeyError:
1040 1051 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1041 1052
1042 1053 result = []
1043 1054 for item in tree:
1044 1055 item_sha = item.hex
1045 1056 item_mode = item.filemode
1046 1057 item_type = item.type_str
1047 1058
1048 1059 if item_type == 'commit':
1049 1060 # NOTE(marcink): submodules we translate to 'link' for backward compat
1050 1061 item_type = 'link'
1051 1062
1052 1063 result.append((item.name, item_mode, item_sha, item_type))
1053 1064 return result
1054 1065 return _tree_items(repo_id, tree_id)
1055 1066
1056 1067 @reraise_safe_exceptions
1057 1068 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1058 1069 """
1059 1070 Old version that uses subprocess to call diff
1060 1071 """
1061 1072
1062 1073 flags = [
1063 1074 '-U%s' % context, '--patch',
1064 1075 '--binary',
1065 1076 '--find-renames',
1066 1077 '--no-indent-heuristic',
1067 1078 # '--indent-heuristic',
1068 1079 #'--full-index',
1069 1080 #'--abbrev=40'
1070 1081 ]
1071 1082
1072 1083 if opt_ignorews:
1073 1084 flags.append('--ignore-all-space')
1074 1085
1075 1086 if commit_id_1 == self.EMPTY_COMMIT:
1076 1087 cmd = ['show'] + flags + [commit_id_2]
1077 1088 else:
1078 1089 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1079 1090
1080 1091 if file_filter:
1081 1092 cmd.extend(['--', file_filter])
1082 1093
1083 1094 diff, __ = self.run_git_command(wire, cmd)
1084 1095 # If we used 'show' command, strip first few lines (until actual diff
1085 1096 # starts)
1086 1097 if commit_id_1 == self.EMPTY_COMMIT:
1087 1098 lines = diff.splitlines()
1088 1099 x = 0
1089 1100 for line in lines:
1090 1101 if line.startswith(b'diff'):
1091 1102 break
1092 1103 x += 1
1093 1104 # Append new line just like 'diff' command do
1094 1105 diff = '\n'.join(lines[x:]) + '\n'
1095 1106 return diff
1096 1107
1097 1108 @reraise_safe_exceptions
1098 1109 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1099 1110 repo_init = self._factory.repo_libgit2(wire)
1100 1111 with repo_init as repo:
1101 1112 swap = True
1102 1113 flags = 0
1103 1114 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1104 1115
1105 1116 if opt_ignorews:
1106 1117 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1107 1118
1108 1119 if commit_id_1 == self.EMPTY_COMMIT:
1109 1120 comm1 = repo[commit_id_2]
1110 1121 diff_obj = comm1.tree.diff_to_tree(
1111 1122 flags=flags, context_lines=context, swap=swap)
1112 1123
1113 1124 else:
1114 1125 comm1 = repo[commit_id_2]
1115 1126 comm2 = repo[commit_id_1]
1116 1127 diff_obj = comm1.tree.diff_to_tree(
1117 1128 comm2.tree, flags=flags, context_lines=context, swap=swap)
1118 1129 similar_flags = 0
1119 1130 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1120 1131 diff_obj.find_similar(flags=similar_flags)
1121 1132
1122 1133 if file_filter:
1123 1134 for p in diff_obj:
1124 1135 if p.delta.old_file.path == file_filter:
1125 1136 return p.patch or ''
1126 1137 # fo matching path == no diff
1127 1138 return ''
1128 1139 return diff_obj.patch or ''
1129 1140
1130 1141 @reraise_safe_exceptions
1131 1142 def node_history(self, wire, commit_id, path, limit):
1132 1143 cache_on, context_uid, repo_id = self._cache_on(wire)
1133 1144 region = self._region(wire)
1134 1145
1135 1146 @region.conditional_cache_on_arguments(condition=cache_on)
1136 1147 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1137 1148 # optimize for n==1, rev-list is much faster for that use-case
1138 1149 if limit == 1:
1139 1150 cmd = ['rev-list', '-1', commit_id, '--', path]
1140 1151 else:
1141 1152 cmd = ['log']
1142 1153 if limit:
1143 1154 cmd.extend(['-n', str(safe_int(limit, 0))])
1144 1155 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1145 1156
1146 1157 output, __ = self.run_git_command(wire, cmd)
1147 1158 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1148 1159
1149 1160 return [x for x in commit_ids]
1150 1161 return _node_history(context_uid, repo_id, commit_id, path, limit)
1151 1162
1152 1163 @reraise_safe_exceptions
1153 1164 def node_annotate_legacy(self, wire, commit_id, path):
1154 1165 #note: replaced by pygit2 impelementation
1155 1166 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1156 1167 # -l ==> outputs long shas (and we need all 40 characters)
1157 1168 # --root ==> doesn't put '^' character for boundaries
1158 1169 # -r commit_id ==> blames for the given commit
1159 1170 output, __ = self.run_git_command(wire, cmd)
1160 1171
1161 1172 result = []
1162 1173 for i, blame_line in enumerate(output.splitlines()[:-1]):
1163 1174 line_no = i + 1
1164 1175 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1165 1176 result.append((line_no, blame_commit_id, line))
1166 1177
1167 1178 return result
1168 1179
1169 1180 @reraise_safe_exceptions
1170 1181 def node_annotate(self, wire, commit_id, path):
1171 1182
1172 1183 result_libgit = []
1173 1184 repo_init = self._factory.repo_libgit2(wire)
1174 1185 with repo_init as repo:
1175 1186 commit = repo[commit_id]
1176 1187 blame_obj = repo.blame(path, newest_commit=commit_id)
1177 1188 for i, line in enumerate(commit.tree[path].data.splitlines()):
1178 1189 line_no = i + 1
1179 1190 hunk = blame_obj.for_line(line_no)
1180 1191 blame_commit_id = hunk.final_commit_id.hex
1181 1192
1182 1193 result_libgit.append((line_no, blame_commit_id, line))
1183 1194
1184 1195 return result_libgit
1185 1196
1186 1197 @reraise_safe_exceptions
1187 1198 def update_server_info(self, wire):
1188 1199 repo = self._factory.repo(wire)
1189 1200 update_server_info(repo)
1190 1201
1191 1202 @reraise_safe_exceptions
1192 1203 def get_all_commit_ids(self, wire):
1193 1204
1194 1205 cache_on, context_uid, repo_id = self._cache_on(wire)
1195 1206 region = self._region(wire)
1196 1207
1197 1208 @region.conditional_cache_on_arguments(condition=cache_on)
1198 1209 def _get_all_commit_ids(_context_uid, _repo_id):
1199 1210
1200 1211 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1201 1212 try:
1202 1213 output, __ = self.run_git_command(wire, cmd)
1203 1214 return output.splitlines()
1204 1215 except Exception:
1205 1216 # Can be raised for empty repositories
1206 1217 return []
1207 1218
1208 1219 @region.conditional_cache_on_arguments(condition=cache_on)
1209 1220 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1210 1221 repo_init = self._factory.repo_libgit2(wire)
1211 1222 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1212 1223 results = []
1213 1224 with repo_init as repo:
1214 1225 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1215 1226 results.append(commit.id.hex)
1216 1227
1217 1228 return _get_all_commit_ids(context_uid, repo_id)
1218 1229
1219 1230 @reraise_safe_exceptions
1220 1231 def run_git_command(self, wire, cmd, **opts):
1221 1232 path = wire.get('path', None)
1222 1233
1223 1234 if path and os.path.isdir(path):
1224 1235 opts['cwd'] = path
1225 1236
1226 1237 if '_bare' in opts:
1227 1238 _copts = []
1228 1239 del opts['_bare']
1229 1240 else:
1230 1241 _copts = ['-c', 'core.quotepath=false', ]
1231 1242 safe_call = False
1232 1243 if '_safe' in opts:
1233 1244 # no exc on failure
1234 1245 del opts['_safe']
1235 1246 safe_call = True
1236 1247
1237 1248 if '_copts' in opts:
1238 1249 _copts.extend(opts['_copts'] or [])
1239 1250 del opts['_copts']
1240 1251
1241 1252 gitenv = os.environ.copy()
1242 1253 gitenv.update(opts.pop('extra_env', {}))
1243 1254 # need to clean fix GIT_DIR !
1244 1255 if 'GIT_DIR' in gitenv:
1245 1256 del gitenv['GIT_DIR']
1246 1257 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1247 1258 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1248 1259
1249 1260 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1250 1261 _opts = {'env': gitenv, 'shell': False}
1251 1262
1252 1263 proc = None
1253 1264 try:
1254 1265 _opts.update(opts)
1255 1266 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1256 1267
1257 1268 return b''.join(proc), b''.join(proc.stderr)
1258 1269 except OSError as err:
1259 1270 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1260 1271 tb_err = ("Couldn't run git command (%s).\n"
1261 1272 "Original error was:%s\n"
1262 1273 "Call options:%s\n"
1263 1274 % (cmd, err, _opts))
1264 1275 log.exception(tb_err)
1265 1276 if safe_call:
1266 1277 return '', err
1267 1278 else:
1268 1279 raise exceptions.VcsException()(tb_err)
1269 1280 finally:
1270 1281 if proc:
1271 1282 proc.close()
1272 1283
1273 1284 @reraise_safe_exceptions
1274 1285 def install_hooks(self, wire, force=False):
1275 1286 from vcsserver.hook_utils import install_git_hooks
1276 1287 bare = self.bare(wire)
1277 1288 path = wire['path']
1278 1289 return install_git_hooks(path, bare, force_create=force)
1279 1290
1280 1291 @reraise_safe_exceptions
1281 1292 def get_hooks_info(self, wire):
1282 1293 from vcsserver.hook_utils import (
1283 1294 get_git_pre_hook_version, get_git_post_hook_version)
1284 1295 bare = self.bare(wire)
1285 1296 path = wire['path']
1286 1297 return {
1287 1298 'pre_version': get_git_pre_hook_version(path, bare),
1288 1299 'post_version': get_git_post_hook_version(path, bare),
1289 1300 }
1290 1301
1291 1302 @reraise_safe_exceptions
1292 1303 def set_head_ref(self, wire, head_name):
1293 1304 log.debug('Setting refs/head to `%s`', head_name)
1294 1305 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1295 1306 output, __ = self.run_git_command(wire, cmd)
1296 1307 return [head_name] + output.splitlines()
1297 1308
1298 1309 @reraise_safe_exceptions
1299 1310 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1300 1311 archive_dir_name, commit_id):
1301 1312
1302 1313 def file_walker(_commit_id, path):
1303 1314 repo_init = self._factory.repo_libgit2(wire)
1304 1315
1305 1316 with repo_init as repo:
1306 1317 commit = repo[commit_id]
1307 1318
1308 1319 if path in ['', '/']:
1309 1320 tree = commit.tree
1310 1321 else:
1311 1322 tree = commit.tree[path.rstrip('/')]
1312 1323 tree_id = tree.id.hex
1313 1324 try:
1314 1325 tree = repo[tree_id]
1315 1326 except KeyError:
1316 1327 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1317 1328
1318 1329 index = LibGit2Index.Index()
1319 1330 index.read_tree(tree)
1320 1331 file_iter = index
1321 1332
1322 1333 for fn in file_iter:
1323 1334 file_path = fn.path
1324 1335 mode = fn.mode
1325 1336 is_link = stat.S_ISLNK(mode)
1326 1337 if mode == pygit2.GIT_FILEMODE_COMMIT:
1327 1338 log.debug('Skipping path %s as a commit node', file_path)
1328 1339 continue
1329 1340 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1330 1341
1331 1342 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1332 1343 archive_dir_name, commit_id)
@@ -1,1072 +1,1086 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 logging
20 20 import stat
21 21 import urllib.request, urllib.parse, urllib.error
22 22 import urllib.request, urllib.error, urllib.parse
23 23 import traceback
24 24
25 25 from hgext import largefiles, rebase, purge
26 26
27 27 from mercurial import commands
28 28 from mercurial import unionrepo
29 29 from mercurial import verify
30 30 from mercurial import repair
31 31
32 32 import vcsserver
33 33 from vcsserver import exceptions
34 34 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
35 35 from vcsserver.hgcompat import (
36 36 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
37 37 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
38 38 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
39 39 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
40 40 RepoLookupError, InterventionRequired, RequirementError,
41 41 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
42 42 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
43 43 from vcsserver.vcs_base import RemoteBase
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 def make_ui_from_config(repo_config):
49 49
50 50 class LoggingUI(ui.ui):
51 51
52 52 def status(self, *msg, **opts):
53 53 str_msg = map(safe_str, msg)
54 54 log.info(' '.join(str_msg).rstrip('\n'))
55 55 #super(LoggingUI, self).status(*msg, **opts)
56 56
57 57 def warn(self, *msg, **opts):
58 58 str_msg = map(safe_str, msg)
59 59 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
60 60 #super(LoggingUI, self).warn(*msg, **opts)
61 61
62 62 def error(self, *msg, **opts):
63 63 str_msg = map(safe_str, msg)
64 64 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
65 65 #super(LoggingUI, self).error(*msg, **opts)
66 66
67 67 def note(self, *msg, **opts):
68 68 str_msg = map(safe_str, msg)
69 69 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
70 70 #super(LoggingUI, self).note(*msg, **opts)
71 71
72 72 def debug(self, *msg, **opts):
73 73 str_msg = map(safe_str, msg)
74 74 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
75 75 #super(LoggingUI, self).debug(*msg, **opts)
76 76
77 77 baseui = LoggingUI()
78 78
79 79 # clean the baseui object
80 80 baseui._ocfg = hgconfig.config()
81 81 baseui._ucfg = hgconfig.config()
82 82 baseui._tcfg = hgconfig.config()
83 83
84 84 for section, option, value in repo_config:
85 85 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
86 86
87 87 # make our hgweb quiet so it doesn't print output
88 88 baseui.setconfig(b'ui', b'quiet', b'true')
89 89
90 90 baseui.setconfig(b'ui', b'paginate', b'never')
91 91 # for better Error reporting of Mercurial
92 92 baseui.setconfig(b'ui', b'message-output', b'stderr')
93 93
94 94 # force mercurial to only use 1 thread, otherwise it may try to set a
95 95 # signal in a non-main thread, thus generating a ValueError.
96 96 baseui.setconfig(b'worker', b'numcpus', 1)
97 97
98 98 # If there is no config for the largefiles extension, we explicitly disable
99 99 # it here. This overrides settings from repositories hgrc file. Recent
100 100 # mercurial versions enable largefiles in hgrc on clone from largefile
101 101 # repo.
102 102 if not baseui.hasconfig(b'extensions', b'largefiles'):
103 103 log.debug('Explicitly disable largefiles extension for repo.')
104 104 baseui.setconfig(b'extensions', b'largefiles', b'!')
105 105
106 106 return baseui
107 107
108 108
109 109 def reraise_safe_exceptions(func):
110 110 """Decorator for converting mercurial exceptions to something neutral."""
111 111
112 112 def wrapper(*args, **kwargs):
113 113 try:
114 114 return func(*args, **kwargs)
115 115 except (Abort, InterventionRequired) as e:
116 116 raise_from_original(exceptions.AbortException(e), e)
117 117 except RepoLookupError as e:
118 118 raise_from_original(exceptions.LookupException(e), e)
119 119 except RequirementError as e:
120 120 raise_from_original(exceptions.RequirementException(e), e)
121 121 except RepoError as e:
122 122 raise_from_original(exceptions.VcsException(e), e)
123 123 except LookupError as e:
124 124 raise_from_original(exceptions.LookupException(e), e)
125 125 except Exception as e:
126 126 if not hasattr(e, '_vcs_kind'):
127 127 log.exception("Unhandled exception in hg remote call")
128 128 raise_from_original(exceptions.UnhandledException(e), e)
129 129
130 130 raise
131 131 return wrapper
132 132
133 133
134 134 class MercurialFactory(RepoFactory):
135 135 repo_type = 'hg'
136 136
137 137 def _create_config(self, config, hooks=True):
138 138 if not hooks:
139 139 hooks_to_clean = frozenset((
140 140 'changegroup.repo_size', 'preoutgoing.pre_pull',
141 141 'outgoing.pull_logger', 'prechangegroup.pre_push'))
142 142 new_config = []
143 143 for section, option, value in config:
144 144 if section == 'hooks' and option in hooks_to_clean:
145 145 continue
146 146 new_config.append((section, option, value))
147 147 config = new_config
148 148
149 149 baseui = make_ui_from_config(config)
150 150 return baseui
151 151
152 152 def _create_repo(self, wire, create):
153 153 baseui = self._create_config(wire["config"])
154 154 return instance(baseui, ascii_bytes(wire["path"]), create)
155 155
156 156 def repo(self, wire, create=False):
157 157 """
158 158 Get a repository instance for the given path.
159 159 """
160 160 return self._create_repo(wire, create)
161 161
162 162
163 163 def patch_ui_message_output(baseui):
164 164 baseui.setconfig(b'ui', b'quiet', b'false')
165 165 output = io.BytesIO()
166 166
167 167 def write(data, **unused_kwargs):
168 168 output.write(data)
169 169
170 170 baseui.status = write
171 171 baseui.write = write
172 172 baseui.warn = write
173 173 baseui.debug = write
174 174
175 175 return baseui, output
176 176
177 177
178 178 class HgRemote(RemoteBase):
179 179
180 180 def __init__(self, factory):
181 181 self._factory = factory
182 182 self._bulk_methods = {
183 183 "affected_files": self.ctx_files,
184 184 "author": self.ctx_user,
185 185 "branch": self.ctx_branch,
186 186 "children": self.ctx_children,
187 187 "date": self.ctx_date,
188 188 "message": self.ctx_description,
189 189 "parents": self.ctx_parents,
190 190 "status": self.ctx_status,
191 191 "obsolete": self.ctx_obsolete,
192 192 "phase": self.ctx_phase,
193 193 "hidden": self.ctx_hidden,
194 194 "_file_paths": self.ctx_list,
195 195 }
196 196
197 197 def _get_ctx(self, repo, ref):
198 198 return get_ctx(repo, ref)
199 199
200 200 @reraise_safe_exceptions
201 201 def discover_hg_version(self):
202 202 from mercurial import util
203 203 return safe_str(util.version())
204 204
205 205 @reraise_safe_exceptions
206 206 def is_empty(self, wire):
207 207 repo = self._factory.repo(wire)
208 208
209 209 try:
210 210 return len(repo) == 0
211 211 except Exception:
212 212 log.exception("failed to read object_store")
213 213 return False
214 214
215 215 @reraise_safe_exceptions
216 216 def bookmarks(self, wire):
217 217 cache_on, context_uid, repo_id = self._cache_on(wire)
218 218 region = self._region(wire)
219 219
220 220 @region.conditional_cache_on_arguments(condition=cache_on)
221 221 def _bookmarks(_context_uid, _repo_id):
222 222 repo = self._factory.repo(wire)
223 223 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
224 224
225 225 return _bookmarks(context_uid, repo_id)
226 226
227 227 @reraise_safe_exceptions
228 228 def branches(self, wire, normal, closed):
229 229 cache_on, context_uid, repo_id = self._cache_on(wire)
230 230 region = self._region(wire)
231 231
232 232 @region.conditional_cache_on_arguments(condition=cache_on)
233 233 def _branches(_context_uid, _repo_id, _normal, _closed):
234 234 repo = self._factory.repo(wire)
235 235 iter_branches = repo.branchmap().iterbranches()
236 236 bt = {}
237 237 for branch_name, _heads, tip_node, is_closed in iter_branches:
238 238 if normal and not is_closed:
239 239 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
240 240 if closed and is_closed:
241 241 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
242 242
243 243 return bt
244 244
245 245 return _branches(context_uid, repo_id, normal, closed)
246 246
247 247 @reraise_safe_exceptions
248 248 def bulk_request(self, wire, commit_id, pre_load):
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 _bulk_request(_repo_id, _commit_id, _pre_load):
254 254 result = {}
255 255 for attr in pre_load:
256 256 try:
257 257 method = self._bulk_methods[attr]
258 258 result[attr] = method(wire, commit_id)
259 259 except KeyError as e:
260 260 raise exceptions.VcsException(e)(
261 261 'Unknown bulk attribute: "%s"' % attr)
262 262 return result
263 263
264 264 return _bulk_request(repo_id, commit_id, sorted(pre_load))
265 265
266 266 @reraise_safe_exceptions
267 267 def ctx_branch(self, wire, commit_id):
268 268 cache_on, context_uid, repo_id = self._cache_on(wire)
269 269 region = self._region(wire)
270 270
271 271 @region.conditional_cache_on_arguments(condition=cache_on)
272 272 def _ctx_branch(_repo_id, _commit_id):
273 273 repo = self._factory.repo(wire)
274 274 ctx = self._get_ctx(repo, commit_id)
275 275 return ctx.branch()
276 276 return _ctx_branch(repo_id, commit_id)
277 277
278 278 @reraise_safe_exceptions
279 279 def ctx_date(self, wire, commit_id):
280 280 cache_on, context_uid, repo_id = self._cache_on(wire)
281 281 region = self._region(wire)
282 282
283 283 @region.conditional_cache_on_arguments(condition=cache_on)
284 284 def _ctx_date(_repo_id, _commit_id):
285 285 repo = self._factory.repo(wire)
286 286 ctx = self._get_ctx(repo, commit_id)
287 287 return ctx.date()
288 288 return _ctx_date(repo_id, commit_id)
289 289
290 290 @reraise_safe_exceptions
291 291 def ctx_description(self, wire, revision):
292 292 repo = self._factory.repo(wire)
293 293 ctx = self._get_ctx(repo, revision)
294 294 return ctx.description()
295 295
296 296 @reraise_safe_exceptions
297 297 def ctx_files(self, wire, commit_id):
298 298 cache_on, context_uid, repo_id = self._cache_on(wire)
299 299 region = self._region(wire)
300 300
301 301 @region.conditional_cache_on_arguments(condition=cache_on)
302 302 def _ctx_files(_repo_id, _commit_id):
303 303 repo = self._factory.repo(wire)
304 304 ctx = self._get_ctx(repo, commit_id)
305 305 return ctx.files()
306 306
307 307 return _ctx_files(repo_id, commit_id)
308 308
309 309 @reraise_safe_exceptions
310 310 def ctx_list(self, path, revision):
311 311 repo = self._factory.repo(path)
312 312 ctx = self._get_ctx(repo, revision)
313 313 return list(ctx)
314 314
315 315 @reraise_safe_exceptions
316 316 def ctx_parents(self, wire, commit_id):
317 317 cache_on, context_uid, repo_id = self._cache_on(wire)
318 318 region = self._region(wire)
319 319
320 320 @region.conditional_cache_on_arguments(condition=cache_on)
321 321 def _ctx_parents(_repo_id, _commit_id):
322 322 repo = self._factory.repo(wire)
323 323 ctx = self._get_ctx(repo, commit_id)
324 324 return [parent.hex() for parent in ctx.parents()
325 325 if not (parent.hidden() or parent.obsolete())]
326 326
327 327 return _ctx_parents(repo_id, commit_id)
328 328
329 329 @reraise_safe_exceptions
330 330 def ctx_children(self, wire, commit_id):
331 331 cache_on, context_uid, repo_id = self._cache_on(wire)
332 332 region = self._region(wire)
333 333
334 334 @region.conditional_cache_on_arguments(condition=cache_on)
335 335 def _ctx_children(_repo_id, _commit_id):
336 336 repo = self._factory.repo(wire)
337 337 ctx = self._get_ctx(repo, commit_id)
338 338 return [child.hex() for child in ctx.children()
339 339 if not (child.hidden() or child.obsolete())]
340 340
341 341 return _ctx_children(repo_id, commit_id)
342 342
343 343 @reraise_safe_exceptions
344 344 def ctx_phase(self, wire, commit_id):
345 345 cache_on, context_uid, repo_id = self._cache_on(wire)
346 346 region = self._region(wire)
347 347
348 348 @region.conditional_cache_on_arguments(condition=cache_on)
349 349 def _ctx_phase(_context_uid, _repo_id, _commit_id):
350 350 repo = self._factory.repo(wire)
351 351 ctx = self._get_ctx(repo, commit_id)
352 352 # public=0, draft=1, secret=3
353 353 return ctx.phase()
354 354 return _ctx_phase(context_uid, repo_id, commit_id)
355 355
356 356 @reraise_safe_exceptions
357 357 def ctx_obsolete(self, wire, commit_id):
358 358 cache_on, context_uid, repo_id = self._cache_on(wire)
359 359 region = self._region(wire)
360 360
361 361 @region.conditional_cache_on_arguments(condition=cache_on)
362 362 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
363 363 repo = self._factory.repo(wire)
364 364 ctx = self._get_ctx(repo, commit_id)
365 365 return ctx.obsolete()
366 366 return _ctx_obsolete(context_uid, repo_id, commit_id)
367 367
368 368 @reraise_safe_exceptions
369 369 def ctx_hidden(self, wire, commit_id):
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 _ctx_hidden(_context_uid, _repo_id, _commit_id):
375 375 repo = self._factory.repo(wire)
376 376 ctx = self._get_ctx(repo, commit_id)
377 377 return ctx.hidden()
378 378 return _ctx_hidden(context_uid, repo_id, commit_id)
379 379
380 380 @reraise_safe_exceptions
381 381 def ctx_substate(self, wire, revision):
382 382 repo = self._factory.repo(wire)
383 383 ctx = self._get_ctx(repo, revision)
384 384 return ctx.substate
385 385
386 386 @reraise_safe_exceptions
387 387 def ctx_status(self, wire, revision):
388 388 repo = self._factory.repo(wire)
389 389 ctx = self._get_ctx(repo, revision)
390 390 status = repo[ctx.p1().node()].status(other=ctx.node())
391 391 # object of status (odd, custom named tuple in mercurial) is not
392 392 # correctly serializable, we make it a list, as the underling
393 393 # API expects this to be a list
394 394 return list(status)
395 395
396 396 @reraise_safe_exceptions
397 397 def ctx_user(self, wire, revision):
398 398 repo = self._factory.repo(wire)
399 399 ctx = self._get_ctx(repo, revision)
400 400 return ctx.user()
401 401
402 402 @reraise_safe_exceptions
403 403 def check_url(self, url, config):
404 404 _proto = None
405 405 if '+' in url[:url.find('://')]:
406 406 _proto = url[0:url.find('+')]
407 407 url = url[url.find('+') + 1:]
408 408 handlers = []
409 409 url_obj = url_parser(url)
410 410 test_uri, authinfo = url_obj.authinfo()
411 411 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
412 412 url_obj.query = obfuscate_qs(url_obj.query)
413 413
414 414 cleaned_uri = str(url_obj)
415 415 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
416 416
417 417 if authinfo:
418 418 # create a password manager
419 419 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
420 420 passmgr.add_password(*authinfo)
421 421
422 422 handlers.extend((httpbasicauthhandler(passmgr),
423 423 httpdigestauthhandler(passmgr)))
424 424
425 425 o = urllib.request.build_opener(*handlers)
426 426 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
427 427 ('Accept', 'application/mercurial-0.1')]
428 428
429 429 q = {"cmd": 'between'}
430 430 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
431 431 qs = '?%s' % urllib.parse.urlencode(q)
432 432 cu = "%s%s" % (test_uri, qs)
433 433 req = urllib.request.Request(cu, None, {})
434 434
435 435 try:
436 436 log.debug("Trying to open URL %s", cleaned_uri)
437 437 resp = o.open(req)
438 438 if resp.code != 200:
439 439 raise exceptions.URLError()('Return Code is not 200')
440 440 except Exception as e:
441 441 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
442 442 # means it cannot be cloned
443 443 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
444 444
445 445 # now check if it's a proper hg repo, but don't do it for svn
446 446 try:
447 447 if _proto == 'svn':
448 448 pass
449 449 else:
450 450 # check for pure hg repos
451 451 log.debug(
452 452 "Verifying if URL is a Mercurial repository: %s",
453 453 cleaned_uri)
454 454 ui = make_ui_from_config(config)
455 455 peer_checker = makepeer(ui, url)
456 456 peer_checker.lookup('tip')
457 457 except Exception as e:
458 458 log.warning("URL is not a valid Mercurial repository: %s",
459 459 cleaned_uri)
460 460 raise exceptions.URLError(e)(
461 461 "url [%s] does not look like an hg repo org_exc: %s"
462 462 % (cleaned_uri, e))
463 463
464 464 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
465 465 return True
466 466
467 467 @reraise_safe_exceptions
468 468 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
469 469 repo = self._factory.repo(wire)
470 470
471 471 if file_filter:
472 472 match_filter = match(file_filter[0], '', [file_filter[1]])
473 473 else:
474 474 match_filter = file_filter
475 475 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
476 476
477 477 try:
478 478 diff_iter = patch.diff(
479 479 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
480 480 return b"".join(diff_iter)
481 481 except RepoLookupError as e:
482 482 raise exceptions.LookupException(e)()
483 483
484 484 @reraise_safe_exceptions
485 485 def node_history(self, wire, revision, path, limit):
486 486 cache_on, context_uid, repo_id = self._cache_on(wire)
487 487 region = self._region(wire)
488 488
489 489 @region.conditional_cache_on_arguments(condition=cache_on)
490 490 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
491 491 repo = self._factory.repo(wire)
492 492
493 493 ctx = self._get_ctx(repo, revision)
494 494 fctx = ctx.filectx(safe_bytes(path))
495 495
496 496 def history_iter():
497 497 limit_rev = fctx.rev()
498 498 for obj in reversed(list(fctx.filelog())):
499 499 obj = fctx.filectx(obj)
500 500 ctx = obj.changectx()
501 501 if ctx.hidden() or ctx.obsolete():
502 502 continue
503 503
504 504 if limit_rev >= obj.rev():
505 505 yield obj
506 506
507 507 history = []
508 508 for cnt, obj in enumerate(history_iter()):
509 509 if limit and cnt >= limit:
510 510 break
511 511 history.append(hex(obj.node()))
512 512
513 513 return [x for x in history]
514 514 return _node_history(context_uid, repo_id, revision, path, limit)
515 515
516 516 @reraise_safe_exceptions
517 517 def node_history_untill(self, wire, revision, path, limit):
518 518 cache_on, context_uid, repo_id = self._cache_on(wire)
519 519 region = self._region(wire)
520 520
521 521 @region.conditional_cache_on_arguments(condition=cache_on)
522 522 def _node_history_until(_context_uid, _repo_id):
523 523 repo = self._factory.repo(wire)
524 524 ctx = self._get_ctx(repo, revision)
525 525 fctx = ctx.filectx(safe_bytes(path))
526 526
527 527 file_log = list(fctx.filelog())
528 528 if limit:
529 529 # Limit to the last n items
530 530 file_log = file_log[-limit:]
531 531
532 532 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
533 533 return _node_history_until(context_uid, repo_id, revision, path, limit)
534 534
535 535 @reraise_safe_exceptions
536 536 def fctx_annotate(self, wire, revision, path):
537 537 repo = self._factory.repo(wire)
538 538 ctx = self._get_ctx(repo, revision)
539 539 fctx = ctx.filectx(safe_bytes(path))
540 540
541 541 result = []
542 542 for i, annotate_obj in enumerate(fctx.annotate(), 1):
543 543 ln_no = i
544 544 sha = hex(annotate_obj.fctx.node())
545 545 content = annotate_obj.text
546 546 result.append((ln_no, sha, content))
547 547 return result
548 548
549 549 @reraise_safe_exceptions
550 550 def fctx_node_data(self, wire, revision, path):
551 551 repo = self._factory.repo(wire)
552 552 ctx = self._get_ctx(repo, revision)
553 553 fctx = ctx.filectx(safe_bytes(path))
554 554 return fctx.data()
555 555
556 556 @reraise_safe_exceptions
557 557 def fctx_flags(self, wire, commit_id, path):
558 558 cache_on, context_uid, repo_id = self._cache_on(wire)
559 559 region = self._region(wire)
560 560
561 561 @region.conditional_cache_on_arguments(condition=cache_on)
562 562 def _fctx_flags(_repo_id, _commit_id, _path):
563 563 repo = self._factory.repo(wire)
564 564 ctx = self._get_ctx(repo, commit_id)
565 565 fctx = ctx.filectx(safe_bytes(path))
566 566 return fctx.flags()
567 567
568 568 return _fctx_flags(repo_id, commit_id, path)
569 569
570 570 @reraise_safe_exceptions
571 571 def fctx_size(self, wire, commit_id, path):
572 572 cache_on, context_uid, repo_id = self._cache_on(wire)
573 573 region = self._region(wire)
574 574
575 575 @region.conditional_cache_on_arguments(condition=cache_on)
576 576 def _fctx_size(_repo_id, _revision, _path):
577 577 repo = self._factory.repo(wire)
578 578 ctx = self._get_ctx(repo, commit_id)
579 579 fctx = ctx.filectx(safe_bytes(path))
580 580 return fctx.size()
581 581 return _fctx_size(repo_id, commit_id, path)
582 582
583 583 @reraise_safe_exceptions
584 584 def get_all_commit_ids(self, wire, name):
585 585 cache_on, context_uid, repo_id = self._cache_on(wire)
586 586 region = self._region(wire)
587 587
588 588 @region.conditional_cache_on_arguments(condition=cache_on)
589 589 def _get_all_commit_ids(_context_uid, _repo_id, _name):
590 590 repo = self._factory.repo(wire)
591 591 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
592 592 return revs
593 593 return _get_all_commit_ids(context_uid, repo_id, name)
594 594
595 595 @reraise_safe_exceptions
596 596 def get_config_value(self, wire, section, name, untrusted=False):
597 597 repo = self._factory.repo(wire)
598 598 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
599 599
600 600 @reraise_safe_exceptions
601 601 def is_large_file(self, wire, commit_id, path):
602 602 cache_on, context_uid, repo_id = self._cache_on(wire)
603 603 region = self._region(wire)
604 604
605 605 @region.conditional_cache_on_arguments(condition=cache_on)
606 606 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
607 607 return largefiles.lfutil.isstandin(safe_bytes(path))
608 608
609 609 return _is_large_file(context_uid, repo_id, commit_id, path)
610 610
611 611 @reraise_safe_exceptions
612 612 def is_binary(self, wire, revision, path):
613 613 cache_on, context_uid, repo_id = self._cache_on(wire)
614 614 region = self._region(wire)
615 615
616 616 @region.conditional_cache_on_arguments(condition=cache_on)
617 617 def _is_binary(_repo_id, _sha, _path):
618 618 repo = self._factory.repo(wire)
619 619 ctx = self._get_ctx(repo, revision)
620 620 fctx = ctx.filectx(safe_bytes(path))
621 621 return fctx.isbinary()
622 622
623 623 return _is_binary(repo_id, revision, path)
624 624
625 625 @reraise_safe_exceptions
626 def md5_hash(self, wire, revision, path):
627 cache_on, context_uid, repo_id = self._cache_on(wire)
628 region = self._region(wire)
629
630 @region.conditional_cache_on_arguments(condition=cache_on)
631 def _md5_hash(_repo_id, _sha, _path):
632 repo = self._factory.repo(wire)
633 ctx = self._get_ctx(repo, revision)
634 fctx = ctx.filectx(safe_bytes(path))
635 return hashlib.md5(fctx.data()).hexdigest()
636
637 return _md5_hash(repo_id, revision, path)
638
639 @reraise_safe_exceptions
626 640 def in_largefiles_store(self, wire, sha):
627 641 repo = self._factory.repo(wire)
628 642 return largefiles.lfutil.instore(repo, sha)
629 643
630 644 @reraise_safe_exceptions
631 645 def in_user_cache(self, wire, sha):
632 646 repo = self._factory.repo(wire)
633 647 return largefiles.lfutil.inusercache(repo.ui, sha)
634 648
635 649 @reraise_safe_exceptions
636 650 def store_path(self, wire, sha):
637 651 repo = self._factory.repo(wire)
638 652 return largefiles.lfutil.storepath(repo, sha)
639 653
640 654 @reraise_safe_exceptions
641 655 def link(self, wire, sha, path):
642 656 repo = self._factory.repo(wire)
643 657 largefiles.lfutil.link(
644 658 largefiles.lfutil.usercachepath(repo.ui, sha), path)
645 659
646 660 @reraise_safe_exceptions
647 661 def localrepository(self, wire, create=False):
648 662 self._factory.repo(wire, create=create)
649 663
650 664 @reraise_safe_exceptions
651 665 def lookup(self, wire, revision, both):
652 666 cache_on, context_uid, repo_id = self._cache_on(wire)
653 667 region = self._region(wire)
654 668
655 669 @region.conditional_cache_on_arguments(condition=cache_on)
656 670 def _lookup(_context_uid, _repo_id, _revision, _both):
657 671
658 672 repo = self._factory.repo(wire)
659 673 rev = _revision
660 674 if isinstance(rev, int):
661 675 # NOTE(marcink):
662 676 # since Mercurial doesn't support negative indexes properly
663 677 # we need to shift accordingly by one to get proper index, e.g
664 678 # repo[-1] => repo[-2]
665 679 # repo[0] => repo[-1]
666 680 if rev <= 0:
667 681 rev = rev + -1
668 682 try:
669 683 ctx = self._get_ctx(repo, rev)
670 684 except (TypeError, RepoLookupError) as e:
671 685 e._org_exc_tb = traceback.format_exc()
672 686 raise exceptions.LookupException(e)(rev)
673 687 except LookupError as e:
674 688 e._org_exc_tb = traceback.format_exc()
675 689 raise exceptions.LookupException(e)(e.name)
676 690
677 691 if not both:
678 692 return ctx.hex()
679 693
680 694 ctx = repo[ctx.hex()]
681 695 return ctx.hex(), ctx.rev()
682 696
683 697 return _lookup(context_uid, repo_id, revision, both)
684 698
685 699 @reraise_safe_exceptions
686 700 def sync_push(self, wire, url):
687 701 if not self.check_url(url, wire['config']):
688 702 return
689 703
690 704 repo = self._factory.repo(wire)
691 705
692 706 # Disable any prompts for this repo
693 707 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
694 708
695 709 bookmarks = list(dict(repo._bookmarks).keys())
696 710 remote = peer(repo, {}, safe_bytes(url))
697 711 # Disable any prompts for this remote
698 712 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
699 713
700 714 return exchange.push(
701 715 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
702 716
703 717 @reraise_safe_exceptions
704 718 def revision(self, wire, rev):
705 719 repo = self._factory.repo(wire)
706 720 ctx = self._get_ctx(repo, rev)
707 721 return ctx.rev()
708 722
709 723 @reraise_safe_exceptions
710 724 def rev_range(self, wire, commit_filter):
711 725 cache_on, context_uid, repo_id = self._cache_on(wire)
712 726 region = self._region(wire)
713 727
714 728 @region.conditional_cache_on_arguments(condition=cache_on)
715 729 def _rev_range(_context_uid, _repo_id, _filter):
716 730 repo = self._factory.repo(wire)
717 731 revisions = [
718 732 ascii_str(repo[rev].hex())
719 733 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
720 734 ]
721 735 return revisions
722 736
723 737 return _rev_range(context_uid, repo_id, sorted(commit_filter))
724 738
725 739 @reraise_safe_exceptions
726 740 def rev_range_hash(self, wire, node):
727 741 repo = self._factory.repo(wire)
728 742
729 743 def get_revs(repo, rev_opt):
730 744 if rev_opt:
731 745 revs = revrange(repo, rev_opt)
732 746 if len(revs) == 0:
733 747 return (nullrev, nullrev)
734 748 return max(revs), min(revs)
735 749 else:
736 750 return len(repo) - 1, 0
737 751
738 752 stop, start = get_revs(repo, [node + ':'])
739 753 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
740 754 return revs
741 755
742 756 @reraise_safe_exceptions
743 757 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
744 758 other_path = kwargs.pop('other_path', None)
745 759
746 760 # case when we want to compare two independent repositories
747 761 if other_path and other_path != wire["path"]:
748 762 baseui = self._factory._create_config(wire["config"])
749 763 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
750 764 else:
751 765 repo = self._factory.repo(wire)
752 766 return list(repo.revs(rev_spec, *args))
753 767
754 768 @reraise_safe_exceptions
755 769 def verify(self, wire,):
756 770 repo = self._factory.repo(wire)
757 771 baseui = self._factory._create_config(wire['config'])
758 772
759 773 baseui, output = patch_ui_message_output(baseui)
760 774
761 775 repo.ui = baseui
762 776 verify.verify(repo)
763 777 return output.getvalue()
764 778
765 779 @reraise_safe_exceptions
766 780 def hg_update_cache(self, wire,):
767 781 repo = self._factory.repo(wire)
768 782 baseui = self._factory._create_config(wire['config'])
769 783 baseui, output = patch_ui_message_output(baseui)
770 784
771 785 repo.ui = baseui
772 786 with repo.wlock(), repo.lock():
773 787 repo.updatecaches(full=True)
774 788
775 789 return output.getvalue()
776 790
777 791 @reraise_safe_exceptions
778 792 def hg_rebuild_fn_cache(self, wire,):
779 793 repo = self._factory.repo(wire)
780 794 baseui = self._factory._create_config(wire['config'])
781 795 baseui, output = patch_ui_message_output(baseui)
782 796
783 797 repo.ui = baseui
784 798
785 799 repair.rebuildfncache(baseui, repo)
786 800
787 801 return output.getvalue()
788 802
789 803 @reraise_safe_exceptions
790 804 def tags(self, wire):
791 805 cache_on, context_uid, repo_id = self._cache_on(wire)
792 806 region = self._region(wire)
793 807
794 808 @region.conditional_cache_on_arguments(condition=cache_on)
795 809 def _tags(_context_uid, _repo_id):
796 810 repo = self._factory.repo(wire)
797 811 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
798 812
799 813 return _tags(context_uid, repo_id)
800 814
801 815 @reraise_safe_exceptions
802 816 def update(self, wire, node=None, clean=False):
803 817 repo = self._factory.repo(wire)
804 818 baseui = self._factory._create_config(wire['config'])
805 819 commands.update(baseui, repo, node=node, clean=clean)
806 820
807 821 @reraise_safe_exceptions
808 822 def identify(self, wire):
809 823 repo = self._factory.repo(wire)
810 824 baseui = self._factory._create_config(wire['config'])
811 825 output = io.BytesIO()
812 826 baseui.write = output.write
813 827 # This is required to get a full node id
814 828 baseui.debugflag = True
815 829 commands.identify(baseui, repo, id=True)
816 830
817 831 return output.getvalue()
818 832
819 833 @reraise_safe_exceptions
820 834 def heads(self, wire, branch=None):
821 835 repo = self._factory.repo(wire)
822 836 baseui = self._factory._create_config(wire['config'])
823 837 output = io.BytesIO()
824 838
825 839 def write(data, **unused_kwargs):
826 840 output.write(data)
827 841
828 842 baseui.write = write
829 843 if branch:
830 844 args = [safe_bytes(branch)]
831 845 else:
832 846 args = []
833 847 commands.heads(baseui, repo, template=b'{node} ', *args)
834 848
835 849 return output.getvalue()
836 850
837 851 @reraise_safe_exceptions
838 852 def ancestor(self, wire, revision1, revision2):
839 853 repo = self._factory.repo(wire)
840 854 changelog = repo.changelog
841 855 lookup = repo.lookup
842 856 a = changelog.ancestor(lookup(revision1), lookup(revision2))
843 857 return hex(a)
844 858
845 859 @reraise_safe_exceptions
846 860 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
847 861 baseui = self._factory._create_config(wire["config"], hooks=hooks)
848 862 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
849 863
850 864 @reraise_safe_exceptions
851 865 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
852 866
853 867 repo = self._factory.repo(wire)
854 868 baseui = self._factory._create_config(wire['config'])
855 869 publishing = baseui.configbool(b'phases', b'publish')
856 870
857 871 def _filectxfn(_repo, ctx, path: bytes):
858 872 """
859 873 Marks given path as added/changed/removed in a given _repo. This is
860 874 for internal mercurial commit function.
861 875 """
862 876
863 877 # check if this path is removed
864 878 if safe_str(path) in removed:
865 879 # returning None is a way to mark node for removal
866 880 return None
867 881
868 882 # check if this path is added
869 883 for node in updated:
870 884 if safe_bytes(node['path']) == path:
871 885 return memfilectx(
872 886 _repo,
873 887 changectx=ctx,
874 888 path=safe_bytes(node['path']),
875 889 data=safe_bytes(node['content']),
876 890 islink=False,
877 891 isexec=bool(node['mode'] & stat.S_IXUSR),
878 892 copysource=False)
879 893 abort_exc = exceptions.AbortException()
880 894 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
881 895
882 896 if publishing:
883 897 new_commit_phase = b'public'
884 898 else:
885 899 new_commit_phase = b'draft'
886 900 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
887 901 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
888 902 commit_ctx = memctx(
889 903 repo=repo,
890 904 parents=parents,
891 905 text=safe_bytes(message),
892 906 files=[safe_bytes(x) for x in files],
893 907 filectxfn=_filectxfn,
894 908 user=safe_bytes(user),
895 909 date=(commit_time, commit_timezone),
896 910 extra=kwargs)
897 911
898 912 n = repo.commitctx(commit_ctx)
899 913 new_id = hex(n)
900 914
901 915 return new_id
902 916
903 917 @reraise_safe_exceptions
904 918 def pull(self, wire, url, commit_ids=None):
905 919 repo = self._factory.repo(wire)
906 920 # Disable any prompts for this repo
907 921 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
908 922
909 923 remote = peer(repo, {}, safe_bytes(url))
910 924 # Disable any prompts for this remote
911 925 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
912 926
913 927 if commit_ids:
914 928 commit_ids = [bin(commit_id) for commit_id in commit_ids]
915 929
916 930 return exchange.pull(
917 931 repo, remote, heads=commit_ids, force=None).cgresult
918 932
919 933 @reraise_safe_exceptions
920 934 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
921 935 repo = self._factory.repo(wire)
922 936 baseui = self._factory._create_config(wire['config'], hooks=hooks)
923 937
924 938 # Mercurial internally has a lot of logic that checks ONLY if
925 939 # option is defined, we just pass those if they are defined then
926 940 opts = {}
927 941 if bookmark:
928 942 opts['bookmark'] = bookmark
929 943 if branch:
930 944 opts['branch'] = branch
931 945 if revision:
932 946 opts['rev'] = revision
933 947
934 948 commands.pull(baseui, repo, source, **opts)
935 949
936 950 @reraise_safe_exceptions
937 951 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
938 952 repo = self._factory.repo(wire)
939 953 baseui = self._factory._create_config(wire['config'], hooks=hooks)
940 954 commands.push(baseui, repo, dest=dest_path, rev=revisions,
941 955 new_branch=push_branches)
942 956
943 957 @reraise_safe_exceptions
944 958 def strip(self, wire, revision, update, backup):
945 959 repo = self._factory.repo(wire)
946 960 ctx = self._get_ctx(repo, revision)
947 961 hgext_strip(
948 962 repo.baseui, repo, ctx.node(), update=update, backup=backup)
949 963
950 964 @reraise_safe_exceptions
951 965 def get_unresolved_files(self, wire):
952 966 repo = self._factory.repo(wire)
953 967
954 968 log.debug('Calculating unresolved files for repo: %s', repo)
955 969 output = io.BytesIO()
956 970
957 971 def write(data, **unused_kwargs):
958 972 output.write(data)
959 973
960 974 baseui = self._factory._create_config(wire['config'])
961 975 baseui.write = write
962 976
963 977 commands.resolve(baseui, repo, list=True)
964 978 unresolved = output.getvalue().splitlines(0)
965 979 return unresolved
966 980
967 981 @reraise_safe_exceptions
968 982 def merge(self, wire, revision):
969 983 repo = self._factory.repo(wire)
970 984 baseui = self._factory._create_config(wire['config'])
971 985 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
972 986
973 987 # In case of sub repositories are used mercurial prompts the user in
974 988 # case of merge conflicts or different sub repository sources. By
975 989 # setting the interactive flag to `False` mercurial doesn't prompt the
976 990 # used but instead uses a default value.
977 991 repo.ui.setconfig(b'ui', b'interactive', False)
978 992 commands.merge(baseui, repo, rev=revision)
979 993
980 994 @reraise_safe_exceptions
981 995 def merge_state(self, wire):
982 996 repo = self._factory.repo(wire)
983 997 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
984 998
985 999 # In case of sub repositories are used mercurial prompts the user in
986 1000 # case of merge conflicts or different sub repository sources. By
987 1001 # setting the interactive flag to `False` mercurial doesn't prompt the
988 1002 # used but instead uses a default value.
989 1003 repo.ui.setconfig(b'ui', b'interactive', False)
990 1004 ms = hg_merge.mergestate(repo)
991 1005 return [x for x in ms.unresolved()]
992 1006
993 1007 @reraise_safe_exceptions
994 1008 def commit(self, wire, message, username, close_branch=False):
995 1009 repo = self._factory.repo(wire)
996 1010 baseui = self._factory._create_config(wire['config'])
997 1011 repo.ui.setconfig(b'ui', b'username', username)
998 1012 commands.commit(baseui, repo, message=message, close_branch=close_branch)
999 1013
1000 1014 @reraise_safe_exceptions
1001 1015 def rebase(self, wire, source=None, dest=None, abort=False):
1002 1016 repo = self._factory.repo(wire)
1003 1017 baseui = self._factory._create_config(wire['config'])
1004 1018 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1005 1019 # In case of sub repositories are used mercurial prompts the user in
1006 1020 # case of merge conflicts or different sub repository sources. By
1007 1021 # setting the interactive flag to `False` mercurial doesn't prompt the
1008 1022 # used but instead uses a default value.
1009 1023 repo.ui.setconfig(b'ui', b'interactive', False)
1010 1024 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
1011 1025
1012 1026 @reraise_safe_exceptions
1013 1027 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1014 1028 repo = self._factory.repo(wire)
1015 1029 ctx = self._get_ctx(repo, revision)
1016 1030 node = ctx.node()
1017 1031
1018 1032 date = (tag_time, tag_timezone)
1019 1033 try:
1020 1034 hg_tag.tag(repo, name, node, message, local, user, date)
1021 1035 except Abort as e:
1022 1036 log.exception("Tag operation aborted")
1023 1037 # Exception can contain unicode which we convert
1024 1038 raise exceptions.AbortException(e)(repr(e))
1025 1039
1026 1040 @reraise_safe_exceptions
1027 1041 def bookmark(self, wire, bookmark, revision=None):
1028 1042 repo = self._factory.repo(wire)
1029 1043 baseui = self._factory._create_config(wire['config'])
1030 1044 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
1031 1045
1032 1046 @reraise_safe_exceptions
1033 1047 def install_hooks(self, wire, force=False):
1034 1048 # we don't need any special hooks for Mercurial
1035 1049 pass
1036 1050
1037 1051 @reraise_safe_exceptions
1038 1052 def get_hooks_info(self, wire):
1039 1053 return {
1040 1054 'pre_version': vcsserver.__version__,
1041 1055 'post_version': vcsserver.__version__,
1042 1056 }
1043 1057
1044 1058 @reraise_safe_exceptions
1045 1059 def set_head_ref(self, wire, head_name):
1046 1060 pass
1047 1061
1048 1062 @reraise_safe_exceptions
1049 1063 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1050 1064 archive_dir_name, commit_id):
1051 1065
1052 1066 def file_walker(_commit_id, path):
1053 1067 repo = self._factory.repo(wire)
1054 1068 ctx = repo[_commit_id]
1055 1069 is_root = path in ['', '/']
1056 1070 if is_root:
1057 1071 matcher = alwaysmatcher(badfn=None)
1058 1072 else:
1059 1073 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1060 1074 file_iter = ctx.manifest().walk(matcher)
1061 1075
1062 1076 for fn in file_iter:
1063 1077 file_path = fn
1064 1078 flags = ctx.flags(fn)
1065 1079 mode = b'x' in flags and 0o755 or 0o644
1066 1080 is_link = b'l' in flags
1067 1081
1068 1082 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1069 1083
1070 1084 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1071 1085 archive_dir_name, commit_id)
1072 1086
@@ -1,864 +1,875 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 31 import svn.client
32 32 import svn.core
33 33 import svn.delta
34 34 import svn.diff
35 35 import svn.fs
36 36 import svn.repos
37 37
38 38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
40 40 from vcsserver.exceptions import NoContentException
41 41 from vcsserver.str_utils import safe_str
42 42 from vcsserver.vcs_base import RemoteBase
43 43 from vcsserver.lib.svnremoterepo import svnremoterepo
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 svn_compatible_versions_map = {
48 48 'pre-1.4-compatible': '1.3',
49 49 'pre-1.5-compatible': '1.4',
50 50 'pre-1.6-compatible': '1.5',
51 51 'pre-1.8-compatible': '1.7',
52 52 'pre-1.9-compatible': '1.8',
53 53 }
54 54
55 55 current_compatible_version = '1.14'
56 56
57 57
58 58 def reraise_safe_exceptions(func):
59 59 """Decorator for converting svn exceptions to something neutral."""
60 60 def wrapper(*args, **kwargs):
61 61 try:
62 62 return func(*args, **kwargs)
63 63 except Exception as e:
64 64 if not hasattr(e, '_vcs_kind'):
65 65 log.exception("Unhandled exception in svn remote call")
66 66 raise_from_original(exceptions.UnhandledException(e))
67 67 raise
68 68 return wrapper
69 69
70 70
71 71 class SubversionFactory(RepoFactory):
72 72 repo_type = 'svn'
73 73
74 74 def _create_repo(self, wire, create, compatible_version):
75 75 path = svn.core.svn_path_canonicalize(wire['path'])
76 76 if create:
77 77 fs_config = {'compatible-version': current_compatible_version}
78 78 if compatible_version:
79 79
80 80 compatible_version_string = \
81 81 svn_compatible_versions_map.get(compatible_version) \
82 82 or compatible_version
83 83 fs_config['compatible-version'] = compatible_version_string
84 84
85 85 log.debug('Create SVN repo with config "%s"', fs_config)
86 86 repo = svn.repos.create(path, "", "", None, fs_config)
87 87 else:
88 88 repo = svn.repos.open(path)
89 89
90 90 log.debug('Got SVN object: %s', repo)
91 91 return repo
92 92
93 93 def repo(self, wire, create=False, compatible_version=None):
94 94 """
95 95 Get a repository instance for the given path.
96 96 """
97 97 return self._create_repo(wire, create, compatible_version)
98 98
99 99
100 100 NODE_TYPE_MAPPING = {
101 101 svn.core.svn_node_file: 'file',
102 102 svn.core.svn_node_dir: 'dir',
103 103 }
104 104
105 105
106 106 class SvnRemote(RemoteBase):
107 107
108 108 def __init__(self, factory, hg_factory=None):
109 109 self._factory = factory
110 110
111 111 @reraise_safe_exceptions
112 112 def discover_svn_version(self):
113 113 try:
114 114 import svn.core
115 115 svn_ver = svn.core.SVN_VERSION
116 116 except ImportError:
117 117 svn_ver = None
118 118 return safe_str(svn_ver)
119 119
120 120 @reraise_safe_exceptions
121 121 def is_empty(self, wire):
122 122
123 123 try:
124 124 return self.lookup(wire, -1) == 0
125 125 except Exception:
126 126 log.exception("failed to read object_store")
127 127 return False
128 128
129 129 def check_url(self, url):
130 130
131 131 # uuid function get's only valid UUID from proper repo, else
132 132 # throws exception
133 133 username, password, src_url = self.get_url_and_credentials(url)
134 134 try:
135 135 svnremoterepo(username, password, src_url).svn().uuid
136 136 except Exception:
137 137 tb = traceback.format_exc()
138 138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 139 raise URLError(
140 140 '"%s" is not a valid Subversion source url.' % (url, ))
141 141 return True
142 142
143 143 def is_path_valid_repository(self, wire, path):
144 144
145 145 # NOTE(marcink): short circuit the check for SVN repo
146 146 # the repos.open might be expensive to check, but we have one cheap
147 147 # pre condition that we can use, to check for 'format' file
148 148
149 149 if not os.path.isfile(os.path.join(path, 'format')):
150 150 return False
151 151
152 152 try:
153 153 svn.repos.open(path)
154 154 except svn.core.SubversionException:
155 155 tb = traceback.format_exc()
156 156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 157 return False
158 158 return True
159 159
160 160 @reraise_safe_exceptions
161 161 def verify(self, wire,):
162 162 repo_path = wire['path']
163 163 if not self.is_path_valid_repository(wire, repo_path):
164 164 raise Exception(
165 165 "Path %s is not a valid Subversion repository." % repo_path)
166 166
167 167 cmd = ['svnadmin', 'info', repo_path]
168 168 stdout, stderr = subprocessio.run_command(cmd)
169 169 return stdout
170 170
171 171 def lookup(self, wire, revision):
172 172 if revision not in [-1, None, 'HEAD']:
173 173 raise NotImplementedError
174 174 repo = self._factory.repo(wire)
175 175 fs_ptr = svn.repos.fs(repo)
176 176 head = svn.fs.youngest_rev(fs_ptr)
177 177 return head
178 178
179 179 def lookup_interval(self, wire, start_ts, end_ts):
180 180 repo = self._factory.repo(wire)
181 181 fsobj = svn.repos.fs(repo)
182 182 start_rev = None
183 183 end_rev = None
184 184 if start_ts:
185 185 start_ts_svn = apr_time_t(start_ts)
186 186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 187 else:
188 188 start_rev = 1
189 189 if end_ts:
190 190 end_ts_svn = apr_time_t(end_ts)
191 191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 192 else:
193 193 end_rev = svn.fs.youngest_rev(fsobj)
194 194 return start_rev, end_rev
195 195
196 196 def revision_properties(self, wire, revision):
197 197
198 198 cache_on, context_uid, repo_id = self._cache_on(wire)
199 199 region = self._region(wire)
200 200 @region.conditional_cache_on_arguments(condition=cache_on)
201 201 def _revision_properties(_repo_id, _revision):
202 202 repo = self._factory.repo(wire)
203 203 fs_ptr = svn.repos.fs(repo)
204 204 return svn.fs.revision_proplist(fs_ptr, revision)
205 205 return _revision_properties(repo_id, revision)
206 206
207 207 def revision_changes(self, wire, revision):
208 208
209 209 repo = self._factory.repo(wire)
210 210 fsobj = svn.repos.fs(repo)
211 211 rev_root = svn.fs.revision_root(fsobj, revision)
212 212
213 213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 215 base_dir = ""
216 216 send_deltas = False
217 217 svn.repos.replay2(
218 218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 219 editor_ptr, editor_baton, None)
220 220
221 221 added = []
222 222 changed = []
223 223 removed = []
224 224
225 225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 226 for path, change in editor.changes.items():
227 227 # TODO: Decide what to do with directory nodes. Subversion can add
228 228 # empty directories.
229 229
230 230 if change.item_kind == svn.core.svn_node_dir:
231 231 continue
232 232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 233 added.append(path)
234 234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 235 svn.repos.CHANGE_ACTION_REPLACE]:
236 236 changed.append(path)
237 237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 238 removed.append(path)
239 239 else:
240 240 raise NotImplementedError(
241 241 "Action %s not supported on path %s" % (
242 242 change.action, path))
243 243
244 244 changes = {
245 245 'added': added,
246 246 'changed': changed,
247 247 'removed': removed,
248 248 }
249 249 return changes
250 250
251 251 @reraise_safe_exceptions
252 252 def node_history(self, wire, path, revision, limit):
253 253 cache_on, context_uid, repo_id = self._cache_on(wire)
254 254 region = self._region(wire)
255 255 @region.conditional_cache_on_arguments(condition=cache_on)
256 256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 257 cross_copies = False
258 258 repo = self._factory.repo(wire)
259 259 fsobj = svn.repos.fs(repo)
260 260 rev_root = svn.fs.revision_root(fsobj, revision)
261 261
262 262 history_revisions = []
263 263 history = svn.fs.node_history(rev_root, path)
264 264 history = svn.fs.history_prev(history, cross_copies)
265 265 while history:
266 266 __, node_revision = svn.fs.history_location(history)
267 267 history_revisions.append(node_revision)
268 268 if limit and len(history_revisions) >= limit:
269 269 break
270 270 history = svn.fs.history_prev(history, cross_copies)
271 271 return history_revisions
272 272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273 273
274 274 def node_properties(self, wire, path, revision):
275 275 cache_on, context_uid, repo_id = self._cache_on(wire)
276 276 region = self._region(wire)
277 277 @region.conditional_cache_on_arguments(condition=cache_on)
278 278 def _node_properties(_repo_id, _path, _revision):
279 279 repo = self._factory.repo(wire)
280 280 fsobj = svn.repos.fs(repo)
281 281 rev_root = svn.fs.revision_root(fsobj, revision)
282 282 return svn.fs.node_proplist(rev_root, path)
283 283 return _node_properties(repo_id, path, revision)
284 284
285 285 def file_annotate(self, wire, path, revision):
286 286 abs_path = 'file://' + urllib.request.pathname2url(
287 287 vcspath.join(wire['path'], path))
288 288 file_uri = svn.core.svn_path_canonicalize(abs_path)
289 289
290 290 start_rev = svn_opt_revision_value_t(0)
291 291 peg_rev = svn_opt_revision_value_t(revision)
292 292 end_rev = peg_rev
293 293
294 294 annotations = []
295 295
296 296 def receiver(line_no, revision, author, date, line, pool):
297 297 annotations.append((line_no, revision, line))
298 298
299 299 # TODO: Cannot use blame5, missing typemap function in the swig code
300 300 try:
301 301 svn.client.blame2(
302 302 file_uri, peg_rev, start_rev, end_rev,
303 303 receiver, svn.client.create_context())
304 304 except svn.core.SubversionException as exc:
305 305 log.exception("Error during blame operation.")
306 306 raise Exception(
307 307 "Blame not supported or file does not exist at path %s. "
308 308 "Error %s." % (path, exc))
309 309
310 310 return annotations
311 311
312 312 def get_node_type(self, wire, path, revision=None):
313 313
314 314 cache_on, context_uid, repo_id = self._cache_on(wire)
315 315 region = self._region(wire)
316 316 @region.conditional_cache_on_arguments(condition=cache_on)
317 317 def _get_node_type(_repo_id, _path, _revision):
318 318 repo = self._factory.repo(wire)
319 319 fs_ptr = svn.repos.fs(repo)
320 320 if _revision is None:
321 321 _revision = svn.fs.youngest_rev(fs_ptr)
322 322 root = svn.fs.revision_root(fs_ptr, _revision)
323 323 node = svn.fs.check_path(root, path)
324 324 return NODE_TYPE_MAPPING.get(node, None)
325 325 return _get_node_type(repo_id, path, revision)
326 326
327 327 def get_nodes(self, wire, path, revision=None):
328 328
329 329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 330 region = self._region(wire)
331 331 @region.conditional_cache_on_arguments(condition=cache_on)
332 332 def _get_nodes(_repo_id, _path, _revision):
333 333 repo = self._factory.repo(wire)
334 334 fsobj = svn.repos.fs(repo)
335 335 if _revision is None:
336 336 _revision = svn.fs.youngest_rev(fsobj)
337 337 root = svn.fs.revision_root(fsobj, _revision)
338 338 entries = svn.fs.dir_entries(root, path)
339 339 result = []
340 340 for entry_path, entry_info in entries.items():
341 341 result.append(
342 342 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
343 343 return result
344 344 return _get_nodes(repo_id, path, revision)
345 345
346 346 def get_file_content(self, wire, path, rev=None):
347 347 repo = self._factory.repo(wire)
348 348 fsobj = svn.repos.fs(repo)
349 349 if rev is None:
350 350 rev = svn.fs.youngest_revision(fsobj)
351 351 root = svn.fs.revision_root(fsobj, rev)
352 352 content = svn.core.Stream(svn.fs.file_contents(root, path))
353 353 return content.read()
354 354
355 355 def get_file_size(self, wire, path, revision=None):
356 356
357 357 cache_on, context_uid, repo_id = self._cache_on(wire)
358 358 region = self._region(wire)
359 359
360 360 @region.conditional_cache_on_arguments(condition=cache_on)
361 361 def _get_file_size(_repo_id, _path, _revision):
362 362 repo = self._factory.repo(wire)
363 363 fsobj = svn.repos.fs(repo)
364 364 if _revision is None:
365 365 _revision = svn.fs.youngest_revision(fsobj)
366 366 root = svn.fs.revision_root(fsobj, _revision)
367 367 size = svn.fs.file_length(root, path)
368 368 return size
369 369 return _get_file_size(repo_id, path, revision)
370 370
371 371 def create_repository(self, wire, compatible_version=None):
372 372 log.info('Creating Subversion repository in path "%s"', wire['path'])
373 373 self._factory.repo(wire, create=True,
374 374 compatible_version=compatible_version)
375 375
376 376 def get_url_and_credentials(self, src_url):
377 377 obj = urllib.parse.urlparse(src_url)
378 378 username = obj.username or None
379 379 password = obj.password or None
380 380 return username, password, src_url
381 381
382 382 def import_remote_repository(self, wire, src_url):
383 383 repo_path = wire['path']
384 384 if not self.is_path_valid_repository(wire, repo_path):
385 385 raise Exception(
386 386 "Path %s is not a valid Subversion repository." % repo_path)
387 387
388 388 username, password, src_url = self.get_url_and_credentials(src_url)
389 389 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
390 390 '--trust-server-cert-failures=unknown-ca']
391 391 if username and password:
392 392 rdump_cmd += ['--username', username, '--password', password]
393 393 rdump_cmd += [src_url]
394 394
395 395 rdump = subprocess.Popen(
396 396 rdump_cmd,
397 397 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
398 398 load = subprocess.Popen(
399 399 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
400 400
401 401 # TODO: johbo: This can be a very long operation, might be better
402 402 # to track some kind of status and provide an api to check if the
403 403 # import is done.
404 404 rdump.wait()
405 405 load.wait()
406 406
407 407 log.debug('Return process ended with code: %s', rdump.returncode)
408 408 if rdump.returncode != 0:
409 409 errors = rdump.stderr.read()
410 410 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
411 411
412 412 reason = 'UNKNOWN'
413 413 if b'svnrdump: E230001:' in errors:
414 414 reason = 'INVALID_CERTIFICATE'
415 415
416 416 if reason == 'UNKNOWN':
417 417 reason = 'UNKNOWN:{}'.format(safe_str(errors))
418 418
419 419 raise Exception(
420 420 'Failed to dump the remote repository from %s. Reason:%s' % (
421 421 src_url, reason))
422 422 if load.returncode != 0:
423 423 raise Exception(
424 424 'Failed to load the dump of remote repository from %s.' %
425 425 (src_url, ))
426 426
427 427 def commit(self, wire, message, author, timestamp, updated, removed):
428 428 assert isinstance(message, str)
429 429 assert isinstance(author, str)
430 430
431 431 repo = self._factory.repo(wire)
432 432 fsobj = svn.repos.fs(repo)
433 433
434 434 rev = svn.fs.youngest_rev(fsobj)
435 435 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
436 436 txn_root = svn.fs.txn_root(txn)
437 437
438 438 for node in updated:
439 439 TxnNodeProcessor(node, txn_root).update()
440 440 for node in removed:
441 441 TxnNodeProcessor(node, txn_root).remove()
442 442
443 443 commit_id = svn.repos.fs_commit_txn(repo, txn)
444 444
445 445 if timestamp:
446 446 apr_time = apr_time_t(timestamp)
447 447 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
448 448 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
449 449
450 450 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
451 451 return commit_id
452 452
453 453 def diff(self, wire, rev1, rev2, path1=None, path2=None,
454 454 ignore_whitespace=False, context=3):
455 455
456 456 wire.update(cache=False)
457 457 repo = self._factory.repo(wire)
458 458 diff_creator = SvnDiffer(
459 459 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
460 460 try:
461 461 return diff_creator.generate_diff()
462 462 except svn.core.SubversionException as e:
463 463 log.exception(
464 464 "Error during diff operation operation. "
465 465 "Path might not exist %s, %s" % (path1, path2))
466 466 return ""
467 467
468 468 @reraise_safe_exceptions
469 469 def is_large_file(self, wire, path):
470 470 return False
471 471
472 472 @reraise_safe_exceptions
473 473 def is_binary(self, wire, rev, path):
474 474 cache_on, context_uid, repo_id = self._cache_on(wire)
475 region = self._region(wire)
475 476
476 region = self._region(wire)
477 477 @region.conditional_cache_on_arguments(condition=cache_on)
478 478 def _is_binary(_repo_id, _rev, _path):
479 479 raw_bytes = self.get_file_content(wire, path, rev)
480 480 return raw_bytes and '\0' in raw_bytes
481 481
482 482 return _is_binary(repo_id, rev, path)
483 483
484 484 @reraise_safe_exceptions
485 def md5_hash(self, wire, rev, path):
486 cache_on, context_uid, repo_id = self._cache_on(wire)
487 region = self._region(wire)
488
489 @region.conditional_cache_on_arguments(condition=cache_on)
490 def _md5_hash(_repo_id, _rev, _path):
491 return ''
492
493 return _md5_hash(repo_id, rev, path)
494
495 @reraise_safe_exceptions
485 496 def run_svn_command(self, wire, cmd, **opts):
486 497 path = wire.get('path', None)
487 498
488 499 if path and os.path.isdir(path):
489 500 opts['cwd'] = path
490 501
491 502 safe_call = opts.pop('_safe', False)
492 503
493 504 svnenv = os.environ.copy()
494 505 svnenv.update(opts.pop('extra_env', {}))
495 506
496 507 _opts = {'env': svnenv, 'shell': False}
497 508
498 509 try:
499 510 _opts.update(opts)
500 511 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
501 512
502 513 return b''.join(proc), b''.join(proc.stderr)
503 514 except OSError as err:
504 515 if safe_call:
505 516 return '', safe_str(err).strip()
506 517 else:
507 518 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
508 519 tb_err = ("Couldn't run svn command (%s).\n"
509 520 "Original error was:%s\n"
510 521 "Call options:%s\n"
511 522 % (cmd, err, _opts))
512 523 log.exception(tb_err)
513 524 raise exceptions.VcsException()(tb_err)
514 525
515 526 @reraise_safe_exceptions
516 527 def install_hooks(self, wire, force=False):
517 528 from vcsserver.hook_utils import install_svn_hooks
518 529 repo_path = wire['path']
519 530 binary_dir = settings.BINARY_DIR
520 531 executable = None
521 532 if binary_dir:
522 533 executable = os.path.join(binary_dir, 'python')
523 534 return install_svn_hooks(
524 535 repo_path, executable=executable, force_create=force)
525 536
526 537 @reraise_safe_exceptions
527 538 def get_hooks_info(self, wire):
528 539 from vcsserver.hook_utils import (
529 540 get_svn_pre_hook_version, get_svn_post_hook_version)
530 541 repo_path = wire['path']
531 542 return {
532 543 'pre_version': get_svn_pre_hook_version(repo_path),
533 544 'post_version': get_svn_post_hook_version(repo_path),
534 545 }
535 546
536 547 @reraise_safe_exceptions
537 548 def set_head_ref(self, wire, head_name):
538 549 pass
539 550
540 551 @reraise_safe_exceptions
541 552 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
542 553 archive_dir_name, commit_id):
543 554
544 555 def walk_tree(root, root_dir, _commit_id):
545 556 """
546 557 Special recursive svn repo walker
547 558 """
548 559
549 560 filemode_default = 0o100644
550 561 filemode_executable = 0o100755
551 562
552 563 file_iter = svn.fs.dir_entries(root, root_dir)
553 564 for f_name in file_iter:
554 565 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
555 566
556 567 if f_type == 'dir':
557 568 # return only DIR, and then all entries in that dir
558 569 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
559 570 new_root = os.path.join(root_dir, f_name)
560 571 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
561 572 yield _f_name, _f_data, _f_type
562 573 else:
563 574 f_path = os.path.join(root_dir, f_name).rstrip('/')
564 575 prop_list = svn.fs.node_proplist(root, f_path)
565 576
566 577 f_mode = filemode_default
567 578 if prop_list.get('svn:executable'):
568 579 f_mode = filemode_executable
569 580
570 581 f_is_link = False
571 582 if prop_list.get('svn:special'):
572 583 f_is_link = True
573 584
574 585 data = {
575 586 'is_link': f_is_link,
576 587 'mode': f_mode,
577 588 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
578 589 }
579 590
580 591 yield f_path, data, f_type
581 592
582 593 def file_walker(_commit_id, path):
583 594 repo = self._factory.repo(wire)
584 595 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
585 596
586 597 def no_content():
587 598 raise NoContentException()
588 599
589 600 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
590 601 file_path = f_name
591 602
592 603 if f_type == 'dir':
593 604 mode = f_data['mode']
594 605 yield ArchiveNode(file_path, mode, False, no_content)
595 606 else:
596 607 mode = f_data['mode']
597 608 is_link = f_data['is_link']
598 609 data_stream = f_data['content_stream']
599 610 yield ArchiveNode(file_path, mode, is_link, data_stream)
600 611
601 612 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
602 613 archive_dir_name, commit_id)
603 614
604 615
605 616 class SvnDiffer(object):
606 617 """
607 618 Utility to create diffs based on difflib and the Subversion api
608 619 """
609 620
610 621 binary_content = False
611 622
612 623 def __init__(
613 624 self, repo, src_rev, src_path, tgt_rev, tgt_path,
614 625 ignore_whitespace, context):
615 626 self.repo = repo
616 627 self.ignore_whitespace = ignore_whitespace
617 628 self.context = context
618 629
619 630 fsobj = svn.repos.fs(repo)
620 631
621 632 self.tgt_rev = tgt_rev
622 633 self.tgt_path = tgt_path or ''
623 634 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
624 635 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
625 636
626 637 self.src_rev = src_rev
627 638 self.src_path = src_path or self.tgt_path
628 639 self.src_root = svn.fs.revision_root(fsobj, src_rev)
629 640 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
630 641
631 642 self._validate()
632 643
633 644 def _validate(self):
634 645 if (self.tgt_kind != svn.core.svn_node_none and
635 646 self.src_kind != svn.core.svn_node_none and
636 647 self.src_kind != self.tgt_kind):
637 648 # TODO: johbo: proper error handling
638 649 raise Exception(
639 650 "Source and target are not compatible for diff generation. "
640 651 "Source type: %s, target type: %s" %
641 652 (self.src_kind, self.tgt_kind))
642 653
643 654 def generate_diff(self):
644 655 buf = io.StringIO()
645 656 if self.tgt_kind == svn.core.svn_node_dir:
646 657 self._generate_dir_diff(buf)
647 658 else:
648 659 self._generate_file_diff(buf)
649 660 return buf.getvalue()
650 661
651 662 def _generate_dir_diff(self, buf):
652 663 editor = DiffChangeEditor()
653 664 editor_ptr, editor_baton = svn.delta.make_editor(editor)
654 665 svn.repos.dir_delta2(
655 666 self.src_root,
656 667 self.src_path,
657 668 '', # src_entry
658 669 self.tgt_root,
659 670 self.tgt_path,
660 671 editor_ptr, editor_baton,
661 672 authorization_callback_allow_all,
662 673 False, # text_deltas
663 674 svn.core.svn_depth_infinity, # depth
664 675 False, # entry_props
665 676 False, # ignore_ancestry
666 677 )
667 678
668 679 for path, __, change in sorted(editor.changes):
669 680 self._generate_node_diff(
670 681 buf, change, path, self.tgt_path, path, self.src_path)
671 682
672 683 def _generate_file_diff(self, buf):
673 684 change = None
674 685 if self.src_kind == svn.core.svn_node_none:
675 686 change = "add"
676 687 elif self.tgt_kind == svn.core.svn_node_none:
677 688 change = "delete"
678 689 tgt_base, tgt_path = vcspath.split(self.tgt_path)
679 690 src_base, src_path = vcspath.split(self.src_path)
680 691 self._generate_node_diff(
681 692 buf, change, tgt_path, tgt_base, src_path, src_base)
682 693
683 694 def _generate_node_diff(
684 695 self, buf, change, tgt_path, tgt_base, src_path, src_base):
685 696
686 697 if self.src_rev == self.tgt_rev and tgt_base == src_base:
687 698 # makes consistent behaviour with git/hg to return empty diff if
688 699 # we compare same revisions
689 700 return
690 701
691 702 tgt_full_path = vcspath.join(tgt_base, tgt_path)
692 703 src_full_path = vcspath.join(src_base, src_path)
693 704
694 705 self.binary_content = False
695 706 mime_type = self._get_mime_type(tgt_full_path)
696 707
697 708 if mime_type and not mime_type.startswith('text'):
698 709 self.binary_content = True
699 710 buf.write("=" * 67 + '\n')
700 711 buf.write("Cannot display: file marked as a binary type.\n")
701 712 buf.write("svn:mime-type = %s\n" % mime_type)
702 713 buf.write("Index: %s\n" % (tgt_path, ))
703 714 buf.write("=" * 67 + '\n')
704 715 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
705 716 'tgt_path': tgt_path})
706 717
707 718 if change == 'add':
708 719 # TODO: johbo: SVN is missing a zero here compared to git
709 720 buf.write("new file mode 10644\n")
710 721
711 722 #TODO(marcink): intro to binary detection of svn patches
712 723 # if self.binary_content:
713 724 # buf.write('GIT binary patch\n')
714 725
715 726 buf.write("--- /dev/null\t(revision 0)\n")
716 727 src_lines = []
717 728 else:
718 729 if change == 'delete':
719 730 buf.write("deleted file mode 10644\n")
720 731
721 732 #TODO(marcink): intro to binary detection of svn patches
722 733 # if self.binary_content:
723 734 # buf.write('GIT binary patch\n')
724 735
725 736 buf.write("--- a/%s\t(revision %s)\n" % (
726 737 src_path, self.src_rev))
727 738 src_lines = self._svn_readlines(self.src_root, src_full_path)
728 739
729 740 if change == 'delete':
730 741 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
731 742 tgt_lines = []
732 743 else:
733 744 buf.write("+++ b/%s\t(revision %s)\n" % (
734 745 tgt_path, self.tgt_rev))
735 746 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
736 747
737 748 if not self.binary_content:
738 749 udiff = svn_diff.unified_diff(
739 750 src_lines, tgt_lines, context=self.context,
740 751 ignore_blank_lines=self.ignore_whitespace,
741 752 ignore_case=False,
742 753 ignore_space_changes=self.ignore_whitespace)
743 754 buf.writelines(udiff)
744 755
745 756 def _get_mime_type(self, path):
746 757 try:
747 758 mime_type = svn.fs.node_prop(
748 759 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
749 760 except svn.core.SubversionException:
750 761 mime_type = svn.fs.node_prop(
751 762 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
752 763 return mime_type
753 764
754 765 def _svn_readlines(self, fs_root, node_path):
755 766 if self.binary_content:
756 767 return []
757 768 node_kind = svn.fs.check_path(fs_root, node_path)
758 769 if node_kind not in (
759 770 svn.core.svn_node_file, svn.core.svn_node_symlink):
760 771 return []
761 772 content = svn.core.Stream(
762 773 svn.fs.file_contents(fs_root, node_path)).read()
763 774 return content.splitlines(True)
764 775
765 776
766 777 class DiffChangeEditor(svn.delta.Editor):
767 778 """
768 779 Records changes between two given revisions
769 780 """
770 781
771 782 def __init__(self):
772 783 self.changes = []
773 784
774 785 def delete_entry(self, path, revision, parent_baton, pool=None):
775 786 self.changes.append((path, None, 'delete'))
776 787
777 788 def add_file(
778 789 self, path, parent_baton, copyfrom_path, copyfrom_revision,
779 790 file_pool=None):
780 791 self.changes.append((path, 'file', 'add'))
781 792
782 793 def open_file(self, path, parent_baton, base_revision, file_pool=None):
783 794 self.changes.append((path, 'file', 'change'))
784 795
785 796
786 797 def authorization_callback_allow_all(root, path, pool):
787 798 return True
788 799
789 800
790 801 class TxnNodeProcessor(object):
791 802 """
792 803 Utility to process the change of one node within a transaction root.
793 804
794 805 It encapsulates the knowledge of how to add, update or remove
795 806 a node for a given transaction root. The purpose is to support the method
796 807 `SvnRemote.commit`.
797 808 """
798 809
799 810 def __init__(self, node, txn_root):
800 811 assert isinstance(node['path'], str)
801 812
802 813 self.node = node
803 814 self.txn_root = txn_root
804 815
805 816 def update(self):
806 817 self._ensure_parent_dirs()
807 818 self._add_file_if_node_does_not_exist()
808 819 self._update_file_content()
809 820 self._update_file_properties()
810 821
811 822 def remove(self):
812 823 svn.fs.delete(self.txn_root, self.node['path'])
813 824 # TODO: Clean up directory if empty
814 825
815 826 def _ensure_parent_dirs(self):
816 827 curdir = vcspath.dirname(self.node['path'])
817 828 dirs_to_create = []
818 829 while not self._svn_path_exists(curdir):
819 830 dirs_to_create.append(curdir)
820 831 curdir = vcspath.dirname(curdir)
821 832
822 833 for curdir in reversed(dirs_to_create):
823 834 log.debug('Creating missing directory "%s"', curdir)
824 835 svn.fs.make_dir(self.txn_root, curdir)
825 836
826 837 def _svn_path_exists(self, path):
827 838 path_status = svn.fs.check_path(self.txn_root, path)
828 839 return path_status != svn.core.svn_node_none
829 840
830 841 def _add_file_if_node_does_not_exist(self):
831 842 kind = svn.fs.check_path(self.txn_root, self.node['path'])
832 843 if kind == svn.core.svn_node_none:
833 844 svn.fs.make_file(self.txn_root, self.node['path'])
834 845
835 846 def _update_file_content(self):
836 847 assert isinstance(self.node['content'], str)
837 848 handler, baton = svn.fs.apply_textdelta(
838 849 self.txn_root, self.node['path'], None, None)
839 850 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
840 851
841 852 def _update_file_properties(self):
842 853 properties = self.node.get('properties', {})
843 854 for key, value in properties.items():
844 855 svn.fs.change_node_prop(
845 856 self.txn_root, self.node['path'], key, value)
846 857
847 858
848 859 def apr_time_t(timestamp):
849 860 """
850 861 Convert a Python timestamp into APR timestamp type apr_time_t
851 862 """
852 863 return timestamp * 1E6
853 864
854 865
855 866 def svn_opt_revision_value_t(num):
856 867 """
857 868 Put `num` into a `svn_opt_revision_value_t` structure.
858 869 """
859 870 value = svn.core.svn_opt_revision_value_t()
860 871 value.number = num
861 872 revision = svn.core.svn_opt_revision_t()
862 873 revision.kind = svn.core.svn_opt_revision_number
863 874 revision.value = value
864 875 return revision
General Comments 0
You need to be logged in to leave comments. Login now