Show More
@@ -16,9 +16,6 b' recursive-include configs *' | |||
|
16 | 16 | # translations |
|
17 | 17 | recursive-include rhodecode/i18n * |
|
18 | 18 | |
|
19 | # hook templates | |
|
20 | recursive-include rhodecode/config/hook_templates * | |
|
21 | ||
|
22 | 19 | # non-python core stuff |
|
23 | 20 | recursive-include rhodecode *.cfg |
|
24 | 21 | recursive-include rhodecode *.json |
@@ -42,7 +42,8 b' class TestGetRepoChangeset(object):' | |||
|
42 | 42 | if details == 'full': |
|
43 | 43 | assert result['refs']['bookmarks'] == getattr( |
|
44 | 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 | 47 | assert result['refs']['tags'] == commit.tags |
|
47 | 48 | |
|
48 | 49 | @pytest.mark.parametrize("details", ['basic', 'extended', 'full']) |
@@ -18,10 +18,13 b'' | |||
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 |
import |
|
|
21 | import os | |
|
22 | import time | |
|
22 | 23 | import logging |
|
24 | import tempfile | |
|
23 | 25 | import traceback |
|
24 | 26 | import threading |
|
27 | ||
|
25 | 28 | from BaseHTTPServer import BaseHTTPRequestHandler |
|
26 | 29 | from SocketServer import TCPServer |
|
27 | 30 | |
@@ -30,14 +33,28 b' from rhodecode.model import meta' | |||
|
30 | 33 | from rhodecode.lib.base import bootstrap_request, bootstrap_config |
|
31 | 34 | from rhodecode.lib import hooks_base |
|
32 | 35 | from rhodecode.lib.utils2 import AttributeDict |
|
36 | from rhodecode.lib.ext_json import json | |
|
33 | 37 | |
|
34 | 38 | |
|
35 | 39 | log = logging.getLogger(__name__) |
|
36 | 40 | |
|
37 | 41 | |
|
38 | 42 | class HooksHttpHandler(BaseHTTPRequestHandler): |
|
43 | ||
|
39 | 44 | def do_POST(self): |
|
40 | 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 | 58 | try: |
|
42 | 59 | result = self._call_hook(method, extras) |
|
43 | 60 | except Exception as e: |
@@ -77,13 +94,14 b' class HooksHttpHandler(BaseHTTPRequestHa' | |||
|
77 | 94 | |
|
78 | 95 | message = format % args |
|
79 | 96 | |
|
80 | # TODO: mikhail: add different log levels support | |
|
81 | 97 | log.debug( |
|
82 | 98 | "%s - - [%s] %s", self.client_address[0], |
|
83 | 99 | self.log_date_time_string(), message) |
|
84 | 100 | |
|
85 | 101 | |
|
86 | 102 | class DummyHooksCallbackDaemon(object): |
|
103 | hooks_uri = '' | |
|
104 | ||
|
87 | 105 | def __init__(self): |
|
88 | 106 | self.hooks_module = Hooks.__module__ |
|
89 | 107 | |
@@ -101,8 +119,8 b' class ThreadedHookCallbackDaemon(object)' | |||
|
101 | 119 | _daemon = None |
|
102 | 120 | _done = False |
|
103 | 121 | |
|
104 | def __init__(self): | |
|
105 | self._prepare() | |
|
122 | def __init__(self, txn_id=None, port=None): | |
|
123 | self._prepare(txn_id=txn_id, port=port) | |
|
106 | 124 | |
|
107 | 125 | def __enter__(self): |
|
108 | 126 | self._run() |
@@ -112,7 +130,7 b' class ThreadedHookCallbackDaemon(object)' | |||
|
112 | 130 | log.debug('Callback daemon exiting now...') |
|
113 | 131 | self._stop() |
|
114 | 132 | |
|
115 | def _prepare(self): | |
|
133 | def _prepare(self, txn_id=None, port=None): | |
|
116 | 134 | raise NotImplementedError() |
|
117 | 135 | |
|
118 | 136 | def _run(self): |
@@ -135,15 +153,18 b' class HttpHooksCallbackDaemon(ThreadedHo' | |||
|
135 | 153 | # request and wastes cpu at all other times. |
|
136 | 154 | POLL_INTERVAL = 0.01 |
|
137 | 155 | |
|
138 | def _prepare(self): | |
|
139 | log.debug("Preparing HTTP callback daemon and registering hook object") | |
|
140 | ||
|
156 | def _prepare(self, txn_id=None, port=None): | |
|
141 | 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 | 159 | _, port = self._daemon.server_address |
|
144 | 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 | 169 | def _run(self): |
|
149 | 170 | log.debug("Running event loop of callback daemon in background thread") |
@@ -160,26 +181,67 b' class HttpHooksCallbackDaemon(ThreadedHo' | |||
|
160 | 181 | self._callback_thread.join() |
|
161 | 182 | self._daemon = None |
|
162 | 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 | 190 | log.debug("Background thread done.") |
|
164 | 191 | |
|
165 | 192 | |
|
166 | def prepare_callback_daemon(extras, protocol, use_direct_calls): | |
|
167 | callback_daemon = None | |
|
193 | def get_txn_id_data_path(txn_id): | |
|
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 | 226 | if use_direct_calls: |
|
170 | 227 | callback_daemon = DummyHooksCallbackDaemon() |
|
171 | 228 | extras['hooks_module'] = callback_daemon.hooks_module |
|
172 | 229 | else: |
|
173 | 230 | if protocol == 'http': |
|
174 | callback_daemon = HttpHooksCallbackDaemon() | |
|
231 | callback_daemon = HttpHooksCallbackDaemon(txn_id=txn_id, port=port) | |
|
175 | 232 | else: |
|
176 | 233 | log.error('Unsupported callback daemon protocol "%s"', protocol) |
|
177 | 234 | raise Exception('Unsupported callback daemon protocol.') |
|
178 | 235 | |
|
179 |
|
|
|
180 |
|
|
|
236 | extras['hooks_uri'] = callback_daemon.hooks_uri | |
|
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 | 245 | return callback_daemon, extras |
|
184 | 246 | |
|
185 | 247 |
@@ -18,17 +18,21 b'' | |||
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | import base64 | |
|
21 | 22 | import logging |
|
22 | 23 | import urllib |
|
23 | 24 | from urlparse import urljoin |
|
24 | 25 | |
|
25 | ||
|
26 | 26 | import requests |
|
27 | 27 | from webob.exc import HTTPNotAcceptable |
|
28 | 28 | |
|
29 | from rhodecode.lib import caches | |
|
29 | 30 | from rhodecode.lib.middleware import simplevcs |
|
30 | 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 | 37 | log = logging.getLogger(__name__) |
|
34 | 38 | |
@@ -39,7 +43,6 b' class SimpleSvnApp(object):' | |||
|
39 | 43 | 'transfer-encoding', 'content-length'] |
|
40 | 44 | rc_extras = {} |
|
41 | 45 | |
|
42 | ||
|
43 | 46 | def __init__(self, config): |
|
44 | 47 | self.config = config |
|
45 | 48 | |
@@ -52,9 +55,19 b' class SimpleSvnApp(object):' | |||
|
52 | 55 | # length, then we should transfer the payload in one request. |
|
53 | 56 | if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ: |
|
54 | 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 | 68 | log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'], |
|
57 | 69 | self._get_url(environ['PATH_INFO'])) |
|
70 | ||
|
58 | 71 | response = requests.request( |
|
59 | 72 | environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']), |
|
60 | 73 | data=data, headers=request_headers) |
@@ -70,6 +83,14 b' class SimpleSvnApp(object):' | |||
|
70 | 83 | log.debug('got response code: %s', response.status_code) |
|
71 | 84 | |
|
72 | 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 | 94 | start_response( |
|
74 | 95 | '{} {}'.format(response.status_code, response.reason), |
|
75 | 96 | response_headers) |
@@ -156,6 +177,14 b' class SimpleSvn(simplevcs.SimpleVCS):' | |||
|
156 | 177 | if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS |
|
157 | 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 | 188 | def _create_wsgi_app(self, repo_path, repo_name, config): |
|
160 | 189 | if self._is_svn_enabled(): |
|
161 | 190 | return SimpleSvnApp(config) |
@@ -28,10 +28,12 b' import re' | |||
|
28 | 28 | import logging |
|
29 | 29 | import importlib |
|
30 | 30 | from functools import wraps |
|
31 | from StringIO import StringIO | |
|
32 | from lxml import etree | |
|
31 | 33 | |
|
32 | 34 | import time |
|
33 | 35 | from paste.httpheaders import REMOTE_USER, AUTH_TYPE |
|
34 | # TODO(marcink): check if we should use webob.exc here ? | |
|
36 | ||
|
35 | 37 | from pyramid.httpexceptions import ( |
|
36 | 38 | HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError) |
|
37 | 39 | from zope.cachedescriptors.property import Lazy as LazyProperty |
@@ -43,9 +45,7 b' from rhodecode.lib import caches' | |||
|
43 | 45 | from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware |
|
44 | 46 | from rhodecode.lib.base import ( |
|
45 | 47 | BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) |
|
46 | from rhodecode.lib.exceptions import ( | |
|
47 | HTTPLockedRC, HTTPRequirementError, UserCreationError, | |
|
48 | NotAllowedToCreateUserError) | |
|
48 | from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError) | |
|
49 | 49 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
50 | 50 | from rhodecode.lib.middleware import appenlight |
|
51 | 51 | from rhodecode.lib.middleware.utils import scm_app_http |
@@ -53,6 +53,7 b' from rhodecode.lib.utils import is_valid' | |||
|
53 | 53 | from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode |
|
54 | 54 | from rhodecode.lib.vcs.conf import settings as vcs_settings |
|
55 | 55 | from rhodecode.lib.vcs.backends import base |
|
56 | ||
|
56 | 57 | from rhodecode.model import meta |
|
57 | 58 | from rhodecode.model.db import User, Repository, PullRequest |
|
58 | 59 | from rhodecode.model.scm import ScmModel |
@@ -62,6 +63,28 b' from rhodecode.model.settings import Set' | |||
|
62 | 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 | 88 | def initialize_generator(factory): |
|
66 | 89 | """ |
|
67 | 90 | Initializes the returned generator by draining its first element. |
@@ -565,48 +588,42 b' class SimpleVCS(object):' | |||
|
565 | 588 | also handles the locking exceptions which will be triggered when |
|
566 | 589 | the first chunk is produced by the underlying WSGI application. |
|
567 | 590 | """ |
|
568 | callback_daemon, extras = self._prepare_callback_daemon(extras) | |
|
569 | config = self._create_config(extras, self.acl_repo_name) | |
|
570 | log.debug('HOOKS extras is %s', extras) | |
|
571 | app = self._create_wsgi_app(repo_path, self.url_repo_name, config) | |
|
572 | app.rc_extras = extras | |
|
591 | txn_id = '' | |
|
592 | if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE': | |
|
593 | # case for SVN, we want to re-use the callback daemon port | |
|
594 | # so we use the txn_id, for this we peek the body, and still save | |
|
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: | |
|
575 | with callback_daemon: | |
|
576 | try: | |
|
577 | response = app(environ, start_response) | |
|
578 | finally: | |
|
579 | # This statement works together with the decorator | |
|
580 | # "initialize_generator" above. The decorator ensures that | |
|
581 | # we hit the first yield statement before the generator is | |
|
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__" | |
|
600 | callback_daemon, extras = self._prepare_callback_daemon( | |
|
601 | extras, environ, action, txn_id=txn_id) | |
|
602 | log.debug('HOOKS extras is %s', extras) | |
|
603 | ||
|
604 | config = self._create_config(extras, self.acl_repo_name) | |
|
605 | app = self._create_wsgi_app(repo_path, self.url_repo_name, config) | |
|
606 | with callback_daemon: | |
|
607 | app.rc_extras = extras | |
|
587 | 608 | |
|
588 | for chunk in response: | |
|
589 | yield chunk | |
|
590 | except Exception as exc: | |
|
591 | # TODO: martinb: Exceptions are only raised in case of the Pyro4 | |
|
592 | # backend. Refactor this except block after dropping Pyro4 support. | |
|
593 | # TODO: johbo: Improve "translating" back the exception. | |
|
594 | if getattr(exc, '_vcs_kind', None) == 'repo_locked': | |
|
595 | exc = HTTPLockedRC(*exc.args) | |
|
596 | _code = rhodecode.CONFIG.get('lock_ret_code') | |
|
597 | log.debug('Repository LOCKED ret code %s!', (_code,)) | |
|
598 | elif getattr(exc, '_vcs_kind', None) == 'requirement': | |
|
599 | log.debug( | |
|
600 | 'Repository requires features unknown to this Mercurial') | |
|
601 | exc = HTTPRequirementError(*exc.args) | |
|
602 | else: | |
|
603 | raise | |
|
609 | try: | |
|
610 | response = app(environ, start_response) | |
|
611 | finally: | |
|
612 | # This statement works together with the decorator | |
|
613 | # "initialize_generator" above. The decorator ensures that | |
|
614 | # we hit the first yield statement before the generator is | |
|
615 | # returned back to the WSGI server. This is needed to | |
|
616 | # ensure that the call to "app" above triggers the | |
|
617 | # needed callback to "start_response" before the | |
|
618 | # generator is actually used. | |
|
619 | yield "__init__" | |
|
604 | 620 | |
|
605 | for chunk in exc(environ, start_response): | |
|
621 | # iter content | |
|
622 | for chunk in response: | |
|
606 | 623 | yield chunk |
|
607 | finally: | |
|
608 | # invalidate cache on push | |
|
624 | ||
|
609 | 625 | try: |
|
626 | # invalidate cache on push | |
|
610 | 627 | if action == 'push': |
|
611 | 628 | self._invalidate_cache(self.url_repo_name) |
|
612 | 629 | finally: |
@@ -634,10 +651,18 b' class SimpleVCS(object):' | |||
|
634 | 651 | """Create a safe config representation.""" |
|
635 | 652 | raise NotImplementedError() |
|
636 | 653 | |
|
637 |
def _ |
|
|
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 | 663 | return prepare_callback_daemon( |
|
639 | 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 | 668 | def _should_check_locking(query_string): |
@@ -514,7 +514,6 b' def repo2db_mapper(initial_repo_list, re' | |||
|
514 | 514 | :param remove_obsolete: check for obsolete entries in database |
|
515 | 515 | """ |
|
516 | 516 | from rhodecode.model.repo import RepoModel |
|
517 | from rhodecode.model.scm import ScmModel | |
|
518 | 517 | from rhodecode.model.repo_group import RepoGroupModel |
|
519 | 518 | from rhodecode.model.settings import SettingsModel |
|
520 | 519 | |
@@ -566,9 +565,8 b' def repo2db_mapper(initial_repo_list, re' | |||
|
566 | 565 | |
|
567 | 566 | config = db_repo._config |
|
568 | 567 | config.set('extensions', 'largefiles', '') |
|
569 | ScmModel().install_hooks( | |
|
570 | db_repo.scm_instance(config=config), | |
|
571 | repo_type=db_repo.repo_type) | |
|
568 | repo = db_repo.scm_instance(config=config) | |
|
569 | repo.install_hooks() | |
|
572 | 570 | |
|
573 | 571 | removed = [] |
|
574 | 572 | if remove_obsolete: |
@@ -175,6 +175,7 b' class BaseRepository(object):' | |||
|
175 | 175 | EMPTY_COMMIT_ID = '0' * 40 |
|
176 | 176 | |
|
177 | 177 | path = None |
|
178 | _remote = None | |
|
178 | 179 | |
|
179 | 180 | def __init__(self, repo_path, config=None, create=False, **kwargs): |
|
180 | 181 | """ |
@@ -648,6 +649,9 b' class BaseRepository(object):' | |||
|
648 | 649 | """ |
|
649 | 650 | return None |
|
650 | 651 | |
|
652 | def install_hooks(self, force=False): | |
|
653 | return self._remote.install_hooks(force) | |
|
654 | ||
|
651 | 655 | |
|
652 | 656 | class BaseCommit(object): |
|
653 | 657 | """ |
@@ -743,7 +747,7 b' class BaseCommit(object):' | |||
|
743 | 747 | |
|
744 | 748 | def _get_refs(self): |
|
745 | 749 | return { |
|
746 | 'branches': [self.branch], | |
|
750 | 'branches': [self.branch] if self.branch else [], | |
|
747 | 751 | 'bookmarks': getattr(self, 'bookmarks', []), |
|
748 | 752 | 'tags': self.tags |
|
749 | 753 | } |
@@ -858,7 +858,7 b' class RepoModel(BaseModel):' | |||
|
858 | 858 | repo = backend( |
|
859 | 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 | 863 | log.debug('Created repo %s with %s backend', |
|
864 | 864 | safe_unicode(repo_name), safe_unicode(repo_type)) |
@@ -807,116 +807,6 b' class ScmModel(BaseModel):' | |||
|
807 | 807 | |
|
808 | 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 | 810 | def get_server_info(self, environ=None): |
|
894 | 811 | server_info = get_system_info(environ) |
|
895 | 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 | 377 | list(result) |
|
378 | 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 | 380 | def test_raises_unknown_exceptions(self): |
|
409 | 381 | result = self.call_controller_with_response_body( |
|
410 | 382 | self.raise_result_iter(vcs_kind='unknown')) |
@@ -412,7 +384,7 b' class TestGenerateVcsResponse(object):' | |||
|
412 | 384 | list(result) |
|
413 | 385 | |
|
414 | 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 | 388 | return DummyHooksCallbackDaemon(), extras |
|
417 | 389 | |
|
418 | 390 | prepare_patcher = mock.patch.object( |
@@ -489,10 +461,11 b' class TestPrepareHooksDaemon(object):' | |||
|
489 | 461 | return_value=(daemon, expected_extras)) |
|
490 | 462 | with prepare_patcher as prepare_mock: |
|
491 | 463 | callback_daemon, extras = controller._prepare_callback_daemon( |
|
492 | expected_extras.copy()) | |
|
464 | expected_extras.copy(), {}, 'push') | |
|
493 | 465 | prepare_mock.assert_called_once_with( |
|
494 | 466 | expected_extras, |
|
495 | 467 | protocol=app_settings['vcs.hooks.protocol'], |
|
468 | txn_id=None, | |
|
496 | 469 | use_direct_calls=app_settings['vcs.hooks.direct_calls']) |
|
497 | 470 | |
|
498 | 471 | assert callback_daemon == daemon |
@@ -179,10 +179,12 b' class TestHttpHooksCallbackDaemon(object' | |||
|
179 | 179 | daemon = hooks_daemon.HttpHooksCallbackDaemon() |
|
180 | 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 | 186 | assert_message_in_log( |
|
183 | caplog.records, | |
|
184 | 'Preparing HTTP callback daemon and registering hook object', | |
|
185 | levelno=logging.DEBUG, module='hooks_daemon') | |
|
187 | caplog.records, msg, levelno=logging.DEBUG, module='hooks_daemon') | |
|
186 | 188 | |
|
187 | 189 | def test_prepare_inits_hooks_uri_and_logs_it( |
|
188 | 190 | self, tcp_server, caplog): |
@@ -193,8 +195,10 b' class TestHttpHooksCallbackDaemon(object' | |||
|
193 | 195 | expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port) |
|
194 | 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 | 200 | assert_message_in_log( |
|
197 |
caplog.records, |
|
|
201 | caplog.records, msg, | |
|
198 | 202 | levelno=logging.DEBUG, module='hooks_daemon') |
|
199 | 203 | |
|
200 | 204 | def test_run_creates_a_thread(self, tcp_server): |
@@ -263,7 +267,8 b' class TestPrepareHooksDaemon(object):' | |||
|
263 | 267 | expected_extras.copy(), protocol=protocol, use_direct_calls=True) |
|
264 | 268 | assert isinstance(callback, hooks_daemon.DummyHooksCallbackDaemon) |
|
265 | 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 | 273 | @pytest.mark.parametrize('protocol, expected_class', ( |
|
269 | 274 | ('http', hooks_daemon.HttpHooksCallbackDaemon), |
@@ -272,12 +277,15 b' class TestPrepareHooksDaemon(object):' | |||
|
272 | 277 | self, protocol, expected_class): |
|
273 | 278 | expected_extras = { |
|
274 | 279 | 'extra1': 'value1', |
|
280 | 'txn_id': 'txnid2', | |
|
275 | 281 | 'hooks_protocol': protocol.lower() |
|
276 | 282 | } |
|
277 | 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 | 286 | assert isinstance(callback, expected_class) |
|
280 |
|
|
|
287 | extras.pop('hooks_uri') | |
|
288 | expected_extras['time'] = extras['time'] | |
|
281 | 289 | assert extras == expected_extras |
|
282 | 290 | |
|
283 | 291 | @pytest.mark.parametrize('protocol', ( |
@@ -245,22 +245,16 b' def test_repo2db_mapper_enables_largefil' | |||
|
245 | 245 | repo = backend.create_repo() |
|
246 | 246 | repo_list = {repo.repo_name: 'test'} |
|
247 | 247 | with mock.patch('rhodecode.model.db.Repository.scm_instance') as scm_mock: |
|
248 | with mock.patch.multiple('rhodecode.model.scm.ScmModel', | |
|
249 | install_git_hook=mock.DEFAULT, | |
|
250 | install_svn_hooks=mock.DEFAULT): | |
|
251 | utils.repo2db_mapper(repo_list, remove_obsolete=False) | |
|
252 | _, kwargs = scm_mock.call_args | |
|
253 | assert kwargs['config'].get('extensions', 'largefiles') == '' | |
|
248 | utils.repo2db_mapper(repo_list, remove_obsolete=False) | |
|
249 | _, kwargs = scm_mock.call_args | |
|
250 | assert kwargs['config'].get('extensions', 'largefiles') == '' | |
|
254 | 251 | |
|
255 | 252 | |
|
256 | 253 | @pytest.mark.backends("git", "svn") |
|
257 | 254 | def test_repo2db_mapper_installs_hooks_for_repos_in_db(backend): |
|
258 | 255 | repo = backend.create_repo() |
|
259 | 256 | repo_list = {repo.repo_name: 'test'} |
|
260 | with mock.patch.object(ScmModel, 'install_hooks') as install_hooks_mock: | |
|
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) | |
|
257 | utils.repo2db_mapper(repo_list, remove_obsolete=False) | |
|
264 | 258 | |
|
265 | 259 | |
|
266 | 260 | @pytest.mark.backends("git", "svn") |
@@ -269,11 +263,7 b' def test_repo2db_mapper_installs_hooks_f' | |||
|
269 | 263 | RepoModel().delete(repo, fs_remove=False) |
|
270 | 264 | meta.Session().commit() |
|
271 | 265 | repo_list = {repo.repo_name: repo.scm_instance()} |
|
272 | with mock.patch.object(ScmModel, 'install_hooks') as install_hooks_mock: | |
|
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 | |
|
266 | utils.repo2db_mapper(repo_list, remove_obsolete=False) | |
|
277 | 267 | |
|
278 | 268 | |
|
279 | 269 | class TestPasswordChanged(object): |
@@ -18,10 +18,11 b'' | |||
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | import os | |
|
22 | import mock | |
|
23 | import pytest | |
|
21 | 24 | import tempfile |
|
22 | 25 | |
|
23 | import mock | |
|
24 | import pytest | |
|
25 | 26 | |
|
26 | 27 | from rhodecode.lib.exceptions import AttachedForksError |
|
27 | 28 | from rhodecode.lib.utils import make_db_config |
@@ -119,22 +120,22 b' class TestRepoModel(object):' | |||
|
119 | 120 | |
|
120 | 121 | @pytest.mark.backends("git", "svn") |
|
121 | 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 | 123 | repo = backend.create_repo() |
|
127 | 124 | repo_name = repo.repo_name |
|
128 | 125 | model = RepoModel() |
|
129 | 126 | repo_location = tempfile.mkdtemp() |
|
130 | 127 | model.repos_path = repo_location |
|
131 | method = hook_methods[backend.alias] | |
|
132 | with mock.patch.object(ScmModel, method) as hooks_mock: | |
|
133 | model._create_filesystem_repo( | |
|
134 | repo_name, backend.alias, repo_group='', clone_uri=None) | |
|
135 | assert hooks_mock.call_count == 1 | |
|
136 | hook_args, hook_kwargs = hooks_mock.call_args | |
|
137 | assert hook_args[0].name == repo_name | |
|
128 | repo = model._create_filesystem_repo( | |
|
129 | repo_name, backend.alias, repo_group='', clone_uri=None) | |
|
130 | ||
|
131 | hooks = { | |
|
132 | 'svn': ('pre-commit', 'post-commit'), | |
|
133 | 'git': ('pre-receive', 'post-receive'), | |
|
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 | 140 | @pytest.mark.parametrize("use_global_config, repo_name_passed", [ |
|
140 | 141 | (True, False), |
@@ -194,143 +194,3 b' def test_get_non_unicode_reference(backe' | |||
|
194 | 194 | u'tag:Ad\xc4\xb1n\xc4\xb1'] |
|
195 | 195 | |
|
196 | 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 |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now