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