##// END OF EJS Templates
svn: enable hooks and integration framework execution....
marcink -
r2677:25d65914 default
parent child Browse files
Show More
@@ -16,9 +16,6 b' recursive-include configs *'
16 # translations
16 # translations
17 recursive-include rhodecode/i18n *
17 recursive-include rhodecode/i18n *
18
18
19 # hook templates
20 recursive-include rhodecode/config/hook_templates *
21
22 # non-python core stuff
19 # non-python core stuff
23 recursive-include rhodecode *.cfg
20 recursive-include rhodecode *.cfg
24 recursive-include rhodecode *.json
21 recursive-include rhodecode *.json
@@ -42,7 +42,8 b' class TestGetRepoChangeset(object):'
42 if details == 'full':
42 if details == 'full':
43 assert result['refs']['bookmarks'] == getattr(
43 assert result['refs']['bookmarks'] == getattr(
44 commit, 'bookmarks', [])
44 commit, 'bookmarks', [])
45 assert result['refs']['branches'] == [commit.branch]
45 branches = [commit.branch] if commit.branch else []
46 assert result['refs']['branches'] == branches
46 assert result['refs']['tags'] == commit.tags
47 assert result['refs']['tags'] == commit.tags
47
48
48 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
49 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
@@ -18,10 +18,13 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import os
22 import time
22 import logging
23 import logging
24 import tempfile
23 import traceback
25 import traceback
24 import threading
26 import threading
27
25 from BaseHTTPServer import BaseHTTPRequestHandler
28 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
29 from SocketServer import TCPServer
27
30
@@ -30,14 +33,28 b' from rhodecode.model import meta'
30 from rhodecode.lib.base import bootstrap_request, bootstrap_config
33 from rhodecode.lib.base import bootstrap_request, bootstrap_config
31 from rhodecode.lib import hooks_base
34 from rhodecode.lib import hooks_base
32 from rhodecode.lib.utils2 import AttributeDict
35 from rhodecode.lib.utils2 import AttributeDict
36 from rhodecode.lib.ext_json import json
33
37
34
38
35 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
36
40
37
41
38 class HooksHttpHandler(BaseHTTPRequestHandler):
42 class HooksHttpHandler(BaseHTTPRequestHandler):
43
39 def do_POST(self):
44 def do_POST(self):
40 method, extras = self._read_request()
45 method, extras = self._read_request()
46 txn_id = getattr(self.server, 'txn_id', None)
47 if txn_id:
48 from rhodecode.lib.caches import compute_key_from_params
49 log.debug('Computing TXN_ID based on `%s`:`%s`',
50 extras['repository'], extras['txn_id'])
51 computed_txn_id = compute_key_from_params(
52 extras['repository'], extras['txn_id'])
53 if txn_id != computed_txn_id:
54 raise Exception(
55 'TXN ID fail: expected {} got {} instead'.format(
56 txn_id, computed_txn_id))
57
41 try:
58 try:
42 result = self._call_hook(method, extras)
59 result = self._call_hook(method, extras)
43 except Exception as e:
60 except Exception as e:
@@ -77,13 +94,14 b' class HooksHttpHandler(BaseHTTPRequestHa'
77
94
78 message = format % args
95 message = format % args
79
96
80 # TODO: mikhail: add different log levels support
81 log.debug(
97 log.debug(
82 "%s - - [%s] %s", self.client_address[0],
98 "%s - - [%s] %s", self.client_address[0],
83 self.log_date_time_string(), message)
99 self.log_date_time_string(), message)
84
100
85
101
86 class DummyHooksCallbackDaemon(object):
102 class DummyHooksCallbackDaemon(object):
103 hooks_uri = ''
104
87 def __init__(self):
105 def __init__(self):
88 self.hooks_module = Hooks.__module__
106 self.hooks_module = Hooks.__module__
89
107
@@ -101,8 +119,8 b' class ThreadedHookCallbackDaemon(object)'
101 _daemon = None
119 _daemon = None
102 _done = False
120 _done = False
103
121
104 def __init__(self):
122 def __init__(self, txn_id=None, port=None):
105 self._prepare()
123 self._prepare(txn_id=txn_id, port=port)
106
124
107 def __enter__(self):
125 def __enter__(self):
108 self._run()
126 self._run()
@@ -112,7 +130,7 b' class ThreadedHookCallbackDaemon(object)'
112 log.debug('Callback daemon exiting now...')
130 log.debug('Callback daemon exiting now...')
113 self._stop()
131 self._stop()
114
132
115 def _prepare(self):
133 def _prepare(self, txn_id=None, port=None):
116 raise NotImplementedError()
134 raise NotImplementedError()
117
135
118 def _run(self):
136 def _run(self):
@@ -135,15 +153,18 b' class HttpHooksCallbackDaemon(ThreadedHo'
135 # request and wastes cpu at all other times.
153 # request and wastes cpu at all other times.
136 POLL_INTERVAL = 0.01
154 POLL_INTERVAL = 0.01
137
155
138 def _prepare(self):
156 def _prepare(self, txn_id=None, port=None):
139 log.debug("Preparing HTTP callback daemon and registering hook object")
140
141 self._done = False
157 self._done = False
142 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
158 self._daemon = TCPServer((self.IP_ADDRESS, port or 0), HooksHttpHandler)
143 _, port = self._daemon.server_address
159 _, port = self._daemon.server_address
144 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
160 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
161 self.txn_id = txn_id
162 # inject transaction_id for later verification
163 self._daemon.txn_id = self.txn_id
145
164
146 log.debug("Hooks uri is: %s", self.hooks_uri)
165 log.debug(
166 "Preparing HTTP callback daemon at `%s` and registering hook object",
167 self.hooks_uri)
147
168
148 def _run(self):
169 def _run(self):
149 log.debug("Running event loop of callback daemon in background thread")
170 log.debug("Running event loop of callback daemon in background thread")
@@ -160,26 +181,67 b' class HttpHooksCallbackDaemon(ThreadedHo'
160 self._callback_thread.join()
181 self._callback_thread.join()
161 self._daemon = None
182 self._daemon = None
162 self._callback_thread = None
183 self._callback_thread = None
184 if self.txn_id:
185 txn_id_file = get_txn_id_data_path(self.txn_id)
186 log.debug('Cleaning up TXN ID %s', txn_id_file)
187 if os.path.isfile(txn_id_file):
188 os.remove(txn_id_file)
189
163 log.debug("Background thread done.")
190 log.debug("Background thread done.")
164
191
165
192
166 def prepare_callback_daemon(extras, protocol, use_direct_calls):
193 def get_txn_id_data_path(txn_id):
167 callback_daemon = None
194 root = tempfile.gettempdir()
195 return os.path.join(root, 'rc_txn_id_{}'.format(txn_id))
196
197
198 def store_txn_id_data(txn_id, data_dict):
199 if not txn_id:
200 log.warning('Cannot store txn_id because it is empty')
201 return
202
203 path = get_txn_id_data_path(txn_id)
204 try:
205 with open(path, 'wb') as f:
206 f.write(json.dumps(data_dict))
207 except Exception:
208 log.exception('Failed to write txn_id metadata')
168
209
210
211 def get_txn_id_from_store(txn_id):
212 """
213 Reads txn_id from store and if present returns the data for callback manager
214 """
215 path = get_txn_id_data_path(txn_id)
216 try:
217 with open(path, 'rb') as f:
218 return json.loads(f.read())
219 except Exception:
220 return {}
221
222
223 def prepare_callback_daemon(extras, protocol, use_direct_calls, txn_id=None):
224 txn_details = get_txn_id_from_store(txn_id)
225 port = txn_details.get('port', 0)
169 if use_direct_calls:
226 if use_direct_calls:
170 callback_daemon = DummyHooksCallbackDaemon()
227 callback_daemon = DummyHooksCallbackDaemon()
171 extras['hooks_module'] = callback_daemon.hooks_module
228 extras['hooks_module'] = callback_daemon.hooks_module
172 else:
229 else:
173 if protocol == 'http':
230 if protocol == 'http':
174 callback_daemon = HttpHooksCallbackDaemon()
231 callback_daemon = HttpHooksCallbackDaemon(txn_id=txn_id, port=port)
175 else:
232 else:
176 log.error('Unsupported callback daemon protocol "%s"', protocol)
233 log.error('Unsupported callback daemon protocol "%s"', protocol)
177 raise Exception('Unsupported callback daemon protocol.')
234 raise Exception('Unsupported callback daemon protocol.')
178
235
179 extras['hooks_uri'] = callback_daemon.hooks_uri
236 extras['hooks_uri'] = callback_daemon.hooks_uri
180 extras['hooks_protocol'] = protocol
237 extras['hooks_protocol'] = protocol
238 extras['time'] = time.time()
181
239
182 log.debug('Prepared a callback daemon: %s', callback_daemon)
240 # register txn_id
241 extras['txn_id'] = txn_id
242
243 log.debug('Prepared a callback daemon: %s at url `%s`',
244 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
183 return callback_daemon, extras
245 return callback_daemon, extras
184
246
185
247
@@ -18,17 +18,21 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import logging
22 import logging
22 import urllib
23 import urllib
23 from urlparse import urljoin
24 from urlparse import urljoin
24
25
25
26 import requests
26 import requests
27 from webob.exc import HTTPNotAcceptable
27 from webob.exc import HTTPNotAcceptable
28
28
29 from rhodecode.lib import caches
29 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool, safe_int
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
32
36
33 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
34
38
@@ -39,7 +43,6 b' class SimpleSvnApp(object):'
39 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
40 rc_extras = {}
44 rc_extras = {}
41
45
42
43 def __init__(self, config):
46 def __init__(self, config):
44 self.config = config
47 self.config = config
45
48
@@ -52,9 +55,19 b' class SimpleSvnApp(object):'
52 # length, then we should transfer the payload in one request.
55 # length, then we should transfer the payload in one request.
53 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
56 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
54 data = data.read()
57 data = data.read()
58 if data.startswith('(create-txn-with-props'):
59 # store on-the-fly our rc_extra using svn revision properties
60 # those can be read later on in hooks executed so we have a way
61 # to pass in the data into svn hooks
62 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
63 rc_data_len = len(rc_data)
64 # header defines data lenght, and serialized data
65 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
66 data = data[:-2] + skel + '))'
55
67
56 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
68 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
57 self._get_url(environ['PATH_INFO']))
69 self._get_url(environ['PATH_INFO']))
70
58 response = requests.request(
71 response = requests.request(
59 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
72 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
60 data=data, headers=request_headers)
73 data=data, headers=request_headers)
@@ -70,6 +83,14 b' class SimpleSvnApp(object):'
70 log.debug('got response code: %s', response.status_code)
83 log.debug('got response code: %s', response.status_code)
71
84
72 response_headers = self._get_response_headers(response.headers)
85 response_headers = self._get_response_headers(response.headers)
86
87 if response.headers.get('SVN-Txn-name'):
88 svn_tx_id = response.headers.get('SVN-Txn-name')
89 txn_id = caches.compute_key_from_params(
90 self.config['repository'], svn_tx_id)
91 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
92 store_txn_id_data(txn_id, {'port': port})
93
73 start_response(
94 start_response(
74 '{} {}'.format(response.status_code, response.reason),
95 '{} {}'.format(response.status_code, response.reason),
75 response_headers)
96 response_headers)
@@ -156,6 +177,14 b' class SimpleSvn(simplevcs.SimpleVCS):'
156 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
177 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
157 else 'push')
178 else 'push')
158
179
180 def _should_use_callback_daemon(self, extras, environ, action):
181 # only MERGE command triggers hooks, so we don't want to start
182 # hooks server too many times. POST however starts the svn transaction
183 # so we also need to run the init of callback daemon of POST
184 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
185 return True
186 return False
187
159 def _create_wsgi_app(self, repo_path, repo_name, config):
188 def _create_wsgi_app(self, repo_path, repo_name, config):
160 if self._is_svn_enabled():
189 if self._is_svn_enabled():
161 return SimpleSvnApp(config)
190 return SimpleSvnApp(config)
@@ -28,10 +28,12 b' import re'
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31 from StringIO import StringIO
32 from lxml import etree
31
33
32 import time
34 import time
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 # TODO(marcink): check if we should use webob.exc here ?
36
35 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
@@ -43,9 +45,7 b' from rhodecode.lib import caches'
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.base import (
46 from rhodecode.lib.base import (
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 from rhodecode.lib.exceptions import (
48 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
48 NotAllowedToCreateUserError)
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
50 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware import appenlight
51 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.middleware.utils import scm_app_http
@@ -53,6 +53,7 b' from rhodecode.lib.utils import is_valid'
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 from rhodecode.lib.vcs.backends import base
55 from rhodecode.lib.vcs.backends import base
56
56 from rhodecode.model import meta
57 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
@@ -62,6 +63,28 b' from rhodecode.model.settings import Set'
62 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
63
64
64
65
66 def extract_svn_txn_id(acl_repo_name, data):
67 """
68 Helper method for extraction of svn txn_id from submited XML data during
69 POST operations
70 """
71 try:
72 root = etree.fromstring(data)
73 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 for el in root:
75 if el.tag == '{DAV:}source':
76 for sub_el in el:
77 if sub_el.tag == '{DAV:}href':
78 match = pat.search(sub_el.text)
79 if match:
80 svn_tx_id = match.groupdict()['txn_id']
81 txn_id = caches.compute_key_from_params(
82 acl_repo_name, svn_tx_id)
83 return txn_id
84 except Exception:
85 log.exception('Failed to extract txn_id')
86
87
65 def initialize_generator(factory):
88 def initialize_generator(factory):
66 """
89 """
67 Initializes the returned generator by draining its first element.
90 Initializes the returned generator by draining its first element.
@@ -565,48 +588,42 b' class SimpleVCS(object):'
565 also handles the locking exceptions which will be triggered when
588 also handles the locking exceptions which will be triggered when
566 the first chunk is produced by the underlying WSGI application.
589 the first chunk is produced by the underlying WSGI application.
567 """
590 """
568 callback_daemon, extras = self._prepare_callback_daemon(extras)
591 txn_id = ''
569 config = self._create_config(extras, self.acl_repo_name)
592 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
570 log.debug('HOOKS extras is %s', extras)
593 # case for SVN, we want to re-use the callback daemon port
571 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
594 # so we use the txn_id, for this we peek the body, and still save
572 app.rc_extras = extras
595 # it as wsgi.input
596 data = environ['wsgi.input'].read()
597 environ['wsgi.input'] = StringIO(data)
598 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
573
599
574 try:
600 callback_daemon, extras = self._prepare_callback_daemon(
575 with callback_daemon:
601 extras, environ, action, txn_id=txn_id)
576 try:
602 log.debug('HOOKS extras is %s', extras)
577 response = app(environ, start_response)
603
578 finally:
604 config = self._create_config(extras, self.acl_repo_name)
579 # This statement works together with the decorator
605 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
580 # "initialize_generator" above. The decorator ensures that
606 with callback_daemon:
581 # we hit the first yield statement before the generator is
607 app.rc_extras = extras
582 # returned back to the WSGI server. This is needed to
583 # ensure that the call to "app" above triggers the
584 # needed callback to "start_response" before the
585 # generator is actually used.
586 yield "__init__"
587
608
588 for chunk in response:
609 try:
589 yield chunk
610 response = app(environ, start_response)
590 except Exception as exc:
611 finally:
591 # TODO: martinb: Exceptions are only raised in case of the Pyro4
612 # This statement works together with the decorator
592 # backend. Refactor this except block after dropping Pyro4 support.
613 # "initialize_generator" above. The decorator ensures that
593 # TODO: johbo: Improve "translating" back the exception.
614 # we hit the first yield statement before the generator is
594 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
615 # returned back to the WSGI server. This is needed to
595 exc = HTTPLockedRC(*exc.args)
616 # ensure that the call to "app" above triggers the
596 _code = rhodecode.CONFIG.get('lock_ret_code')
617 # needed callback to "start_response" before the
597 log.debug('Repository LOCKED ret code %s!', (_code,))
618 # generator is actually used.
598 elif getattr(exc, '_vcs_kind', None) == 'requirement':
619 yield "__init__"
599 log.debug(
600 'Repository requires features unknown to this Mercurial')
601 exc = HTTPRequirementError(*exc.args)
602 else:
603 raise
604
620
605 for chunk in exc(environ, start_response):
621 # iter content
622 for chunk in response:
606 yield chunk
623 yield chunk
607 finally:
624
608 # invalidate cache on push
609 try:
625 try:
626 # invalidate cache on push
610 if action == 'push':
627 if action == 'push':
611 self._invalidate_cache(self.url_repo_name)
628 self._invalidate_cache(self.url_repo_name)
612 finally:
629 finally:
@@ -634,10 +651,18 b' class SimpleVCS(object):'
634 """Create a safe config representation."""
651 """Create a safe config representation."""
635 raise NotImplementedError()
652 raise NotImplementedError()
636
653
637 def _prepare_callback_daemon(self, extras):
654 def _should_use_callback_daemon(self, extras, environ, action):
655 return True
656
657 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
658 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
659 if not self._should_use_callback_daemon(extras, environ, action):
660 # disable callback daemon for actions that don't require it
661 direct_calls = True
662
638 return prepare_callback_daemon(
663 return prepare_callback_daemon(
639 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
664 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
640 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
665 use_direct_calls=direct_calls, txn_id=txn_id)
641
666
642
667
643 def _should_check_locking(query_string):
668 def _should_check_locking(query_string):
@@ -514,7 +514,6 b' def repo2db_mapper(initial_repo_list, re'
514 :param remove_obsolete: check for obsolete entries in database
514 :param remove_obsolete: check for obsolete entries in database
515 """
515 """
516 from rhodecode.model.repo import RepoModel
516 from rhodecode.model.repo import RepoModel
517 from rhodecode.model.scm import ScmModel
518 from rhodecode.model.repo_group import RepoGroupModel
517 from rhodecode.model.repo_group import RepoGroupModel
519 from rhodecode.model.settings import SettingsModel
518 from rhodecode.model.settings import SettingsModel
520
519
@@ -566,9 +565,8 b' def repo2db_mapper(initial_repo_list, re'
566
565
567 config = db_repo._config
566 config = db_repo._config
568 config.set('extensions', 'largefiles', '')
567 config.set('extensions', 'largefiles', '')
569 ScmModel().install_hooks(
568 repo = db_repo.scm_instance(config=config)
570 db_repo.scm_instance(config=config),
569 repo.install_hooks()
571 repo_type=db_repo.repo_type)
572
570
573 removed = []
571 removed = []
574 if remove_obsolete:
572 if remove_obsolete:
@@ -175,6 +175,7 b' class BaseRepository(object):'
175 EMPTY_COMMIT_ID = '0' * 40
175 EMPTY_COMMIT_ID = '0' * 40
176
176
177 path = None
177 path = None
178 _remote = None
178
179
179 def __init__(self, repo_path, config=None, create=False, **kwargs):
180 def __init__(self, repo_path, config=None, create=False, **kwargs):
180 """
181 """
@@ -648,6 +649,9 b' class BaseRepository(object):'
648 """
649 """
649 return None
650 return None
650
651
652 def install_hooks(self, force=False):
653 return self._remote.install_hooks(force)
654
651
655
652 class BaseCommit(object):
656 class BaseCommit(object):
653 """
657 """
@@ -743,7 +747,7 b' class BaseCommit(object):'
743
747
744 def _get_refs(self):
748 def _get_refs(self):
745 return {
749 return {
746 'branches': [self.branch],
750 'branches': [self.branch] if self.branch else [],
747 'bookmarks': getattr(self, 'bookmarks', []),
751 'bookmarks': getattr(self, 'bookmarks', []),
748 'tags': self.tags
752 'tags': self.tags
749 }
753 }
@@ -858,7 +858,7 b' class RepoModel(BaseModel):'
858 repo = backend(
858 repo = backend(
859 repo_path, config=config, create=True, src_url=clone_uri)
859 repo_path, config=config, create=True, src_url=clone_uri)
860
860
861 ScmModel().install_hooks(repo, repo_type=repo_type)
861 repo.install_hooks()
862
862
863 log.debug('Created repo %s with %s backend',
863 log.debug('Created repo %s with %s backend',
864 safe_unicode(repo_name), safe_unicode(repo_type))
864 safe_unicode(repo_name), safe_unicode(repo_type))
@@ -807,116 +807,6 b' class ScmModel(BaseModel):'
807
807
808 return choices, hist_l
808 return choices, hist_l
809
809
810 def install_git_hook(self, repo, force_create=False):
811 """
812 Creates a rhodecode hook inside a git repository
813
814 :param repo: Instance of VCS repo
815 :param force_create: Create even if same name hook exists
816 """
817
818 loc = os.path.join(repo.path, 'hooks')
819 if not repo.bare:
820 loc = os.path.join(repo.path, '.git', 'hooks')
821 if not os.path.isdir(loc):
822 os.makedirs(loc, mode=0777)
823
824 tmpl_post = pkg_resources.resource_string(
825 'rhodecode', '/'.join(
826 ('config', 'hook_templates', 'git_post_receive.py.tmpl')))
827 tmpl_pre = pkg_resources.resource_string(
828 'rhodecode', '/'.join(
829 ('config', 'hook_templates', 'git_pre_receive.py.tmpl')))
830
831 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
832 _hook_file = os.path.join(loc, '%s-receive' % h_type)
833 log.debug('Installing git hook in repo %s', repo)
834 _rhodecode_hook = _check_rhodecode_hook(_hook_file)
835
836 if _rhodecode_hook or force_create:
837 log.debug('writing %s hook file !', h_type)
838 try:
839 with open(_hook_file, 'wb') as f:
840 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
841 tmpl = tmpl.replace('_ENV_', sys.executable)
842 f.write(tmpl)
843 os.chmod(_hook_file, 0755)
844 except IOError:
845 log.exception('error writing hook file %s', _hook_file)
846 else:
847 log.debug('skipping writing hook file')
848
849 def install_svn_hooks(self, repo, force_create=False):
850 """
851 Creates rhodecode hooks inside a svn repository
852
853 :param repo: Instance of VCS repo
854 :param force_create: Create even if same name hook exists
855 """
856 hooks_path = os.path.join(repo.path, 'hooks')
857 if not os.path.isdir(hooks_path):
858 os.makedirs(hooks_path)
859 post_commit_tmpl = pkg_resources.resource_string(
860 'rhodecode', '/'.join(
861 ('config', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
862 pre_commit_template = pkg_resources.resource_string(
863 'rhodecode', '/'.join(
864 ('config', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
865 templates = {
866 'post-commit': post_commit_tmpl,
867 'pre-commit': pre_commit_template
868 }
869 for filename in templates:
870 _hook_file = os.path.join(hooks_path, filename)
871 _rhodecode_hook = _check_rhodecode_hook(_hook_file)
872 if _rhodecode_hook or force_create:
873 log.debug('writing %s hook file !', filename)
874 template = templates[filename]
875 try:
876 with open(_hook_file, 'wb') as f:
877 template = template.replace(
878 '_TMPL_', rhodecode.__version__)
879 template = template.replace('_ENV_', sys.executable)
880 f.write(template)
881 os.chmod(_hook_file, 0755)
882 except IOError:
883 log.exception('error writing hook file %s', filename)
884 else:
885 log.debug('skipping writing hook file')
886
887 def install_hooks(self, repo, repo_type):
888 if repo_type == 'git':
889 self.install_git_hook(repo)
890 elif repo_type == 'svn':
891 self.install_svn_hooks(repo)
892
893 def get_server_info(self, environ=None):
810 def get_server_info(self, environ=None):
894 server_info = get_system_info(environ)
811 server_info = get_system_info(environ)
895 return server_info
812 return server_info
896
897
898 def _check_rhodecode_hook(hook_path):
899 """
900 Check if the hook was created by RhodeCode
901 """
902 if not os.path.exists(hook_path):
903 return True
904
905 log.debug('hook exists, checking if it is from rhodecode')
906 hook_content = _read_hook(hook_path)
907 matches = re.search(r'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
908 if matches:
909 try:
910 version = matches.groups()[0]
911 log.debug('got %s, it is rhodecode', version)
912 return True
913 except Exception:
914 log.exception("Exception while reading the hook version.")
915
916 return False
917
918
919 def _read_hook(hook_path):
920 with open(hook_path, 'rb') as f:
921 content = f.read()
922 return content
@@ -377,34 +377,6 b' class TestGenerateVcsResponse(object):'
377 list(result)
377 list(result)
378 assert self.was_cache_invalidated()
378 assert self.was_cache_invalidated()
379
379
380 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
381 def test_handles_locking_exception(self, http_locked_rc):
382 result = self.call_controller_with_response_body(
383 self.raise_result_iter(vcs_kind='repo_locked'))
384 assert not http_locked_rc.called
385 # Consume the result
386 list(result)
387 assert http_locked_rc.called
388
389 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
390 def test_handles_requirement_exception(self, http_requirement):
391 result = self.call_controller_with_response_body(
392 self.raise_result_iter(vcs_kind='requirement'))
393 assert not http_requirement.called
394 # Consume the result
395 list(result)
396 assert http_requirement.called
397
398 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
399 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
400 app_factory_patcher = mock.patch.object(
401 StubVCSController, '_create_wsgi_app')
402 with app_factory_patcher as app_factory:
403 app_factory().side_effect = self.vcs_exception()
404 result = self.call_controller_with_response_body(['a'])
405 list(result)
406 assert http_locked_rc.called
407
408 def test_raises_unknown_exceptions(self):
380 def test_raises_unknown_exceptions(self):
409 result = self.call_controller_with_response_body(
381 result = self.call_controller_with_response_body(
410 self.raise_result_iter(vcs_kind='unknown'))
382 self.raise_result_iter(vcs_kind='unknown'))
@@ -412,7 +384,7 b' class TestGenerateVcsResponse(object):'
412 list(result)
384 list(result)
413
385
414 def test_prepare_callback_daemon_is_called(self):
386 def test_prepare_callback_daemon_is_called(self):
415 def side_effect(extras):
387 def side_effect(extras, environ, action, txn_id=None):
416 return DummyHooksCallbackDaemon(), extras
388 return DummyHooksCallbackDaemon(), extras
417
389
418 prepare_patcher = mock.patch.object(
390 prepare_patcher = mock.patch.object(
@@ -489,10 +461,11 b' class TestPrepareHooksDaemon(object):'
489 return_value=(daemon, expected_extras))
461 return_value=(daemon, expected_extras))
490 with prepare_patcher as prepare_mock:
462 with prepare_patcher as prepare_mock:
491 callback_daemon, extras = controller._prepare_callback_daemon(
463 callback_daemon, extras = controller._prepare_callback_daemon(
492 expected_extras.copy())
464 expected_extras.copy(), {}, 'push')
493 prepare_mock.assert_called_once_with(
465 prepare_mock.assert_called_once_with(
494 expected_extras,
466 expected_extras,
495 protocol=app_settings['vcs.hooks.protocol'],
467 protocol=app_settings['vcs.hooks.protocol'],
468 txn_id=None,
496 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
469 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
497
470
498 assert callback_daemon == daemon
471 assert callback_daemon == daemon
@@ -179,10 +179,12 b' class TestHttpHooksCallbackDaemon(object'
179 daemon = hooks_daemon.HttpHooksCallbackDaemon()
179 daemon = hooks_daemon.HttpHooksCallbackDaemon()
180 assert daemon._daemon == tcp_server
180 assert daemon._daemon == tcp_server
181
181
182 _, port = tcp_server.server_address
183 expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port)
184 msg = 'Preparing HTTP callback daemon at `{}` and ' \
185 'registering hook object'.format(expected_uri)
182 assert_message_in_log(
186 assert_message_in_log(
183 caplog.records,
187 caplog.records, msg, levelno=logging.DEBUG, module='hooks_daemon')
184 'Preparing HTTP callback daemon and registering hook object',
185 levelno=logging.DEBUG, module='hooks_daemon')
186
188
187 def test_prepare_inits_hooks_uri_and_logs_it(
189 def test_prepare_inits_hooks_uri_and_logs_it(
188 self, tcp_server, caplog):
190 self, tcp_server, caplog):
@@ -193,8 +195,10 b' class TestHttpHooksCallbackDaemon(object'
193 expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port)
195 expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port)
194 assert daemon.hooks_uri == expected_uri
196 assert daemon.hooks_uri == expected_uri
195
197
198 msg = 'Preparing HTTP callback daemon at `{}` and ' \
199 'registering hook object'.format(expected_uri)
196 assert_message_in_log(
200 assert_message_in_log(
197 caplog.records, 'Hooks uri is: {}'.format(expected_uri),
201 caplog.records, msg,
198 levelno=logging.DEBUG, module='hooks_daemon')
202 levelno=logging.DEBUG, module='hooks_daemon')
199
203
200 def test_run_creates_a_thread(self, tcp_server):
204 def test_run_creates_a_thread(self, tcp_server):
@@ -263,7 +267,8 b' class TestPrepareHooksDaemon(object):'
263 expected_extras.copy(), protocol=protocol, use_direct_calls=True)
267 expected_extras.copy(), protocol=protocol, use_direct_calls=True)
264 assert isinstance(callback, hooks_daemon.DummyHooksCallbackDaemon)
268 assert isinstance(callback, hooks_daemon.DummyHooksCallbackDaemon)
265 expected_extras['hooks_module'] = 'rhodecode.lib.hooks_daemon'
269 expected_extras['hooks_module'] = 'rhodecode.lib.hooks_daemon'
266 assert extras == expected_extras
270 expected_extras['time'] = extras['time']
271 assert 'extra1' in extras
267
272
268 @pytest.mark.parametrize('protocol, expected_class', (
273 @pytest.mark.parametrize('protocol, expected_class', (
269 ('http', hooks_daemon.HttpHooksCallbackDaemon),
274 ('http', hooks_daemon.HttpHooksCallbackDaemon),
@@ -272,12 +277,15 b' class TestPrepareHooksDaemon(object):'
272 self, protocol, expected_class):
277 self, protocol, expected_class):
273 expected_extras = {
278 expected_extras = {
274 'extra1': 'value1',
279 'extra1': 'value1',
280 'txn_id': 'txnid2',
275 'hooks_protocol': protocol.lower()
281 'hooks_protocol': protocol.lower()
276 }
282 }
277 callback, extras = hooks_daemon.prepare_callback_daemon(
283 callback, extras = hooks_daemon.prepare_callback_daemon(
278 expected_extras.copy(), protocol=protocol, use_direct_calls=False)
284 expected_extras.copy(), protocol=protocol, use_direct_calls=False,
285 txn_id='txnid2')
279 assert isinstance(callback, expected_class)
286 assert isinstance(callback, expected_class)
280 hooks_uri = extras.pop('hooks_uri')
287 extras.pop('hooks_uri')
288 expected_extras['time'] = extras['time']
281 assert extras == expected_extras
289 assert extras == expected_extras
282
290
283 @pytest.mark.parametrize('protocol', (
291 @pytest.mark.parametrize('protocol', (
@@ -245,22 +245,16 b' def test_repo2db_mapper_enables_largefil'
245 repo = backend.create_repo()
245 repo = backend.create_repo()
246 repo_list = {repo.repo_name: 'test'}
246 repo_list = {repo.repo_name: 'test'}
247 with mock.patch('rhodecode.model.db.Repository.scm_instance') as scm_mock:
247 with mock.patch('rhodecode.model.db.Repository.scm_instance') as scm_mock:
248 with mock.patch.multiple('rhodecode.model.scm.ScmModel',
248 utils.repo2db_mapper(repo_list, remove_obsolete=False)
249 install_git_hook=mock.DEFAULT,
249 _, kwargs = scm_mock.call_args
250 install_svn_hooks=mock.DEFAULT):
250 assert kwargs['config'].get('extensions', 'largefiles') == ''
251 utils.repo2db_mapper(repo_list, remove_obsolete=False)
252 _, kwargs = scm_mock.call_args
253 assert kwargs['config'].get('extensions', 'largefiles') == ''
254
251
255
252
256 @pytest.mark.backends("git", "svn")
253 @pytest.mark.backends("git", "svn")
257 def test_repo2db_mapper_installs_hooks_for_repos_in_db(backend):
254 def test_repo2db_mapper_installs_hooks_for_repos_in_db(backend):
258 repo = backend.create_repo()
255 repo = backend.create_repo()
259 repo_list = {repo.repo_name: 'test'}
256 repo_list = {repo.repo_name: 'test'}
260 with mock.patch.object(ScmModel, 'install_hooks') as install_hooks_mock:
257 utils.repo2db_mapper(repo_list, remove_obsolete=False)
261 utils.repo2db_mapper(repo_list, remove_obsolete=False)
262 install_hooks_mock.assert_called_once_with(
263 repo.scm_instance(), repo_type=backend.alias)
264
258
265
259
266 @pytest.mark.backends("git", "svn")
260 @pytest.mark.backends("git", "svn")
@@ -269,11 +263,7 b' def test_repo2db_mapper_installs_hooks_f'
269 RepoModel().delete(repo, fs_remove=False)
263 RepoModel().delete(repo, fs_remove=False)
270 meta.Session().commit()
264 meta.Session().commit()
271 repo_list = {repo.repo_name: repo.scm_instance()}
265 repo_list = {repo.repo_name: repo.scm_instance()}
272 with mock.patch.object(ScmModel, 'install_hooks') as install_hooks_mock:
266 utils.repo2db_mapper(repo_list, remove_obsolete=False)
273 utils.repo2db_mapper(repo_list, remove_obsolete=False)
274 assert install_hooks_mock.call_count == 1
275 install_hooks_args, _ = install_hooks_mock.call_args
276 assert install_hooks_args[0].name == repo.repo_name
277
267
278
268
279 class TestPasswordChanged(object):
269 class TestPasswordChanged(object):
@@ -18,10 +18,11 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
22 import mock
23 import pytest
21 import tempfile
24 import tempfile
22
25
23 import mock
24 import pytest
25
26
26 from rhodecode.lib.exceptions import AttachedForksError
27 from rhodecode.lib.exceptions import AttachedForksError
27 from rhodecode.lib.utils import make_db_config
28 from rhodecode.lib.utils import make_db_config
@@ -119,22 +120,22 b' class TestRepoModel(object):'
119
120
120 @pytest.mark.backends("git", "svn")
121 @pytest.mark.backends("git", "svn")
121 def test_create_filesystem_repo_installs_hooks(self, tmpdir, backend):
122 def test_create_filesystem_repo_installs_hooks(self, tmpdir, backend):
122 hook_methods = {
123 'git': 'install_git_hook',
124 'svn': 'install_svn_hooks'
125 }
126 repo = backend.create_repo()
123 repo = backend.create_repo()
127 repo_name = repo.repo_name
124 repo_name = repo.repo_name
128 model = RepoModel()
125 model = RepoModel()
129 repo_location = tempfile.mkdtemp()
126 repo_location = tempfile.mkdtemp()
130 model.repos_path = repo_location
127 model.repos_path = repo_location
131 method = hook_methods[backend.alias]
128 repo = model._create_filesystem_repo(
132 with mock.patch.object(ScmModel, method) as hooks_mock:
129 repo_name, backend.alias, repo_group='', clone_uri=None)
133 model._create_filesystem_repo(
130
134 repo_name, backend.alias, repo_group='', clone_uri=None)
131 hooks = {
135 assert hooks_mock.call_count == 1
132 'svn': ('pre-commit', 'post-commit'),
136 hook_args, hook_kwargs = hooks_mock.call_args
133 'git': ('pre-receive', 'post-receive'),
137 assert hook_args[0].name == repo_name
134 }
135 for hook in hooks[backend.alias]:
136 with open(os.path.join(repo.path, 'hooks', hook)) as f:
137 data = f.read()
138 assert 'RC_HOOK_VER' in data
138
139
139 @pytest.mark.parametrize("use_global_config, repo_name_passed", [
140 @pytest.mark.parametrize("use_global_config, repo_name_passed", [
140 (True, False),
141 (True, False),
@@ -194,143 +194,3 b' def test_get_non_unicode_reference(backe'
194 u'tag:Ad\xc4\xb1n\xc4\xb1']
194 u'tag:Ad\xc4\xb1n\xc4\xb1']
195
195
196 assert choices == valid_choices
196 assert choices == valid_choices
197
198
199 class TestInstallSvnHooks(object):
200 HOOK_FILES = ('pre-commit', 'post-commit')
201
202 def test_new_hooks_are_created(self, backend_svn):
203 model = scm.ScmModel()
204 repo = backend_svn.create_repo()
205 vcs_repo = repo.scm_instance()
206 model.install_svn_hooks(vcs_repo)
207
208 hooks_path = os.path.join(vcs_repo.path, 'hooks')
209 assert os.path.isdir(hooks_path)
210 for file_name in self.HOOK_FILES:
211 file_path = os.path.join(hooks_path, file_name)
212 self._check_hook_file_mode(file_path)
213 self._check_hook_file_content(file_path)
214
215 def test_rc_hooks_are_replaced(self, backend_svn):
216 model = scm.ScmModel()
217 repo = backend_svn.create_repo()
218 vcs_repo = repo.scm_instance()
219 hooks_path = os.path.join(vcs_repo.path, 'hooks')
220 file_paths = [os.path.join(hooks_path, f) for f in self.HOOK_FILES]
221
222 for file_path in file_paths:
223 self._create_fake_hook(
224 file_path, content="RC_HOOK_VER = 'abcde'\n")
225
226 model.install_svn_hooks(vcs_repo)
227
228 for file_path in file_paths:
229 self._check_hook_file_content(file_path)
230
231 def test_non_rc_hooks_are_not_replaced_without_force_create(
232 self, backend_svn):
233 model = scm.ScmModel()
234 repo = backend_svn.create_repo()
235 vcs_repo = repo.scm_instance()
236 hooks_path = os.path.join(vcs_repo.path, 'hooks')
237 file_paths = [os.path.join(hooks_path, f) for f in self.HOOK_FILES]
238 non_rc_content = "exit 0\n"
239
240 for file_path in file_paths:
241 self._create_fake_hook(file_path, content=non_rc_content)
242
243 model.install_svn_hooks(vcs_repo)
244
245 for file_path in file_paths:
246 with open(file_path, 'rt') as hook_file:
247 content = hook_file.read()
248 assert content == non_rc_content
249
250 def test_non_rc_hooks_are_replaced_with_force_create(self, backend_svn):
251 model = scm.ScmModel()
252 repo = backend_svn.create_repo()
253 vcs_repo = repo.scm_instance()
254 hooks_path = os.path.join(vcs_repo.path, 'hooks')
255 file_paths = [os.path.join(hooks_path, f) for f in self.HOOK_FILES]
256 non_rc_content = "exit 0\n"
257
258 for file_path in file_paths:
259 self._create_fake_hook(file_path, content=non_rc_content)
260
261 model.install_svn_hooks(vcs_repo, force_create=True)
262
263 for file_path in file_paths:
264 self._check_hook_file_content(file_path)
265
266 def _check_hook_file_mode(self, file_path):
267 assert os.path.exists(file_path)
268 stat_info = os.stat(file_path)
269
270 file_mode = stat.S_IMODE(stat_info.st_mode)
271 expected_mode = int('755', 8)
272 assert expected_mode == file_mode
273
274 def _check_hook_file_content(self, file_path):
275 with open(file_path, 'rt') as hook_file:
276 content = hook_file.read()
277
278 expected_env = '#!{}'.format(sys.executable)
279 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(
280 rhodecode.__version__)
281 assert content.strip().startswith(expected_env)
282 assert expected_rc_version in content
283
284 def _create_fake_hook(self, file_path, content):
285 with open(file_path, 'w') as hook_file:
286 hook_file.write(content)
287
288
289 class TestCheckRhodecodeHook(object):
290
291 @patch('os.path.exists', Mock(return_value=False))
292 def test_returns_true_when_no_hook_found(self):
293 result = scm._check_rhodecode_hook('/tmp/fake_hook_file.py')
294 assert result
295
296 @pytest.mark.parametrize("file_content, expected_result", [
297 ("RC_HOOK_VER = '3.3.3'\n", True),
298 ("RC_HOOK = '3.3.3'\n", False),
299 ], ids=no_newline_id_generator)
300 @patch('os.path.exists', Mock(return_value=True))
301 def test_signatures(self, file_content, expected_result):
302 hook_content_patcher = patch.object(
303 scm, '_read_hook', return_value=file_content)
304 with hook_content_patcher:
305 result = scm._check_rhodecode_hook('/tmp/fake_hook_file.py')
306
307 assert result is expected_result
308
309
310 class TestInstallHooks(object):
311 def test_hooks_are_installed_for_git_repo(self, backend_git):
312 repo = backend_git.create_repo()
313 model = scm.ScmModel()
314 scm_repo = repo.scm_instance()
315 with patch.object(model, 'install_git_hook') as hooks_mock:
316 model.install_hooks(scm_repo, repo_type='git')
317 hooks_mock.assert_called_once_with(scm_repo)
318
319 def test_hooks_are_installed_for_svn_repo(self, backend_svn):
320 repo = backend_svn.create_repo()
321 scm_repo = repo.scm_instance()
322 model = scm.ScmModel()
323 with patch.object(scm.ScmModel, 'install_svn_hooks') as hooks_mock:
324 model.install_hooks(scm_repo, repo_type='svn')
325 hooks_mock.assert_called_once_with(scm_repo)
326
327 @pytest.mark.parametrize('hook_method', [
328 'install_svn_hooks',
329 'install_git_hook'])
330 def test_mercurial_doesnt_trigger_hooks(self, backend_hg, hook_method):
331 repo = backend_hg.create_repo()
332 scm_repo = repo.scm_instance()
333 model = scm.ScmModel()
334 with patch.object(scm.ScmModel, hook_method) as hooks_mock:
335 model.install_hooks(scm_repo, repo_type='hg')
336 assert hooks_mock.call_count == 0
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now