##// END OF EJS Templates
ruff: fix multiple statements on one line
Mads Kiilerich -
r8762:0e7dab99 default
parent child Browse files
Show More
@@ -1,545 +1,546 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils2
15 kallithea.lib.utils2
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Some simple helper functions.
18 Some simple helper functions.
19 Note: all these functions should be independent of Kallithea classes, i.e.
19 Note: all these functions should be independent of Kallithea classes, i.e.
20 models, controllers, etc. to prevent import cycles.
20 models, controllers, etc. to prevent import cycles.
21
21
22 This file was forked by the Kallithea project in July 2014.
22 This file was forked by the Kallithea project in July 2014.
23 Original author and date, and relevant copyright and licensing information is below:
23 Original author and date, and relevant copyright and licensing information is below:
24 :created_on: Jan 5, 2011
24 :created_on: Jan 5, 2011
25 :author: marcink
25 :author: marcink
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :license: GPLv3, see LICENSE.md for more details.
27 :license: GPLv3, see LICENSE.md for more details.
28 """
28 """
29
29
30 import binascii
30 import binascii
31 import datetime
31 import datetime
32 import hashlib
32 import hashlib
33 import json
33 import json
34 import logging
34 import logging
35 import os
35 import os
36 import re
36 import re
37 import string
37 import string
38 import sys
38 import sys
39 import time
39 import time
40 import urllib.parse
40 import urllib.parse
41 from distutils.version import StrictVersion
41 from distutils.version import StrictVersion
42
42
43 import bcrypt
43 import bcrypt
44 import urlobject
44 import urlobject
45 from sqlalchemy.engine import url as sa_url
45 from sqlalchemy.engine import url as sa_url
46 from sqlalchemy.exc import ArgumentError
46 from sqlalchemy.exc import ArgumentError
47 from tg import tmpl_context
47 from tg import tmpl_context
48 from tg.support.converters import asbool, aslist
48 from tg.support.converters import asbool, aslist
49 from webhelpers2.text import collapse, remove_formatting, strip_tags
49 from webhelpers2.text import collapse, remove_formatting, strip_tags
50
50
51 import kallithea
51 import kallithea
52 from kallithea.lib import webutils
52 from kallithea.lib import webutils
53 from kallithea.lib.vcs.backends.base import BaseRepository, EmptyChangeset
53 from kallithea.lib.vcs.backends.base import BaseRepository, EmptyChangeset
54 from kallithea.lib.vcs.backends.git.repository import GitRepository
54 from kallithea.lib.vcs.backends.git.repository import GitRepository
55 from kallithea.lib.vcs.conf import settings
55 from kallithea.lib.vcs.conf import settings
56 from kallithea.lib.vcs.exceptions import RepositoryError
56 from kallithea.lib.vcs.exceptions import RepositoryError
57 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str # re-export
57 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str # re-export
58 from kallithea.lib.vcs.utils.lazy import LazyProperty
58 from kallithea.lib.vcs.utils.lazy import LazyProperty
59
59
60
60
61 try:
61 try:
62 import pwd
62 import pwd
63 except ImportError:
63 except ImportError:
64 pass
64 pass
65
65
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 # mute pyflakes "imported but unused"
70 # mute pyflakes "imported but unused"
71 assert asbool
71 assert asbool
72 assert aslist
72 assert aslist
73 assert ascii_bytes
73 assert ascii_bytes
74 assert ascii_str
74 assert ascii_str
75 assert safe_bytes
75 assert safe_bytes
76 assert safe_str
76 assert safe_str
77 assert LazyProperty
77 assert LazyProperty
78
78
79
79
80 # get current umask value without changing it
80 # get current umask value without changing it
81 umask = os.umask(0)
81 umask = os.umask(0)
82 os.umask(umask)
82 os.umask(umask)
83
83
84
84
85 def convert_line_endings(line, mode):
85 def convert_line_endings(line, mode):
86 """
86 """
87 Converts a given line "line end" according to given mode
87 Converts a given line "line end" according to given mode
88
88
89 Available modes are::
89 Available modes are::
90 0 - Unix
90 0 - Unix
91 1 - Mac
91 1 - Mac
92 2 - DOS
92 2 - DOS
93
93
94 :param line: given line to convert
94 :param line: given line to convert
95 :param mode: mode to convert to
95 :param mode: mode to convert to
96 :rtype: str
96 :rtype: str
97 :return: converted line according to mode
97 :return: converted line according to mode
98 """
98 """
99 if mode == 0:
99 if mode == 0:
100 line = line.replace('\r\n', '\n')
100 line = line.replace('\r\n', '\n')
101 line = line.replace('\r', '\n')
101 line = line.replace('\r', '\n')
102 elif mode == 1:
102 elif mode == 1:
103 line = line.replace('\r\n', '\r')
103 line = line.replace('\r\n', '\r')
104 line = line.replace('\n', '\r')
104 line = line.replace('\n', '\r')
105 elif mode == 2:
105 elif mode == 2:
106 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
106 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
107 return line
107 return line
108
108
109
109
110 def detect_mode(line, default):
110 def detect_mode(line, default):
111 """
111 """
112 Detects line break for given line, if line break couldn't be found
112 Detects line break for given line, if line break couldn't be found
113 given default value is returned
113 given default value is returned
114
114
115 :param line: str line
115 :param line: str line
116 :param default: default
116 :param default: default
117 :rtype: int
117 :rtype: int
118 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
118 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
119 """
119 """
120 if line.endswith('\r\n'):
120 if line.endswith('\r\n'):
121 return 2
121 return 2
122 elif line.endswith('\n'):
122 elif line.endswith('\n'):
123 return 0
123 return 0
124 elif line.endswith('\r'):
124 elif line.endswith('\r'):
125 return 1
125 return 1
126 else:
126 else:
127 return default
127 return default
128
128
129
129
130 def generate_api_key():
130 def generate_api_key():
131 """
131 """
132 Generates a random (presumably unique) API key.
132 Generates a random (presumably unique) API key.
133
133
134 This value is used in URLs and "Bearer" HTTP Authorization headers,
134 This value is used in URLs and "Bearer" HTTP Authorization headers,
135 which in practice means it should only contain URL-safe characters
135 which in practice means it should only contain URL-safe characters
136 (RFC 3986):
136 (RFC 3986):
137
137
138 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
138 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
139 """
139 """
140 # Hexadecimal certainly qualifies as URL-safe.
140 # Hexadecimal certainly qualifies as URL-safe.
141 return ascii_str(binascii.hexlify(os.urandom(20)))
141 return ascii_str(binascii.hexlify(os.urandom(20)))
142
142
143
143
144 def safe_int(val, default=None):
144 def safe_int(val, default=None):
145 """
145 """
146 Returns int() of val if val is not convertable to int use default
146 Returns int() of val if val is not convertable to int use default
147 instead
147 instead
148
148
149 :param val:
149 :param val:
150 :param default:
150 :param default:
151 """
151 """
152 try:
152 try:
153 val = int(val)
153 val = int(val)
154 except (ValueError, TypeError):
154 except (ValueError, TypeError):
155 val = default
155 val = default
156 return val
156 return val
157
157
158
158
159 def remove_suffix(s, suffix):
159 def remove_suffix(s, suffix):
160 if s.endswith(suffix):
160 if s.endswith(suffix):
161 s = s[:-1 * len(suffix)]
161 s = s[:-1 * len(suffix)]
162 return s
162 return s
163
163
164
164
165 def remove_prefix(s, prefix):
165 def remove_prefix(s, prefix):
166 if s.startswith(prefix):
166 if s.startswith(prefix):
167 s = s[len(prefix):]
167 s = s[len(prefix):]
168 return s
168 return s
169
169
170
170
171 def uri_filter(uri):
171 def uri_filter(uri):
172 """
172 """
173 Removes user:password from given url string
173 Removes user:password from given url string
174
174
175 :param uri:
175 :param uri:
176 :rtype: str
176 :rtype: str
177 :returns: filtered list of strings
177 :returns: filtered list of strings
178 """
178 """
179 if not uri:
179 if not uri:
180 return []
180 return []
181
181
182 proto = ''
182 proto = ''
183
183
184 for pat in ('https://', 'http://', 'git://'):
184 for pat in ('https://', 'http://', 'git://'):
185 if uri.startswith(pat):
185 if uri.startswith(pat):
186 uri = uri[len(pat):]
186 uri = uri[len(pat):]
187 proto = pat
187 proto = pat
188 break
188 break
189
189
190 # remove passwords and username
190 # remove passwords and username
191 uri = uri[uri.find('@') + 1:]
191 uri = uri[uri.find('@') + 1:]
192
192
193 # get the port
193 # get the port
194 cred_pos = uri.find(':')
194 cred_pos = uri.find(':')
195 if cred_pos == -1:
195 if cred_pos == -1:
196 host, port = uri, None
196 host, port = uri, None
197 else:
197 else:
198 host, port = uri[:cred_pos], uri[cred_pos + 1:]
198 host, port = uri[:cred_pos], uri[cred_pos + 1:]
199
199
200 return [_f for _f in [proto, host, port] if _f]
200 return [_f for _f in [proto, host, port] if _f]
201
201
202
202
203 def credentials_filter(uri):
203 def credentials_filter(uri):
204 """
204 """
205 Returns a url with removed credentials
205 Returns a url with removed credentials
206
206
207 :param uri:
207 :param uri:
208 """
208 """
209
209
210 uri = uri_filter(uri)
210 uri = uri_filter(uri)
211 # check if we have port
211 # check if we have port
212 if len(uri) > 2 and uri[2]:
212 if len(uri) > 2 and uri[2]:
213 uri[2] = ':' + uri[2]
213 uri[2] = ':' + uri[2]
214
214
215 return ''.join(uri)
215 return ''.join(uri)
216
216
217
217
218 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
218 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
219 parsed_url = urlobject.URLObject(prefix_url)
219 parsed_url = urlobject.URLObject(prefix_url)
220 prefix = urllib.parse.unquote(parsed_url.path.rstrip('/'))
220 prefix = urllib.parse.unquote(parsed_url.path.rstrip('/'))
221 try:
221 try:
222 system_user = pwd.getpwuid(os.getuid()).pw_name
222 system_user = pwd.getpwuid(os.getuid()).pw_name
223 except NameError: # TODO: support all systems - especially Windows
223 except NameError: # TODO: support all systems - especially Windows
224 system_user = 'kallithea' # hardcoded default value ...
224 system_user = 'kallithea' # hardcoded default value ...
225 args = {
225 args = {
226 'scheme': parsed_url.scheme,
226 'scheme': parsed_url.scheme,
227 'user': urllib.parse.quote(username or ''),
227 'user': urllib.parse.quote(username or ''),
228 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
228 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
229 'prefix': prefix, # undocumented, empty or starting with /
229 'prefix': prefix, # undocumented, empty or starting with /
230 'repo': repo_name,
230 'repo': repo_name,
231 'repoid': str(repo_id),
231 'repoid': str(repo_id),
232 'system_user': system_user,
232 'system_user': system_user,
233 'hostname': parsed_url.hostname,
233 'hostname': parsed_url.hostname,
234 }
234 }
235 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
235 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
236
236
237 # remove leading @ sign if it's present. Case of empty user
237 # remove leading @ sign if it's present. Case of empty user
238 url_obj = urlobject.URLObject(url)
238 url_obj = urlobject.URLObject(url)
239 if not url_obj.username:
239 if not url_obj.username:
240 url_obj = url_obj.with_username(None)
240 url_obj = url_obj.with_username(None)
241
241
242 return str(url_obj)
242 return str(url_obj)
243
243
244
244
245 def short_ref_name(ref_type, ref_name):
245 def short_ref_name(ref_type, ref_name):
246 """Return short description of PR ref - revs will be truncated"""
246 """Return short description of PR ref - revs will be truncated"""
247 if ref_type == 'rev':
247 if ref_type == 'rev':
248 return ref_name[:12]
248 return ref_name[:12]
249 return ref_name
249 return ref_name
250
250
251
251
252 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
252 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
253 """
253 """
254 Return full markup for a PR ref to changeset_home for a changeset.
254 Return full markup for a PR ref to changeset_home for a changeset.
255 If ref_type is 'branch', it will link to changelog.
255 If ref_type is 'branch', it will link to changelog.
256 ref_name is shortened if ref_type is 'rev'.
256 ref_name is shortened if ref_type is 'rev'.
257 if rev is specified, show it too, explicitly linking to that revision.
257 if rev is specified, show it too, explicitly linking to that revision.
258 """
258 """
259 txt = short_ref_name(ref_type, ref_name)
259 txt = short_ref_name(ref_type, ref_name)
260 if ref_type == 'branch':
260 if ref_type == 'branch':
261 u = webutils.url('changelog_home', repo_name=repo_name, branch=ref_name)
261 u = webutils.url('changelog_home', repo_name=repo_name, branch=ref_name)
262 else:
262 else:
263 u = webutils.url('changeset_home', repo_name=repo_name, revision=ref_name)
263 u = webutils.url('changeset_home', repo_name=repo_name, revision=ref_name)
264 l = webutils.link_to(repo_name + '#' + txt, u)
264 l = webutils.link_to(repo_name + '#' + txt, u)
265 if rev and ref_type != 'rev':
265 if rev and ref_type != 'rev':
266 l = webutils.literal('%s (%s)' % (l, webutils.link_to(rev[:12], webutils.url('changeset_home', repo_name=repo_name, revision=rev))))
266 l = webutils.literal('%s (%s)' % (l, webutils.link_to(rev[:12], webutils.url('changeset_home', repo_name=repo_name, revision=rev))))
267 return l
267 return l
268
268
269
269
270 def get_changeset_safe(repo, rev):
270 def get_changeset_safe(repo, rev):
271 """
271 """
272 Safe version of get_changeset if this changeset doesn't exists for a
272 Safe version of get_changeset if this changeset doesn't exists for a
273 repo it returns a Dummy one instead
273 repo it returns a Dummy one instead
274
274
275 :param repo:
275 :param repo:
276 :param rev:
276 :param rev:
277 """
277 """
278 if not isinstance(repo, BaseRepository):
278 if not isinstance(repo, BaseRepository):
279 raise Exception('You must pass an Repository '
279 raise Exception('You must pass an Repository '
280 'object as first argument got %s' % type(repo))
280 'object as first argument got %s' % type(repo))
281
281
282 try:
282 try:
283 cs = repo.get_changeset(rev)
283 cs = repo.get_changeset(rev)
284 except (RepositoryError, LookupError):
284 except (RepositoryError, LookupError):
285 cs = EmptyChangeset(requested_revision=rev)
285 cs = EmptyChangeset(requested_revision=rev)
286 return cs
286 return cs
287
287
288
288
289 def datetime_to_time(dt):
289 def datetime_to_time(dt):
290 if dt:
290 if dt:
291 return time.mktime(dt.timetuple())
291 return time.mktime(dt.timetuple())
292
292
293
293
294 def time_to_datetime(tm):
294 def time_to_datetime(tm):
295 if tm:
295 if tm:
296 if isinstance(tm, str):
296 if isinstance(tm, str):
297 try:
297 try:
298 tm = float(tm)
298 tm = float(tm)
299 except ValueError:
299 except ValueError:
300 return
300 return
301 return datetime.datetime.fromtimestamp(tm)
301 return datetime.datetime.fromtimestamp(tm)
302
302
303
303
304 class AttributeDict(dict):
304 class AttributeDict(dict):
305 def __getattr__(self, attr):
305 def __getattr__(self, attr):
306 return self.get(attr, None)
306 return self.get(attr, None)
307 __setattr__ = dict.__setitem__
307 __setattr__ = dict.__setitem__
308 __delattr__ = dict.__delitem__
308 __delattr__ = dict.__delitem__
309
309
310
310
311 def obfuscate_url_pw(engine):
311 def obfuscate_url_pw(engine):
312 try:
312 try:
313 _url = sa_url.make_url(engine or '')
313 _url = sa_url.make_url(engine or '')
314 except ArgumentError:
314 except ArgumentError:
315 return engine
315 return engine
316 if _url.password:
316 if _url.password:
317 _url.password = 'XXXXX'
317 _url.password = 'XXXXX'
318 return str(_url)
318 return str(_url)
319
319
320
320
321 class HookEnvironmentError(Exception): pass
321 class HookEnvironmentError(Exception):
322 pass
322
323
323
324
324 def get_hook_environment():
325 def get_hook_environment():
325 """
326 """
326 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
327 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
327 variable.
328 variable.
328
329
329 Called early in Git out-of-process hooks to get .ini config path so the
330 Called early in Git out-of-process hooks to get .ini config path so the
330 basic environment can be configured properly. Also used in all hooks to get
331 basic environment can be configured properly. Also used in all hooks to get
331 information about the action that triggered it.
332 information about the action that triggered it.
332 """
333 """
333
334
334 try:
335 try:
335 kallithea_extras = os.environ['KALLITHEA_EXTRAS']
336 kallithea_extras = os.environ['KALLITHEA_EXTRAS']
336 except KeyError:
337 except KeyError:
337 raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found")
338 raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found")
338
339
339 extras = json.loads(kallithea_extras)
340 extras = json.loads(kallithea_extras)
340 for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']:
341 for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']:
341 try:
342 try:
342 extras[k]
343 extras[k]
343 except KeyError:
344 except KeyError:
344 raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
345 raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
345
346
346 return AttributeDict(extras)
347 return AttributeDict(extras)
347
348
348
349
349 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
350 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
350 """Prepare global context for running hooks by serializing data in the
351 """Prepare global context for running hooks by serializing data in the
351 global KALLITHEA_EXTRAS environment variable.
352 global KALLITHEA_EXTRAS environment variable.
352
353
353 Most importantly, this allow Git hooks to do proper logging and updating of
354 Most importantly, this allow Git hooks to do proper logging and updating of
354 caches after pushes.
355 caches after pushes.
355
356
356 Must always be called before anything with hooks are invoked.
357 Must always be called before anything with hooks are invoked.
357 """
358 """
358 extras = {
359 extras = {
359 'ip': ip_addr, # used in action_logger
360 'ip': ip_addr, # used in action_logger
360 'username': username,
361 'username': username,
361 'action': action or 'push_local', # used in process_pushed_raw_ids action_logger
362 'action': action or 'push_local', # used in process_pushed_raw_ids action_logger
362 'repository': repo_name,
363 'repository': repo_name,
363 'scm': repo_alias,
364 'scm': repo_alias,
364 'config': kallithea.CONFIG['__file__'], # used by git hook to read config
365 'config': kallithea.CONFIG['__file__'], # used by git hook to read config
365 }
366 }
366 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
367 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
367
368
368
369
369 def get_current_authuser():
370 def get_current_authuser():
370 """
371 """
371 Gets kallithea user from threadlocal tmpl_context variable if it's
372 Gets kallithea user from threadlocal tmpl_context variable if it's
372 defined, else returns None.
373 defined, else returns None.
373 """
374 """
374 try:
375 try:
375 return getattr(tmpl_context, 'authuser', None)
376 return getattr(tmpl_context, 'authuser', None)
376 except TypeError: # No object (name: context) has been registered for this thread
377 except TypeError: # No object (name: context) has been registered for this thread
377 return None
378 return None
378
379
379
380
380 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
381 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
381 return _cleanstringsub('_', s).rstrip('_')
382 return _cleanstringsub('_', s).rstrip('_')
382
383
383
384
384 def recursive_replace(str_, replace=' '):
385 def recursive_replace(str_, replace=' '):
385 """
386 """
386 Recursive replace of given sign to just one instance
387 Recursive replace of given sign to just one instance
387
388
388 :param str_: given string
389 :param str_: given string
389 :param replace: char to find and replace multiple instances
390 :param replace: char to find and replace multiple instances
390
391
391 Examples::
392 Examples::
392 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
393 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
393 'Mighty-Mighty-Bo-sstones'
394 'Mighty-Mighty-Bo-sstones'
394 """
395 """
395
396
396 if str_.find(replace * 2) == -1:
397 if str_.find(replace * 2) == -1:
397 return str_
398 return str_
398 else:
399 else:
399 str_ = str_.replace(replace * 2, replace)
400 str_ = str_.replace(replace * 2, replace)
400 return recursive_replace(str_, replace)
401 return recursive_replace(str_, replace)
401
402
402
403
403 def repo_name_slug(value):
404 def repo_name_slug(value):
404 """
405 """
405 Return slug of name of repository
406 Return slug of name of repository
406 This function is called on each creation/modification
407 This function is called on each creation/modification
407 of repository to prevent bad names in repo
408 of repository to prevent bad names in repo
408 """
409 """
409
410
410 slug = remove_formatting(value)
411 slug = remove_formatting(value)
411 slug = strip_tags(slug)
412 slug = strip_tags(slug)
412
413
413 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
414 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
414 slug = slug.replace(c, '-')
415 slug = slug.replace(c, '-')
415 slug = recursive_replace(slug, '-')
416 slug = recursive_replace(slug, '-')
416 slug = collapse(slug, '-')
417 slug = collapse(slug, '-')
417 return slug
418 return slug
418
419
419
420
420 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
421 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
421 while True:
422 while True:
422 ok = input(prompt)
423 ok = input(prompt)
423 if ok in ('y', 'ye', 'yes'):
424 if ok in ('y', 'ye', 'yes'):
424 return True
425 return True
425 if ok in ('n', 'no', 'nop', 'nope'):
426 if ok in ('n', 'no', 'nop', 'nope'):
426 return False
427 return False
427 retries = retries - 1
428 retries = retries - 1
428 if retries < 0:
429 if retries < 0:
429 raise IOError
430 raise IOError
430 print(complaint)
431 print(complaint)
431
432
432
433
433 class PasswordGenerator(object):
434 class PasswordGenerator(object):
434 """
435 """
435 This is a simple class for generating password from different sets of
436 This is a simple class for generating password from different sets of
436 characters
437 characters
437 usage::
438 usage::
438
439
439 passwd_gen = PasswordGenerator()
440 passwd_gen = PasswordGenerator()
440 #print 8-letter password containing only big and small letters
441 #print 8-letter password containing only big and small letters
441 of alphabet
442 of alphabet
442 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
443 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
443 """
444 """
444 ALPHABETS_NUM = r'''1234567890'''
445 ALPHABETS_NUM = r'''1234567890'''
445 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
446 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
446 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
447 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
447 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
448 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
448 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
449 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
449 + ALPHABETS_NUM + ALPHABETS_SPECIAL
450 + ALPHABETS_NUM + ALPHABETS_SPECIAL
450 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
451 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
451 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
452 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
452 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
453 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
453 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
454 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
454
455
455 def gen_password(self, length, alphabet=ALPHABETS_FULL):
456 def gen_password(self, length, alphabet=ALPHABETS_FULL):
456 assert len(alphabet) <= 256, alphabet
457 assert len(alphabet) <= 256, alphabet
457 l = []
458 l = []
458 while len(l) < length:
459 while len(l) < length:
459 i = ord(os.urandom(1))
460 i = ord(os.urandom(1))
460 if i < len(alphabet):
461 if i < len(alphabet):
461 l.append(alphabet[i])
462 l.append(alphabet[i])
462 return ''.join(l)
463 return ''.join(l)
463
464
464
465
465 def get_crypt_password(password):
466 def get_crypt_password(password):
466 """
467 """
467 Cryptographic function used for bcrypt password hashing.
468 Cryptographic function used for bcrypt password hashing.
468
469
469 :param password: password to hash
470 :param password: password to hash
470 """
471 """
471 return ascii_str(bcrypt.hashpw(safe_bytes(password), bcrypt.gensalt(10)))
472 return ascii_str(bcrypt.hashpw(safe_bytes(password), bcrypt.gensalt(10)))
472
473
473
474
474 def check_password(password, hashed):
475 def check_password(password, hashed):
475 """
476 """
476 Checks password match the hashed value using bcrypt.
477 Checks password match the hashed value using bcrypt.
477 Remains backwards compatible and accept plain sha256 hashes which used to
478 Remains backwards compatible and accept plain sha256 hashes which used to
478 be used on Windows.
479 be used on Windows.
479
480
480 :param password: password
481 :param password: password
481 :param hashed: password in hashed form
482 :param hashed: password in hashed form
482 """
483 """
483 # sha256 hashes will always be 64 hex chars
484 # sha256 hashes will always be 64 hex chars
484 # bcrypt hashes will always contain $ (and be shorter)
485 # bcrypt hashes will always contain $ (and be shorter)
485 if len(hashed) == 64 and all(x in string.hexdigits for x in hashed):
486 if len(hashed) == 64 and all(x in string.hexdigits for x in hashed):
486 return hashlib.sha256(password).hexdigest() == hashed
487 return hashlib.sha256(password).hexdigest() == hashed
487 try:
488 try:
488 return bcrypt.checkpw(safe_bytes(password), ascii_bytes(hashed))
489 return bcrypt.checkpw(safe_bytes(password), ascii_bytes(hashed))
489 except ValueError as e:
490 except ValueError as e:
490 # bcrypt will throw ValueError 'Invalid hashed_password salt' on all password errors
491 # bcrypt will throw ValueError 'Invalid hashed_password salt' on all password errors
491 log.error('error from bcrypt checking password: %s', e)
492 log.error('error from bcrypt checking password: %s', e)
492 return False
493 return False
493 log.error('check_password failed - no method found for hash length %s', len(hashed))
494 log.error('check_password failed - no method found for hash length %s', len(hashed))
494 return False
495 return False
495
496
496
497
497 git_req_ver = StrictVersion('1.7.4')
498 git_req_ver = StrictVersion('1.7.4')
498
499
499 def check_git_version():
500 def check_git_version():
500 """
501 """
501 Checks what version of git is installed on the system, and raise a system exit
502 Checks what version of git is installed on the system, and raise a system exit
502 if it's too old for Kallithea to work properly.
503 if it's too old for Kallithea to work properly.
503 """
504 """
504 if 'git' not in kallithea.BACKENDS:
505 if 'git' not in kallithea.BACKENDS:
505 return None
506 return None
506
507
507 if not settings.GIT_EXECUTABLE_PATH:
508 if not settings.GIT_EXECUTABLE_PATH:
508 log.warning('No git executable configured - check "git_path" in the ini file.')
509 log.warning('No git executable configured - check "git_path" in the ini file.')
509 return None
510 return None
510
511
511 try:
512 try:
512 stdout, stderr = GitRepository._run_git_command(['--version'])
513 stdout, stderr = GitRepository._run_git_command(['--version'])
513 except RepositoryError as e:
514 except RepositoryError as e:
514 # message will already have been logged as error
515 # message will already have been logged as error
515 log.warning('No working git executable found - check "git_path" in the ini file.')
516 log.warning('No working git executable found - check "git_path" in the ini file.')
516 return None
517 return None
517
518
518 if stderr:
519 if stderr:
519 log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
520 log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
520
521
521 if not stdout:
522 if not stdout:
522 log.warning('No working git executable found - check "git_path" in the ini file.')
523 log.warning('No working git executable found - check "git_path" in the ini file.')
523 return None
524 return None
524
525
525 output = safe_str(stdout).strip()
526 output = safe_str(stdout).strip()
526 m = re.search(r"\d+.\d+.\d+", output)
527 m = re.search(r"\d+.\d+.\d+", output)
527 if m:
528 if m:
528 ver = StrictVersion(m.group(0))
529 ver = StrictVersion(m.group(0))
529 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
530 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
530 settings.GIT_EXECUTABLE_PATH, ver, output)
531 settings.GIT_EXECUTABLE_PATH, ver, output)
531 if ver < git_req_ver:
532 if ver < git_req_ver:
532 log.error('Kallithea detected %s version %s, which is too old '
533 log.error('Kallithea detected %s version %s, which is too old '
533 'for the system to function properly. '
534 'for the system to function properly. '
534 'Please upgrade to version %s or later. '
535 'Please upgrade to version %s or later. '
535 'If you strictly need Mercurial repositories, you can '
536 'If you strictly need Mercurial repositories, you can '
536 'clear the "git_path" setting in the ini file.',
537 'clear the "git_path" setting in the ini file.',
537 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
538 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
538 log.error("Terminating ...")
539 log.error("Terminating ...")
539 sys.exit(1)
540 sys.exit(1)
540 else:
541 else:
541 ver = StrictVersion('0.0.0')
542 ver = StrictVersion('0.0.0')
542 log.warning('Error finding version number in "%s --version" stdout:\n%s',
543 log.warning('Error finding version number in "%s --version" stdout:\n%s',
543 settings.GIT_EXECUTABLE_PATH, output)
544 settings.GIT_EXECUTABLE_PATH, output)
544
545
545 return ver
546 return ver
@@ -1,641 +1,642 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Test suite for vcs push/pull operations.
15 Test suite for vcs push/pull operations.
16
16
17 The tests need Git > 1.8.1.
17 The tests need Git > 1.8.1.
18
18
19 This file was forked by the Kallithea project in July 2014.
19 This file was forked by the Kallithea project in July 2014.
20 Original author and date, and relevant copyright and licensing information is below:
20 Original author and date, and relevant copyright and licensing information is below:
21 :created_on: Dec 30, 2010
21 :created_on: Dec 30, 2010
22 :author: marcink
22 :author: marcink
23 :copyright: (c) 2013 RhodeCode GmbH, and others.
23 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :license: GPLv3, see LICENSE.md for more details.
24 :license: GPLv3, see LICENSE.md for more details.
25
25
26 """
26 """
27
27
28 import json
28 import json
29 import os
29 import os
30 import re
30 import re
31 import tempfile
31 import tempfile
32 import time
32 import time
33 import urllib.request
33 import urllib.request
34 from subprocess import PIPE, Popen
34 from subprocess import PIPE, Popen
35 from tempfile import _RandomNameSequence
35 from tempfile import _RandomNameSequence
36
36
37 import pytest
37 import pytest
38
38
39 import kallithea
39 import kallithea
40 from kallithea.lib.utils2 import ascii_bytes, safe_str
40 from kallithea.lib.utils2 import ascii_bytes, safe_str
41 from kallithea.model import db, meta
41 from kallithea.model import db, meta
42 from kallithea.model.ssh_key import SshKeyModel
42 from kallithea.model.ssh_key import SshKeyModel
43 from kallithea.model.user import UserModel
43 from kallithea.model.user import UserModel
44 from kallithea.tests import base
44 from kallithea.tests import base
45 from kallithea.tests.fixture import Fixture
45 from kallithea.tests.fixture import Fixture
46
46
47
47
48 DEBUG = True
48 DEBUG = True
49 HOST = '127.0.0.1:4999' # test host
49 HOST = '127.0.0.1:4999' # test host
50
50
51 fixture = Fixture()
51 fixture = Fixture()
52
52
53
53
54 # Parameterize different kinds of VCS testing - both the kind of VCS and the
54 # Parameterize different kinds of VCS testing - both the kind of VCS and the
55 # access method (HTTP/SSH)
55 # access method (HTTP/SSH)
56
56
57 # Mixin for using HTTP and SSH URLs
57 # Mixin for using HTTP and SSH URLs
58 class HttpVcsTest(object):
58 class HttpVcsTest(object):
59 @staticmethod
59 @staticmethod
60 def repo_url_param(webserver, repo_name, **kwargs):
60 def repo_url_param(webserver, repo_name, **kwargs):
61 return webserver.repo_url(repo_name, **kwargs)
61 return webserver.repo_url(repo_name, **kwargs)
62
62
63 class SshVcsTest(object):
63 class SshVcsTest(object):
64 public_keys = {
64 public_keys = {
65 base.TEST_USER_REGULAR_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== kallithea@localhost',
65 base.TEST_USER_REGULAR_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== kallithea@localhost',
66 base.TEST_USER_ADMIN_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUq== kallithea@localhost',
66 base.TEST_USER_ADMIN_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUq== kallithea@localhost',
67 }
67 }
68
68
69 @classmethod
69 @classmethod
70 def repo_url_param(cls, webserver, repo_name, username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS, client_ip=base.IP_ADDR):
70 def repo_url_param(cls, webserver, repo_name, username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS, client_ip=base.IP_ADDR):
71 user = db.User.get_by_username(username)
71 user = db.User.get_by_username(username)
72 if user.ssh_keys:
72 if user.ssh_keys:
73 ssh_key = user.ssh_keys[0]
73 ssh_key = user.ssh_keys[0]
74 else:
74 else:
75 sshkeymodel = SshKeyModel()
75 sshkeymodel = SshKeyModel()
76 ssh_key = sshkeymodel.create(user, 'test key', cls.public_keys[user.username])
76 ssh_key = sshkeymodel.create(user, 'test key', cls.public_keys[user.username])
77 meta.Session().commit()
77 meta.Session().commit()
78
78
79 return cls._ssh_param(repo_name, user, ssh_key, client_ip)
79 return cls._ssh_param(repo_name, user, ssh_key, client_ip)
80
80
81 # Mixins for using Mercurial and Git
81 # Mixins for using Mercurial and Git
82 class HgVcsTest(object):
82 class HgVcsTest(object):
83 repo_type = 'hg'
83 repo_type = 'hg'
84 repo_name = base.HG_REPO
84 repo_name = base.HG_REPO
85
85
86 class GitVcsTest(object):
86 class GitVcsTest(object):
87 repo_type = 'git'
87 repo_type = 'git'
88 repo_name = base.GIT_REPO
88 repo_name = base.GIT_REPO
89
89
90 # Combine mixins to give the combinations we want to parameterize tests with
90 # Combine mixins to give the combinations we want to parameterize tests with
91 class HgHttpVcsTest(HgVcsTest, HttpVcsTest):
91 class HgHttpVcsTest(HgVcsTest, HttpVcsTest):
92 pass
92 pass
93
93
94 class GitHttpVcsTest(GitVcsTest, HttpVcsTest):
94 class GitHttpVcsTest(GitVcsTest, HttpVcsTest):
95 pass
95 pass
96
96
97 class HgSshVcsTest(HgVcsTest, SshVcsTest):
97 class HgSshVcsTest(HgVcsTest, SshVcsTest):
98 @staticmethod
98 @staticmethod
99 def _ssh_param(repo_name, user, ssh_key, client_ip):
99 def _ssh_param(repo_name, user, ssh_key, client_ip):
100 # Specify a custom ssh command on the command line
100 # Specify a custom ssh command on the command line
101 return r"""--config ui.ssh="bash -c 'SSH_ORIGINAL_COMMAND=\"\$2\" SSH_CONNECTION=\"%s 1024 127.0.0.1 22\" kallithea-cli ssh-serve -c %s %s %s' --" ssh://someuser@somehost/%s""" % (
101 return r"""--config ui.ssh="bash -c 'SSH_ORIGINAL_COMMAND=\"\$2\" SSH_CONNECTION=\"%s 1024 127.0.0.1 22\" kallithea-cli ssh-serve -c %s %s %s' --" ssh://someuser@somehost/%s""" % (
102 client_ip,
102 client_ip,
103 kallithea.CONFIG['__file__'],
103 kallithea.CONFIG['__file__'],
104 user.user_id,
104 user.user_id,
105 ssh_key.user_ssh_key_id,
105 ssh_key.user_ssh_key_id,
106 repo_name)
106 repo_name)
107
107
108 class GitSshVcsTest(GitVcsTest, SshVcsTest):
108 class GitSshVcsTest(GitVcsTest, SshVcsTest):
109 @staticmethod
109 @staticmethod
110 def _ssh_param(repo_name, user, ssh_key, client_ip):
110 def _ssh_param(repo_name, user, ssh_key, client_ip):
111 # Set a custom ssh command in the global environment
111 # Set a custom ssh command in the global environment
112 os.environ['GIT_SSH_COMMAND'] = r"""bash -c 'SSH_ORIGINAL_COMMAND="$2" SSH_CONNECTION="%s 1024 127.0.0.1 22" kallithea-cli ssh-serve -c %s %s %s' --""" % (
112 os.environ['GIT_SSH_COMMAND'] = r"""bash -c 'SSH_ORIGINAL_COMMAND="$2" SSH_CONNECTION="%s 1024 127.0.0.1 22" kallithea-cli ssh-serve -c %s %s %s' --""" % (
113 client_ip,
113 client_ip,
114 kallithea.CONFIG['__file__'],
114 kallithea.CONFIG['__file__'],
115 user.user_id,
115 user.user_id,
116 ssh_key.user_ssh_key_id)
116 ssh_key.user_ssh_key_id)
117 return "ssh://someuser@somehost/%s""" % repo_name
117 return "ssh://someuser@somehost/%s""" % repo_name
118
118
119 parametrize_vcs_test = base.parametrize('vt', [
119 parametrize_vcs_test = base.parametrize('vt', [
120 HgHttpVcsTest,
120 HgHttpVcsTest,
121 GitHttpVcsTest,
121 GitHttpVcsTest,
122 HgSshVcsTest,
122 HgSshVcsTest,
123 GitSshVcsTest,
123 GitSshVcsTest,
124 ])
124 ])
125 parametrize_vcs_test_hg = base.parametrize('vt', [
125 parametrize_vcs_test_hg = base.parametrize('vt', [
126 HgHttpVcsTest,
126 HgHttpVcsTest,
127 HgSshVcsTest,
127 HgSshVcsTest,
128 ])
128 ])
129 parametrize_vcs_test_http = base.parametrize('vt', [
129 parametrize_vcs_test_http = base.parametrize('vt', [
130 HgHttpVcsTest,
130 HgHttpVcsTest,
131 GitHttpVcsTest,
131 GitHttpVcsTest,
132 ])
132 ])
133
133
134 class Command(object):
134 class Command(object):
135
135
136 def __init__(self, cwd):
136 def __init__(self, cwd):
137 self.cwd = cwd
137 self.cwd = cwd
138
138
139 def execute(self, *args, **environ):
139 def execute(self, *args, **environ):
140 """
140 """
141 Runs command on the system with given ``args`` using simple space
141 Runs command on the system with given ``args`` using simple space
142 join without safe quoting.
142 join without safe quoting.
143 """
143 """
144 command = ' '.join(args)
144 command = ' '.join(args)
145 ignoreReturnCode = environ.pop('ignoreReturnCode', False)
145 ignoreReturnCode = environ.pop('ignoreReturnCode', False)
146 if DEBUG:
146 if DEBUG:
147 print('*** CMD %s ***' % command)
147 print('*** CMD %s ***' % command)
148 testenv = dict(os.environ)
148 testenv = dict(os.environ)
149 testenv['LANG'] = 'en_US.UTF-8'
149 testenv['LANG'] = 'en_US.UTF-8'
150 testenv['LANGUAGE'] = 'en_US:en'
150 testenv['LANGUAGE'] = 'en_US:en'
151 testenv['HGPLAIN'] = ''
151 testenv['HGPLAIN'] = ''
152 testenv['HGRCPATH'] = ''
152 testenv['HGRCPATH'] = ''
153 testenv.update(environ)
153 testenv.update(environ)
154 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd, env=testenv)
154 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd, env=testenv)
155 stdout, stderr = p.communicate()
155 stdout, stderr = p.communicate()
156 if DEBUG:
156 if DEBUG:
157 if stdout:
157 if stdout:
158 print('stdout:', stdout)
158 print('stdout:', stdout)
159 if stderr:
159 if stderr:
160 print('stderr:', stderr)
160 print('stderr:', stderr)
161 if not ignoreReturnCode:
161 if not ignoreReturnCode:
162 assert p.returncode == 0
162 assert p.returncode == 0
163 return safe_str(stdout), safe_str(stderr)
163 return safe_str(stdout), safe_str(stderr)
164
164
165
165
166 def _get_tmp_dir(prefix='vcs_operations-', suffix=''):
166 def _get_tmp_dir(prefix='vcs_operations-', suffix=''):
167 return tempfile.mkdtemp(dir=base.TESTS_TMP_PATH, prefix=prefix, suffix=suffix)
167 return tempfile.mkdtemp(dir=base.TESTS_TMP_PATH, prefix=prefix, suffix=suffix)
168
168
169
169
170 def _add_files(vcs, dest_dir, files_no=3):
170 def _add_files(vcs, dest_dir, files_no=3):
171 """
171 """
172 Generate some files, add it to dest_dir repo and push back
172 Generate some files, add it to dest_dir repo and push back
173 vcs is git or hg and defines what VCS we want to make those files for
173 vcs is git or hg and defines what VCS we want to make those files for
174
174
175 :param vcs:
175 :param vcs:
176 :param dest_dir:
176 :param dest_dir:
177 """
177 """
178 added_file = '%ssetup.py' % next(_RandomNameSequence())
178 added_file = '%ssetup.py' % next(_RandomNameSequence())
179 open(os.path.join(dest_dir, added_file), 'a').close()
179 open(os.path.join(dest_dir, added_file), 'a').close()
180 Command(dest_dir).execute(vcs, 'add', added_file)
180 Command(dest_dir).execute(vcs, 'add', added_file)
181
181
182 email = 'me@example.com'
182 email = 'me@example.com'
183 if os.name == 'nt':
183 if os.name == 'nt':
184 author_str = 'User <%s>' % email
184 author_str = 'User <%s>' % email
185 else:
185 else:
186 author_str = 'User ǝɯɐᴎ <%s>' % email
186 author_str = 'User ǝɯɐᴎ <%s>' % email
187 for i in range(files_no):
187 for i in range(files_no):
188 cmd = """echo "added_line%s" >> %s""" % (i, added_file)
188 cmd = """echo "added_line%s" >> %s""" % (i, added_file)
189 Command(dest_dir).execute(cmd)
189 Command(dest_dir).execute(cmd)
190 if vcs == 'hg':
190 if vcs == 'hg':
191 cmd = """hg commit -m "committed new %s" -u "%s" "%s" """ % (
191 cmd = """hg commit -m "committed new %s" -u "%s" "%s" """ % (
192 i, author_str, added_file
192 i, author_str, added_file
193 )
193 )
194 elif vcs == 'git':
194 elif vcs == 'git':
195 cmd = """git commit -m "committed new %s" --author "%s" "%s" """ % (
195 cmd = """git commit -m "committed new %s" --author "%s" "%s" """ % (
196 i, author_str, added_file
196 i, author_str, added_file
197 )
197 )
198 # git commit needs EMAIL on some machines
198 # git commit needs EMAIL on some machines
199 Command(dest_dir).execute(cmd, EMAIL=email)
199 Command(dest_dir).execute(cmd, EMAIL=email)
200
200
201 def _add_files_and_push(webserver, vt, dest_dir, clone_url, ignoreReturnCode=False, files_no=3):
201 def _add_files_and_push(webserver, vt, dest_dir, clone_url, ignoreReturnCode=False, files_no=3):
202 _add_files(vt.repo_type, dest_dir, files_no=files_no)
202 _add_files(vt.repo_type, dest_dir, files_no=files_no)
203 # PUSH it back
203 # PUSH it back
204 stdout = stderr = None
204 stdout = stderr = None
205 if vt.repo_type == 'hg':
205 if vt.repo_type == 'hg':
206 stdout, stderr = Command(dest_dir).execute('hg push -f --verbose', clone_url, ignoreReturnCode=ignoreReturnCode)
206 stdout, stderr = Command(dest_dir).execute('hg push -f --verbose', clone_url, ignoreReturnCode=ignoreReturnCode)
207 elif vt.repo_type == 'git':
207 elif vt.repo_type == 'git':
208 stdout, stderr = Command(dest_dir).execute('git push -f --verbose', clone_url, "master", ignoreReturnCode=ignoreReturnCode)
208 stdout, stderr = Command(dest_dir).execute('git push -f --verbose', clone_url, "master", ignoreReturnCode=ignoreReturnCode)
209
209
210 return stdout, stderr
210 return stdout, stderr
211
211
212
212
213 def _check_outgoing(vcs, cwd, clone_url):
213 def _check_outgoing(vcs, cwd, clone_url):
214 if vcs == 'hg':
214 if vcs == 'hg':
215 # hg removes the password from default URLs, so we have to provide it here via the clone_url
215 # hg removes the password from default URLs, so we have to provide it here via the clone_url
216 return Command(cwd).execute('hg -q outgoing', clone_url, ignoreReturnCode=True)
216 return Command(cwd).execute('hg -q outgoing', clone_url, ignoreReturnCode=True)
217 elif vcs == 'git':
217 elif vcs == 'git':
218 Command(cwd).execute('git remote update')
218 Command(cwd).execute('git remote update')
219 return Command(cwd).execute('git log origin/master..master')
219 return Command(cwd).execute('git log origin/master..master')
220
220
221
221
222 def set_anonymous_access(enable=True):
222 def set_anonymous_access(enable=True):
223 user = db.User.get_default_user()
223 user = db.User.get_default_user()
224 user.active = enable
224 user.active = enable
225 meta.Session().commit()
225 meta.Session().commit()
226 if enable != db.User.get_default_user().active:
226 if enable != db.User.get_default_user().active:
227 raise Exception('Cannot set anonymous access')
227 raise Exception('Cannot set anonymous access')
228
228
229
229
230 #==============================================================================
230 #==============================================================================
231 # TESTS
231 # TESTS
232 #==============================================================================
232 #==============================================================================
233
233
234
234
235 def _check_proper_git_push(stdout, stderr):
235 def _check_proper_git_push(stdout, stderr):
236 assert 'fatal' not in stderr
236 assert 'fatal' not in stderr
237 assert 'rejected' not in stderr
237 assert 'rejected' not in stderr
238 assert 'Pushing to' in stderr
238 assert 'Pushing to' in stderr
239 assert 'master -> master' in stderr
239 assert 'master -> master' in stderr
240
240
241
241
242 @pytest.mark.usefixtures("test_context_fixture")
242 @pytest.mark.usefixtures("test_context_fixture")
243 class TestVCSOperations(base.TestController):
243 class TestVCSOperations(base.TestController):
244
244
245 @classmethod
245 @classmethod
246 def setup_class(cls):
246 def setup_class(cls):
247 # DISABLE ANONYMOUS ACCESS
247 # DISABLE ANONYMOUS ACCESS
248 set_anonymous_access(False)
248 set_anonymous_access(False)
249
249
250 @pytest.fixture()
250 @pytest.fixture()
251 def testhook_cleanup(self):
251 def testhook_cleanup(self):
252 yield
252 yield
253 # remove hook
253 # remove hook
254 for hook in ['prechangegroup', 'pretxnchangegroup', 'preoutgoing', 'changegroup', 'outgoing', 'incoming']:
254 for hook in ['prechangegroup', 'pretxnchangegroup', 'preoutgoing', 'changegroup', 'outgoing', 'incoming']:
255 entry = db.Ui.get_by_key('hooks', '%s.testhook' % hook)
255 entry = db.Ui.get_by_key('hooks', '%s.testhook' % hook)
256 if entry:
256 if entry:
257 meta.Session().delete(entry)
257 meta.Session().delete(entry)
258 meta.Session().commit()
258 meta.Session().commit()
259
259
260 @pytest.fixture(scope="module")
260 @pytest.fixture(scope="module")
261 def testfork(self):
261 def testfork(self):
262 # create fork so the repo stays untouched
262 # create fork so the repo stays untouched
263 git_fork_name = '%s_fork%s' % (base.GIT_REPO, next(_RandomNameSequence()))
263 git_fork_name = '%s_fork%s' % (base.GIT_REPO, next(_RandomNameSequence()))
264 fixture.create_fork(base.GIT_REPO, git_fork_name)
264 fixture.create_fork(base.GIT_REPO, git_fork_name)
265 hg_fork_name = '%s_fork%s' % (base.HG_REPO, next(_RandomNameSequence()))
265 hg_fork_name = '%s_fork%s' % (base.HG_REPO, next(_RandomNameSequence()))
266 fixture.create_fork(base.HG_REPO, hg_fork_name)
266 fixture.create_fork(base.HG_REPO, hg_fork_name)
267 return {'git': git_fork_name, 'hg': hg_fork_name}
267 return {'git': git_fork_name, 'hg': hg_fork_name}
268
268
269 @parametrize_vcs_test
269 @parametrize_vcs_test
270 def test_clone_repo_by_admin(self, webserver, vt):
270 def test_clone_repo_by_admin(self, webserver, vt):
271 clone_url = vt.repo_url_param(webserver, vt.repo_name)
271 clone_url = vt.repo_url_param(webserver, vt.repo_name)
272 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
272 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
273
273
274 if vt.repo_type == 'git':
274 if vt.repo_type == 'git':
275 assert 'Cloning into' in stdout + stderr
275 assert 'Cloning into' in stdout + stderr
276 assert stderr == '' or stdout == ''
276 assert stderr == '' or stdout == ''
277 elif vt.repo_type == 'hg':
277 elif vt.repo_type == 'hg':
278 assert 'requesting all changes' in stdout
278 assert 'requesting all changes' in stdout
279 assert 'adding changesets' in stdout
279 assert 'adding changesets' in stdout
280 assert 'adding manifests' in stdout
280 assert 'adding manifests' in stdout
281 assert 'adding file changes' in stdout
281 assert 'adding file changes' in stdout
282 assert stderr == ''
282 assert stderr == ''
283
283
284 @parametrize_vcs_test_http
284 @parametrize_vcs_test_http
285 def test_clone_wrong_credentials(self, webserver, vt):
285 def test_clone_wrong_credentials(self, webserver, vt):
286 clone_url = vt.repo_url_param(webserver, vt.repo_name, password='bad!')
286 clone_url = vt.repo_url_param(webserver, vt.repo_name, password='bad!')
287 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
287 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
288 if vt.repo_type == 'git':
288 if vt.repo_type == 'git':
289 assert 'fatal: Authentication failed' in stderr
289 assert 'fatal: Authentication failed' in stderr
290 elif vt.repo_type == 'hg':
290 elif vt.repo_type == 'hg':
291 assert 'abort: authorization failed' in stderr
291 assert 'abort: authorization failed' in stderr
292
292
293 def test_clone_git_dir_as_hg(self, webserver):
293 def test_clone_git_dir_as_hg(self, webserver):
294 clone_url = HgHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
294 clone_url = HgHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
295 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('hg clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
295 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('hg clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
296 assert 'HTTP Error 404: Not Found' in stderr or "not a valid repository" in stdout and 'abort:' in stderr
296 assert 'HTTP Error 404: Not Found' in stderr or "not a valid repository" in stdout and 'abort:' in stderr
297
297
298 def test_clone_hg_repo_as_git(self, webserver):
298 def test_clone_hg_repo_as_git(self, webserver):
299 clone_url = GitHttpVcsTest.repo_url_param(webserver, base.HG_REPO)
299 clone_url = GitHttpVcsTest.repo_url_param(webserver, base.HG_REPO)
300 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
300 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
301 assert 'not found' in stderr
301 assert 'not found' in stderr
302
302
303 @parametrize_vcs_test
303 @parametrize_vcs_test
304 def test_clone_non_existing_path(self, webserver, vt):
304 def test_clone_non_existing_path(self, webserver, vt):
305 clone_url = vt.repo_url_param(webserver, 'trololo')
305 clone_url = vt.repo_url_param(webserver, 'trololo')
306 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
306 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
307 if vt.repo_type == 'git':
307 if vt.repo_type == 'git':
308 assert 'not found' in stderr or 'abort: Access to %r denied' % 'trololo' in stderr
308 assert 'not found' in stderr or 'abort: Access to %r denied' % 'trololo' in stderr
309 elif vt.repo_type == 'hg':
309 elif vt.repo_type == 'hg':
310 assert 'HTTP Error 404: Not Found' in stderr or 'abort: no suitable response from remote hg' in stderr and 'remote: abort: Access to %r denied' % 'trololo' in stdout + stderr
310 assert 'HTTP Error 404: Not Found' in stderr or 'abort: no suitable response from remote hg' in stderr and 'remote: abort: Access to %r denied' % 'trololo' in stdout + stderr
311
311
312 @parametrize_vcs_test
312 @parametrize_vcs_test
313 def test_push_new_repo(self, webserver, vt):
313 def test_push_new_repo(self, webserver, vt):
314 # Clear the log so we know what is added
314 # Clear the log so we know what is added
315 db.UserLog.query().delete()
315 db.UserLog.query().delete()
316 meta.Session().commit()
316 meta.Session().commit()
317
317
318 # Create an empty server repo using the API
318 # Create an empty server repo using the API
319 repo_name = 'new_%s_%s' % (vt.repo_type, next(_RandomNameSequence()))
319 repo_name = 'new_%s_%s' % (vt.repo_type, next(_RandomNameSequence()))
320 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
320 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
321 params = {
321 params = {
322 "id": 7,
322 "id": 7,
323 "api_key": usr.api_key,
323 "api_key": usr.api_key,
324 "method": 'create_repo',
324 "method": 'create_repo',
325 "args": dict(repo_name=repo_name,
325 "args": dict(repo_name=repo_name,
326 owner=base.TEST_USER_ADMIN_LOGIN,
326 owner=base.TEST_USER_ADMIN_LOGIN,
327 repo_type=vt.repo_type),
327 repo_type=vt.repo_type),
328 }
328 }
329 req = urllib.request.Request(
329 req = urllib.request.Request(
330 'http://%s:%s/_admin/api' % webserver.server_address,
330 'http://%s:%s/_admin/api' % webserver.server_address,
331 data=ascii_bytes(json.dumps(params)),
331 data=ascii_bytes(json.dumps(params)),
332 headers={'content-type': 'application/json'})
332 headers={'content-type': 'application/json'})
333 response = urllib.request.urlopen(req)
333 response = urllib.request.urlopen(req)
334 result = json.loads(response.read())
334 result = json.loads(response.read())
335 # Expect something like:
335 # Expect something like:
336 # {u'result': {u'msg': u'Created new repository `new_XXX`', u'task': None, u'success': True}, u'id': 7, u'error': None}
336 # {u'result': {u'msg': u'Created new repository `new_XXX`', u'task': None, u'success': True}, u'id': 7, u'error': None}
337 assert result['result']['success']
337 assert result['result']['success']
338
338
339 # Create local clone of the empty server repo
339 # Create local clone of the empty server repo
340 local_clone_dir = _get_tmp_dir()
340 local_clone_dir = _get_tmp_dir()
341 clone_url = vt.repo_url_param(webserver, repo_name)
341 clone_url = vt.repo_url_param(webserver, repo_name)
342 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, local_clone_dir)
342 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, local_clone_dir)
343
343
344 # Make 3 commits and push to the empty server repo.
344 # Make 3 commits and push to the empty server repo.
345 # The server repo doesn't have any other heads than the
345 # The server repo doesn't have any other heads than the
346 # refs/heads/master we are pushing, but the `git log` in the push hook
346 # refs/heads/master we are pushing, but the `git log` in the push hook
347 # should still list the 3 commits.
347 # should still list the 3 commits.
348 stdout, stderr = _add_files_and_push(webserver, vt, local_clone_dir, clone_url=clone_url)
348 stdout, stderr = _add_files_and_push(webserver, vt, local_clone_dir, clone_url=clone_url)
349 if vt.repo_type == 'git':
349 if vt.repo_type == 'git':
350 _check_proper_git_push(stdout, stderr)
350 _check_proper_git_push(stdout, stderr)
351 elif vt.repo_type == 'hg':
351 elif vt.repo_type == 'hg':
352 assert 'pushing to ' in stdout
352 assert 'pushing to ' in stdout
353 assert 'remote: added ' in stdout
353 assert 'remote: added ' in stdout
354
354
355 # Verify that we got the right events in UserLog. Expect something like:
355 # Verify that we got the right events in UserLog. Expect something like:
356 # <UserLog('id:new_git_XXX:started_following_repo')>
356 # <UserLog('id:new_git_XXX:started_following_repo')>
357 # <UserLog('id:new_git_XXX:user_created_repo')>
357 # <UserLog('id:new_git_XXX:user_created_repo')>
358 # <UserLog('id:new_git_XXX:pull')>
358 # <UserLog('id:new_git_XXX:pull')>
359 # <UserLog('id:new_git_XXX:push:aed9d4c1732a1927da3be42c47eb9afdc200d427,d38b083a07af10a9f44193486959a96a23db78da,4841ff9a2b385bec995f4679ef649adb3f437622')>
359 # <UserLog('id:new_git_XXX:push:aed9d4c1732a1927da3be42c47eb9afdc200d427,d38b083a07af10a9f44193486959a96a23db78da,4841ff9a2b385bec995f4679ef649adb3f437622')>
360 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
360 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
361 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
361 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
362 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == ([
362 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == ([
363 ('started_following_repo', 0),
363 ('started_following_repo', 0),
364 ('user_created_repo', 0),
364 ('user_created_repo', 0),
365 ('pull', 0),
365 ('pull', 0),
366 ('push', 3)]
366 ('push', 3)]
367 if vt.repo_type == 'git' else [
367 if vt.repo_type == 'git' else [
368 ('started_following_repo', 0),
368 ('started_following_repo', 0),
369 ('user_created_repo', 0),
369 ('user_created_repo', 0),
370 # (u'pull', 0), # Mercurial outgoing hook is not called for empty clones
370 # (u'pull', 0), # Mercurial outgoing hook is not called for empty clones
371 ('push', 3)])
371 ('push', 3)])
372
372
373 @parametrize_vcs_test
373 @parametrize_vcs_test
374 def test_push_new_file(self, webserver, testfork, vt):
374 def test_push_new_file(self, webserver, testfork, vt):
375 db.UserLog.query().delete()
375 db.UserLog.query().delete()
376 meta.Session().commit()
376 meta.Session().commit()
377
377
378 dest_dir = _get_tmp_dir()
378 dest_dir = _get_tmp_dir()
379 clone_url = vt.repo_url_param(webserver, vt.repo_name)
379 clone_url = vt.repo_url_param(webserver, vt.repo_name)
380 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
380 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
381
381
382 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
382 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
383 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url=clone_url)
383 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url=clone_url)
384
384
385 if vt.repo_type == 'git':
385 if vt.repo_type == 'git':
386 _check_proper_git_push(stdout, stderr)
386 _check_proper_git_push(stdout, stderr)
387 elif vt.repo_type == 'hg':
387 elif vt.repo_type == 'hg':
388 assert 'pushing to' in stdout
388 assert 'pushing to' in stdout
389 assert 'Repository size' in stdout
389 assert 'Repository size' in stdout
390 assert 'Last revision is now' in stdout
390 assert 'Last revision is now' in stdout
391
391
392 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
392 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
393 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
393 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
394 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
394 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
395 [('pull', 0), ('push', 3)]
395 [('pull', 0), ('push', 3)]
396
396
397 @parametrize_vcs_test
397 @parametrize_vcs_test
398 def test_pull(self, webserver, testfork, vt):
398 def test_pull(self, webserver, testfork, vt):
399 db.UserLog.query().delete()
399 db.UserLog.query().delete()
400 meta.Session().commit()
400 meta.Session().commit()
401
401
402 dest_dir = _get_tmp_dir()
402 dest_dir = _get_tmp_dir()
403 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'init', dest_dir)
403 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'init', dest_dir)
404
404
405 clone_url = vt.repo_url_param(webserver, vt.repo_name)
405 clone_url = vt.repo_url_param(webserver, vt.repo_name)
406 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url)
406 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url)
407 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
407 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
408
408
409 if vt.repo_type == 'git':
409 if vt.repo_type == 'git':
410 assert 'FETCH_HEAD' in stderr
410 assert 'FETCH_HEAD' in stderr
411 elif vt.repo_type == 'hg':
411 elif vt.repo_type == 'hg':
412 assert 'new changesets' in stdout
412 assert 'new changesets' in stdout
413
413
414 action_parts = [ul.action for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
414 action_parts = [ul.action for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
415 assert action_parts == ['pull']
415 assert action_parts == ['pull']
416
416
417 # Test handling of URLs with extra '/' around repo_name
417 # Test handling of URLs with extra '/' around repo_name
418 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/./%s/' % vt.repo_name), ignoreReturnCode=True)
418 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/./%s/' % vt.repo_name), ignoreReturnCode=True)
419 if issubclass(vt, HttpVcsTest):
419 if issubclass(vt, HttpVcsTest):
420 if vt.repo_type == 'git':
420 if vt.repo_type == 'git':
421 # NOTE: when pulling from http://hostname/./vcs_test_git/ , the git client will normalize that and issue an HTTP request to /vcs_test_git/info/refs
421 # NOTE: when pulling from http://hostname/./vcs_test_git/ , the git client will normalize that and issue an HTTP request to /vcs_test_git/info/refs
422 assert 'Already up to date.' in stdout
422 assert 'Already up to date.' in stdout
423 else:
423 else:
424 assert vt.repo_type == 'hg'
424 assert vt.repo_type == 'hg'
425 assert "abort: HTTP Error 404: Not Found" in stderr
425 assert "abort: HTTP Error 404: Not Found" in stderr
426 else:
426 else:
427 assert issubclass(vt, SshVcsTest)
427 assert issubclass(vt, SshVcsTest)
428 if vt.repo_type == 'git':
428 if vt.repo_type == 'git':
429 assert "abort: Access to './%s' denied" % vt.repo_name in stderr
429 assert "abort: Access to './%s' denied" % vt.repo_name in stderr
430 else:
430 else:
431 assert "abort: Access to './%s' denied" % vt.repo_name in stdout + stderr
431 assert "abort: Access to './%s' denied" % vt.repo_name in stdout + stderr
432
432
433 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/%s/' % vt.repo_name), ignoreReturnCode=True)
433 stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/%s/' % vt.repo_name), ignoreReturnCode=True)
434 if vt.repo_type == 'git':
434 if vt.repo_type == 'git':
435 assert 'Already up to date.' in stdout
435 assert 'Already up to date.' in stdout
436 else:
436 else:
437 assert vt.repo_type == 'hg'
437 assert vt.repo_type == 'hg'
438 assert "no changes found" in stdout
438 assert "no changes found" in stdout
439 assert "denied" not in stderr
439 assert "denied" not in stderr
440 assert "denied" not in stdout
440 assert "denied" not in stdout
441 assert "404" not in stdout
441 assert "404" not in stdout
442
442
443 @parametrize_vcs_test
443 @parametrize_vcs_test
444 def test_push_invalidates_cache(self, webserver, testfork, vt):
444 def test_push_invalidates_cache(self, webserver, testfork, vt):
445 pre_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == testfork[vt.repo_type])]
445 pre_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == testfork[vt.repo_type])]
446
446
447 dest_dir = _get_tmp_dir()
447 dest_dir = _get_tmp_dir()
448 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
448 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
449 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
449 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
450
450
451 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, files_no=1, clone_url=clone_url)
451 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, files_no=1, clone_url=clone_url)
452
452
453 if vt.repo_type == 'git':
453 if vt.repo_type == 'git':
454 _check_proper_git_push(stdout, stderr)
454 _check_proper_git_push(stdout, stderr)
455
455
456 meta.Session.close() # expire session to make sure SA fetches new Repository instances after last_changeset has been updated by server side hook in another process
456 meta.Session.close() # expire session to make sure SA fetches new Repository instances after last_changeset has been updated by server side hook in another process
457 post_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == testfork[vt.repo_type])]
457 post_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == testfork[vt.repo_type])]
458 assert pre_cached_tip != post_cached_tip
458 assert pre_cached_tip != post_cached_tip
459
459
460 @parametrize_vcs_test_http
460 @parametrize_vcs_test_http
461 def test_push_wrong_credentials(self, webserver, vt):
461 def test_push_wrong_credentials(self, webserver, vt):
462 dest_dir = _get_tmp_dir()
462 dest_dir = _get_tmp_dir()
463 clone_url = vt.repo_url_param(webserver, vt.repo_name)
463 clone_url = vt.repo_url_param(webserver, vt.repo_name)
464 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
464 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
465
465
466 clone_url = webserver.repo_url(vt.repo_name, username='bad', password='name')
466 clone_url = webserver.repo_url(vt.repo_name, username='bad', password='name')
467 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir,
467 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir,
468 clone_url=clone_url, ignoreReturnCode=True)
468 clone_url=clone_url, ignoreReturnCode=True)
469
469
470 if vt.repo_type == 'git':
470 if vt.repo_type == 'git':
471 assert 'fatal: Authentication failed' in stderr
471 assert 'fatal: Authentication failed' in stderr
472 elif vt.repo_type == 'hg':
472 elif vt.repo_type == 'hg':
473 assert 'abort: authorization failed' in stderr
473 assert 'abort: authorization failed' in stderr
474
474
475 @parametrize_vcs_test
475 @parametrize_vcs_test
476 def test_push_with_readonly_credentials(self, webserver, vt):
476 def test_push_with_readonly_credentials(self, webserver, vt):
477 db.UserLog.query().delete()
477 db.UserLog.query().delete()
478 meta.Session().commit()
478 meta.Session().commit()
479
479
480 dest_dir = _get_tmp_dir()
480 dest_dir = _get_tmp_dir()
481 clone_url = vt.repo_url_param(webserver, vt.repo_name, username=base.TEST_USER_REGULAR_LOGIN, password=base.TEST_USER_REGULAR_PASS)
481 clone_url = vt.repo_url_param(webserver, vt.repo_name, username=base.TEST_USER_REGULAR_LOGIN, password=base.TEST_USER_REGULAR_PASS)
482 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
482 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
483
483
484 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, ignoreReturnCode=True, clone_url=clone_url)
484 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, ignoreReturnCode=True, clone_url=clone_url)
485
485
486 if vt.repo_type == 'git':
486 if vt.repo_type == 'git':
487 assert 'The requested URL returned error: 403' in stderr or 'abort: Push access to %r denied' % str(vt.repo_name) in stderr
487 assert 'The requested URL returned error: 403' in stderr or 'abort: Push access to %r denied' % str(vt.repo_name) in stderr
488 elif vt.repo_type == 'hg':
488 elif vt.repo_type == 'hg':
489 assert 'abort: HTTP Error 403: Forbidden' in stderr or 'abort: push failed on remote' in stderr and 'remote: Push access to %r denied' % str(vt.repo_name) in stdout
489 assert 'abort: HTTP Error 403: Forbidden' in stderr or 'abort: push failed on remote' in stderr and 'remote: Push access to %r denied' % str(vt.repo_name) in stdout
490
490
491 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
491 meta.Session.close() # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
492 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
492 action_parts = [ul.action.split(':', 1) for ul in db.UserLog.query().order_by(db.UserLog.user_log_id)]
493 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
493 assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
494 [('pull', 0)]
494 [('pull', 0)]
495
495
496 @parametrize_vcs_test
496 @parametrize_vcs_test
497 def test_push_back_to_wrong_url(self, webserver, vt):
497 def test_push_back_to_wrong_url(self, webserver, vt):
498 dest_dir = _get_tmp_dir()
498 dest_dir = _get_tmp_dir()
499 clone_url = vt.repo_url_param(webserver, vt.repo_name)
499 clone_url = vt.repo_url_param(webserver, vt.repo_name)
500 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
500 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
501
501
502 stdout, stderr = _add_files_and_push(
502 stdout, stderr = _add_files_and_push(
503 webserver, vt, dest_dir, clone_url='http://%s:%s/tmp' % (
503 webserver, vt, dest_dir, clone_url='http://%s:%s/tmp' % (
504 webserver.server_address[0], webserver.server_address[1]),
504 webserver.server_address[0], webserver.server_address[1]),
505 ignoreReturnCode=True)
505 ignoreReturnCode=True)
506
506
507 if vt.repo_type == 'git':
507 if vt.repo_type == 'git':
508 assert 'not found' in stderr
508 assert 'not found' in stderr
509 elif vt.repo_type == 'hg':
509 elif vt.repo_type == 'hg':
510 assert 'HTTP Error 404: Not Found' in stderr
510 assert 'HTTP Error 404: Not Found' in stderr
511
511
512 @parametrize_vcs_test
512 @parametrize_vcs_test
513 def test_ip_restriction(self, webserver, vt):
513 def test_ip_restriction(self, webserver, vt):
514 user_model = UserModel()
514 user_model = UserModel()
515 try:
515 try:
516 # Add IP constraint that excludes the test context:
516 # Add IP constraint that excludes the test context:
517 user_model.add_extra_ip(base.TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
517 user_model.add_extra_ip(base.TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
518 meta.Session().commit()
518 meta.Session().commit()
519 # IP permissions are cached, need to wait for the cache in the server process to expire
519 # IP permissions are cached, need to wait for the cache in the server process to expire
520 time.sleep(1.5)
520 time.sleep(1.5)
521 clone_url = vt.repo_url_param(webserver, vt.repo_name)
521 clone_url = vt.repo_url_param(webserver, vt.repo_name)
522 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
522 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
523 if vt.repo_type == 'git':
523 if vt.repo_type == 'git':
524 # The message apparently changed in Git 1.8.3, so match it loosely.
524 # The message apparently changed in Git 1.8.3, so match it loosely.
525 assert re.search(r'\b403\b', stderr) or 'abort: User test_admin from 127.0.0.127 cannot be authorized' in stderr
525 assert re.search(r'\b403\b', stderr) or 'abort: User test_admin from 127.0.0.127 cannot be authorized' in stderr
526 elif vt.repo_type == 'hg':
526 elif vt.repo_type == 'hg':
527 assert 'abort: HTTP Error 403: Forbidden' in stderr or 'remote: abort: User test_admin from 127.0.0.127 cannot be authorized' in stdout + stderr
527 assert 'abort: HTTP Error 403: Forbidden' in stderr or 'remote: abort: User test_admin from 127.0.0.127 cannot be authorized' in stdout + stderr
528 finally:
528 finally:
529 # release IP restrictions
529 # release IP restrictions
530 for ip in db.UserIpMap.query():
530 for ip in db.UserIpMap.query():
531 db.UserIpMap.delete(ip.ip_id)
531 db.UserIpMap.delete(ip.ip_id)
532 meta.Session().commit()
532 meta.Session().commit()
533 # IP permissions are cached, need to wait for the cache in the server process to expire
533 # IP permissions are cached, need to wait for the cache in the server process to expire
534 time.sleep(1.5)
534 time.sleep(1.5)
535
535
536 clone_url = vt.repo_url_param(webserver, vt.repo_name)
536 clone_url = vt.repo_url_param(webserver, vt.repo_name)
537 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
537 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
538
538
539 if vt.repo_type == 'git':
539 if vt.repo_type == 'git':
540 assert 'Cloning into' in stdout + stderr
540 assert 'Cloning into' in stdout + stderr
541 assert stderr == '' or stdout == ''
541 assert stderr == '' or stdout == ''
542 elif vt.repo_type == 'hg':
542 elif vt.repo_type == 'hg':
543 assert 'requesting all changes' in stdout
543 assert 'requesting all changes' in stdout
544 assert 'adding changesets' in stdout
544 assert 'adding changesets' in stdout
545 assert 'adding manifests' in stdout
545 assert 'adding manifests' in stdout
546 assert 'adding file changes' in stdout
546 assert 'adding file changes' in stdout
547
547
548 assert stderr == ''
548 assert stderr == ''
549
549
550 @parametrize_vcs_test_hg # git hooks doesn't work like hg hooks
550 @parametrize_vcs_test_hg # git hooks doesn't work like hg hooks
551 def test_custom_hooks_preoutgoing(self, testhook_cleanup, webserver, testfork, vt):
551 def test_custom_hooks_preoutgoing(self, testhook_cleanup, webserver, testfork, vt):
552 # set prechangegroup to failing hook (returns True)
552 # set prechangegroup to failing hook (returns True)
553 db.Ui.create_or_update_hook('preoutgoing.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
553 db.Ui.create_or_update_hook('preoutgoing.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
554 meta.Session().commit()
554 meta.Session().commit()
555 # clone repo
555 # clone repo
556 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
556 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
557 dest_dir = _get_tmp_dir()
557 dest_dir = _get_tmp_dir()
558 stdout, stderr = Command(base.TESTS_TMP_PATH) \
558 stdout, stderr = Command(base.TESTS_TMP_PATH) \
559 .execute(vt.repo_type, 'clone', clone_url, dest_dir, ignoreReturnCode=True)
559 .execute(vt.repo_type, 'clone', clone_url, dest_dir, ignoreReturnCode=True)
560 if vt.repo_type == 'hg':
560 if vt.repo_type == 'hg':
561 assert 'preoutgoing.testhook hook failed' in stdout + stderr
561 assert 'preoutgoing.testhook hook failed' in stdout + stderr
562 elif vt.repo_type == 'git':
562 elif vt.repo_type == 'git':
563 assert 'error: 406' in stderr
563 assert 'error: 406' in stderr
564
564
565 @parametrize_vcs_test_hg # git hooks doesn't work like hg hooks
565 @parametrize_vcs_test_hg # git hooks doesn't work like hg hooks
566 def test_custom_hooks_prechangegroup(self, testhook_cleanup, webserver, testfork, vt):
566 def test_custom_hooks_prechangegroup(self, testhook_cleanup, webserver, testfork, vt):
567 # set prechangegroup to failing hook (returns exit code 1)
567 # set prechangegroup to failing hook (returns exit code 1)
568 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
568 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
569 meta.Session().commit()
569 meta.Session().commit()
570 # clone repo
570 # clone repo
571 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
571 clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
572 dest_dir = _get_tmp_dir()
572 dest_dir = _get_tmp_dir()
573 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
573 stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
574
574
575 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url,
575 stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url,
576 ignoreReturnCode=True)
576 ignoreReturnCode=True)
577 assert 'failing_test_hook failed' in stdout + stderr
577 assert 'failing_test_hook failed' in stdout + stderr
578 assert 'Traceback' not in stdout + stderr
578 assert 'Traceback' not in stdout + stderr
579 assert 'prechangegroup.testhook hook failed' in stdout + stderr
579 assert 'prechangegroup.testhook hook failed' in stdout + stderr
580 # there are still outgoing changesets
580 # there are still outgoing changesets
581 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
581 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
582 assert stdout != ''
582 assert stdout != ''
583
583
584 # set prechangegroup hook to exception throwing method
584 # set prechangegroup hook to exception throwing method
585 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.exception_test_hook')
585 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.exception_test_hook')
586 meta.Session().commit()
586 meta.Session().commit()
587 # re-try to push
587 # re-try to push
588 stdout, stderr = Command(dest_dir).execute('%s push' % vt.repo_type, clone_url, ignoreReturnCode=True)
588 stdout, stderr = Command(dest_dir).execute('%s push' % vt.repo_type, clone_url, ignoreReturnCode=True)
589 if vt is HgHttpVcsTest:
589 if vt is HgHttpVcsTest:
590 # like with 'hg serve...' 'HTTP Error 500: INTERNAL SERVER ERROR' should be returned
590 # like with 'hg serve...' 'HTTP Error 500: INTERNAL SERVER ERROR' should be returned
591 assert 'HTTP Error 500: INTERNAL SERVER ERROR' in stderr
591 assert 'HTTP Error 500: INTERNAL SERVER ERROR' in stderr
592 elif vt is HgSshVcsTest:
592 elif vt is HgSshVcsTest:
593 assert 'remote: Exception: exception_test_hook threw an exception' in stdout
593 assert 'remote: Exception: exception_test_hook threw an exception' in stdout
594 else: assert False
594 else:
595 assert False
595 # there are still outgoing changesets
596 # there are still outgoing changesets
596 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
597 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
597 assert stdout != ''
598 assert stdout != ''
598
599
599 # set prechangegroup hook to method that returns False
600 # set prechangegroup hook to method that returns False
600 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.passing_test_hook')
601 db.Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.passing_test_hook')
601 meta.Session().commit()
602 meta.Session().commit()
602 # re-try to push
603 # re-try to push
603 stdout, stderr = Command(dest_dir).execute('%s push' % vt.repo_type, clone_url, ignoreReturnCode=True)
604 stdout, stderr = Command(dest_dir).execute('%s push' % vt.repo_type, clone_url, ignoreReturnCode=True)
604 assert 'passing_test_hook succeeded' in stdout + stderr
605 assert 'passing_test_hook succeeded' in stdout + stderr
605 assert 'Traceback' not in stdout + stderr
606 assert 'Traceback' not in stdout + stderr
606 assert 'prechangegroup.testhook hook failed' not in stdout + stderr
607 assert 'prechangegroup.testhook hook failed' not in stdout + stderr
607 # no more outgoing changesets
608 # no more outgoing changesets
608 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
609 stdout, stderr = _check_outgoing(vt.repo_type, dest_dir, clone_url)
609 assert stdout == ''
610 assert stdout == ''
610 assert stderr == ''
611 assert stderr == ''
611
612
612 def test_add_submodule_git(self, webserver, testfork):
613 def test_add_submodule_git(self, webserver, testfork):
613 dest_dir = _get_tmp_dir()
614 dest_dir = _get_tmp_dir()
614 clone_url = GitHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
615 clone_url = GitHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
615
616
616 fork_url = GitHttpVcsTest.repo_url_param(webserver, testfork['git'])
617 fork_url = GitHttpVcsTest.repo_url_param(webserver, testfork['git'])
617
618
618 # add submodule
619 # add submodule
619 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', fork_url, dest_dir)
620 stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', fork_url, dest_dir)
620 stdout, stderr = Command(dest_dir).execute('git submodule add', clone_url, 'testsubmodule')
621 stdout, stderr = Command(dest_dir).execute('git submodule add', clone_url, 'testsubmodule')
621 stdout, stderr = Command(dest_dir).execute('git commit -am "added testsubmodule pointing to', clone_url, '"', EMAIL=base.TEST_USER_ADMIN_EMAIL)
622 stdout, stderr = Command(dest_dir).execute('git commit -am "added testsubmodule pointing to', clone_url, '"', EMAIL=base.TEST_USER_ADMIN_EMAIL)
622 stdout, stderr = Command(dest_dir).execute('git push', fork_url, 'master')
623 stdout, stderr = Command(dest_dir).execute('git push', fork_url, 'master')
623
624
624 # check for testsubmodule link in files page
625 # check for testsubmodule link in files page
625 self.log_user()
626 self.log_user()
626 response = self.app.get(base.url(controller='files', action='index',
627 response = self.app.get(base.url(controller='files', action='index',
627 repo_name=testfork['git'],
628 repo_name=testfork['git'],
628 revision='tip',
629 revision='tip',
629 f_path='/'))
630 f_path='/'))
630 # check _repo_files_url that will be used to reload as AJAX
631 # check _repo_files_url that will be used to reload as AJAX
631 response.mustcontain('var _repo_files_url = ("/%s/files/");' % testfork['git'])
632 response.mustcontain('var _repo_files_url = ("/%s/files/");' % testfork['git'])
632
633
633 response.mustcontain('<a class="submodule-dir" href="%s" target="_blank"><i class="icon-file-submodule"></i><span>testsubmodule @ ' % clone_url)
634 response.mustcontain('<a class="submodule-dir" href="%s" target="_blank"><i class="icon-file-submodule"></i><span>testsubmodule @ ' % clone_url)
634
635
635 # check that following a submodule link actually works - and redirects
636 # check that following a submodule link actually works - and redirects
636 response = self.app.get(base.url(controller='files', action='index',
637 response = self.app.get(base.url(controller='files', action='index',
637 repo_name=testfork['git'],
638 repo_name=testfork['git'],
638 revision='tip',
639 revision='tip',
639 f_path='/testsubmodule'),
640 f_path='/testsubmodule'),
640 status=302)
641 status=302)
641 assert response.location == clone_url
642 assert response.location == clone_url
General Comments 0
You need to be logged in to leave comments. Login now