##// END OF EJS Templates
archive: implemented efficient way to perform archive for each repository.
milka -
r894:5a847e1a default
parent child Browse files
Show More
@@ -1,76 +1,130 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17 import os
18 import sys
18 import sys
19 import traceback
19 import traceback
20 import logging
20 import logging
21 import urlparse
21 import urlparse
22
22
23 from vcsserver import exceptions
24 from vcsserver.exceptions import NoContentException
25 from vcsserver.hgcompat import (archival)
23 from vcsserver.lib.rc_cache import region_meta
26 from vcsserver.lib.rc_cache import region_meta
24 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
25
28
26
29
27 class RepoFactory(object):
30 class RepoFactory(object):
28 """
31 """
29 Utility to create instances of repository
32 Utility to create instances of repository
30
33
31 It provides internal caching of the `repo` object based on
34 It provides internal caching of the `repo` object based on
32 the :term:`call context`.
35 the :term:`call context`.
33 """
36 """
34 repo_type = None
37 repo_type = None
35
38
36 def __init__(self):
39 def __init__(self):
37 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
40 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
38
41
39 def _create_config(self, path, config):
42 def _create_config(self, path, config):
40 config = {}
43 config = {}
41 return config
44 return config
42
45
43 def _create_repo(self, wire, create):
46 def _create_repo(self, wire, create):
44 raise NotImplementedError()
47 raise NotImplementedError()
45
48
46 def repo(self, wire, create=False):
49 def repo(self, wire, create=False):
47 raise NotImplementedError()
50 raise NotImplementedError()
48
51
49
52
50 def obfuscate_qs(query_string):
53 def obfuscate_qs(query_string):
51 if query_string is None:
54 if query_string is None:
52 return None
55 return None
53
56
54 parsed = []
57 parsed = []
55 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
58 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
56 if k in ['auth_token', 'api_key']:
59 if k in ['auth_token', 'api_key']:
57 v = "*****"
60 v = "*****"
58 parsed.append((k, v))
61 parsed.append((k, v))
59
62
60 return '&'.join('{}{}'.format(
63 return '&'.join('{}{}'.format(
61 k, '={}'.format(v) if v else '') for k, v in parsed)
64 k, '={}'.format(v) if v else '') for k, v in parsed)
62
65
63
66
64 def raise_from_original(new_type):
67 def raise_from_original(new_type):
65 """
68 """
66 Raise a new exception type with original args and traceback.
69 Raise a new exception type with original args and traceback.
67 """
70 """
68 exc_type, exc_value, exc_traceback = sys.exc_info()
71 exc_type, exc_value, exc_traceback = sys.exc_info()
69 new_exc = new_type(*exc_value.args)
72 new_exc = new_type(*exc_value.args)
70 # store the original traceback into the new exc
73 # store the original traceback into the new exc
71 new_exc._org_exc_tb = traceback.format_exc(exc_traceback)
74 new_exc._org_exc_tb = traceback.format_exc(exc_traceback)
72
75
73 try:
76 try:
74 raise new_exc, None, exc_traceback
77 raise new_exc, None, exc_traceback
75 finally:
78 finally:
76 del exc_traceback
79 del exc_traceback
80
81
82 class ArchiveNode(object):
83 def __init__(self, path, mode, is_link, raw_bytes):
84 self.path = path
85 self.mode = mode
86 self.is_link = is_link
87 self.raw_bytes = raw_bytes
88
89
90 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
91 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
92 """
93 walker should be a file walker, for example:
94 def walker():
95 for file_info in files:
96 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
97 """
98 extra_metadata = extra_metadata or {}
99
100 if kind == "tgz":
101 archiver = archival.tarit(archive_dest_path, mtime, "gz")
102 elif kind == "tbz2":
103 archiver = archival.tarit(archive_dest_path, mtime, "bz2")
104 elif kind == 'zip':
105 archiver = archival.zipit(archive_dest_path, mtime)
106 else:
107 raise exceptions.ArchiveException()(
108 'Remote does not support: "%s" archive type.' % kind)
109
110 for f in walker(commit_id, archive_at_path):
111 f_path = os.path.join(archive_dir_name, f.path.lstrip('/'))
112 try:
113 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
114 except NoContentException:
115 # NOTE(marcink): this is a special case for SVN so we can create "empty"
116 # directories which arent supported by archiver
117 archiver.addfile(os.path.join(f_path, '.dir'), f.mode, f.is_link, '')
118
119 if write_metadata:
120 metadata = dict([
121 ('commit_id', commit_id),
122 ('mtime', mtime),
123 ])
124 metadata.update(extra_metadata)
125
126 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata.items()]
127 f_path = os.path.join(archive_dir_name, '.archival.txt')
128 archiver.addfile(f_path, 0o644, False, '\n'.join(meta))
129
130 return archiver.done()
@@ -1,121 +1,125 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Special exception handling over the wire.
19 Special exception handling over the wire.
20
20
21 Since we cannot assume that our client is able to import our exception classes,
21 Since we cannot assume that our client is able to import our exception classes,
22 this module provides a "wrapping" mechanism to raise plain exceptions
22 this module provides a "wrapping" mechanism to raise plain exceptions
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 different error conditions.
24 different error conditions.
25 """
25 """
26
26
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28
28
29
29
30 def _make_exception(kind, org_exc, *args):
30 def _make_exception(kind, org_exc, *args):
31 """
31 """
32 Prepares a base `Exception` instance to be sent over the wire.
32 Prepares a base `Exception` instance to be sent over the wire.
33
33
34 To give our caller a hint what this is about, it will attach an attribute
34 To give our caller a hint what this is about, it will attach an attribute
35 `_vcs_kind` to the exception.
35 `_vcs_kind` to the exception.
36 """
36 """
37 exc = Exception(*args)
37 exc = Exception(*args)
38 exc._vcs_kind = kind
38 exc._vcs_kind = kind
39 exc._org_exc = org_exc
39 exc._org_exc = org_exc
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
41 return exc
41 return exc
42
42
43
43
44 def AbortException(org_exc=None):
44 def AbortException(org_exc=None):
45 def _make_exception_wrapper(*args):
45 def _make_exception_wrapper(*args):
46 return _make_exception('abort', org_exc, *args)
46 return _make_exception('abort', org_exc, *args)
47 return _make_exception_wrapper
47 return _make_exception_wrapper
48
48
49
49
50 def ArchiveException(org_exc=None):
50 def ArchiveException(org_exc=None):
51 def _make_exception_wrapper(*args):
51 def _make_exception_wrapper(*args):
52 return _make_exception('archive', org_exc, *args)
52 return _make_exception('archive', org_exc, *args)
53 return _make_exception_wrapper
53 return _make_exception_wrapper
54
54
55
55
56 def LookupException(org_exc=None):
56 def LookupException(org_exc=None):
57 def _make_exception_wrapper(*args):
57 def _make_exception_wrapper(*args):
58 return _make_exception('lookup', org_exc, *args)
58 return _make_exception('lookup', org_exc, *args)
59 return _make_exception_wrapper
59 return _make_exception_wrapper
60
60
61
61
62 def VcsException(org_exc=None):
62 def VcsException(org_exc=None):
63 def _make_exception_wrapper(*args):
63 def _make_exception_wrapper(*args):
64 return _make_exception('error', org_exc, *args)
64 return _make_exception('error', org_exc, *args)
65 return _make_exception_wrapper
65 return _make_exception_wrapper
66
66
67
67
68 def RepositoryLockedException(org_exc=None):
68 def RepositoryLockedException(org_exc=None):
69 def _make_exception_wrapper(*args):
69 def _make_exception_wrapper(*args):
70 return _make_exception('repo_locked', org_exc, *args)
70 return _make_exception('repo_locked', org_exc, *args)
71 return _make_exception_wrapper
71 return _make_exception_wrapper
72
72
73
73
74 def RepositoryBranchProtectedException(org_exc=None):
74 def RepositoryBranchProtectedException(org_exc=None):
75 def _make_exception_wrapper(*args):
75 def _make_exception_wrapper(*args):
76 return _make_exception('repo_branch_protected', org_exc, *args)
76 return _make_exception('repo_branch_protected', org_exc, *args)
77 return _make_exception_wrapper
77 return _make_exception_wrapper
78
78
79
79
80 def RequirementException(org_exc=None):
80 def RequirementException(org_exc=None):
81 def _make_exception_wrapper(*args):
81 def _make_exception_wrapper(*args):
82 return _make_exception('requirement', org_exc, *args)
82 return _make_exception('requirement', org_exc, *args)
83 return _make_exception_wrapper
83 return _make_exception_wrapper
84
84
85
85
86 def UnhandledException(org_exc=None):
86 def UnhandledException(org_exc=None):
87 def _make_exception_wrapper(*args):
87 def _make_exception_wrapper(*args):
88 return _make_exception('unhandled', org_exc, *args)
88 return _make_exception('unhandled', org_exc, *args)
89 return _make_exception_wrapper
89 return _make_exception_wrapper
90
90
91
91
92 def URLError(org_exc=None):
92 def URLError(org_exc=None):
93 def _make_exception_wrapper(*args):
93 def _make_exception_wrapper(*args):
94 return _make_exception('url_error', org_exc, *args)
94 return _make_exception('url_error', org_exc, *args)
95 return _make_exception_wrapper
95 return _make_exception_wrapper
96
96
97
97
98 def SubrepoMergeException(org_exc=None):
98 def SubrepoMergeException(org_exc=None):
99 def _make_exception_wrapper(*args):
99 def _make_exception_wrapper(*args):
100 return _make_exception('subrepo_merge_error', org_exc, *args)
100 return _make_exception('subrepo_merge_error', org_exc, *args)
101 return _make_exception_wrapper
101 return _make_exception_wrapper
102
102
103
103
104 class HTTPRepoLocked(HTTPLocked):
104 class HTTPRepoLocked(HTTPLocked):
105 """
105 """
106 Subclass of HTTPLocked response that allows to set the title and status
106 Subclass of HTTPLocked response that allows to set the title and status
107 code via constructor arguments.
107 code via constructor arguments.
108 """
108 """
109 def __init__(self, title, status_code=None, **kwargs):
109 def __init__(self, title, status_code=None, **kwargs):
110 self.code = status_code or HTTPLocked.code
110 self.code = status_code or HTTPLocked.code
111 self.title = title
111 self.title = title
112 super(HTTPRepoLocked, self).__init__(**kwargs)
112 super(HTTPRepoLocked, self).__init__(**kwargs)
113
113
114
114
115 class HTTPRepoBranchProtected(HTTPForbidden):
115 class HTTPRepoBranchProtected(HTTPForbidden):
116 def __init__(self, *args, **kwargs):
116 def __init__(self, *args, **kwargs):
117 super(HTTPForbidden, self).__init__(*args, **kwargs)
117 super(HTTPForbidden, self).__init__(*args, **kwargs)
118
118
119
119
120 class RefNotFoundException(KeyError):
120 class RefNotFoundException(KeyError):
121 pass
121 pass
122
123
124 class NoContentException(ValueError):
125 pass
@@ -1,1192 +1,1226 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import collections
18 import collections
19 import logging
19 import logging
20 import os
20 import os
21 import posixpath as vcspath
21 import posixpath as vcspath
22 import re
22 import re
23 import stat
23 import stat
24 import traceback
24 import traceback
25 import urllib
25 import urllib
26 import urllib2
26 import urllib2
27 from functools import wraps
27 from functools import wraps
28
28
29 import more_itertools
29 import more_itertools
30 import pygit2
30 import pygit2
31 from pygit2 import Repository as LibGit2Repo
31 from pygit2 import Repository as LibGit2Repo
32 from pygit2 import index as LibGit2Index
32 from dulwich import index, objects
33 from dulwich import index, objects
33 from dulwich.client import HttpGitClient, LocalGitClient
34 from dulwich.client import HttpGitClient, LocalGitClient
34 from dulwich.errors import (
35 from dulwich.errors import (
35 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
37 UnexpectedCommandError)
38 UnexpectedCommandError)
38 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.server import update_server_info
40 from dulwich.server import update_server_info
40
41
41 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver.utils import safe_str, safe_int, safe_unicode
43 from vcsserver.utils import safe_str, safe_int, safe_unicode
43 from vcsserver.base import RepoFactory, obfuscate_qs
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
44 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
45 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
48
49
49 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
50 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
51 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
52 PEELED_REF_MARKER = '^{}'
53 PEELED_REF_MARKER = '^{}'
53
54
54
55
55 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
56
57
57
58
58 def str_to_dulwich(value):
59 def str_to_dulwich(value):
59 """
60 """
60 Dulwich 0.10.1a requires `unicode` objects to be passed in.
61 Dulwich 0.10.1a requires `unicode` objects to be passed in.
61 """
62 """
62 return value.decode(settings.WIRE_ENCODING)
63 return value.decode(settings.WIRE_ENCODING)
63
64
64
65
65 def reraise_safe_exceptions(func):
66 def reraise_safe_exceptions(func):
66 """Converts Dulwich exceptions to something neutral."""
67 """Converts Dulwich exceptions to something neutral."""
67
68
68 @wraps(func)
69 @wraps(func)
69 def wrapper(*args, **kwargs):
70 def wrapper(*args, **kwargs):
70 try:
71 try:
71 return func(*args, **kwargs)
72 return func(*args, **kwargs)
72 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
73 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
73 exc = exceptions.LookupException(org_exc=e)
74 exc = exceptions.LookupException(org_exc=e)
74 raise exc(safe_str(e))
75 raise exc(safe_str(e))
75 except (HangupException, UnexpectedCommandError) as e:
76 except (HangupException, UnexpectedCommandError) as e:
76 exc = exceptions.VcsException(org_exc=e)
77 exc = exceptions.VcsException(org_exc=e)
77 raise exc(safe_str(e))
78 raise exc(safe_str(e))
78 except Exception as e:
79 except Exception as e:
79 # NOTE(marcink): becuase of how dulwich handles some exceptions
80 # NOTE(marcink): becuase of how dulwich handles some exceptions
80 # (KeyError on empty repos), we cannot track this and catch all
81 # (KeyError on empty repos), we cannot track this and catch all
81 # exceptions, it's an exceptions from other handlers
82 # exceptions, it's an exceptions from other handlers
82 #if not hasattr(e, '_vcs_kind'):
83 #if not hasattr(e, '_vcs_kind'):
83 #log.exception("Unhandled exception in git remote call")
84 #log.exception("Unhandled exception in git remote call")
84 #raise_from_original(exceptions.UnhandledException)
85 #raise_from_original(exceptions.UnhandledException)
85 raise
86 raise
86 return wrapper
87 return wrapper
87
88
88
89
89 class Repo(DulwichRepo):
90 class Repo(DulwichRepo):
90 """
91 """
91 A wrapper for dulwich Repo class.
92 A wrapper for dulwich Repo class.
92
93
93 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
94 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
94 "Too many open files" error. We need to close all opened file descriptors
95 "Too many open files" error. We need to close all opened file descriptors
95 once the repo object is destroyed.
96 once the repo object is destroyed.
96 """
97 """
97 def __del__(self):
98 def __del__(self):
98 if hasattr(self, 'object_store'):
99 if hasattr(self, 'object_store'):
99 self.close()
100 self.close()
100
101
101
102
102 class Repository(LibGit2Repo):
103 class Repository(LibGit2Repo):
103
104
104 def __enter__(self):
105 def __enter__(self):
105 return self
106 return self
106
107
107 def __exit__(self, exc_type, exc_val, exc_tb):
108 def __exit__(self, exc_type, exc_val, exc_tb):
108 self.free()
109 self.free()
109
110
110
111
111 class GitFactory(RepoFactory):
112 class GitFactory(RepoFactory):
112 repo_type = 'git'
113 repo_type = 'git'
113
114
114 def _create_repo(self, wire, create, use_libgit2=False):
115 def _create_repo(self, wire, create, use_libgit2=False):
115 if use_libgit2:
116 if use_libgit2:
116 return Repository(wire['path'])
117 return Repository(wire['path'])
117 else:
118 else:
118 repo_path = str_to_dulwich(wire['path'])
119 repo_path = str_to_dulwich(wire['path'])
119 return Repo(repo_path)
120 return Repo(repo_path)
120
121
121 def repo(self, wire, create=False, use_libgit2=False):
122 def repo(self, wire, create=False, use_libgit2=False):
122 """
123 """
123 Get a repository instance for the given path.
124 Get a repository instance for the given path.
124 """
125 """
125 return self._create_repo(wire, create, use_libgit2)
126 return self._create_repo(wire, create, use_libgit2)
126
127
127 def repo_libgit2(self, wire):
128 def repo_libgit2(self, wire):
128 return self.repo(wire, use_libgit2=True)
129 return self.repo(wire, use_libgit2=True)
129
130
130
131
131 class GitRemote(RemoteBase):
132 class GitRemote(RemoteBase):
132
133
133 def __init__(self, factory):
134 def __init__(self, factory):
134 self._factory = factory
135 self._factory = factory
135 self._bulk_methods = {
136 self._bulk_methods = {
136 "date": self.date,
137 "date": self.date,
137 "author": self.author,
138 "author": self.author,
138 "branch": self.branch,
139 "branch": self.branch,
139 "message": self.message,
140 "message": self.message,
140 "parents": self.parents,
141 "parents": self.parents,
141 "_commit": self.revision,
142 "_commit": self.revision,
142 }
143 }
143
144
144 def _wire_to_config(self, wire):
145 def _wire_to_config(self, wire):
145 if 'config' in wire:
146 if 'config' in wire:
146 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
147 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
147 return {}
148 return {}
148
149
149 def _remote_conf(self, config):
150 def _remote_conf(self, config):
150 params = [
151 params = [
151 '-c', 'core.askpass=""',
152 '-c', 'core.askpass=""',
152 ]
153 ]
153 ssl_cert_dir = config.get('vcs_ssl_dir')
154 ssl_cert_dir = config.get('vcs_ssl_dir')
154 if ssl_cert_dir:
155 if ssl_cert_dir:
155 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
156 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
156 return params
157 return params
157
158
158 @reraise_safe_exceptions
159 @reraise_safe_exceptions
159 def discover_git_version(self):
160 def discover_git_version(self):
160 stdout, _ = self.run_git_command(
161 stdout, _ = self.run_git_command(
161 {}, ['--version'], _bare=True, _safe=True)
162 {}, ['--version'], _bare=True, _safe=True)
162 prefix = 'git version'
163 prefix = 'git version'
163 if stdout.startswith(prefix):
164 if stdout.startswith(prefix):
164 stdout = stdout[len(prefix):]
165 stdout = stdout[len(prefix):]
165 return stdout.strip()
166 return stdout.strip()
166
167
167 @reraise_safe_exceptions
168 @reraise_safe_exceptions
168 def is_empty(self, wire):
169 def is_empty(self, wire):
169 repo_init = self._factory.repo_libgit2(wire)
170 repo_init = self._factory.repo_libgit2(wire)
170 with repo_init as repo:
171 with repo_init as repo:
171
172
172 try:
173 try:
173 has_head = repo.head.name
174 has_head = repo.head.name
174 if has_head:
175 if has_head:
175 return False
176 return False
176
177
177 # NOTE(marcink): check again using more expensive method
178 # NOTE(marcink): check again using more expensive method
178 return repo.is_empty
179 return repo.is_empty
179 except Exception:
180 except Exception:
180 pass
181 pass
181
182
182 return True
183 return True
183
184
184 @reraise_safe_exceptions
185 @reraise_safe_exceptions
185 def assert_correct_path(self, wire):
186 def assert_correct_path(self, wire):
186 cache_on, context_uid, repo_id = self._cache_on(wire)
187 cache_on, context_uid, repo_id = self._cache_on(wire)
187 @self.region.conditional_cache_on_arguments(condition=cache_on)
188 @self.region.conditional_cache_on_arguments(condition=cache_on)
188 def _assert_correct_path(_context_uid, _repo_id):
189 def _assert_correct_path(_context_uid, _repo_id):
189 try:
190 try:
190 repo_init = self._factory.repo_libgit2(wire)
191 repo_init = self._factory.repo_libgit2(wire)
191 with repo_init as repo:
192 with repo_init as repo:
192 pass
193 pass
193 except pygit2.GitError:
194 except pygit2.GitError:
194 path = wire.get('path')
195 path = wire.get('path')
195 tb = traceback.format_exc()
196 tb = traceback.format_exc()
196 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
197 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
197 return False
198 return False
198
199
199 return True
200 return True
200 return _assert_correct_path(context_uid, repo_id)
201 return _assert_correct_path(context_uid, repo_id)
201
202
202 @reraise_safe_exceptions
203 @reraise_safe_exceptions
203 def bare(self, wire):
204 def bare(self, wire):
204 repo_init = self._factory.repo_libgit2(wire)
205 repo_init = self._factory.repo_libgit2(wire)
205 with repo_init as repo:
206 with repo_init as repo:
206 return repo.is_bare
207 return repo.is_bare
207
208
208 @reraise_safe_exceptions
209 @reraise_safe_exceptions
209 def blob_as_pretty_string(self, wire, sha):
210 def blob_as_pretty_string(self, wire, sha):
210 repo_init = self._factory.repo_libgit2(wire)
211 repo_init = self._factory.repo_libgit2(wire)
211 with repo_init as repo:
212 with repo_init as repo:
212 blob_obj = repo[sha]
213 blob_obj = repo[sha]
213 blob = blob_obj.data
214 blob = blob_obj.data
214 return blob
215 return blob
215
216
216 @reraise_safe_exceptions
217 @reraise_safe_exceptions
217 def blob_raw_length(self, wire, sha):
218 def blob_raw_length(self, wire, sha):
218 cache_on, context_uid, repo_id = self._cache_on(wire)
219 cache_on, context_uid, repo_id = self._cache_on(wire)
219 @self.region.conditional_cache_on_arguments(condition=cache_on)
220 @self.region.conditional_cache_on_arguments(condition=cache_on)
220 def _blob_raw_length(_repo_id, _sha):
221 def _blob_raw_length(_repo_id, _sha):
221
222
222 repo_init = self._factory.repo_libgit2(wire)
223 repo_init = self._factory.repo_libgit2(wire)
223 with repo_init as repo:
224 with repo_init as repo:
224 blob = repo[sha]
225 blob = repo[sha]
225 return blob.size
226 return blob.size
226
227
227 return _blob_raw_length(repo_id, sha)
228 return _blob_raw_length(repo_id, sha)
228
229
229 def _parse_lfs_pointer(self, raw_content):
230 def _parse_lfs_pointer(self, raw_content):
230
231
231 spec_string = 'version https://git-lfs.github.com/spec'
232 spec_string = 'version https://git-lfs.github.com/spec'
232 if raw_content and raw_content.startswith(spec_string):
233 if raw_content and raw_content.startswith(spec_string):
233 pattern = re.compile(r"""
234 pattern = re.compile(r"""
234 (?:\n)?
235 (?:\n)?
235 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
236 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
236 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
237 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
237 ^size[ ](?P<oid_size>[0-9]+)\n
238 ^size[ ](?P<oid_size>[0-9]+)\n
238 (?:\n)?
239 (?:\n)?
239 """, re.VERBOSE | re.MULTILINE)
240 """, re.VERBOSE | re.MULTILINE)
240 match = pattern.match(raw_content)
241 match = pattern.match(raw_content)
241 if match:
242 if match:
242 return match.groupdict()
243 return match.groupdict()
243
244
244 return {}
245 return {}
245
246
246 @reraise_safe_exceptions
247 @reraise_safe_exceptions
247 def is_large_file(self, wire, commit_id):
248 def is_large_file(self, wire, commit_id):
248 cache_on, context_uid, repo_id = self._cache_on(wire)
249 cache_on, context_uid, repo_id = self._cache_on(wire)
249
250
250 @self.region.conditional_cache_on_arguments(condition=cache_on)
251 @self.region.conditional_cache_on_arguments(condition=cache_on)
251 def _is_large_file(_repo_id, _sha):
252 def _is_large_file(_repo_id, _sha):
252 repo_init = self._factory.repo_libgit2(wire)
253 repo_init = self._factory.repo_libgit2(wire)
253 with repo_init as repo:
254 with repo_init as repo:
254 blob = repo[commit_id]
255 blob = repo[commit_id]
255 if blob.is_binary:
256 if blob.is_binary:
256 return {}
257 return {}
257
258
258 return self._parse_lfs_pointer(blob.data)
259 return self._parse_lfs_pointer(blob.data)
259
260
260 return _is_large_file(repo_id, commit_id)
261 return _is_large_file(repo_id, commit_id)
261
262
262 @reraise_safe_exceptions
263 @reraise_safe_exceptions
263 def is_binary(self, wire, tree_id):
264 def is_binary(self, wire, tree_id):
264 cache_on, context_uid, repo_id = self._cache_on(wire)
265 cache_on, context_uid, repo_id = self._cache_on(wire)
265
266
266 @self.region.conditional_cache_on_arguments(condition=cache_on)
267 @self.region.conditional_cache_on_arguments(condition=cache_on)
267 def _is_binary(_repo_id, _tree_id):
268 def _is_binary(_repo_id, _tree_id):
268 repo_init = self._factory.repo_libgit2(wire)
269 repo_init = self._factory.repo_libgit2(wire)
269 with repo_init as repo:
270 with repo_init as repo:
270 blob_obj = repo[tree_id]
271 blob_obj = repo[tree_id]
271 return blob_obj.is_binary
272 return blob_obj.is_binary
272
273
273 return _is_binary(repo_id, tree_id)
274 return _is_binary(repo_id, tree_id)
274
275
275 @reraise_safe_exceptions
276 @reraise_safe_exceptions
276 def in_largefiles_store(self, wire, oid):
277 def in_largefiles_store(self, wire, oid):
277 conf = self._wire_to_config(wire)
278 conf = self._wire_to_config(wire)
278 repo_init = self._factory.repo_libgit2(wire)
279 repo_init = self._factory.repo_libgit2(wire)
279 with repo_init as repo:
280 with repo_init as repo:
280 repo_name = repo.path
281 repo_name = repo.path
281
282
282 store_location = conf.get('vcs_git_lfs_store_location')
283 store_location = conf.get('vcs_git_lfs_store_location')
283 if store_location:
284 if store_location:
284
285
285 store = LFSOidStore(
286 store = LFSOidStore(
286 oid=oid, repo=repo_name, store_location=store_location)
287 oid=oid, repo=repo_name, store_location=store_location)
287 return store.has_oid()
288 return store.has_oid()
288
289
289 return False
290 return False
290
291
291 @reraise_safe_exceptions
292 @reraise_safe_exceptions
292 def store_path(self, wire, oid):
293 def store_path(self, wire, oid):
293 conf = self._wire_to_config(wire)
294 conf = self._wire_to_config(wire)
294 repo_init = self._factory.repo_libgit2(wire)
295 repo_init = self._factory.repo_libgit2(wire)
295 with repo_init as repo:
296 with repo_init as repo:
296 repo_name = repo.path
297 repo_name = repo.path
297
298
298 store_location = conf.get('vcs_git_lfs_store_location')
299 store_location = conf.get('vcs_git_lfs_store_location')
299 if store_location:
300 if store_location:
300 store = LFSOidStore(
301 store = LFSOidStore(
301 oid=oid, repo=repo_name, store_location=store_location)
302 oid=oid, repo=repo_name, store_location=store_location)
302 return store.oid_path
303 return store.oid_path
303 raise ValueError('Unable to fetch oid with path {}'.format(oid))
304 raise ValueError('Unable to fetch oid with path {}'.format(oid))
304
305
305 @reraise_safe_exceptions
306 @reraise_safe_exceptions
306 def bulk_request(self, wire, rev, pre_load):
307 def bulk_request(self, wire, rev, pre_load):
307 cache_on, context_uid, repo_id = self._cache_on(wire)
308 cache_on, context_uid, repo_id = self._cache_on(wire)
308 @self.region.conditional_cache_on_arguments(condition=cache_on)
309 @self.region.conditional_cache_on_arguments(condition=cache_on)
309 def _bulk_request(_repo_id, _rev, _pre_load):
310 def _bulk_request(_repo_id, _rev, _pre_load):
310 result = {}
311 result = {}
311 for attr in pre_load:
312 for attr in pre_load:
312 try:
313 try:
313 method = self._bulk_methods[attr]
314 method = self._bulk_methods[attr]
314 args = [wire, rev]
315 args = [wire, rev]
315 result[attr] = method(*args)
316 result[attr] = method(*args)
316 except KeyError as e:
317 except KeyError as e:
317 raise exceptions.VcsException(e)(
318 raise exceptions.VcsException(e)(
318 "Unknown bulk attribute: %s" % attr)
319 "Unknown bulk attribute: %s" % attr)
319 return result
320 return result
320
321
321 return _bulk_request(repo_id, rev, sorted(pre_load))
322 return _bulk_request(repo_id, rev, sorted(pre_load))
322
323
323 def _build_opener(self, url):
324 def _build_opener(self, url):
324 handlers = []
325 handlers = []
325 url_obj = url_parser(url)
326 url_obj = url_parser(url)
326 _, authinfo = url_obj.authinfo()
327 _, authinfo = url_obj.authinfo()
327
328
328 if authinfo:
329 if authinfo:
329 # create a password manager
330 # create a password manager
330 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
331 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
331 passmgr.add_password(*authinfo)
332 passmgr.add_password(*authinfo)
332
333
333 handlers.extend((httpbasicauthhandler(passmgr),
334 handlers.extend((httpbasicauthhandler(passmgr),
334 httpdigestauthhandler(passmgr)))
335 httpdigestauthhandler(passmgr)))
335
336
336 return urllib2.build_opener(*handlers)
337 return urllib2.build_opener(*handlers)
337
338
338 def _type_id_to_name(self, type_id):
339 def _type_id_to_name(self, type_id):
339 return {
340 return {
340 1: b'commit',
341 1: b'commit',
341 2: b'tree',
342 2: b'tree',
342 3: b'blob',
343 3: b'blob',
343 4: b'tag'
344 4: b'tag'
344 }[type_id]
345 }[type_id]
345
346
346 @reraise_safe_exceptions
347 @reraise_safe_exceptions
347 def check_url(self, url, config):
348 def check_url(self, url, config):
348 url_obj = url_parser(url)
349 url_obj = url_parser(url)
349 test_uri, _ = url_obj.authinfo()
350 test_uri, _ = url_obj.authinfo()
350 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
351 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
351 url_obj.query = obfuscate_qs(url_obj.query)
352 url_obj.query = obfuscate_qs(url_obj.query)
352 cleaned_uri = str(url_obj)
353 cleaned_uri = str(url_obj)
353 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
354 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
354
355
355 if not test_uri.endswith('info/refs'):
356 if not test_uri.endswith('info/refs'):
356 test_uri = test_uri.rstrip('/') + '/info/refs'
357 test_uri = test_uri.rstrip('/') + '/info/refs'
357
358
358 o = self._build_opener(url)
359 o = self._build_opener(url)
359 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
360 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
360
361
361 q = {"service": 'git-upload-pack'}
362 q = {"service": 'git-upload-pack'}
362 qs = '?%s' % urllib.urlencode(q)
363 qs = '?%s' % urllib.urlencode(q)
363 cu = "%s%s" % (test_uri, qs)
364 cu = "%s%s" % (test_uri, qs)
364 req = urllib2.Request(cu, None, {})
365 req = urllib2.Request(cu, None, {})
365
366
366 try:
367 try:
367 log.debug("Trying to open URL %s", cleaned_uri)
368 log.debug("Trying to open URL %s", cleaned_uri)
368 resp = o.open(req)
369 resp = o.open(req)
369 if resp.code != 200:
370 if resp.code != 200:
370 raise exceptions.URLError()('Return Code is not 200')
371 raise exceptions.URLError()('Return Code is not 200')
371 except Exception as e:
372 except Exception as e:
372 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
373 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
373 # means it cannot be cloned
374 # means it cannot be cloned
374 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
375 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
375
376
376 # now detect if it's proper git repo
377 # now detect if it's proper git repo
377 gitdata = resp.read()
378 gitdata = resp.read()
378 if 'service=git-upload-pack' in gitdata:
379 if 'service=git-upload-pack' in gitdata:
379 pass
380 pass
380 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
381 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
381 # old style git can return some other format !
382 # old style git can return some other format !
382 pass
383 pass
383 else:
384 else:
384 raise exceptions.URLError()(
385 raise exceptions.URLError()(
385 "url [%s] does not look like an git" % (cleaned_uri,))
386 "url [%s] does not look like an git" % (cleaned_uri,))
386
387
387 return True
388 return True
388
389
389 @reraise_safe_exceptions
390 @reraise_safe_exceptions
390 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
391 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
391 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
392 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
392 remote_refs = self.pull(wire, url, apply_refs=False)
393 remote_refs = self.pull(wire, url, apply_refs=False)
393 repo = self._factory.repo(wire)
394 repo = self._factory.repo(wire)
394 if isinstance(valid_refs, list):
395 if isinstance(valid_refs, list):
395 valid_refs = tuple(valid_refs)
396 valid_refs = tuple(valid_refs)
396
397
397 for k in remote_refs:
398 for k in remote_refs:
398 # only parse heads/tags and skip so called deferred tags
399 # only parse heads/tags and skip so called deferred tags
399 if k.startswith(valid_refs) and not k.endswith(deferred):
400 if k.startswith(valid_refs) and not k.endswith(deferred):
400 repo[k] = remote_refs[k]
401 repo[k] = remote_refs[k]
401
402
402 if update_after_clone:
403 if update_after_clone:
403 # we want to checkout HEAD
404 # we want to checkout HEAD
404 repo["HEAD"] = remote_refs["HEAD"]
405 repo["HEAD"] = remote_refs["HEAD"]
405 index.build_index_from_tree(repo.path, repo.index_path(),
406 index.build_index_from_tree(repo.path, repo.index_path(),
406 repo.object_store, repo["HEAD"].tree)
407 repo.object_store, repo["HEAD"].tree)
407
408
408 @reraise_safe_exceptions
409 @reraise_safe_exceptions
409 def branch(self, wire, commit_id):
410 def branch(self, wire, commit_id):
410 cache_on, context_uid, repo_id = self._cache_on(wire)
411 cache_on, context_uid, repo_id = self._cache_on(wire)
411 @self.region.conditional_cache_on_arguments(condition=cache_on)
412 @self.region.conditional_cache_on_arguments(condition=cache_on)
412 def _branch(_context_uid, _repo_id, _commit_id):
413 def _branch(_context_uid, _repo_id, _commit_id):
413 regex = re.compile('^refs/heads')
414 regex = re.compile('^refs/heads')
414
415
415 def filter_with(ref):
416 def filter_with(ref):
416 return regex.match(ref[0]) and ref[1] == _commit_id
417 return regex.match(ref[0]) and ref[1] == _commit_id
417
418
418 branches = filter(filter_with, self.get_refs(wire).items())
419 branches = filter(filter_with, self.get_refs(wire).items())
419 return [x[0].split('refs/heads/')[-1] for x in branches]
420 return [x[0].split('refs/heads/')[-1] for x in branches]
420
421
421 return _branch(context_uid, repo_id, commit_id)
422 return _branch(context_uid, repo_id, commit_id)
422
423
423 @reraise_safe_exceptions
424 @reraise_safe_exceptions
424 def commit_branches(self, wire, commit_id):
425 def commit_branches(self, wire, commit_id):
425 cache_on, context_uid, repo_id = self._cache_on(wire)
426 cache_on, context_uid, repo_id = self._cache_on(wire)
426 @self.region.conditional_cache_on_arguments(condition=cache_on)
427 @self.region.conditional_cache_on_arguments(condition=cache_on)
427 def _commit_branches(_context_uid, _repo_id, _commit_id):
428 def _commit_branches(_context_uid, _repo_id, _commit_id):
428 repo_init = self._factory.repo_libgit2(wire)
429 repo_init = self._factory.repo_libgit2(wire)
429 with repo_init as repo:
430 with repo_init as repo:
430 branches = [x for x in repo.branches.with_commit(_commit_id)]
431 branches = [x for x in repo.branches.with_commit(_commit_id)]
431 return branches
432 return branches
432
433
433 return _commit_branches(context_uid, repo_id, commit_id)
434 return _commit_branches(context_uid, repo_id, commit_id)
434
435
435 @reraise_safe_exceptions
436 @reraise_safe_exceptions
436 def add_object(self, wire, content):
437 def add_object(self, wire, content):
437 repo_init = self._factory.repo_libgit2(wire)
438 repo_init = self._factory.repo_libgit2(wire)
438 with repo_init as repo:
439 with repo_init as repo:
439 blob = objects.Blob()
440 blob = objects.Blob()
440 blob.set_raw_string(content)
441 blob.set_raw_string(content)
441 repo.object_store.add_object(blob)
442 repo.object_store.add_object(blob)
442 return blob.id
443 return blob.id
443
444
444 # TODO: this is quite complex, check if that can be simplified
445 # TODO: this is quite complex, check if that can be simplified
445 @reraise_safe_exceptions
446 @reraise_safe_exceptions
446 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
447 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
447 repo = self._factory.repo(wire)
448 repo = self._factory.repo(wire)
448 object_store = repo.object_store
449 object_store = repo.object_store
449
450
450 # Create tree and populates it with blobs
451 # Create tree and populates it with blobs
451 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
452 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
452
453
453 for node in updated:
454 for node in updated:
454 # Compute subdirs if needed
455 # Compute subdirs if needed
455 dirpath, nodename = vcspath.split(node['path'])
456 dirpath, nodename = vcspath.split(node['path'])
456 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
457 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
457 parent = commit_tree
458 parent = commit_tree
458 ancestors = [('', parent)]
459 ancestors = [('', parent)]
459
460
460 # Tries to dig for the deepest existing tree
461 # Tries to dig for the deepest existing tree
461 while dirnames:
462 while dirnames:
462 curdir = dirnames.pop(0)
463 curdir = dirnames.pop(0)
463 try:
464 try:
464 dir_id = parent[curdir][1]
465 dir_id = parent[curdir][1]
465 except KeyError:
466 except KeyError:
466 # put curdir back into dirnames and stops
467 # put curdir back into dirnames and stops
467 dirnames.insert(0, curdir)
468 dirnames.insert(0, curdir)
468 break
469 break
469 else:
470 else:
470 # If found, updates parent
471 # If found, updates parent
471 parent = repo[dir_id]
472 parent = repo[dir_id]
472 ancestors.append((curdir, parent))
473 ancestors.append((curdir, parent))
473 # Now parent is deepest existing tree and we need to create
474 # Now parent is deepest existing tree and we need to create
474 # subtrees for dirnames (in reverse order)
475 # subtrees for dirnames (in reverse order)
475 # [this only applies for nodes from added]
476 # [this only applies for nodes from added]
476 new_trees = []
477 new_trees = []
477
478
478 blob = objects.Blob.from_string(node['content'])
479 blob = objects.Blob.from_string(node['content'])
479
480
480 if dirnames:
481 if dirnames:
481 # If there are trees which should be created we need to build
482 # If there are trees which should be created we need to build
482 # them now (in reverse order)
483 # them now (in reverse order)
483 reversed_dirnames = list(reversed(dirnames))
484 reversed_dirnames = list(reversed(dirnames))
484 curtree = objects.Tree()
485 curtree = objects.Tree()
485 curtree[node['node_path']] = node['mode'], blob.id
486 curtree[node['node_path']] = node['mode'], blob.id
486 new_trees.append(curtree)
487 new_trees.append(curtree)
487 for dirname in reversed_dirnames[:-1]:
488 for dirname in reversed_dirnames[:-1]:
488 newtree = objects.Tree()
489 newtree = objects.Tree()
489 newtree[dirname] = (DIR_STAT, curtree.id)
490 newtree[dirname] = (DIR_STAT, curtree.id)
490 new_trees.append(newtree)
491 new_trees.append(newtree)
491 curtree = newtree
492 curtree = newtree
492 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
493 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
493 else:
494 else:
494 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
495 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
495
496
496 new_trees.append(parent)
497 new_trees.append(parent)
497 # Update ancestors
498 # Update ancestors
498 reversed_ancestors = reversed(
499 reversed_ancestors = reversed(
499 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
500 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
500 for parent, tree, path in reversed_ancestors:
501 for parent, tree, path in reversed_ancestors:
501 parent[path] = (DIR_STAT, tree.id)
502 parent[path] = (DIR_STAT, tree.id)
502 object_store.add_object(tree)
503 object_store.add_object(tree)
503
504
504 object_store.add_object(blob)
505 object_store.add_object(blob)
505 for tree in new_trees:
506 for tree in new_trees:
506 object_store.add_object(tree)
507 object_store.add_object(tree)
507
508
508 for node_path in removed:
509 for node_path in removed:
509 paths = node_path.split('/')
510 paths = node_path.split('/')
510 tree = commit_tree
511 tree = commit_tree
511 trees = [tree]
512 trees = [tree]
512 # Traverse deep into the forest...
513 # Traverse deep into the forest...
513 for path in paths:
514 for path in paths:
514 try:
515 try:
515 obj = repo[tree[path][1]]
516 obj = repo[tree[path][1]]
516 if isinstance(obj, objects.Tree):
517 if isinstance(obj, objects.Tree):
517 trees.append(obj)
518 trees.append(obj)
518 tree = obj
519 tree = obj
519 except KeyError:
520 except KeyError:
520 break
521 break
521 # Cut down the blob and all rotten trees on the way back...
522 # Cut down the blob and all rotten trees on the way back...
522 for path, tree in reversed(zip(paths, trees)):
523 for path, tree in reversed(zip(paths, trees)):
523 del tree[path]
524 del tree[path]
524 if tree:
525 if tree:
525 # This tree still has elements - don't remove it or any
526 # This tree still has elements - don't remove it or any
526 # of it's parents
527 # of it's parents
527 break
528 break
528
529
529 object_store.add_object(commit_tree)
530 object_store.add_object(commit_tree)
530
531
531 # Create commit
532 # Create commit
532 commit = objects.Commit()
533 commit = objects.Commit()
533 commit.tree = commit_tree.id
534 commit.tree = commit_tree.id
534 for k, v in commit_data.iteritems():
535 for k, v in commit_data.iteritems():
535 setattr(commit, k, v)
536 setattr(commit, k, v)
536 object_store.add_object(commit)
537 object_store.add_object(commit)
537
538
538 self.create_branch(wire, branch, commit.id)
539 self.create_branch(wire, branch, commit.id)
539
540
540 # dulwich set-ref
541 # dulwich set-ref
541 ref = 'refs/heads/%s' % branch
542 ref = 'refs/heads/%s' % branch
542 repo.refs[ref] = commit.id
543 repo.refs[ref] = commit.id
543
544
544 return commit.id
545 return commit.id
545
546
546 @reraise_safe_exceptions
547 @reraise_safe_exceptions
547 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
548 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
548 if url != 'default' and '://' not in url:
549 if url != 'default' and '://' not in url:
549 client = LocalGitClient(url)
550 client = LocalGitClient(url)
550 else:
551 else:
551 url_obj = url_parser(url)
552 url_obj = url_parser(url)
552 o = self._build_opener(url)
553 o = self._build_opener(url)
553 url, _ = url_obj.authinfo()
554 url, _ = url_obj.authinfo()
554 client = HttpGitClient(base_url=url, opener=o)
555 client = HttpGitClient(base_url=url, opener=o)
555 repo = self._factory.repo(wire)
556 repo = self._factory.repo(wire)
556
557
557 determine_wants = repo.object_store.determine_wants_all
558 determine_wants = repo.object_store.determine_wants_all
558 if refs:
559 if refs:
559 def determine_wants_requested(references):
560 def determine_wants_requested(references):
560 return [references[r] for r in references if r in refs]
561 return [references[r] for r in references if r in refs]
561 determine_wants = determine_wants_requested
562 determine_wants = determine_wants_requested
562
563
563 try:
564 try:
564 remote_refs = client.fetch(
565 remote_refs = client.fetch(
565 path=url, target=repo, determine_wants=determine_wants)
566 path=url, target=repo, determine_wants=determine_wants)
566 except NotGitRepository as e:
567 except NotGitRepository as e:
567 log.warning(
568 log.warning(
568 'Trying to fetch from "%s" failed, not a Git repository.', url)
569 'Trying to fetch from "%s" failed, not a Git repository.', url)
569 # Exception can contain unicode which we convert
570 # Exception can contain unicode which we convert
570 raise exceptions.AbortException(e)(repr(e))
571 raise exceptions.AbortException(e)(repr(e))
571
572
572 # mikhail: client.fetch() returns all the remote refs, but fetches only
573 # mikhail: client.fetch() returns all the remote refs, but fetches only
573 # refs filtered by `determine_wants` function. We need to filter result
574 # refs filtered by `determine_wants` function. We need to filter result
574 # as well
575 # as well
575 if refs:
576 if refs:
576 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
577 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
577
578
578 if apply_refs:
579 if apply_refs:
579 # TODO: johbo: Needs proper test coverage with a git repository
580 # TODO: johbo: Needs proper test coverage with a git repository
580 # that contains a tag object, so that we would end up with
581 # that contains a tag object, so that we would end up with
581 # a peeled ref at this point.
582 # a peeled ref at this point.
582 for k in remote_refs:
583 for k in remote_refs:
583 if k.endswith(PEELED_REF_MARKER):
584 if k.endswith(PEELED_REF_MARKER):
584 log.debug("Skipping peeled reference %s", k)
585 log.debug("Skipping peeled reference %s", k)
585 continue
586 continue
586 repo[k] = remote_refs[k]
587 repo[k] = remote_refs[k]
587
588
588 if refs and not update_after:
589 if refs and not update_after:
589 # mikhail: explicitly set the head to the last ref.
590 # mikhail: explicitly set the head to the last ref.
590 repo['HEAD'] = remote_refs[refs[-1]]
591 repo['HEAD'] = remote_refs[refs[-1]]
591
592
592 if update_after:
593 if update_after:
593 # we want to checkout HEAD
594 # we want to checkout HEAD
594 repo["HEAD"] = remote_refs["HEAD"]
595 repo["HEAD"] = remote_refs["HEAD"]
595 index.build_index_from_tree(repo.path, repo.index_path(),
596 index.build_index_from_tree(repo.path, repo.index_path(),
596 repo.object_store, repo["HEAD"].tree)
597 repo.object_store, repo["HEAD"].tree)
597 return remote_refs
598 return remote_refs
598
599
599 @reraise_safe_exceptions
600 @reraise_safe_exceptions
600 def sync_fetch(self, wire, url, refs=None, all_refs=False):
601 def sync_fetch(self, wire, url, refs=None, all_refs=False):
601 repo = self._factory.repo(wire)
602 repo = self._factory.repo(wire)
602 if refs and not isinstance(refs, (list, tuple)):
603 if refs and not isinstance(refs, (list, tuple)):
603 refs = [refs]
604 refs = [refs]
604
605
605 config = self._wire_to_config(wire)
606 config = self._wire_to_config(wire)
606 # get all remote refs we'll use to fetch later
607 # get all remote refs we'll use to fetch later
607 cmd = ['ls-remote']
608 cmd = ['ls-remote']
608 if not all_refs:
609 if not all_refs:
609 cmd += ['--heads', '--tags']
610 cmd += ['--heads', '--tags']
610 cmd += [url]
611 cmd += [url]
611 output, __ = self.run_git_command(
612 output, __ = self.run_git_command(
612 wire, cmd, fail_on_stderr=False,
613 wire, cmd, fail_on_stderr=False,
613 _copts=self._remote_conf(config),
614 _copts=self._remote_conf(config),
614 extra_env={'GIT_TERMINAL_PROMPT': '0'})
615 extra_env={'GIT_TERMINAL_PROMPT': '0'})
615
616
616 remote_refs = collections.OrderedDict()
617 remote_refs = collections.OrderedDict()
617 fetch_refs = []
618 fetch_refs = []
618
619
619 for ref_line in output.splitlines():
620 for ref_line in output.splitlines():
620 sha, ref = ref_line.split('\t')
621 sha, ref = ref_line.split('\t')
621 sha = sha.strip()
622 sha = sha.strip()
622 if ref in remote_refs:
623 if ref in remote_refs:
623 # duplicate, skip
624 # duplicate, skip
624 continue
625 continue
625 if ref.endswith(PEELED_REF_MARKER):
626 if ref.endswith(PEELED_REF_MARKER):
626 log.debug("Skipping peeled reference %s", ref)
627 log.debug("Skipping peeled reference %s", ref)
627 continue
628 continue
628 # don't sync HEAD
629 # don't sync HEAD
629 if ref in ['HEAD']:
630 if ref in ['HEAD']:
630 continue
631 continue
631
632
632 remote_refs[ref] = sha
633 remote_refs[ref] = sha
633
634
634 if refs and sha in refs:
635 if refs and sha in refs:
635 # we filter fetch using our specified refs
636 # we filter fetch using our specified refs
636 fetch_refs.append('{}:{}'.format(ref, ref))
637 fetch_refs.append('{}:{}'.format(ref, ref))
637 elif not refs:
638 elif not refs:
638 fetch_refs.append('{}:{}'.format(ref, ref))
639 fetch_refs.append('{}:{}'.format(ref, ref))
639 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
640 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
640
641
641 if fetch_refs:
642 if fetch_refs:
642 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
643 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
643 fetch_refs_chunks = list(chunk)
644 fetch_refs_chunks = list(chunk)
644 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
645 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
645 _out, _err = self.run_git_command(
646 _out, _err = self.run_git_command(
646 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
647 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
647 fail_on_stderr=False,
648 fail_on_stderr=False,
648 _copts=self._remote_conf(config),
649 _copts=self._remote_conf(config),
649 extra_env={'GIT_TERMINAL_PROMPT': '0'})
650 extra_env={'GIT_TERMINAL_PROMPT': '0'})
650
651
651 return remote_refs
652 return remote_refs
652
653
653 @reraise_safe_exceptions
654 @reraise_safe_exceptions
654 def sync_push(self, wire, url, refs=None):
655 def sync_push(self, wire, url, refs=None):
655 if not self.check_url(url, wire):
656 if not self.check_url(url, wire):
656 return
657 return
657 config = self._wire_to_config(wire)
658 config = self._wire_to_config(wire)
658 self._factory.repo(wire)
659 self._factory.repo(wire)
659 self.run_git_command(
660 self.run_git_command(
660 wire, ['push', url, '--mirror'], fail_on_stderr=False,
661 wire, ['push', url, '--mirror'], fail_on_stderr=False,
661 _copts=self._remote_conf(config),
662 _copts=self._remote_conf(config),
662 extra_env={'GIT_TERMINAL_PROMPT': '0'})
663 extra_env={'GIT_TERMINAL_PROMPT': '0'})
663
664
664 @reraise_safe_exceptions
665 @reraise_safe_exceptions
665 def get_remote_refs(self, wire, url):
666 def get_remote_refs(self, wire, url):
666 repo = Repo(url)
667 repo = Repo(url)
667 return repo.get_refs()
668 return repo.get_refs()
668
669
669 @reraise_safe_exceptions
670 @reraise_safe_exceptions
670 def get_description(self, wire):
671 def get_description(self, wire):
671 repo = self._factory.repo(wire)
672 repo = self._factory.repo(wire)
672 return repo.get_description()
673 return repo.get_description()
673
674
674 @reraise_safe_exceptions
675 @reraise_safe_exceptions
675 def get_missing_revs(self, wire, rev1, rev2, path2):
676 def get_missing_revs(self, wire, rev1, rev2, path2):
676 repo = self._factory.repo(wire)
677 repo = self._factory.repo(wire)
677 LocalGitClient(thin_packs=False).fetch(path2, repo)
678 LocalGitClient(thin_packs=False).fetch(path2, repo)
678
679
679 wire_remote = wire.copy()
680 wire_remote = wire.copy()
680 wire_remote['path'] = path2
681 wire_remote['path'] = path2
681 repo_remote = self._factory.repo(wire_remote)
682 repo_remote = self._factory.repo(wire_remote)
682 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
683 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
683
684
684 revs = [
685 revs = [
685 x.commit.id
686 x.commit.id
686 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
687 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
687 return revs
688 return revs
688
689
689 @reraise_safe_exceptions
690 @reraise_safe_exceptions
690 def get_object(self, wire, sha, maybe_unreachable=False):
691 def get_object(self, wire, sha, maybe_unreachable=False):
691 cache_on, context_uid, repo_id = self._cache_on(wire)
692 cache_on, context_uid, repo_id = self._cache_on(wire)
692 @self.region.conditional_cache_on_arguments(condition=cache_on)
693 @self.region.conditional_cache_on_arguments(condition=cache_on)
693 def _get_object(_context_uid, _repo_id, _sha):
694 def _get_object(_context_uid, _repo_id, _sha):
694 repo_init = self._factory.repo_libgit2(wire)
695 repo_init = self._factory.repo_libgit2(wire)
695 with repo_init as repo:
696 with repo_init as repo:
696
697
697 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
698 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
698 try:
699 try:
699 commit = repo.revparse_single(sha)
700 commit = repo.revparse_single(sha)
700 except KeyError:
701 except KeyError:
701 # NOTE(marcink): KeyError doesn't give us any meaningful information
702 # NOTE(marcink): KeyError doesn't give us any meaningful information
702 # here, we instead give something more explicit
703 # here, we instead give something more explicit
703 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
704 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
704 raise exceptions.LookupException(e)(missing_commit_err)
705 raise exceptions.LookupException(e)(missing_commit_err)
705 except ValueError as e:
706 except ValueError as e:
706 raise exceptions.LookupException(e)(missing_commit_err)
707 raise exceptions.LookupException(e)(missing_commit_err)
707
708
708 is_tag = False
709 is_tag = False
709 if isinstance(commit, pygit2.Tag):
710 if isinstance(commit, pygit2.Tag):
710 commit = repo.get(commit.target)
711 commit = repo.get(commit.target)
711 is_tag = True
712 is_tag = True
712
713
713 check_dangling = True
714 check_dangling = True
714 if is_tag:
715 if is_tag:
715 check_dangling = False
716 check_dangling = False
716
717
717 if check_dangling and maybe_unreachable:
718 if check_dangling and maybe_unreachable:
718 check_dangling = False
719 check_dangling = False
719
720
720 # we used a reference and it parsed means we're not having a dangling commit
721 # we used a reference and it parsed means we're not having a dangling commit
721 if sha != commit.hex:
722 if sha != commit.hex:
722 check_dangling = False
723 check_dangling = False
723
724
724 if check_dangling:
725 if check_dangling:
725 # check for dangling commit
726 # check for dangling commit
726 for branch in repo.branches.with_commit(commit.hex):
727 for branch in repo.branches.with_commit(commit.hex):
727 if branch:
728 if branch:
728 break
729 break
729 else:
730 else:
730 # NOTE(marcink): Empty error doesn't give us any meaningful information
731 # NOTE(marcink): Empty error doesn't give us any meaningful information
731 # here, we instead give something more explicit
732 # here, we instead give something more explicit
732 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
733 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
733 raise exceptions.LookupException(e)(missing_commit_err)
734 raise exceptions.LookupException(e)(missing_commit_err)
734
735
735 commit_id = commit.hex
736 commit_id = commit.hex
736 type_id = commit.type
737 type_id = commit.type
737
738
738 return {
739 return {
739 'id': commit_id,
740 'id': commit_id,
740 'type': self._type_id_to_name(type_id),
741 'type': self._type_id_to_name(type_id),
741 'commit_id': commit_id,
742 'commit_id': commit_id,
742 'idx': 0
743 'idx': 0
743 }
744 }
744
745
745 return _get_object(context_uid, repo_id, sha)
746 return _get_object(context_uid, repo_id, sha)
746
747
747 @reraise_safe_exceptions
748 @reraise_safe_exceptions
748 def get_refs(self, wire):
749 def get_refs(self, wire):
749 cache_on, context_uid, repo_id = self._cache_on(wire)
750 cache_on, context_uid, repo_id = self._cache_on(wire)
750 @self.region.conditional_cache_on_arguments(condition=cache_on)
751 @self.region.conditional_cache_on_arguments(condition=cache_on)
751 def _get_refs(_context_uid, _repo_id):
752 def _get_refs(_context_uid, _repo_id):
752
753
753 repo_init = self._factory.repo_libgit2(wire)
754 repo_init = self._factory.repo_libgit2(wire)
754 with repo_init as repo:
755 with repo_init as repo:
755 regex = re.compile('^refs/(heads|tags)/')
756 regex = re.compile('^refs/(heads|tags)/')
756 return {x.name: x.target.hex for x in
757 return {x.name: x.target.hex for x in
757 filter(lambda ref: regex.match(ref.name) ,repo.listall_reference_objects())}
758 filter(lambda ref: regex.match(ref.name) ,repo.listall_reference_objects())}
758
759
759 return _get_refs(context_uid, repo_id)
760 return _get_refs(context_uid, repo_id)
760
761
761 @reraise_safe_exceptions
762 @reraise_safe_exceptions
762 def get_branch_pointers(self, wire):
763 def get_branch_pointers(self, wire):
763 cache_on, context_uid, repo_id = self._cache_on(wire)
764 cache_on, context_uid, repo_id = self._cache_on(wire)
764 @self.region.conditional_cache_on_arguments(condition=cache_on)
765 @self.region.conditional_cache_on_arguments(condition=cache_on)
765 def _get_branch_pointers(_context_uid, _repo_id):
766 def _get_branch_pointers(_context_uid, _repo_id):
766
767
767 repo_init = self._factory.repo_libgit2(wire)
768 repo_init = self._factory.repo_libgit2(wire)
768 regex = re.compile('^refs/heads')
769 regex = re.compile('^refs/heads')
769 with repo_init as repo:
770 with repo_init as repo:
770 branches = filter(lambda ref: regex.match(ref.name), repo.listall_reference_objects())
771 branches = filter(lambda ref: regex.match(ref.name), repo.listall_reference_objects())
771 return {x.target.hex: x.shorthand for x in branches}
772 return {x.target.hex: x.shorthand for x in branches}
772
773
773 return _get_branch_pointers(context_uid, repo_id)
774 return _get_branch_pointers(context_uid, repo_id)
774
775
775 @reraise_safe_exceptions
776 @reraise_safe_exceptions
776 def head(self, wire, show_exc=True):
777 def head(self, wire, show_exc=True):
777 cache_on, context_uid, repo_id = self._cache_on(wire)
778 cache_on, context_uid, repo_id = self._cache_on(wire)
778 @self.region.conditional_cache_on_arguments(condition=cache_on)
779 @self.region.conditional_cache_on_arguments(condition=cache_on)
779 def _head(_context_uid, _repo_id, _show_exc):
780 def _head(_context_uid, _repo_id, _show_exc):
780 repo_init = self._factory.repo_libgit2(wire)
781 repo_init = self._factory.repo_libgit2(wire)
781 with repo_init as repo:
782 with repo_init as repo:
782 try:
783 try:
783 return repo.head.peel().hex
784 return repo.head.peel().hex
784 except Exception:
785 except Exception:
785 if show_exc:
786 if show_exc:
786 raise
787 raise
787 return _head(context_uid, repo_id, show_exc)
788 return _head(context_uid, repo_id, show_exc)
788
789
789 @reraise_safe_exceptions
790 @reraise_safe_exceptions
790 def init(self, wire):
791 def init(self, wire):
791 repo_path = str_to_dulwich(wire['path'])
792 repo_path = str_to_dulwich(wire['path'])
792 self.repo = Repo.init(repo_path)
793 self.repo = Repo.init(repo_path)
793
794
794 @reraise_safe_exceptions
795 @reraise_safe_exceptions
795 def init_bare(self, wire):
796 def init_bare(self, wire):
796 repo_path = str_to_dulwich(wire['path'])
797 repo_path = str_to_dulwich(wire['path'])
797 self.repo = Repo.init_bare(repo_path)
798 self.repo = Repo.init_bare(repo_path)
798
799
799 @reraise_safe_exceptions
800 @reraise_safe_exceptions
800 def revision(self, wire, rev):
801 def revision(self, wire, rev):
801
802
802 cache_on, context_uid, repo_id = self._cache_on(wire)
803 cache_on, context_uid, repo_id = self._cache_on(wire)
803 @self.region.conditional_cache_on_arguments(condition=cache_on)
804 @self.region.conditional_cache_on_arguments(condition=cache_on)
804 def _revision(_context_uid, _repo_id, _rev):
805 def _revision(_context_uid, _repo_id, _rev):
805 repo_init = self._factory.repo_libgit2(wire)
806 repo_init = self._factory.repo_libgit2(wire)
806 with repo_init as repo:
807 with repo_init as repo:
807 commit = repo[rev]
808 commit = repo[rev]
808 obj_data = {
809 obj_data = {
809 'id': commit.id.hex,
810 'id': commit.id.hex,
810 }
811 }
811 # tree objects itself don't have tree_id attribute
812 # tree objects itself don't have tree_id attribute
812 if hasattr(commit, 'tree_id'):
813 if hasattr(commit, 'tree_id'):
813 obj_data['tree'] = commit.tree_id.hex
814 obj_data['tree'] = commit.tree_id.hex
814
815
815 return obj_data
816 return obj_data
816 return _revision(context_uid, repo_id, rev)
817 return _revision(context_uid, repo_id, rev)
817
818
818 @reraise_safe_exceptions
819 @reraise_safe_exceptions
819 def date(self, wire, commit_id):
820 def date(self, wire, commit_id):
820 cache_on, context_uid, repo_id = self._cache_on(wire)
821 cache_on, context_uid, repo_id = self._cache_on(wire)
821 @self.region.conditional_cache_on_arguments(condition=cache_on)
822 @self.region.conditional_cache_on_arguments(condition=cache_on)
822 def _date(_repo_id, _commit_id):
823 def _date(_repo_id, _commit_id):
823 repo_init = self._factory.repo_libgit2(wire)
824 repo_init = self._factory.repo_libgit2(wire)
824 with repo_init as repo:
825 with repo_init as repo:
825 commit = repo[commit_id]
826 commit = repo[commit_id]
826
827
827 if hasattr(commit, 'commit_time'):
828 if hasattr(commit, 'commit_time'):
828 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
829 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
829 else:
830 else:
830 commit = commit.get_object()
831 commit = commit.get_object()
831 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
832 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
832
833
833 # TODO(marcink): check dulwich difference of offset vs timezone
834 # TODO(marcink): check dulwich difference of offset vs timezone
834 return [commit_time, commit_time_offset]
835 return [commit_time, commit_time_offset]
835 return _date(repo_id, commit_id)
836 return _date(repo_id, commit_id)
836
837
837 @reraise_safe_exceptions
838 @reraise_safe_exceptions
838 def author(self, wire, commit_id):
839 def author(self, wire, commit_id):
839 cache_on, context_uid, repo_id = self._cache_on(wire)
840 cache_on, context_uid, repo_id = self._cache_on(wire)
840 @self.region.conditional_cache_on_arguments(condition=cache_on)
841 @self.region.conditional_cache_on_arguments(condition=cache_on)
841 def _author(_repo_id, _commit_id):
842 def _author(_repo_id, _commit_id):
842 repo_init = self._factory.repo_libgit2(wire)
843 repo_init = self._factory.repo_libgit2(wire)
843 with repo_init as repo:
844 with repo_init as repo:
844 commit = repo[commit_id]
845 commit = repo[commit_id]
845
846
846 if hasattr(commit, 'author'):
847 if hasattr(commit, 'author'):
847 author = commit.author
848 author = commit.author
848 else:
849 else:
849 author = commit.get_object().author
850 author = commit.get_object().author
850
851
851 if author.email:
852 if author.email:
852 return u"{} <{}>".format(author.name, author.email)
853 return u"{} <{}>".format(author.name, author.email)
853
854
854 try:
855 try:
855 return u"{}".format(author.name)
856 return u"{}".format(author.name)
856 except Exception:
857 except Exception:
857 return u"{}".format(safe_unicode(author.raw_name))
858 return u"{}".format(safe_unicode(author.raw_name))
858
859
859 return _author(repo_id, commit_id)
860 return _author(repo_id, commit_id)
860
861
861 @reraise_safe_exceptions
862 @reraise_safe_exceptions
862 def message(self, wire, commit_id):
863 def message(self, wire, commit_id):
863 cache_on, context_uid, repo_id = self._cache_on(wire)
864 cache_on, context_uid, repo_id = self._cache_on(wire)
864 @self.region.conditional_cache_on_arguments(condition=cache_on)
865 @self.region.conditional_cache_on_arguments(condition=cache_on)
865 def _message(_repo_id, _commit_id):
866 def _message(_repo_id, _commit_id):
866 repo_init = self._factory.repo_libgit2(wire)
867 repo_init = self._factory.repo_libgit2(wire)
867 with repo_init as repo:
868 with repo_init as repo:
868 commit = repo[commit_id]
869 commit = repo[commit_id]
869 return commit.message
870 return commit.message
870 return _message(repo_id, commit_id)
871 return _message(repo_id, commit_id)
871
872
872 @reraise_safe_exceptions
873 @reraise_safe_exceptions
873 def parents(self, wire, commit_id):
874 def parents(self, wire, commit_id):
874 cache_on, context_uid, repo_id = self._cache_on(wire)
875 cache_on, context_uid, repo_id = self._cache_on(wire)
875 @self.region.conditional_cache_on_arguments(condition=cache_on)
876 @self.region.conditional_cache_on_arguments(condition=cache_on)
876 def _parents(_repo_id, _commit_id):
877 def _parents(_repo_id, _commit_id):
877 repo_init = self._factory.repo_libgit2(wire)
878 repo_init = self._factory.repo_libgit2(wire)
878 with repo_init as repo:
879 with repo_init as repo:
879 commit = repo[commit_id]
880 commit = repo[commit_id]
880 if hasattr(commit, 'parent_ids'):
881 if hasattr(commit, 'parent_ids'):
881 parent_ids = commit.parent_ids
882 parent_ids = commit.parent_ids
882 else:
883 else:
883 parent_ids = commit.get_object().parent_ids
884 parent_ids = commit.get_object().parent_ids
884
885
885 return [x.hex for x in parent_ids]
886 return [x.hex for x in parent_ids]
886 return _parents(repo_id, commit_id)
887 return _parents(repo_id, commit_id)
887
888
888 @reraise_safe_exceptions
889 @reraise_safe_exceptions
889 def children(self, wire, commit_id):
890 def children(self, wire, commit_id):
890 cache_on, context_uid, repo_id = self._cache_on(wire)
891 cache_on, context_uid, repo_id = self._cache_on(wire)
891 @self.region.conditional_cache_on_arguments(condition=cache_on)
892 @self.region.conditional_cache_on_arguments(condition=cache_on)
892 def _children(_repo_id, _commit_id):
893 def _children(_repo_id, _commit_id):
893 output, __ = self.run_git_command(
894 output, __ = self.run_git_command(
894 wire, ['rev-list', '--all', '--children'])
895 wire, ['rev-list', '--all', '--children'])
895
896
896 child_ids = []
897 child_ids = []
897 pat = re.compile(r'^%s' % commit_id)
898 pat = re.compile(r'^%s' % commit_id)
898 for l in output.splitlines():
899 for l in output.splitlines():
899 if pat.match(l):
900 if pat.match(l):
900 found_ids = l.split(' ')[1:]
901 found_ids = l.split(' ')[1:]
901 child_ids.extend(found_ids)
902 child_ids.extend(found_ids)
902
903
903 return child_ids
904 return child_ids
904 return _children(repo_id, commit_id)
905 return _children(repo_id, commit_id)
905
906
906 @reraise_safe_exceptions
907 @reraise_safe_exceptions
907 def set_refs(self, wire, key, value):
908 def set_refs(self, wire, key, value):
908 repo_init = self._factory.repo_libgit2(wire)
909 repo_init = self._factory.repo_libgit2(wire)
909 with repo_init as repo:
910 with repo_init as repo:
910 repo.references.create(key, value, force=True)
911 repo.references.create(key, value, force=True)
911
912
912 @reraise_safe_exceptions
913 @reraise_safe_exceptions
913 def create_branch(self, wire, branch_name, commit_id, force=False):
914 def create_branch(self, wire, branch_name, commit_id, force=False):
914 repo_init = self._factory.repo_libgit2(wire)
915 repo_init = self._factory.repo_libgit2(wire)
915 with repo_init as repo:
916 with repo_init as repo:
916 commit = repo[commit_id]
917 commit = repo[commit_id]
917
918
918 if force:
919 if force:
919 repo.branches.local.create(branch_name, commit, force=force)
920 repo.branches.local.create(branch_name, commit, force=force)
920 elif not repo.branches.get(branch_name):
921 elif not repo.branches.get(branch_name):
921 # create only if that branch isn't existing
922 # create only if that branch isn't existing
922 repo.branches.local.create(branch_name, commit, force=force)
923 repo.branches.local.create(branch_name, commit, force=force)
923
924
924 @reraise_safe_exceptions
925 @reraise_safe_exceptions
925 def remove_ref(self, wire, key):
926 def remove_ref(self, wire, key):
926 repo_init = self._factory.repo_libgit2(wire)
927 repo_init = self._factory.repo_libgit2(wire)
927 with repo_init as repo:
928 with repo_init as repo:
928 repo.references.delete(key)
929 repo.references.delete(key)
929
930
930 @reraise_safe_exceptions
931 @reraise_safe_exceptions
931 def tag_remove(self, wire, tag_name):
932 def tag_remove(self, wire, tag_name):
932 repo_init = self._factory.repo_libgit2(wire)
933 repo_init = self._factory.repo_libgit2(wire)
933 with repo_init as repo:
934 with repo_init as repo:
934 key = 'refs/tags/{}'.format(tag_name)
935 key = 'refs/tags/{}'.format(tag_name)
935 repo.references.delete(key)
936 repo.references.delete(key)
936
937
937 @reraise_safe_exceptions
938 @reraise_safe_exceptions
938 def tree_changes(self, wire, source_id, target_id):
939 def tree_changes(self, wire, source_id, target_id):
939 # TODO(marcink): remove this seems it's only used by tests
940 # TODO(marcink): remove this seems it's only used by tests
940 repo = self._factory.repo(wire)
941 repo = self._factory.repo(wire)
941 source = repo[source_id].tree if source_id else None
942 source = repo[source_id].tree if source_id else None
942 target = repo[target_id].tree
943 target = repo[target_id].tree
943 result = repo.object_store.tree_changes(source, target)
944 result = repo.object_store.tree_changes(source, target)
944 return list(result)
945 return list(result)
945
946
946 @reraise_safe_exceptions
947 @reraise_safe_exceptions
947 def tree_and_type_for_path(self, wire, commit_id, path):
948 def tree_and_type_for_path(self, wire, commit_id, path):
948
949
949 cache_on, context_uid, repo_id = self._cache_on(wire)
950 cache_on, context_uid, repo_id = self._cache_on(wire)
950 @self.region.conditional_cache_on_arguments(condition=cache_on)
951 @self.region.conditional_cache_on_arguments(condition=cache_on)
951 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
952 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
952 repo_init = self._factory.repo_libgit2(wire)
953 repo_init = self._factory.repo_libgit2(wire)
953
954
954 with repo_init as repo:
955 with repo_init as repo:
955 commit = repo[commit_id]
956 commit = repo[commit_id]
956 try:
957 try:
957 tree = commit.tree[path]
958 tree = commit.tree[path]
958 except KeyError:
959 except KeyError:
959 return None, None, None
960 return None, None, None
960
961
961 return tree.id.hex, tree.type, tree.filemode
962 return tree.id.hex, tree.type, tree.filemode
962 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
963 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
963
964
964 @reraise_safe_exceptions
965 @reraise_safe_exceptions
965 def tree_items(self, wire, tree_id):
966 def tree_items(self, wire, tree_id):
966 cache_on, context_uid, repo_id = self._cache_on(wire)
967 cache_on, context_uid, repo_id = self._cache_on(wire)
967 @self.region.conditional_cache_on_arguments(condition=cache_on)
968 @self.region.conditional_cache_on_arguments(condition=cache_on)
968 def _tree_items(_repo_id, _tree_id):
969 def _tree_items(_repo_id, _tree_id):
969
970
970 repo_init = self._factory.repo_libgit2(wire)
971 repo_init = self._factory.repo_libgit2(wire)
971 with repo_init as repo:
972 with repo_init as repo:
972 try:
973 try:
973 tree = repo[tree_id]
974 tree = repo[tree_id]
974 except KeyError:
975 except KeyError:
975 raise ObjectMissing('No tree with id: {}'.format(tree_id))
976 raise ObjectMissing('No tree with id: {}'.format(tree_id))
976
977
977 result = []
978 result = []
978 for item in tree:
979 for item in tree:
979 item_sha = item.hex
980 item_sha = item.hex
980 item_mode = item.filemode
981 item_mode = item.filemode
981 item_type = item.type
982 item_type = item.type
982
983
983 if item_type == 'commit':
984 if item_type == 'commit':
984 # NOTE(marcink): submodules we translate to 'link' for backward compat
985 # NOTE(marcink): submodules we translate to 'link' for backward compat
985 item_type = 'link'
986 item_type = 'link'
986
987
987 result.append((item.name, item_mode, item_sha, item_type))
988 result.append((item.name, item_mode, item_sha, item_type))
988 return result
989 return result
989 return _tree_items(repo_id, tree_id)
990 return _tree_items(repo_id, tree_id)
990
991
991 @reraise_safe_exceptions
992 @reraise_safe_exceptions
992 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
993 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
993 """
994 """
994 Old version that uses subprocess to call diff
995 Old version that uses subprocess to call diff
995 """
996 """
996
997
997 flags = [
998 flags = [
998 '-U%s' % context, '--patch',
999 '-U%s' % context, '--patch',
999 '--binary',
1000 '--binary',
1000 '--find-renames',
1001 '--find-renames',
1001 '--no-indent-heuristic',
1002 '--no-indent-heuristic',
1002 # '--indent-heuristic',
1003 # '--indent-heuristic',
1003 #'--full-index',
1004 #'--full-index',
1004 #'--abbrev=40'
1005 #'--abbrev=40'
1005 ]
1006 ]
1006
1007
1007 if opt_ignorews:
1008 if opt_ignorews:
1008 flags.append('--ignore-all-space')
1009 flags.append('--ignore-all-space')
1009
1010
1010 if commit_id_1 == self.EMPTY_COMMIT:
1011 if commit_id_1 == self.EMPTY_COMMIT:
1011 cmd = ['show'] + flags + [commit_id_2]
1012 cmd = ['show'] + flags + [commit_id_2]
1012 else:
1013 else:
1013 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1014 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1014
1015
1015 if file_filter:
1016 if file_filter:
1016 cmd.extend(['--', file_filter])
1017 cmd.extend(['--', file_filter])
1017
1018
1018 diff, __ = self.run_git_command(wire, cmd)
1019 diff, __ = self.run_git_command(wire, cmd)
1019 # If we used 'show' command, strip first few lines (until actual diff
1020 # If we used 'show' command, strip first few lines (until actual diff
1020 # starts)
1021 # starts)
1021 if commit_id_1 == self.EMPTY_COMMIT:
1022 if commit_id_1 == self.EMPTY_COMMIT:
1022 lines = diff.splitlines()
1023 lines = diff.splitlines()
1023 x = 0
1024 x = 0
1024 for line in lines:
1025 for line in lines:
1025 if line.startswith('diff'):
1026 if line.startswith('diff'):
1026 break
1027 break
1027 x += 1
1028 x += 1
1028 # Append new line just like 'diff' command do
1029 # Append new line just like 'diff' command do
1029 diff = '\n'.join(lines[x:]) + '\n'
1030 diff = '\n'.join(lines[x:]) + '\n'
1030 return diff
1031 return diff
1031
1032
1032 @reraise_safe_exceptions
1033 @reraise_safe_exceptions
1033 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1034 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1034 repo_init = self._factory.repo_libgit2(wire)
1035 repo_init = self._factory.repo_libgit2(wire)
1035 with repo_init as repo:
1036 with repo_init as repo:
1036 swap = True
1037 swap = True
1037 flags = 0
1038 flags = 0
1038 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1039 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1039
1040
1040 if opt_ignorews:
1041 if opt_ignorews:
1041 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1042 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1042
1043
1043 if commit_id_1 == self.EMPTY_COMMIT:
1044 if commit_id_1 == self.EMPTY_COMMIT:
1044 comm1 = repo[commit_id_2]
1045 comm1 = repo[commit_id_2]
1045 diff_obj = comm1.tree.diff_to_tree(
1046 diff_obj = comm1.tree.diff_to_tree(
1046 flags=flags, context_lines=context, swap=swap)
1047 flags=flags, context_lines=context, swap=swap)
1047
1048
1048 else:
1049 else:
1049 comm1 = repo[commit_id_2]
1050 comm1 = repo[commit_id_2]
1050 comm2 = repo[commit_id_1]
1051 comm2 = repo[commit_id_1]
1051 diff_obj = comm1.tree.diff_to_tree(
1052 diff_obj = comm1.tree.diff_to_tree(
1052 comm2.tree, flags=flags, context_lines=context, swap=swap)
1053 comm2.tree, flags=flags, context_lines=context, swap=swap)
1053 similar_flags = 0
1054 similar_flags = 0
1054 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1055 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1055 diff_obj.find_similar(flags=similar_flags)
1056 diff_obj.find_similar(flags=similar_flags)
1056
1057
1057 if file_filter:
1058 if file_filter:
1058 for p in diff_obj:
1059 for p in diff_obj:
1059 if p.delta.old_file.path == file_filter:
1060 if p.delta.old_file.path == file_filter:
1060 return p.patch or ''
1061 return p.patch or ''
1061 # fo matching path == no diff
1062 # fo matching path == no diff
1062 return ''
1063 return ''
1063 return diff_obj.patch or ''
1064 return diff_obj.patch or ''
1064
1065
1065 @reraise_safe_exceptions
1066 @reraise_safe_exceptions
1066 def node_history(self, wire, commit_id, path, limit):
1067 def node_history(self, wire, commit_id, path, limit):
1067 cache_on, context_uid, repo_id = self._cache_on(wire)
1068 cache_on, context_uid, repo_id = self._cache_on(wire)
1068 @self.region.conditional_cache_on_arguments(condition=cache_on)
1069 @self.region.conditional_cache_on_arguments(condition=cache_on)
1069 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1070 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1070 # optimize for n==1, rev-list is much faster for that use-case
1071 # optimize for n==1, rev-list is much faster for that use-case
1071 if limit == 1:
1072 if limit == 1:
1072 cmd = ['rev-list', '-1', commit_id, '--', path]
1073 cmd = ['rev-list', '-1', commit_id, '--', path]
1073 else:
1074 else:
1074 cmd = ['log']
1075 cmd = ['log']
1075 if limit:
1076 if limit:
1076 cmd.extend(['-n', str(safe_int(limit, 0))])
1077 cmd.extend(['-n', str(safe_int(limit, 0))])
1077 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1078 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1078
1079
1079 output, __ = self.run_git_command(wire, cmd)
1080 output, __ = self.run_git_command(wire, cmd)
1080 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
1081 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
1081
1082
1082 return [x for x in commit_ids]
1083 return [x for x in commit_ids]
1083 return _node_history(context_uid, repo_id, commit_id, path, limit)
1084 return _node_history(context_uid, repo_id, commit_id, path, limit)
1084
1085
1085 @reraise_safe_exceptions
1086 @reraise_safe_exceptions
1086 def node_annotate(self, wire, commit_id, path):
1087 def node_annotate(self, wire, commit_id, path):
1087
1088
1088 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1089 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1089 # -l ==> outputs long shas (and we need all 40 characters)
1090 # -l ==> outputs long shas (and we need all 40 characters)
1090 # --root ==> doesn't put '^' character for boundaries
1091 # --root ==> doesn't put '^' character for boundaries
1091 # -r commit_id ==> blames for the given commit
1092 # -r commit_id ==> blames for the given commit
1092 output, __ = self.run_git_command(wire, cmd)
1093 output, __ = self.run_git_command(wire, cmd)
1093
1094
1094 result = []
1095 result = []
1095 for i, blame_line in enumerate(output.split('\n')[:-1]):
1096 for i, blame_line in enumerate(output.split('\n')[:-1]):
1096 line_no = i + 1
1097 line_no = i + 1
1097 commit_id, line = re.split(r' ', blame_line, 1)
1098 commit_id, line = re.split(r' ', blame_line, 1)
1098 result.append((line_no, commit_id, line))
1099 result.append((line_no, commit_id, line))
1099 return result
1100 return result
1100
1101
1101 @reraise_safe_exceptions
1102 @reraise_safe_exceptions
1102 def update_server_info(self, wire):
1103 def update_server_info(self, wire):
1103 repo = self._factory.repo(wire)
1104 repo = self._factory.repo(wire)
1104 update_server_info(repo)
1105 update_server_info(repo)
1105
1106
1106 @reraise_safe_exceptions
1107 @reraise_safe_exceptions
1107 def get_all_commit_ids(self, wire):
1108 def get_all_commit_ids(self, wire):
1108
1109
1109 cache_on, context_uid, repo_id = self._cache_on(wire)
1110 cache_on, context_uid, repo_id = self._cache_on(wire)
1110 @self.region.conditional_cache_on_arguments(condition=cache_on)
1111 @self.region.conditional_cache_on_arguments(condition=cache_on)
1111 def _get_all_commit_ids(_context_uid, _repo_id):
1112 def _get_all_commit_ids(_context_uid, _repo_id):
1112
1113
1113 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1114 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1114 try:
1115 try:
1115 output, __ = self.run_git_command(wire, cmd)
1116 output, __ = self.run_git_command(wire, cmd)
1116 return output.splitlines()
1117 return output.splitlines()
1117 except Exception:
1118 except Exception:
1118 # Can be raised for empty repositories
1119 # Can be raised for empty repositories
1119 return []
1120 return []
1120 return _get_all_commit_ids(context_uid, repo_id)
1121 return _get_all_commit_ids(context_uid, repo_id)
1121
1122
1122 @reraise_safe_exceptions
1123 @reraise_safe_exceptions
1123 def run_git_command(self, wire, cmd, **opts):
1124 def run_git_command(self, wire, cmd, **opts):
1124 path = wire.get('path', None)
1125 path = wire.get('path', None)
1125
1126
1126 if path and os.path.isdir(path):
1127 if path and os.path.isdir(path):
1127 opts['cwd'] = path
1128 opts['cwd'] = path
1128
1129
1129 if '_bare' in opts:
1130 if '_bare' in opts:
1130 _copts = []
1131 _copts = []
1131 del opts['_bare']
1132 del opts['_bare']
1132 else:
1133 else:
1133 _copts = ['-c', 'core.quotepath=false', ]
1134 _copts = ['-c', 'core.quotepath=false', ]
1134 safe_call = False
1135 safe_call = False
1135 if '_safe' in opts:
1136 if '_safe' in opts:
1136 # no exc on failure
1137 # no exc on failure
1137 del opts['_safe']
1138 del opts['_safe']
1138 safe_call = True
1139 safe_call = True
1139
1140
1140 if '_copts' in opts:
1141 if '_copts' in opts:
1141 _copts.extend(opts['_copts'] or [])
1142 _copts.extend(opts['_copts'] or [])
1142 del opts['_copts']
1143 del opts['_copts']
1143
1144
1144 gitenv = os.environ.copy()
1145 gitenv = os.environ.copy()
1145 gitenv.update(opts.pop('extra_env', {}))
1146 gitenv.update(opts.pop('extra_env', {}))
1146 # need to clean fix GIT_DIR !
1147 # need to clean fix GIT_DIR !
1147 if 'GIT_DIR' in gitenv:
1148 if 'GIT_DIR' in gitenv:
1148 del gitenv['GIT_DIR']
1149 del gitenv['GIT_DIR']
1149 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1150 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1150 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1151 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1151
1152
1152 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1153 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1153 _opts = {'env': gitenv, 'shell': False}
1154 _opts = {'env': gitenv, 'shell': False}
1154
1155
1155 proc = None
1156 proc = None
1156 try:
1157 try:
1157 _opts.update(opts)
1158 _opts.update(opts)
1158 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1159 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1159
1160
1160 return ''.join(proc), ''.join(proc.error)
1161 return ''.join(proc), ''.join(proc.error)
1161 except (EnvironmentError, OSError) as err:
1162 except (EnvironmentError, OSError) as err:
1162 cmd = ' '.join(cmd) # human friendly CMD
1163 cmd = ' '.join(cmd) # human friendly CMD
1163 tb_err = ("Couldn't run git command (%s).\n"
1164 tb_err = ("Couldn't run git command (%s).\n"
1164 "Original error was:%s\n"
1165 "Original error was:%s\n"
1165 "Call options:%s\n"
1166 "Call options:%s\n"
1166 % (cmd, err, _opts))
1167 % (cmd, err, _opts))
1167 log.exception(tb_err)
1168 log.exception(tb_err)
1168 if safe_call:
1169 if safe_call:
1169 return '', err
1170 return '', err
1170 else:
1171 else:
1171 raise exceptions.VcsException()(tb_err)
1172 raise exceptions.VcsException()(tb_err)
1172 finally:
1173 finally:
1173 if proc:
1174 if proc:
1174 proc.close()
1175 proc.close()
1175
1176
1176 @reraise_safe_exceptions
1177 @reraise_safe_exceptions
1177 def install_hooks(self, wire, force=False):
1178 def install_hooks(self, wire, force=False):
1178 from vcsserver.hook_utils import install_git_hooks
1179 from vcsserver.hook_utils import install_git_hooks
1179 bare = self.bare(wire)
1180 bare = self.bare(wire)
1180 path = wire['path']
1181 path = wire['path']
1181 return install_git_hooks(path, bare, force_create=force)
1182 return install_git_hooks(path, bare, force_create=force)
1182
1183
1183 @reraise_safe_exceptions
1184 @reraise_safe_exceptions
1184 def get_hooks_info(self, wire):
1185 def get_hooks_info(self, wire):
1185 from vcsserver.hook_utils import (
1186 from vcsserver.hook_utils import (
1186 get_git_pre_hook_version, get_git_post_hook_version)
1187 get_git_pre_hook_version, get_git_post_hook_version)
1187 bare = self.bare(wire)
1188 bare = self.bare(wire)
1188 path = wire['path']
1189 path = wire['path']
1189 return {
1190 return {
1190 'pre_version': get_git_pre_hook_version(path, bare),
1191 'pre_version': get_git_pre_hook_version(path, bare),
1191 'post_version': get_git_post_hook_version(path, bare),
1192 'post_version': get_git_post_hook_version(path, bare),
1192 }
1193 }
1194
1195 @reraise_safe_exceptions
1196 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1197 archive_dir_name, commit_id):
1198
1199 def file_walker(_commit_id, path):
1200 repo_init = self._factory.repo_libgit2(wire)
1201
1202 with repo_init as repo:
1203 commit = repo[commit_id]
1204
1205 if path in ['', '/']:
1206 tree = commit.tree
1207 else:
1208 tree = commit.tree[path.rstrip('/')]
1209 tree_id = tree.id.hex
1210 try:
1211 tree = repo[tree_id]
1212 except KeyError:
1213 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1214
1215 index = LibGit2Index.Index()
1216 index.read_tree(tree)
1217 file_iter = index
1218
1219 for fn in file_iter:
1220 file_path = fn.path
1221 mode = fn.mode
1222 is_link = stat.S_ISLNK(mode)
1223 yield ArchiveNode(file_path, mode, is_link, repo[fn.id].read_raw)
1224
1225 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1226 archive_dir_name, commit_id)
@@ -1,1009 +1,1021 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17 import functools
18 import io
18 import io
19 import logging
19 import logging
20 import os
20 import stat
21 import stat
21 import urllib
22 import urllib
22 import urllib2
23 import urllib2
23 import traceback
24 import traceback
24
25
25 from hgext import largefiles, rebase, purge
26 from hgext import largefiles, rebase, purge
26 from hgext.strip import strip as hgext_strip
27 from hgext.strip import strip as hgext_strip
27 from mercurial import commands
28 from mercurial import commands
28 from mercurial import unionrepo
29 from mercurial import unionrepo
29 from mercurial import verify
30 from mercurial import verify
30 from mercurial import repair
31 from mercurial import repair
31
32
32 import vcsserver
33 import vcsserver
33 from vcsserver import exceptions
34 from vcsserver import exceptions
34 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
35 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
35 from vcsserver.hgcompat import (
36 from vcsserver.hgcompat import (
36 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
37 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
37 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
38 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
38 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
39 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
39 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
40 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
40 RepoLookupError, InterventionRequired, RequirementError)
41 RepoLookupError, InterventionRequired, RequirementError,
42 alwaysmatcher, patternmatcher, hgutil)
41 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.vcs_base import RemoteBase
42
44
43 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
44
46
45
47
46 def make_ui_from_config(repo_config):
48 def make_ui_from_config(repo_config):
47
49
48 class LoggingUI(ui.ui):
50 class LoggingUI(ui.ui):
49 def status(self, *msg, **opts):
51 def status(self, *msg, **opts):
50 log.info(' '.join(msg).rstrip('\n'))
52 log.info(' '.join(msg).rstrip('\n'))
51 super(LoggingUI, self).status(*msg, **opts)
53 super(LoggingUI, self).status(*msg, **opts)
52
54
53 def warn(self, *msg, **opts):
55 def warn(self, *msg, **opts):
54 log.warn(' '.join(msg).rstrip('\n'))
56 log.warn(' '.join(msg).rstrip('\n'))
55 super(LoggingUI, self).warn(*msg, **opts)
57 super(LoggingUI, self).warn(*msg, **opts)
56
58
57 def error(self, *msg, **opts):
59 def error(self, *msg, **opts):
58 log.error(' '.join(msg).rstrip('\n'))
60 log.error(' '.join(msg).rstrip('\n'))
59 super(LoggingUI, self).error(*msg, **opts)
61 super(LoggingUI, self).error(*msg, **opts)
60
62
61 def note(self, *msg, **opts):
63 def note(self, *msg, **opts):
62 log.info(' '.join(msg).rstrip('\n'))
64 log.info(' '.join(msg).rstrip('\n'))
63 super(LoggingUI, self).note(*msg, **opts)
65 super(LoggingUI, self).note(*msg, **opts)
64
66
65 def debug(self, *msg, **opts):
67 def debug(self, *msg, **opts):
66 log.debug(' '.join(msg).rstrip('\n'))
68 log.debug(' '.join(msg).rstrip('\n'))
67 super(LoggingUI, self).debug(*msg, **opts)
69 super(LoggingUI, self).debug(*msg, **opts)
68
70
69 baseui = LoggingUI()
71 baseui = LoggingUI()
70
72
71 # clean the baseui object
73 # clean the baseui object
72 baseui._ocfg = hgconfig.config()
74 baseui._ocfg = hgconfig.config()
73 baseui._ucfg = hgconfig.config()
75 baseui._ucfg = hgconfig.config()
74 baseui._tcfg = hgconfig.config()
76 baseui._tcfg = hgconfig.config()
75
77
76 for section, option, value in repo_config:
78 for section, option, value in repo_config:
77 baseui.setconfig(section, option, value)
79 baseui.setconfig(section, option, value)
78
80
79 # make our hgweb quiet so it doesn't print output
81 # make our hgweb quiet so it doesn't print output
80 baseui.setconfig('ui', 'quiet', 'true')
82 baseui.setconfig('ui', 'quiet', 'true')
81
83
82 baseui.setconfig('ui', 'paginate', 'never')
84 baseui.setconfig('ui', 'paginate', 'never')
83 # for better Error reporting of Mercurial
85 # for better Error reporting of Mercurial
84 baseui.setconfig('ui', 'message-output', 'stderr')
86 baseui.setconfig('ui', 'message-output', 'stderr')
85
87
86 # force mercurial to only use 1 thread, otherwise it may try to set a
88 # force mercurial to only use 1 thread, otherwise it may try to set a
87 # signal in a non-main thread, thus generating a ValueError.
89 # signal in a non-main thread, thus generating a ValueError.
88 baseui.setconfig('worker', 'numcpus', 1)
90 baseui.setconfig('worker', 'numcpus', 1)
89
91
90 # If there is no config for the largefiles extension, we explicitly disable
92 # If there is no config for the largefiles extension, we explicitly disable
91 # it here. This overrides settings from repositories hgrc file. Recent
93 # it here. This overrides settings from repositories hgrc file. Recent
92 # mercurial versions enable largefiles in hgrc on clone from largefile
94 # mercurial versions enable largefiles in hgrc on clone from largefile
93 # repo.
95 # repo.
94 if not baseui.hasconfig('extensions', 'largefiles'):
96 if not baseui.hasconfig('extensions', 'largefiles'):
95 log.debug('Explicitly disable largefiles extension for repo.')
97 log.debug('Explicitly disable largefiles extension for repo.')
96 baseui.setconfig('extensions', 'largefiles', '!')
98 baseui.setconfig('extensions', 'largefiles', '!')
97
99
98 return baseui
100 return baseui
99
101
100
102
101 def reraise_safe_exceptions(func):
103 def reraise_safe_exceptions(func):
102 """Decorator for converting mercurial exceptions to something neutral."""
104 """Decorator for converting mercurial exceptions to something neutral."""
103
105
104 def wrapper(*args, **kwargs):
106 def wrapper(*args, **kwargs):
105 try:
107 try:
106 return func(*args, **kwargs)
108 return func(*args, **kwargs)
107 except (Abort, InterventionRequired) as e:
109 except (Abort, InterventionRequired) as e:
108 raise_from_original(exceptions.AbortException(e))
110 raise_from_original(exceptions.AbortException(e))
109 except RepoLookupError as e:
111 except RepoLookupError as e:
110 raise_from_original(exceptions.LookupException(e))
112 raise_from_original(exceptions.LookupException(e))
111 except RequirementError as e:
113 except RequirementError as e:
112 raise_from_original(exceptions.RequirementException(e))
114 raise_from_original(exceptions.RequirementException(e))
113 except RepoError as e:
115 except RepoError as e:
114 raise_from_original(exceptions.VcsException(e))
116 raise_from_original(exceptions.VcsException(e))
115 except LookupError as e:
117 except LookupError as e:
116 raise_from_original(exceptions.LookupException(e))
118 raise_from_original(exceptions.LookupException(e))
117 except Exception as e:
119 except Exception as e:
118 if not hasattr(e, '_vcs_kind'):
120 if not hasattr(e, '_vcs_kind'):
119 log.exception("Unhandled exception in hg remote call")
121 log.exception("Unhandled exception in hg remote call")
120 raise_from_original(exceptions.UnhandledException(e))
122 raise_from_original(exceptions.UnhandledException(e))
121
123
122 raise
124 raise
123 return wrapper
125 return wrapper
124
126
125
127
126 class MercurialFactory(RepoFactory):
128 class MercurialFactory(RepoFactory):
127 repo_type = 'hg'
129 repo_type = 'hg'
128
130
129 def _create_config(self, config, hooks=True):
131 def _create_config(self, config, hooks=True):
130 if not hooks:
132 if not hooks:
131 hooks_to_clean = frozenset((
133 hooks_to_clean = frozenset((
132 'changegroup.repo_size', 'preoutgoing.pre_pull',
134 'changegroup.repo_size', 'preoutgoing.pre_pull',
133 'outgoing.pull_logger', 'prechangegroup.pre_push'))
135 'outgoing.pull_logger', 'prechangegroup.pre_push'))
134 new_config = []
136 new_config = []
135 for section, option, value in config:
137 for section, option, value in config:
136 if section == 'hooks' and option in hooks_to_clean:
138 if section == 'hooks' and option in hooks_to_clean:
137 continue
139 continue
138 new_config.append((section, option, value))
140 new_config.append((section, option, value))
139 config = new_config
141 config = new_config
140
142
141 baseui = make_ui_from_config(config)
143 baseui = make_ui_from_config(config)
142 return baseui
144 return baseui
143
145
144 def _create_repo(self, wire, create):
146 def _create_repo(self, wire, create):
145 baseui = self._create_config(wire["config"])
147 baseui = self._create_config(wire["config"])
146 return instance(baseui, wire["path"], create)
148 return instance(baseui, wire["path"], create)
147
149
148 def repo(self, wire, create=False):
150 def repo(self, wire, create=False):
149 """
151 """
150 Get a repository instance for the given path.
152 Get a repository instance for the given path.
151 """
153 """
152 return self._create_repo(wire, create)
154 return self._create_repo(wire, create)
153
155
154
156
155 def patch_ui_message_output(baseui):
157 def patch_ui_message_output(baseui):
156 baseui.setconfig('ui', 'quiet', 'false')
158 baseui.setconfig('ui', 'quiet', 'false')
157 output = io.BytesIO()
159 output = io.BytesIO()
158
160
159 def write(data, **unused_kwargs):
161 def write(data, **unused_kwargs):
160 output.write(data)
162 output.write(data)
161
163
162 baseui.status = write
164 baseui.status = write
163 baseui.write = write
165 baseui.write = write
164 baseui.warn = write
166 baseui.warn = write
165 baseui.debug = write
167 baseui.debug = write
166
168
167 return baseui, output
169 return baseui, output
168
170
169
171
170 class HgRemote(RemoteBase):
172 class HgRemote(RemoteBase):
171
173
172 def __init__(self, factory):
174 def __init__(self, factory):
173 self._factory = factory
175 self._factory = factory
174 self._bulk_methods = {
176 self._bulk_methods = {
175 "affected_files": self.ctx_files,
177 "affected_files": self.ctx_files,
176 "author": self.ctx_user,
178 "author": self.ctx_user,
177 "branch": self.ctx_branch,
179 "branch": self.ctx_branch,
178 "children": self.ctx_children,
180 "children": self.ctx_children,
179 "date": self.ctx_date,
181 "date": self.ctx_date,
180 "message": self.ctx_description,
182 "message": self.ctx_description,
181 "parents": self.ctx_parents,
183 "parents": self.ctx_parents,
182 "status": self.ctx_status,
184 "status": self.ctx_status,
183 "obsolete": self.ctx_obsolete,
185 "obsolete": self.ctx_obsolete,
184 "phase": self.ctx_phase,
186 "phase": self.ctx_phase,
185 "hidden": self.ctx_hidden,
187 "hidden": self.ctx_hidden,
186 "_file_paths": self.ctx_list,
188 "_file_paths": self.ctx_list,
187 }
189 }
188
190
189 def _get_ctx(self, repo, ref):
191 def _get_ctx(self, repo, ref):
190 return get_ctx(repo, ref)
192 return get_ctx(repo, ref)
191
193
192 @reraise_safe_exceptions
194 @reraise_safe_exceptions
193 def discover_hg_version(self):
195 def discover_hg_version(self):
194 from mercurial import util
196 from mercurial import util
195 return util.version()
197 return util.version()
196
198
197 @reraise_safe_exceptions
199 @reraise_safe_exceptions
198 def is_empty(self, wire):
200 def is_empty(self, wire):
199 repo = self._factory.repo(wire)
201 repo = self._factory.repo(wire)
200
202
201 try:
203 try:
202 return len(repo) == 0
204 return len(repo) == 0
203 except Exception:
205 except Exception:
204 log.exception("failed to read object_store")
206 log.exception("failed to read object_store")
205 return False
207 return False
206
208
207 @reraise_safe_exceptions
209 @reraise_safe_exceptions
208 def archive_repo(self, archive_path, mtime, file_info, kind):
209 if kind == "tgz":
210 archiver = archival.tarit(archive_path, mtime, "gz")
211 elif kind == "tbz2":
212 archiver = archival.tarit(archive_path, mtime, "bz2")
213 elif kind == 'zip':
214 archiver = archival.zipit(archive_path, mtime)
215 else:
216 raise exceptions.ArchiveException()(
217 'Remote does not support: "%s".' % kind)
218
219 for f_path, f_mode, f_is_link, f_content in file_info:
220 archiver.addfile(f_path, f_mode, f_is_link, f_content)
221 archiver.done()
222
223 @reraise_safe_exceptions
224 def bookmarks(self, wire):
210 def bookmarks(self, wire):
225 cache_on, context_uid, repo_id = self._cache_on(wire)
211 cache_on, context_uid, repo_id = self._cache_on(wire)
226 @self.region.conditional_cache_on_arguments(condition=cache_on)
212 @self.region.conditional_cache_on_arguments(condition=cache_on)
227 def _bookmarks(_context_uid, _repo_id):
213 def _bookmarks(_context_uid, _repo_id):
228 repo = self._factory.repo(wire)
214 repo = self._factory.repo(wire)
229 return dict(repo._bookmarks)
215 return dict(repo._bookmarks)
230
216
231 return _bookmarks(context_uid, repo_id)
217 return _bookmarks(context_uid, repo_id)
232
218
233 @reraise_safe_exceptions
219 @reraise_safe_exceptions
234 def branches(self, wire, normal, closed):
220 def branches(self, wire, normal, closed):
235 cache_on, context_uid, repo_id = self._cache_on(wire)
221 cache_on, context_uid, repo_id = self._cache_on(wire)
236 @self.region.conditional_cache_on_arguments(condition=cache_on)
222 @self.region.conditional_cache_on_arguments(condition=cache_on)
237 def _branches(_context_uid, _repo_id, _normal, _closed):
223 def _branches(_context_uid, _repo_id, _normal, _closed):
238 repo = self._factory.repo(wire)
224 repo = self._factory.repo(wire)
239 iter_branches = repo.branchmap().iterbranches()
225 iter_branches = repo.branchmap().iterbranches()
240 bt = {}
226 bt = {}
241 for branch_name, _heads, tip, is_closed in iter_branches:
227 for branch_name, _heads, tip, is_closed in iter_branches:
242 if normal and not is_closed:
228 if normal and not is_closed:
243 bt[branch_name] = tip
229 bt[branch_name] = tip
244 if closed and is_closed:
230 if closed and is_closed:
245 bt[branch_name] = tip
231 bt[branch_name] = tip
246
232
247 return bt
233 return bt
248
234
249 return _branches(context_uid, repo_id, normal, closed)
235 return _branches(context_uid, repo_id, normal, closed)
250
236
251 @reraise_safe_exceptions
237 @reraise_safe_exceptions
252 def bulk_request(self, wire, commit_id, pre_load):
238 def bulk_request(self, wire, commit_id, pre_load):
253 cache_on, context_uid, repo_id = self._cache_on(wire)
239 cache_on, context_uid, repo_id = self._cache_on(wire)
254 @self.region.conditional_cache_on_arguments(condition=cache_on)
240 @self.region.conditional_cache_on_arguments(condition=cache_on)
255 def _bulk_request(_repo_id, _commit_id, _pre_load):
241 def _bulk_request(_repo_id, _commit_id, _pre_load):
256 result = {}
242 result = {}
257 for attr in pre_load:
243 for attr in pre_load:
258 try:
244 try:
259 method = self._bulk_methods[attr]
245 method = self._bulk_methods[attr]
260 result[attr] = method(wire, commit_id)
246 result[attr] = method(wire, commit_id)
261 except KeyError as e:
247 except KeyError as e:
262 raise exceptions.VcsException(e)(
248 raise exceptions.VcsException(e)(
263 'Unknown bulk attribute: "%s"' % attr)
249 'Unknown bulk attribute: "%s"' % attr)
264 return result
250 return result
265
251
266 return _bulk_request(repo_id, commit_id, sorted(pre_load))
252 return _bulk_request(repo_id, commit_id, sorted(pre_load))
267
253
268 @reraise_safe_exceptions
254 @reraise_safe_exceptions
269 def ctx_branch(self, wire, commit_id):
255 def ctx_branch(self, wire, commit_id):
270 cache_on, context_uid, repo_id = self._cache_on(wire)
256 cache_on, context_uid, repo_id = self._cache_on(wire)
271 @self.region.conditional_cache_on_arguments(condition=cache_on)
257 @self.region.conditional_cache_on_arguments(condition=cache_on)
272 def _ctx_branch(_repo_id, _commit_id):
258 def _ctx_branch(_repo_id, _commit_id):
273 repo = self._factory.repo(wire)
259 repo = self._factory.repo(wire)
274 ctx = self._get_ctx(repo, commit_id)
260 ctx = self._get_ctx(repo, commit_id)
275 return ctx.branch()
261 return ctx.branch()
276 return _ctx_branch(repo_id, commit_id)
262 return _ctx_branch(repo_id, commit_id)
277
263
278 @reraise_safe_exceptions
264 @reraise_safe_exceptions
279 def ctx_date(self, wire, commit_id):
265 def ctx_date(self, wire, commit_id):
280 cache_on, context_uid, repo_id = self._cache_on(wire)
266 cache_on, context_uid, repo_id = self._cache_on(wire)
281 @self.region.conditional_cache_on_arguments(condition=cache_on)
267 @self.region.conditional_cache_on_arguments(condition=cache_on)
282 def _ctx_date(_repo_id, _commit_id):
268 def _ctx_date(_repo_id, _commit_id):
283 repo = self._factory.repo(wire)
269 repo = self._factory.repo(wire)
284 ctx = self._get_ctx(repo, commit_id)
270 ctx = self._get_ctx(repo, commit_id)
285 return ctx.date()
271 return ctx.date()
286 return _ctx_date(repo_id, commit_id)
272 return _ctx_date(repo_id, commit_id)
287
273
288 @reraise_safe_exceptions
274 @reraise_safe_exceptions
289 def ctx_description(self, wire, revision):
275 def ctx_description(self, wire, revision):
290 repo = self._factory.repo(wire)
276 repo = self._factory.repo(wire)
291 ctx = self._get_ctx(repo, revision)
277 ctx = self._get_ctx(repo, revision)
292 return ctx.description()
278 return ctx.description()
293
279
294 @reraise_safe_exceptions
280 @reraise_safe_exceptions
295 def ctx_files(self, wire, commit_id):
281 def ctx_files(self, wire, commit_id):
296 cache_on, context_uid, repo_id = self._cache_on(wire)
282 cache_on, context_uid, repo_id = self._cache_on(wire)
297 @self.region.conditional_cache_on_arguments(condition=cache_on)
283 @self.region.conditional_cache_on_arguments(condition=cache_on)
298 def _ctx_files(_repo_id, _commit_id):
284 def _ctx_files(_repo_id, _commit_id):
299 repo = self._factory.repo(wire)
285 repo = self._factory.repo(wire)
300 ctx = self._get_ctx(repo, commit_id)
286 ctx = self._get_ctx(repo, commit_id)
301 return ctx.files()
287 return ctx.files()
302
288
303 return _ctx_files(repo_id, commit_id)
289 return _ctx_files(repo_id, commit_id)
304
290
305 @reraise_safe_exceptions
291 @reraise_safe_exceptions
306 def ctx_list(self, path, revision):
292 def ctx_list(self, path, revision):
307 repo = self._factory.repo(path)
293 repo = self._factory.repo(path)
308 ctx = self._get_ctx(repo, revision)
294 ctx = self._get_ctx(repo, revision)
309 return list(ctx)
295 return list(ctx)
310
296
311 @reraise_safe_exceptions
297 @reraise_safe_exceptions
312 def ctx_parents(self, wire, commit_id):
298 def ctx_parents(self, wire, commit_id):
313 cache_on, context_uid, repo_id = self._cache_on(wire)
299 cache_on, context_uid, repo_id = self._cache_on(wire)
314 @self.region.conditional_cache_on_arguments(condition=cache_on)
300 @self.region.conditional_cache_on_arguments(condition=cache_on)
315 def _ctx_parents(_repo_id, _commit_id):
301 def _ctx_parents(_repo_id, _commit_id):
316 repo = self._factory.repo(wire)
302 repo = self._factory.repo(wire)
317 ctx = self._get_ctx(repo, commit_id)
303 ctx = self._get_ctx(repo, commit_id)
318 return [parent.hex() for parent in ctx.parents()
304 return [parent.hex() for parent in ctx.parents()
319 if not (parent.hidden() or parent.obsolete())]
305 if not (parent.hidden() or parent.obsolete())]
320
306
321 return _ctx_parents(repo_id, commit_id)
307 return _ctx_parents(repo_id, commit_id)
322
308
323 @reraise_safe_exceptions
309 @reraise_safe_exceptions
324 def ctx_children(self, wire, commit_id):
310 def ctx_children(self, wire, commit_id):
325 cache_on, context_uid, repo_id = self._cache_on(wire)
311 cache_on, context_uid, repo_id = self._cache_on(wire)
326 @self.region.conditional_cache_on_arguments(condition=cache_on)
312 @self.region.conditional_cache_on_arguments(condition=cache_on)
327 def _ctx_children(_repo_id, _commit_id):
313 def _ctx_children(_repo_id, _commit_id):
328 repo = self._factory.repo(wire)
314 repo = self._factory.repo(wire)
329 ctx = self._get_ctx(repo, commit_id)
315 ctx = self._get_ctx(repo, commit_id)
330 return [child.hex() for child in ctx.children()
316 return [child.hex() for child in ctx.children()
331 if not (child.hidden() or child.obsolete())]
317 if not (child.hidden() or child.obsolete())]
332
318
333 return _ctx_children(repo_id, commit_id)
319 return _ctx_children(repo_id, commit_id)
334
320
335 @reraise_safe_exceptions
321 @reraise_safe_exceptions
336 def ctx_phase(self, wire, commit_id):
322 def ctx_phase(self, wire, commit_id):
337 cache_on, context_uid, repo_id = self._cache_on(wire)
323 cache_on, context_uid, repo_id = self._cache_on(wire)
338 @self.region.conditional_cache_on_arguments(condition=cache_on)
324 @self.region.conditional_cache_on_arguments(condition=cache_on)
339 def _ctx_phase(_context_uid, _repo_id, _commit_id):
325 def _ctx_phase(_context_uid, _repo_id, _commit_id):
340 repo = self._factory.repo(wire)
326 repo = self._factory.repo(wire)
341 ctx = self._get_ctx(repo, commit_id)
327 ctx = self._get_ctx(repo, commit_id)
342 # public=0, draft=1, secret=3
328 # public=0, draft=1, secret=3
343 return ctx.phase()
329 return ctx.phase()
344 return _ctx_phase(context_uid, repo_id, commit_id)
330 return _ctx_phase(context_uid, repo_id, commit_id)
345
331
346 @reraise_safe_exceptions
332 @reraise_safe_exceptions
347 def ctx_obsolete(self, wire, commit_id):
333 def ctx_obsolete(self, wire, commit_id):
348 cache_on, context_uid, repo_id = self._cache_on(wire)
334 cache_on, context_uid, repo_id = self._cache_on(wire)
349 @self.region.conditional_cache_on_arguments(condition=cache_on)
335 @self.region.conditional_cache_on_arguments(condition=cache_on)
350 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
336 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
351 repo = self._factory.repo(wire)
337 repo = self._factory.repo(wire)
352 ctx = self._get_ctx(repo, commit_id)
338 ctx = self._get_ctx(repo, commit_id)
353 return ctx.obsolete()
339 return ctx.obsolete()
354 return _ctx_obsolete(context_uid, repo_id, commit_id)
340 return _ctx_obsolete(context_uid, repo_id, commit_id)
355
341
356 @reraise_safe_exceptions
342 @reraise_safe_exceptions
357 def ctx_hidden(self, wire, commit_id):
343 def ctx_hidden(self, wire, commit_id):
358 cache_on, context_uid, repo_id = self._cache_on(wire)
344 cache_on, context_uid, repo_id = self._cache_on(wire)
359 @self.region.conditional_cache_on_arguments(condition=cache_on)
345 @self.region.conditional_cache_on_arguments(condition=cache_on)
360 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
346 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
361 repo = self._factory.repo(wire)
347 repo = self._factory.repo(wire)
362 ctx = self._get_ctx(repo, commit_id)
348 ctx = self._get_ctx(repo, commit_id)
363 return ctx.hidden()
349 return ctx.hidden()
364 return _ctx_hidden(context_uid, repo_id, commit_id)
350 return _ctx_hidden(context_uid, repo_id, commit_id)
365
351
366 @reraise_safe_exceptions
352 @reraise_safe_exceptions
367 def ctx_substate(self, wire, revision):
353 def ctx_substate(self, wire, revision):
368 repo = self._factory.repo(wire)
354 repo = self._factory.repo(wire)
369 ctx = self._get_ctx(repo, revision)
355 ctx = self._get_ctx(repo, revision)
370 return ctx.substate
356 return ctx.substate
371
357
372 @reraise_safe_exceptions
358 @reraise_safe_exceptions
373 def ctx_status(self, wire, revision):
359 def ctx_status(self, wire, revision):
374 repo = self._factory.repo(wire)
360 repo = self._factory.repo(wire)
375 ctx = self._get_ctx(repo, revision)
361 ctx = self._get_ctx(repo, revision)
376 status = repo[ctx.p1().node()].status(other=ctx.node())
362 status = repo[ctx.p1().node()].status(other=ctx.node())
377 # object of status (odd, custom named tuple in mercurial) is not
363 # object of status (odd, custom named tuple in mercurial) is not
378 # correctly serializable, we make it a list, as the underling
364 # correctly serializable, we make it a list, as the underling
379 # API expects this to be a list
365 # API expects this to be a list
380 return list(status)
366 return list(status)
381
367
382 @reraise_safe_exceptions
368 @reraise_safe_exceptions
383 def ctx_user(self, wire, revision):
369 def ctx_user(self, wire, revision):
384 repo = self._factory.repo(wire)
370 repo = self._factory.repo(wire)
385 ctx = self._get_ctx(repo, revision)
371 ctx = self._get_ctx(repo, revision)
386 return ctx.user()
372 return ctx.user()
387
373
388 @reraise_safe_exceptions
374 @reraise_safe_exceptions
389 def check_url(self, url, config):
375 def check_url(self, url, config):
390 _proto = None
376 _proto = None
391 if '+' in url[:url.find('://')]:
377 if '+' in url[:url.find('://')]:
392 _proto = url[0:url.find('+')]
378 _proto = url[0:url.find('+')]
393 url = url[url.find('+') + 1:]
379 url = url[url.find('+') + 1:]
394 handlers = []
380 handlers = []
395 url_obj = url_parser(url)
381 url_obj = url_parser(url)
396 test_uri, authinfo = url_obj.authinfo()
382 test_uri, authinfo = url_obj.authinfo()
397 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
383 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
398 url_obj.query = obfuscate_qs(url_obj.query)
384 url_obj.query = obfuscate_qs(url_obj.query)
399
385
400 cleaned_uri = str(url_obj)
386 cleaned_uri = str(url_obj)
401 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
387 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
402
388
403 if authinfo:
389 if authinfo:
404 # create a password manager
390 # create a password manager
405 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
391 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
406 passmgr.add_password(*authinfo)
392 passmgr.add_password(*authinfo)
407
393
408 handlers.extend((httpbasicauthhandler(passmgr),
394 handlers.extend((httpbasicauthhandler(passmgr),
409 httpdigestauthhandler(passmgr)))
395 httpdigestauthhandler(passmgr)))
410
396
411 o = urllib2.build_opener(*handlers)
397 o = urllib2.build_opener(*handlers)
412 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
398 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
413 ('Accept', 'application/mercurial-0.1')]
399 ('Accept', 'application/mercurial-0.1')]
414
400
415 q = {"cmd": 'between'}
401 q = {"cmd": 'between'}
416 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
402 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
417 qs = '?%s' % urllib.urlencode(q)
403 qs = '?%s' % urllib.urlencode(q)
418 cu = "%s%s" % (test_uri, qs)
404 cu = "%s%s" % (test_uri, qs)
419 req = urllib2.Request(cu, None, {})
405 req = urllib2.Request(cu, None, {})
420
406
421 try:
407 try:
422 log.debug("Trying to open URL %s", cleaned_uri)
408 log.debug("Trying to open URL %s", cleaned_uri)
423 resp = o.open(req)
409 resp = o.open(req)
424 if resp.code != 200:
410 if resp.code != 200:
425 raise exceptions.URLError()('Return Code is not 200')
411 raise exceptions.URLError()('Return Code is not 200')
426 except Exception as e:
412 except Exception as e:
427 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
413 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
428 # means it cannot be cloned
414 # means it cannot be cloned
429 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
415 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
430
416
431 # now check if it's a proper hg repo, but don't do it for svn
417 # now check if it's a proper hg repo, but don't do it for svn
432 try:
418 try:
433 if _proto == 'svn':
419 if _proto == 'svn':
434 pass
420 pass
435 else:
421 else:
436 # check for pure hg repos
422 # check for pure hg repos
437 log.debug(
423 log.debug(
438 "Verifying if URL is a Mercurial repository: %s",
424 "Verifying if URL is a Mercurial repository: %s",
439 cleaned_uri)
425 cleaned_uri)
440 ui = make_ui_from_config(config)
426 ui = make_ui_from_config(config)
441 peer_checker = makepeer(ui, url)
427 peer_checker = makepeer(ui, url)
442 peer_checker.lookup('tip')
428 peer_checker.lookup('tip')
443 except Exception as e:
429 except Exception as e:
444 log.warning("URL is not a valid Mercurial repository: %s",
430 log.warning("URL is not a valid Mercurial repository: %s",
445 cleaned_uri)
431 cleaned_uri)
446 raise exceptions.URLError(e)(
432 raise exceptions.URLError(e)(
447 "url [%s] does not look like an hg repo org_exc: %s"
433 "url [%s] does not look like an hg repo org_exc: %s"
448 % (cleaned_uri, e))
434 % (cleaned_uri, e))
449
435
450 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
436 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
451 return True
437 return True
452
438
453 @reraise_safe_exceptions
439 @reraise_safe_exceptions
454 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
440 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
455 repo = self._factory.repo(wire)
441 repo = self._factory.repo(wire)
456
442
457 if file_filter:
443 if file_filter:
458 match_filter = match(file_filter[0], '', [file_filter[1]])
444 match_filter = match(file_filter[0], '', [file_filter[1]])
459 else:
445 else:
460 match_filter = file_filter
446 match_filter = file_filter
461 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
447 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
462
448
463 try:
449 try:
464 return "".join(patch.diff(
450 return "".join(patch.diff(
465 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts))
451 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts))
466 except RepoLookupError as e:
452 except RepoLookupError as e:
467 raise exceptions.LookupException(e)()
453 raise exceptions.LookupException(e)()
468
454
469 @reraise_safe_exceptions
455 @reraise_safe_exceptions
470 def node_history(self, wire, revision, path, limit):
456 def node_history(self, wire, revision, path, limit):
471 cache_on, context_uid, repo_id = self._cache_on(wire)
457 cache_on, context_uid, repo_id = self._cache_on(wire)
472 @self.region.conditional_cache_on_arguments(condition=cache_on)
458 @self.region.conditional_cache_on_arguments(condition=cache_on)
473 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
459 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
474 repo = self._factory.repo(wire)
460 repo = self._factory.repo(wire)
475
461
476 ctx = self._get_ctx(repo, revision)
462 ctx = self._get_ctx(repo, revision)
477 fctx = ctx.filectx(path)
463 fctx = ctx.filectx(path)
478
464
479 def history_iter():
465 def history_iter():
480 limit_rev = fctx.rev()
466 limit_rev = fctx.rev()
481 for obj in reversed(list(fctx.filelog())):
467 for obj in reversed(list(fctx.filelog())):
482 obj = fctx.filectx(obj)
468 obj = fctx.filectx(obj)
483 ctx = obj.changectx()
469 ctx = obj.changectx()
484 if ctx.hidden() or ctx.obsolete():
470 if ctx.hidden() or ctx.obsolete():
485 continue
471 continue
486
472
487 if limit_rev >= obj.rev():
473 if limit_rev >= obj.rev():
488 yield obj
474 yield obj
489
475
490 history = []
476 history = []
491 for cnt, obj in enumerate(history_iter()):
477 for cnt, obj in enumerate(history_iter()):
492 if limit and cnt >= limit:
478 if limit and cnt >= limit:
493 break
479 break
494 history.append(hex(obj.node()))
480 history.append(hex(obj.node()))
495
481
496 return [x for x in history]
482 return [x for x in history]
497 return _node_history(context_uid, repo_id, revision, path, limit)
483 return _node_history(context_uid, repo_id, revision, path, limit)
498
484
499 @reraise_safe_exceptions
485 @reraise_safe_exceptions
500 def node_history_untill(self, wire, revision, path, limit):
486 def node_history_untill(self, wire, revision, path, limit):
501 cache_on, context_uid, repo_id = self._cache_on(wire)
487 cache_on, context_uid, repo_id = self._cache_on(wire)
502 @self.region.conditional_cache_on_arguments(condition=cache_on)
488 @self.region.conditional_cache_on_arguments(condition=cache_on)
503 def _node_history_until(_context_uid, _repo_id):
489 def _node_history_until(_context_uid, _repo_id):
504 repo = self._factory.repo(wire)
490 repo = self._factory.repo(wire)
505 ctx = self._get_ctx(repo, revision)
491 ctx = self._get_ctx(repo, revision)
506 fctx = ctx.filectx(path)
492 fctx = ctx.filectx(path)
507
493
508 file_log = list(fctx.filelog())
494 file_log = list(fctx.filelog())
509 if limit:
495 if limit:
510 # Limit to the last n items
496 # Limit to the last n items
511 file_log = file_log[-limit:]
497 file_log = file_log[-limit:]
512
498
513 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
499 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
514 return _node_history_until(context_uid, repo_id, revision, path, limit)
500 return _node_history_until(context_uid, repo_id, revision, path, limit)
515
501
516 @reraise_safe_exceptions
502 @reraise_safe_exceptions
517 def fctx_annotate(self, wire, revision, path):
503 def fctx_annotate(self, wire, revision, path):
518 repo = self._factory.repo(wire)
504 repo = self._factory.repo(wire)
519 ctx = self._get_ctx(repo, revision)
505 ctx = self._get_ctx(repo, revision)
520 fctx = ctx.filectx(path)
506 fctx = ctx.filectx(path)
521
507
522 result = []
508 result = []
523 for i, annotate_obj in enumerate(fctx.annotate(), 1):
509 for i, annotate_obj in enumerate(fctx.annotate(), 1):
524 ln_no = i
510 ln_no = i
525 sha = hex(annotate_obj.fctx.node())
511 sha = hex(annotate_obj.fctx.node())
526 content = annotate_obj.text
512 content = annotate_obj.text
527 result.append((ln_no, sha, content))
513 result.append((ln_no, sha, content))
528 return result
514 return result
529
515
530 @reraise_safe_exceptions
516 @reraise_safe_exceptions
531 def fctx_node_data(self, wire, revision, path):
517 def fctx_node_data(self, wire, revision, path):
532 repo = self._factory.repo(wire)
518 repo = self._factory.repo(wire)
533 ctx = self._get_ctx(repo, revision)
519 ctx = self._get_ctx(repo, revision)
534 fctx = ctx.filectx(path)
520 fctx = ctx.filectx(path)
535 return fctx.data()
521 return fctx.data()
536
522
537 @reraise_safe_exceptions
523 @reraise_safe_exceptions
538 def fctx_flags(self, wire, commit_id, path):
524 def fctx_flags(self, wire, commit_id, path):
539 cache_on, context_uid, repo_id = self._cache_on(wire)
525 cache_on, context_uid, repo_id = self._cache_on(wire)
540 @self.region.conditional_cache_on_arguments(condition=cache_on)
526 @self.region.conditional_cache_on_arguments(condition=cache_on)
541 def _fctx_flags(_repo_id, _commit_id, _path):
527 def _fctx_flags(_repo_id, _commit_id, _path):
542 repo = self._factory.repo(wire)
528 repo = self._factory.repo(wire)
543 ctx = self._get_ctx(repo, commit_id)
529 ctx = self._get_ctx(repo, commit_id)
544 fctx = ctx.filectx(path)
530 fctx = ctx.filectx(path)
545 return fctx.flags()
531 return fctx.flags()
546
532
547 return _fctx_flags(repo_id, commit_id, path)
533 return _fctx_flags(repo_id, commit_id, path)
548
534
549 @reraise_safe_exceptions
535 @reraise_safe_exceptions
550 def fctx_size(self, wire, commit_id, path):
536 def fctx_size(self, wire, commit_id, path):
551 cache_on, context_uid, repo_id = self._cache_on(wire)
537 cache_on, context_uid, repo_id = self._cache_on(wire)
552 @self.region.conditional_cache_on_arguments(condition=cache_on)
538 @self.region.conditional_cache_on_arguments(condition=cache_on)
553 def _fctx_size(_repo_id, _revision, _path):
539 def _fctx_size(_repo_id, _revision, _path):
554 repo = self._factory.repo(wire)
540 repo = self._factory.repo(wire)
555 ctx = self._get_ctx(repo, commit_id)
541 ctx = self._get_ctx(repo, commit_id)
556 fctx = ctx.filectx(path)
542 fctx = ctx.filectx(path)
557 return fctx.size()
543 return fctx.size()
558 return _fctx_size(repo_id, commit_id, path)
544 return _fctx_size(repo_id, commit_id, path)
559
545
560 @reraise_safe_exceptions
546 @reraise_safe_exceptions
561 def get_all_commit_ids(self, wire, name):
547 def get_all_commit_ids(self, wire, name):
562 cache_on, context_uid, repo_id = self._cache_on(wire)
548 cache_on, context_uid, repo_id = self._cache_on(wire)
563 @self.region.conditional_cache_on_arguments(condition=cache_on)
549 @self.region.conditional_cache_on_arguments(condition=cache_on)
564 def _get_all_commit_ids(_context_uid, _repo_id, _name):
550 def _get_all_commit_ids(_context_uid, _repo_id, _name):
565 repo = self._factory.repo(wire)
551 repo = self._factory.repo(wire)
566 repo = repo.filtered(name)
552 repo = repo.filtered(name)
567 revs = map(lambda x: hex(x[7]), repo.changelog.index)
553 revs = map(lambda x: hex(x[7]), repo.changelog.index)
568 return revs
554 return revs
569 return _get_all_commit_ids(context_uid, repo_id, name)
555 return _get_all_commit_ids(context_uid, repo_id, name)
570
556
571 @reraise_safe_exceptions
557 @reraise_safe_exceptions
572 def get_config_value(self, wire, section, name, untrusted=False):
558 def get_config_value(self, wire, section, name, untrusted=False):
573 repo = self._factory.repo(wire)
559 repo = self._factory.repo(wire)
574 return repo.ui.config(section, name, untrusted=untrusted)
560 return repo.ui.config(section, name, untrusted=untrusted)
575
561
576 @reraise_safe_exceptions
562 @reraise_safe_exceptions
577 def is_large_file(self, wire, commit_id, path):
563 def is_large_file(self, wire, commit_id, path):
578 cache_on, context_uid, repo_id = self._cache_on(wire)
564 cache_on, context_uid, repo_id = self._cache_on(wire)
579 @self.region.conditional_cache_on_arguments(condition=cache_on)
565 @self.region.conditional_cache_on_arguments(condition=cache_on)
580 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
566 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
581 return largefiles.lfutil.isstandin(path)
567 return largefiles.lfutil.isstandin(path)
582
568
583 return _is_large_file(context_uid, repo_id, commit_id, path)
569 return _is_large_file(context_uid, repo_id, commit_id, path)
584
570
585 @reraise_safe_exceptions
571 @reraise_safe_exceptions
586 def is_binary(self, wire, revision, path):
572 def is_binary(self, wire, revision, path):
587 cache_on, context_uid, repo_id = self._cache_on(wire)
573 cache_on, context_uid, repo_id = self._cache_on(wire)
588
574
589 @self.region.conditional_cache_on_arguments(condition=cache_on)
575 @self.region.conditional_cache_on_arguments(condition=cache_on)
590 def _is_binary(_repo_id, _sha, _path):
576 def _is_binary(_repo_id, _sha, _path):
591 repo = self._factory.repo(wire)
577 repo = self._factory.repo(wire)
592 ctx = self._get_ctx(repo, revision)
578 ctx = self._get_ctx(repo, revision)
593 fctx = ctx.filectx(path)
579 fctx = ctx.filectx(path)
594 return fctx.isbinary()
580 return fctx.isbinary()
595
581
596 return _is_binary(repo_id, revision, path)
582 return _is_binary(repo_id, revision, path)
597
583
598 @reraise_safe_exceptions
584 @reraise_safe_exceptions
599 def in_largefiles_store(self, wire, sha):
585 def in_largefiles_store(self, wire, sha):
600 repo = self._factory.repo(wire)
586 repo = self._factory.repo(wire)
601 return largefiles.lfutil.instore(repo, sha)
587 return largefiles.lfutil.instore(repo, sha)
602
588
603 @reraise_safe_exceptions
589 @reraise_safe_exceptions
604 def in_user_cache(self, wire, sha):
590 def in_user_cache(self, wire, sha):
605 repo = self._factory.repo(wire)
591 repo = self._factory.repo(wire)
606 return largefiles.lfutil.inusercache(repo.ui, sha)
592 return largefiles.lfutil.inusercache(repo.ui, sha)
607
593
608 @reraise_safe_exceptions
594 @reraise_safe_exceptions
609 def store_path(self, wire, sha):
595 def store_path(self, wire, sha):
610 repo = self._factory.repo(wire)
596 repo = self._factory.repo(wire)
611 return largefiles.lfutil.storepath(repo, sha)
597 return largefiles.lfutil.storepath(repo, sha)
612
598
613 @reraise_safe_exceptions
599 @reraise_safe_exceptions
614 def link(self, wire, sha, path):
600 def link(self, wire, sha, path):
615 repo = self._factory.repo(wire)
601 repo = self._factory.repo(wire)
616 largefiles.lfutil.link(
602 largefiles.lfutil.link(
617 largefiles.lfutil.usercachepath(repo.ui, sha), path)
603 largefiles.lfutil.usercachepath(repo.ui, sha), path)
618
604
619 @reraise_safe_exceptions
605 @reraise_safe_exceptions
620 def localrepository(self, wire, create=False):
606 def localrepository(self, wire, create=False):
621 self._factory.repo(wire, create=create)
607 self._factory.repo(wire, create=create)
622
608
623 @reraise_safe_exceptions
609 @reraise_safe_exceptions
624 def lookup(self, wire, revision, both):
610 def lookup(self, wire, revision, both):
625 cache_on, context_uid, repo_id = self._cache_on(wire)
611 cache_on, context_uid, repo_id = self._cache_on(wire)
626 @self.region.conditional_cache_on_arguments(condition=cache_on)
612 @self.region.conditional_cache_on_arguments(condition=cache_on)
627 def _lookup(_context_uid, _repo_id, _revision, _both):
613 def _lookup(_context_uid, _repo_id, _revision, _both):
628
614
629 repo = self._factory.repo(wire)
615 repo = self._factory.repo(wire)
630 rev = _revision
616 rev = _revision
631 if isinstance(rev, int):
617 if isinstance(rev, int):
632 # NOTE(marcink):
618 # NOTE(marcink):
633 # since Mercurial doesn't support negative indexes properly
619 # since Mercurial doesn't support negative indexes properly
634 # we need to shift accordingly by one to get proper index, e.g
620 # we need to shift accordingly by one to get proper index, e.g
635 # repo[-1] => repo[-2]
621 # repo[-1] => repo[-2]
636 # repo[0] => repo[-1]
622 # repo[0] => repo[-1]
637 if rev <= 0:
623 if rev <= 0:
638 rev = rev + -1
624 rev = rev + -1
639 try:
625 try:
640 ctx = self._get_ctx(repo, rev)
626 ctx = self._get_ctx(repo, rev)
641 except (TypeError, RepoLookupError) as e:
627 except (TypeError, RepoLookupError) as e:
642 e._org_exc_tb = traceback.format_exc()
628 e._org_exc_tb = traceback.format_exc()
643 raise exceptions.LookupException(e)(rev)
629 raise exceptions.LookupException(e)(rev)
644 except LookupError as e:
630 except LookupError as e:
645 e._org_exc_tb = traceback.format_exc()
631 e._org_exc_tb = traceback.format_exc()
646 raise exceptions.LookupException(e)(e.name)
632 raise exceptions.LookupException(e)(e.name)
647
633
648 if not both:
634 if not both:
649 return ctx.hex()
635 return ctx.hex()
650
636
651 ctx = repo[ctx.hex()]
637 ctx = repo[ctx.hex()]
652 return ctx.hex(), ctx.rev()
638 return ctx.hex(), ctx.rev()
653
639
654 return _lookup(context_uid, repo_id, revision, both)
640 return _lookup(context_uid, repo_id, revision, both)
655
641
656 @reraise_safe_exceptions
642 @reraise_safe_exceptions
657 def sync_push(self, wire, url):
643 def sync_push(self, wire, url):
658 if not self.check_url(url, wire['config']):
644 if not self.check_url(url, wire['config']):
659 return
645 return
660
646
661 repo = self._factory.repo(wire)
647 repo = self._factory.repo(wire)
662
648
663 # Disable any prompts for this repo
649 # Disable any prompts for this repo
664 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
650 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
665
651
666 bookmarks = dict(repo._bookmarks).keys()
652 bookmarks = dict(repo._bookmarks).keys()
667 remote = peer(repo, {}, url)
653 remote = peer(repo, {}, url)
668 # Disable any prompts for this remote
654 # Disable any prompts for this remote
669 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
655 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
670
656
671 return exchange.push(
657 return exchange.push(
672 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
658 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
673
659
674 @reraise_safe_exceptions
660 @reraise_safe_exceptions
675 def revision(self, wire, rev):
661 def revision(self, wire, rev):
676 repo = self._factory.repo(wire)
662 repo = self._factory.repo(wire)
677 ctx = self._get_ctx(repo, rev)
663 ctx = self._get_ctx(repo, rev)
678 return ctx.rev()
664 return ctx.rev()
679
665
680 @reraise_safe_exceptions
666 @reraise_safe_exceptions
681 def rev_range(self, wire, commit_filter):
667 def rev_range(self, wire, commit_filter):
682 cache_on, context_uid, repo_id = self._cache_on(wire)
668 cache_on, context_uid, repo_id = self._cache_on(wire)
683
669
684 @self.region.conditional_cache_on_arguments(condition=cache_on)
670 @self.region.conditional_cache_on_arguments(condition=cache_on)
685 def _rev_range(_context_uid, _repo_id, _filter):
671 def _rev_range(_context_uid, _repo_id, _filter):
686 repo = self._factory.repo(wire)
672 repo = self._factory.repo(wire)
687 revisions = [rev for rev in revrange(repo, commit_filter)]
673 revisions = [rev for rev in revrange(repo, commit_filter)]
688 return revisions
674 return revisions
689
675
690 return _rev_range(context_uid, repo_id, sorted(commit_filter))
676 return _rev_range(context_uid, repo_id, sorted(commit_filter))
691
677
692 @reraise_safe_exceptions
678 @reraise_safe_exceptions
693 def rev_range_hash(self, wire, node):
679 def rev_range_hash(self, wire, node):
694 repo = self._factory.repo(wire)
680 repo = self._factory.repo(wire)
695
681
696 def get_revs(repo, rev_opt):
682 def get_revs(repo, rev_opt):
697 if rev_opt:
683 if rev_opt:
698 revs = revrange(repo, rev_opt)
684 revs = revrange(repo, rev_opt)
699 if len(revs) == 0:
685 if len(revs) == 0:
700 return (nullrev, nullrev)
686 return (nullrev, nullrev)
701 return max(revs), min(revs)
687 return max(revs), min(revs)
702 else:
688 else:
703 return len(repo) - 1, 0
689 return len(repo) - 1, 0
704
690
705 stop, start = get_revs(repo, [node + ':'])
691 stop, start = get_revs(repo, [node + ':'])
706 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
692 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
707 return revs
693 return revs
708
694
709 @reraise_safe_exceptions
695 @reraise_safe_exceptions
710 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
696 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
711 other_path = kwargs.pop('other_path', None)
697 other_path = kwargs.pop('other_path', None)
712
698
713 # case when we want to compare two independent repositories
699 # case when we want to compare two independent repositories
714 if other_path and other_path != wire["path"]:
700 if other_path and other_path != wire["path"]:
715 baseui = self._factory._create_config(wire["config"])
701 baseui = self._factory._create_config(wire["config"])
716 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
702 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
717 else:
703 else:
718 repo = self._factory.repo(wire)
704 repo = self._factory.repo(wire)
719 return list(repo.revs(rev_spec, *args))
705 return list(repo.revs(rev_spec, *args))
720
706
721 @reraise_safe_exceptions
707 @reraise_safe_exceptions
722 def verify(self, wire,):
708 def verify(self, wire,):
723 repo = self._factory.repo(wire)
709 repo = self._factory.repo(wire)
724 baseui = self._factory._create_config(wire['config'])
710 baseui = self._factory._create_config(wire['config'])
725
711
726 baseui, output = patch_ui_message_output(baseui)
712 baseui, output = patch_ui_message_output(baseui)
727
713
728 repo.ui = baseui
714 repo.ui = baseui
729 verify.verify(repo)
715 verify.verify(repo)
730 return output.getvalue()
716 return output.getvalue()
731
717
732 @reraise_safe_exceptions
718 @reraise_safe_exceptions
733 def hg_update_cache(self, wire,):
719 def hg_update_cache(self, wire,):
734 repo = self._factory.repo(wire)
720 repo = self._factory.repo(wire)
735 baseui = self._factory._create_config(wire['config'])
721 baseui = self._factory._create_config(wire['config'])
736 baseui, output = patch_ui_message_output(baseui)
722 baseui, output = patch_ui_message_output(baseui)
737
723
738 repo.ui = baseui
724 repo.ui = baseui
739 with repo.wlock(), repo.lock():
725 with repo.wlock(), repo.lock():
740 repo.updatecaches(full=True)
726 repo.updatecaches(full=True)
741
727
742 return output.getvalue()
728 return output.getvalue()
743
729
744 @reraise_safe_exceptions
730 @reraise_safe_exceptions
745 def hg_rebuild_fn_cache(self, wire,):
731 def hg_rebuild_fn_cache(self, wire,):
746 repo = self._factory.repo(wire)
732 repo = self._factory.repo(wire)
747 baseui = self._factory._create_config(wire['config'])
733 baseui = self._factory._create_config(wire['config'])
748 baseui, output = patch_ui_message_output(baseui)
734 baseui, output = patch_ui_message_output(baseui)
749
735
750 repo.ui = baseui
736 repo.ui = baseui
751
737
752 repair.rebuildfncache(baseui, repo)
738 repair.rebuildfncache(baseui, repo)
753
739
754 return output.getvalue()
740 return output.getvalue()
755
741
756 @reraise_safe_exceptions
742 @reraise_safe_exceptions
757 def tags(self, wire):
743 def tags(self, wire):
758 cache_on, context_uid, repo_id = self._cache_on(wire)
744 cache_on, context_uid, repo_id = self._cache_on(wire)
759 @self.region.conditional_cache_on_arguments(condition=cache_on)
745 @self.region.conditional_cache_on_arguments(condition=cache_on)
760 def _tags(_context_uid, _repo_id):
746 def _tags(_context_uid, _repo_id):
761 repo = self._factory.repo(wire)
747 repo = self._factory.repo(wire)
762 return repo.tags()
748 return repo.tags()
763
749
764 return _tags(context_uid, repo_id)
750 return _tags(context_uid, repo_id)
765
751
766 @reraise_safe_exceptions
752 @reraise_safe_exceptions
767 def update(self, wire, node=None, clean=False):
753 def update(self, wire, node=None, clean=False):
768 repo = self._factory.repo(wire)
754 repo = self._factory.repo(wire)
769 baseui = self._factory._create_config(wire['config'])
755 baseui = self._factory._create_config(wire['config'])
770 commands.update(baseui, repo, node=node, clean=clean)
756 commands.update(baseui, repo, node=node, clean=clean)
771
757
772 @reraise_safe_exceptions
758 @reraise_safe_exceptions
773 def identify(self, wire):
759 def identify(self, wire):
774 repo = self._factory.repo(wire)
760 repo = self._factory.repo(wire)
775 baseui = self._factory._create_config(wire['config'])
761 baseui = self._factory._create_config(wire['config'])
776 output = io.BytesIO()
762 output = io.BytesIO()
777 baseui.write = output.write
763 baseui.write = output.write
778 # This is required to get a full node id
764 # This is required to get a full node id
779 baseui.debugflag = True
765 baseui.debugflag = True
780 commands.identify(baseui, repo, id=True)
766 commands.identify(baseui, repo, id=True)
781
767
782 return output.getvalue()
768 return output.getvalue()
783
769
784 @reraise_safe_exceptions
770 @reraise_safe_exceptions
785 def heads(self, wire, branch=None):
771 def heads(self, wire, branch=None):
786 repo = self._factory.repo(wire)
772 repo = self._factory.repo(wire)
787 baseui = self._factory._create_config(wire['config'])
773 baseui = self._factory._create_config(wire['config'])
788 output = io.BytesIO()
774 output = io.BytesIO()
789
775
790 def write(data, **unused_kwargs):
776 def write(data, **unused_kwargs):
791 output.write(data)
777 output.write(data)
792
778
793 baseui.write = write
779 baseui.write = write
794 if branch:
780 if branch:
795 args = [branch]
781 args = [branch]
796 else:
782 else:
797 args = []
783 args = []
798 commands.heads(baseui, repo, template='{node} ', *args)
784 commands.heads(baseui, repo, template='{node} ', *args)
799
785
800 return output.getvalue()
786 return output.getvalue()
801
787
802 @reraise_safe_exceptions
788 @reraise_safe_exceptions
803 def ancestor(self, wire, revision1, revision2):
789 def ancestor(self, wire, revision1, revision2):
804 repo = self._factory.repo(wire)
790 repo = self._factory.repo(wire)
805 changelog = repo.changelog
791 changelog = repo.changelog
806 lookup = repo.lookup
792 lookup = repo.lookup
807 a = changelog.ancestor(lookup(revision1), lookup(revision2))
793 a = changelog.ancestor(lookup(revision1), lookup(revision2))
808 return hex(a)
794 return hex(a)
809
795
810 @reraise_safe_exceptions
796 @reraise_safe_exceptions
811 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
797 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
812 baseui = self._factory._create_config(wire["config"], hooks=hooks)
798 baseui = self._factory._create_config(wire["config"], hooks=hooks)
813 clone(baseui, source, dest, noupdate=not update_after_clone)
799 clone(baseui, source, dest, noupdate=not update_after_clone)
814
800
815 @reraise_safe_exceptions
801 @reraise_safe_exceptions
816 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
802 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
817
803
818 repo = self._factory.repo(wire)
804 repo = self._factory.repo(wire)
819 baseui = self._factory._create_config(wire['config'])
805 baseui = self._factory._create_config(wire['config'])
820 publishing = baseui.configbool('phases', 'publish')
806 publishing = baseui.configbool('phases', 'publish')
821 if publishing:
807 if publishing:
822 new_commit = 'public'
808 new_commit = 'public'
823 else:
809 else:
824 new_commit = 'draft'
810 new_commit = 'draft'
825
811
826 def _filectxfn(_repo, ctx, path):
812 def _filectxfn(_repo, ctx, path):
827 """
813 """
828 Marks given path as added/changed/removed in a given _repo. This is
814 Marks given path as added/changed/removed in a given _repo. This is
829 for internal mercurial commit function.
815 for internal mercurial commit function.
830 """
816 """
831
817
832 # check if this path is removed
818 # check if this path is removed
833 if path in removed:
819 if path in removed:
834 # returning None is a way to mark node for removal
820 # returning None is a way to mark node for removal
835 return None
821 return None
836
822
837 # check if this path is added
823 # check if this path is added
838 for node in updated:
824 for node in updated:
839 if node['path'] == path:
825 if node['path'] == path:
840 return memfilectx(
826 return memfilectx(
841 _repo,
827 _repo,
842 changectx=ctx,
828 changectx=ctx,
843 path=node['path'],
829 path=node['path'],
844 data=node['content'],
830 data=node['content'],
845 islink=False,
831 islink=False,
846 isexec=bool(node['mode'] & stat.S_IXUSR),
832 isexec=bool(node['mode'] & stat.S_IXUSR),
847 copysource=False)
833 copysource=False)
848
834
849 raise exceptions.AbortException()(
835 raise exceptions.AbortException()(
850 "Given path haven't been marked as added, "
836 "Given path haven't been marked as added, "
851 "changed or removed (%s)" % path)
837 "changed or removed (%s)" % path)
852
838
853 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
839 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
854
840
855 commit_ctx = memctx(
841 commit_ctx = memctx(
856 repo=repo,
842 repo=repo,
857 parents=parents,
843 parents=parents,
858 text=message,
844 text=message,
859 files=files,
845 files=files,
860 filectxfn=_filectxfn,
846 filectxfn=_filectxfn,
861 user=user,
847 user=user,
862 date=(commit_time, commit_timezone),
848 date=(commit_time, commit_timezone),
863 extra=extra)
849 extra=extra)
864
850
865 n = repo.commitctx(commit_ctx)
851 n = repo.commitctx(commit_ctx)
866 new_id = hex(n)
852 new_id = hex(n)
867
853
868 return new_id
854 return new_id
869
855
870 @reraise_safe_exceptions
856 @reraise_safe_exceptions
871 def pull(self, wire, url, commit_ids=None):
857 def pull(self, wire, url, commit_ids=None):
872 repo = self._factory.repo(wire)
858 repo = self._factory.repo(wire)
873 # Disable any prompts for this repo
859 # Disable any prompts for this repo
874 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
860 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
875
861
876 remote = peer(repo, {}, url)
862 remote = peer(repo, {}, url)
877 # Disable any prompts for this remote
863 # Disable any prompts for this remote
878 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
864 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
879
865
880 if commit_ids:
866 if commit_ids:
881 commit_ids = [bin(commit_id) for commit_id in commit_ids]
867 commit_ids = [bin(commit_id) for commit_id in commit_ids]
882
868
883 return exchange.pull(
869 return exchange.pull(
884 repo, remote, heads=commit_ids, force=None).cgresult
870 repo, remote, heads=commit_ids, force=None).cgresult
885
871
886 @reraise_safe_exceptions
872 @reraise_safe_exceptions
887 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
873 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
888 repo = self._factory.repo(wire)
874 repo = self._factory.repo(wire)
889 baseui = self._factory._create_config(wire['config'], hooks=hooks)
875 baseui = self._factory._create_config(wire['config'], hooks=hooks)
890
876
891 # Mercurial internally has a lot of logic that checks ONLY if
877 # Mercurial internally has a lot of logic that checks ONLY if
892 # option is defined, we just pass those if they are defined then
878 # option is defined, we just pass those if they are defined then
893 opts = {}
879 opts = {}
894 if bookmark:
880 if bookmark:
895 opts['bookmark'] = bookmark
881 opts['bookmark'] = bookmark
896 if branch:
882 if branch:
897 opts['branch'] = branch
883 opts['branch'] = branch
898 if revision:
884 if revision:
899 opts['rev'] = revision
885 opts['rev'] = revision
900
886
901 commands.pull(baseui, repo, source, **opts)
887 commands.pull(baseui, repo, source, **opts)
902
888
903 @reraise_safe_exceptions
889 @reraise_safe_exceptions
904 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
890 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
905 repo = self._factory.repo(wire)
891 repo = self._factory.repo(wire)
906 baseui = self._factory._create_config(wire['config'], hooks=hooks)
892 baseui = self._factory._create_config(wire['config'], hooks=hooks)
907 commands.push(baseui, repo, dest=dest_path, rev=revisions,
893 commands.push(baseui, repo, dest=dest_path, rev=revisions,
908 new_branch=push_branches)
894 new_branch=push_branches)
909
895
910 @reraise_safe_exceptions
896 @reraise_safe_exceptions
911 def strip(self, wire, revision, update, backup):
897 def strip(self, wire, revision, update, backup):
912 repo = self._factory.repo(wire)
898 repo = self._factory.repo(wire)
913 ctx = self._get_ctx(repo, revision)
899 ctx = self._get_ctx(repo, revision)
914 hgext_strip(
900 hgext_strip(
915 repo.baseui, repo, ctx.node(), update=update, backup=backup)
901 repo.baseui, repo, ctx.node(), update=update, backup=backup)
916
902
917 @reraise_safe_exceptions
903 @reraise_safe_exceptions
918 def get_unresolved_files(self, wire):
904 def get_unresolved_files(self, wire):
919 repo = self._factory.repo(wire)
905 repo = self._factory.repo(wire)
920
906
921 log.debug('Calculating unresolved files for repo: %s', repo)
907 log.debug('Calculating unresolved files for repo: %s', repo)
922 output = io.BytesIO()
908 output = io.BytesIO()
923
909
924 def write(data, **unused_kwargs):
910 def write(data, **unused_kwargs):
925 output.write(data)
911 output.write(data)
926
912
927 baseui = self._factory._create_config(wire['config'])
913 baseui = self._factory._create_config(wire['config'])
928 baseui.write = write
914 baseui.write = write
929
915
930 commands.resolve(baseui, repo, list=True)
916 commands.resolve(baseui, repo, list=True)
931 unresolved = output.getvalue().splitlines(0)
917 unresolved = output.getvalue().splitlines(0)
932 return unresolved
918 return unresolved
933
919
934 @reraise_safe_exceptions
920 @reraise_safe_exceptions
935 def merge(self, wire, revision):
921 def merge(self, wire, revision):
936 repo = self._factory.repo(wire)
922 repo = self._factory.repo(wire)
937 baseui = self._factory._create_config(wire['config'])
923 baseui = self._factory._create_config(wire['config'])
938 repo.ui.setconfig('ui', 'merge', 'internal:dump')
924 repo.ui.setconfig('ui', 'merge', 'internal:dump')
939
925
940 # In case of sub repositories are used mercurial prompts the user in
926 # In case of sub repositories are used mercurial prompts the user in
941 # case of merge conflicts or different sub repository sources. By
927 # case of merge conflicts or different sub repository sources. By
942 # setting the interactive flag to `False` mercurial doesn't prompt the
928 # setting the interactive flag to `False` mercurial doesn't prompt the
943 # used but instead uses a default value.
929 # used but instead uses a default value.
944 repo.ui.setconfig('ui', 'interactive', False)
930 repo.ui.setconfig('ui', 'interactive', False)
945 commands.merge(baseui, repo, rev=revision)
931 commands.merge(baseui, repo, rev=revision)
946
932
947 @reraise_safe_exceptions
933 @reraise_safe_exceptions
948 def merge_state(self, wire):
934 def merge_state(self, wire):
949 repo = self._factory.repo(wire)
935 repo = self._factory.repo(wire)
950 repo.ui.setconfig('ui', 'merge', 'internal:dump')
936 repo.ui.setconfig('ui', 'merge', 'internal:dump')
951
937
952 # In case of sub repositories are used mercurial prompts the user in
938 # In case of sub repositories are used mercurial prompts the user in
953 # case of merge conflicts or different sub repository sources. By
939 # case of merge conflicts or different sub repository sources. By
954 # setting the interactive flag to `False` mercurial doesn't prompt the
940 # setting the interactive flag to `False` mercurial doesn't prompt the
955 # used but instead uses a default value.
941 # used but instead uses a default value.
956 repo.ui.setconfig('ui', 'interactive', False)
942 repo.ui.setconfig('ui', 'interactive', False)
957 ms = hg_merge.mergestate(repo)
943 ms = hg_merge.mergestate(repo)
958 return [x for x in ms.unresolved()]
944 return [x for x in ms.unresolved()]
959
945
960 @reraise_safe_exceptions
946 @reraise_safe_exceptions
961 def commit(self, wire, message, username, close_branch=False):
947 def commit(self, wire, message, username, close_branch=False):
962 repo = self._factory.repo(wire)
948 repo = self._factory.repo(wire)
963 baseui = self._factory._create_config(wire['config'])
949 baseui = self._factory._create_config(wire['config'])
964 repo.ui.setconfig('ui', 'username', username)
950 repo.ui.setconfig('ui', 'username', username)
965 commands.commit(baseui, repo, message=message, close_branch=close_branch)
951 commands.commit(baseui, repo, message=message, close_branch=close_branch)
966
952
967 @reraise_safe_exceptions
953 @reraise_safe_exceptions
968 def rebase(self, wire, source=None, dest=None, abort=False):
954 def rebase(self, wire, source=None, dest=None, abort=False):
969 repo = self._factory.repo(wire)
955 repo = self._factory.repo(wire)
970 baseui = self._factory._create_config(wire['config'])
956 baseui = self._factory._create_config(wire['config'])
971 repo.ui.setconfig('ui', 'merge', 'internal:dump')
957 repo.ui.setconfig('ui', 'merge', 'internal:dump')
972 # In case of sub repositories are used mercurial prompts the user in
958 # In case of sub repositories are used mercurial prompts the user in
973 # case of merge conflicts or different sub repository sources. By
959 # case of merge conflicts or different sub repository sources. By
974 # setting the interactive flag to `False` mercurial doesn't prompt the
960 # setting the interactive flag to `False` mercurial doesn't prompt the
975 # used but instead uses a default value.
961 # used but instead uses a default value.
976 repo.ui.setconfig('ui', 'interactive', False)
962 repo.ui.setconfig('ui', 'interactive', False)
977 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
963 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
978
964
979 @reraise_safe_exceptions
965 @reraise_safe_exceptions
980 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
966 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
981 repo = self._factory.repo(wire)
967 repo = self._factory.repo(wire)
982 ctx = self._get_ctx(repo, revision)
968 ctx = self._get_ctx(repo, revision)
983 node = ctx.node()
969 node = ctx.node()
984
970
985 date = (tag_time, tag_timezone)
971 date = (tag_time, tag_timezone)
986 try:
972 try:
987 hg_tag.tag(repo, name, node, message, local, user, date)
973 hg_tag.tag(repo, name, node, message, local, user, date)
988 except Abort as e:
974 except Abort as e:
989 log.exception("Tag operation aborted")
975 log.exception("Tag operation aborted")
990 # Exception can contain unicode which we convert
976 # Exception can contain unicode which we convert
991 raise exceptions.AbortException(e)(repr(e))
977 raise exceptions.AbortException(e)(repr(e))
992
978
993 @reraise_safe_exceptions
979 @reraise_safe_exceptions
994 def bookmark(self, wire, bookmark, revision=None):
980 def bookmark(self, wire, bookmark, revision=None):
995 repo = self._factory.repo(wire)
981 repo = self._factory.repo(wire)
996 baseui = self._factory._create_config(wire['config'])
982 baseui = self._factory._create_config(wire['config'])
997 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
983 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
998
984
999 @reraise_safe_exceptions
985 @reraise_safe_exceptions
1000 def install_hooks(self, wire, force=False):
986 def install_hooks(self, wire, force=False):
1001 # we don't need any special hooks for Mercurial
987 # we don't need any special hooks for Mercurial
1002 pass
988 pass
1003
989
1004 @reraise_safe_exceptions
990 @reraise_safe_exceptions
1005 def get_hooks_info(self, wire):
991 def get_hooks_info(self, wire):
1006 return {
992 return {
1007 'pre_version': vcsserver.__version__,
993 'pre_version': vcsserver.__version__,
1008 'post_version': vcsserver.__version__,
994 'post_version': vcsserver.__version__,
1009 }
995 }
996
997 @reraise_safe_exceptions
998 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
999 archive_dir_name, commit_id):
1000
1001 def file_walker(_commit_id, path):
1002 repo = self._factory.repo(wire)
1003 ctx = repo[_commit_id]
1004 is_root = path in ['', '/']
1005 if is_root:
1006 matcher = alwaysmatcher(badfn=None)
1007 else:
1008 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1009 file_iter = ctx.manifest().walk(matcher)
1010
1011 for fn in file_iter:
1012 file_path = fn
1013 flags = ctx.flags(fn)
1014 mode = b'x' in flags and 0o755 or 0o644
1015 is_link = b'l' in flags
1016
1017 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1018
1019 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1020 archive_dir_name, commit_id)
1021
@@ -1,79 +1,79 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Mercurial libs compatibility
19 Mercurial libs compatibility
20 """
20 """
21
21
22 import mercurial
22 import mercurial
23 from mercurial import demandimport
23 from mercurial import demandimport
24 # patch demandimport, due to bug in mercurial when it always triggers
24 # patch demandimport, due to bug in mercurial when it always triggers
25 # demandimport.enable()
25 # demandimport.enable()
26 demandimport.enable = lambda *args, **kwargs: 1
26 demandimport.enable = lambda *args, **kwargs: 1
27
27
28 from mercurial import ui
28 from mercurial import ui
29 from mercurial import patch
29 from mercurial import patch
30 from mercurial import config
30 from mercurial import config
31 from mercurial import extensions
31 from mercurial import extensions
32 from mercurial import scmutil
32 from mercurial import scmutil
33 from mercurial import archival
33 from mercurial import archival
34 from mercurial import discovery
34 from mercurial import discovery
35 from mercurial import unionrepo
35 from mercurial import unionrepo
36 from mercurial import localrepo
36 from mercurial import localrepo
37 from mercurial import merge as hg_merge
37 from mercurial import merge as hg_merge
38 from mercurial import subrepo
38 from mercurial import subrepo
39 from mercurial import subrepoutil
39 from mercurial import subrepoutil
40 from mercurial import tags as hg_tag
40 from mercurial import tags as hg_tag
41
41 from mercurial import util as hgutil
42 from mercurial.commands import clone, nullid, pull
42 from mercurial.commands import clone, nullid, pull
43 from mercurial.context import memctx, memfilectx
43 from mercurial.context import memctx, memfilectx
44 from mercurial.error import (
44 from mercurial.error import (
45 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
45 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
46 RequirementError, ProgrammingError)
46 RequirementError, ProgrammingError)
47 from mercurial.hgweb import hgweb_mod
47 from mercurial.hgweb import hgweb_mod
48 from mercurial.localrepo import instance
48 from mercurial.localrepo import instance
49 from mercurial.match import match
49 from mercurial.match import match, alwaysmatcher, patternmatcher
50 from mercurial.mdiff import diffopts
50 from mercurial.mdiff import diffopts
51 from mercurial.node import bin, hex
51 from mercurial.node import bin, hex
52 from mercurial.encoding import tolocal
52 from mercurial.encoding import tolocal
53 from mercurial.discovery import findcommonoutgoing
53 from mercurial.discovery import findcommonoutgoing
54 from mercurial.hg import peer
54 from mercurial.hg import peer
55 from mercurial.httppeer import makepeer
55 from mercurial.httppeer import makepeer
56 from mercurial.util import url as hg_url
56 from mercurial.util import url as hg_url
57 from mercurial.scmutil import revrange, revsymbol
57 from mercurial.scmutil import revrange, revsymbol
58 from mercurial.node import nullrev
58 from mercurial.node import nullrev
59 from mercurial import exchange
59 from mercurial import exchange
60 from hgext import largefiles
60 from hgext import largefiles
61
61
62 # those authnadlers are patched for python 2.6.5 bug an
62 # those authnadlers are patched for python 2.6.5 bug an
63 # infinit looping when given invalid resources
63 # infinit looping when given invalid resources
64 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
64 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
65
65
66
66
67 def get_ctx(repo, ref):
67 def get_ctx(repo, ref):
68 try:
68 try:
69 ctx = repo[ref]
69 ctx = repo[ref]
70 except ProgrammingError:
70 except ProgrammingError:
71 # we're unable to find the rev using a regular lookup, we fallback
71 # we're unable to find the rev using a regular lookup, we fallback
72 # to slower, but backward compat revsymbol usage
72 # to slower, but backward compat revsymbol usage
73 ctx = revsymbol(repo, ref)
73 ctx = revsymbol(repo, ref)
74 except (LookupError, RepoLookupError):
74 except (LookupError, RepoLookupError):
75 # Similar case as above but only for refs that are not numeric
75 # Similar case as above but only for refs that are not numeric
76 if isinstance(ref, (int, long)):
76 if isinstance(ref, (int, long)):
77 raise
77 raise
78 ctx = revsymbol(repo, ref)
78 ctx = revsymbol(repo, ref)
79 return ctx
79 return ctx
@@ -1,692 +1,692 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import base64
20 import base64
21 import locale
21 import locale
22 import logging
22 import logging
23 import uuid
23 import uuid
24 import wsgiref.util
24 import wsgiref.util
25 import traceback
25 import traceback
26 import tempfile
26 import tempfile
27 import resource
27 import resource
28 from itertools import chain
28 from itertools import chain
29 from cStringIO import StringIO
29 from cStringIO import StringIO
30
30
31 import simplejson as json
31 import simplejson as json
32 import msgpack
32 import msgpack
33 from pyramid.config import Configurator
33 from pyramid.config import Configurator
34 from pyramid.settings import asbool, aslist
34 from pyramid.settings import asbool, aslist
35 from pyramid.wsgi import wsgiapp
35 from pyramid.wsgi import wsgiapp
36 from pyramid.compat import configparser
36 from pyramid.compat import configparser
37 from pyramid.response import Response
37 from pyramid.response import Response
38
38
39 from vcsserver.utils import safe_int
39 from vcsserver.utils import safe_int
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
43 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
44 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
44 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
45
45
46 try:
46 try:
47 locale.setlocale(locale.LC_ALL, '')
47 locale.setlocale(locale.LC_ALL, '')
48 except locale.Error as e:
48 except locale.Error as e:
49 log.error(
49 log.error(
50 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
50 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
51 os.environ['LC_ALL'] = 'C'
51 os.environ['LC_ALL'] = 'C'
52
52
53 import vcsserver
53 import vcsserver
54 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
54 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
55 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
55 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
56 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
56 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
57 from vcsserver.echo_stub.echo_app import EchoApp
57 from vcsserver.echo_stub.echo_app import EchoApp
58 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
58 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
59 from vcsserver.lib.exc_tracking import store_exception
59 from vcsserver.lib.exc_tracking import store_exception
60 from vcsserver.server import VcsServer
60 from vcsserver.server import VcsServer
61
61
62 try:
62 try:
63 from vcsserver.git import GitFactory, GitRemote
63 from vcsserver.git import GitFactory, GitRemote
64 except ImportError:
64 except ImportError:
65 GitFactory = None
65 GitFactory = None
66 GitRemote = None
66 GitRemote = None
67
67
68 try:
68 try:
69 from vcsserver.hg import MercurialFactory, HgRemote
69 from vcsserver.hg import MercurialFactory, HgRemote
70 except ImportError:
70 except ImportError:
71 MercurialFactory = None
71 MercurialFactory = None
72 HgRemote = None
72 HgRemote = None
73
73
74 try:
74 try:
75 from vcsserver.svn import SubversionFactory, SvnRemote
75 from vcsserver.svn import SubversionFactory, SvnRemote
76 except ImportError:
76 except ImportError:
77 SubversionFactory = None
77 SubversionFactory = None
78 SvnRemote = None
78 SvnRemote = None
79
79
80
80
81 def _is_request_chunked(environ):
81 def _is_request_chunked(environ):
82 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
82 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
83 return stream
83 return stream
84
84
85
85
86 def _int_setting(settings, name, default):
86 def _int_setting(settings, name, default):
87 settings[name] = int(settings.get(name, default))
87 settings[name] = int(settings.get(name, default))
88 return settings[name]
88 return settings[name]
89
89
90
90
91 def _bool_setting(settings, name, default):
91 def _bool_setting(settings, name, default):
92 input_val = settings.get(name, default)
92 input_val = settings.get(name, default)
93 if isinstance(input_val, unicode):
93 if isinstance(input_val, unicode):
94 input_val = input_val.encode('utf8')
94 input_val = input_val.encode('utf8')
95 settings[name] = asbool(input_val)
95 settings[name] = asbool(input_val)
96 return settings[name]
96 return settings[name]
97
97
98
98
99 def _list_setting(settings, name, default):
99 def _list_setting(settings, name, default):
100 raw_value = settings.get(name, default)
100 raw_value = settings.get(name, default)
101
101
102 # Otherwise we assume it uses pyramids space/newline separation.
102 # Otherwise we assume it uses pyramids space/newline separation.
103 settings[name] = aslist(raw_value)
103 settings[name] = aslist(raw_value)
104 return settings[name]
104 return settings[name]
105
105
106
106
107 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
107 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
108 value = settings.get(name, default)
108 value = settings.get(name, default)
109
109
110 if default_when_empty and not value:
110 if default_when_empty and not value:
111 # use default value when value is empty
111 # use default value when value is empty
112 value = default
112 value = default
113
113
114 if lower:
114 if lower:
115 value = value.lower()
115 value = value.lower()
116 settings[name] = value
116 settings[name] = value
117 return settings[name]
117 return settings[name]
118
118
119
119
120 class VCS(object):
120 class VCS(object):
121 def __init__(self, locale_conf=None, cache_config=None):
121 def __init__(self, locale_conf=None, cache_config=None):
122 self.locale = locale_conf
122 self.locale = locale_conf
123 self.cache_config = cache_config
123 self.cache_config = cache_config
124 self._configure_locale()
124 self._configure_locale()
125
125
126 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
126 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
127 log.info('Max file descriptors value: %s', maxfd)
127 log.info('Max file descriptors value: %s', maxfd)
128
128
129 if GitFactory and GitRemote:
129 if GitFactory and GitRemote:
130 git_factory = GitFactory()
130 git_factory = GitFactory()
131 self._git_remote = GitRemote(git_factory)
131 self._git_remote = GitRemote(git_factory)
132 else:
132 else:
133 log.info("Git client import failed")
133 log.info("Git client import failed")
134
134
135 if MercurialFactory and HgRemote:
135 if MercurialFactory and HgRemote:
136 hg_factory = MercurialFactory()
136 hg_factory = MercurialFactory()
137 self._hg_remote = HgRemote(hg_factory)
137 self._hg_remote = HgRemote(hg_factory)
138 else:
138 else:
139 log.info("Mercurial client import failed")
139 log.info("Mercurial client import failed")
140
140
141 if SubversionFactory and SvnRemote:
141 if SubversionFactory and SvnRemote:
142 svn_factory = SubversionFactory()
142 svn_factory = SubversionFactory()
143
143
144 # hg factory is used for svn url validation
144 # hg factory is used for svn url validation
145 hg_factory = MercurialFactory()
145 hg_factory = MercurialFactory()
146 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
146 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
147 else:
147 else:
148 log.info("Subversion client import failed")
148 log.info("Subversion client import failed")
149
149
150 self._vcsserver = VcsServer()
150 self._vcsserver = VcsServer()
151
151
152 def _configure_locale(self):
152 def _configure_locale(self):
153 if self.locale:
153 if self.locale:
154 log.info('Settings locale: `LC_ALL` to %s', self.locale)
154 log.info('Settings locale: `LC_ALL` to %s', self.locale)
155 else:
155 else:
156 log.info(
156 log.info(
157 'Configuring locale subsystem based on environment variables')
157 'Configuring locale subsystem based on environment variables')
158 try:
158 try:
159 # If self.locale is the empty string, then the locale
159 # If self.locale is the empty string, then the locale
160 # module will use the environment variables. See the
160 # module will use the environment variables. See the
161 # documentation of the package `locale`.
161 # documentation of the package `locale`.
162 locale.setlocale(locale.LC_ALL, self.locale)
162 locale.setlocale(locale.LC_ALL, self.locale)
163
163
164 language_code, encoding = locale.getlocale()
164 language_code, encoding = locale.getlocale()
165 log.info(
165 log.info(
166 'Locale set to language code "%s" with encoding "%s".',
166 'Locale set to language code "%s" with encoding "%s".',
167 language_code, encoding)
167 language_code, encoding)
168 except locale.Error:
168 except locale.Error:
169 log.exception(
169 log.exception(
170 'Cannot set locale, not configuring the locale system')
170 'Cannot set locale, not configuring the locale system')
171
171
172
172
173 class WsgiProxy(object):
173 class WsgiProxy(object):
174 def __init__(self, wsgi):
174 def __init__(self, wsgi):
175 self.wsgi = wsgi
175 self.wsgi = wsgi
176
176
177 def __call__(self, environ, start_response):
177 def __call__(self, environ, start_response):
178 input_data = environ['wsgi.input'].read()
178 input_data = environ['wsgi.input'].read()
179 input_data = msgpack.unpackb(input_data)
179 input_data = msgpack.unpackb(input_data)
180
180
181 error = None
181 error = None
182 try:
182 try:
183 data, status, headers = self.wsgi.handle(
183 data, status, headers = self.wsgi.handle(
184 input_data['environment'], input_data['input_data'],
184 input_data['environment'], input_data['input_data'],
185 *input_data['args'], **input_data['kwargs'])
185 *input_data['args'], **input_data['kwargs'])
186 except Exception as e:
186 except Exception as e:
187 data, status, headers = [], None, None
187 data, status, headers = [], None, None
188 error = {
188 error = {
189 'message': str(e),
189 'message': str(e),
190 '_vcs_kind': getattr(e, '_vcs_kind', None)
190 '_vcs_kind': getattr(e, '_vcs_kind', None)
191 }
191 }
192
192
193 start_response(200, {})
193 start_response(200, {})
194 return self._iterator(error, status, headers, data)
194 return self._iterator(error, status, headers, data)
195
195
196 def _iterator(self, error, status, headers, data):
196 def _iterator(self, error, status, headers, data):
197 initial_data = [
197 initial_data = [
198 error,
198 error,
199 status,
199 status,
200 headers,
200 headers,
201 ]
201 ]
202
202
203 for d in chain(initial_data, data):
203 for d in chain(initial_data, data):
204 yield msgpack.packb(d)
204 yield msgpack.packb(d)
205
205
206
206
207 def not_found(request):
207 def not_found(request):
208 return {'status': '404 NOT FOUND'}
208 return {'status': '404 NOT FOUND'}
209
209
210
210
211 class VCSViewPredicate(object):
211 class VCSViewPredicate(object):
212 def __init__(self, val, config):
212 def __init__(self, val, config):
213 self.remotes = val
213 self.remotes = val
214
214
215 def text(self):
215 def text(self):
216 return 'vcs view method = %s' % (self.remotes.keys(),)
216 return 'vcs view method = %s' % (self.remotes.keys(),)
217
217
218 phash = text
218 phash = text
219
219
220 def __call__(self, context, request):
220 def __call__(self, context, request):
221 """
221 """
222 View predicate that returns true if given backend is supported by
222 View predicate that returns true if given backend is supported by
223 defined remotes.
223 defined remotes.
224 """
224 """
225 backend = request.matchdict.get('backend')
225 backend = request.matchdict.get('backend')
226 return backend in self.remotes
226 return backend in self.remotes
227
227
228
228
229 class HTTPApplication(object):
229 class HTTPApplication(object):
230 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
230 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
231
231
232 remote_wsgi = remote_wsgi
232 remote_wsgi = remote_wsgi
233 _use_echo_app = False
233 _use_echo_app = False
234
234
235 def __init__(self, settings=None, global_config=None):
235 def __init__(self, settings=None, global_config=None):
236 self._sanitize_settings_and_apply_defaults(settings)
236 self._sanitize_settings_and_apply_defaults(settings)
237
237
238 self.config = Configurator(settings=settings)
238 self.config = Configurator(settings=settings)
239 self.global_config = global_config
239 self.global_config = global_config
240 self.config.include('vcsserver.lib.rc_cache')
240 self.config.include('vcsserver.lib.rc_cache')
241
241
242 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
242 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
243 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
243 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
244 self._remotes = {
244 self._remotes = {
245 'hg': vcs._hg_remote,
245 'hg': vcs._hg_remote,
246 'git': vcs._git_remote,
246 'git': vcs._git_remote,
247 'svn': vcs._svn_remote,
247 'svn': vcs._svn_remote,
248 'server': vcs._vcsserver,
248 'server': vcs._vcsserver,
249 }
249 }
250 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
250 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
251 self._use_echo_app = True
251 self._use_echo_app = True
252 log.warning("Using EchoApp for VCS operations.")
252 log.warning("Using EchoApp for VCS operations.")
253 self.remote_wsgi = remote_wsgi_stub
253 self.remote_wsgi = remote_wsgi_stub
254
254
255 self._configure_settings(global_config, settings)
255 self._configure_settings(global_config, settings)
256 self._configure()
256 self._configure()
257
257
258 def _configure_settings(self, global_config, app_settings):
258 def _configure_settings(self, global_config, app_settings):
259 """
259 """
260 Configure the settings module.
260 Configure the settings module.
261 """
261 """
262 settings_merged = global_config.copy()
262 settings_merged = global_config.copy()
263 settings_merged.update(app_settings)
263 settings_merged.update(app_settings)
264
264
265 git_path = app_settings.get('git_path', None)
265 git_path = app_settings.get('git_path', None)
266 if git_path:
266 if git_path:
267 settings.GIT_EXECUTABLE = git_path
267 settings.GIT_EXECUTABLE = git_path
268 binary_dir = app_settings.get('core.binary_dir', None)
268 binary_dir = app_settings.get('core.binary_dir', None)
269 if binary_dir:
269 if binary_dir:
270 settings.BINARY_DIR = binary_dir
270 settings.BINARY_DIR = binary_dir
271
271
272 # Store the settings to make them available to other modules.
272 # Store the settings to make them available to other modules.
273 vcsserver.PYRAMID_SETTINGS = settings_merged
273 vcsserver.PYRAMID_SETTINGS = settings_merged
274 vcsserver.CONFIG = settings_merged
274 vcsserver.CONFIG = settings_merged
275
275
276 def _sanitize_settings_and_apply_defaults(self, settings):
276 def _sanitize_settings_and_apply_defaults(self, settings):
277 temp_store = tempfile.gettempdir()
277 temp_store = tempfile.gettempdir()
278 default_cache_dir = os.path.join(temp_store, 'rc_cache')
278 default_cache_dir = os.path.join(temp_store, 'rc_cache')
279
279
280 # save default, cache dir, and use it for all backends later.
280 # save default, cache dir, and use it for all backends later.
281 default_cache_dir = _string_setting(
281 default_cache_dir = _string_setting(
282 settings,
282 settings,
283 'cache_dir',
283 'cache_dir',
284 default_cache_dir, lower=False, default_when_empty=True)
284 default_cache_dir, lower=False, default_when_empty=True)
285
285
286 # ensure we have our dir created
286 # ensure we have our dir created
287 if not os.path.isdir(default_cache_dir):
287 if not os.path.isdir(default_cache_dir):
288 os.makedirs(default_cache_dir, mode=0o755)
288 os.makedirs(default_cache_dir, mode=0o755)
289
289
290 # exception store cache
290 # exception store cache
291 _string_setting(
291 _string_setting(
292 settings,
292 settings,
293 'exception_tracker.store_path',
293 'exception_tracker.store_path',
294 temp_store, lower=False, default_when_empty=True)
294 temp_store, lower=False, default_when_empty=True)
295
295
296 # repo_object cache
296 # repo_object cache
297 _string_setting(
297 _string_setting(
298 settings,
298 settings,
299 'rc_cache.repo_object.backend',
299 'rc_cache.repo_object.backend',
300 'dogpile.cache.rc.file_namespace', lower=False)
300 'dogpile.cache.rc.file_namespace', lower=False)
301 _int_setting(
301 _int_setting(
302 settings,
302 settings,
303 'rc_cache.repo_object.expiration_time',
303 'rc_cache.repo_object.expiration_time',
304 30 * 24 * 60 * 60)
304 30 * 24 * 60 * 60)
305 _string_setting(
305 _string_setting(
306 settings,
306 settings,
307 'rc_cache.repo_object.arguments.filename',
307 'rc_cache.repo_object.arguments.filename',
308 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
308 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
309
309
310 def _configure(self):
310 def _configure(self):
311 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
311 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
312
312
313 self.config.add_route('service', '/_service')
313 self.config.add_route('service', '/_service')
314 self.config.add_route('status', '/status')
314 self.config.add_route('status', '/status')
315 self.config.add_route('hg_proxy', '/proxy/hg')
315 self.config.add_route('hg_proxy', '/proxy/hg')
316 self.config.add_route('git_proxy', '/proxy/git')
316 self.config.add_route('git_proxy', '/proxy/git')
317
317
318 # rpc methods
318 # rpc methods
319 self.config.add_route('vcs', '/{backend}')
319 self.config.add_route('vcs', '/{backend}')
320
320
321 # streaming rpc remote methods
321 # streaming rpc remote methods
322 self.config.add_route('vcs_stream', '/{backend}/stream')
322 self.config.add_route('vcs_stream', '/{backend}/stream')
323
323
324 # vcs operations clone/push as streaming
324 # vcs operations clone/push as streaming
325 self.config.add_route('stream_git', '/stream/git/*repo_name')
325 self.config.add_route('stream_git', '/stream/git/*repo_name')
326 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
326 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
327
327
328 self.config.add_view(self.status_view, route_name='status', renderer='json')
328 self.config.add_view(self.status_view, route_name='status', renderer='json')
329 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
329 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
330
330
331 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
331 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
332 self.config.add_view(self.git_proxy(), route_name='git_proxy')
332 self.config.add_view(self.git_proxy(), route_name='git_proxy')
333 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
333 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
334 vcs_view=self._remotes)
334 vcs_view=self._remotes)
335 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
335 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
336 vcs_view=self._remotes)
336 vcs_view=self._remotes)
337
337
338 self.config.add_view(self.hg_stream(), route_name='stream_hg')
338 self.config.add_view(self.hg_stream(), route_name='stream_hg')
339 self.config.add_view(self.git_stream(), route_name='stream_git')
339 self.config.add_view(self.git_stream(), route_name='stream_git')
340
340
341 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
341 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
342
342
343 self.config.add_notfound_view(not_found, renderer='json')
343 self.config.add_notfound_view(not_found, renderer='json')
344
344
345 self.config.add_view(self.handle_vcs_exception, context=Exception)
345 self.config.add_view(self.handle_vcs_exception, context=Exception)
346
346
347 self.config.add_tween(
347 self.config.add_tween(
348 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
348 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
349 )
349 )
350 self.config.add_request_method(
350 self.config.add_request_method(
351 'vcsserver.lib.request_counter.get_request_counter',
351 'vcsserver.lib.request_counter.get_request_counter',
352 'request_count')
352 'request_count')
353
353
354 def wsgi_app(self):
354 def wsgi_app(self):
355 return self.config.make_wsgi_app()
355 return self.config.make_wsgi_app()
356
356
357 def _vcs_view_params(self, request):
357 def _vcs_view_params(self, request):
358 remote = self._remotes[request.matchdict['backend']]
358 remote = self._remotes[request.matchdict['backend']]
359 payload = msgpack.unpackb(request.body, use_list=True)
359 payload = msgpack.unpackb(request.body, use_list=True)
360 method = payload.get('method')
360 method = payload.get('method')
361 params = payload['params']
361 params = payload['params']
362 wire = params.get('wire')
362 wire = params.get('wire')
363 args = params.get('args')
363 args = params.get('args')
364 kwargs = params.get('kwargs')
364 kwargs = params.get('kwargs')
365 context_uid = None
365 context_uid = None
366
366
367 if wire:
367 if wire:
368 try:
368 try:
369 wire['context'] = context_uid = uuid.UUID(wire['context'])
369 wire['context'] = context_uid = uuid.UUID(wire['context'])
370 except KeyError:
370 except KeyError:
371 pass
371 pass
372 args.insert(0, wire)
372 args.insert(0, wire)
373 repo_state_uid = wire.get('repo_state_uid') if wire else None
373 repo_state_uid = wire.get('repo_state_uid') if wire else None
374
374
375 # NOTE(marcink): trading complexity for slight performance
375 # NOTE(marcink): trading complexity for slight performance
376 if log.isEnabledFor(logging.DEBUG):
376 if log.isEnabledFor(logging.DEBUG):
377 no_args_methods = [
377 no_args_methods = [
378 'archive_repo'
378
379 ]
379 ]
380 if method in no_args_methods:
380 if method in no_args_methods:
381 call_args = ''
381 call_args = ''
382 else:
382 else:
383 call_args = args[1:]
383 call_args = args[1:]
384
384
385 log.debug('method requested:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
385 log.debug('method requested:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
386 method, call_args, kwargs, context_uid, repo_state_uid)
386 method, call_args, kwargs, context_uid, repo_state_uid)
387
387
388 return payload, remote, method, args, kwargs
388 return payload, remote, method, args, kwargs
389
389
390 def vcs_view(self, request):
390 def vcs_view(self, request):
391
391
392 payload, remote, method, args, kwargs = self._vcs_view_params(request)
392 payload, remote, method, args, kwargs = self._vcs_view_params(request)
393 payload_id = payload.get('id')
393 payload_id = payload.get('id')
394
394
395 try:
395 try:
396 resp = getattr(remote, method)(*args, **kwargs)
396 resp = getattr(remote, method)(*args, **kwargs)
397 except Exception as e:
397 except Exception as e:
398 exc_info = list(sys.exc_info())
398 exc_info = list(sys.exc_info())
399 exc_type, exc_value, exc_traceback = exc_info
399 exc_type, exc_value, exc_traceback = exc_info
400
400
401 org_exc = getattr(e, '_org_exc', None)
401 org_exc = getattr(e, '_org_exc', None)
402 org_exc_name = None
402 org_exc_name = None
403 org_exc_tb = ''
403 org_exc_tb = ''
404 if org_exc:
404 if org_exc:
405 org_exc_name = org_exc.__class__.__name__
405 org_exc_name = org_exc.__class__.__name__
406 org_exc_tb = getattr(e, '_org_exc_tb', '')
406 org_exc_tb = getattr(e, '_org_exc_tb', '')
407 # replace our "faked" exception with our org
407 # replace our "faked" exception with our org
408 exc_info[0] = org_exc.__class__
408 exc_info[0] = org_exc.__class__
409 exc_info[1] = org_exc
409 exc_info[1] = org_exc
410
410
411 should_store_exc = True
411 should_store_exc = True
412 if org_exc:
412 if org_exc:
413 def get_exc_fqn(_exc_obj):
413 def get_exc_fqn(_exc_obj):
414 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
414 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
415 return module_name + '.' + org_exc_name
415 return module_name + '.' + org_exc_name
416
416
417 exc_fqn = get_exc_fqn(org_exc)
417 exc_fqn = get_exc_fqn(org_exc)
418
418
419 if exc_fqn in ['mercurial.error.RepoLookupError',
419 if exc_fqn in ['mercurial.error.RepoLookupError',
420 'vcsserver.exceptions.RefNotFoundException']:
420 'vcsserver.exceptions.RefNotFoundException']:
421 should_store_exc = False
421 should_store_exc = False
422
422
423 if should_store_exc:
423 if should_store_exc:
424 store_exception(id(exc_info), exc_info)
424 store_exception(id(exc_info), exc_info)
425
425
426 tb_info = ''.join(
426 tb_info = ''.join(
427 traceback.format_exception(exc_type, exc_value, exc_traceback))
427 traceback.format_exception(exc_type, exc_value, exc_traceback))
428
428
429 type_ = e.__class__.__name__
429 type_ = e.__class__.__name__
430 if type_ not in self.ALLOWED_EXCEPTIONS:
430 if type_ not in self.ALLOWED_EXCEPTIONS:
431 type_ = None
431 type_ = None
432
432
433 resp = {
433 resp = {
434 'id': payload_id,
434 'id': payload_id,
435 'error': {
435 'error': {
436 'message': e.message,
436 'message': e.message,
437 'traceback': tb_info,
437 'traceback': tb_info,
438 'org_exc': org_exc_name,
438 'org_exc': org_exc_name,
439 'org_exc_tb': org_exc_tb,
439 'org_exc_tb': org_exc_tb,
440 'type': type_
440 'type': type_
441 }
441 }
442 }
442 }
443 try:
443 try:
444 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
444 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
445 except AttributeError:
445 except AttributeError:
446 pass
446 pass
447 else:
447 else:
448 resp = {
448 resp = {
449 'id': payload_id,
449 'id': payload_id,
450 'result': resp
450 'result': resp
451 }
451 }
452
452
453 return resp
453 return resp
454
454
455 def vcs_stream_view(self, request):
455 def vcs_stream_view(self, request):
456 payload, remote, method, args, kwargs = self._vcs_view_params(request)
456 payload, remote, method, args, kwargs = self._vcs_view_params(request)
457 # this method has a stream: marker we remove it here
457 # this method has a stream: marker we remove it here
458 method = method.split('stream:')[-1]
458 method = method.split('stream:')[-1]
459 chunk_size = safe_int(payload.get('chunk_size')) or 4096
459 chunk_size = safe_int(payload.get('chunk_size')) or 4096
460
460
461 try:
461 try:
462 resp = getattr(remote, method)(*args, **kwargs)
462 resp = getattr(remote, method)(*args, **kwargs)
463 except Exception as e:
463 except Exception as e:
464 raise
464 raise
465
465
466 def get_chunked_data(method_resp):
466 def get_chunked_data(method_resp):
467 stream = StringIO(method_resp)
467 stream = StringIO(method_resp)
468 while 1:
468 while 1:
469 chunk = stream.read(chunk_size)
469 chunk = stream.read(chunk_size)
470 if not chunk:
470 if not chunk:
471 break
471 break
472 yield chunk
472 yield chunk
473
473
474 response = Response(app_iter=get_chunked_data(resp))
474 response = Response(app_iter=get_chunked_data(resp))
475 response.content_type = 'application/octet-stream'
475 response.content_type = 'application/octet-stream'
476
476
477 return response
477 return response
478
478
479 def status_view(self, request):
479 def status_view(self, request):
480 import vcsserver
480 import vcsserver
481 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
481 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
482 'pid': os.getpid()}
482 'pid': os.getpid()}
483
483
484 def service_view(self, request):
484 def service_view(self, request):
485 import vcsserver
485 import vcsserver
486
486
487 payload = msgpack.unpackb(request.body, use_list=True)
487 payload = msgpack.unpackb(request.body, use_list=True)
488 server_config, app_config = {}, {}
488 server_config, app_config = {}, {}
489
489
490 try:
490 try:
491 path = self.global_config['__file__']
491 path = self.global_config['__file__']
492 config = configparser.RawConfigParser()
492 config = configparser.RawConfigParser()
493
493
494 config.read(path)
494 config.read(path)
495
495
496 if config.has_section('server:main'):
496 if config.has_section('server:main'):
497 server_config = dict(config.items('server:main'))
497 server_config = dict(config.items('server:main'))
498 if config.has_section('app:main'):
498 if config.has_section('app:main'):
499 app_config = dict(config.items('app:main'))
499 app_config = dict(config.items('app:main'))
500
500
501 except Exception:
501 except Exception:
502 log.exception('Failed to read .ini file for display')
502 log.exception('Failed to read .ini file for display')
503
503
504 environ = os.environ.items()
504 environ = os.environ.items()
505
505
506 resp = {
506 resp = {
507 'id': payload.get('id'),
507 'id': payload.get('id'),
508 'result': dict(
508 'result': dict(
509 version=vcsserver.__version__,
509 version=vcsserver.__version__,
510 config=server_config,
510 config=server_config,
511 app_config=app_config,
511 app_config=app_config,
512 environ=environ,
512 environ=environ,
513 payload=payload,
513 payload=payload,
514 )
514 )
515 }
515 }
516 return resp
516 return resp
517
517
518 def _msgpack_renderer_factory(self, info):
518 def _msgpack_renderer_factory(self, info):
519 def _render(value, system):
519 def _render(value, system):
520 request = system.get('request')
520 request = system.get('request')
521 if request is not None:
521 if request is not None:
522 response = request.response
522 response = request.response
523 ct = response.content_type
523 ct = response.content_type
524 if ct == response.default_content_type:
524 if ct == response.default_content_type:
525 response.content_type = 'application/x-msgpack'
525 response.content_type = 'application/x-msgpack'
526 return msgpack.packb(value)
526 return msgpack.packb(value)
527 return _render
527 return _render
528
528
529 def set_env_from_config(self, environ, config):
529 def set_env_from_config(self, environ, config):
530 dict_conf = {}
530 dict_conf = {}
531 try:
531 try:
532 for elem in config:
532 for elem in config:
533 if elem[0] == 'rhodecode':
533 if elem[0] == 'rhodecode':
534 dict_conf = json.loads(elem[2])
534 dict_conf = json.loads(elem[2])
535 break
535 break
536 except Exception:
536 except Exception:
537 log.exception('Failed to fetch SCM CONFIG')
537 log.exception('Failed to fetch SCM CONFIG')
538 return
538 return
539
539
540 username = dict_conf.get('username')
540 username = dict_conf.get('username')
541 if username:
541 if username:
542 environ['REMOTE_USER'] = username
542 environ['REMOTE_USER'] = username
543 # mercurial specific, some extension api rely on this
543 # mercurial specific, some extension api rely on this
544 environ['HGUSER'] = username
544 environ['HGUSER'] = username
545
545
546 ip = dict_conf.get('ip')
546 ip = dict_conf.get('ip')
547 if ip:
547 if ip:
548 environ['REMOTE_HOST'] = ip
548 environ['REMOTE_HOST'] = ip
549
549
550 if _is_request_chunked(environ):
550 if _is_request_chunked(environ):
551 # set the compatibility flag for webob
551 # set the compatibility flag for webob
552 environ['wsgi.input_terminated'] = True
552 environ['wsgi.input_terminated'] = True
553
553
554 def hg_proxy(self):
554 def hg_proxy(self):
555 @wsgiapp
555 @wsgiapp
556 def _hg_proxy(environ, start_response):
556 def _hg_proxy(environ, start_response):
557 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
557 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
558 return app(environ, start_response)
558 return app(environ, start_response)
559 return _hg_proxy
559 return _hg_proxy
560
560
561 def git_proxy(self):
561 def git_proxy(self):
562 @wsgiapp
562 @wsgiapp
563 def _git_proxy(environ, start_response):
563 def _git_proxy(environ, start_response):
564 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
564 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
565 return app(environ, start_response)
565 return app(environ, start_response)
566 return _git_proxy
566 return _git_proxy
567
567
568 def hg_stream(self):
568 def hg_stream(self):
569 if self._use_echo_app:
569 if self._use_echo_app:
570 @wsgiapp
570 @wsgiapp
571 def _hg_stream(environ, start_response):
571 def _hg_stream(environ, start_response):
572 app = EchoApp('fake_path', 'fake_name', None)
572 app = EchoApp('fake_path', 'fake_name', None)
573 return app(environ, start_response)
573 return app(environ, start_response)
574 return _hg_stream
574 return _hg_stream
575 else:
575 else:
576 @wsgiapp
576 @wsgiapp
577 def _hg_stream(environ, start_response):
577 def _hg_stream(environ, start_response):
578 log.debug('http-app: handling hg stream')
578 log.debug('http-app: handling hg stream')
579 repo_path = environ['HTTP_X_RC_REPO_PATH']
579 repo_path = environ['HTTP_X_RC_REPO_PATH']
580 repo_name = environ['HTTP_X_RC_REPO_NAME']
580 repo_name = environ['HTTP_X_RC_REPO_NAME']
581 packed_config = base64.b64decode(
581 packed_config = base64.b64decode(
582 environ['HTTP_X_RC_REPO_CONFIG'])
582 environ['HTTP_X_RC_REPO_CONFIG'])
583 config = msgpack.unpackb(packed_config)
583 config = msgpack.unpackb(packed_config)
584 app = scm_app.create_hg_wsgi_app(
584 app = scm_app.create_hg_wsgi_app(
585 repo_path, repo_name, config)
585 repo_path, repo_name, config)
586
586
587 # Consistent path information for hgweb
587 # Consistent path information for hgweb
588 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
588 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
589 environ['REPO_NAME'] = repo_name
589 environ['REPO_NAME'] = repo_name
590 self.set_env_from_config(environ, config)
590 self.set_env_from_config(environ, config)
591
591
592 log.debug('http-app: starting app handler '
592 log.debug('http-app: starting app handler '
593 'with %s and process request', app)
593 'with %s and process request', app)
594 return app(environ, ResponseFilter(start_response))
594 return app(environ, ResponseFilter(start_response))
595 return _hg_stream
595 return _hg_stream
596
596
597 def git_stream(self):
597 def git_stream(self):
598 if self._use_echo_app:
598 if self._use_echo_app:
599 @wsgiapp
599 @wsgiapp
600 def _git_stream(environ, start_response):
600 def _git_stream(environ, start_response):
601 app = EchoApp('fake_path', 'fake_name', None)
601 app = EchoApp('fake_path', 'fake_name', None)
602 return app(environ, start_response)
602 return app(environ, start_response)
603 return _git_stream
603 return _git_stream
604 else:
604 else:
605 @wsgiapp
605 @wsgiapp
606 def _git_stream(environ, start_response):
606 def _git_stream(environ, start_response):
607 log.debug('http-app: handling git stream')
607 log.debug('http-app: handling git stream')
608 repo_path = environ['HTTP_X_RC_REPO_PATH']
608 repo_path = environ['HTTP_X_RC_REPO_PATH']
609 repo_name = environ['HTTP_X_RC_REPO_NAME']
609 repo_name = environ['HTTP_X_RC_REPO_NAME']
610 packed_config = base64.b64decode(
610 packed_config = base64.b64decode(
611 environ['HTTP_X_RC_REPO_CONFIG'])
611 environ['HTTP_X_RC_REPO_CONFIG'])
612 config = msgpack.unpackb(packed_config)
612 config = msgpack.unpackb(packed_config)
613
613
614 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
614 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
615 self.set_env_from_config(environ, config)
615 self.set_env_from_config(environ, config)
616
616
617 content_type = environ.get('CONTENT_TYPE', '')
617 content_type = environ.get('CONTENT_TYPE', '')
618
618
619 path = environ['PATH_INFO']
619 path = environ['PATH_INFO']
620 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
620 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
621 log.debug(
621 log.debug(
622 'LFS: Detecting if request `%s` is LFS server path based '
622 'LFS: Detecting if request `%s` is LFS server path based '
623 'on content type:`%s`, is_lfs:%s',
623 'on content type:`%s`, is_lfs:%s',
624 path, content_type, is_lfs_request)
624 path, content_type, is_lfs_request)
625
625
626 if not is_lfs_request:
626 if not is_lfs_request:
627 # fallback detection by path
627 # fallback detection by path
628 if GIT_LFS_PROTO_PAT.match(path):
628 if GIT_LFS_PROTO_PAT.match(path):
629 is_lfs_request = True
629 is_lfs_request = True
630 log.debug(
630 log.debug(
631 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
631 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
632 path, is_lfs_request)
632 path, is_lfs_request)
633
633
634 if is_lfs_request:
634 if is_lfs_request:
635 app = scm_app.create_git_lfs_wsgi_app(
635 app = scm_app.create_git_lfs_wsgi_app(
636 repo_path, repo_name, config)
636 repo_path, repo_name, config)
637 else:
637 else:
638 app = scm_app.create_git_wsgi_app(
638 app = scm_app.create_git_wsgi_app(
639 repo_path, repo_name, config)
639 repo_path, repo_name, config)
640
640
641 log.debug('http-app: starting app handler '
641 log.debug('http-app: starting app handler '
642 'with %s and process request', app)
642 'with %s and process request', app)
643
643
644 return app(environ, start_response)
644 return app(environ, start_response)
645
645
646 return _git_stream
646 return _git_stream
647
647
648 def handle_vcs_exception(self, exception, request):
648 def handle_vcs_exception(self, exception, request):
649 _vcs_kind = getattr(exception, '_vcs_kind', '')
649 _vcs_kind = getattr(exception, '_vcs_kind', '')
650 if _vcs_kind == 'repo_locked':
650 if _vcs_kind == 'repo_locked':
651 # Get custom repo-locked status code if present.
651 # Get custom repo-locked status code if present.
652 status_code = request.headers.get('X-RC-Locked-Status-Code')
652 status_code = request.headers.get('X-RC-Locked-Status-Code')
653 return HTTPRepoLocked(
653 return HTTPRepoLocked(
654 title=exception.message, status_code=status_code)
654 title=exception.message, status_code=status_code)
655
655
656 elif _vcs_kind == 'repo_branch_protected':
656 elif _vcs_kind == 'repo_branch_protected':
657 # Get custom repo-branch-protected status code if present.
657 # Get custom repo-branch-protected status code if present.
658 return HTTPRepoBranchProtected(title=exception.message)
658 return HTTPRepoBranchProtected(title=exception.message)
659
659
660 exc_info = request.exc_info
660 exc_info = request.exc_info
661 store_exception(id(exc_info), exc_info)
661 store_exception(id(exc_info), exc_info)
662
662
663 traceback_info = 'unavailable'
663 traceback_info = 'unavailable'
664 if request.exc_info:
664 if request.exc_info:
665 exc_type, exc_value, exc_tb = request.exc_info
665 exc_type, exc_value, exc_tb = request.exc_info
666 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
666 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
667
667
668 log.error(
668 log.error(
669 'error occurred handling this request for path: %s, \n tb: %s',
669 'error occurred handling this request for path: %s, \n tb: %s',
670 request.path, traceback_info)
670 request.path, traceback_info)
671 raise exception
671 raise exception
672
672
673
673
674 class ResponseFilter(object):
674 class ResponseFilter(object):
675
675
676 def __init__(self, start_response):
676 def __init__(self, start_response):
677 self._start_response = start_response
677 self._start_response = start_response
678
678
679 def __call__(self, status, response_headers, exc_info=None):
679 def __call__(self, status, response_headers, exc_info=None):
680 headers = tuple(
680 headers = tuple(
681 (h, v) for h, v in response_headers
681 (h, v) for h, v in response_headers
682 if not wsgiref.util.is_hop_by_hop(h))
682 if not wsgiref.util.is_hop_by_hop(h))
683 return self._start_response(status, headers, exc_info)
683 return self._start_response(status, headers, exc_info)
684
684
685
685
686 def main(global_config, **settings):
686 def main(global_config, **settings):
687 if MercurialFactory:
687 if MercurialFactory:
688 hgpatches.patch_largefiles_capabilities()
688 hgpatches.patch_largefiles_capabilities()
689 hgpatches.patch_subrepo_type_mapping()
689 hgpatches.patch_subrepo_type_mapping()
690
690
691 app = HTTPApplication(settings=settings, global_config=global_config)
691 app = HTTPApplication(settings=settings, global_config=global_config)
692 return app.wsgi_app()
692 return app.wsgi_app()
@@ -1,791 +1,856 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 from __future__ import absolute_import
18 from __future__ import absolute_import
19
19
20 import os
20 import os
21 import subprocess
21 import subprocess
22 import time
22 from urllib2 import URLError
23 from urllib2 import URLError
23 import urlparse
24 import urlparse
24 import logging
25 import logging
25 import posixpath as vcspath
26 import posixpath as vcspath
26 import StringIO
27 import StringIO
27 import urllib
28 import urllib
28 import traceback
29 import traceback
29
30
30 import svn.client
31 import svn.client
31 import svn.core
32 import svn.core
32 import svn.delta
33 import svn.delta
33 import svn.diff
34 import svn.diff
34 import svn.fs
35 import svn.fs
35 import svn.repos
36 import svn.repos
36
37
37 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver.base import RepoFactory, raise_from_original
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
40 from vcsserver.exceptions import NoContentException
39 from vcsserver.vcs_base import RemoteBase
41 from vcsserver.vcs_base import RemoteBase
40
42
41 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
42
44
43
45
44 svn_compatible_versions_map = {
46 svn_compatible_versions_map = {
45 'pre-1.4-compatible': '1.3',
47 'pre-1.4-compatible': '1.3',
46 'pre-1.5-compatible': '1.4',
48 'pre-1.5-compatible': '1.4',
47 'pre-1.6-compatible': '1.5',
49 'pre-1.6-compatible': '1.5',
48 'pre-1.8-compatible': '1.7',
50 'pre-1.8-compatible': '1.7',
49 'pre-1.9-compatible': '1.8',
51 'pre-1.9-compatible': '1.8',
50 }
52 }
51
53
52 current_compatible_version = '1.12'
54 current_compatible_version = '1.12'
53
55
54
56
55 def reraise_safe_exceptions(func):
57 def reraise_safe_exceptions(func):
56 """Decorator for converting svn exceptions to something neutral."""
58 """Decorator for converting svn exceptions to something neutral."""
57 def wrapper(*args, **kwargs):
59 def wrapper(*args, **kwargs):
58 try:
60 try:
59 return func(*args, **kwargs)
61 return func(*args, **kwargs)
60 except Exception as e:
62 except Exception as e:
61 if not hasattr(e, '_vcs_kind'):
63 if not hasattr(e, '_vcs_kind'):
62 log.exception("Unhandled exception in svn remote call")
64 log.exception("Unhandled exception in svn remote call")
63 raise_from_original(exceptions.UnhandledException(e))
65 raise_from_original(exceptions.UnhandledException(e))
64 raise
66 raise
65 return wrapper
67 return wrapper
66
68
67
69
68 class SubversionFactory(RepoFactory):
70 class SubversionFactory(RepoFactory):
69 repo_type = 'svn'
71 repo_type = 'svn'
70
72
71 def _create_repo(self, wire, create, compatible_version):
73 def _create_repo(self, wire, create, compatible_version):
72 path = svn.core.svn_path_canonicalize(wire['path'])
74 path = svn.core.svn_path_canonicalize(wire['path'])
73 if create:
75 if create:
74 fs_config = {'compatible-version': current_compatible_version}
76 fs_config = {'compatible-version': current_compatible_version}
75 if compatible_version:
77 if compatible_version:
76
78
77 compatible_version_string = \
79 compatible_version_string = \
78 svn_compatible_versions_map.get(compatible_version) \
80 svn_compatible_versions_map.get(compatible_version) \
79 or compatible_version
81 or compatible_version
80 fs_config['compatible-version'] = compatible_version_string
82 fs_config['compatible-version'] = compatible_version_string
81
83
82 log.debug('Create SVN repo with config "%s"', fs_config)
84 log.debug('Create SVN repo with config "%s"', fs_config)
83 repo = svn.repos.create(path, "", "", None, fs_config)
85 repo = svn.repos.create(path, "", "", None, fs_config)
84 else:
86 else:
85 repo = svn.repos.open(path)
87 repo = svn.repos.open(path)
86
88
87 log.debug('Got SVN object: %s', repo)
89 log.debug('Got SVN object: %s', repo)
88 return repo
90 return repo
89
91
90 def repo(self, wire, create=False, compatible_version=None):
92 def repo(self, wire, create=False, compatible_version=None):
91 """
93 """
92 Get a repository instance for the given path.
94 Get a repository instance for the given path.
93 """
95 """
94 return self._create_repo(wire, create, compatible_version)
96 return self._create_repo(wire, create, compatible_version)
95
97
96
98
97 NODE_TYPE_MAPPING = {
99 NODE_TYPE_MAPPING = {
98 svn.core.svn_node_file: 'file',
100 svn.core.svn_node_file: 'file',
99 svn.core.svn_node_dir: 'dir',
101 svn.core.svn_node_dir: 'dir',
100 }
102 }
101
103
102
104
103 class SvnRemote(RemoteBase):
105 class SvnRemote(RemoteBase):
104
106
105 def __init__(self, factory, hg_factory=None):
107 def __init__(self, factory, hg_factory=None):
106 self._factory = factory
108 self._factory = factory
107 # TODO: Remove once we do not use internal Mercurial objects anymore
109 # TODO: Remove once we do not use internal Mercurial objects anymore
108 # for subversion
110 # for subversion
109 self._hg_factory = hg_factory
111 self._hg_factory = hg_factory
110
112
111 @reraise_safe_exceptions
113 @reraise_safe_exceptions
112 def discover_svn_version(self):
114 def discover_svn_version(self):
113 try:
115 try:
114 import svn.core
116 import svn.core
115 svn_ver = svn.core.SVN_VERSION
117 svn_ver = svn.core.SVN_VERSION
116 except ImportError:
118 except ImportError:
117 svn_ver = None
119 svn_ver = None
118 return svn_ver
120 return svn_ver
119
121
120 @reraise_safe_exceptions
122 @reraise_safe_exceptions
121 def is_empty(self, wire):
123 def is_empty(self, wire):
122
124
123 try:
125 try:
124 return self.lookup(wire, -1) == 0
126 return self.lookup(wire, -1) == 0
125 except Exception:
127 except Exception:
126 log.exception("failed to read object_store")
128 log.exception("failed to read object_store")
127 return False
129 return False
128
130
129 def check_url(self, url, config_items):
131 def check_url(self, url, config_items):
130 # this can throw exception if not installed, but we detect this
132 # this can throw exception if not installed, but we detect this
131 from hgsubversion import svnrepo
133 from hgsubversion import svnrepo
132
134
133 baseui = self._hg_factory._create_config(config_items)
135 baseui = self._hg_factory._create_config(config_items)
134 # uuid function get's only valid UUID from proper repo, else
136 # uuid function get's only valid UUID from proper repo, else
135 # throws exception
137 # throws exception
136 try:
138 try:
137 svnrepo.svnremoterepo(baseui, url).svn.uuid
139 svnrepo.svnremoterepo(baseui, url).svn.uuid
138 except Exception:
140 except Exception:
139 tb = traceback.format_exc()
141 tb = traceback.format_exc()
140 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
142 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
141 raise URLError(
143 raise URLError(
142 '"%s" is not a valid Subversion source url.' % (url, ))
144 '"%s" is not a valid Subversion source url.' % (url, ))
143 return True
145 return True
144
146
145 def is_path_valid_repository(self, wire, path):
147 def is_path_valid_repository(self, wire, path):
146
148
147 # NOTE(marcink): short circuit the check for SVN repo
149 # NOTE(marcink): short circuit the check for SVN repo
148 # the repos.open might be expensive to check, but we have one cheap
150 # the repos.open might be expensive to check, but we have one cheap
149 # pre condition that we can use, to check for 'format' file
151 # pre condition that we can use, to check for 'format' file
150
152
151 if not os.path.isfile(os.path.join(path, 'format')):
153 if not os.path.isfile(os.path.join(path, 'format')):
152 return False
154 return False
153
155
154 try:
156 try:
155 svn.repos.open(path)
157 svn.repos.open(path)
156 except svn.core.SubversionException:
158 except svn.core.SubversionException:
157 tb = traceback.format_exc()
159 tb = traceback.format_exc()
158 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
160 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
159 return False
161 return False
160 return True
162 return True
161
163
162 @reraise_safe_exceptions
164 @reraise_safe_exceptions
163 def verify(self, wire,):
165 def verify(self, wire,):
164 repo_path = wire['path']
166 repo_path = wire['path']
165 if not self.is_path_valid_repository(wire, repo_path):
167 if not self.is_path_valid_repository(wire, repo_path):
166 raise Exception(
168 raise Exception(
167 "Path %s is not a valid Subversion repository." % repo_path)
169 "Path %s is not a valid Subversion repository." % repo_path)
168
170
169 cmd = ['svnadmin', 'info', repo_path]
171 cmd = ['svnadmin', 'info', repo_path]
170 stdout, stderr = subprocessio.run_command(cmd)
172 stdout, stderr = subprocessio.run_command(cmd)
171 return stdout
173 return stdout
172
174
173 def lookup(self, wire, revision):
175 def lookup(self, wire, revision):
174 if revision not in [-1, None, 'HEAD']:
176 if revision not in [-1, None, 'HEAD']:
175 raise NotImplementedError
177 raise NotImplementedError
176 repo = self._factory.repo(wire)
178 repo = self._factory.repo(wire)
177 fs_ptr = svn.repos.fs(repo)
179 fs_ptr = svn.repos.fs(repo)
178 head = svn.fs.youngest_rev(fs_ptr)
180 head = svn.fs.youngest_rev(fs_ptr)
179 return head
181 return head
180
182
181 def lookup_interval(self, wire, start_ts, end_ts):
183 def lookup_interval(self, wire, start_ts, end_ts):
182 repo = self._factory.repo(wire)
184 repo = self._factory.repo(wire)
183 fsobj = svn.repos.fs(repo)
185 fsobj = svn.repos.fs(repo)
184 start_rev = None
186 start_rev = None
185 end_rev = None
187 end_rev = None
186 if start_ts:
188 if start_ts:
187 start_ts_svn = apr_time_t(start_ts)
189 start_ts_svn = apr_time_t(start_ts)
188 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
190 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
189 else:
191 else:
190 start_rev = 1
192 start_rev = 1
191 if end_ts:
193 if end_ts:
192 end_ts_svn = apr_time_t(end_ts)
194 end_ts_svn = apr_time_t(end_ts)
193 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
195 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
194 else:
196 else:
195 end_rev = svn.fs.youngest_rev(fsobj)
197 end_rev = svn.fs.youngest_rev(fsobj)
196 return start_rev, end_rev
198 return start_rev, end_rev
197
199
198 def revision_properties(self, wire, revision):
200 def revision_properties(self, wire, revision):
199
201
200 cache_on, context_uid, repo_id = self._cache_on(wire)
202 cache_on, context_uid, repo_id = self._cache_on(wire)
201 @self.region.conditional_cache_on_arguments(condition=cache_on)
203 @self.region.conditional_cache_on_arguments(condition=cache_on)
202 def _revision_properties(_repo_id, _revision):
204 def _revision_properties(_repo_id, _revision):
203 repo = self._factory.repo(wire)
205 repo = self._factory.repo(wire)
204 fs_ptr = svn.repos.fs(repo)
206 fs_ptr = svn.repos.fs(repo)
205 return svn.fs.revision_proplist(fs_ptr, revision)
207 return svn.fs.revision_proplist(fs_ptr, revision)
206 return _revision_properties(repo_id, revision)
208 return _revision_properties(repo_id, revision)
207
209
208 def revision_changes(self, wire, revision):
210 def revision_changes(self, wire, revision):
209
211
210 repo = self._factory.repo(wire)
212 repo = self._factory.repo(wire)
211 fsobj = svn.repos.fs(repo)
213 fsobj = svn.repos.fs(repo)
212 rev_root = svn.fs.revision_root(fsobj, revision)
214 rev_root = svn.fs.revision_root(fsobj, revision)
213
215
214 editor = svn.repos.ChangeCollector(fsobj, rev_root)
216 editor = svn.repos.ChangeCollector(fsobj, rev_root)
215 editor_ptr, editor_baton = svn.delta.make_editor(editor)
217 editor_ptr, editor_baton = svn.delta.make_editor(editor)
216 base_dir = ""
218 base_dir = ""
217 send_deltas = False
219 send_deltas = False
218 svn.repos.replay2(
220 svn.repos.replay2(
219 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
221 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
220 editor_ptr, editor_baton, None)
222 editor_ptr, editor_baton, None)
221
223
222 added = []
224 added = []
223 changed = []
225 changed = []
224 removed = []
226 removed = []
225
227
226 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
228 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
227 for path, change in editor.changes.iteritems():
229 for path, change in editor.changes.iteritems():
228 # TODO: Decide what to do with directory nodes. Subversion can add
230 # TODO: Decide what to do with directory nodes. Subversion can add
229 # empty directories.
231 # empty directories.
230
232
231 if change.item_kind == svn.core.svn_node_dir:
233 if change.item_kind == svn.core.svn_node_dir:
232 continue
234 continue
233 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
235 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
234 added.append(path)
236 added.append(path)
235 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
237 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
236 svn.repos.CHANGE_ACTION_REPLACE]:
238 svn.repos.CHANGE_ACTION_REPLACE]:
237 changed.append(path)
239 changed.append(path)
238 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
240 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
239 removed.append(path)
241 removed.append(path)
240 else:
242 else:
241 raise NotImplementedError(
243 raise NotImplementedError(
242 "Action %s not supported on path %s" % (
244 "Action %s not supported on path %s" % (
243 change.action, path))
245 change.action, path))
244
246
245 changes = {
247 changes = {
246 'added': added,
248 'added': added,
247 'changed': changed,
249 'changed': changed,
248 'removed': removed,
250 'removed': removed,
249 }
251 }
250 return changes
252 return changes
251
253
252 @reraise_safe_exceptions
254 @reraise_safe_exceptions
253 def node_history(self, wire, path, revision, limit):
255 def node_history(self, wire, path, revision, limit):
254 cache_on, context_uid, repo_id = self._cache_on(wire)
256 cache_on, context_uid, repo_id = self._cache_on(wire)
255 @self.region.conditional_cache_on_arguments(condition=cache_on)
257 @self.region.conditional_cache_on_arguments(condition=cache_on)
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
258 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 cross_copies = False
259 cross_copies = False
258 repo = self._factory.repo(wire)
260 repo = self._factory.repo(wire)
259 fsobj = svn.repos.fs(repo)
261 fsobj = svn.repos.fs(repo)
260 rev_root = svn.fs.revision_root(fsobj, revision)
262 rev_root = svn.fs.revision_root(fsobj, revision)
261
263
262 history_revisions = []
264 history_revisions = []
263 history = svn.fs.node_history(rev_root, path)
265 history = svn.fs.node_history(rev_root, path)
264 history = svn.fs.history_prev(history, cross_copies)
266 history = svn.fs.history_prev(history, cross_copies)
265 while history:
267 while history:
266 __, node_revision = svn.fs.history_location(history)
268 __, node_revision = svn.fs.history_location(history)
267 history_revisions.append(node_revision)
269 history_revisions.append(node_revision)
268 if limit and len(history_revisions) >= limit:
270 if limit and len(history_revisions) >= limit:
269 break
271 break
270 history = svn.fs.history_prev(history, cross_copies)
272 history = svn.fs.history_prev(history, cross_copies)
271 return history_revisions
273 return history_revisions
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
274 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273
275
274 def node_properties(self, wire, path, revision):
276 def node_properties(self, wire, path, revision):
275 cache_on, context_uid, repo_id = self._cache_on(wire)
277 cache_on, context_uid, repo_id = self._cache_on(wire)
276 @self.region.conditional_cache_on_arguments(condition=cache_on)
278 @self.region.conditional_cache_on_arguments(condition=cache_on)
277 def _node_properties(_repo_id, _path, _revision):
279 def _node_properties(_repo_id, _path, _revision):
278 repo = self._factory.repo(wire)
280 repo = self._factory.repo(wire)
279 fsobj = svn.repos.fs(repo)
281 fsobj = svn.repos.fs(repo)
280 rev_root = svn.fs.revision_root(fsobj, revision)
282 rev_root = svn.fs.revision_root(fsobj, revision)
281 return svn.fs.node_proplist(rev_root, path)
283 return svn.fs.node_proplist(rev_root, path)
282 return _node_properties(repo_id, path, revision)
284 return _node_properties(repo_id, path, revision)
283
285
284 def file_annotate(self, wire, path, revision):
286 def file_annotate(self, wire, path, revision):
285 abs_path = 'file://' + urllib.pathname2url(
287 abs_path = 'file://' + urllib.pathname2url(
286 vcspath.join(wire['path'], path))
288 vcspath.join(wire['path'], path))
287 file_uri = svn.core.svn_path_canonicalize(abs_path)
289 file_uri = svn.core.svn_path_canonicalize(abs_path)
288
290
289 start_rev = svn_opt_revision_value_t(0)
291 start_rev = svn_opt_revision_value_t(0)
290 peg_rev = svn_opt_revision_value_t(revision)
292 peg_rev = svn_opt_revision_value_t(revision)
291 end_rev = peg_rev
293 end_rev = peg_rev
292
294
293 annotations = []
295 annotations = []
294
296
295 def receiver(line_no, revision, author, date, line, pool):
297 def receiver(line_no, revision, author, date, line, pool):
296 annotations.append((line_no, revision, line))
298 annotations.append((line_no, revision, line))
297
299
298 # TODO: Cannot use blame5, missing typemap function in the swig code
300 # TODO: Cannot use blame5, missing typemap function in the swig code
299 try:
301 try:
300 svn.client.blame2(
302 svn.client.blame2(
301 file_uri, peg_rev, start_rev, end_rev,
303 file_uri, peg_rev, start_rev, end_rev,
302 receiver, svn.client.create_context())
304 receiver, svn.client.create_context())
303 except svn.core.SubversionException as exc:
305 except svn.core.SubversionException as exc:
304 log.exception("Error during blame operation.")
306 log.exception("Error during blame operation.")
305 raise Exception(
307 raise Exception(
306 "Blame not supported or file does not exist at path %s. "
308 "Blame not supported or file does not exist at path %s. "
307 "Error %s." % (path, exc))
309 "Error %s." % (path, exc))
308
310
309 return annotations
311 return annotations
310
312
311 def get_node_type(self, wire, path, revision=None):
313 def get_node_type(self, wire, path, revision=None):
312
314
313 cache_on, context_uid, repo_id = self._cache_on(wire)
315 cache_on, context_uid, repo_id = self._cache_on(wire)
314 @self.region.conditional_cache_on_arguments(condition=cache_on)
316 @self.region.conditional_cache_on_arguments(condition=cache_on)
315 def _get_node_type(_repo_id, _path, _revision):
317 def _get_node_type(_repo_id, _path, _revision):
316 repo = self._factory.repo(wire)
318 repo = self._factory.repo(wire)
317 fs_ptr = svn.repos.fs(repo)
319 fs_ptr = svn.repos.fs(repo)
318 if _revision is None:
320 if _revision is None:
319 _revision = svn.fs.youngest_rev(fs_ptr)
321 _revision = svn.fs.youngest_rev(fs_ptr)
320 root = svn.fs.revision_root(fs_ptr, _revision)
322 root = svn.fs.revision_root(fs_ptr, _revision)
321 node = svn.fs.check_path(root, path)
323 node = svn.fs.check_path(root, path)
322 return NODE_TYPE_MAPPING.get(node, None)
324 return NODE_TYPE_MAPPING.get(node, None)
323 return _get_node_type(repo_id, path, revision)
325 return _get_node_type(repo_id, path, revision)
324
326
325 def get_nodes(self, wire, path, revision=None):
327 def get_nodes(self, wire, path, revision=None):
326
328
327 cache_on, context_uid, repo_id = self._cache_on(wire)
329 cache_on, context_uid, repo_id = self._cache_on(wire)
328 @self.region.conditional_cache_on_arguments(condition=cache_on)
330 @self.region.conditional_cache_on_arguments(condition=cache_on)
329 def _get_nodes(_repo_id, _path, _revision):
331 def _get_nodes(_repo_id, _path, _revision):
330 repo = self._factory.repo(wire)
332 repo = self._factory.repo(wire)
331 fsobj = svn.repos.fs(repo)
333 fsobj = svn.repos.fs(repo)
332 if _revision is None:
334 if _revision is None:
333 _revision = svn.fs.youngest_rev(fsobj)
335 _revision = svn.fs.youngest_rev(fsobj)
334 root = svn.fs.revision_root(fsobj, _revision)
336 root = svn.fs.revision_root(fsobj, _revision)
335 entries = svn.fs.dir_entries(root, path)
337 entries = svn.fs.dir_entries(root, path)
336 result = []
338 result = []
337 for entry_path, entry_info in entries.iteritems():
339 for entry_path, entry_info in entries.iteritems():
338 result.append(
340 result.append(
339 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
341 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
340 return result
342 return result
341 return _get_nodes(repo_id, path, revision)
343 return _get_nodes(repo_id, path, revision)
342
344
343 def get_file_content(self, wire, path, rev=None):
345 def get_file_content(self, wire, path, rev=None):
344 repo = self._factory.repo(wire)
346 repo = self._factory.repo(wire)
345 fsobj = svn.repos.fs(repo)
347 fsobj = svn.repos.fs(repo)
346 if rev is None:
348 if rev is None:
347 rev = svn.fs.youngest_revision(fsobj)
349 rev = svn.fs.youngest_revision(fsobj)
348 root = svn.fs.revision_root(fsobj, rev)
350 root = svn.fs.revision_root(fsobj, rev)
349 content = svn.core.Stream(svn.fs.file_contents(root, path))
351 content = svn.core.Stream(svn.fs.file_contents(root, path))
350 return content.read()
352 return content.read()
351
353
352 def get_file_size(self, wire, path, revision=None):
354 def get_file_size(self, wire, path, revision=None):
353
355
354 cache_on, context_uid, repo_id = self._cache_on(wire)
356 cache_on, context_uid, repo_id = self._cache_on(wire)
355 @self.region.conditional_cache_on_arguments(condition=cache_on)
357 @self.region.conditional_cache_on_arguments(condition=cache_on)
356 def _get_file_size(_repo_id, _path, _revision):
358 def _get_file_size(_repo_id, _path, _revision):
357 repo = self._factory.repo(wire)
359 repo = self._factory.repo(wire)
358 fsobj = svn.repos.fs(repo)
360 fsobj = svn.repos.fs(repo)
359 if _revision is None:
361 if _revision is None:
360 _revision = svn.fs.youngest_revision(fsobj)
362 _revision = svn.fs.youngest_revision(fsobj)
361 root = svn.fs.revision_root(fsobj, _revision)
363 root = svn.fs.revision_root(fsobj, _revision)
362 size = svn.fs.file_length(root, path)
364 size = svn.fs.file_length(root, path)
363 return size
365 return size
364 return _get_file_size(repo_id, path, revision)
366 return _get_file_size(repo_id, path, revision)
365
367
366 def create_repository(self, wire, compatible_version=None):
368 def create_repository(self, wire, compatible_version=None):
367 log.info('Creating Subversion repository in path "%s"', wire['path'])
369 log.info('Creating Subversion repository in path "%s"', wire['path'])
368 self._factory.repo(wire, create=True,
370 self._factory.repo(wire, create=True,
369 compatible_version=compatible_version)
371 compatible_version=compatible_version)
370
372
371 def get_url_and_credentials(self, src_url):
373 def get_url_and_credentials(self, src_url):
372 obj = urlparse.urlparse(src_url)
374 obj = urlparse.urlparse(src_url)
373 username = obj.username or None
375 username = obj.username or None
374 password = obj.password or None
376 password = obj.password or None
375 return username, password, src_url
377 return username, password, src_url
376
378
377 def import_remote_repository(self, wire, src_url):
379 def import_remote_repository(self, wire, src_url):
378 repo_path = wire['path']
380 repo_path = wire['path']
379 if not self.is_path_valid_repository(wire, repo_path):
381 if not self.is_path_valid_repository(wire, repo_path):
380 raise Exception(
382 raise Exception(
381 "Path %s is not a valid Subversion repository." % repo_path)
383 "Path %s is not a valid Subversion repository." % repo_path)
382
384
383 username, password, src_url = self.get_url_and_credentials(src_url)
385 username, password, src_url = self.get_url_and_credentials(src_url)
384 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
386 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
385 '--trust-server-cert-failures=unknown-ca']
387 '--trust-server-cert-failures=unknown-ca']
386 if username and password:
388 if username and password:
387 rdump_cmd += ['--username', username, '--password', password]
389 rdump_cmd += ['--username', username, '--password', password]
388 rdump_cmd += [src_url]
390 rdump_cmd += [src_url]
389
391
390 rdump = subprocess.Popen(
392 rdump = subprocess.Popen(
391 rdump_cmd,
393 rdump_cmd,
392 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
394 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
393 load = subprocess.Popen(
395 load = subprocess.Popen(
394 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
396 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
395
397
396 # TODO: johbo: This can be a very long operation, might be better
398 # TODO: johbo: This can be a very long operation, might be better
397 # to track some kind of status and provide an api to check if the
399 # to track some kind of status and provide an api to check if the
398 # import is done.
400 # import is done.
399 rdump.wait()
401 rdump.wait()
400 load.wait()
402 load.wait()
401
403
402 log.debug('Return process ended with code: %s', rdump.returncode)
404 log.debug('Return process ended with code: %s', rdump.returncode)
403 if rdump.returncode != 0:
405 if rdump.returncode != 0:
404 errors = rdump.stderr.read()
406 errors = rdump.stderr.read()
405 log.error('svnrdump dump failed: statuscode %s: message: %s',
407 log.error('svnrdump dump failed: statuscode %s: message: %s',
406 rdump.returncode, errors)
408 rdump.returncode, errors)
407 reason = 'UNKNOWN'
409 reason = 'UNKNOWN'
408 if 'svnrdump: E230001:' in errors:
410 if 'svnrdump: E230001:' in errors:
409 reason = 'INVALID_CERTIFICATE'
411 reason = 'INVALID_CERTIFICATE'
410
412
411 if reason == 'UNKNOWN':
413 if reason == 'UNKNOWN':
412 reason = 'UNKNOWN:{}'.format(errors)
414 reason = 'UNKNOWN:{}'.format(errors)
413 raise Exception(
415 raise Exception(
414 'Failed to dump the remote repository from %s. Reason:%s' % (
416 'Failed to dump the remote repository from %s. Reason:%s' % (
415 src_url, reason))
417 src_url, reason))
416 if load.returncode != 0:
418 if load.returncode != 0:
417 raise Exception(
419 raise Exception(
418 'Failed to load the dump of remote repository from %s.' %
420 'Failed to load the dump of remote repository from %s.' %
419 (src_url, ))
421 (src_url, ))
420
422
421 def commit(self, wire, message, author, timestamp, updated, removed):
423 def commit(self, wire, message, author, timestamp, updated, removed):
422 assert isinstance(message, str)
424 assert isinstance(message, str)
423 assert isinstance(author, str)
425 assert isinstance(author, str)
424
426
425 repo = self._factory.repo(wire)
427 repo = self._factory.repo(wire)
426 fsobj = svn.repos.fs(repo)
428 fsobj = svn.repos.fs(repo)
427
429
428 rev = svn.fs.youngest_rev(fsobj)
430 rev = svn.fs.youngest_rev(fsobj)
429 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
431 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
430 txn_root = svn.fs.txn_root(txn)
432 txn_root = svn.fs.txn_root(txn)
431
433
432 for node in updated:
434 for node in updated:
433 TxnNodeProcessor(node, txn_root).update()
435 TxnNodeProcessor(node, txn_root).update()
434 for node in removed:
436 for node in removed:
435 TxnNodeProcessor(node, txn_root).remove()
437 TxnNodeProcessor(node, txn_root).remove()
436
438
437 commit_id = svn.repos.fs_commit_txn(repo, txn)
439 commit_id = svn.repos.fs_commit_txn(repo, txn)
438
440
439 if timestamp:
441 if timestamp:
440 apr_time = apr_time_t(timestamp)
442 apr_time = apr_time_t(timestamp)
441 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
443 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
442 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
444 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
443
445
444 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
446 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
445 return commit_id
447 return commit_id
446
448
447 def diff(self, wire, rev1, rev2, path1=None, path2=None,
449 def diff(self, wire, rev1, rev2, path1=None, path2=None,
448 ignore_whitespace=False, context=3):
450 ignore_whitespace=False, context=3):
449
451
450 wire.update(cache=False)
452 wire.update(cache=False)
451 repo = self._factory.repo(wire)
453 repo = self._factory.repo(wire)
452 diff_creator = SvnDiffer(
454 diff_creator = SvnDiffer(
453 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
455 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
454 try:
456 try:
455 return diff_creator.generate_diff()
457 return diff_creator.generate_diff()
456 except svn.core.SubversionException as e:
458 except svn.core.SubversionException as e:
457 log.exception(
459 log.exception(
458 "Error during diff operation operation. "
460 "Error during diff operation operation. "
459 "Path might not exist %s, %s" % (path1, path2))
461 "Path might not exist %s, %s" % (path1, path2))
460 return ""
462 return ""
461
463
462 @reraise_safe_exceptions
464 @reraise_safe_exceptions
463 def is_large_file(self, wire, path):
465 def is_large_file(self, wire, path):
464 return False
466 return False
465
467
466 @reraise_safe_exceptions
468 @reraise_safe_exceptions
467 def is_binary(self, wire, rev, path):
469 def is_binary(self, wire, rev, path):
468 cache_on, context_uid, repo_id = self._cache_on(wire)
470 cache_on, context_uid, repo_id = self._cache_on(wire)
469
471
470 @self.region.conditional_cache_on_arguments(condition=cache_on)
472 @self.region.conditional_cache_on_arguments(condition=cache_on)
471 def _is_binary(_repo_id, _rev, _path):
473 def _is_binary(_repo_id, _rev, _path):
472 raw_bytes = self.get_file_content(wire, path, rev)
474 raw_bytes = self.get_file_content(wire, path, rev)
473 return raw_bytes and '\0' in raw_bytes
475 return raw_bytes and '\0' in raw_bytes
474
476
475 return _is_binary(repo_id, rev, path)
477 return _is_binary(repo_id, rev, path)
476
478
477 @reraise_safe_exceptions
479 @reraise_safe_exceptions
478 def run_svn_command(self, wire, cmd, **opts):
480 def run_svn_command(self, wire, cmd, **opts):
479 path = wire.get('path', None)
481 path = wire.get('path', None)
480
482
481 if path and os.path.isdir(path):
483 if path and os.path.isdir(path):
482 opts['cwd'] = path
484 opts['cwd'] = path
483
485
484 safe_call = False
486 safe_call = False
485 if '_safe' in opts:
487 if '_safe' in opts:
486 safe_call = True
488 safe_call = True
487
489
488 svnenv = os.environ.copy()
490 svnenv = os.environ.copy()
489 svnenv.update(opts.pop('extra_env', {}))
491 svnenv.update(opts.pop('extra_env', {}))
490
492
491 _opts = {'env': svnenv, 'shell': False}
493 _opts = {'env': svnenv, 'shell': False}
492
494
493 try:
495 try:
494 _opts.update(opts)
496 _opts.update(opts)
495 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
497 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
496
498
497 return ''.join(p), ''.join(p.error)
499 return ''.join(p), ''.join(p.error)
498 except (EnvironmentError, OSError) as err:
500 except (EnvironmentError, OSError) as err:
499 cmd = ' '.join(cmd) # human friendly CMD
501 cmd = ' '.join(cmd) # human friendly CMD
500 tb_err = ("Couldn't run svn command (%s).\n"
502 tb_err = ("Couldn't run svn command (%s).\n"
501 "Original error was:%s\n"
503 "Original error was:%s\n"
502 "Call options:%s\n"
504 "Call options:%s\n"
503 % (cmd, err, _opts))
505 % (cmd, err, _opts))
504 log.exception(tb_err)
506 log.exception(tb_err)
505 if safe_call:
507 if safe_call:
506 return '', err
508 return '', err
507 else:
509 else:
508 raise exceptions.VcsException()(tb_err)
510 raise exceptions.VcsException()(tb_err)
509
511
510 @reraise_safe_exceptions
512 @reraise_safe_exceptions
511 def install_hooks(self, wire, force=False):
513 def install_hooks(self, wire, force=False):
512 from vcsserver.hook_utils import install_svn_hooks
514 from vcsserver.hook_utils import install_svn_hooks
513 repo_path = wire['path']
515 repo_path = wire['path']
514 binary_dir = settings.BINARY_DIR
516 binary_dir = settings.BINARY_DIR
515 executable = None
517 executable = None
516 if binary_dir:
518 if binary_dir:
517 executable = os.path.join(binary_dir, 'python')
519 executable = os.path.join(binary_dir, 'python')
518 return install_svn_hooks(
520 return install_svn_hooks(
519 repo_path, executable=executable, force_create=force)
521 repo_path, executable=executable, force_create=force)
520
522
521 @reraise_safe_exceptions
523 @reraise_safe_exceptions
522 def get_hooks_info(self, wire):
524 def get_hooks_info(self, wire):
523 from vcsserver.hook_utils import (
525 from vcsserver.hook_utils import (
524 get_svn_pre_hook_version, get_svn_post_hook_version)
526 get_svn_pre_hook_version, get_svn_post_hook_version)
525 repo_path = wire['path']
527 repo_path = wire['path']
526 return {
528 return {
527 'pre_version': get_svn_pre_hook_version(repo_path),
529 'pre_version': get_svn_pre_hook_version(repo_path),
528 'post_version': get_svn_post_hook_version(repo_path),
530 'post_version': get_svn_post_hook_version(repo_path),
529 }
531 }
530
532
533 @reraise_safe_exceptions
534 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
535 archive_dir_name, commit_id):
536
537 def walk_tree(root, root_dir, _commit_id):
538 """
539 Special recursive svn repo walker
540 """
541
542 filemode_default = 0o100644
543 filemode_executable = 0o100755
544
545 file_iter = svn.fs.dir_entries(root, root_dir)
546 for f_name in file_iter:
547 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
548
549 if f_type == 'dir':
550 # return only DIR, and then all entries in that dir
551 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
552 new_root = os.path.join(root_dir, f_name)
553 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
554 yield _f_name, _f_data, _f_type
555 else:
556 f_path = os.path.join(root_dir, f_name).rstrip('/')
557 prop_list = svn.fs.node_proplist(root, f_path)
558
559 f_mode = filemode_default
560 if prop_list.get('svn:executable'):
561 f_mode = filemode_executable
562
563 f_is_link = False
564 if prop_list.get('svn:special'):
565 f_is_link = True
566
567 data = {
568 'is_link': f_is_link,
569 'mode': f_mode,
570 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
571 }
572
573 yield f_path, data, f_type
574
575 def file_walker(_commit_id, path):
576 repo = self._factory.repo(wire)
577 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
578
579 def no_content():
580 raise NoContentException()
581
582 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
583 file_path = f_name
584
585 if f_type == 'dir':
586 mode = f_data['mode']
587 yield ArchiveNode(file_path, mode, False, no_content)
588 else:
589 mode = f_data['mode']
590 is_link = f_data['is_link']
591 data_stream = f_data['content_stream']
592 yield ArchiveNode(file_path, mode, is_link, data_stream)
593
594 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
595 archive_dir_name, commit_id)
596
531
597
532 class SvnDiffer(object):
598 class SvnDiffer(object):
533 """
599 """
534 Utility to create diffs based on difflib and the Subversion api
600 Utility to create diffs based on difflib and the Subversion api
535 """
601 """
536
602
537 binary_content = False
603 binary_content = False
538
604
539 def __init__(
605 def __init__(
540 self, repo, src_rev, src_path, tgt_rev, tgt_path,
606 self, repo, src_rev, src_path, tgt_rev, tgt_path,
541 ignore_whitespace, context):
607 ignore_whitespace, context):
542 self.repo = repo
608 self.repo = repo
543 self.ignore_whitespace = ignore_whitespace
609 self.ignore_whitespace = ignore_whitespace
544 self.context = context
610 self.context = context
545
611
546 fsobj = svn.repos.fs(repo)
612 fsobj = svn.repos.fs(repo)
547
613
548 self.tgt_rev = tgt_rev
614 self.tgt_rev = tgt_rev
549 self.tgt_path = tgt_path or ''
615 self.tgt_path = tgt_path or ''
550 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
616 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
551 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
617 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
552
618
553 self.src_rev = src_rev
619 self.src_rev = src_rev
554 self.src_path = src_path or self.tgt_path
620 self.src_path = src_path or self.tgt_path
555 self.src_root = svn.fs.revision_root(fsobj, src_rev)
621 self.src_root = svn.fs.revision_root(fsobj, src_rev)
556 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
622 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
557
623
558 self._validate()
624 self._validate()
559
625
560 def _validate(self):
626 def _validate(self):
561 if (self.tgt_kind != svn.core.svn_node_none and
627 if (self.tgt_kind != svn.core.svn_node_none and
562 self.src_kind != svn.core.svn_node_none and
628 self.src_kind != svn.core.svn_node_none and
563 self.src_kind != self.tgt_kind):
629 self.src_kind != self.tgt_kind):
564 # TODO: johbo: proper error handling
630 # TODO: johbo: proper error handling
565 raise Exception(
631 raise Exception(
566 "Source and target are not compatible for diff generation. "
632 "Source and target are not compatible for diff generation. "
567 "Source type: %s, target type: %s" %
633 "Source type: %s, target type: %s" %
568 (self.src_kind, self.tgt_kind))
634 (self.src_kind, self.tgt_kind))
569
635
570 def generate_diff(self):
636 def generate_diff(self):
571 buf = StringIO.StringIO()
637 buf = StringIO.StringIO()
572 if self.tgt_kind == svn.core.svn_node_dir:
638 if self.tgt_kind == svn.core.svn_node_dir:
573 self._generate_dir_diff(buf)
639 self._generate_dir_diff(buf)
574 else:
640 else:
575 self._generate_file_diff(buf)
641 self._generate_file_diff(buf)
576 return buf.getvalue()
642 return buf.getvalue()
577
643
578 def _generate_dir_diff(self, buf):
644 def _generate_dir_diff(self, buf):
579 editor = DiffChangeEditor()
645 editor = DiffChangeEditor()
580 editor_ptr, editor_baton = svn.delta.make_editor(editor)
646 editor_ptr, editor_baton = svn.delta.make_editor(editor)
581 svn.repos.dir_delta2(
647 svn.repos.dir_delta2(
582 self.src_root,
648 self.src_root,
583 self.src_path,
649 self.src_path,
584 '', # src_entry
650 '', # src_entry
585 self.tgt_root,
651 self.tgt_root,
586 self.tgt_path,
652 self.tgt_path,
587 editor_ptr, editor_baton,
653 editor_ptr, editor_baton,
588 authorization_callback_allow_all,
654 authorization_callback_allow_all,
589 False, # text_deltas
655 False, # text_deltas
590 svn.core.svn_depth_infinity, # depth
656 svn.core.svn_depth_infinity, # depth
591 False, # entry_props
657 False, # entry_props
592 False, # ignore_ancestry
658 False, # ignore_ancestry
593 )
659 )
594
660
595 for path, __, change in sorted(editor.changes):
661 for path, __, change in sorted(editor.changes):
596 self._generate_node_diff(
662 self._generate_node_diff(
597 buf, change, path, self.tgt_path, path, self.src_path)
663 buf, change, path, self.tgt_path, path, self.src_path)
598
664
599 def _generate_file_diff(self, buf):
665 def _generate_file_diff(self, buf):
600 change = None
666 change = None
601 if self.src_kind == svn.core.svn_node_none:
667 if self.src_kind == svn.core.svn_node_none:
602 change = "add"
668 change = "add"
603 elif self.tgt_kind == svn.core.svn_node_none:
669 elif self.tgt_kind == svn.core.svn_node_none:
604 change = "delete"
670 change = "delete"
605 tgt_base, tgt_path = vcspath.split(self.tgt_path)
671 tgt_base, tgt_path = vcspath.split(self.tgt_path)
606 src_base, src_path = vcspath.split(self.src_path)
672 src_base, src_path = vcspath.split(self.src_path)
607 self._generate_node_diff(
673 self._generate_node_diff(
608 buf, change, tgt_path, tgt_base, src_path, src_base)
674 buf, change, tgt_path, tgt_base, src_path, src_base)
609
675
610 def _generate_node_diff(
676 def _generate_node_diff(
611 self, buf, change, tgt_path, tgt_base, src_path, src_base):
677 self, buf, change, tgt_path, tgt_base, src_path, src_base):
612
678
613 if self.src_rev == self.tgt_rev and tgt_base == src_base:
679 if self.src_rev == self.tgt_rev and tgt_base == src_base:
614 # makes consistent behaviour with git/hg to return empty diff if
680 # makes consistent behaviour with git/hg to return empty diff if
615 # we compare same revisions
681 # we compare same revisions
616 return
682 return
617
683
618 tgt_full_path = vcspath.join(tgt_base, tgt_path)
684 tgt_full_path = vcspath.join(tgt_base, tgt_path)
619 src_full_path = vcspath.join(src_base, src_path)
685 src_full_path = vcspath.join(src_base, src_path)
620
686
621 self.binary_content = False
687 self.binary_content = False
622 mime_type = self._get_mime_type(tgt_full_path)
688 mime_type = self._get_mime_type(tgt_full_path)
623
689
624 if mime_type and not mime_type.startswith('text'):
690 if mime_type and not mime_type.startswith('text'):
625 self.binary_content = True
691 self.binary_content = True
626 buf.write("=" * 67 + '\n')
692 buf.write("=" * 67 + '\n')
627 buf.write("Cannot display: file marked as a binary type.\n")
693 buf.write("Cannot display: file marked as a binary type.\n")
628 buf.write("svn:mime-type = %s\n" % mime_type)
694 buf.write("svn:mime-type = %s\n" % mime_type)
629 buf.write("Index: %s\n" % (tgt_path, ))
695 buf.write("Index: %s\n" % (tgt_path, ))
630 buf.write("=" * 67 + '\n')
696 buf.write("=" * 67 + '\n')
631 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
697 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
632 'tgt_path': tgt_path})
698 'tgt_path': tgt_path})
633
699
634 if change == 'add':
700 if change == 'add':
635 # TODO: johbo: SVN is missing a zero here compared to git
701 # TODO: johbo: SVN is missing a zero here compared to git
636 buf.write("new file mode 10644\n")
702 buf.write("new file mode 10644\n")
637
703
638 #TODO(marcink): intro to binary detection of svn patches
704 #TODO(marcink): intro to binary detection of svn patches
639 # if self.binary_content:
705 # if self.binary_content:
640 # buf.write('GIT binary patch\n')
706 # buf.write('GIT binary patch\n')
641
707
642 buf.write("--- /dev/null\t(revision 0)\n")
708 buf.write("--- /dev/null\t(revision 0)\n")
643 src_lines = []
709 src_lines = []
644 else:
710 else:
645 if change == 'delete':
711 if change == 'delete':
646 buf.write("deleted file mode 10644\n")
712 buf.write("deleted file mode 10644\n")
647
713
648 #TODO(marcink): intro to binary detection of svn patches
714 #TODO(marcink): intro to binary detection of svn patches
649 # if self.binary_content:
715 # if self.binary_content:
650 # buf.write('GIT binary patch\n')
716 # buf.write('GIT binary patch\n')
651
717
652 buf.write("--- a/%s\t(revision %s)\n" % (
718 buf.write("--- a/%s\t(revision %s)\n" % (
653 src_path, self.src_rev))
719 src_path, self.src_rev))
654 src_lines = self._svn_readlines(self.src_root, src_full_path)
720 src_lines = self._svn_readlines(self.src_root, src_full_path)
655
721
656 if change == 'delete':
722 if change == 'delete':
657 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
723 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
658 tgt_lines = []
724 tgt_lines = []
659 else:
725 else:
660 buf.write("+++ b/%s\t(revision %s)\n" % (
726 buf.write("+++ b/%s\t(revision %s)\n" % (
661 tgt_path, self.tgt_rev))
727 tgt_path, self.tgt_rev))
662 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
728 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
663
729
664 if not self.binary_content:
730 if not self.binary_content:
665 udiff = svn_diff.unified_diff(
731 udiff = svn_diff.unified_diff(
666 src_lines, tgt_lines, context=self.context,
732 src_lines, tgt_lines, context=self.context,
667 ignore_blank_lines=self.ignore_whitespace,
733 ignore_blank_lines=self.ignore_whitespace,
668 ignore_case=False,
734 ignore_case=False,
669 ignore_space_changes=self.ignore_whitespace)
735 ignore_space_changes=self.ignore_whitespace)
670 buf.writelines(udiff)
736 buf.writelines(udiff)
671
737
672 def _get_mime_type(self, path):
738 def _get_mime_type(self, path):
673 try:
739 try:
674 mime_type = svn.fs.node_prop(
740 mime_type = svn.fs.node_prop(
675 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
741 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
676 except svn.core.SubversionException:
742 except svn.core.SubversionException:
677 mime_type = svn.fs.node_prop(
743 mime_type = svn.fs.node_prop(
678 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
744 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
679 return mime_type
745 return mime_type
680
746
681 def _svn_readlines(self, fs_root, node_path):
747 def _svn_readlines(self, fs_root, node_path):
682 if self.binary_content:
748 if self.binary_content:
683 return []
749 return []
684 node_kind = svn.fs.check_path(fs_root, node_path)
750 node_kind = svn.fs.check_path(fs_root, node_path)
685 if node_kind not in (
751 if node_kind not in (
686 svn.core.svn_node_file, svn.core.svn_node_symlink):
752 svn.core.svn_node_file, svn.core.svn_node_symlink):
687 return []
753 return []
688 content = svn.core.Stream(
754 content = svn.core.Stream(svn.fs.file_contents(fs_root, node_path)).read()
689 svn.fs.file_contents(fs_root, node_path)).read()
690 return content.splitlines(True)
755 return content.splitlines(True)
691
756
692
757
693 class DiffChangeEditor(svn.delta.Editor):
758 class DiffChangeEditor(svn.delta.Editor):
694 """
759 """
695 Records changes between two given revisions
760 Records changes between two given revisions
696 """
761 """
697
762
698 def __init__(self):
763 def __init__(self):
699 self.changes = []
764 self.changes = []
700
765
701 def delete_entry(self, path, revision, parent_baton, pool=None):
766 def delete_entry(self, path, revision, parent_baton, pool=None):
702 self.changes.append((path, None, 'delete'))
767 self.changes.append((path, None, 'delete'))
703
768
704 def add_file(
769 def add_file(
705 self, path, parent_baton, copyfrom_path, copyfrom_revision,
770 self, path, parent_baton, copyfrom_path, copyfrom_revision,
706 file_pool=None):
771 file_pool=None):
707 self.changes.append((path, 'file', 'add'))
772 self.changes.append((path, 'file', 'add'))
708
773
709 def open_file(self, path, parent_baton, base_revision, file_pool=None):
774 def open_file(self, path, parent_baton, base_revision, file_pool=None):
710 self.changes.append((path, 'file', 'change'))
775 self.changes.append((path, 'file', 'change'))
711
776
712
777
713 def authorization_callback_allow_all(root, path, pool):
778 def authorization_callback_allow_all(root, path, pool):
714 return True
779 return True
715
780
716
781
717 class TxnNodeProcessor(object):
782 class TxnNodeProcessor(object):
718 """
783 """
719 Utility to process the change of one node within a transaction root.
784 Utility to process the change of one node within a transaction root.
720
785
721 It encapsulates the knowledge of how to add, update or remove
786 It encapsulates the knowledge of how to add, update or remove
722 a node for a given transaction root. The purpose is to support the method
787 a node for a given transaction root. The purpose is to support the method
723 `SvnRemote.commit`.
788 `SvnRemote.commit`.
724 """
789 """
725
790
726 def __init__(self, node, txn_root):
791 def __init__(self, node, txn_root):
727 assert isinstance(node['path'], str)
792 assert isinstance(node['path'], str)
728
793
729 self.node = node
794 self.node = node
730 self.txn_root = txn_root
795 self.txn_root = txn_root
731
796
732 def update(self):
797 def update(self):
733 self._ensure_parent_dirs()
798 self._ensure_parent_dirs()
734 self._add_file_if_node_does_not_exist()
799 self._add_file_if_node_does_not_exist()
735 self._update_file_content()
800 self._update_file_content()
736 self._update_file_properties()
801 self._update_file_properties()
737
802
738 def remove(self):
803 def remove(self):
739 svn.fs.delete(self.txn_root, self.node['path'])
804 svn.fs.delete(self.txn_root, self.node['path'])
740 # TODO: Clean up directory if empty
805 # TODO: Clean up directory if empty
741
806
742 def _ensure_parent_dirs(self):
807 def _ensure_parent_dirs(self):
743 curdir = vcspath.dirname(self.node['path'])
808 curdir = vcspath.dirname(self.node['path'])
744 dirs_to_create = []
809 dirs_to_create = []
745 while not self._svn_path_exists(curdir):
810 while not self._svn_path_exists(curdir):
746 dirs_to_create.append(curdir)
811 dirs_to_create.append(curdir)
747 curdir = vcspath.dirname(curdir)
812 curdir = vcspath.dirname(curdir)
748
813
749 for curdir in reversed(dirs_to_create):
814 for curdir in reversed(dirs_to_create):
750 log.debug('Creating missing directory "%s"', curdir)
815 log.debug('Creating missing directory "%s"', curdir)
751 svn.fs.make_dir(self.txn_root, curdir)
816 svn.fs.make_dir(self.txn_root, curdir)
752
817
753 def _svn_path_exists(self, path):
818 def _svn_path_exists(self, path):
754 path_status = svn.fs.check_path(self.txn_root, path)
819 path_status = svn.fs.check_path(self.txn_root, path)
755 return path_status != svn.core.svn_node_none
820 return path_status != svn.core.svn_node_none
756
821
757 def _add_file_if_node_does_not_exist(self):
822 def _add_file_if_node_does_not_exist(self):
758 kind = svn.fs.check_path(self.txn_root, self.node['path'])
823 kind = svn.fs.check_path(self.txn_root, self.node['path'])
759 if kind == svn.core.svn_node_none:
824 if kind == svn.core.svn_node_none:
760 svn.fs.make_file(self.txn_root, self.node['path'])
825 svn.fs.make_file(self.txn_root, self.node['path'])
761
826
762 def _update_file_content(self):
827 def _update_file_content(self):
763 assert isinstance(self.node['content'], str)
828 assert isinstance(self.node['content'], str)
764 handler, baton = svn.fs.apply_textdelta(
829 handler, baton = svn.fs.apply_textdelta(
765 self.txn_root, self.node['path'], None, None)
830 self.txn_root, self.node['path'], None, None)
766 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
831 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
767
832
768 def _update_file_properties(self):
833 def _update_file_properties(self):
769 properties = self.node.get('properties', {})
834 properties = self.node.get('properties', {})
770 for key, value in properties.iteritems():
835 for key, value in properties.iteritems():
771 svn.fs.change_node_prop(
836 svn.fs.change_node_prop(
772 self.txn_root, self.node['path'], key, value)
837 self.txn_root, self.node['path'], key, value)
773
838
774
839
775 def apr_time_t(timestamp):
840 def apr_time_t(timestamp):
776 """
841 """
777 Convert a Python timestamp into APR timestamp type apr_time_t
842 Convert a Python timestamp into APR timestamp type apr_time_t
778 """
843 """
779 return timestamp * 1E6
844 return timestamp * 1E6
780
845
781
846
782 def svn_opt_revision_value_t(num):
847 def svn_opt_revision_value_t(num):
783 """
848 """
784 Put `num` into a `svn_opt_revision_value_t` structure.
849 Put `num` into a `svn_opt_revision_value_t` structure.
785 """
850 """
786 value = svn.core.svn_opt_revision_value_t()
851 value = svn.core.svn_opt_revision_value_t()
787 value.number = num
852 value.number = num
788 revision = svn.core.svn_opt_revision_t()
853 revision = svn.core.svn_opt_revision_t()
789 revision.kind = svn.core.svn_opt_revision_number
854 revision.kind = svn.core.svn_opt_revision_number
790 revision.value = value
855 revision.value = value
791 return revision
856 return revision
General Comments 0
You need to be logged in to leave comments. Login now