##// END OF EJS Templates
Implemented simple gist functionality ref #530....
marcink -
r3840:dc464486 beta
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,159 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.bin.gist
4 ~~~~~~~~~~~~~~~~~~
5
6 Gist CLI client for RhodeCode
7
8 :created_on: May 9, 2013
9 :author: marcink
10 :copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 from __future__ import with_statement
27 import os
28 import sys
29 import stat
30 import argparse
31 import fileinput
32
33 from rhodecode.bin.base import api_call, RcConf
34
35
36 def argparser(argv):
37 usage = (
38 "rhodecode-gist [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
39 "[--config=CONFIG] [--save-config] "
40 "[filename or stdin use - for terminal stdin ]\n"
41 "Create config file: rhodecode-gist --apikey=<key> --apihost=http://rhodecode.server --save-config"
42 )
43
44 parser = argparse.ArgumentParser(description='RhodeCode Gist cli',
45 usage=usage)
46
47 ## config
48 group = parser.add_argument_group('config')
49 group.add_argument('--apikey', help='api access key')
50 group.add_argument('--apihost', help='api host')
51 group.add_argument('--config', help='config file')
52 group.add_argument('--save-config', action='store_true',
53 help='save the given config into a file')
54
55 group = parser.add_argument_group('GIST')
56 group.add_argument('-f', '--filename', help='set uploaded gist filename')
57 group.add_argument('-p', '--private', action='store_true',
58 help='Create private Gist')
59 group.add_argument('-d', '--description', help='Gist description')
60 group.add_argument('-l', '--lifetime', metavar='MINUTES',
61 help='Gist lifetime in minutes, -1 (Default) is forever')
62
63 args, other = parser.parse_known_args()
64 return parser, args, other
65
66
67 def _run(argv):
68 conf = None
69 parser, args, other = argparser(argv)
70
71 api_credentials_given = (args.apikey and args.apihost)
72 if args.save_config:
73 if not api_credentials_given:
74 raise parser.error('--save-config requires --apikey and --apihost')
75 conf = RcConf(config_location=args.config,
76 autocreate=True, config={'apikey': args.apikey,
77 'apihost': args.apihost})
78 sys.exit()
79
80 if not conf:
81 conf = RcConf(config_location=args.config, autoload=True)
82 if not conf:
83 if not api_credentials_given:
84 parser.error('Could not find config file and missing '
85 '--apikey or --apihost in params')
86
87 apikey = args.apikey or conf['apikey']
88 host = args.apihost or conf['apihost']
89 DEFAULT_FILENAME = 'gistfile1.txt'
90 if other:
91 # skip multifiles for now
92 filename = other[0]
93 if filename == '-':
94 filename = DEFAULT_FILENAME
95 gist_content = ''
96 for line in fileinput.input():
97 gist_content += line
98 else:
99 with open(filename, 'rb') as f:
100 gist_content = f.read()
101
102 else:
103 filename = DEFAULT_FILENAME
104 gist_content = None
105 # little bit hacky but cross platform check where the
106 # stdin comes from we skip the terminal case it can be handled by '-'
107 mode = os.fstat(0).st_mode
108 if stat.S_ISFIFO(mode):
109 # "stdin is piped"
110 gist_content = sys.stdin.read()
111 elif stat.S_ISREG(mode):
112 # "stdin is redirected"
113 gist_content = sys.stdin.read()
114 else:
115 # "stdin is terminal"
116 pass
117
118 # make sure we don't upload binary stuff
119 if gist_content and '\0' in gist_content:
120 raise Exception('Error: binary files upload is not possible')
121
122 filename = args.filename or filename
123 if gist_content:
124 files = {
125 filename: {
126 'content': gist_content,
127 'lexer': None
128 }
129 }
130
131 margs = dict(
132 gist_lifetime=args.lifetime,
133 gist_description=args.description,
134 gist_type='private' if args.private else 'public',
135 files=files
136 )
137
138 api_call(apikey, host, 'json', 'create_gist', **margs)
139 return 0
140
141
142 def main(argv=None):
143 """
144 Main execution function for cli
145
146 :param argv:
147 """
148 if argv is None:
149 argv = sys.argv
150
151 try:
152 return _run(argv)
153 except Exception, e:
154 print e
155 return 1
156
157
158 if __name__ == '__main__':
159 sys.exit(main(sys.argv))
@@ -0,0 +1,180 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.admin.gist
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 gist controller for RhodeCode
7
8 :created_on: May 9, 2013
9 :author: marcink
10 :copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import time
26 import logging
27 import traceback
28 import formencode
29 from formencode import htmlfill
30
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import abort, redirect
33 from pylons.i18n.translation import _
34
35 from rhodecode.model.forms import GistForm
36 from rhodecode.model.gist import GistModel
37 from rhodecode.model.meta import Session
38 from rhodecode.model.db import Gist
39 from rhodecode.lib import helpers as h
40 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.auth import LoginRequired, NotAnonymous
42 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
43 from rhodecode.lib.helpers import Page
44 from webob.exc import HTTPNotFound
45 from sqlalchemy.sql.expression import or_
46 from rhodecode.lib.vcs.exceptions import VCSError
47
48 log = logging.getLogger(__name__)
49
50
51 class GistsController(BaseController):
52 """REST Controller styled on the Atom Publishing Protocol"""
53
54 def __load_defaults(self):
55 c.lifetime_values = [
56 (str(-1), _('forever')),
57 (str(5), _('5 minutes')),
58 (str(60), _('1 hour')),
59 (str(60 * 24), _('1 day')),
60 (str(60 * 24 * 30), _('1 month')),
61 ]
62 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
63
64 @LoginRequired()
65 def index(self, format='html'):
66 """GET /admin/gists: All items in the collection"""
67 # url('gists')
68 c.show_private = request.GET.get('private') and c.rhodecode_user.username != 'default'
69 gists = Gist().query()\
70 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
71 .order_by(Gist.created_on.desc())
72 if c.show_private:
73 c.gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
74 .filter(Gist.gist_owner == c.rhodecode_user.user_id)
75 else:
76 c.gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
77 p = safe_int(request.GET.get('page', 1), 1)
78 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
79 return render('admin/gists/index.html')
80
81 @LoginRequired()
82 @NotAnonymous()
83 def create(self):
84 """POST /admin/gists: Create a new item"""
85 # url('gists')
86 self.__load_defaults()
87 gist_form = GistForm([x[0] for x in c.lifetime_values])()
88 try:
89 form_result = gist_form.to_python(dict(request.POST))
90 #TODO: multiple files support, from the form
91 nodes = {
92 form_result['filename'] or 'gistfile1.txt': {
93 'content': form_result['content'],
94 'lexer': None # autodetect
95 }
96 }
97 _public = form_result['public']
98 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
99 gist = GistModel().create(
100 description=form_result['description'],
101 owner=c.rhodecode_user,
102 gist_mapping=nodes,
103 gist_type=gist_type,
104 lifetime=form_result['lifetime']
105 )
106 Session().commit()
107 new_gist_id = gist.gist_access_id
108 except formencode.Invalid, errors:
109 defaults = errors.value
110
111 return formencode.htmlfill.render(
112 render('admin/gists/new.html'),
113 defaults=defaults,
114 errors=errors.error_dict or {},
115 prefix_error=False,
116 encoding="UTF-8"
117 )
118
119 except Exception, e:
120 log.error(traceback.format_exc())
121 h.flash(_('Error occurred during gist creation'), category='error')
122 return redirect(url('new_gist'))
123 return redirect(url('gist', id=new_gist_id))
124
125 @LoginRequired()
126 @NotAnonymous()
127 def new(self, format='html'):
128 """GET /admin/gists/new: Form to create a new item"""
129 # url('new_gist')
130 self.__load_defaults()
131 return render('admin/gists/new.html')
132
133 @LoginRequired()
134 @NotAnonymous()
135 def update(self, id):
136 """PUT /admin/gists/id: Update an existing item"""
137 # Forms posted to this method should contain a hidden field:
138 # <input type="hidden" name="_method" value="PUT" />
139 # Or using helpers:
140 # h.form(url('gist', id=ID),
141 # method='put')
142 # url('gist', id=ID)
143
144 @LoginRequired()
145 @NotAnonymous()
146 def delete(self, id):
147 """DELETE /admin/gists/id: Delete an existing item"""
148 # Forms posted to this method should contain a hidden field:
149 # <input type="hidden" name="_method" value="DELETE" />
150 # Or using helpers:
151 # h.form(url('gist', id=ID),
152 # method='delete')
153 # url('gist', id=ID)
154
155 @LoginRequired()
156 def show(self, id, format='html'):
157 """GET /admin/gists/id: Show a specific item"""
158 # url('gist', id=ID)
159 gist_id = id
160 c.gist = Gist.get_or_404(gist_id)
161
162 #check if this gist is not expired
163 if c.gist.gist_expires != -1:
164 if time.time() > c.gist.gist_expires:
165 log.error('Gist expired at %s' %
166 (time_to_datetime(c.gist.gist_expires)))
167 raise HTTPNotFound()
168 try:
169 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
170 except VCSError:
171 log.error(traceback.format_exc())
172 raise HTTPNotFound()
173
174 return render('admin/gists/show.html')
175
176 @LoginRequired()
177 @NotAnonymous()
178 def edit(self, id, format='html'):
179 """GET /admin/gists/id/edit: Form to edit an existing item"""
180 # url('edit_gist', id=ID)
@@ -0,0 +1,161 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.gist
4 ~~~~~~~~~~~~~~~~~~~~
5
6 gist model for RhodeCode
7
8 :created_on: May 9, 2013
9 :author: marcink
10 :copyright: (C) 2011-2013 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import os
27 import time
28 import logging
29 import traceback
30 import shutil
31
32 from pylons.i18n.translation import _
33 from rhodecode.lib.utils2 import safe_unicode, unique_id, safe_int, \
34 time_to_datetime, safe_str, AttributeDict
35 from rhodecode.lib import helpers as h
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import Gist
38 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.scm import ScmModel
40 from rhodecode.lib.vcs import get_repo
41
42 log = logging.getLogger(__name__)
43
44 GIST_STORE_LOC = '.gist_store'
45
46
47 class GistModel(BaseModel):
48
49 def _get_gist(self, gist):
50 """
51 Helper method to get gist by ID, or gist_access_id as a fallback
52
53 :param gist: GistID, gist_access_id, or Gist instance
54 """
55 return self._get_instance(Gist, gist,
56 callback=Gist.get_by_access_id)
57
58 def __delete_gist(self, gist):
59 """
60 removes gist from filesystem
61
62 :param gist: gist object
63 """
64 root_path = RepoModel().repos_path
65 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
66 log.info("Removing %s" % (rm_path))
67 shutil.rmtree(rm_path)
68
69 def get_gist_files(self, gist_access_id):
70 """
71 Get files for given gist
72
73 :param gist_access_id:
74 """
75 root_path = RepoModel().repos_path
76 r = get_repo(os.path.join(*map(safe_str,
77 [root_path, GIST_STORE_LOC, gist_access_id])))
78 cs = r.get_changeset()
79 return (
80 cs, [n for n in cs.get_node('/')]
81 )
82
83 def create(self, description, owner, gist_mapping,
84 gist_type=Gist.GIST_PUBLIC, lifetime=-1):
85 """
86
87 :param description: description of the gist
88 :param owner: user who created this gist
89 :param gist_mapping: mapping {filename:{'content':content},...}
90 :param gist_type: type of gist private/public
91 :param lifetime: in minutes, -1 == forever
92 """
93 gist_id = safe_unicode(unique_id(20))
94 lifetime = safe_int(lifetime, -1)
95 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
96 log.debug('set GIST expiration date to: %s'
97 % (time_to_datetime(gist_expires)
98 if gist_expires != -1 else 'forever'))
99 #create the Database version
100 gist = Gist()
101 gist.gist_description = description
102 gist.gist_access_id = gist_id
103 gist.gist_owner = owner.user_id
104 gist.gist_expires = gist_expires
105 gist.gist_type = safe_unicode(gist_type)
106 self.sa.add(gist)
107 self.sa.flush()
108 if gist_type == Gist.GIST_PUBLIC:
109 # use DB ID for easy to use GIST ID
110 gist_id = safe_unicode(gist.gist_id)
111 gist.gist_access_id = gist_id
112 self.sa.add(gist)
113
114 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
115 log.debug('Creating new %s GIST repo in %s' % (gist_type, gist_repo_path))
116 repo = RepoModel()._create_repo(repo_name=gist_repo_path, alias='hg',
117 parent=None)
118
119 processed_mapping = {}
120 for filename in gist_mapping:
121 content = gist_mapping[filename]['content']
122 #TODO: expand support for setting explicit lexers
123 # if lexer is None:
124 # try:
125 # lexer = pygments.lexers.guess_lexer_for_filename(filename,content)
126 # except pygments.util.ClassNotFound:
127 # lexer = 'text'
128 processed_mapping[filename] = {'content': content}
129
130 # now create single multifile commit
131 message = 'added file'
132 message += 's: ' if len(processed_mapping) > 1 else ': '
133 message += ', '.join([x for x in processed_mapping])
134
135 #fake RhodeCode Repository object
136 fake_repo = AttributeDict(dict(
137 repo_name=gist_repo_path,
138 scm_instance_no_cache=lambda: repo,
139 ))
140 ScmModel().create_nodes(
141 user=owner.user_id, repo=fake_repo,
142 message=message,
143 nodes=processed_mapping,
144 trigger_push_hook=False
145 )
146
147 return gist
148
149 def delete(self, gist, fs_remove=True):
150 gist = self._get_gist(gist)
151
152 try:
153 self.sa.delete(gist)
154 if fs_remove:
155 self.__delete_gist(gist)
156 else:
157 log.debug('skipping removal from filesystem')
158
159 except Exception:
160 log.error(traceback.format_exc())
161 raise
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,148 +1,148 b''
1 1 """
2 2 Base utils for shell scripts
3 3 """
4 4 import os
5 5 import sys
6 6 import random
7 7 import urllib2
8 8 import pprint
9 9
10 10 try:
11 11 from rhodecode.lib.ext_json import json
12 12 except ImportError:
13 13 try:
14 14 import simplejson as json
15 15 except ImportError:
16 16 import json
17 17
18 18 CONFIG_NAME = '.rhodecode'
19 19 FORMAT_PRETTY = 'pretty'
20 20 FORMAT_JSON = 'json'
21 21
22 22
23 23 def api_call(apikey, apihost, format, method=None, **kw):
24 24 """
25 25 Api_call wrapper for RhodeCode
26 26
27 27 :param apikey:
28 28 :param apihost:
29 29 :param format: formatting, pretty means prints and pprint of json
30 30 json returns unparsed json
31 31 :param method:
32 32 """
33 33 def _build_data(random_id):
34 34 """
35 35 Builds API data with given random ID
36 36
37 37 :param random_id:
38 :type random_id:
39 38 """
40 39 return {
41 40 "id": random_id,
42 41 "api_key": apikey,
43 42 "method": method,
44 43 "args": kw
45 44 }
46 45
47 46 if not method:
48 47 raise Exception('please specify method name !')
49 48 id_ = random.randrange(1, 9999)
50 49 req = urllib2.Request('%s/_admin/api' % apihost,
51 50 data=json.dumps(_build_data(id_)),
52 51 headers={'content-type': 'text/plain'})
53 52 if format == FORMAT_PRETTY:
54 53 sys.stdout.write('calling %s to %s \n' % (req.get_data(), apihost))
55 54 ret = urllib2.urlopen(req)
56 55 raw_json = ret.read()
57 56 json_data = json.loads(raw_json)
58 57 id_ret = json_data['id']
59 58 _formatted_json = pprint.pformat(json_data)
60 59 if id_ret == id_:
61 60 if format == FORMAT_JSON:
62 61 sys.stdout.write(str(raw_json))
63 62 else:
64 63 sys.stdout.write('rhodecode returned:\n%s\n' % (_formatted_json))
65 64
66 65 else:
67 66 raise Exception('something went wrong. '
68 67 'ID mismatch got %s, expected %s | %s' % (
69 68 id_ret, id_, _formatted_json))
70 69
71 70
72 71 class RcConf(object):
73 72 """
74 73 RhodeCode config for API
75 74
76 75 conf = RcConf()
77 76 conf['key']
78 77
79 78 """
80 79
81 80 def __init__(self, config_location=None, autoload=True, autocreate=False,
82 81 config=None):
83 self._conf_name = CONFIG_NAME if not config_location else config_location
82 HOME = os.getenv('HOME', os.getenv('USERPROFILE')) or ''
83 HOME_CONF = os.path.abspath(os.path.join(HOME, CONFIG_NAME))
84 self._conf_name = HOME_CONF if not config_location else config_location
84 85 self._conf = {}
85 86 if autocreate:
86 87 self.make_config(config)
87 88 if autoload:
88 89 self._conf = self.load_config()
89 90
90 91 def __getitem__(self, key):
91 92 return self._conf[key]
92 93
93 94 def __nonzero__(self):
94 95 if self._conf:
95 96 return True
96 97 return False
97 98
98 99 def __eq__(self):
99 100 return self._conf.__eq__()
100 101
101 102 def __repr__(self):
102 103 return 'RcConf<%s>' % self._conf.__repr__()
103 104
104 105 def make_config(self, config):
105 106 """
106 107 Saves given config as a JSON dump in the _conf_name location
107 108
108 109 :param config:
109 :type config:
110 110 """
111 111 update = False
112 112 if os.path.exists(self._conf_name):
113 113 update = True
114 114 with open(self._conf_name, 'wb') as f:
115 115 json.dump(config, f, indent=4)
116 116
117 117 if update:
118 118 sys.stdout.write('Updated config in %s\n' % self._conf_name)
119 119 else:
120 120 sys.stdout.write('Created new config in %s\n' % self._conf_name)
121 121
122 122 def update_config(self, new_config):
123 123 """
124 124 Reads the JSON config updates it's values with new_config and
125 125 saves it back as JSON dump
126 126
127 127 :param new_config:
128 128 """
129 129 config = {}
130 130 try:
131 131 with open(self._conf_name, 'rb') as conf:
132 132 config = json.load(conf)
133 133 except IOError, e:
134 134 sys.stderr.write(str(e) + '\n')
135 135
136 136 config.update(new_config)
137 137 self.make_config(config)
138 138
139 139 def load_config(self):
140 140 """
141 141 Loads config from file and returns loaded JSON object
142 142 """
143 143 try:
144 144 with open(self._conf_name, 'rb') as conf:
145 145 return json.load(conf)
146 146 except IOError, e:
147 147 #sys.stderr.write(str(e) + '\n')
148 148 pass
@@ -1,107 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.bin.api
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Api CLI client for RhodeCode
7 7
8 8 :created_on: Jun 3, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 from __future__ import with_statement
27 27 import sys
28 28 import argparse
29 29
30 30 from rhodecode.bin.base import api_call, RcConf, FORMAT_JSON, FORMAT_PRETTY
31 31
32 32
33 33 def argparser(argv):
34 34 usage = (
35 35 "rhodecode-api [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
36 36 "[--config=CONFIG] [--save-config] "
37 37 "METHOD <key:val> <key2:val> ...\n"
38 38 "Create config file: rhodecode-gist --apikey=<key> --apihost=http://rhodecode.server --save-config"
39 39 )
40 40
41 41 parser = argparse.ArgumentParser(description='RhodeCode API cli',
42 42 usage=usage)
43 43
44 44 ## config
45 45 group = parser.add_argument_group('config')
46 46 group.add_argument('--apikey', help='api access key')
47 47 group.add_argument('--apihost', help='api host')
48 48 group.add_argument('--config', help='config file')
49 49 group.add_argument('--save-config', action='store_true', help='save the given config into a file')
50 50
51 51 group = parser.add_argument_group('API')
52 52 group.add_argument('method', metavar='METHOD', nargs='?', type=str, default=None,
53 53 help='API method name to call followed by key:value attributes',
54 54 )
55 55 group.add_argument('--format', dest='format', type=str,
56 56 help='output format default: `pretty` can '
57 57 'be also `%s`' % FORMAT_JSON,
58 58 default=FORMAT_PRETTY
59 59 )
60 60 args, other = parser.parse_known_args()
61 61 return parser, args, other
62 62
63 63
64 64 def main(argv=None):
65 65 """
66 66 Main execution function for cli
67 67
68 68 :param argv:
69 :type argv:
70 69 """
71 70 if argv is None:
72 71 argv = sys.argv
73 72
74 73 conf = None
75 74 parser, args, other = argparser(argv)
76 75
77 76 api_credentials_given = (args.apikey and args.apihost)
78 77 if args.save_config:
79 78 if not api_credentials_given:
80 79 raise parser.error('--save-config requires --apikey and --apihost')
81 80 conf = RcConf(config_location=args.config,
82 81 autocreate=True, config={'apikey': args.apikey,
83 82 'apihost': args.apihost})
84 83 sys.exit()
85 84
86 85 if not conf:
87 86 conf = RcConf(config_location=args.config, autoload=True)
88 87 if not conf:
89 88 if not api_credentials_given:
90 89 parser.error('Could not find config file and missing '
91 90 '--apikey or --apihost in params')
92 91
93 92 apikey = args.apikey or conf['apikey']
94 93 host = args.apihost or conf['apihost']
95 94 method = args.method
96 95
97 96 try:
98 97 margs = dict(map(lambda s: s.split(':', 1), other))
99 98 except Exception:
100 99 sys.stderr.write('Error parsing arguments \n')
101 100 sys.exit()
102 101
103 102 api_call(apikey, host, args.format, method, **margs)
104 103 return 0
105 104
106 105 if __name__ == '__main__':
107 106 sys.exit(main(sys.argv))
@@ -1,680 +1,683 b''
1 1 """
2 2 Routes configuration
3 3
4 4 The more specific and detailed routes should be defined first so they
5 5 may take precedent over the more generic routes. For more information
6 6 refer to the routes manual at http://routes.groovie.org/docs/
7 7 """
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11 11 # prefix for non repository related links needs to be prefixed with `/`
12 12 ADMIN_PREFIX = '/_admin'
13 13
14 14
15 15 def make_map(config):
16 16 """Create, configure and return the routes Mapper"""
17 17 rmap = Mapper(directory=config['pylons.paths']['controllers'],
18 18 always_scan=config['debug'])
19 19 rmap.minimization = False
20 20 rmap.explicit = False
21 21
22 22 from rhodecode.lib.utils import is_valid_repo
23 23 from rhodecode.lib.utils import is_valid_repos_group
24 24
25 25 def check_repo(environ, match_dict):
26 26 """
27 27 check for valid repository for proper 404 handling
28 28
29 29 :param environ:
30 30 :param match_dict:
31 31 """
32 32 from rhodecode.model.db import Repository
33 33 repo_name = match_dict.get('repo_name')
34 34
35 35 if match_dict.get('f_path'):
36 36 #fix for multiple initial slashes that causes errors
37 37 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
38 38
39 39 try:
40 40 by_id = repo_name.split('_')
41 41 if len(by_id) == 2 and by_id[1].isdigit() and by_id[0] == '':
42 42 repo_name = Repository.get(by_id[1]).repo_name
43 43 match_dict['repo_name'] = repo_name
44 44 except Exception:
45 45 pass
46 46
47 47 return is_valid_repo(repo_name, config['base_path'])
48 48
49 49 def check_group(environ, match_dict):
50 50 """
51 51 check for valid repository group for proper 404 handling
52 52
53 53 :param environ:
54 54 :param match_dict:
55 55 """
56 56 repos_group_name = match_dict.get('group_name')
57 57 return is_valid_repos_group(repos_group_name, config['base_path'])
58 58
59 59 def check_group_skip_path(environ, match_dict):
60 60 """
61 61 check for valid repository group for proper 404 handling, but skips
62 62 verification of existing path
63 63
64 64 :param environ:
65 65 :param match_dict:
66 66 """
67 67 repos_group_name = match_dict.get('group_name')
68 68 return is_valid_repos_group(repos_group_name, config['base_path'],
69 69 skip_path_check=True)
70 70
71 71 def check_user_group(environ, match_dict):
72 72 """
73 73 check for valid user group for proper 404 handling
74 74
75 75 :param environ:
76 76 :param match_dict:
77 77 """
78 78 return True
79 79
80 80 def check_int(environ, match_dict):
81 81 return match_dict.get('id').isdigit()
82 82
83 83 # The ErrorController route (handles 404/500 error pages); it should
84 84 # likely stay at the top, ensuring it can always be resolved
85 85 rmap.connect('/error/{action}', controller='error')
86 86 rmap.connect('/error/{action}/{id}', controller='error')
87 87
88 88 #==========================================================================
89 89 # CUSTOM ROUTES HERE
90 90 #==========================================================================
91 91
92 92 #MAIN PAGE
93 93 rmap.connect('home', '/', controller='home', action='index')
94 94 rmap.connect('repo_switcher', '/repos', controller='home',
95 95 action='repo_switcher')
96 96 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*?}',
97 97 controller='home', action='branch_tag_switcher')
98 98 rmap.connect('bugtracker',
99 99 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
100 100 _static=True)
101 101 rmap.connect('rst_help',
102 102 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
103 103 _static=True)
104 104 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
105 105
106 106 #ADMIN REPOSITORY REST ROUTES
107 107 with rmap.submapper(path_prefix=ADMIN_PREFIX,
108 108 controller='admin/repos') as m:
109 109 m.connect("repos", "/repos",
110 110 action="create", conditions=dict(method=["POST"]))
111 111 m.connect("repos", "/repos",
112 112 action="index", conditions=dict(method=["GET"]))
113 113 m.connect("formatted_repos", "/repos.{format}",
114 114 action="index",
115 115 conditions=dict(method=["GET"]))
116 116 m.connect("new_repo", "/create_repository",
117 117 action="create_repository", conditions=dict(method=["GET"]))
118 118 m.connect("/repos/{repo_name:.*?}",
119 119 action="update", conditions=dict(method=["PUT"],
120 120 function=check_repo))
121 121 m.connect("/repos/{repo_name:.*?}",
122 122 action="delete", conditions=dict(method=["DELETE"],
123 123 function=check_repo))
124 124 m.connect("formatted_edit_repo", "/repos/{repo_name:.*?}.{format}/edit",
125 125 action="edit", conditions=dict(method=["GET"],
126 126 function=check_repo))
127 127 m.connect("repo", "/repos/{repo_name:.*?}",
128 128 action="show", conditions=dict(method=["GET"],
129 129 function=check_repo))
130 130 m.connect("formatted_repo", "/repos/{repo_name:.*?}.{format}",
131 131 action="show", conditions=dict(method=["GET"],
132 132 function=check_repo))
133 133 #add repo perm member
134 134 m.connect('set_repo_perm_member',
135 135 "/repos/{repo_name:.*?}/grant_perm",
136 136 action="set_repo_perm_member",
137 137 conditions=dict(method=["POST"], function=check_repo))
138 138
139 139 #ajax delete repo perm user
140 140 m.connect('delete_repo_perm_member',
141 141 "/repos/{repo_name:.*?}/revoke_perm",
142 142 action="delete_repo_perm_member",
143 143 conditions=dict(method=["DELETE"], function=check_repo))
144 144
145 145 #settings actions
146 146 m.connect('repo_stats', "/repos_stats/{repo_name:.*?}",
147 147 action="repo_stats", conditions=dict(method=["DELETE"],
148 148 function=check_repo))
149 149 m.connect('repo_cache', "/repos_cache/{repo_name:.*?}",
150 150 action="repo_cache", conditions=dict(method=["DELETE"],
151 151 function=check_repo))
152 152 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*?}",
153 153 action="repo_public_journal", conditions=dict(method=["PUT"],
154 154 function=check_repo))
155 155 m.connect('repo_pull', "/repo_pull/{repo_name:.*?}",
156 156 action="repo_pull", conditions=dict(method=["PUT"],
157 157 function=check_repo))
158 158 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}",
159 159 action="repo_as_fork", conditions=dict(method=["PUT"],
160 160 function=check_repo))
161 161 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
162 162 action="repo_locking", conditions=dict(method=["PUT"],
163 163 function=check_repo))
164 164 m.connect('toggle_locking', "/locking_toggle/{repo_name:.*?}",
165 165 action="toggle_locking", conditions=dict(method=["GET"],
166 166 function=check_repo))
167 167
168 168 #repo fields
169 169 m.connect('create_repo_fields', "/repo_fields/{repo_name:.*?}/new",
170 170 action="create_repo_field", conditions=dict(method=["PUT"],
171 171 function=check_repo))
172 172
173 173 m.connect('delete_repo_fields', "/repo_fields/{repo_name:.*?}/{field_id}",
174 174 action="delete_repo_field", conditions=dict(method=["DELETE"],
175 175 function=check_repo))
176 176
177 177 with rmap.submapper(path_prefix=ADMIN_PREFIX,
178 178 controller='admin/repos_groups') as m:
179 179 m.connect("repos_groups", "/repos_groups",
180 180 action="create", conditions=dict(method=["POST"]))
181 181 m.connect("repos_groups", "/repos_groups",
182 182 action="index", conditions=dict(method=["GET"]))
183 183 m.connect("formatted_repos_groups", "/repos_groups.{format}",
184 184 action="index", conditions=dict(method=["GET"]))
185 185 m.connect("new_repos_group", "/repos_groups/new",
186 186 action="new", conditions=dict(method=["GET"]))
187 187 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
188 188 action="new", conditions=dict(method=["GET"]))
189 189 m.connect("update_repos_group", "/repos_groups/{group_name:.*?}",
190 190 action="update", conditions=dict(method=["PUT"],
191 191 function=check_group))
192 192 #add repo group perm member
193 193 m.connect('set_repo_group_perm_member',
194 194 "/repos_groups/{group_name:.*?}/grant_perm",
195 195 action="set_repo_group_perm_member",
196 196 conditions=dict(method=["POST"], function=check_group))
197 197
198 198 #ajax delete repo group perm
199 199 m.connect('delete_repo_group_perm_member',
200 200 "/repos_groups/{group_name:.*?}/revoke_perm",
201 201 action="delete_repo_group_perm_member",
202 202 conditions=dict(method=["DELETE"], function=check_group))
203 203
204 204 m.connect("delete_repos_group", "/repos_groups/{group_name:.*?}",
205 205 action="delete", conditions=dict(method=["DELETE"],
206 206 function=check_group_skip_path))
207 207 m.connect("edit_repos_group", "/repos_groups/{group_name:.*?}/edit",
208 208 action="edit", conditions=dict(method=["GET"],
209 209 function=check_group))
210 210 m.connect("formatted_edit_repos_group",
211 211 "/repos_groups/{group_name:.*?}.{format}/edit",
212 212 action="edit", conditions=dict(method=["GET"],
213 213 function=check_group))
214 214 m.connect("repos_group", "/repos_groups/{group_name:.*?}",
215 215 action="show", conditions=dict(method=["GET"],
216 216 function=check_group))
217 217 m.connect("formatted_repos_group", "/repos_groups/{group_name:.*?}.{format}",
218 218 action="show", conditions=dict(method=["GET"],
219 219 function=check_group))
220 220
221 221 #ADMIN USER REST ROUTES
222 222 with rmap.submapper(path_prefix=ADMIN_PREFIX,
223 223 controller='admin/users') as m:
224 224 m.connect("users", "/users",
225 225 action="create", conditions=dict(method=["POST"]))
226 226 m.connect("users", "/users",
227 227 action="index", conditions=dict(method=["GET"]))
228 228 m.connect("formatted_users", "/users.{format}",
229 229 action="index", conditions=dict(method=["GET"]))
230 230 m.connect("new_user", "/users/new",
231 231 action="new", conditions=dict(method=["GET"]))
232 232 m.connect("formatted_new_user", "/users/new.{format}",
233 233 action="new", conditions=dict(method=["GET"]))
234 234 m.connect("update_user", "/users/{id}",
235 235 action="update", conditions=dict(method=["PUT"]))
236 236 m.connect("delete_user", "/users/{id}",
237 237 action="delete", conditions=dict(method=["DELETE"]))
238 238 m.connect("edit_user", "/users/{id}/edit",
239 239 action="edit", conditions=dict(method=["GET"]))
240 240 m.connect("formatted_edit_user",
241 241 "/users/{id}.{format}/edit",
242 242 action="edit", conditions=dict(method=["GET"]))
243 243 m.connect("user", "/users/{id}",
244 244 action="show", conditions=dict(method=["GET"]))
245 245 m.connect("formatted_user", "/users/{id}.{format}",
246 246 action="show", conditions=dict(method=["GET"]))
247 247
248 248 #EXTRAS USER ROUTES
249 249 m.connect("user_perm", "/users_perm/{id}",
250 250 action="update_perm", conditions=dict(method=["PUT"]))
251 251 m.connect("user_emails", "/users_emails/{id}",
252 252 action="add_email", conditions=dict(method=["PUT"]))
253 253 m.connect("user_emails_delete", "/users_emails/{id}",
254 254 action="delete_email", conditions=dict(method=["DELETE"]))
255 255 m.connect("user_ips", "/users_ips/{id}",
256 256 action="add_ip", conditions=dict(method=["PUT"]))
257 257 m.connect("user_ips_delete", "/users_ips/{id}",
258 258 action="delete_ip", conditions=dict(method=["DELETE"]))
259 259
260 260 #ADMIN USER GROUPS REST ROUTES
261 261 with rmap.submapper(path_prefix=ADMIN_PREFIX,
262 262 controller='admin/users_groups') as m:
263 263 m.connect("users_groups", "/users_groups",
264 264 action="create", conditions=dict(method=["POST"]))
265 265 m.connect("users_groups", "/users_groups",
266 266 action="index", conditions=dict(method=["GET"]))
267 267 m.connect("formatted_users_groups", "/users_groups.{format}",
268 268 action="index", conditions=dict(method=["GET"]))
269 269 m.connect("new_users_group", "/users_groups/new",
270 270 action="new", conditions=dict(method=["GET"]))
271 271 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
272 272 action="new", conditions=dict(method=["GET"]))
273 273 m.connect("update_users_group", "/users_groups/{id}",
274 274 action="update", conditions=dict(method=["PUT"]))
275 275 m.connect("delete_users_group", "/users_groups/{id}",
276 276 action="delete", conditions=dict(method=["DELETE"]))
277 277 m.connect("edit_users_group", "/users_groups/{id}/edit",
278 278 action="edit", conditions=dict(method=["GET"]),
279 279 function=check_user_group)
280 280 m.connect("formatted_edit_users_group",
281 281 "/users_groups/{id}.{format}/edit",
282 282 action="edit", conditions=dict(method=["GET"]))
283 283 m.connect("users_group", "/users_groups/{id}",
284 284 action="show", conditions=dict(method=["GET"]))
285 285 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
286 286 action="show", conditions=dict(method=["GET"]))
287 287
288 288 #EXTRAS USER ROUTES
289 289 # update
290 290 m.connect("users_group_perm", "/users_groups/{id}/update_global_perm",
291 291 action="update_perm", conditions=dict(method=["PUT"]))
292 292
293 293 #add user group perm member
294 294 m.connect('set_user_group_perm_member', "/users_groups/{id}/grant_perm",
295 295 action="set_user_group_perm_member",
296 296 conditions=dict(method=["POST"]))
297 297
298 298 #ajax delete user group perm
299 299 m.connect('delete_user_group_perm_member', "/users_groups/{id}/revoke_perm",
300 300 action="delete_user_group_perm_member",
301 301 conditions=dict(method=["DELETE"]))
302 302
303 303 #ADMIN GROUP REST ROUTES
304 304 rmap.resource('group', 'groups',
305 305 controller='admin/groups', path_prefix=ADMIN_PREFIX)
306 306
307 307 #ADMIN PERMISSIONS REST ROUTES
308 308 rmap.resource('permission', 'permissions',
309 309 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
310 310
311 311 #ADMIN DEFAULTS REST ROUTES
312 312 rmap.resource('default', 'defaults',
313 313 controller='admin/defaults', path_prefix=ADMIN_PREFIX)
314 314
315 315 ##ADMIN LDAP SETTINGS
316 316 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
317 317 controller='admin/ldap_settings', action='ldap_settings',
318 318 conditions=dict(method=["POST"]))
319 319
320 320 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
321 321 controller='admin/ldap_settings')
322 322
323 323 #ADMIN SETTINGS REST ROUTES
324 324 with rmap.submapper(path_prefix=ADMIN_PREFIX,
325 325 controller='admin/settings') as m:
326 326 m.connect("admin_settings", "/settings",
327 327 action="create", conditions=dict(method=["POST"]))
328 328 m.connect("admin_settings", "/settings",
329 329 action="index", conditions=dict(method=["GET"]))
330 330 m.connect("formatted_admin_settings", "/settings.{format}",
331 331 action="index", conditions=dict(method=["GET"]))
332 332 m.connect("admin_new_setting", "/settings/new",
333 333 action="new", conditions=dict(method=["GET"]))
334 334 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
335 335 action="new", conditions=dict(method=["GET"]))
336 336 m.connect("/settings/{setting_id}",
337 337 action="update", conditions=dict(method=["PUT"]))
338 338 m.connect("/settings/{setting_id}",
339 339 action="delete", conditions=dict(method=["DELETE"]))
340 340 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
341 341 action="edit", conditions=dict(method=["GET"]))
342 342 m.connect("formatted_admin_edit_setting",
343 343 "/settings/{setting_id}.{format}/edit",
344 344 action="edit", conditions=dict(method=["GET"]))
345 345 m.connect("admin_setting", "/settings/{setting_id}",
346 346 action="show", conditions=dict(method=["GET"]))
347 347 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
348 348 action="show", conditions=dict(method=["GET"]))
349 349 m.connect("admin_settings_my_account", "/my_account",
350 350 action="my_account", conditions=dict(method=["GET"]))
351 351 m.connect("admin_settings_my_account_update", "/my_account_update",
352 352 action="my_account_update", conditions=dict(method=["PUT"]))
353 353 m.connect("admin_settings_my_repos", "/my_account/repos",
354 354 action="my_account_my_repos", conditions=dict(method=["GET"]))
355 355 m.connect("admin_settings_my_pullrequests", "/my_account/pull_requests",
356 356 action="my_account_my_pullrequests", conditions=dict(method=["GET"]))
357 357
358 358 #NOTIFICATION REST ROUTES
359 359 with rmap.submapper(path_prefix=ADMIN_PREFIX,
360 360 controller='admin/notifications') as m:
361 361 m.connect("notifications", "/notifications",
362 362 action="create", conditions=dict(method=["POST"]))
363 363 m.connect("notifications", "/notifications",
364 364 action="index", conditions=dict(method=["GET"]))
365 365 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
366 366 action="mark_all_read", conditions=dict(method=["GET"]))
367 367 m.connect("formatted_notifications", "/notifications.{format}",
368 368 action="index", conditions=dict(method=["GET"]))
369 369 m.connect("new_notification", "/notifications/new",
370 370 action="new", conditions=dict(method=["GET"]))
371 371 m.connect("formatted_new_notification", "/notifications/new.{format}",
372 372 action="new", conditions=dict(method=["GET"]))
373 373 m.connect("/notification/{notification_id}",
374 374 action="update", conditions=dict(method=["PUT"]))
375 375 m.connect("/notification/{notification_id}",
376 376 action="delete", conditions=dict(method=["DELETE"]))
377 377 m.connect("edit_notification", "/notification/{notification_id}/edit",
378 378 action="edit", conditions=dict(method=["GET"]))
379 379 m.connect("formatted_edit_notification",
380 380 "/notification/{notification_id}.{format}/edit",
381 381 action="edit", conditions=dict(method=["GET"]))
382 382 m.connect("notification", "/notification/{notification_id}",
383 383 action="show", conditions=dict(method=["GET"]))
384 384 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
385 385 action="show", conditions=dict(method=["GET"]))
386 386
387 387 #ADMIN MAIN PAGES
388 388 with rmap.submapper(path_prefix=ADMIN_PREFIX,
389 389 controller='admin/admin') as m:
390 390 m.connect('admin_home', '', action='index')
391 391 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
392 392 action='add_repo')
393 393
394 #ADMIN GIST
395 rmap.resource('gist', 'gists', controller='admin/gists',
396 path_prefix=ADMIN_PREFIX)
394 397 #==========================================================================
395 398 # API V2
396 399 #==========================================================================
397 400 with rmap.submapper(path_prefix=ADMIN_PREFIX,
398 401 controller='api/api') as m:
399 402 m.connect('api', '/api')
400 403
401 404 #USER JOURNAL
402 405 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
403 406 controller='journal', action='index')
404 407 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
405 408 controller='journal', action='journal_rss')
406 409 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
407 410 controller='journal', action='journal_atom')
408 411
409 412 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
410 413 controller='journal', action="public_journal")
411 414
412 415 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
413 416 controller='journal', action="public_journal_rss")
414 417
415 418 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
416 419 controller='journal', action="public_journal_rss")
417 420
418 421 rmap.connect('public_journal_atom',
419 422 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
420 423 action="public_journal_atom")
421 424
422 425 rmap.connect('public_journal_atom_old',
423 426 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
424 427 action="public_journal_atom")
425 428
426 429 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
427 430 controller='journal', action='toggle_following',
428 431 conditions=dict(method=["POST"]))
429 432
430 433 #SEARCH
431 434 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
432 435 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
433 436 controller='search',
434 437 conditions=dict(function=check_repo))
435 438 rmap.connect('search_repo', '/{repo_name:.*?}/search',
436 439 controller='search',
437 440 conditions=dict(function=check_repo),
438 441 )
439 442
440 443 #LOGIN/LOGOUT/REGISTER/SIGN IN
441 444 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
442 445 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
443 446 action='logout')
444 447
445 448 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
446 449 action='register')
447 450
448 451 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
449 452 controller='login', action='password_reset')
450 453
451 454 rmap.connect('reset_password_confirmation',
452 455 '%s/password_reset_confirmation' % ADMIN_PREFIX,
453 456 controller='login', action='password_reset_confirmation')
454 457
455 458 #FEEDS
456 459 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
457 460 controller='feed', action='rss',
458 461 conditions=dict(function=check_repo))
459 462
460 463 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
461 464 controller='feed', action='atom',
462 465 conditions=dict(function=check_repo))
463 466
464 467 #==========================================================================
465 468 # REPOSITORY ROUTES
466 469 #==========================================================================
467 470 rmap.connect('summary_home', '/{repo_name:.*?}',
468 471 controller='summary',
469 472 conditions=dict(function=check_repo))
470 473
471 474 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
472 475 controller='summary', action='repo_size',
473 476 conditions=dict(function=check_repo))
474 477
475 478 rmap.connect('repos_group_home', '/{group_name:.*}',
476 479 controller='admin/repos_groups', action="show_by_name",
477 480 conditions=dict(function=check_group))
478 481
479 482 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision}',
480 483 controller='changeset', revision='tip',
481 484 conditions=dict(function=check_repo))
482 485
483 486 # no longer user, but kept for routes to work
484 487 rmap.connect("_edit_repo", "/{repo_name:.*?}/edit",
485 488 controller='admin/repos', action="edit",
486 489 conditions=dict(method=["GET"], function=check_repo)
487 490 )
488 491
489 492 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
490 493 controller='admin/repos', action="edit",
491 494 conditions=dict(method=["GET"], function=check_repo)
492 495 )
493 496
494 497 #still working url for backward compat.
495 498 rmap.connect('raw_changeset_home_depraced',
496 499 '/{repo_name:.*?}/raw-changeset/{revision}',
497 500 controller='changeset', action='changeset_raw',
498 501 revision='tip', conditions=dict(function=check_repo))
499 502
500 503 ## new URLs
501 504 rmap.connect('changeset_raw_home',
502 505 '/{repo_name:.*?}/changeset-diff/{revision}',
503 506 controller='changeset', action='changeset_raw',
504 507 revision='tip', conditions=dict(function=check_repo))
505 508
506 509 rmap.connect('changeset_patch_home',
507 510 '/{repo_name:.*?}/changeset-patch/{revision}',
508 511 controller='changeset', action='changeset_patch',
509 512 revision='tip', conditions=dict(function=check_repo))
510 513
511 514 rmap.connect('changeset_download_home',
512 515 '/{repo_name:.*?}/changeset-download/{revision}',
513 516 controller='changeset', action='changeset_download',
514 517 revision='tip', conditions=dict(function=check_repo))
515 518
516 519 rmap.connect('changeset_comment',
517 520 '/{repo_name:.*?}/changeset/{revision}/comment',
518 521 controller='changeset', revision='tip', action='comment',
519 522 conditions=dict(function=check_repo))
520 523
521 524 rmap.connect('changeset_comment_preview',
522 525 '/{repo_name:.*?}/changeset/comment/preview',
523 526 controller='changeset', action='preview_comment',
524 527 conditions=dict(function=check_repo, method=["POST"]))
525 528
526 529 rmap.connect('changeset_comment_delete',
527 530 '/{repo_name:.*?}/changeset/comment/{comment_id}/delete',
528 531 controller='changeset', action='delete_comment',
529 532 conditions=dict(function=check_repo, method=["DELETE"]))
530 533
531 534 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
532 535 controller='changeset', action='changeset_info')
533 536
534 537 rmap.connect('compare_url',
535 538 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref:.*?}...{other_ref_type}@{other_ref:.*?}',
536 539 controller='compare', action='index',
537 540 conditions=dict(function=check_repo),
538 541 requirements=dict(
539 542 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
540 543 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
541 544 )
542 545
543 546 rmap.connect('pullrequest_home',
544 547 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
545 548 action='index', conditions=dict(function=check_repo,
546 549 method=["GET"]))
547 550
548 551 rmap.connect('pullrequest',
549 552 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
550 553 action='create', conditions=dict(function=check_repo,
551 554 method=["POST"]))
552 555
553 556 rmap.connect('pullrequest_show',
554 557 '/{repo_name:.*?}/pull-request/{pull_request_id}',
555 558 controller='pullrequests',
556 559 action='show', conditions=dict(function=check_repo,
557 560 method=["GET"]))
558 561 rmap.connect('pullrequest_update',
559 562 '/{repo_name:.*?}/pull-request/{pull_request_id}',
560 563 controller='pullrequests',
561 564 action='update', conditions=dict(function=check_repo,
562 565 method=["PUT"]))
563 566 rmap.connect('pullrequest_delete',
564 567 '/{repo_name:.*?}/pull-request/{pull_request_id}',
565 568 controller='pullrequests',
566 569 action='delete', conditions=dict(function=check_repo,
567 570 method=["DELETE"]))
568 571
569 572 rmap.connect('pullrequest_show_all',
570 573 '/{repo_name:.*?}/pull-request',
571 574 controller='pullrequests',
572 575 action='show_all', conditions=dict(function=check_repo,
573 576 method=["GET"]))
574 577
575 578 rmap.connect('pullrequest_comment',
576 579 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
577 580 controller='pullrequests',
578 581 action='comment', conditions=dict(function=check_repo,
579 582 method=["POST"]))
580 583
581 584 rmap.connect('pullrequest_comment_delete',
582 585 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
583 586 controller='pullrequests', action='delete_comment',
584 587 conditions=dict(function=check_repo, method=["DELETE"]))
585 588
586 589 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
587 590 controller='summary', conditions=dict(function=check_repo))
588 591
589 592 rmap.connect('branches_home', '/{repo_name:.*?}/branches',
590 593 controller='branches', conditions=dict(function=check_repo))
591 594
592 595 rmap.connect('tags_home', '/{repo_name:.*?}/tags',
593 596 controller='tags', conditions=dict(function=check_repo))
594 597
595 598 rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
596 599 controller='bookmarks', conditions=dict(function=check_repo))
597 600
598 601 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
599 602 controller='changelog', conditions=dict(function=check_repo))
600 603
601 604 rmap.connect('changelog_summary_home', '/{repo_name:.*?}/changelog_summary',
602 605 controller='changelog', action='changelog_summary',
603 606 conditions=dict(function=check_repo))
604 607
605 608 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
606 609 controller='changelog', f_path=None,
607 610 conditions=dict(function=check_repo))
608 611
609 612 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
610 613 controller='changelog', action='changelog_details',
611 614 conditions=dict(function=check_repo))
612 615
613 616 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
614 617 controller='files', revision='tip', f_path='',
615 618 conditions=dict(function=check_repo))
616 619
617 620 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
618 621 controller='files', revision='tip', f_path='',
619 622 conditions=dict(function=check_repo))
620 623
621 624 rmap.connect('files_history_home',
622 625 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
623 626 controller='files', action='history', revision='tip', f_path='',
624 627 conditions=dict(function=check_repo))
625 628
626 629 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
627 630 controller='files', action='diff', revision='tip', f_path='',
628 631 conditions=dict(function=check_repo))
629 632
630 633 rmap.connect('files_rawfile_home',
631 634 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
632 635 controller='files', action='rawfile', revision='tip',
633 636 f_path='', conditions=dict(function=check_repo))
634 637
635 638 rmap.connect('files_raw_home',
636 639 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
637 640 controller='files', action='raw', revision='tip', f_path='',
638 641 conditions=dict(function=check_repo))
639 642
640 643 rmap.connect('files_annotate_home',
641 644 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
642 645 controller='files', action='index', revision='tip',
643 646 f_path='', annotate=True, conditions=dict(function=check_repo))
644 647
645 648 rmap.connect('files_edit_home',
646 649 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
647 650 controller='files', action='edit', revision='tip',
648 651 f_path='', conditions=dict(function=check_repo))
649 652
650 653 rmap.connect('files_add_home',
651 654 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
652 655 controller='files', action='add', revision='tip',
653 656 f_path='', conditions=dict(function=check_repo))
654 657
655 658 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
656 659 controller='files', action='archivefile',
657 660 conditions=dict(function=check_repo))
658 661
659 662 rmap.connect('files_nodelist_home',
660 663 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
661 664 controller='files', action='nodelist',
662 665 conditions=dict(function=check_repo))
663 666
664 667 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
665 668 controller='forks', action='fork_create',
666 669 conditions=dict(function=check_repo, method=["POST"]))
667 670
668 671 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
669 672 controller='forks', action='fork',
670 673 conditions=dict(function=check_repo))
671 674
672 675 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
673 676 controller='forks', action='forks',
674 677 conditions=dict(function=check_repo))
675 678
676 679 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
677 680 controller='followers', action='followers',
678 681 conditions=dict(function=check_repo))
679 682
680 683 return rmap
@@ -1,1066 +1,1109 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 API controller for RhodeCode
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import time
29 29 import traceback
30 30 import logging
31 31
32 32 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
33 33 from rhodecode.lib.auth import PasswordGenerator, AuthUser, \
34 34 HasPermissionAllDecorator, HasPermissionAnyDecorator, \
35 35 HasPermissionAnyApi, HasRepoPermissionAnyApi
36 36 from rhodecode.lib.utils import map_groups, repo2db_mapper
37 37 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_int
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.scm import ScmModel
41 41 from rhodecode.model.repo import RepoModel
42 42 from rhodecode.model.user import UserModel
43 43 from rhodecode.model.users_group import UserGroupModel
44 44 from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap,\
45 Permission, User
45 Permission, User, Gist
46 46 from rhodecode.lib.compat import json
47 47 from rhodecode.lib.exceptions import DefaultUserException
48 from rhodecode.model.gist import GistModel
48 49
49 50 log = logging.getLogger(__name__)
50 51
51 52
52 53 class OptionalAttr(object):
53 54 """
54 55 Special Optional Option that defines other attribute
55 56 """
56 57 def __init__(self, attr_name):
57 58 self.attr_name = attr_name
58 59
59 60 def __repr__(self):
60 61 return '<OptionalAttr:%s>' % self.attr_name
61 62
62 63 def __call__(self):
63 64 return self
64 65 #alias
65 66 OAttr = OptionalAttr
66 67
67 68
68 69 class Optional(object):
69 70 """
70 71 Defines an optional parameter::
71 72
72 73 param = param.getval() if isinstance(param, Optional) else param
73 74 param = param() if isinstance(param, Optional) else param
74 75
75 76 is equivalent of::
76 77
77 78 param = Optional.extract(param)
78 79
79 80 """
80 81 def __init__(self, type_):
81 82 self.type_ = type_
82 83
83 84 def __repr__(self):
84 85 return '<Optional:%s>' % self.type_.__repr__()
85 86
86 87 def __call__(self):
87 88 return self.getval()
88 89
89 90 def getval(self):
90 91 """
91 92 returns value from this Optional instance
92 93 """
93 94 return self.type_
94 95
95 96 @classmethod
96 97 def extract(cls, val):
97 98 if isinstance(val, cls):
98 99 return val.getval()
99 100 return val
100 101
101 102
102 103 def get_user_or_error(userid):
103 104 """
104 105 Get user by id or name or return JsonRPCError if not found
105 106
106 107 :param userid:
107 108 """
108 109 user = UserModel().get_user(userid)
109 110 if user is None:
110 111 raise JSONRPCError("user `%s` does not exist" % userid)
111 112 return user
112 113
113 114
114 115 def get_repo_or_error(repoid):
115 116 """
116 117 Get repo by id or name or return JsonRPCError if not found
117 118
118 119 :param userid:
119 120 """
120 121 repo = RepoModel().get_repo(repoid)
121 122 if repo is None:
122 123 raise JSONRPCError('repository `%s` does not exist' % (repoid))
123 124 return repo
124 125
125 126
126 127 def get_users_group_or_error(usersgroupid):
127 128 """
128 129 Get user group by id or name or return JsonRPCError if not found
129 130
130 131 :param userid:
131 132 """
132 133 users_group = UserGroupModel().get_group(usersgroupid)
133 134 if users_group is None:
134 135 raise JSONRPCError('user group `%s` does not exist' % usersgroupid)
135 136 return users_group
136 137
137 138
138 139 def get_perm_or_error(permid):
139 140 """
140 141 Get permission by id or name or return JsonRPCError if not found
141 142
142 143 :param userid:
143 144 """
144 145 perm = Permission.get_by_key(permid)
145 146 if perm is None:
146 147 raise JSONRPCError('permission `%s` does not exist' % (permid))
147 148 return perm
148 149
149 150
150 151 class ApiController(JSONRPCController):
151 152 """
152 153 API Controller
153 154
154 155
155 156 Each method needs to have USER as argument this is then based on given
156 157 API_KEY propagated as instance of user object
157 158
158 159 Preferably this should be first argument also
159 160
160 161
161 162 Each function should also **raise** JSONRPCError for any
162 163 errors that happens
163 164
164 165 """
165 166
166 167 @HasPermissionAllDecorator('hg.admin')
167 168 def pull(self, apiuser, repoid):
168 169 """
169 170 Dispatch pull action on given repo
170 171
171 172 :param apiuser:
172 173 :param repoid:
173 174 """
174 175
175 176 repo = get_repo_or_error(repoid)
176 177
177 178 try:
178 179 ScmModel().pull_changes(repo.repo_name,
179 180 self.rhodecode_user.username)
180 181 return 'Pulled from `%s`' % repo.repo_name
181 182 except Exception:
182 183 log.error(traceback.format_exc())
183 184 raise JSONRPCError(
184 185 'Unable to pull changes from `%s`' % repo.repo_name
185 186 )
186 187
187 188 @HasPermissionAllDecorator('hg.admin')
188 189 def rescan_repos(self, apiuser, remove_obsolete=Optional(False)):
189 190 """
190 191 Dispatch rescan repositories action. If remove_obsolete is set
191 192 than also delete repos that are in database but not in the filesystem.
192 193 aka "clean zombies"
193 194
194 195 :param apiuser:
195 196 :param remove_obsolete:
196 197 """
197 198
198 199 try:
199 200 rm_obsolete = Optional.extract(remove_obsolete)
200 201 added, removed = repo2db_mapper(ScmModel().repo_scan(),
201 202 remove_obsolete=rm_obsolete)
202 203 return {'added': added, 'removed': removed}
203 204 except Exception:
204 205 log.error(traceback.format_exc())
205 206 raise JSONRPCError(
206 207 'Error occurred during rescan repositories action'
207 208 )
208 209
209 210 def invalidate_cache(self, apiuser, repoid):
210 211 """
211 212 Dispatch cache invalidation action on given repo
212 213
213 214 :param apiuser:
214 215 :param repoid:
215 216 """
216 217 repo = get_repo_or_error(repoid)
217 218 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
218 219 # check if we have admin permission for this repo !
219 220 if HasRepoPermissionAnyApi('repository.admin',
220 221 'repository.write')(user=apiuser,
221 222 repo_name=repo.repo_name) is False:
222 223 raise JSONRPCError('repository `%s` does not exist' % (repoid))
223 224
224 225 try:
225 226 ScmModel().mark_for_invalidation(repo.repo_name)
226 227 return ('Caches of repository `%s` was invalidated' % repoid)
227 228 except Exception:
228 229 log.error(traceback.format_exc())
229 230 raise JSONRPCError(
230 231 'Error occurred during cache invalidation action'
231 232 )
232 233
233 234 def lock(self, apiuser, repoid, locked=Optional(None),
234 235 userid=Optional(OAttr('apiuser'))):
235 236 """
236 237 Set locking state on particular repository by given user, if
237 238 this command is runned by non-admin account userid is set to user
238 239 who is calling this method
239 240
240 241 :param apiuser:
241 242 :param repoid:
242 243 :param userid:
243 244 :param locked:
244 245 """
245 246 repo = get_repo_or_error(repoid)
246 247 if HasPermissionAnyApi('hg.admin')(user=apiuser):
247 248 pass
248 249 elif HasRepoPermissionAnyApi('repository.admin',
249 250 'repository.write')(user=apiuser,
250 251 repo_name=repo.repo_name):
251 252 #make sure normal user does not pass someone else userid,
252 253 #he is not allowed to do that
253 254 if not isinstance(userid, Optional) and userid != apiuser.user_id:
254 255 raise JSONRPCError(
255 256 'userid is not the same as your user'
256 257 )
257 258 else:
258 259 raise JSONRPCError('repository `%s` does not exist' % (repoid))
259 260
260 261 if isinstance(userid, Optional):
261 262 userid = apiuser.user_id
262 263
263 264 user = get_user_or_error(userid)
264 265
265 266 if isinstance(locked, Optional):
266 267 lockobj = Repository.getlock(repo)
267 268
268 269 if lockobj[0] is None:
269 270 _d = {
270 271 'repo': repo.repo_name,
271 272 'locked': False,
272 273 'locked_since': None,
273 274 'locked_by': None,
274 275 'msg': 'Repo `%s` not locked.' % repo.repo_name
275 276 }
276 277 return _d
277 278 else:
278 279 userid, time_ = lockobj
279 280 lock_user = get_user_or_error(userid)
280 281 _d = {
281 282 'repo': repo.repo_name,
282 283 'locked': True,
283 284 'locked_since': time_,
284 285 'locked_by': lock_user.username,
285 286 'msg': ('Repo `%s` locked by `%s`. '
286 287 % (repo.repo_name,
287 288 json.dumps(time_to_datetime(time_))))
288 289 }
289 290 return _d
290 291
291 292 # force locked state through a flag
292 293 else:
293 294 locked = str2bool(locked)
294 295 try:
295 296 if locked:
296 297 lock_time = time.time()
297 298 Repository.lock(repo, user.user_id, lock_time)
298 299 else:
299 300 lock_time = None
300 301 Repository.unlock(repo)
301 302 _d = {
302 303 'repo': repo.repo_name,
303 304 'locked': locked,
304 305 'locked_since': lock_time,
305 306 'locked_by': user.username,
306 307 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
307 308 % (user.username, repo.repo_name, locked))
308 309 }
309 310 return _d
310 311 except Exception:
311 312 log.error(traceback.format_exc())
312 313 raise JSONRPCError(
313 314 'Error occurred locking repository `%s`' % repo.repo_name
314 315 )
315 316
316 317 def get_locks(self, apiuser, userid=Optional(OAttr('apiuser'))):
317 318 """
318 319 Get all locks for given userid, if
319 320 this command is runned by non-admin account userid is set to user
320 321 who is calling this method, thus returning locks for himself
321 322
322 323 :param apiuser:
323 324 :param userid:
324 325 """
325 326 if HasPermissionAnyApi('hg.admin')(user=apiuser):
326 327 pass
327 328 else:
328 329 #make sure normal user does not pass someone else userid,
329 330 #he is not allowed to do that
330 331 if not isinstance(userid, Optional) and userid != apiuser.user_id:
331 332 raise JSONRPCError(
332 333 'userid is not the same as your user'
333 334 )
334 335 ret = []
335 336 if isinstance(userid, Optional):
336 337 user = None
337 338 else:
338 339 user = get_user_or_error(userid)
339 340
340 341 #show all locks
341 342 for r in Repository.getAll():
342 343 userid, time_ = r.locked
343 344 if time_:
344 345 _api_data = r.get_api_data()
345 346 # if we use userfilter just show the locks for this user
346 347 if user:
347 348 if safe_int(userid) == user.user_id:
348 349 ret.append(_api_data)
349 350 else:
350 351 ret.append(_api_data)
351 352
352 353 return ret
353 354
354 355 @HasPermissionAllDecorator('hg.admin')
355 356 def show_ip(self, apiuser, userid):
356 357 """
357 358 Shows IP address as seen from RhodeCode server, together with all
358 359 defined IP addresses for given user
359 360
360 361 :param apiuser:
361 362 :param userid:
362 363 """
363 364 user = get_user_or_error(userid)
364 365 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
365 366 return dict(
366 367 ip_addr_server=self.ip_addr,
367 368 user_ips=ips
368 369 )
369 370
370 371 def get_user(self, apiuser, userid=Optional(OAttr('apiuser'))):
371 372 """"
372 373 Get a user by username, or userid, if userid is given
373 374
374 375 :param apiuser:
375 376 :param userid:
376 377 """
377 378 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
378 379 #make sure normal user does not pass someone else userid,
379 380 #he is not allowed to do that
380 381 if not isinstance(userid, Optional) and userid != apiuser.user_id:
381 382 raise JSONRPCError(
382 383 'userid is not the same as your user'
383 384 )
384 385
385 386 if isinstance(userid, Optional):
386 387 userid = apiuser.user_id
387 388
388 389 user = get_user_or_error(userid)
389 390 data = user.get_api_data()
390 391 data['permissions'] = AuthUser(user_id=user.user_id).permissions
391 392 return data
392 393
393 394 @HasPermissionAllDecorator('hg.admin')
394 395 def get_users(self, apiuser):
395 396 """"
396 397 Get all users
397 398
398 399 :param apiuser:
399 400 """
400 401
401 402 result = []
402 403 users_list = User.query().order_by(User.username)\
403 404 .filter(User.username != User.DEFAULT_USER)\
404 405 .all()
405 406 for user in users_list:
406 407 result.append(user.get_api_data())
407 408 return result
408 409
409 410 @HasPermissionAllDecorator('hg.admin')
410 411 def create_user(self, apiuser, username, email, password=Optional(None),
411 412 firstname=Optional(None), lastname=Optional(None),
412 413 active=Optional(True), admin=Optional(False),
413 414 ldap_dn=Optional(None)):
414 415 """
415 416 Create new user
416 417
417 418 :param apiuser:
418 419 :param username:
419 420 :param email:
420 421 :param password:
421 422 :param firstname:
422 423 :param lastname:
423 424 :param active:
424 425 :param admin:
425 426 :param ldap_dn:
426 427 """
427 428
428 429 if UserModel().get_by_username(username):
429 430 raise JSONRPCError("user `%s` already exist" % username)
430 431
431 432 if UserModel().get_by_email(email, case_insensitive=True):
432 433 raise JSONRPCError("email `%s` already exist" % email)
433 434
434 435 if Optional.extract(ldap_dn):
435 436 # generate temporary password if ldap_dn
436 437 password = PasswordGenerator().gen_password(length=8)
437 438
438 439 try:
439 440 user = UserModel().create_or_update(
440 441 username=Optional.extract(username),
441 442 password=Optional.extract(password),
442 443 email=Optional.extract(email),
443 444 firstname=Optional.extract(firstname),
444 445 lastname=Optional.extract(lastname),
445 446 active=Optional.extract(active),
446 447 admin=Optional.extract(admin),
447 448 ldap_dn=Optional.extract(ldap_dn)
448 449 )
449 450 Session().commit()
450 451 return dict(
451 452 msg='created new user `%s`' % username,
452 453 user=user.get_api_data()
453 454 )
454 455 except Exception:
455 456 log.error(traceback.format_exc())
456 457 raise JSONRPCError('failed to create user `%s`' % username)
457 458
458 459 @HasPermissionAllDecorator('hg.admin')
459 460 def update_user(self, apiuser, userid, username=Optional(None),
460 461 email=Optional(None), firstname=Optional(None),
461 462 lastname=Optional(None), active=Optional(None),
462 463 admin=Optional(None), ldap_dn=Optional(None),
463 464 password=Optional(None)):
464 465 """
465 466 Updates given user
466 467
467 468 :param apiuser:
468 469 :param userid:
469 470 :param username:
470 471 :param email:
471 472 :param firstname:
472 473 :param lastname:
473 474 :param active:
474 475 :param admin:
475 476 :param ldap_dn:
476 477 :param password:
477 478 """
478 479
479 480 user = get_user_or_error(userid)
480 481
481 482 # call function and store only updated arguments
482 483 updates = {}
483 484
484 485 def store_update(attr, name):
485 486 if not isinstance(attr, Optional):
486 487 updates[name] = attr
487 488
488 489 try:
489 490
490 491 store_update(username, 'username')
491 492 store_update(password, 'password')
492 493 store_update(email, 'email')
493 494 store_update(firstname, 'name')
494 495 store_update(lastname, 'lastname')
495 496 store_update(active, 'active')
496 497 store_update(admin, 'admin')
497 498 store_update(ldap_dn, 'ldap_dn')
498 499
499 500 user = UserModel().update_user(user, **updates)
500 501 Session().commit()
501 502 return dict(
502 503 msg='updated user ID:%s %s' % (user.user_id, user.username),
503 504 user=user.get_api_data()
504 505 )
505 506 except DefaultUserException:
506 507 log.error(traceback.format_exc())
507 508 raise JSONRPCError('editing default user is forbidden')
508 509 except Exception:
509 510 log.error(traceback.format_exc())
510 511 raise JSONRPCError('failed to update user `%s`' % userid)
511 512
512 513 @HasPermissionAllDecorator('hg.admin')
513 514 def delete_user(self, apiuser, userid):
514 515 """"
515 516 Deletes an user
516 517
517 518 :param apiuser:
518 519 :param userid:
519 520 """
520 521 user = get_user_or_error(userid)
521 522
522 523 try:
523 524 UserModel().delete(userid)
524 525 Session().commit()
525 526 return dict(
526 527 msg='deleted user ID:%s %s' % (user.user_id, user.username),
527 528 user=None
528 529 )
529 530 except Exception:
530 531 log.error(traceback.format_exc())
531 532 raise JSONRPCError('failed to delete ID:%s %s' % (user.user_id,
532 533 user.username))
533 534
534 535 @HasPermissionAllDecorator('hg.admin')
535 536 def get_users_group(self, apiuser, usersgroupid):
536 537 """"
537 538 Get user group by name or id
538 539
539 540 :param apiuser:
540 541 :param usersgroupid:
541 542 """
542 543 users_group = get_users_group_or_error(usersgroupid)
543 544
544 545 data = users_group.get_api_data()
545 546
546 547 members = []
547 548 for user in users_group.members:
548 549 user = user.user
549 550 members.append(user.get_api_data())
550 551 data['members'] = members
551 552 return data
552 553
553 554 @HasPermissionAllDecorator('hg.admin')
554 555 def get_users_groups(self, apiuser):
555 556 """"
556 557 Get all user groups
557 558
558 559 :param apiuser:
559 560 """
560 561
561 562 result = []
562 563 for users_group in UserGroupModel().get_all():
563 564 result.append(users_group.get_api_data())
564 565 return result
565 566
566 567 @HasPermissionAllDecorator('hg.admin')
567 568 def create_users_group(self, apiuser, group_name,
568 569 owner=Optional(OAttr('apiuser')),
569 570 active=Optional(True)):
570 571 """
571 572 Creates an new usergroup
572 573
573 574 :param apiuser:
574 575 :param group_name:
575 576 :param owner:
576 577 :param active:
577 578 """
578 579
579 580 if UserGroupModel().get_by_name(group_name):
580 581 raise JSONRPCError("user group `%s` already exist" % group_name)
581 582
582 583 try:
583 584 if isinstance(owner, Optional):
584 585 owner = apiuser.user_id
585 586
586 587 owner = get_user_or_error(owner)
587 588 active = Optional.extract(active)
588 589 ug = UserGroupModel().create(name=group_name,
589 590 owner=owner,
590 591 active=active)
591 592 Session().commit()
592 593 return dict(
593 594 msg='created new user group `%s`' % group_name,
594 595 users_group=ug.get_api_data()
595 596 )
596 597 except Exception:
597 598 log.error(traceback.format_exc())
598 599 raise JSONRPCError('failed to create group `%s`' % group_name)
599 600
600 601 @HasPermissionAllDecorator('hg.admin')
601 602 def add_user_to_users_group(self, apiuser, usersgroupid, userid):
602 603 """"
603 604 Add a user to a user group
604 605
605 606 :param apiuser:
606 607 :param usersgroupid:
607 608 :param userid:
608 609 """
609 610 user = get_user_or_error(userid)
610 611 users_group = get_users_group_or_error(usersgroupid)
611 612
612 613 try:
613 614 ugm = UserGroupModel().add_user_to_group(users_group, user)
614 615 success = True if ugm != True else False
615 616 msg = 'added member `%s` to user group `%s`' % (
616 617 user.username, users_group.users_group_name
617 618 )
618 619 msg = msg if success else 'User is already in that group'
619 620 Session().commit()
620 621
621 622 return dict(
622 623 success=success,
623 624 msg=msg
624 625 )
625 626 except Exception:
626 627 log.error(traceback.format_exc())
627 628 raise JSONRPCError(
628 629 'failed to add member to user group `%s`' % (
629 630 users_group.users_group_name
630 631 )
631 632 )
632 633
633 634 @HasPermissionAllDecorator('hg.admin')
634 635 def remove_user_from_users_group(self, apiuser, usersgroupid, userid):
635 636 """
636 637 Remove user from a group
637 638
638 639 :param apiuser:
639 640 :param usersgroupid:
640 641 :param userid:
641 642 """
642 643 user = get_user_or_error(userid)
643 644 users_group = get_users_group_or_error(usersgroupid)
644 645
645 646 try:
646 647 success = UserGroupModel().remove_user_from_group(users_group,
647 648 user)
648 649 msg = 'removed member `%s` from user group `%s`' % (
649 650 user.username, users_group.users_group_name
650 651 )
651 652 msg = msg if success else "User wasn't in group"
652 653 Session().commit()
653 654 return dict(success=success, msg=msg)
654 655 except Exception:
655 656 log.error(traceback.format_exc())
656 657 raise JSONRPCError(
657 658 'failed to remove member from user group `%s`' % (
658 659 users_group.users_group_name
659 660 )
660 661 )
661 662
662 663 def get_repo(self, apiuser, repoid):
663 664 """"
664 665 Get repository by name
665 666
666 667 :param apiuser:
667 668 :param repoid:
668 669 """
669 670 repo = get_repo_or_error(repoid)
670 671
671 672 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
672 673 # check if we have admin permission for this repo !
673 674 if HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
674 675 repo_name=repo.repo_name) is False:
675 676 raise JSONRPCError('repository `%s` does not exist' % (repoid))
676 677
677 678 members = []
678 679 followers = []
679 680 for user in repo.repo_to_perm:
680 681 perm = user.permission.permission_name
681 682 user = user.user
682 683 user_data = user.get_api_data()
683 684 user_data['type'] = "user"
684 685 user_data['permission'] = perm
685 686 members.append(user_data)
686 687
687 688 for users_group in repo.users_group_to_perm:
688 689 perm = users_group.permission.permission_name
689 690 users_group = users_group.users_group
690 691 users_group_data = users_group.get_api_data()
691 692 users_group_data['type'] = "users_group"
692 693 users_group_data['permission'] = perm
693 694 members.append(users_group_data)
694 695
695 696 for user in repo.followers:
696 697 followers.append(user.user.get_api_data())
697 698
698 699 data = repo.get_api_data()
699 700 data['members'] = members
700 701 data['followers'] = followers
701 702 return data
702 703
703 704 def get_repos(self, apiuser):
704 705 """"
705 706 Get all repositories
706 707
707 708 :param apiuser:
708 709 """
709 710 result = []
710 711 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
711 712 repos = RepoModel().get_all_user_repos(user=apiuser)
712 713 else:
713 714 repos = RepoModel().get_all()
714 715
715 716 for repo in repos:
716 717 result.append(repo.get_api_data())
717 718 return result
718 719
719 720 @HasPermissionAllDecorator('hg.admin')
720 721 def get_repo_nodes(self, apiuser, repoid, revision, root_path,
721 722 ret_type='all'):
722 723 """
723 724 returns a list of nodes and it's children
724 725 for a given path at given revision. It's possible to specify ret_type
725 726 to show only files or dirs
726 727
727 728 :param apiuser:
728 729 :param repoid: name or id of repository
729 730 :param revision: revision for which listing should be done
730 731 :param root_path: path from which start displaying
731 732 :param ret_type: return type 'all|files|dirs' nodes
732 733 """
733 734 repo = get_repo_or_error(repoid)
734 735 try:
735 736 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
736 737 flat=False)
737 738 _map = {
738 739 'all': _d + _f,
739 740 'files': _f,
740 741 'dirs': _d,
741 742 }
742 743 return _map[ret_type]
743 744 except KeyError:
744 745 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
745 746 except Exception:
746 747 log.error(traceback.format_exc())
747 748 raise JSONRPCError(
748 749 'failed to get repo: `%s` nodes' % repo.repo_name
749 750 )
750 751
751 752 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
752 753 def create_repo(self, apiuser, repo_name, owner=Optional(OAttr('apiuser')),
753 754 repo_type=Optional('hg'),
754 755 description=Optional(''), private=Optional(False),
755 756 clone_uri=Optional(None), landing_rev=Optional('tip'),
756 757 enable_statistics=Optional(False),
757 758 enable_locking=Optional(False),
758 759 enable_downloads=Optional(False)):
759 760 """
760 761 Create repository, if clone_url is given it makes a remote clone
761 762 if repo_name is within a group name the groups will be created
762 763 automatically if they aren't present
763 764
764 765 :param apiuser:
765 766 :param repo_name:
766 767 :param onwer:
767 768 :param repo_type:
768 769 :param description:
769 770 :param private:
770 771 :param clone_uri:
771 772 :param landing_rev:
772 773 """
773 774 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
774 775 if not isinstance(owner, Optional):
775 776 #forbid setting owner for non-admins
776 777 raise JSONRPCError(
777 778 'Only RhodeCode admin can specify `owner` param'
778 779 )
779 780 if isinstance(owner, Optional):
780 781 owner = apiuser.user_id
781 782
782 783 owner = get_user_or_error(owner)
783 784
784 785 if RepoModel().get_by_repo_name(repo_name):
785 786 raise JSONRPCError("repo `%s` already exist" % repo_name)
786 787
787 788 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
788 789 if isinstance(private, Optional):
789 790 private = defs.get('repo_private') or Optional.extract(private)
790 791 if isinstance(repo_type, Optional):
791 792 repo_type = defs.get('repo_type')
792 793 if isinstance(enable_statistics, Optional):
793 794 enable_statistics = defs.get('repo_enable_statistics')
794 795 if isinstance(enable_locking, Optional):
795 796 enable_locking = defs.get('repo_enable_locking')
796 797 if isinstance(enable_downloads, Optional):
797 798 enable_downloads = defs.get('repo_enable_downloads')
798 799
799 800 clone_uri = Optional.extract(clone_uri)
800 801 description = Optional.extract(description)
801 802 landing_rev = Optional.extract(landing_rev)
802 803
803 804 try:
804 805 # create structure of groups and return the last group
805 806 group = map_groups(repo_name)
806 807
807 808 repo = RepoModel().create_repo(
808 809 repo_name=repo_name,
809 810 repo_type=repo_type,
810 811 description=description,
811 812 owner=owner,
812 813 private=private,
813 814 clone_uri=clone_uri,
814 815 repos_group=group,
815 816 landing_rev=landing_rev,
816 817 enable_statistics=enable_statistics,
817 818 enable_downloads=enable_downloads,
818 819 enable_locking=enable_locking
819 820 )
820 821
821 822 Session().commit()
822 823 return dict(
823 824 msg="Created new repository `%s`" % (repo.repo_name),
824 825 repo=repo.get_api_data()
825 826 )
826 827 except Exception:
827 828 log.error(traceback.format_exc())
828 829 raise JSONRPCError('failed to create repository `%s`' % repo_name)
829 830
830 831 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
831 832 def fork_repo(self, apiuser, repoid, fork_name, owner=Optional(OAttr('apiuser')),
832 833 description=Optional(''), copy_permissions=Optional(False),
833 834 private=Optional(False), landing_rev=Optional('tip')):
834 835 repo = get_repo_or_error(repoid)
835 836 repo_name = repo.repo_name
836 837
837 838 _repo = RepoModel().get_by_repo_name(fork_name)
838 839 if _repo:
839 840 type_ = 'fork' if _repo.fork else 'repo'
840 841 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
841 842
842 843 if HasPermissionAnyApi('hg.admin')(user=apiuser):
843 844 pass
844 845 elif HasRepoPermissionAnyApi('repository.admin',
845 846 'repository.write',
846 847 'repository.read')(user=apiuser,
847 848 repo_name=repo.repo_name):
848 849 if not isinstance(owner, Optional):
849 850 #forbid setting owner for non-admins
850 851 raise JSONRPCError(
851 852 'Only RhodeCode admin can specify `owner` param'
852 853 )
853 854 else:
854 855 raise JSONRPCError('repository `%s` does not exist' % (repoid))
855 856
856 857 if isinstance(owner, Optional):
857 858 owner = apiuser.user_id
858 859
859 860 owner = get_user_or_error(owner)
860 861
861 862 try:
862 863 # create structure of groups and return the last group
863 864 group = map_groups(fork_name)
864 865
865 866 form_data = dict(
866 867 repo_name=fork_name,
867 868 repo_name_full=fork_name,
868 869 repo_group=group,
869 870 repo_type=repo.repo_type,
870 871 description=Optional.extract(description),
871 872 private=Optional.extract(private),
872 873 copy_permissions=Optional.extract(copy_permissions),
873 874 landing_rev=Optional.extract(landing_rev),
874 875 update_after_clone=False,
875 876 fork_parent_id=repo.repo_id,
876 877 )
877 878 RepoModel().create_fork(form_data, cur_user=owner)
878 879 return dict(
879 880 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
880 881 fork_name),
881 882 success=True # cannot return the repo data here since fork
882 883 # cann be done async
883 884 )
884 885 except Exception:
885 886 log.error(traceback.format_exc())
886 887 raise JSONRPCError(
887 888 'failed to fork repository `%s` as `%s`' % (repo_name,
888 889 fork_name)
889 890 )
890 891
892 # perms handled inside
891 893 def delete_repo(self, apiuser, repoid, forks=Optional(None)):
892 894 """
893 895 Deletes a given repository
894 896
895 897 :param apiuser:
896 898 :param repoid:
897 899 :param forks: detach or delete, what do do with attached forks for repo
898 900 """
899 901 repo = get_repo_or_error(repoid)
900 902
901 903 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
902 904 # check if we have admin permission for this repo !
903 905 if HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
904 906 repo_name=repo.repo_name) is False:
905 907 raise JSONRPCError('repository `%s` does not exist' % (repoid))
906 908
907 909 try:
908 910 handle_forks = Optional.extract(forks)
909 911 _forks_msg = ''
910 912 _forks = [f for f in repo.forks]
911 913 if handle_forks == 'detach':
912 914 _forks_msg = ' ' + _('Detached %s forks') % len(_forks)
913 915 elif handle_forks == 'delete':
914 916 _forks_msg = ' ' + _('Deleted %s forks') % len(_forks)
915 917 elif _forks:
916 918 raise JSONRPCError(
917 919 'Cannot delete `%s` it still contains attached forks'
918 920 % repo.repo_name
919 921 )
920 922
921 923 RepoModel().delete(repo, forks=forks)
922 924 Session().commit()
923 925 return dict(
924 926 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
925 927 success=True
926 928 )
927 929 except Exception:
928 930 log.error(traceback.format_exc())
929 931 raise JSONRPCError(
930 932 'failed to delete repository `%s`' % repo.repo_name
931 933 )
932 934
933 935 @HasPermissionAllDecorator('hg.admin')
934 936 def grant_user_permission(self, apiuser, repoid, userid, perm):
935 937 """
936 938 Grant permission for user on given repository, or update existing one
937 939 if found
938 940
939 941 :param repoid:
940 942 :param userid:
941 943 :param perm:
942 944 """
943 945 repo = get_repo_or_error(repoid)
944 946 user = get_user_or_error(userid)
945 947 perm = get_perm_or_error(perm)
946 948
947 949 try:
948 950
949 951 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
950 952
951 953 Session().commit()
952 954 return dict(
953 955 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
954 956 perm.permission_name, user.username, repo.repo_name
955 957 ),
956 958 success=True
957 959 )
958 960 except Exception:
959 961 log.error(traceback.format_exc())
960 962 raise JSONRPCError(
961 963 'failed to edit permission for user: `%s` in repo: `%s`' % (
962 964 userid, repoid
963 965 )
964 966 )
965 967
966 968 @HasPermissionAllDecorator('hg.admin')
967 969 def revoke_user_permission(self, apiuser, repoid, userid):
968 970 """
969 971 Revoke permission for user on given repository
970 972
971 973 :param apiuser:
972 974 :param repoid:
973 975 :param userid:
974 976 """
975 977
976 978 repo = get_repo_or_error(repoid)
977 979 user = get_user_or_error(userid)
978 980 try:
979 981
980 982 RepoModel().revoke_user_permission(repo=repo, user=user)
981 983
982 984 Session().commit()
983 985 return dict(
984 986 msg='Revoked perm for user: `%s` in repo: `%s`' % (
985 987 user.username, repo.repo_name
986 988 ),
987 989 success=True
988 990 )
989 991 except Exception:
990 992 log.error(traceback.format_exc())
991 993 raise JSONRPCError(
992 994 'failed to edit permission for user: `%s` in repo: `%s`' % (
993 995 userid, repoid
994 996 )
995 997 )
996 998
997 999 @HasPermissionAllDecorator('hg.admin')
998 1000 def grant_users_group_permission(self, apiuser, repoid, usersgroupid,
999 1001 perm):
1000 1002 """
1001 1003 Grant permission for user group on given repository, or update
1002 1004 existing one if found
1003 1005
1004 1006 :param apiuser:
1005 1007 :param repoid:
1006 1008 :param usersgroupid:
1007 1009 :param perm:
1008 1010 """
1009 1011 repo = get_repo_or_error(repoid)
1010 1012 perm = get_perm_or_error(perm)
1011 1013 users_group = get_users_group_or_error(usersgroupid)
1012 1014
1013 1015 try:
1014 1016 RepoModel().grant_users_group_permission(repo=repo,
1015 1017 group_name=users_group,
1016 1018 perm=perm)
1017 1019
1018 1020 Session().commit()
1019 1021 return dict(
1020 1022 msg='Granted perm: `%s` for user group: `%s` in '
1021 1023 'repo: `%s`' % (
1022 1024 perm.permission_name, users_group.users_group_name,
1023 1025 repo.repo_name
1024 1026 ),
1025 1027 success=True
1026 1028 )
1027 1029 except Exception:
1028 1030 log.error(traceback.format_exc())
1029 1031 raise JSONRPCError(
1030 1032 'failed to edit permission for user group: `%s` in '
1031 1033 'repo: `%s`' % (
1032 1034 usersgroupid, repo.repo_name
1033 1035 )
1034 1036 )
1035 1037
1036 1038 @HasPermissionAllDecorator('hg.admin')
1037 1039 def revoke_users_group_permission(self, apiuser, repoid, usersgroupid):
1038 1040 """
1039 1041 Revoke permission for user group on given repository
1040 1042
1041 1043 :param apiuser:
1042 1044 :param repoid:
1043 1045 :param usersgroupid:
1044 1046 """
1045 1047 repo = get_repo_or_error(repoid)
1046 1048 users_group = get_users_group_or_error(usersgroupid)
1047 1049
1048 1050 try:
1049 1051 RepoModel().revoke_users_group_permission(repo=repo,
1050 1052 group_name=users_group)
1051 1053
1052 1054 Session().commit()
1053 1055 return dict(
1054 1056 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1055 1057 users_group.users_group_name, repo.repo_name
1056 1058 ),
1057 1059 success=True
1058 1060 )
1059 1061 except Exception:
1060 1062 log.error(traceback.format_exc())
1061 1063 raise JSONRPCError(
1062 1064 'failed to edit permission for user group: `%s` in '
1063 1065 'repo: `%s`' % (
1064 1066 users_group.users_group_name, repo.repo_name
1065 1067 )
1066 1068 )
1069
1070 def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
1071 gist_type=Optional(Gist.GIST_PUBLIC),
1072 gist_lifetime=Optional(-1),
1073 gist_description=Optional('')):
1074
1075 try:
1076 if isinstance(owner, Optional):
1077 owner = apiuser.user_id
1078
1079 owner = get_user_or_error(owner)
1080 description = Optional.extract(gist_description)
1081 gist_type = Optional.extract(gist_type)
1082 gist_lifetime = Optional.extract(gist_lifetime)
1083
1084 # files: {
1085 # 'filename': {'content':'...', 'lexer': null},
1086 # 'filename2': {'content':'...', 'lexer': null}
1087 #}
1088 gist = GistModel().create(description=description,
1089 owner=owner,
1090 gist_mapping=files,
1091 gist_type=gist_type,
1092 lifetime=gist_lifetime)
1093 Session().commit()
1094 return dict(
1095 msg='created new gist',
1096 gist_url=gist.gist_url(),
1097 gist_id=gist.gist_access_id,
1098 gist_type=gist.gist_type,
1099 files=files.keys()
1100 )
1101 except Exception:
1102 log.error(traceback.format_exc())
1103 raise JSONRPCError('failed to create gist')
1104
1105 def update_gist(self, apiuser):
1106 pass
1107
1108 def delete_gist(self, apiuser):
1109 pass
@@ -1,650 +1,658 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30 import shutil
31 31
32 32 from pylons import request, response, tmpl_context as c, url
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import redirect
35 35 from rhodecode.lib.utils import jsonify
36 36
37 37 from rhodecode.lib import diffs
38 38 from rhodecode.lib import helpers as h
39 39
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
42 42 str2bool
43 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 44 from rhodecode.lib.base import BaseRepoController, render
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.vcs.conf import settings
47 47 from rhodecode.lib.vcs.exceptions import RepositoryError, \
48 48 ChangesetDoesNotExistError, EmptyRepositoryError, \
49 49 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
50 50 NodeDoesNotExistError, ChangesetError, NodeError
51 51 from rhodecode.lib.vcs.nodes import FileNode
52 52
53 53 from rhodecode.model.repo import RepoModel
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
58 58 _context_url, get_line_ctx, get_ignore_ws
59 59 from webob.exc import HTTPNotFound
60 from rhodecode.lib.exceptions import NonRelativePathError
60 61
61 62
62 63 log = logging.getLogger(__name__)
63 64
64 65
65 66 class FilesController(BaseRepoController):
66 67
67 68 def __before__(self):
68 69 super(FilesController, self).__before__()
69 70 c.cut_off_limit = self.cut_off_limit
70 71
71 72 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
72 73 """
73 74 Safe way to get changeset if error occur it redirects to tip with
74 75 proper message
75 76
76 77 :param rev: revision to fetch
77 78 :param repo_name: repo name to redirect after
78 79 """
79 80
80 81 try:
81 82 return c.rhodecode_repo.get_changeset(rev)
82 83 except EmptyRepositoryError, e:
83 84 if not redirect_after:
84 85 return None
85 86 url_ = url('files_add_home',
86 87 repo_name=c.repo_name,
87 88 revision=0, f_path='')
88 89 add_new = h.link_to(_('Click here to add new file'), url_)
89 90 h.flash(h.literal(_('There are no files yet %s') % add_new),
90 91 category='warning')
91 92 redirect(h.url('summary_home', repo_name=repo_name))
92 93
93 94 except RepositoryError, e: # including ChangesetDoesNotExistError
94 95 h.flash(str(e), category='error')
95 96 raise HTTPNotFound()
96 97
97 98 def __get_filenode_or_redirect(self, repo_name, cs, path):
98 99 """
99 100 Returns file_node, if error occurs or given path is directory,
100 101 it'll redirect to top level path
101 102
102 103 :param repo_name: repo_name
103 104 :param cs: given changeset
104 105 :param path: path to lookup
105 106 """
106 107
107 108 try:
108 109 file_node = cs.get_node(path)
109 110 if file_node.is_dir():
110 111 raise RepositoryError('given path is a directory')
111 112 except RepositoryError, e:
112 113 h.flash(str(e), category='error')
113 114 raise HTTPNotFound()
114 115
115 116 return file_node
116 117
117 118 @LoginRequired()
118 119 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
119 120 'repository.admin')
120 121 def index(self, repo_name, revision, f_path, annotate=False):
121 122 # redirect to given revision from form if given
122 123 post_revision = request.POST.get('at_rev', None)
123 124 if post_revision:
124 125 cs = self.__get_cs_or_redirect(post_revision, repo_name)
125 126
126 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
127 128 c.branch = request.GET.get('branch', None)
128 129 c.f_path = f_path
129 130 c.annotate = annotate
130 131 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
131 132 cur_rev = c.changeset.revision
132 133
133 134 # prev link
134 135 try:
135 136 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 137 c.url_prev = url('files_home', repo_name=c.repo_name,
137 138 revision=prev_rev.raw_id, f_path=f_path)
138 139 if c.branch:
139 140 c.url_prev += '?branch=%s' % c.branch
140 141 except (ChangesetDoesNotExistError, VCSError):
141 142 c.url_prev = '#'
142 143
143 144 # next link
144 145 try:
145 146 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 147 c.url_next = url('files_home', repo_name=c.repo_name,
147 148 revision=next_rev.raw_id, f_path=f_path)
148 149 if c.branch:
149 150 c.url_next += '?branch=%s' % c.branch
150 151 except (ChangesetDoesNotExistError, VCSError):
151 152 c.url_next = '#'
152 153
153 154 # files or dirs
154 155 try:
155 156 c.file = c.changeset.get_node(f_path)
156 157
157 158 if c.file.is_file():
158 159 c.load_full_history = False
159 160 file_last_cs = c.file.last_changeset
160 161 c.file_changeset = (c.changeset
161 162 if c.changeset.revision < file_last_cs.revision
162 163 else file_last_cs)
163 164 #determine if we're on branch head
164 165 _branches = c.rhodecode_repo.branches
165 166 c.on_branch_head = revision in _branches.keys() + _branches.values()
166 167 _hist = []
167 168 c.file_history = []
168 169 if c.load_full_history:
169 170 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
170 171
171 172 c.authors = []
172 173 for a in set([x.author for x in _hist]):
173 174 c.authors.append((h.email(a), h.person(a)))
174 175 else:
175 176 c.authors = c.file_history = []
176 177 except RepositoryError, e:
177 178 h.flash(str(e), category='error')
178 179 raise HTTPNotFound()
179 180
180 181 if request.environ.get('HTTP_X_PARTIAL_XHR'):
181 182 return render('files/files_ypjax.html')
182 183
183 184 return render('files/files.html')
184 185
185 186 @LoginRequired()
186 187 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
187 188 'repository.admin')
188 189 def history(self, repo_name, revision, f_path, annotate=False):
189 190 if request.environ.get('HTTP_X_PARTIAL_XHR'):
190 191 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
191 192 c.f_path = f_path
192 193 c.annotate = annotate
193 194 c.file = c.changeset.get_node(f_path)
194 195 if c.file.is_file():
195 196 file_last_cs = c.file.last_changeset
196 197 c.file_changeset = (c.changeset
197 198 if c.changeset.revision < file_last_cs.revision
198 199 else file_last_cs)
199 200 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
200 201 c.authors = []
201 202 for a in set([x.author for x in _hist]):
202 203 c.authors.append((h.email(a), h.person(a)))
203 204 return render('files/files_history_box.html')
204 205
205 206 @LoginRequired()
206 207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
207 208 'repository.admin')
208 209 def rawfile(self, repo_name, revision, f_path):
209 210 cs = self.__get_cs_or_redirect(revision, repo_name)
210 211 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
211 212
212 213 response.content_disposition = 'attachment; filename=%s' % \
213 214 safe_str(f_path.split(Repository.url_sep())[-1])
214 215
215 216 response.content_type = file_node.mimetype
216 217 return file_node.content
217 218
218 219 @LoginRequired()
219 220 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
220 221 'repository.admin')
221 222 def raw(self, repo_name, revision, f_path):
222 223 cs = self.__get_cs_or_redirect(revision, repo_name)
223 224 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
224 225
225 226 raw_mimetype_mapping = {
226 227 # map original mimetype to a mimetype used for "show as raw"
227 228 # you can also provide a content-disposition to override the
228 229 # default "attachment" disposition.
229 230 # orig_type: (new_type, new_dispo)
230 231
231 232 # show images inline:
232 233 'image/x-icon': ('image/x-icon', 'inline'),
233 234 'image/png': ('image/png', 'inline'),
234 235 'image/gif': ('image/gif', 'inline'),
235 236 'image/jpeg': ('image/jpeg', 'inline'),
236 237 'image/svg+xml': ('image/svg+xml', 'inline'),
237 238 }
238 239
239 240 mimetype = file_node.mimetype
240 241 try:
241 242 mimetype, dispo = raw_mimetype_mapping[mimetype]
242 243 except KeyError:
243 244 # we don't know anything special about this, handle it safely
244 245 if file_node.is_binary:
245 246 # do same as download raw for binary files
246 247 mimetype, dispo = 'application/octet-stream', 'attachment'
247 248 else:
248 249 # do not just use the original mimetype, but force text/plain,
249 250 # otherwise it would serve text/html and that might be unsafe.
250 251 # Note: underlying vcs library fakes text/plain mimetype if the
251 252 # mimetype can not be determined and it thinks it is not
252 253 # binary.This might lead to erroneous text display in some
253 254 # cases, but helps in other cases, like with text files
254 255 # without extension.
255 256 mimetype, dispo = 'text/plain', 'inline'
256 257
257 258 if dispo == 'attachment':
258 259 dispo = 'attachment; filename=%s' % \
259 260 safe_str(f_path.split(os.sep)[-1])
260 261
261 262 response.content_disposition = dispo
262 263 response.content_type = mimetype
263 264 return file_node.content
264 265
265 266 @LoginRequired()
266 267 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
267 268 def edit(self, repo_name, revision, f_path):
268 269 repo = c.rhodecode_db_repo
269 270 if repo.enable_locking and repo.locked[0]:
270 271 h.flash(_('This repository is has been locked by %s on %s')
271 272 % (h.person_by_id(repo.locked[0]),
272 273 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
273 274 'warning')
274 275 return redirect(h.url('files_home',
275 276 repo_name=repo_name, revision='tip'))
276 277
277 278 # check if revision is a branch identifier- basically we cannot
278 279 # create multiple heads via file editing
279 280 _branches = repo.scm_instance.branches
280 281 # check if revision is a branch name or branch hash
281 282 if revision not in _branches.keys() + _branches.values():
282 283 h.flash(_('You can only edit files with revision '
283 284 'being a valid branch '), category='warning')
284 285 return redirect(h.url('files_home',
285 286 repo_name=repo_name, revision='tip',
286 287 f_path=f_path))
287 288
288 289 r_post = request.POST
289 290
290 291 c.cs = self.__get_cs_or_redirect(revision, repo_name)
291 292 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
292 293
293 294 if c.file.is_binary:
294 295 return redirect(url('files_home', repo_name=c.repo_name,
295 296 revision=c.cs.raw_id, f_path=f_path))
296 297 c.default_message = _('Edited file %s via RhodeCode') % (f_path)
297 298 c.f_path = f_path
298 299
299 300 if r_post:
300 301
301 302 old_content = c.file.content
302 303 sl = old_content.splitlines(1)
303 304 first_line = sl[0] if sl else ''
304 305 # modes: 0 - Unix, 1 - Mac, 2 - DOS
305 306 mode = detect_mode(first_line, 0)
306 307 content = convert_line_endings(r_post.get('content', ''), mode)
307 308
308 309 message = r_post.get('message') or c.default_message
309 310 author = self.rhodecode_user.full_contact
310 311
311 312 if content == old_content:
312 313 h.flash(_('No changes'), category='warning')
313 314 return redirect(url('changeset_home', repo_name=c.repo_name,
314 315 revision='tip'))
315 316 try:
316 317 self.scm_model.commit_change(repo=c.rhodecode_repo,
317 318 repo_name=repo_name, cs=c.cs,
318 319 user=self.rhodecode_user.user_id,
319 320 author=author, message=message,
320 321 content=content, f_path=f_path)
321 322 h.flash(_('Successfully committed to %s') % f_path,
322 323 category='success')
323 324
324 325 except Exception:
325 326 log.error(traceback.format_exc())
326 327 h.flash(_('Error occurred during commit'), category='error')
327 328 return redirect(url('changeset_home',
328 329 repo_name=c.repo_name, revision='tip'))
329 330
330 331 return render('files/files_edit.html')
331 332
332 333 @LoginRequired()
333 334 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
334 335 def add(self, repo_name, revision, f_path):
335 336
336 337 repo = Repository.get_by_repo_name(repo_name)
337 338 if repo.enable_locking and repo.locked[0]:
338 339 h.flash(_('This repository is has been locked by %s on %s')
339 340 % (h.person_by_id(repo.locked[0]),
340 341 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
341 342 'warning')
342 343 return redirect(h.url('files_home',
343 344 repo_name=repo_name, revision='tip'))
344 345
345 346 r_post = request.POST
346 347 c.cs = self.__get_cs_or_redirect(revision, repo_name,
347 348 redirect_after=False)
348 349 if c.cs is None:
349 350 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
350 351 c.default_message = (_('Added file via RhodeCode'))
351 352 c.f_path = f_path
352 353
353 354 if r_post:
354 355 unix_mode = 0
355 356 content = convert_line_endings(r_post.get('content', ''), unix_mode)
356 357
357 358 message = r_post.get('message') or c.default_message
358 359 filename = r_post.get('filename')
359 360 location = r_post.get('location', '')
360 361 file_obj = r_post.get('upload_file', None)
361 362
362 363 if file_obj is not None and hasattr(file_obj, 'filename'):
363 364 filename = file_obj.filename
364 365 content = file_obj.file
365 366
366 367 if not content:
367 368 h.flash(_('No content'), category='warning')
368 369 return redirect(url('changeset_home', repo_name=c.repo_name,
369 370 revision='tip'))
370 371 if not filename:
371 372 h.flash(_('No filename'), category='warning')
372 373 return redirect(url('changeset_home', repo_name=c.repo_name,
373 374 revision='tip'))
374 if location.startswith('/') or location.startswith('.') or '../' in location:
375 h.flash(_('Location must be relative path and must not '
376 'contain .. in path'), category='warning')
377 return redirect(url('changeset_home', repo_name=c.repo_name,
378 revision='tip'))
379 if location:
380 location = os.path.normpath(location)
375 #strip all crap out of file, just leave the basename
381 376 filename = os.path.basename(filename)
382 377 node_path = os.path.join(location, filename)
383 378 author = self.rhodecode_user.full_contact
384 379
385 380 try:
386 self.scm_model.create_node(repo=c.rhodecode_repo,
387 repo_name=repo_name, cs=c.cs,
388 user=self.rhodecode_user.user_id,
389 author=author, message=message,
390 content=content, f_path=node_path)
381 nodes = {
382 node_path: {
383 'content': content
384 }
385 }
386 self.scm_model.create_nodes(
387 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
388 message=message,
389 nodes=nodes,
390 parent_cs=c.cs,
391 author=author,
392 )
393
391 394 h.flash(_('Successfully committed to %s') % node_path,
392 395 category='success')
396 except NonRelativePathError, e:
397 h.flash(_('Location must be relative path and must not '
398 'contain .. in path'), category='warning')
399 return redirect(url('changeset_home', repo_name=c.repo_name,
400 revision='tip'))
393 401 except (NodeError, NodeAlreadyExistsError), e:
394 402 h.flash(_(e), category='error')
395 403 except Exception:
396 404 log.error(traceback.format_exc())
397 405 h.flash(_('Error occurred during commit'), category='error')
398 406 return redirect(url('changeset_home',
399 407 repo_name=c.repo_name, revision='tip'))
400 408
401 409 return render('files/files_add.html')
402 410
403 411 @LoginRequired()
404 412 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
405 413 'repository.admin')
406 414 def archivefile(self, repo_name, fname):
407 415
408 416 fileformat = None
409 417 revision = None
410 418 ext = None
411 419 subrepos = request.GET.get('subrepos') == 'true'
412 420
413 421 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
414 422 archive_spec = fname.split(ext_data[1])
415 423 if len(archive_spec) == 2 and archive_spec[1] == '':
416 424 fileformat = a_type or ext_data[1]
417 425 revision = archive_spec[0]
418 426 ext = ext_data[1]
419 427
420 428 try:
421 429 dbrepo = RepoModel().get_by_repo_name(repo_name)
422 430 if not dbrepo.enable_downloads:
423 431 return _('Downloads disabled')
424 432
425 433 if c.rhodecode_repo.alias == 'hg':
426 434 # patch and reset hooks section of UI config to not run any
427 435 # hooks on fetching archives with subrepos
428 436 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
429 437 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
430 438
431 439 cs = c.rhodecode_repo.get_changeset(revision)
432 440 content_type = settings.ARCHIVE_SPECS[fileformat][0]
433 441 except ChangesetDoesNotExistError:
434 442 return _('Unknown revision %s') % revision
435 443 except EmptyRepositoryError:
436 444 return _('Empty repository')
437 445 except (ImproperArchiveTypeError, KeyError):
438 446 return _('Unknown archive type')
439 447 # archive cache
440 448 from rhodecode import CONFIG
441 449 rev_name = cs.raw_id[:12]
442 450 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
443 451 safe_str(rev_name), ext)
444 452
445 453 use_cached_archive = False # defines if we use cached version of archive
446 454 archive_cache_enabled = CONFIG.get('archive_cache_dir')
447 455 if not subrepos and archive_cache_enabled:
448 456 #check if we it's ok to write
449 457 if not os.path.isdir(CONFIG['archive_cache_dir']):
450 458 os.makedirs(CONFIG['archive_cache_dir'])
451 459 cached_archive_path = os.path.join(CONFIG['archive_cache_dir'], archive_name)
452 460 if os.path.isfile(cached_archive_path):
453 461 log.debug('Found cached archive in %s' % cached_archive_path)
454 462 fd, archive = None, cached_archive_path
455 463 use_cached_archive = True
456 464 else:
457 465 log.debug('Archive %s is not yet cached' % (archive_name))
458 466
459 467 if not use_cached_archive:
460 468 #generate new archive
461 469 try:
462 470 fd, archive = tempfile.mkstemp()
463 471 t = open(archive, 'wb')
464 472 log.debug('Creating new temp archive in %s' % archive)
465 473 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
466 474 if archive_cache_enabled:
467 475 #if we generated the archive and use cache rename that
468 476 log.debug('Storing new archive in %s' % cached_archive_path)
469 477 shutil.move(archive, cached_archive_path)
470 478 archive = cached_archive_path
471 479 finally:
472 480 t.close()
473 481
474 482 def get_chunked_archive(archive):
475 483 stream = open(archive, 'rb')
476 484 while True:
477 485 data = stream.read(16 * 1024)
478 486 if not data:
479 487 stream.close()
480 488 if fd: # fd means we used temporary file
481 489 os.close(fd)
482 490 if not archive_cache_enabled:
483 491 log.debug('Destroing temp archive %s' % archive)
484 492 os.remove(archive)
485 493 break
486 494 yield data
487 495
488 496 response.content_disposition = str('attachment; filename=%s' % (archive_name))
489 497 response.content_type = str(content_type)
490 498 return get_chunked_archive(archive)
491 499
492 500 @LoginRequired()
493 501 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
494 502 'repository.admin')
495 503 def diff(self, repo_name, f_path):
496 504 ignore_whitespace = request.GET.get('ignorews') == '1'
497 505 line_context = request.GET.get('context', 3)
498 506 diff1 = request.GET.get('diff1', '')
499 507 diff2 = request.GET.get('diff2', '')
500 508 c.action = request.GET.get('diff')
501 509 c.no_changes = diff1 == diff2
502 510 c.f_path = f_path
503 511 c.big_diff = False
504 512 c.anchor_url = anchor_url
505 513 c.ignorews_url = _ignorews_url
506 514 c.context_url = _context_url
507 515 c.changes = OrderedDict()
508 516 c.changes[diff2] = []
509 517
510 518 #special case if we want a show rev only, it's impl here
511 519 #to reduce JS and callbacks
512 520
513 521 if request.GET.get('show_rev'):
514 522 if str2bool(request.GET.get('annotate', 'False')):
515 523 _url = url('files_annotate_home', repo_name=c.repo_name,
516 524 revision=diff1, f_path=c.f_path)
517 525 else:
518 526 _url = url('files_home', repo_name=c.repo_name,
519 527 revision=diff1, f_path=c.f_path)
520 528
521 529 return redirect(_url)
522 530 try:
523 531 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
524 532 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
525 533 try:
526 534 node1 = c.changeset_1.get_node(f_path)
527 535 if node1.is_dir():
528 536 raise NodeError('%s path is a %s not a file'
529 537 % (node1, type(node1)))
530 538 except NodeDoesNotExistError:
531 539 c.changeset_1 = EmptyChangeset(cs=diff1,
532 540 revision=c.changeset_1.revision,
533 541 repo=c.rhodecode_repo)
534 542 node1 = FileNode(f_path, '', changeset=c.changeset_1)
535 543 else:
536 544 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
537 545 node1 = FileNode(f_path, '', changeset=c.changeset_1)
538 546
539 547 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
540 548 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
541 549 try:
542 550 node2 = c.changeset_2.get_node(f_path)
543 551 if node2.is_dir():
544 552 raise NodeError('%s path is a %s not a file'
545 553 % (node2, type(node2)))
546 554 except NodeDoesNotExistError:
547 555 c.changeset_2 = EmptyChangeset(cs=diff2,
548 556 revision=c.changeset_2.revision,
549 557 repo=c.rhodecode_repo)
550 558 node2 = FileNode(f_path, '', changeset=c.changeset_2)
551 559 else:
552 560 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
553 561 node2 = FileNode(f_path, '', changeset=c.changeset_2)
554 562 except (RepositoryError, NodeError):
555 563 log.error(traceback.format_exc())
556 564 return redirect(url('files_home', repo_name=c.repo_name,
557 565 f_path=f_path))
558 566
559 567 if c.action == 'download':
560 568 _diff = diffs.get_gitdiff(node1, node2,
561 569 ignore_whitespace=ignore_whitespace,
562 570 context=line_context)
563 571 diff = diffs.DiffProcessor(_diff, format='gitdiff')
564 572
565 573 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
566 574 response.content_type = 'text/plain'
567 575 response.content_disposition = (
568 576 'attachment; filename=%s' % diff_name
569 577 )
570 578 return diff.as_raw()
571 579
572 580 elif c.action == 'raw':
573 581 _diff = diffs.get_gitdiff(node1, node2,
574 582 ignore_whitespace=ignore_whitespace,
575 583 context=line_context)
576 584 diff = diffs.DiffProcessor(_diff, format='gitdiff')
577 585 response.content_type = 'text/plain'
578 586 return diff.as_raw()
579 587
580 588 else:
581 589 fid = h.FID(diff2, node2.path)
582 590 line_context_lcl = get_line_ctx(fid, request.GET)
583 591 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
584 592
585 593 lim = request.GET.get('fulldiff') or self.cut_off_limit
586 594 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
587 595 filenode_new=node2,
588 596 cut_off_limit=lim,
589 597 ignore_whitespace=ign_whitespace_lcl,
590 598 line_context=line_context_lcl,
591 599 enable_comments=False)
592 600 op = ''
593 601 filename = node1.path
594 602 cs_changes = {
595 603 'fid': [cs1, cs2, op, filename, diff, st]
596 604 }
597 605 c.changes = cs_changes
598 606
599 607 return render('files/file_diff.html')
600 608
601 609 def _get_node_history(self, cs, f_path, changesets=None):
602 610 """
603 611 get changesets history for given node
604 612
605 613 :param cs: changeset to calculate history
606 614 :param f_path: path for node to calculate history for
607 615 :param changesets: if passed don't calculate history and take
608 616 changesets defined in this list
609 617 """
610 618 # calculate history based on tip
611 619 tip_cs = c.rhodecode_repo.get_changeset()
612 620 if changesets is None:
613 621 try:
614 622 changesets = tip_cs.get_file_history(f_path)
615 623 except (NodeDoesNotExistError, ChangesetError):
616 624 #this node is not present at tip !
617 625 changesets = cs.get_file_history(f_path)
618 626 hist_l = []
619 627
620 628 changesets_group = ([], _("Changesets"))
621 629 branches_group = ([], _("Branches"))
622 630 tags_group = ([], _("Tags"))
623 631 _hg = cs.repository.alias == 'hg'
624 632 for chs in changesets:
625 633 #_branch = '(%s)' % chs.branch if _hg else ''
626 634 _branch = chs.branch
627 635 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
628 636 changesets_group[0].append((chs.raw_id, n_desc,))
629 637 hist_l.append(changesets_group)
630 638
631 639 for name, chs in c.rhodecode_repo.branches.items():
632 640 branches_group[0].append((chs, name),)
633 641 hist_l.append(branches_group)
634 642
635 643 for name, chs in c.rhodecode_repo.tags.items():
636 644 tags_group[0].append((chs, name),)
637 645 hist_l.append(tags_group)
638 646
639 647 return hist_l, changesets
640 648
641 649 @LoginRequired()
642 650 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
643 651 'repository.admin')
644 652 @jsonify
645 653 def nodelist(self, repo_name, revision, f_path):
646 654 if request.environ.get('HTTP_X_PARTIAL_XHR'):
647 655 cs = self.__get_cs_or_redirect(revision, repo_name)
648 656 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
649 657 flat=False)
650 658 return {'nodes': _d + _f}
@@ -1,560 +1,559 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 40 NotAnonymous
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import diffs
44 44 from rhodecode.lib.utils import action_logger, jsonify
45 45 from rhodecode.lib.vcs.utils import safe_str
46 46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48 from rhodecode.lib.diffs import LimitedDiffContainer
49 49 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
50 50 ChangesetComment
51 51 from rhodecode.model.pull_request import PullRequestModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.repo import RepoModel
54 54 from rhodecode.model.comment import ChangesetCommentsModel
55 55 from rhodecode.model.changeset_status import ChangesetStatusModel
56 56 from rhodecode.model.forms import PullRequestForm
57 57 from mercurial import scmutil
58 58 from rhodecode.lib.utils2 import safe_int
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class PullrequestsController(BaseRepoController):
64 64
65 65 def __before__(self):
66 66 super(PullrequestsController, self).__before__()
67 67 repo_model = RepoModel()
68 68 c.users_array = repo_model.get_users_js()
69 69 c.users_groups_array = repo_model.get_users_groups_js()
70 70
71 71 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
72 72 """return a structure with repo's interesting changesets, suitable for
73 73 the selectors in pullrequest.html
74 74
75 75 rev: a revision that must be in the list somehow and selected by default
76 76 branch: a branch that must be in the list and selected by default - even if closed
77 77 branch_rev: a revision of which peers should be preferred and available."""
78 78 # list named branches that has been merged to this named branch - it should probably merge back
79 79 peers = []
80 80
81 81 if rev:
82 82 rev = safe_str(rev)
83 83
84 84 if branch:
85 85 branch = safe_str(branch)
86 86
87 87 if branch_rev:
88 88 branch_rev = safe_str(branch_rev)
89 89 # not restricting to merge() would also get branch point and be better
90 90 # (especially because it would get the branch point) ... but is currently too expensive
91 91 otherbranches = {}
92 92 for i in repo._repo.revs(
93 93 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
94 94 branch_rev, branch_rev):
95 95 cs = repo.get_changeset(i)
96 96 otherbranches[cs.branch] = cs.raw_id
97 97 for abranch, node in otherbranches.iteritems():
98 98 selected = 'branch:%s:%s' % (abranch, node)
99 99 peers.append((selected, abranch))
100 100
101 101 selected = None
102 102
103 103 branches = []
104 104 for abranch, branchrev in repo.branches.iteritems():
105 105 n = 'branch:%s:%s' % (abranch, branchrev)
106 106 branches.append((n, abranch))
107 107 if rev == branchrev:
108 108 selected = n
109 109 if branch == abranch:
110 110 selected = n
111 111 branch = None
112 112 if branch: # branch not in list - it is probably closed
113 113 revs = repo._repo.revs('max(branch(%s))', branch)
114 114 if revs:
115 115 cs = repo.get_changeset(revs[0])
116 116 selected = 'branch:%s:%s' % (branch, cs.raw_id)
117 117 branches.append((selected, branch))
118 118
119 119 bookmarks = []
120 120 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
121 121 n = 'book:%s:%s' % (bookmark, bookmarkrev)
122 122 bookmarks.append((n, bookmark))
123 123 if rev == bookmarkrev:
124 124 selected = n
125 125
126 126 tags = []
127 127 for tag, tagrev in repo.tags.iteritems():
128 128 n = 'tag:%s:%s' % (tag, tagrev)
129 129 tags.append((n, tag))
130 130 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
131 131 selected = n
132 132
133 133 # prio 1: rev was selected as existing entry above
134 134
135 135 # prio 2: create special entry for rev; rev _must_ be used
136 136 specials = []
137 137 if rev and selected is None:
138 138 selected = 'rev:%s:%s' % (rev, rev)
139 139 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
140 140
141 141 # prio 3: most recent peer branch
142 142 if peers and not selected:
143 143 selected = peers[0][0][0]
144 144
145 145 # prio 4: tip revision
146 146 if not selected:
147 147 selected = 'tag:tip:%s' % repo.tags['tip']
148 148
149 149 groups = [(specials, _("Special")),
150 150 (peers, _("Peer branches")),
151 151 (bookmarks, _("Bookmarks")),
152 152 (branches, _("Branches")),
153 153 (tags, _("Tags")),
154 154 ]
155 155 return [g for g in groups if g[0]], selected
156 156
157 157 def _get_is_allowed_change_status(self, pull_request):
158 158 owner = self.rhodecode_user.user_id == pull_request.user_id
159 159 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
160 160 pull_request.reviewers]
161 161 return (self.rhodecode_user.admin or owner or reviewer)
162 162
163 163 def _load_compare_data(self, pull_request, enable_comments=True):
164 164 """
165 165 Load context data needed for generating compare diff
166 166
167 167 :param pull_request:
168 :type pull_request:
169 168 """
170 169 org_repo = pull_request.org_repo
171 170 (org_ref_type,
172 171 org_ref_name,
173 172 org_ref_rev) = pull_request.org_ref.split(':')
174 173
175 174 other_repo = org_repo
176 175 (other_ref_type,
177 176 other_ref_name,
178 177 other_ref_rev) = pull_request.other_ref.split(':')
179 178
180 179 # despite opening revisions for bookmarks/branches/tags, we always
181 180 # convert this to rev to prevent changes after bookmark or branch change
182 181 org_ref = ('rev', org_ref_rev)
183 182 other_ref = ('rev', other_ref_rev)
184 183
185 184 c.org_repo = org_repo
186 185 c.other_repo = other_repo
187 186
188 187 c.fulldiff = fulldiff = request.GET.get('fulldiff')
189 188
190 189 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
191 190
192 191 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
193 192
194 193 c.org_ref = org_ref[1]
195 194 c.org_ref_type = org_ref[0]
196 195 c.other_ref = other_ref[1]
197 196 c.other_ref_type = other_ref[0]
198 197
199 198 diff_limit = self.cut_off_limit if not fulldiff else None
200 199
201 200 # we swap org/other ref since we run a simple diff on one repo
202 201 log.debug('running diff between %s and %s in %s'
203 202 % (other_ref, org_ref, org_repo.scm_instance.path))
204 203 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
205 204
206 205 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
207 206 diff_limit=diff_limit)
208 207 _parsed = diff_processor.prepare()
209 208
210 209 c.limited_diff = False
211 210 if isinstance(_parsed, LimitedDiffContainer):
212 211 c.limited_diff = True
213 212
214 213 c.files = []
215 214 c.changes = {}
216 215 c.lines_added = 0
217 216 c.lines_deleted = 0
218 217
219 218 for f in _parsed:
220 219 st = f['stats']
221 220 c.lines_added += st['added']
222 221 c.lines_deleted += st['deleted']
223 222 fid = h.FID('', f['filename'])
224 223 c.files.append([fid, f['operation'], f['filename'], f['stats']])
225 224 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
226 225 parsed_lines=[f])
227 226 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
228 227
229 228 @LoginRequired()
230 229 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
231 230 'repository.admin')
232 231 def show_all(self, repo_name):
233 232 c.pull_requests = PullRequestModel().get_all(repo_name)
234 233 c.repo_name = repo_name
235 234 p = safe_int(request.GET.get('page', 1), 1)
236 235
237 236 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
238 237
239 238 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
240 239
241 240 if request.environ.get('HTTP_X_PARTIAL_XHR'):
242 241 return c.pullrequest_data
243 242
244 243 return render('/pullrequests/pullrequest_show_all.html')
245 244
246 245 @LoginRequired()
247 246 @NotAnonymous()
248 247 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
249 248 'repository.admin')
250 249 def index(self):
251 250 org_repo = c.rhodecode_db_repo
252 251
253 252 if org_repo.scm_instance.alias != 'hg':
254 253 log.error('Review not available for GIT REPOS')
255 254 raise HTTPNotFound
256 255
257 256 try:
258 257 org_repo.scm_instance.get_changeset()
259 258 except EmptyRepositoryError, e:
260 259 h.flash(h.literal(_('There are no changesets yet')),
261 260 category='warning')
262 261 redirect(url('summary_home', repo_name=org_repo.repo_name))
263 262
264 263 org_rev = request.GET.get('rev_end')
265 264 # rev_start is not directly useful - its parent could however be used
266 265 # as default for other and thus give a simple compare view
267 266 #other_rev = request.POST.get('rev_start')
268 267 branch = request.GET.get('branch')
269 268
270 269 c.org_repos = []
271 270 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
272 271 c.default_org_repo = org_repo.repo_name
273 272 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
274 273
275 274 c.other_repos = []
276 275 other_repos_info = {}
277 276
278 277 def add_other_repo(repo, branch_rev=None):
279 278 if repo.repo_name in other_repos_info: # shouldn't happen
280 279 return
281 280 c.other_repos.append((repo.repo_name, repo.repo_name))
282 281 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
283 282 other_repos_info[repo.repo_name] = {
284 283 'user': dict(user_id=repo.user.user_id,
285 284 username=repo.user.username,
286 285 firstname=repo.user.firstname,
287 286 lastname=repo.user.lastname,
288 287 gravatar_link=h.gravatar_url(repo.user.email, 14)),
289 288 'description': repo.description.split('\n', 1)[0],
290 289 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
291 290 }
292 291
293 292 # add org repo to other so we can open pull request against peer branches on itself
294 293 add_other_repo(org_repo, branch_rev=org_rev)
295 294 c.default_other_repo = org_repo.repo_name
296 295
297 296 # gather forks and add to this list ... even though it is rare to
298 297 # request forks to pull from their parent
299 298 for fork in org_repo.forks:
300 299 add_other_repo(fork)
301 300
302 301 # add parents of this fork also, but only if it's not empty
303 302 if org_repo.parent and org_repo.parent.scm_instance.revisions:
304 303 add_other_repo(org_repo.parent)
305 304 c.default_other_repo = org_repo.parent.repo_name
306 305
307 306 c.default_other_repo_info = other_repos_info[c.default_other_repo]
308 307 c.other_repos_info = json.dumps(other_repos_info)
309 308
310 309 return render('/pullrequests/pullrequest.html')
311 310
312 311 @LoginRequired()
313 312 @NotAnonymous()
314 313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
315 314 'repository.admin')
316 315 def create(self, repo_name):
317 316 repo = RepoModel()._get_repo(repo_name)
318 317 try:
319 318 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
320 319 except formencode.Invalid, errors:
321 320 log.error(traceback.format_exc())
322 321 if errors.error_dict.get('revisions'):
323 322 msg = 'Revisions: %s' % errors.error_dict['revisions']
324 323 elif errors.error_dict.get('pullrequest_title'):
325 324 msg = _('Pull request requires a title with min. 3 chars')
326 325 else:
327 326 msg = _('Error creating pull request')
328 327
329 328 h.flash(msg, 'error')
330 329 return redirect(url('pullrequest_home', repo_name=repo_name))
331 330
332 331 org_repo = _form['org_repo']
333 332 org_ref = 'rev:merge:%s' % _form['merge_rev']
334 333 other_repo = _form['other_repo']
335 334 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev']
336 335 revisions = [x for x in reversed(_form['revisions'])]
337 336 reviewers = _form['review_members']
338 337
339 338 title = _form['pullrequest_title']
340 339 description = _form['pullrequest_desc']
341 340 try:
342 341 pull_request = PullRequestModel().create(
343 342 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
344 343 other_ref, revisions, reviewers, title, description
345 344 )
346 345 Session().commit()
347 346 h.flash(_('Successfully opened new pull request'),
348 347 category='success')
349 348 except Exception:
350 349 h.flash(_('Error occurred during sending pull request'),
351 350 category='error')
352 351 log.error(traceback.format_exc())
353 352 return redirect(url('pullrequest_home', repo_name=repo_name))
354 353
355 354 return redirect(url('pullrequest_show', repo_name=other_repo,
356 355 pull_request_id=pull_request.pull_request_id))
357 356
358 357 @LoginRequired()
359 358 @NotAnonymous()
360 359 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
361 360 'repository.admin')
362 361 @jsonify
363 362 def update(self, repo_name, pull_request_id):
364 363 pull_request = PullRequest.get_or_404(pull_request_id)
365 364 if pull_request.is_closed():
366 365 raise HTTPForbidden()
367 366 #only owner or admin can update it
368 367 owner = pull_request.author.user_id == c.rhodecode_user.user_id
369 368 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
370 369 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
371 370 request.POST.get('reviewers_ids', '').split(',')))
372 371
373 372 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
374 373 Session().commit()
375 374 return True
376 375 raise HTTPForbidden()
377 376
378 377 @LoginRequired()
379 378 @NotAnonymous()
380 379 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
381 380 'repository.admin')
382 381 @jsonify
383 382 def delete(self, repo_name, pull_request_id):
384 383 pull_request = PullRequest.get_or_404(pull_request_id)
385 384 #only owner can delete it !
386 385 if pull_request.author.user_id == c.rhodecode_user.user_id:
387 386 PullRequestModel().delete(pull_request)
388 387 Session().commit()
389 388 h.flash(_('Successfully deleted pull request'),
390 389 category='success')
391 390 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
392 391 raise HTTPForbidden()
393 392
394 393 @LoginRequired()
395 394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 395 'repository.admin')
397 396 def show(self, repo_name, pull_request_id):
398 397 repo_model = RepoModel()
399 398 c.users_array = repo_model.get_users_js()
400 399 c.users_groups_array = repo_model.get_users_groups_js()
401 400 c.pull_request = PullRequest.get_or_404(pull_request_id)
402 401 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
403 402 cc_model = ChangesetCommentsModel()
404 403 cs_model = ChangesetStatusModel()
405 404 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
406 405 pull_request=c.pull_request,
407 406 with_revisions=True)
408 407
409 408 cs_statuses = defaultdict(list)
410 409 for st in _cs_statuses:
411 410 cs_statuses[st.author.username] += [st]
412 411
413 412 c.pull_request_reviewers = []
414 413 c.pull_request_pending_reviewers = []
415 414 for o in c.pull_request.reviewers:
416 415 st = cs_statuses.get(o.user.username, None)
417 416 if st:
418 417 sorter = lambda k: k.version
419 418 st = [(x, list(y)[0])
420 419 for x, y in (groupby(sorted(st, key=sorter), sorter))]
421 420 else:
422 421 c.pull_request_pending_reviewers.append(o.user)
423 422 c.pull_request_reviewers.append([o.user, st])
424 423
425 424 # pull_requests repo_name we opened it against
426 425 # ie. other_repo must match
427 426 if repo_name != c.pull_request.other_repo.repo_name:
428 427 raise HTTPNotFound
429 428
430 429 # load compare data into template context
431 430 enable_comments = not c.pull_request.is_closed()
432 431 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
433 432
434 433 # inline comments
435 434 c.inline_cnt = 0
436 435 c.inline_comments = cc_model.get_inline_comments(
437 436 c.rhodecode_db_repo.repo_id,
438 437 pull_request=pull_request_id)
439 438 # count inline comments
440 439 for __, lines in c.inline_comments:
441 440 for comments in lines.values():
442 441 c.inline_cnt += len(comments)
443 442 # comments
444 443 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
445 444 pull_request=pull_request_id)
446 445
447 446 try:
448 447 cur_status = c.statuses[c.pull_request.revisions[0]][0]
449 448 except Exception:
450 449 log.error(traceback.format_exc())
451 450 cur_status = 'undefined'
452 451 if c.pull_request.is_closed() and 0:
453 452 c.current_changeset_status = cur_status
454 453 else:
455 454 # changeset(pull-request) status calulation based on reviewers
456 455 c.current_changeset_status = cs_model.calculate_status(
457 456 c.pull_request_reviewers,
458 457 )
459 458 c.changeset_statuses = ChangesetStatus.STATUSES
460 459
461 460 c.as_form = False
462 461 c.ancestor = None # there is one - but right here we don't know which
463 462 return render('/pullrequests/pullrequest_show.html')
464 463
465 464 @LoginRequired()
466 465 @NotAnonymous()
467 466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 467 'repository.admin')
469 468 @jsonify
470 469 def comment(self, repo_name, pull_request_id):
471 470 pull_request = PullRequest.get_or_404(pull_request_id)
472 471 if pull_request.is_closed():
473 472 raise HTTPForbidden()
474 473
475 474 status = request.POST.get('changeset_status')
476 475 change_status = request.POST.get('change_changeset_status')
477 476 text = request.POST.get('text')
478 477 close_pr = request.POST.get('save_close')
479 478
480 479 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
481 480 if status and change_status and allowed_to_change_status:
482 481 _def = (_('Status change -> %s')
483 482 % ChangesetStatus.get_status_lbl(status))
484 483 if close_pr:
485 484 _def = _('Closing with') + ' ' + _def
486 485 text = text or _def
487 486 comm = ChangesetCommentsModel().create(
488 487 text=text,
489 488 repo=c.rhodecode_db_repo.repo_id,
490 489 user=c.rhodecode_user.user_id,
491 490 pull_request=pull_request_id,
492 491 f_path=request.POST.get('f_path'),
493 492 line_no=request.POST.get('line'),
494 493 status_change=(ChangesetStatus.get_status_lbl(status)
495 494 if status and change_status
496 495 and allowed_to_change_status else None),
497 496 closing_pr=close_pr
498 497 )
499 498
500 499 action_logger(self.rhodecode_user,
501 500 'user_commented_pull_request:%s' % pull_request_id,
502 501 c.rhodecode_db_repo, self.ip_addr, self.sa)
503 502
504 503 if allowed_to_change_status:
505 504 # get status if set !
506 505 if status and change_status:
507 506 ChangesetStatusModel().set_status(
508 507 c.rhodecode_db_repo.repo_id,
509 508 status,
510 509 c.rhodecode_user.user_id,
511 510 comm,
512 511 pull_request=pull_request_id
513 512 )
514 513
515 514 if close_pr:
516 515 if status in ['rejected', 'approved']:
517 516 PullRequestModel().close_pull_request(pull_request_id)
518 517 action_logger(self.rhodecode_user,
519 518 'user_closed_pull_request:%s' % pull_request_id,
520 519 c.rhodecode_db_repo, self.ip_addr, self.sa)
521 520 else:
522 521 h.flash(_('Closing pull request on other statuses than '
523 522 'rejected or approved forbidden'),
524 523 category='warning')
525 524
526 525 Session().commit()
527 526
528 527 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
529 528 return redirect(h.url('pullrequest_show', repo_name=repo_name,
530 529 pull_request_id=pull_request_id))
531 530
532 531 data = {
533 532 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
534 533 }
535 534 if comm:
536 535 c.co = comm
537 536 data.update(comm.get_dict())
538 537 data.update({'rendered_text':
539 538 render('changeset/changeset_comment_block.html')})
540 539
541 540 return data
542 541
543 542 @LoginRequired()
544 543 @NotAnonymous()
545 544 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
546 545 'repository.admin')
547 546 @jsonify
548 547 def delete_comment(self, repo_name, comment_id):
549 548 co = ChangesetComment.get(comment_id)
550 549 if co.pull_request.is_closed():
551 550 #don't allow deleting comments on closed pull request
552 551 raise HTTPForbidden()
553 552
554 553 owner = co.author.user_id == c.rhodecode_user.user_id
555 554 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
556 555 ChangesetCommentsModel().delete(comment=co)
557 556 Session().commit()
558 557 return True
559 558 else:
560 559 raise HTTPForbidden()
@@ -1,726 +1,725 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.db_manage
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Database creation, and setup module for RhodeCode. Used for creation
7 7 of database as well as for migration operations
8 8
9 9 :created_on: Apr 10, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import sys
29 29 import uuid
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32
33 33 from rhodecode import __dbversion__, __py_version__
34 34
35 35 from rhodecode.model.user import UserModel
36 36 from rhodecode.lib.utils import ask_ok
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
39 39 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup, \
40 40 UserRepoGroupToPerm, CacheInvalidation, UserGroup
41 41
42 42 from sqlalchemy.engine import create_engine
43 43 from rhodecode.model.repos_group import ReposGroupModel
44 44 #from rhodecode.model import meta
45 45 from rhodecode.model.meta import Session, Base
46 46 from rhodecode.model.repo import RepoModel
47 47 from rhodecode.model.permission import PermissionModel
48 48 from rhodecode.model.users_group import UserGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def notify(msg):
55 55 """
56 56 Notification for migrations messages
57 57 """
58 58 ml = len(msg) + (4 * 2)
59 59 print >> sys.stdout, ('*** %s ***\n%s' % (msg, '*' * ml)).upper()
60 60
61 61
62 62 class DbManage(object):
63 63 def __init__(self, log_sql, dbconf, root, tests=False, cli_args={}):
64 64 self.dbname = dbconf.split('/')[-1]
65 65 self.tests = tests
66 66 self.root = root
67 67 self.dburi = dbconf
68 68 self.log_sql = log_sql
69 69 self.db_exists = False
70 70 self.cli_args = cli_args
71 71 self.init_db()
72 72
73 73 force_ask = self.cli_args.get('force_ask')
74 74 if force_ask is not None:
75 75 global ask_ok
76 76 ask_ok = lambda *args, **kwargs: force_ask
77 77
78 78 def init_db(self):
79 79 engine = create_engine(self.dburi, echo=self.log_sql)
80 80 init_model(engine)
81 81 self.sa = Session()
82 82
83 83 def create_tables(self, override=False):
84 84 """
85 85 Create a auth database
86 86 """
87 87
88 88 log.info("Any existing database is going to be destroyed")
89 89 if self.tests:
90 90 destroy = True
91 91 else:
92 92 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
93 93 if not destroy:
94 94 sys.exit('Nothing tables created')
95 95 if destroy:
96 96 Base.metadata.drop_all()
97 97
98 98 checkfirst = not override
99 99 Base.metadata.create_all(checkfirst=checkfirst)
100 100 log.info('Created tables for %s' % self.dbname)
101 101
102 102 def set_db_version(self):
103 103 ver = DbMigrateVersion()
104 104 ver.version = __dbversion__
105 105 ver.repository_id = 'rhodecode_db_migrations'
106 106 ver.repository_path = 'versions'
107 107 self.sa.add(ver)
108 108 log.info('db version set to: %s' % __dbversion__)
109 109
110 110 def upgrade(self):
111 111 """
112 112 Upgrades given database schema to given revision following
113 113 all needed steps, to perform the upgrade
114 114
115 115 """
116 116
117 117 from rhodecode.lib.dbmigrate.migrate.versioning import api
118 118 from rhodecode.lib.dbmigrate.migrate.exceptions import \
119 119 DatabaseNotControlledError
120 120
121 121 if 'sqlite' in self.dburi:
122 122 print (
123 123 '********************** WARNING **********************\n'
124 124 'Make sure your version of sqlite is at least 3.7.X. \n'
125 125 'Earlier versions are known to fail on some migrations\n'
126 126 '*****************************************************\n'
127 127 )
128 128 upgrade = ask_ok('You are about to perform database upgrade, make '
129 129 'sure You backed up your database before. '
130 130 'Continue ? [y/n]')
131 131 if not upgrade:
132 132 sys.exit('No upgrade performed')
133 133
134 134 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
135 135 'rhodecode/lib/dbmigrate')
136 136 db_uri = self.dburi
137 137
138 138 try:
139 139 curr_version = api.db_version(db_uri, repository_path)
140 140 msg = ('Found current database under version'
141 141 ' control with version %s' % curr_version)
142 142
143 143 except (RuntimeError, DatabaseNotControlledError):
144 144 curr_version = 1
145 145 msg = ('Current database is not under version control. Setting'
146 146 ' as version %s' % curr_version)
147 147 api.version_control(db_uri, repository_path, curr_version)
148 148
149 149 notify(msg)
150 150
151 151 if curr_version == __dbversion__:
152 152 sys.exit('This database is already at the newest version')
153 153
154 154 # clear cache keys
155 155 log.info("Clearing cache keys now...")
156 156 CacheInvalidation.clear_cache()
157 157
158 158 #======================================================================
159 159 # UPGRADE STEPS
160 160 #======================================================================
161 161
162 162 class UpgradeSteps(object):
163 163 """
164 164 Those steps follow schema versions so for example schema
165 165 for example schema with seq 002 == step_2 and so on.
166 166 """
167 167
168 168 def __init__(self, klass):
169 169 self.klass = klass
170 170
171 171 def step_0(self):
172 172 # step 0 is the schema upgrade, and than follow proper upgrades
173 173 notify('attempting to do database upgrade from '
174 174 'version %s to version %s' %(curr_version, __dbversion__))
175 175 api.upgrade(db_uri, repository_path, __dbversion__)
176 176 notify('Schema upgrade completed')
177 177
178 178 def step_1(self):
179 179 pass
180 180
181 181 def step_2(self):
182 182 notify('Patching repo paths for newer version of RhodeCode')
183 183 self.klass.fix_repo_paths()
184 184
185 185 notify('Patching default user of RhodeCode')
186 186 self.klass.fix_default_user()
187 187
188 188 log.info('Changing ui settings')
189 189 self.klass.create_ui_settings()
190 190
191 191 def step_3(self):
192 192 notify('Adding additional settings into RhodeCode db')
193 193 self.klass.fix_settings()
194 194 notify('Adding ldap defaults')
195 195 self.klass.create_ldap_options(skip_existing=True)
196 196
197 197 def step_4(self):
198 198 notify('create permissions and fix groups')
199 199 self.klass.create_permissions()
200 200 self.klass.fixup_groups()
201 201
202 202 def step_5(self):
203 203 pass
204 204
205 205 def step_6(self):
206 206
207 207 notify('re-checking permissions')
208 208 self.klass.create_permissions()
209 209
210 210 notify('installing new UI options')
211 211 sett4 = RhodeCodeSetting('show_public_icon', True)
212 212 Session().add(sett4)
213 213 sett5 = RhodeCodeSetting('show_private_icon', True)
214 214 Session().add(sett5)
215 215 sett6 = RhodeCodeSetting('stylify_metatags', False)
216 216 Session().add(sett6)
217 217
218 218 notify('fixing old PULL hook')
219 219 _pull = RhodeCodeUi.get_by_key('preoutgoing.pull_logger')
220 220 if _pull:
221 221 _pull.ui_key = RhodeCodeUi.HOOK_PULL
222 222 Session().add(_pull)
223 223
224 224 notify('fixing old PUSH hook')
225 225 _push = RhodeCodeUi.get_by_key('pretxnchangegroup.push_logger')
226 226 if _push:
227 227 _push.ui_key = RhodeCodeUi.HOOK_PUSH
228 228 Session().add(_push)
229 229
230 230 notify('installing new pre-push hook')
231 231 hooks4 = RhodeCodeUi()
232 232 hooks4.ui_section = 'hooks'
233 233 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
234 234 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
235 235 Session().add(hooks4)
236 236
237 237 notify('installing new pre-pull hook')
238 238 hooks6 = RhodeCodeUi()
239 239 hooks6.ui_section = 'hooks'
240 240 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
241 241 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
242 242 Session().add(hooks6)
243 243
244 244 notify('installing hgsubversion option')
245 245 # enable hgsubversion disabled by default
246 246 hgsubversion = RhodeCodeUi()
247 247 hgsubversion.ui_section = 'extensions'
248 248 hgsubversion.ui_key = 'hgsubversion'
249 249 hgsubversion.ui_value = ''
250 250 hgsubversion.ui_active = False
251 251 Session().add(hgsubversion)
252 252
253 253 notify('installing hg git option')
254 254 # enable hggit disabled by default
255 255 hggit = RhodeCodeUi()
256 256 hggit.ui_section = 'extensions'
257 257 hggit.ui_key = 'hggit'
258 258 hggit.ui_value = ''
259 259 hggit.ui_active = False
260 260 Session().add(hggit)
261 261
262 262 notify('re-check default permissions')
263 263 default_user = User.get_by_username(User.DEFAULT_USER)
264 264 perm = Permission.get_by_key('hg.fork.repository')
265 265 reg_perm = UserToPerm()
266 266 reg_perm.user = default_user
267 267 reg_perm.permission = perm
268 268 Session().add(reg_perm)
269 269
270 270 def step_7(self):
271 271 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
272 272 Session().commit()
273 273 if perm_fixes:
274 274 notify('There was an inconsistent state of permissions '
275 275 'detected for default user. Permissions are now '
276 276 'reset to the default value for default user. '
277 277 'Please validate and check default permissions '
278 278 'in admin panel')
279 279
280 280 def step_8(self):
281 281 self.klass.create_permissions()
282 282 self.klass.populate_default_permissions()
283 283 self.klass.create_default_options(skip_existing=True)
284 284 Session().commit()
285 285
286 286 def step_9(self):
287 287 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
288 288 Session().commit()
289 289 if perm_fixes:
290 290 notify('There was an inconsistent state of permissions '
291 291 'detected for default user. Permissions are now '
292 292 'reset to the default value for default user. '
293 293 'Please validate and check default permissions '
294 294 'in admin panel')
295 295
296 296 def step_10(self):
297 297 pass
298 298
299 299 def step_11(self):
300 300 self.klass.update_repo_info()
301 301
302 302 def step_12(self):
303 303 self.klass.create_permissions()
304 304 Session().commit()
305 305
306 306 self.klass.populate_default_permissions()
307 307 Session().commit()
308 308
309 309 #fix all usergroups
310 310 ug_model = UserGroupModel()
311 311 for ug in UserGroup.get_all():
312 312 perm_obj = ug_model._create_default_perms(ug)
313 313 Session().add(perm_obj)
314 314 Session().commit()
315 315
316 316 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
317 317
318 318 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
319 319 _step = None
320 320 for step in upgrade_steps:
321 321 notify('performing upgrade step %s' % step)
322 322 getattr(UpgradeSteps(self), 'step_%s' % step)()
323 323 self.sa.commit()
324 324 _step = step
325 325
326 326 notify('upgrade to version %s successful' % _step)
327 327
328 328 def fix_repo_paths(self):
329 329 """
330 330 Fixes a old rhodecode version path into new one without a '*'
331 331 """
332 332
333 333 paths = self.sa.query(RhodeCodeUi)\
334 334 .filter(RhodeCodeUi.ui_key == '/')\
335 335 .scalar()
336 336
337 337 paths.ui_value = paths.ui_value.replace('*', '')
338 338
339 339 try:
340 340 self.sa.add(paths)
341 341 self.sa.commit()
342 342 except Exception:
343 343 self.sa.rollback()
344 344 raise
345 345
346 346 def fix_default_user(self):
347 347 """
348 348 Fixes a old default user with some 'nicer' default values,
349 349 used mostly for anonymous access
350 350 """
351 351 def_user = self.sa.query(User)\
352 352 .filter(User.username == 'default')\
353 353 .one()
354 354
355 355 def_user.name = 'Anonymous'
356 356 def_user.lastname = 'User'
357 357 def_user.email = 'anonymous@rhodecode.org'
358 358
359 359 try:
360 360 self.sa.add(def_user)
361 361 self.sa.commit()
362 362 except Exception:
363 363 self.sa.rollback()
364 364 raise
365 365
366 366 def fix_settings(self):
367 367 """
368 368 Fixes rhodecode settings adds ga_code key for google analytics
369 369 """
370 370
371 371 hgsettings3 = RhodeCodeSetting('ga_code', '')
372 372
373 373 try:
374 374 self.sa.add(hgsettings3)
375 375 self.sa.commit()
376 376 except Exception:
377 377 self.sa.rollback()
378 378 raise
379 379
380 380 def admin_prompt(self, second=False):
381 381 if not self.tests:
382 382 import getpass
383 383
384 384 # defaults
385 385 defaults = self.cli_args
386 386 username = defaults.get('username')
387 387 password = defaults.get('password')
388 388 email = defaults.get('email')
389 389
390 390 def get_password():
391 391 password = getpass.getpass('Specify admin password '
392 392 '(min 6 chars):')
393 393 confirm = getpass.getpass('Confirm password:')
394 394
395 395 if password != confirm:
396 396 log.error('passwords mismatch')
397 397 return False
398 398 if len(password) < 6:
399 399 log.error('password is to short use at least 6 characters')
400 400 return False
401 401
402 402 return password
403 403 if username is None:
404 404 username = raw_input('Specify admin username:')
405 405 if password is None:
406 406 password = get_password()
407 407 if not password:
408 408 #second try
409 409 password = get_password()
410 410 if not password:
411 411 sys.exit()
412 412 if email is None:
413 413 email = raw_input('Specify admin email:')
414 414 self.create_user(username, password, email, True)
415 415 else:
416 416 log.info('creating admin and regular test users')
417 417 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
418 418 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
419 419 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
420 420 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
421 421 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
422 422
423 423 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
424 424 TEST_USER_ADMIN_EMAIL, True)
425 425
426 426 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
427 427 TEST_USER_REGULAR_EMAIL, False)
428 428
429 429 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
430 430 TEST_USER_REGULAR2_EMAIL, False)
431 431
432 432 def create_ui_settings(self):
433 433 """
434 434 Creates ui settings, fills out hooks
435 435 and disables dotencode
436 436 """
437 437
438 438 #HOOKS
439 439 hooks1_key = RhodeCodeUi.HOOK_UPDATE
440 440 hooks1_ = self.sa.query(RhodeCodeUi)\
441 441 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
442 442
443 443 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
444 444 hooks1.ui_section = 'hooks'
445 445 hooks1.ui_key = hooks1_key
446 446 hooks1.ui_value = 'hg update >&2'
447 447 hooks1.ui_active = False
448 448 self.sa.add(hooks1)
449 449
450 450 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
451 451 hooks2_ = self.sa.query(RhodeCodeUi)\
452 452 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
453 453 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
454 454 hooks2.ui_section = 'hooks'
455 455 hooks2.ui_key = hooks2_key
456 456 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
457 457 self.sa.add(hooks2)
458 458
459 459 hooks3 = RhodeCodeUi()
460 460 hooks3.ui_section = 'hooks'
461 461 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
462 462 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
463 463 self.sa.add(hooks3)
464 464
465 465 hooks4 = RhodeCodeUi()
466 466 hooks4.ui_section = 'hooks'
467 467 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
468 468 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
469 469 self.sa.add(hooks4)
470 470
471 471 hooks5 = RhodeCodeUi()
472 472 hooks5.ui_section = 'hooks'
473 473 hooks5.ui_key = RhodeCodeUi.HOOK_PULL
474 474 hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
475 475 self.sa.add(hooks5)
476 476
477 477 hooks6 = RhodeCodeUi()
478 478 hooks6.ui_section = 'hooks'
479 479 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
480 480 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
481 481 self.sa.add(hooks6)
482 482
483 483 # enable largefiles
484 484 largefiles = RhodeCodeUi()
485 485 largefiles.ui_section = 'extensions'
486 486 largefiles.ui_key = 'largefiles'
487 487 largefiles.ui_value = ''
488 488 self.sa.add(largefiles)
489 489
490 490 # enable hgsubversion disabled by default
491 491 hgsubversion = RhodeCodeUi()
492 492 hgsubversion.ui_section = 'extensions'
493 493 hgsubversion.ui_key = 'hgsubversion'
494 494 hgsubversion.ui_value = ''
495 495 hgsubversion.ui_active = False
496 496 self.sa.add(hgsubversion)
497 497
498 498 # enable hggit disabled by default
499 499 hggit = RhodeCodeUi()
500 500 hggit.ui_section = 'extensions'
501 501 hggit.ui_key = 'hggit'
502 502 hggit.ui_value = ''
503 503 hggit.ui_active = False
504 504 self.sa.add(hggit)
505 505
506 506 def create_ldap_options(self, skip_existing=False):
507 507 """Creates ldap settings"""
508 508
509 509 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
510 510 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
511 511 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
512 512 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
513 513 ('ldap_filter', ''), ('ldap_search_scope', ''),
514 514 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
515 515 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
516 516
517 517 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
518 518 log.debug('Skipping option %s' % k)
519 519 continue
520 520 setting = RhodeCodeSetting(k, v)
521 521 self.sa.add(setting)
522 522
523 523 def create_default_options(self, skip_existing=False):
524 524 """Creates default settings"""
525 525
526 526 for k, v in [
527 527 ('default_repo_enable_locking', False),
528 528 ('default_repo_enable_downloads', False),
529 529 ('default_repo_enable_statistics', False),
530 530 ('default_repo_private', False),
531 531 ('default_repo_type', 'hg')]:
532 532
533 533 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
534 534 log.debug('Skipping option %s' % k)
535 535 continue
536 536 setting = RhodeCodeSetting(k, v)
537 537 self.sa.add(setting)
538 538
539 539 def fixup_groups(self):
540 540 def_usr = User.get_default_user()
541 541 for g in RepoGroup.query().all():
542 542 g.group_name = g.get_new_name(g.name)
543 543 self.sa.add(g)
544 544 # get default perm
545 545 default = UserRepoGroupToPerm.query()\
546 546 .filter(UserRepoGroupToPerm.group == g)\
547 547 .filter(UserRepoGroupToPerm.user == def_usr)\
548 548 .scalar()
549 549
550 550 if default is None:
551 551 log.debug('missing default permission for group %s adding' % g)
552 552 perm_obj = ReposGroupModel()._create_default_perms(g)
553 553 self.sa.add(perm_obj)
554 554
555 555 def reset_permissions(self, username):
556 556 """
557 557 Resets permissions to default state, usefull when old systems had
558 558 bad permissions, we must clean them up
559 559
560 560 :param username:
561 :type username:
562 561 """
563 562 default_user = User.get_by_username(username)
564 563 if not default_user:
565 564 return
566 565
567 566 u2p = UserToPerm.query()\
568 567 .filter(UserToPerm.user == default_user).all()
569 568 fixed = False
570 569 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
571 570 for p in u2p:
572 571 Session().delete(p)
573 572 fixed = True
574 573 self.populate_default_permissions()
575 574 return fixed
576 575
577 576 def update_repo_info(self):
578 577 RepoModel.update_repoinfo()
579 578
580 579 def config_prompt(self, test_repo_path='', retries=3):
581 580 defaults = self.cli_args
582 581 _path = defaults.get('repos_location')
583 582 if retries == 3:
584 583 log.info('Setting up repositories config')
585 584
586 585 if _path is not None:
587 586 path = _path
588 587 elif not self.tests and not test_repo_path:
589 588 path = raw_input(
590 589 'Enter a valid absolute path to store repositories. '
591 590 'All repositories in that path will be added automatically:'
592 591 )
593 592 else:
594 593 path = test_repo_path
595 594 path_ok = True
596 595
597 596 # check proper dir
598 597 if not os.path.isdir(path):
599 598 path_ok = False
600 599 log.error('Given path %s is not a valid directory' % path)
601 600
602 601 elif not os.path.isabs(path):
603 602 path_ok = False
604 603 log.error('Given path %s is not an absolute path' % path)
605 604
606 605 # check write access
607 606 elif not os.access(path, os.W_OK) and path_ok:
608 607 path_ok = False
609 608 log.error('No write permission to given path %s' % path)
610 609
611 610 if retries == 0:
612 611 sys.exit('max retries reached')
613 612 if not path_ok:
614 613 retries -= 1
615 614 return self.config_prompt(test_repo_path, retries)
616 615
617 616 real_path = os.path.normpath(os.path.realpath(path))
618 617
619 618 if real_path != os.path.normpath(path):
620 619 if not ask_ok(('Path looks like a symlink, Rhodecode will store '
621 620 'given path as %s ? [y/n]') % (real_path)):
622 621 log.error('Canceled by user')
623 622 sys.exit(-1)
624 623
625 624 return real_path
626 625
627 626 def create_settings(self, path):
628 627
629 628 self.create_ui_settings()
630 629
631 630 #HG UI OPTIONS
632 631 web1 = RhodeCodeUi()
633 632 web1.ui_section = 'web'
634 633 web1.ui_key = 'push_ssl'
635 634 web1.ui_value = 'false'
636 635
637 636 web2 = RhodeCodeUi()
638 637 web2.ui_section = 'web'
639 638 web2.ui_key = 'allow_archive'
640 639 web2.ui_value = 'gz zip bz2'
641 640
642 641 web3 = RhodeCodeUi()
643 642 web3.ui_section = 'web'
644 643 web3.ui_key = 'allow_push'
645 644 web3.ui_value = '*'
646 645
647 646 web4 = RhodeCodeUi()
648 647 web4.ui_section = 'web'
649 648 web4.ui_key = 'baseurl'
650 649 web4.ui_value = '/'
651 650
652 651 paths = RhodeCodeUi()
653 652 paths.ui_section = 'paths'
654 653 paths.ui_key = '/'
655 654 paths.ui_value = path
656 655
657 656 phases = RhodeCodeUi()
658 657 phases.ui_section = 'phases'
659 658 phases.ui_key = 'publish'
660 659 phases.ui_value = False
661 660
662 661 sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
663 662 sett2 = RhodeCodeSetting('title', 'RhodeCode')
664 663 sett3 = RhodeCodeSetting('ga_code', '')
665 664
666 665 sett4 = RhodeCodeSetting('show_public_icon', True)
667 666 sett5 = RhodeCodeSetting('show_private_icon', True)
668 667 sett6 = RhodeCodeSetting('stylify_metatags', False)
669 668
670 669 self.sa.add(web1)
671 670 self.sa.add(web2)
672 671 self.sa.add(web3)
673 672 self.sa.add(web4)
674 673 self.sa.add(paths)
675 674 self.sa.add(sett1)
676 675 self.sa.add(sett2)
677 676 self.sa.add(sett3)
678 677 self.sa.add(sett4)
679 678 self.sa.add(sett5)
680 679 self.sa.add(sett6)
681 680
682 681 self.create_ldap_options()
683 682 self.create_default_options()
684 683
685 684 log.info('created ui config')
686 685
687 686 def create_user(self, username, password, email='', admin=False):
688 687 log.info('creating user %s' % username)
689 688 UserModel().create_or_update(username, password, email,
690 689 firstname='RhodeCode', lastname='Admin',
691 690 active=True, admin=admin)
692 691
693 692 def create_default_user(self):
694 693 log.info('creating default user')
695 694 # create default user for handling default permissions.
696 695 UserModel().create_or_update(username='default',
697 696 password=str(uuid.uuid1())[:8],
698 697 email='anonymous@rhodecode.org',
699 698 firstname='Anonymous', lastname='User')
700 699
701 700 def create_permissions(self):
702 701 """
703 702 Creates all permissions defined in the system
704 703 """
705 704 # module.(access|create|change|delete)_[name]
706 705 # module.(none|read|write|admin)
707 706 log.info('creating permissions')
708 707 PermissionModel(self.sa).create_permissions()
709 708
710 709 def populate_default_permissions(self):
711 710 """
712 711 Populate default permissions. It will create only the default
713 712 permissions that are missing, and not alter already defined ones
714 713 """
715 714 log.info('creating default user permissions')
716 715 PermissionModel(self.sa).create_default_permissions(user=User.DEFAULT_USER)
717 716
718 717 @staticmethod
719 718 def check_waitress():
720 719 """
721 720 Function executed at the end of setup
722 721 """
723 722 if not __py_version__ >= (2, 6):
724 723 notify('Python2.5 detected, please switch '
725 724 'egg:waitress#main -> egg:Paste#http '
726 725 'in your .ini file')
@@ -1,62 +1,69 b''
1 1 import logging
2 2 import datetime
3 3
4 4 from sqlalchemy import *
5 5 from sqlalchemy.exc import DatabaseError
6 6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 7 from sqlalchemy.orm.session import Session
8 8 from sqlalchemy.ext.declarative import declarative_base
9 9
10 10 from rhodecode.lib.dbmigrate.migrate import *
11 11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 12
13 13 from rhodecode.model.meta import Base
14 14 from rhodecode.model import meta
15 15 from rhodecode.lib.dbmigrate.versions import _reset_base
16 16
17 17 log = logging.getLogger(__name__)
18 18
19 19
20 20 def upgrade(migrate_engine):
21 21 """
22 22 Upgrade operations go here.
23 23 Don't create your own engine; bind migrate_engine to your metadata
24 24 """
25 25 _reset_base(migrate_engine)
26 26
27 27 #==========================================================================
28 28 # UserUserGroupToPerm
29 29 #==========================================================================
30 30 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserUserGroupToPerm
31 31 tbl = UserUserGroupToPerm.__table__
32 32 tbl.create()
33 33
34 34 #==========================================================================
35 35 # UserGroupUserGroupToPerm
36 36 #==========================================================================
37 37 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroupUserGroupToPerm
38 38 tbl = UserGroupUserGroupToPerm.__table__
39 39 tbl.create()
40 40
41 41 #==========================================================================
42 # Gist
43 #==========================================================================
44 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import Gist
45 tbl = Gist.__table__
46 tbl.create()
47
48 #==========================================================================
42 49 # UserGroup
43 50 #==========================================================================
44 51 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroup
45 52 tbl = UserGroup.__table__
46 53 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=False, default=None)
47 54 # create username column
48 55 user_id.create(table=tbl)
49 56
50 57 #==========================================================================
51 # UserGroup
58 # RepoGroup
52 59 #==========================================================================
53 60 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
54 61 tbl = RepoGroup.__table__
55 62 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=False, default=None)
56 63 # create username column
57 64 user_id.create(table=tbl)
58 65
59 66
60 67 def downgrade(migrate_engine):
61 68 meta = MetaData()
62 69 meta.bind = migrate_engine
@@ -1,713 +1,711 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31
32 32 from itertools import tee, imap
33 33
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.lib.vcs.exceptions import VCSError
37 37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 39 from rhodecode.lib.helpers import escape
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def wrap_to_table(str_):
46 46 return '''<table class="code-difftable">
47 47 <tr class="line no-comment">
48 48 <td class="lineno new"></td>
49 49 <td class="code no-comment"><pre>%s</pre></td>
50 50 </tr>
51 51 </table>''' % str_
52 52
53 53
54 54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 55 ignore_whitespace=True, line_context=3,
56 56 enable_comments=False):
57 57 """
58 58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 59 proper message
60 60 """
61 61
62 62 if filenode_old is None:
63 63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64 64
65 65 if filenode_old.is_binary or filenode_new.is_binary:
66 66 diff = wrap_to_table(_('Binary file'))
67 67 stats = (0, 0)
68 68 size = 0
69 69
70 70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72 72
73 73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 74 ignore_whitespace=ignore_whitespace,
75 75 context=line_context)
76 76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77 77
78 78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 79 stats = diff_processor.stat()
80 80 size = len(diff or '')
81 81 else:
82 82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 83 'diff menu to display this diff'))
84 84 stats = (0, 0)
85 85 size = 0
86 86 if not diff:
87 87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 88 [filenode_new, filenode_old])
89 89 if submodules:
90 90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 91 else:
92 92 diff = wrap_to_table(_('No changes detected'))
93 93
94 94 cs1 = filenode_old.changeset.raw_id
95 95 cs2 = filenode_new.changeset.raw_id
96 96
97 97 return size, cs1, cs2, diff, stats
98 98
99 99
100 100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 101 """
102 102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103 103
104 104 :param ignore_whitespace: ignore whitespaces in diff
105 105 """
106 106 # make sure we pass in default context
107 107 context = context or 3
108 108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 109 [filenode_new, filenode_old])
110 110 if submodules:
111 111 return ''
112 112
113 113 for filenode in (filenode_old, filenode_new):
114 114 if not isinstance(filenode, FileNode):
115 115 raise VCSError("Given object should be FileNode object, not %s"
116 116 % filenode.__class__)
117 117
118 118 repo = filenode_new.changeset.repository
119 119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121 121
122 122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 123 ignore_whitespace, context)
124 124 return vcs_gitdiff
125 125
126 126 NEW_FILENODE = 1
127 127 DEL_FILENODE = 2
128 128 MOD_FILENODE = 3
129 129 RENAMED_FILENODE = 4
130 130 CHMOD_FILENODE = 5
131 131 BIN_FILENODE = 6
132 132
133 133
134 134 class DiffLimitExceeded(Exception):
135 135 pass
136 136
137 137
138 138 class LimitedDiffContainer(object):
139 139
140 140 def __init__(self, diff_limit, cur_diff_size, diff):
141 141 self.diff = diff
142 142 self.diff_limit = diff_limit
143 143 self.cur_diff_size = cur_diff_size
144 144
145 145 def __iter__(self):
146 146 for l in self.diff:
147 147 yield l
148 148
149 149
150 150 class DiffProcessor(object):
151 151 """
152 152 Give it a unified or git diff and it returns a list of the files that were
153 153 mentioned in the diff together with a dict of meta information that
154 154 can be used to render it in a HTML template.
155 155 """
156 156 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
157 157 _newline_marker = re.compile(r'^\\ No newline at end of file')
158 158 _git_header_re = re.compile(r"""
159 159 #^diff[ ]--git
160 160 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
161 161 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
162 162 ^rename[ ]from[ ](?P<rename_from>\S+)\n
163 163 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
164 164 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
165 165 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
166 166 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
167 167 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
168 168 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
169 169 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
170 170 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
171 171 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
172 172 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
173 173 """, re.VERBOSE | re.MULTILINE)
174 174 _hg_header_re = re.compile(r"""
175 175 #^diff[ ]--git
176 176 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
177 177 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
178 178 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
179 179 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
180 180 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
181 181 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
182 182 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
183 183 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
184 184 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
185 185 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
186 186 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
187 187 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
188 188 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
189 189 """, re.VERBOSE | re.MULTILINE)
190 190
191 191 #used for inline highlighter word split
192 192 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
193 193
194 194 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
195 195 """
196 196 :param diff: a text in diff format
197 197 :param vcs: type of version controll hg or git
198 198 :param format: format of diff passed, `udiff` or `gitdiff`
199 199 :param diff_limit: define the size of diff that is considered "big"
200 200 based on that parameter cut off will be triggered, set to None
201 201 to show full diff
202 202 """
203 203 if not isinstance(diff, basestring):
204 204 raise Exception('Diff must be a basestring got %s instead' % type(diff))
205 205
206 206 self._diff = diff
207 207 self._format = format
208 208 self.adds = 0
209 209 self.removes = 0
210 210 # calculate diff size
211 211 self.diff_size = len(diff)
212 212 self.diff_limit = diff_limit
213 213 self.cur_diff_size = 0
214 214 self.parsed = False
215 215 self.parsed_diff = []
216 216 self.vcs = vcs
217 217
218 218 if format == 'gitdiff':
219 219 self.differ = self._highlight_line_difflib
220 220 self._parser = self._parse_gitdiff
221 221 else:
222 222 self.differ = self._highlight_line_udiff
223 223 self._parser = self._parse_udiff
224 224
225 225 def _copy_iterator(self):
226 226 """
227 227 make a fresh copy of generator, we should not iterate thru
228 228 an original as it's needed for repeating operations on
229 229 this instance of DiffProcessor
230 230 """
231 231 self.__udiff, iterator_copy = tee(self.__udiff)
232 232 return iterator_copy
233 233
234 234 def _escaper(self, string):
235 235 """
236 236 Escaper for diff escapes special chars and checks the diff limit
237 237
238 238 :param string:
239 :type string:
240 239 """
241 240
242 241 self.cur_diff_size += len(string)
243 242
244 243 # escaper get's iterated on each .next() call and it checks if each
245 244 # parsed line doesn't exceed the diff limit
246 245 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
247 246 raise DiffLimitExceeded('Diff Limit Exceeded')
248 247
249 248 return safe_unicode(string).replace('&', '&amp;')\
250 249 .replace('<', '&lt;')\
251 250 .replace('>', '&gt;')
252 251
253 252 def _line_counter(self, l):
254 253 """
255 254 Checks each line and bumps total adds/removes for this diff
256 255
257 256 :param l:
258 257 """
259 258 if l.startswith('+') and not l.startswith('+++'):
260 259 self.adds += 1
261 260 elif l.startswith('-') and not l.startswith('---'):
262 261 self.removes += 1
263 262 return safe_unicode(l)
264 263
265 264 def _highlight_line_difflib(self, line, next_):
266 265 """
267 266 Highlight inline changes in both lines.
268 267 """
269 268
270 269 if line['action'] == 'del':
271 270 old, new = line, next_
272 271 else:
273 272 old, new = next_, line
274 273
275 274 oldwords = self._token_re.split(old['line'])
276 275 newwords = self._token_re.split(new['line'])
277 276 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
278 277
279 278 oldfragments, newfragments = [], []
280 279 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
281 280 oldfrag = ''.join(oldwords[i1:i2])
282 281 newfrag = ''.join(newwords[j1:j2])
283 282 if tag != 'equal':
284 283 if oldfrag:
285 284 oldfrag = '<del>%s</del>' % oldfrag
286 285 if newfrag:
287 286 newfrag = '<ins>%s</ins>' % newfrag
288 287 oldfragments.append(oldfrag)
289 288 newfragments.append(newfrag)
290 289
291 290 old['line'] = "".join(oldfragments)
292 291 new['line'] = "".join(newfragments)
293 292
294 293 def _highlight_line_udiff(self, line, next_):
295 294 """
296 295 Highlight inline changes in both lines.
297 296 """
298 297 start = 0
299 298 limit = min(len(line['line']), len(next_['line']))
300 299 while start < limit and line['line'][start] == next_['line'][start]:
301 300 start += 1
302 301 end = -1
303 302 limit -= start
304 303 while -end <= limit and line['line'][end] == next_['line'][end]:
305 304 end -= 1
306 305 end += 1
307 306 if start or end:
308 307 def do(l):
309 308 last = end + len(l['line'])
310 309 if l['action'] == 'add':
311 310 tag = 'ins'
312 311 else:
313 312 tag = 'del'
314 313 l['line'] = '%s<%s>%s</%s>%s' % (
315 314 l['line'][:start],
316 315 tag,
317 316 l['line'][start:last],
318 317 tag,
319 318 l['line'][last:]
320 319 )
321 320 do(line)
322 321 do(next_)
323 322
324 323 def _get_header(self, diff_chunk):
325 324 """
326 325 parses the diff header, and returns parts, and leftover diff
327 326 parts consists of 14 elements::
328 327
329 328 a_path, b_path, similarity_index, rename_from, rename_to,
330 329 old_mode, new_mode, new_file_mode, deleted_file_mode,
331 330 a_blob_id, b_blob_id, b_mode, a_file, b_file
332 331
333 332 :param diff_chunk:
334 :type diff_chunk:
335 333 """
336 334
337 335 if self.vcs == 'git':
338 336 match = self._git_header_re.match(diff_chunk)
339 337 diff = diff_chunk[match.end():]
340 338 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
341 339 elif self.vcs == 'hg':
342 340 match = self._hg_header_re.match(diff_chunk)
343 341 diff = diff_chunk[match.end():]
344 342 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
345 343 else:
346 344 raise Exception('VCS type %s is not supported' % self.vcs)
347 345
348 346 def _clean_line(self, line, command):
349 347 if command in ['+', '-', ' ']:
350 348 #only modify the line if it's actually a diff thing
351 349 line = line[1:]
352 350 return line
353 351
354 352 def _parse_gitdiff(self, inline_diff=True):
355 353 _files = []
356 354 diff_container = lambda arg: arg
357 355
358 356 ##split the diff in chunks of separate --git a/file b/file chunks
359 357 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
360 358 head, diff = self._get_header(raw_diff)
361 359
362 360 op = None
363 361 stats = {
364 362 'added': 0,
365 363 'deleted': 0,
366 364 'binary': False,
367 365 'ops': {},
368 366 }
369 367
370 368 if head['deleted_file_mode']:
371 369 op = 'D'
372 370 stats['binary'] = True
373 371 stats['ops'][DEL_FILENODE] = 'deleted file'
374 372
375 373 elif head['new_file_mode']:
376 374 op = 'A'
377 375 stats['binary'] = True
378 376 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
379 377 else: # modify operation, can be cp, rename, chmod
380 378 # CHMOD
381 379 if head['new_mode'] and head['old_mode']:
382 380 op = 'M'
383 381 stats['binary'] = True
384 382 stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
385 383 % (head['old_mode'], head['new_mode']))
386 384 # RENAME
387 385 if (head['rename_from'] and head['rename_to']
388 386 and head['rename_from'] != head['rename_to']):
389 387 op = 'M'
390 388 stats['binary'] = True
391 389 stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
392 390 % (head['rename_from'], head['rename_to']))
393 391
394 392 # FALL BACK: detect missed old style add or remove
395 393 if op is None:
396 394 if not head['a_file'] and head['b_file']:
397 395 op = 'A'
398 396 stats['binary'] = True
399 397 stats['ops'][NEW_FILENODE] = 'new file'
400 398
401 399 elif head['a_file'] and not head['b_file']:
402 400 op = 'D'
403 401 stats['binary'] = True
404 402 stats['ops'][DEL_FILENODE] = 'deleted file'
405 403
406 404 # it's not ADD not DELETE
407 405 if op is None:
408 406 op = 'M'
409 407 stats['binary'] = True
410 408 stats['ops'][MOD_FILENODE] = 'modified file'
411 409
412 410 # a real non-binary diff
413 411 if head['a_file'] or head['b_file']:
414 412 try:
415 413 chunks, _stats = self._parse_lines(diff)
416 414 stats['binary'] = False
417 415 stats['added'] = _stats[0]
418 416 stats['deleted'] = _stats[1]
419 417 # explicit mark that it's a modified file
420 418 if op == 'M':
421 419 stats['ops'][MOD_FILENODE] = 'modified file'
422 420
423 421 except DiffLimitExceeded:
424 422 diff_container = lambda _diff: \
425 423 LimitedDiffContainer(self.diff_limit,
426 424 self.cur_diff_size, _diff)
427 425 break
428 426 else: # GIT binary patch (or empty diff)
429 427 # GIT Binary patch
430 428 if head['bin_patch']:
431 429 stats['ops'][BIN_FILENODE] = 'binary diff not shown'
432 430 chunks = []
433 431
434 432 chunks.insert(0, [{
435 433 'old_lineno': '',
436 434 'new_lineno': '',
437 435 'action': 'context',
438 436 'line': msg,
439 437 } for _op, msg in stats['ops'].iteritems()
440 438 if _op not in [MOD_FILENODE]])
441 439
442 440 _files.append({
443 441 'filename': head['b_path'],
444 442 'old_revision': head['a_blob_id'],
445 443 'new_revision': head['b_blob_id'],
446 444 'chunks': chunks,
447 445 'operation': op,
448 446 'stats': stats,
449 447 })
450 448
451 449 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
452 450
453 451 if not inline_diff:
454 452 return diff_container(sorted(_files, key=sorter))
455 453
456 454 # highlight inline changes
457 455 for diff_data in _files:
458 456 for chunk in diff_data['chunks']:
459 457 lineiter = iter(chunk)
460 458 try:
461 459 while 1:
462 460 line = lineiter.next()
463 461 if line['action'] not in ['unmod', 'context']:
464 462 nextline = lineiter.next()
465 463 if nextline['action'] in ['unmod', 'context'] or \
466 464 nextline['action'] == line['action']:
467 465 continue
468 466 self.differ(line, nextline)
469 467 except StopIteration:
470 468 pass
471 469
472 470 return diff_container(sorted(_files, key=sorter))
473 471
474 472 def _parse_udiff(self, inline_diff=True):
475 473 raise NotImplementedError()
476 474
477 475 def _parse_lines(self, diff):
478 476 """
479 477 Parse the diff an return data for the template.
480 478 """
481 479
482 480 lineiter = iter(diff)
483 481 stats = [0, 0]
484 482
485 483 try:
486 484 chunks = []
487 485 line = lineiter.next()
488 486
489 487 while line:
490 488 lines = []
491 489 chunks.append(lines)
492 490
493 491 match = self._chunk_re.match(line)
494 492
495 493 if not match:
496 494 break
497 495
498 496 gr = match.groups()
499 497 (old_line, old_end,
500 498 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
501 499 old_line -= 1
502 500 new_line -= 1
503 501
504 502 context = len(gr) == 5
505 503 old_end += old_line
506 504 new_end += new_line
507 505
508 506 if context:
509 507 # skip context only if it's first line
510 508 if int(gr[0]) > 1:
511 509 lines.append({
512 510 'old_lineno': '...',
513 511 'new_lineno': '...',
514 512 'action': 'context',
515 513 'line': line,
516 514 })
517 515
518 516 line = lineiter.next()
519 517
520 518 while old_line < old_end or new_line < new_end:
521 519 command = ' '
522 520 if line:
523 521 command = line[0]
524 522
525 523 affects_old = affects_new = False
526 524
527 525 # ignore those if we don't expect them
528 526 if command in '#@':
529 527 continue
530 528 elif command == '+':
531 529 affects_new = True
532 530 action = 'add'
533 531 stats[0] += 1
534 532 elif command == '-':
535 533 affects_old = True
536 534 action = 'del'
537 535 stats[1] += 1
538 536 else:
539 537 affects_old = affects_new = True
540 538 action = 'unmod'
541 539
542 540 if not self._newline_marker.match(line):
543 541 old_line += affects_old
544 542 new_line += affects_new
545 543 lines.append({
546 544 'old_lineno': affects_old and old_line or '',
547 545 'new_lineno': affects_new and new_line or '',
548 546 'action': action,
549 547 'line': self._clean_line(line, command)
550 548 })
551 549
552 550 line = lineiter.next()
553 551
554 552 if self._newline_marker.match(line):
555 553 # we need to append to lines, since this is not
556 554 # counted in the line specs of diff
557 555 lines.append({
558 556 'old_lineno': '...',
559 557 'new_lineno': '...',
560 558 'action': 'context',
561 559 'line': self._clean_line(line, command)
562 560 })
563 561
564 562 except StopIteration:
565 563 pass
566 564 return chunks, stats
567 565
568 566 def _safe_id(self, idstring):
569 567 """Make a string safe for including in an id attribute.
570 568
571 569 The HTML spec says that id attributes 'must begin with
572 570 a letter ([A-Za-z]) and may be followed by any number
573 571 of letters, digits ([0-9]), hyphens ("-"), underscores
574 572 ("_"), colons (":"), and periods (".")'. These regexps
575 573 are slightly over-zealous, in that they remove colons
576 574 and periods unnecessarily.
577 575
578 576 Whitespace is transformed into underscores, and then
579 577 anything which is not a hyphen or a character that
580 578 matches \w (alphanumerics and underscore) is removed.
581 579
582 580 """
583 581 # Transform all whitespace to underscore
584 582 idstring = re.sub(r'\s', "_", '%s' % idstring)
585 583 # Remove everything that is not a hyphen or a member of \w
586 584 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
587 585 return idstring
588 586
589 587 def prepare(self, inline_diff=True):
590 588 """
591 589 Prepare the passed udiff for HTML rendering. It'l return a list
592 590 of dicts with diff information
593 591 """
594 592 parsed = self._parser(inline_diff=inline_diff)
595 593 self.parsed = True
596 594 self.parsed_diff = parsed
597 595 return parsed
598 596
599 597 def as_raw(self, diff_lines=None):
600 598 """
601 599 Returns raw string diff
602 600 """
603 601 return self._diff
604 602 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
605 603
606 604 def as_html(self, table_class='code-difftable', line_class='line',
607 605 old_lineno_class='lineno old', new_lineno_class='lineno new',
608 606 code_class='code', enable_comments=False, parsed_lines=None):
609 607 """
610 608 Return given diff as html table with customized css classes
611 609 """
612 610 def _link_to_if(condition, label, url):
613 611 """
614 612 Generates a link if condition is meet or just the label if not.
615 613 """
616 614
617 615 if condition:
618 616 return '''<a href="%(url)s">%(label)s</a>''' % {
619 617 'url': url,
620 618 'label': label
621 619 }
622 620 else:
623 621 return label
624 622 if not self.parsed:
625 623 self.prepare()
626 624
627 625 diff_lines = self.parsed_diff
628 626 if parsed_lines:
629 627 diff_lines = parsed_lines
630 628
631 629 _html_empty = True
632 630 _html = []
633 631 _html.append('''<table class="%(table_class)s">\n''' % {
634 632 'table_class': table_class
635 633 })
636 634
637 635 for diff in diff_lines:
638 636 for line in diff['chunks']:
639 637 _html_empty = False
640 638 for change in line:
641 639 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
642 640 'lc': line_class,
643 641 'action': change['action']
644 642 })
645 643 anchor_old_id = ''
646 644 anchor_new_id = ''
647 645 anchor_old = "%(filename)s_o%(oldline_no)s" % {
648 646 'filename': self._safe_id(diff['filename']),
649 647 'oldline_no': change['old_lineno']
650 648 }
651 649 anchor_new = "%(filename)s_n%(oldline_no)s" % {
652 650 'filename': self._safe_id(diff['filename']),
653 651 'oldline_no': change['new_lineno']
654 652 }
655 653 cond_old = (change['old_lineno'] != '...' and
656 654 change['old_lineno'])
657 655 cond_new = (change['new_lineno'] != '...' and
658 656 change['new_lineno'])
659 657 if cond_old:
660 658 anchor_old_id = 'id="%s"' % anchor_old
661 659 if cond_new:
662 660 anchor_new_id = 'id="%s"' % anchor_new
663 661 ###########################################################
664 662 # OLD LINE NUMBER
665 663 ###########################################################
666 664 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
667 665 'a_id': anchor_old_id,
668 666 'olc': old_lineno_class
669 667 })
670 668
671 669 _html.append('''%(link)s''' % {
672 670 'link': _link_to_if(True, change['old_lineno'],
673 671 '#%s' % anchor_old)
674 672 })
675 673 _html.append('''</td>\n''')
676 674 ###########################################################
677 675 # NEW LINE NUMBER
678 676 ###########################################################
679 677
680 678 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
681 679 'a_id': anchor_new_id,
682 680 'nlc': new_lineno_class
683 681 })
684 682
685 683 _html.append('''%(link)s''' % {
686 684 'link': _link_to_if(True, change['new_lineno'],
687 685 '#%s' % anchor_new)
688 686 })
689 687 _html.append('''</td>\n''')
690 688 ###########################################################
691 689 # CODE
692 690 ###########################################################
693 691 comments = '' if enable_comments else 'no-comment'
694 692 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
695 693 'cc': code_class,
696 694 'inc': comments
697 695 })
698 696 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
699 697 'code': change['line']
700 698 })
701 699
702 700 _html.append('''\t</td>''')
703 701 _html.append('''\n</tr>\n''')
704 702 _html.append('''</table>''')
705 703 if _html_empty:
706 704 return None
707 705 return ''.join(_html)
708 706
709 707 def stat(self):
710 708 """
711 709 Returns tuple of added, and removed lines for this instance
712 710 """
713 711 return self.adds, self.removes
@@ -1,84 +1,88 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.exceptions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of custom exceptions used in RhodeCode
7 7
8 8 :created_on: Nov 17, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 from webob.exc import HTTPClientError
27 27
28 28
29 29 class LdapUsernameError(Exception):
30 30 pass
31 31
32 32
33 33 class LdapPasswordError(Exception):
34 34 pass
35 35
36 36
37 37 class LdapConnectionError(Exception):
38 38 pass
39 39
40 40
41 41 class LdapImportError(Exception):
42 42 pass
43 43
44 44
45 45 class DefaultUserException(Exception):
46 46 pass
47 47
48 48
49 49 class UserOwnsReposException(Exception):
50 50 pass
51 51
52 52
53 53 class UserGroupsAssignedException(Exception):
54 54 pass
55 55
56 56
57 57 class StatusChangeOnClosedPullRequestError(Exception):
58 58 pass
59 59
60 60
61 61 class AttachedForksError(Exception):
62 62 pass
63 63
64 64
65 65 class RepoGroupAssignmentError(Exception):
66 66 pass
67 67
68 68
69 class NonRelativePathError(Exception):
70 pass
71
72
69 73 class HTTPLockedRC(HTTPClientError):
70 74 """
71 75 Special Exception For locked Repos in RhodeCode, the return code can
72 76 be overwritten by _code keyword argument passed into constructors
73 77 """
74 78 code = 423
75 79 title = explanation = 'Repository Locked'
76 80
77 81 def __init__(self, reponame, username, *args, **kwargs):
78 82 from rhodecode import CONFIG
79 83 from rhodecode.lib.utils2 import safe_int
80 84 _code = CONFIG.get('lock_ret_code')
81 85 self.code = safe_int(_code, self.code)
82 86 self.title = self.explanation = ('Repository `%s` locked by '
83 87 'user `%s`' % (reponame, username))
84 88 super(HTTPLockedRC, self).__init__(*args, **kwargs)
@@ -1,390 +1,387 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.hooks
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Hooks runned by rhodecode
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27 import time
28 28 import binascii
29 29 import traceback
30 30 from inspect import isfunction
31 31
32 32 from mercurial.scmutil import revrange
33 33 from mercurial.node import nullrev
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils import action_logger
37 37 from rhodecode.lib.vcs.backends.base import EmptyChangeset
38 38 from rhodecode.lib.compat import json
39 39 from rhodecode.lib.exceptions import HTTPLockedRC
40 40 from rhodecode.lib.utils2 import safe_str, _extract_extras
41 41 from rhodecode.model.db import Repository, User
42 42
43 43
44 44 def _get_scm_size(alias, root_path):
45 45
46 46 if not alias.startswith('.'):
47 47 alias += '.'
48 48
49 49 size_scm, size_root = 0, 0
50 50 for path, dirs, files in os.walk(safe_str(root_path)):
51 51 if path.find(alias) != -1:
52 52 for f in files:
53 53 try:
54 54 size_scm += os.path.getsize(os.path.join(path, f))
55 55 except OSError:
56 56 pass
57 57 else:
58 58 for f in files:
59 59 try:
60 60 size_root += os.path.getsize(os.path.join(path, f))
61 61 except OSError:
62 62 pass
63 63
64 64 size_scm_f = h.format_byte_size(size_scm)
65 65 size_root_f = h.format_byte_size(size_root)
66 66 size_total_f = h.format_byte_size(size_root + size_scm)
67 67
68 68 return size_scm_f, size_root_f, size_total_f
69 69
70 70
71 71 def repo_size(ui, repo, hooktype=None, **kwargs):
72 72 """
73 73 Presents size of repository after push
74 74
75 75 :param ui:
76 76 :param repo:
77 77 :param hooktype:
78 78 """
79 79
80 80 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
81 81
82 82 last_cs = repo[len(repo) - 1]
83 83
84 84 msg = ('Repository size .hg:%s repo:%s total:%s\n'
85 85 'Last revision is now r%s:%s\n') % (
86 86 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
87 87 )
88 88
89 89 sys.stdout.write(msg)
90 90
91 91
92 92 def pre_push(ui, repo, **kwargs):
93 93 # pre push function, currently used to ban pushing when
94 94 # repository is locked
95 95 ex = _extract_extras()
96 96
97 97 usr = User.get_by_username(ex.username)
98 98 if ex.locked_by[0] and usr.user_id != int(ex.locked_by[0]):
99 99 locked_by = User.get(ex.locked_by[0]).username
100 100 # this exception is interpreted in git/hg middlewares and based
101 101 # on that proper return code is server to client
102 102 _http_ret = HTTPLockedRC(ex.repository, locked_by)
103 103 if str(_http_ret.code).startswith('2'):
104 104 #2xx Codes don't raise exceptions
105 105 sys.stdout.write(_http_ret.title)
106 106 else:
107 107 raise _http_ret
108 108
109 109
110 110 def pre_pull(ui, repo, **kwargs):
111 111 # pre push function, currently used to ban pushing when
112 112 # repository is locked
113 113 ex = _extract_extras()
114 114 if ex.locked_by[0]:
115 115 locked_by = User.get(ex.locked_by[0]).username
116 116 # this exception is interpreted in git/hg middlewares and based
117 117 # on that proper return code is server to client
118 118 _http_ret = HTTPLockedRC(ex.repository, locked_by)
119 119 if str(_http_ret.code).startswith('2'):
120 120 #2xx Codes don't raise exceptions
121 121 sys.stdout.write(_http_ret.title)
122 122 else:
123 123 raise _http_ret
124 124
125 125
126 126 def log_pull_action(ui, repo, **kwargs):
127 127 """
128 128 Logs user last pull action
129 129
130 130 :param ui:
131 131 :param repo:
132 132 """
133 133 ex = _extract_extras()
134 134
135 135 user = User.get_by_username(ex.username)
136 136 action = 'pull'
137 137 action_logger(user, action, ex.repository, ex.ip, commit=True)
138 138 # extension hook call
139 139 from rhodecode import EXTENSIONS
140 140 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
141 141 if isfunction(callback):
142 142 kw = {}
143 143 kw.update(ex)
144 144 callback(**kw)
145 145
146 146 if ex.make_lock is not None and ex.make_lock:
147 147 Repository.lock(Repository.get_by_repo_name(ex.repository), user.user_id)
148 148 #msg = 'Made lock on repo `%s`' % repository
149 149 #sys.stdout.write(msg)
150 150
151 151 if ex.locked_by[0]:
152 152 locked_by = User.get(ex.locked_by[0]).username
153 153 _http_ret = HTTPLockedRC(ex.repository, locked_by)
154 154 if str(_http_ret.code).startswith('2'):
155 155 #2xx Codes don't raise exceptions
156 156 sys.stdout.write(_http_ret.title)
157 157 return 0
158 158
159 159
160 160 def log_push_action(ui, repo, **kwargs):
161 161 """
162 162 Maps user last push action to new changeset id, from mercurial
163 163
164 164 :param ui:
165 165 :param repo: repo object containing the `ui` object
166 166 """
167 167
168 168 ex = _extract_extras()
169 169
170 170 action = ex.action + ':%s'
171 171
172 172 if ex.scm == 'hg':
173 173 node = kwargs['node']
174 174
175 175 def get_revs(repo, rev_opt):
176 176 if rev_opt:
177 177 revs = revrange(repo, rev_opt)
178 178
179 179 if len(revs) == 0:
180 180 return (nullrev, nullrev)
181 181 return (max(revs), min(revs))
182 182 else:
183 183 return (len(repo) - 1, 0)
184 184
185 185 stop, start = get_revs(repo, [node + ':'])
186 186 h = binascii.hexlify
187 187 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
188 188 elif ex.scm == 'git':
189 189 revs = kwargs.get('_git_revs', [])
190 190 if '_git_revs' in kwargs:
191 191 kwargs.pop('_git_revs')
192 192
193 193 action = action % ','.join(revs)
194 194
195 195 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
196 196
197 197 # extension hook call
198 198 from rhodecode import EXTENSIONS
199 199 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
200 200 if isfunction(callback):
201 201 kw = {'pushed_revs': revs}
202 202 kw.update(ex)
203 203 callback(**kw)
204 204
205 205 if ex.make_lock is not None and not ex.make_lock:
206 206 Repository.unlock(Repository.get_by_repo_name(ex.repository))
207 207 msg = 'Released lock on repo `%s`\n' % ex.repository
208 208 sys.stdout.write(msg)
209 209
210 210 if ex.locked_by[0]:
211 211 locked_by = User.get(ex.locked_by[0]).username
212 212 _http_ret = HTTPLockedRC(ex.repository, locked_by)
213 213 if str(_http_ret.code).startswith('2'):
214 214 #2xx Codes don't raise exceptions
215 215 sys.stdout.write(_http_ret.title)
216 216
217 217 return 0
218 218
219 219
220 220 def log_create_repository(repository_dict, created_by, **kwargs):
221 221 """
222 222 Post create repository Hook. This is a dummy function for admins to re-use
223 223 if needed. It's taken from rhodecode-extensions module and executed
224 224 if present
225 225
226 226 :param repository: dict dump of repository object
227 227 :param created_by: username who created repository
228 228
229 229 available keys of repository_dict:
230 230
231 231 'repo_type',
232 232 'description',
233 233 'private',
234 234 'created_on',
235 235 'enable_downloads',
236 236 'repo_id',
237 237 'user_id',
238 238 'enable_statistics',
239 239 'clone_uri',
240 240 'fork_id',
241 241 'group_id',
242 242 'repo_name'
243 243
244 244 """
245 245 from rhodecode import EXTENSIONS
246 246 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
247 247 if isfunction(callback):
248 248 kw = {}
249 249 kw.update(repository_dict)
250 250 kw.update({'created_by': created_by})
251 251 kw.update(kwargs)
252 252 return callback(**kw)
253 253
254 254 return 0
255 255
256 256
257 257 def log_delete_repository(repository_dict, deleted_by, **kwargs):
258 258 """
259 259 Post delete repository Hook. This is a dummy function for admins to re-use
260 260 if needed. It's taken from rhodecode-extensions module and executed
261 261 if present
262 262
263 263 :param repository: dict dump of repository object
264 264 :param deleted_by: username who deleted the repository
265 265
266 266 available keys of repository_dict:
267 267
268 268 'repo_type',
269 269 'description',
270 270 'private',
271 271 'created_on',
272 272 'enable_downloads',
273 273 'repo_id',
274 274 'user_id',
275 275 'enable_statistics',
276 276 'clone_uri',
277 277 'fork_id',
278 278 'group_id',
279 279 'repo_name'
280 280
281 281 """
282 282 from rhodecode import EXTENSIONS
283 283 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
284 284 if isfunction(callback):
285 285 kw = {}
286 286 kw.update(repository_dict)
287 287 kw.update({'deleted_by': deleted_by,
288 288 'deleted_on': time.time()})
289 289 kw.update(kwargs)
290 290 return callback(**kw)
291 291
292 292 return 0
293 293
294 294
295 295 handle_git_pre_receive = (lambda repo_path, revs, env:
296 296 handle_git_receive(repo_path, revs, env, hook_type='pre'))
297 297 handle_git_post_receive = (lambda repo_path, revs, env:
298 298 handle_git_receive(repo_path, revs, env, hook_type='post'))
299 299
300 300
301 301 def handle_git_receive(repo_path, revs, env, hook_type='post'):
302 302 """
303 303 A really hacky method that is runned by git post-receive hook and logs
304 304 an push action together with pushed revisions. It's executed by subprocess
305 305 thus needs all info to be able to create a on the fly pylons enviroment,
306 306 connect to database and run the logging code. Hacky as sh*t but works.
307 307
308 308 :param repo_path:
309 :type repo_path:
310 309 :param revs:
311 :type revs:
312 310 :param env:
313 :type env:
314 311 """
315 312 from paste.deploy import appconfig
316 313 from sqlalchemy import engine_from_config
317 314 from rhodecode.config.environment import load_environment
318 315 from rhodecode.model import init_model
319 316 from rhodecode.model.db import RhodeCodeUi
320 317 from rhodecode.lib.utils import make_ui
321 318 extras = _extract_extras(env)
322 319
323 320 path, ini_name = os.path.split(extras['config'])
324 321 conf = appconfig('config:%s' % ini_name, relative_to=path)
325 322 load_environment(conf.global_conf, conf.local_conf)
326 323
327 324 engine = engine_from_config(conf, 'sqlalchemy.db1.')
328 325 init_model(engine)
329 326
330 327 baseui = make_ui('db')
331 328 # fix if it's not a bare repo
332 329 if repo_path.endswith(os.sep + '.git'):
333 330 repo_path = repo_path[:-5]
334 331
335 332 repo = Repository.get_by_full_path(repo_path)
336 333 if not repo:
337 334 raise OSError('Repository %s not found in database'
338 335 % (safe_str(repo_path)))
339 336
340 337 _hooks = dict(baseui.configitems('hooks')) or {}
341 338
342 339 if hook_type == 'pre':
343 340 repo = repo.scm_instance
344 341 else:
345 342 #post push shouldn't use the cached instance never
346 343 repo = repo.scm_instance_no_cache()
347 344
348 345 if hook_type == 'pre':
349 346 pre_push(baseui, repo)
350 347
351 348 # if push hook is enabled via web interface
352 349 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
353 350
354 351 rev_data = []
355 352 for l in revs:
356 353 old_rev, new_rev, ref = l.split(' ')
357 354 _ref_data = ref.split('/')
358 355 if _ref_data[1] in ['tags', 'heads']:
359 356 rev_data.append({'old_rev': old_rev,
360 357 'new_rev': new_rev,
361 358 'ref': ref,
362 359 'type': _ref_data[1],
363 360 'name': _ref_data[2].strip()})
364 361
365 362 git_revs = []
366 363 for push_ref in rev_data:
367 364 _type = push_ref['type']
368 365 if _type == 'heads':
369 366 if push_ref['old_rev'] == EmptyChangeset().raw_id:
370 367 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
371 368 heads = repo.run_git_command(cmd)[0]
372 369 heads = heads.replace(push_ref['ref'], '')
373 370 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
374 371 heads.splitlines()))
375 372 cmd = (('log %(new_rev)s' % push_ref) +
376 373 ' --reverse --pretty=format:"%H" --not ' + heads)
377 374 git_revs += repo.run_git_command(cmd)[0].splitlines()
378 375
379 376 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
380 377 #delete branch case
381 378 git_revs += ['delete_branch=>%s' % push_ref['name']]
382 379 else:
383 380 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
384 381 ' --reverse --pretty=format:"%H"')
385 382 git_revs += repo.run_git_command(cmd)[0].splitlines()
386 383
387 384 elif _type == 'tags':
388 385 git_revs += ['tag=>%s' % push_ref['name']]
389 386
390 387 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,201 +1,200 b''
1 1 import os
2 2 import socket
3 3 import logging
4 4 import subprocess
5 5 import traceback
6 6
7 7 from webob import Request, Response, exc
8 8
9 9 import rhodecode
10 10 from rhodecode.lib.vcs import subprocessio
11 11
12 12 log = logging.getLogger(__name__)
13 13
14 14
15 15 class FileWrapper(object):
16 16
17 17 def __init__(self, fd, content_length):
18 18 self.fd = fd
19 19 self.content_length = content_length
20 20 self.remain = content_length
21 21
22 22 def read(self, size):
23 23 if size <= self.remain:
24 24 try:
25 25 data = self.fd.read(size)
26 26 except socket.error:
27 27 raise IOError(self)
28 28 self.remain -= size
29 29 elif self.remain:
30 30 data = self.fd.read(self.remain)
31 31 self.remain = 0
32 32 else:
33 33 data = None
34 34 return data
35 35
36 36 def __repr__(self):
37 37 return '<FileWrapper %s len: %s, read: %s>' % (
38 38 self.fd, self.content_length, self.content_length - self.remain
39 39 )
40 40
41 41
42 42 class GitRepository(object):
43 43 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
44 44 commands = ['git-upload-pack', 'git-receive-pack']
45 45
46 46 def __init__(self, repo_name, content_path, extras):
47 47 files = set([f.lower() for f in os.listdir(content_path)])
48 48 if not (self.git_folder_signature.intersection(files)
49 49 == self.git_folder_signature):
50 50 raise OSError('%s missing git signature' % content_path)
51 51 self.content_path = content_path
52 52 self.valid_accepts = ['application/x-%s-result' %
53 53 c for c in self.commands]
54 54 self.repo_name = repo_name
55 55 self.extras = extras
56 56
57 57 def _get_fixedpath(self, path):
58 58 """
59 59 Small fix for repo_path
60 60
61 61 :param path:
62 :type path:
63 62 """
64 63 return path.split(self.repo_name, 1)[-1].strip('/')
65 64
66 65 def inforefs(self, request, environ):
67 66 """
68 67 WSGI Response producer for HTTP GET Git Smart
69 68 HTTP /info/refs request.
70 69 """
71 70
72 71 git_command = request.GET.get('service')
73 72 if git_command not in self.commands:
74 73 log.debug('command %s not allowed' % git_command)
75 74 return exc.HTTPMethodNotAllowed()
76 75
77 76 # note to self:
78 77 # please, resist the urge to add '\n' to git capture and increment
79 78 # line count by 1.
80 79 # The code in Git client not only does NOT need '\n', but actually
81 80 # blows up if you sprinkle "flush" (0000) as "0001\n".
82 81 # It reads binary, per number of bytes specified.
83 82 # if you do add '\n' as part of data, count it.
84 83 server_advert = '# service=%s' % git_command
85 84 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
86 85 _git_path = rhodecode.CONFIG.get('git_path', 'git')
87 86 try:
88 87 out = subprocessio.SubprocessIOChunker(
89 88 r'%s %s --stateless-rpc --advertise-refs "%s"' % (
90 89 _git_path, git_command[4:], self.content_path),
91 90 starting_values=[
92 91 packet_len + server_advert + '0000'
93 92 ]
94 93 )
95 94 except EnvironmentError, e:
96 95 log.error(traceback.format_exc())
97 96 raise exc.HTTPExpectationFailed()
98 97 resp = Response()
99 98 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
100 99 resp.charset = None
101 100 resp.app_iter = out
102 101 return resp
103 102
104 103 def backend(self, request, environ):
105 104 """
106 105 WSGI Response producer for HTTP POST Git Smart HTTP requests.
107 106 Reads commands and data from HTTP POST's body.
108 107 returns an iterator obj with contents of git command's
109 108 response to stdout
110 109 """
111 110 git_command = self._get_fixedpath(request.path_info)
112 111 if git_command not in self.commands:
113 112 log.debug('command %s not allowed' % git_command)
114 113 return exc.HTTPMethodNotAllowed()
115 114
116 115 if 'CONTENT_LENGTH' in environ:
117 116 inputstream = FileWrapper(environ['wsgi.input'],
118 117 request.content_length)
119 118 else:
120 119 inputstream = environ['wsgi.input']
121 120
122 121 try:
123 122 gitenv = os.environ
124 123 # forget all configs
125 124 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
126 125 opts = dict(
127 126 env=gitenv,
128 127 cwd=os.getcwd()
129 128 )
130 129 cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
131 130 self.content_path),
132 131 log.debug('handling cmd %s' % cmd)
133 132 out = subprocessio.SubprocessIOChunker(
134 133 cmd,
135 134 inputstream=inputstream,
136 135 **opts
137 136 )
138 137 except EnvironmentError, e:
139 138 log.error(traceback.format_exc())
140 139 raise exc.HTTPExpectationFailed()
141 140
142 141 if git_command in [u'git-receive-pack']:
143 142 # updating refs manually after each push.
144 143 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
145 144 _git_path = rhodecode.CONFIG.get('git_path', 'git')
146 145 cmd = (u'%s --git-dir "%s" '
147 146 'update-server-info' % (_git_path, self.content_path))
148 147 log.debug('handling cmd %s' % cmd)
149 148 subprocess.call(cmd, shell=True)
150 149
151 150 resp = Response()
152 151 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
153 152 resp.charset = None
154 153 resp.app_iter = out
155 154 return resp
156 155
157 156 def __call__(self, environ, start_response):
158 157 request = Request(environ)
159 158 _path = self._get_fixedpath(request.path_info)
160 159 if _path.startswith('info/refs'):
161 160 app = self.inforefs
162 161 elif [a for a in self.valid_accepts if a in request.accept]:
163 162 app = self.backend
164 163 try:
165 164 resp = app(request, environ)
166 165 except exc.HTTPException, e:
167 166 resp = e
168 167 log.error(traceback.format_exc())
169 168 except Exception, e:
170 169 log.error(traceback.format_exc())
171 170 resp = exc.HTTPInternalServerError()
172 171 return resp(environ, start_response)
173 172
174 173
175 174 class GitDirectory(object):
176 175
177 176 def __init__(self, repo_root, repo_name, extras):
178 177 repo_location = os.path.join(repo_root, repo_name)
179 178 if not os.path.isdir(repo_location):
180 179 raise OSError(repo_location)
181 180
182 181 self.content_path = repo_location
183 182 self.repo_name = repo_name
184 183 self.repo_location = repo_location
185 184 self.extras = extras
186 185
187 186 def __call__(self, environ, start_response):
188 187 content_path = self.content_path
189 188 try:
190 189 app = GitRepository(self.repo_name, content_path, self.extras)
191 190 except (AssertionError, OSError):
192 191 content_path = os.path.join(content_path, '.git')
193 192 if os.path.isdir(content_path):
194 193 app = GitRepository(self.repo_name, content_path, self.extras)
195 194 else:
196 195 return exc.HTTPNotFound()(environ, start_response)
197 196 return app(environ, start_response)
198 197
199 198
200 199 def make_wsgi_app(repo_name, repo_root, extras):
201 200 return GitDirectory(repo_root, repo_name, extras)
@@ -1,609 +1,646 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import sys
29 29 import time
30 import uuid
30 31 import datetime
31 32 import traceback
32 33 import webob
33 34
34 35 from pylons.i18n.translation import _, ungettext
35 36 from rhodecode.lib.vcs.utils.lazy import LazyProperty
36 37 from rhodecode.lib.compat import json
37 38
38 39
39 40 def __get_lem():
40 41 """
41 42 Get language extension map based on what's inside pygments lexers
42 43 """
43 44 from pygments import lexers
44 45 from string import lower
45 46 from collections import defaultdict
46 47
47 48 d = defaultdict(lambda: [])
48 49
49 50 def __clean(s):
50 51 s = s.lstrip('*')
51 52 s = s.lstrip('.')
52 53
53 54 if s.find('[') != -1:
54 55 exts = []
55 56 start, stop = s.find('['), s.find(']')
56 57
57 58 for suffix in s[start + 1:stop]:
58 59 exts.append(s[:s.find('[')] + suffix)
59 60 return map(lower, exts)
60 61 else:
61 62 return map(lower, [s])
62 63
63 64 for lx, t in sorted(lexers.LEXERS.items()):
64 65 m = map(__clean, t[-2])
65 66 if m:
66 67 m = reduce(lambda x, y: x + y, m)
67 68 for ext in m:
68 69 desc = lx.replace('Lexer', '')
69 70 d[ext].append(desc)
70 71
71 72 return dict(d)
72 73
73 74
74 75 def str2bool(_str):
75 76 """
76 77 returs True/False value from given string, it tries to translate the
77 78 string into boolean
78 79
79 80 :param _str: string value to translate into boolean
80 81 :rtype: boolean
81 82 :returns: boolean from given string
82 83 """
83 84 if _str is None:
84 85 return False
85 86 if _str in (True, False):
86 87 return _str
87 88 _str = str(_str).strip().lower()
88 89 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
89 90
90 91
91 92 def aslist(obj, sep=None, strip=True):
92 93 """
93 94 Returns given string separated by sep as list
94 95
95 96 :param obj:
96 97 :param sep:
97 98 :param strip:
98 99 """
99 100 if isinstance(obj, (basestring)):
100 101 lst = obj.split(sep)
101 102 if strip:
102 103 lst = [v.strip() for v in lst]
103 104 return lst
104 105 elif isinstance(obj, (list, tuple)):
105 106 return obj
106 107 elif obj is None:
107 108 return []
108 109 else:
109 110 return [obj]
110 111
111 112
112 113 def convert_line_endings(line, mode):
113 114 """
114 115 Converts a given line "line end" accordingly to given mode
115 116
116 117 Available modes are::
117 118 0 - Unix
118 119 1 - Mac
119 120 2 - DOS
120 121
121 122 :param line: given line to convert
122 123 :param mode: mode to convert to
123 124 :rtype: str
124 125 :return: converted line according to mode
125 126 """
126 127 from string import replace
127 128
128 129 if mode == 0:
129 130 line = replace(line, '\r\n', '\n')
130 131 line = replace(line, '\r', '\n')
131 132 elif mode == 1:
132 133 line = replace(line, '\r\n', '\r')
133 134 line = replace(line, '\n', '\r')
134 135 elif mode == 2:
135 136 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
136 137 return line
137 138
138 139
139 140 def detect_mode(line, default):
140 141 """
141 142 Detects line break for given line, if line break couldn't be found
142 143 given default value is returned
143 144
144 145 :param line: str line
145 146 :param default: default
146 147 :rtype: int
147 148 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
148 149 """
149 150 if line.endswith('\r\n'):
150 151 return 2
151 152 elif line.endswith('\n'):
152 153 return 0
153 154 elif line.endswith('\r'):
154 155 return 1
155 156 else:
156 157 return default
157 158
158 159
159 160 def generate_api_key(username, salt=None):
160 161 """
161 162 Generates unique API key for given username, if salt is not given
162 163 it'll be generated from some random string
163 164
164 165 :param username: username as string
165 166 :param salt: salt to hash generate KEY
166 167 :rtype: str
167 168 :returns: sha1 hash from username+salt
168 169 """
169 170 from tempfile import _RandomNameSequence
170 171 import hashlib
171 172
172 173 if salt is None:
173 174 salt = _RandomNameSequence().next()
174 175
175 176 return hashlib.sha1(username + salt).hexdigest()
176 177
177 178
178 179 def safe_int(val, default=None):
179 180 """
180 181 Returns int() of val if val is not convertable to int use default
181 182 instead
182 183
183 184 :param val:
184 185 :param default:
185 186 """
186 187
187 188 try:
188 189 val = int(val)
189 190 except (ValueError, TypeError):
190 191 val = default
191 192
192 193 return val
193 194
194 195
195 196 def safe_unicode(str_, from_encoding=None):
196 197 """
197 198 safe unicode function. Does few trick to turn str_ into unicode
198 199
199 200 In case of UnicodeDecode error we try to return it with encoding detected
200 201 by chardet library if it fails fallback to unicode with errors replaced
201 202
202 203 :param str_: string to decode
203 204 :rtype: unicode
204 205 :returns: unicode object
205 206 """
206 207 if isinstance(str_, unicode):
207 208 return str_
208 209
209 210 if not from_encoding:
210 211 import rhodecode
211 212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
212 213 'utf8'), sep=',')
213 214 from_encoding = DEFAULT_ENCODINGS
214 215
215 216 if not isinstance(from_encoding, (list, tuple)):
216 217 from_encoding = [from_encoding]
217 218
218 219 try:
219 220 return unicode(str_)
220 221 except UnicodeDecodeError:
221 222 pass
222 223
223 224 for enc in from_encoding:
224 225 try:
225 226 return unicode(str_, enc)
226 227 except UnicodeDecodeError:
227 228 pass
228 229
229 230 try:
230 231 import chardet
231 232 encoding = chardet.detect(str_)['encoding']
232 233 if encoding is None:
233 234 raise Exception()
234 235 return str_.decode(encoding)
235 236 except (ImportError, UnicodeDecodeError, Exception):
236 237 return unicode(str_, from_encoding[0], 'replace')
237 238
238 239
239 240 def safe_str(unicode_, to_encoding=None):
240 241 """
241 242 safe str function. Does few trick to turn unicode_ into string
242 243
243 244 In case of UnicodeEncodeError we try to return it with encoding detected
244 245 by chardet library if it fails fallback to string with errors replaced
245 246
246 247 :param unicode_: unicode to encode
247 248 :rtype: str
248 249 :returns: str object
249 250 """
250 251
251 252 # if it's not basestr cast to str
252 253 if not isinstance(unicode_, basestring):
253 254 return str(unicode_)
254 255
255 256 if isinstance(unicode_, str):
256 257 return unicode_
257 258
258 259 if not to_encoding:
259 260 import rhodecode
260 261 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 262 'utf8'), sep=',')
262 263 to_encoding = DEFAULT_ENCODINGS
263 264
264 265 if not isinstance(to_encoding, (list, tuple)):
265 266 to_encoding = [to_encoding]
266 267
267 268 for enc in to_encoding:
268 269 try:
269 270 return unicode_.encode(enc)
270 271 except UnicodeEncodeError:
271 272 pass
272 273
273 274 try:
274 275 import chardet
275 276 encoding = chardet.detect(unicode_)['encoding']
276 277 if encoding is None:
277 278 raise UnicodeEncodeError()
278 279
279 280 return unicode_.encode(encoding)
280 281 except (ImportError, UnicodeEncodeError):
281 282 return unicode_.encode(to_encoding[0], 'replace')
282 283
283 284 return safe_str
284 285
285 286
286 287 def remove_suffix(s, suffix):
287 288 if s.endswith(suffix):
288 289 s = s[:-1 * len(suffix)]
289 290 return s
290 291
291 292
292 293 def remove_prefix(s, prefix):
293 294 if s.startswith(prefix):
294 295 s = s[len(prefix):]
295 296 return s
296 297
297 298
298 299 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
299 300 """
300 301 Custom engine_from_config functions that makes sure we use NullPool for
301 302 file based sqlite databases. This prevents errors on sqlite. This only
302 303 applies to sqlalchemy versions < 0.7.0
303 304
304 305 """
305 306 import sqlalchemy
306 307 from sqlalchemy import engine_from_config as efc
307 308 import logging
308 309
309 310 if int(sqlalchemy.__version__.split('.')[1]) < 7:
310 311
311 312 # This solution should work for sqlalchemy < 0.7.0, and should use
312 313 # proxy=TimerProxy() for execution time profiling
313 314
314 315 from sqlalchemy.pool import NullPool
315 316 url = configuration[prefix + 'url']
316 317
317 318 if url.startswith('sqlite'):
318 319 kwargs.update({'poolclass': NullPool})
319 320 return efc(configuration, prefix, **kwargs)
320 321 else:
321 322 import time
322 323 from sqlalchemy import event
323 324 from sqlalchemy.engine import Engine
324 325
325 326 log = logging.getLogger('sqlalchemy.engine')
326 327 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
327 328 engine = efc(configuration, prefix, **kwargs)
328 329
329 330 def color_sql(sql):
330 331 COLOR_SEQ = "\033[1;%dm"
331 332 COLOR_SQL = YELLOW
332 333 normal = '\x1b[0m'
333 334 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
334 335
335 336 if configuration['debug']:
336 337 #attach events only for debug configuration
337 338
338 339 def before_cursor_execute(conn, cursor, statement,
339 340 parameters, context, executemany):
340 341 context._query_start_time = time.time()
341 342 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
342 343
343 344 def after_cursor_execute(conn, cursor, statement,
344 345 parameters, context, executemany):
345 346 total = time.time() - context._query_start_time
346 347 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
347 348
348 349 event.listen(engine, "before_cursor_execute",
349 350 before_cursor_execute)
350 351 event.listen(engine, "after_cursor_execute",
351 352 after_cursor_execute)
352 353
353 354 return engine
354 355
355 356
356 357 def age(prevdate, show_short_version=False, now=None):
357 358 """
358 359 turns a datetime into an age string.
359 360 If show_short_version is True, then it will generate a not so accurate but shorter string,
360 361 example: 2days ago, instead of 2 days and 23 hours ago.
361 362
362 363 :param prevdate: datetime object
363 364 :param show_short_version: if it should aproximate the date and return a shorter string
364 365 :rtype: unicode
365 366 :returns: unicode words describing age
366 367 """
367 368 now = now or datetime.datetime.now()
368 369 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
369 370 deltas = {}
370 371 future = False
371 372
372 373 if prevdate > now:
373 374 now, prevdate = prevdate, now
374 375 future = True
375 376 if future:
376 377 prevdate = prevdate.replace(microsecond=0)
377 378 # Get date parts deltas
378 379 from dateutil import relativedelta
379 380 for part in order:
380 381 d = relativedelta.relativedelta(now, prevdate)
381 382 deltas[part] = getattr(d, part + 's')
382 383
383 384 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
384 385 # not 1 hour, -59 minutes and -59 seconds)
385 386 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
386 387 part = order[num]
387 388 carry_part = order[num - 1]
388 389
389 390 if deltas[part] < 0:
390 391 deltas[part] += length
391 392 deltas[carry_part] -= 1
392 393
393 394 # Same thing for days except that the increment depends on the (variable)
394 395 # number of days in the month
395 396 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
396 397 if deltas['day'] < 0:
397 398 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
398 399 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
399 400 deltas['day'] += 29
400 401 else:
401 402 deltas['day'] += month_lengths[prevdate.month - 1]
402 403
403 404 deltas['month'] -= 1
404 405
405 406 if deltas['month'] < 0:
406 407 deltas['month'] += 12
407 408 deltas['year'] -= 1
408 409
409 410 # Format the result
410 411 fmt_funcs = {
411 412 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
412 413 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
413 414 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
414 415 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
415 416 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
416 417 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
417 418 }
418 419
419 420 for i, part in enumerate(order):
420 421 value = deltas[part]
421 422 if value == 0:
422 423 continue
423 424
424 425 if i < 5:
425 426 sub_part = order[i + 1]
426 427 sub_value = deltas[sub_part]
427 428 else:
428 429 sub_value = 0
429 430
430 431 if sub_value == 0 or show_short_version:
431 432 if future:
432 433 return _(u'in %s') % fmt_funcs[part](value)
433 434 else:
434 435 return _(u'%s ago') % fmt_funcs[part](value)
435 436 if future:
436 437 return _(u'in %s and %s') % (fmt_funcs[part](value),
437 438 fmt_funcs[sub_part](sub_value))
438 439 else:
439 440 return _(u'%s and %s ago') % (fmt_funcs[part](value),
440 441 fmt_funcs[sub_part](sub_value))
441 442
442 443 return _(u'just now')
443 444
444 445
445 446 def uri_filter(uri):
446 447 """
447 448 Removes user:password from given url string
448 449
449 450 :param uri:
450 451 :rtype: unicode
451 452 :returns: filtered list of strings
452 453 """
453 454 if not uri:
454 455 return ''
455 456
456 457 proto = ''
457 458
458 459 for pat in ('https://', 'http://'):
459 460 if uri.startswith(pat):
460 461 uri = uri[len(pat):]
461 462 proto = pat
462 463 break
463 464
464 465 # remove passwords and username
465 466 uri = uri[uri.find('@') + 1:]
466 467
467 468 # get the port
468 469 cred_pos = uri.find(':')
469 470 if cred_pos == -1:
470 471 host, port = uri, None
471 472 else:
472 473 host, port = uri[:cred_pos], uri[cred_pos + 1:]
473 474
474 475 return filter(None, [proto, host, port])
475 476
476 477
477 478 def credentials_filter(uri):
478 479 """
479 480 Returns a url with removed credentials
480 481
481 482 :param uri:
482 483 """
483 484
484 485 uri = uri_filter(uri)
485 486 #check if we have port
486 487 if len(uri) > 2 and uri[2]:
487 488 uri[2] = ':' + uri[2]
488 489
489 490 return ''.join(uri)
490 491
491 492
492 493 def get_changeset_safe(repo, rev):
493 494 """
494 495 Safe version of get_changeset if this changeset doesn't exists for a
495 496 repo it returns a Dummy one instead
496 497
497 498 :param repo:
498 499 :param rev:
499 500 """
500 501 from rhodecode.lib.vcs.backends.base import BaseRepository
501 502 from rhodecode.lib.vcs.exceptions import RepositoryError
502 503 from rhodecode.lib.vcs.backends.base import EmptyChangeset
503 504 if not isinstance(repo, BaseRepository):
504 505 raise Exception('You must pass an Repository '
505 506 'object as first argument got %s', type(repo))
506 507
507 508 try:
508 509 cs = repo.get_changeset(rev)
509 510 except RepositoryError:
510 511 cs = EmptyChangeset(requested_revision=rev)
511 512 return cs
512 513
513 514
514 515 def datetime_to_time(dt):
515 516 if dt:
516 517 return time.mktime(dt.timetuple())
517 518
518 519
519 520 def time_to_datetime(tm):
520 521 if tm:
521 522 if isinstance(tm, basestring):
522 523 try:
523 524 tm = float(tm)
524 525 except ValueError:
525 526 return
526 527 return datetime.datetime.fromtimestamp(tm)
527 528
528 529 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
529 530
530 531
531 532 def extract_mentioned_users(s):
532 533 """
533 534 Returns unique usernames from given string s that have @mention
534 535
535 536 :param s: string to get mentions
536 537 """
537 538 usrs = set()
538 539 for username in re.findall(MENTIONS_REGEX, s):
539 540 usrs.add(username)
540 541
541 542 return sorted(list(usrs), key=lambda k: k.lower())
542 543
543 544
544 545 class AttributeDict(dict):
545 546 def __getattr__(self, attr):
546 547 return self.get(attr, None)
547 548 __setattr__ = dict.__setitem__
548 549 __delattr__ = dict.__delitem__
549 550
550 551
551 552 def fix_PATH(os_=None):
552 553 """
553 554 Get current active python path, and append it to PATH variable to fix issues
554 555 of subprocess calls and different python versions
555 556 """
556 557 if os_ is None:
557 558 import os
558 559 else:
559 560 os = os_
560 561
561 562 cur_path = os.path.split(sys.executable)[0]
562 563 if not os.environ['PATH'].startswith(cur_path):
563 564 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
564 565
565 566
566 567 def obfuscate_url_pw(engine):
567 568 _url = engine or ''
568 569 from sqlalchemy.engine import url as sa_url
569 570 try:
570 571 _url = sa_url.make_url(engine)
571 572 if _url.password:
572 573 _url.password = 'XXXXX'
573 574 except Exception:
574 575 pass
575 576 return str(_url)
576 577
577 578
578 579 def get_server_url(environ):
579 580 req = webob.Request(environ)
580 581 return req.host_url + req.script_name
581 582
582 583
583 584 def _extract_extras(env=None):
584 585 """
585 586 Extracts the rc extras data from os.environ, and wraps it into named
586 587 AttributeDict object
587 588 """
588 589 if not env:
589 590 env = os.environ
590 591
591 592 try:
592 593 rc_extras = json.loads(env['RC_SCM_DATA'])
593 594 except Exception:
594 595 print os.environ
595 596 print >> sys.stderr, traceback.format_exc()
596 597 rc_extras = {}
597 598
598 599 try:
599 600 for k in ['username', 'repository', 'locked_by', 'scm', 'make_lock',
600 601 'action', 'ip']:
601 602 rc_extras[k]
602 603 except KeyError, e:
603 604 raise Exception('Missing key %s in os.environ %s' % (e, rc_extras))
604 605
605 606 return AttributeDict(rc_extras)
606 607
607 608
608 609 def _set_extras(extras):
609 610 os.environ['RC_SCM_DATA'] = json.dumps(extras)
611
612
613 def unique_id(hexlen=32):
614 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
615 return suuid(truncate_to=hexlen, alphabet=alphabet)
616
617
618 def suuid(url=None, truncate_to=22, alphabet=None):
619 """
620 Generate and return a short URL safe UUID.
621
622 If the url parameter is provided, set the namespace to the provided
623 URL and generate a UUID.
624
625 :param url to get the uuid for
626 :truncate_to: truncate the basic 22 UUID to shorter version
627
628 The IDs won't be universally unique any longer, but the probability of
629 a collision will still be very low.
630 """
631 # Define our alphabet.
632 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
633
634 # If no URL is given, generate a random UUID.
635 if url is None:
636 unique_id = uuid.uuid4().int
637 else:
638 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
639
640 alphabet_length = len(_ALPHABET)
641 output = []
642 while unique_id > 0:
643 digit = unique_id % alphabet_length
644 output.append(_ALPHABET[digit])
645 unique_id = int(unique_id / alphabet_length)
646 return "".join(output)[:truncate_to]
@@ -1,140 +1,137 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 The application's model objects
7 7
8 8 :created_on: Nov 25, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12
13 13
14 14 :example:
15 15
16 16 .. code-block:: python
17 17
18 18 from paste.deploy import appconfig
19 19 from pylons import config
20 20 from sqlalchemy import engine_from_config
21 21 from rhodecode.config.environment import load_environment
22 22
23 23 conf = appconfig('config:development.ini', relative_to = './../../')
24 24 load_environment(conf.global_conf, conf.local_conf)
25 25
26 26 engine = engine_from_config(config, 'sqlalchemy.')
27 27 init_model(engine)
28 28 # RUN YOUR CODE HERE
29 29
30 30 """
31 31 # This program is free software: you can redistribute it and/or modify
32 32 # it under the terms of the GNU General Public License as published by
33 33 # the Free Software Foundation, either version 3 of the License, or
34 34 # (at your option) any later version.
35 35 #
36 36 # This program is distributed in the hope that it will be useful,
37 37 # but WITHOUT ANY WARRANTY; without even the implied warranty of
38 38 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39 39 # GNU General Public License for more details.
40 40 #
41 41 # You should have received a copy of the GNU General Public License
42 42 # along with this program. If not, see <http://www.gnu.org/licenses/>.
43 43
44 44 import logging
45 45 from rhodecode.model import meta
46 46 from rhodecode.lib.utils2 import safe_str, obfuscate_url_pw
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 def init_model(engine):
52 52 """
53 53 Initializes db session, bind the engine with the metadata,
54 54 Call this before using any of the tables or classes in the model,
55 55 preferably once in application start
56 56
57 57 :param engine: engine to bind to
58 58 """
59 59 engine_str = obfuscate_url_pw(str(engine.url))
60 60 log.info("initializing db for %s" % engine_str)
61 61 meta.Base.metadata.bind = engine
62 62
63 63
64 64 class BaseModel(object):
65 65 """
66 66 Base Model for all RhodeCode models, it adds sql alchemy session
67 67 into instance of model
68 68
69 69 :param sa: If passed it reuses this session instead of creating a new one
70 70 """
71 71
72 72 cls = None # override in child class
73 73
74 74 def __init__(self, sa=None):
75 75 if sa is not None:
76 76 self.sa = sa
77 77 else:
78 78 self.sa = meta.Session()
79 79
80 80 def _get_instance(self, cls, instance, callback=None):
81 81 """
82 82 Get's instance of given cls using some simple lookup mechanism.
83 83
84 84 :param cls: class to fetch
85 85 :param instance: int or Instance
86 86 :param callback: callback to call if all lookups failed
87 87 """
88 88
89 89 if isinstance(instance, cls):
90 90 return instance
91 91 elif isinstance(instance, (int, long)) or safe_str(instance).isdigit():
92 92 return cls.get(instance)
93 93 else:
94 94 if instance:
95 95 if callback is None:
96 96 raise Exception(
97 97 'given object must be int, long or Instance of %s '
98 98 'got %s, no callback provided' % (cls, type(instance))
99 99 )
100 100 else:
101 101 return callback(instance)
102 102
103 103 def _get_user(self, user):
104 104 """
105 105 Helper method to get user by ID, or username fallback
106 106
107 :param user:
108 :type user: UserID, username, or User instance
107 :param user: UserID, username, or User instance
109 108 """
110 109 from rhodecode.model.db import User
111 110 return self._get_instance(User, user,
112 111 callback=User.get_by_username)
113 112
114 113 def _get_repo(self, repository):
115 114 """
116 115 Helper method to get repository by ID, or repository name
117 116
118 :param repository:
119 :type repository: RepoID, repository name or Repository Instance
117 :param repository: RepoID, repository name or Repository Instance
120 118 """
121 119 from rhodecode.model.db import Repository
122 120 return self._get_instance(Repository, repository,
123 121 callback=Repository.get_by_repo_name)
124 122
125 123 def _get_perm(self, permission):
126 124 """
127 125 Helper method to get permission by ID, or permission name
128 126
129 :param permission:
130 :type permission: PermissionID, permission_name or Permission instance
127 :param permission: PermissionID, permission_name or Permission instance
131 128 """
132 129 from rhodecode.model.db import Permission
133 130 return self._get_instance(Permission, permission,
134 131 callback=Permission.get_by_key)
135 132
136 133 def get_all(self):
137 134 """
138 135 Returns all instances of what is defined in `cls` class variable
139 136 """
140 137 return self.cls.getAll()
@@ -1,2133 +1,2170 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 # deprecated and left for backward compatibility
135 135 return cls.get_all()
136 136
137 137 @classmethod
138 138 def get_all(cls):
139 139 return cls.query().all()
140 140
141 141 @classmethod
142 142 def delete(cls, id_):
143 143 obj = cls.query().get(id_)
144 144 Session().delete(obj)
145 145
146 146 def __repr__(self):
147 147 if hasattr(self, '__unicode__'):
148 148 # python repr needs to return str
149 149 return safe_str(self.__unicode__())
150 150 return '<DB:%s>' % (self.__class__.__name__)
151 151
152 152
153 153 class RhodeCodeSetting(Base, BaseModel):
154 154 __tablename__ = 'rhodecode_settings'
155 155 __table_args__ = (
156 156 UniqueConstraint('app_settings_name'),
157 157 {'extend_existing': True, 'mysql_engine': 'InnoDB',
158 158 'mysql_charset': 'utf8'}
159 159 )
160 160 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
161 161 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
162 162 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
163 163
164 164 def __init__(self, k='', v=''):
165 165 self.app_settings_name = k
166 166 self.app_settings_value = v
167 167
168 168 @validates('_app_settings_value')
169 169 def validate_settings_value(self, key, val):
170 170 assert type(val) == unicode
171 171 return val
172 172
173 173 @hybrid_property
174 174 def app_settings_value(self):
175 175 v = self._app_settings_value
176 176 if self.app_settings_name in ["ldap_active",
177 177 "default_repo_enable_statistics",
178 178 "default_repo_enable_locking",
179 179 "default_repo_private",
180 180 "default_repo_enable_downloads"]:
181 181 v = str2bool(v)
182 182 return v
183 183
184 184 @app_settings_value.setter
185 185 def app_settings_value(self, val):
186 186 """
187 187 Setter that will always make sure we use unicode in app_settings_value
188 188
189 189 :param val:
190 190 """
191 191 self._app_settings_value = safe_unicode(val)
192 192
193 193 def __unicode__(self):
194 194 return u"<%s('%s:%s')>" % (
195 195 self.__class__.__name__,
196 196 self.app_settings_name, self.app_settings_value
197 197 )
198 198
199 199 @classmethod
200 200 def get_by_name(cls, key):
201 201 return cls.query()\
202 202 .filter(cls.app_settings_name == key).scalar()
203 203
204 204 @classmethod
205 205 def get_by_name_or_create(cls, key):
206 206 res = cls.get_by_name(key)
207 207 if not res:
208 208 res = cls(key)
209 209 return res
210 210
211 211 @classmethod
212 212 def get_app_settings(cls, cache=False):
213 213
214 214 ret = cls.query()
215 215
216 216 if cache:
217 217 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
218 218
219 219 if not ret:
220 220 raise Exception('Could not get application settings !')
221 221 settings = {}
222 222 for each in ret:
223 223 settings['rhodecode_' + each.app_settings_name] = \
224 224 each.app_settings_value
225 225
226 226 return settings
227 227
228 228 @classmethod
229 229 def get_ldap_settings(cls, cache=False):
230 230 ret = cls.query()\
231 231 .filter(cls.app_settings_name.startswith('ldap_')).all()
232 232 fd = {}
233 233 for row in ret:
234 234 fd.update({row.app_settings_name: row.app_settings_value})
235 235
236 236 return fd
237 237
238 238 @classmethod
239 239 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
240 240 ret = cls.query()\
241 241 .filter(cls.app_settings_name.startswith('default_')).all()
242 242 fd = {}
243 243 for row in ret:
244 244 key = row.app_settings_name
245 245 if strip_prefix:
246 246 key = remove_prefix(key, prefix='default_')
247 247 fd.update({key: row.app_settings_value})
248 248
249 249 return fd
250 250
251 251
252 252 class RhodeCodeUi(Base, BaseModel):
253 253 __tablename__ = 'rhodecode_ui'
254 254 __table_args__ = (
255 255 UniqueConstraint('ui_key'),
256 256 {'extend_existing': True, 'mysql_engine': 'InnoDB',
257 257 'mysql_charset': 'utf8'}
258 258 )
259 259
260 260 HOOK_UPDATE = 'changegroup.update'
261 261 HOOK_REPO_SIZE = 'changegroup.repo_size'
262 262 HOOK_PUSH = 'changegroup.push_logger'
263 263 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
264 264 HOOK_PULL = 'outgoing.pull_logger'
265 265 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
266 266
267 267 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
268 268 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
270 270 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
271 271 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
272 272
273 273 @classmethod
274 274 def get_by_key(cls, key):
275 275 return cls.query().filter(cls.ui_key == key).scalar()
276 276
277 277 @classmethod
278 278 def get_builtin_hooks(cls):
279 279 q = cls.query()
280 280 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
281 281 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
282 282 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
283 283 return q.all()
284 284
285 285 @classmethod
286 286 def get_custom_hooks(cls):
287 287 q = cls.query()
288 288 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
289 289 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
290 290 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
291 291 q = q.filter(cls.ui_section == 'hooks')
292 292 return q.all()
293 293
294 294 @classmethod
295 295 def get_repos_location(cls):
296 296 return cls.get_by_key('/').ui_value
297 297
298 298 @classmethod
299 299 def create_or_update_hook(cls, key, val):
300 300 new_ui = cls.get_by_key(key) or cls()
301 301 new_ui.ui_section = 'hooks'
302 302 new_ui.ui_active = True
303 303 new_ui.ui_key = key
304 304 new_ui.ui_value = val
305 305
306 306 Session().add(new_ui)
307 307
308 308 def __repr__(self):
309 309 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
310 310 self.ui_value)
311 311
312 312
313 313 class User(Base, BaseModel):
314 314 __tablename__ = 'users'
315 315 __table_args__ = (
316 316 UniqueConstraint('username'), UniqueConstraint('email'),
317 317 Index('u_username_idx', 'username'),
318 318 Index('u_email_idx', 'email'),
319 319 {'extend_existing': True, 'mysql_engine': 'InnoDB',
320 320 'mysql_charset': 'utf8'}
321 321 )
322 322 DEFAULT_USER = 'default'
323 323
324 324 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
325 325 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
326 326 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
328 328 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
329 329 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
333 333 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
334 334 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
335 335 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
336 336
337 337 user_log = relationship('UserLog')
338 338 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
339 339
340 340 repositories = relationship('Repository')
341 341 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
342 342 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
343 343
344 344 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
345 345 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
346 346
347 347 group_member = relationship('UserGroupMember', cascade='all')
348 348
349 349 notifications = relationship('UserNotification', cascade='all')
350 350 # notifications assigned to this user
351 351 user_created_notifications = relationship('Notification', cascade='all')
352 352 # comments created by this user
353 353 user_comments = relationship('ChangesetComment', cascade='all')
354 354 #extra emails for this user
355 355 user_emails = relationship('UserEmailMap', cascade='all')
356 356
357 357 @hybrid_property
358 358 def email(self):
359 359 return self._email
360 360
361 361 @email.setter
362 362 def email(self, val):
363 363 self._email = val.lower() if val else None
364 364
365 365 @property
366 366 def firstname(self):
367 367 # alias for future
368 368 return self.name
369 369
370 370 @property
371 371 def emails(self):
372 372 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
373 373 return [self.email] + [x.email for x in other]
374 374
375 375 @property
376 376 def ip_addresses(self):
377 377 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
378 378 return [x.ip_addr for x in ret]
379 379
380 380 @property
381 381 def username_and_name(self):
382 382 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
383 383
384 384 @property
385 385 def full_name(self):
386 386 return '%s %s' % (self.firstname, self.lastname)
387 387
388 388 @property
389 389 def full_name_or_username(self):
390 390 return ('%s %s' % (self.firstname, self.lastname)
391 391 if (self.firstname and self.lastname) else self.username)
392 392
393 393 @property
394 394 def full_contact(self):
395 395 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
396 396
397 397 @property
398 398 def short_contact(self):
399 399 return '%s %s' % (self.firstname, self.lastname)
400 400
401 401 @property
402 402 def is_admin(self):
403 403 return self.admin
404 404
405 405 @property
406 406 def AuthUser(self):
407 407 """
408 408 Returns instance of AuthUser for this user
409 409 """
410 410 from rhodecode.lib.auth import AuthUser
411 411 return AuthUser(user_id=self.user_id, api_key=self.api_key,
412 412 username=self.username)
413 413
414 414 def __unicode__(self):
415 415 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
416 416 self.user_id, self.username)
417 417
418 418 @classmethod
419 419 def get_by_username(cls, username, case_insensitive=False, cache=False):
420 420 if case_insensitive:
421 421 q = cls.query().filter(cls.username.ilike(username))
422 422 else:
423 423 q = cls.query().filter(cls.username == username)
424 424
425 425 if cache:
426 426 q = q.options(FromCache(
427 427 "sql_cache_short",
428 428 "get_user_%s" % _hash_key(username)
429 429 )
430 430 )
431 431 return q.scalar()
432 432
433 433 @classmethod
434 434 def get_by_api_key(cls, api_key, cache=False):
435 435 q = cls.query().filter(cls.api_key == api_key)
436 436
437 437 if cache:
438 438 q = q.options(FromCache("sql_cache_short",
439 439 "get_api_key_%s" % api_key))
440 440 return q.scalar()
441 441
442 442 @classmethod
443 443 def get_by_email(cls, email, case_insensitive=False, cache=False):
444 444 if case_insensitive:
445 445 q = cls.query().filter(cls.email.ilike(email))
446 446 else:
447 447 q = cls.query().filter(cls.email == email)
448 448
449 449 if cache:
450 450 q = q.options(FromCache("sql_cache_short",
451 451 "get_email_key_%s" % email))
452 452
453 453 ret = q.scalar()
454 454 if ret is None:
455 455 q = UserEmailMap.query()
456 456 # try fetching in alternate email map
457 457 if case_insensitive:
458 458 q = q.filter(UserEmailMap.email.ilike(email))
459 459 else:
460 460 q = q.filter(UserEmailMap.email == email)
461 461 q = q.options(joinedload(UserEmailMap.user))
462 462 if cache:
463 463 q = q.options(FromCache("sql_cache_short",
464 464 "get_email_map_key_%s" % email))
465 465 ret = getattr(q.scalar(), 'user', None)
466 466
467 467 return ret
468 468
469 469 @classmethod
470 470 def get_from_cs_author(cls, author):
471 471 """
472 472 Tries to get User objects out of commit author string
473 473
474 474 :param author:
475 475 """
476 476 from rhodecode.lib.helpers import email, author_name
477 477 # Valid email in the attribute passed, see if they're in the system
478 478 _email = email(author)
479 479 if _email:
480 480 user = cls.get_by_email(_email, case_insensitive=True)
481 481 if user:
482 482 return user
483 483 # Maybe we can match by username?
484 484 _author = author_name(author)
485 485 user = cls.get_by_username(_author, case_insensitive=True)
486 486 if user:
487 487 return user
488 488
489 489 def update_lastlogin(self):
490 490 """Update user lastlogin"""
491 491 self.last_login = datetime.datetime.now()
492 492 Session().add(self)
493 493 log.debug('updated user %s lastlogin' % self.username)
494 494
495 495 @classmethod
496 496 def get_first_admin(cls):
497 497 user = User.query().filter(User.admin == True).first()
498 498 if user is None:
499 499 raise Exception('Missing administrative account!')
500 500 return user
501 501
502 502 @classmethod
503 503 def get_default_user(cls, cache=False):
504 504 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
505 505 if user is None:
506 506 raise Exception('Missing default account!')
507 507 return user
508 508
509 509 def get_api_data(self):
510 510 """
511 511 Common function for generating user related data for API
512 512 """
513 513 user = self
514 514 data = dict(
515 515 user_id=user.user_id,
516 516 username=user.username,
517 517 firstname=user.name,
518 518 lastname=user.lastname,
519 519 email=user.email,
520 520 emails=user.emails,
521 521 api_key=user.api_key,
522 522 active=user.active,
523 523 admin=user.admin,
524 524 ldap_dn=user.ldap_dn,
525 525 last_login=user.last_login,
526 526 ip_addresses=user.ip_addresses
527 527 )
528 528 return data
529 529
530 530 def __json__(self):
531 531 data = dict(
532 532 full_name=self.full_name,
533 533 full_name_or_username=self.full_name_or_username,
534 534 short_contact=self.short_contact,
535 535 full_contact=self.full_contact
536 536 )
537 537 data.update(self.get_api_data())
538 538 return data
539 539
540 540
541 541 class UserEmailMap(Base, BaseModel):
542 542 __tablename__ = 'user_email_map'
543 543 __table_args__ = (
544 544 Index('uem_email_idx', 'email'),
545 545 UniqueConstraint('email'),
546 546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
547 547 'mysql_charset': 'utf8'}
548 548 )
549 549 __mapper_args__ = {}
550 550
551 551 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
552 552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
553 553 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
554 554 user = relationship('User', lazy='joined')
555 555
556 556 @validates('_email')
557 557 def validate_email(self, key, email):
558 558 # check if this email is not main one
559 559 main_email = Session().query(User).filter(User.email == email).scalar()
560 560 if main_email is not None:
561 561 raise AttributeError('email %s is present is user table' % email)
562 562 return email
563 563
564 564 @hybrid_property
565 565 def email(self):
566 566 return self._email
567 567
568 568 @email.setter
569 569 def email(self, val):
570 570 self._email = val.lower() if val else None
571 571
572 572
573 573 class UserIpMap(Base, BaseModel):
574 574 __tablename__ = 'user_ip_map'
575 575 __table_args__ = (
576 576 UniqueConstraint('user_id', 'ip_addr'),
577 577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
578 578 'mysql_charset': 'utf8'}
579 579 )
580 580 __mapper_args__ = {}
581 581
582 582 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
583 583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
584 584 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
585 585 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
586 586 user = relationship('User', lazy='joined')
587 587
588 588 @classmethod
589 589 def _get_ip_range(cls, ip_addr):
590 590 from rhodecode.lib import ipaddr
591 591 net = ipaddr.IPNetwork(address=ip_addr)
592 592 return [str(net.network), str(net.broadcast)]
593 593
594 594 def __json__(self):
595 595 return dict(
596 596 ip_addr=self.ip_addr,
597 597 ip_range=self._get_ip_range(self.ip_addr)
598 598 )
599 599
600 600
601 601 class UserLog(Base, BaseModel):
602 602 __tablename__ = 'user_logs'
603 603 __table_args__ = (
604 604 {'extend_existing': True, 'mysql_engine': 'InnoDB',
605 605 'mysql_charset': 'utf8'},
606 606 )
607 607 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
608 608 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
609 609 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
610 610 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
611 611 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
612 612 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
613 613 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
614 614 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
615 615
616 616 @property
617 617 def action_as_day(self):
618 618 return datetime.date(*self.action_date.timetuple()[:3])
619 619
620 620 user = relationship('User')
621 621 repository = relationship('Repository', cascade='')
622 622
623 623
624 624 class UserGroup(Base, BaseModel):
625 625 __tablename__ = 'users_groups'
626 626 __table_args__ = (
627 627 {'extend_existing': True, 'mysql_engine': 'InnoDB',
628 628 'mysql_charset': 'utf8'},
629 629 )
630 630
631 631 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
632 632 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
633 633 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
634 634 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
635 635 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
636 636
637 637 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
638 638 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
639 639 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
640 640 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
641 641 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
642 642 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
643 643
644 644 user = relationship('User')
645 645
646 646 def __unicode__(self):
647 647 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
648 648 self.users_group_id,
649 649 self.users_group_name)
650 650
651 651 @classmethod
652 652 def get_by_group_name(cls, group_name, cache=False,
653 653 case_insensitive=False):
654 654 if case_insensitive:
655 655 q = cls.query().filter(cls.users_group_name.ilike(group_name))
656 656 else:
657 657 q = cls.query().filter(cls.users_group_name == group_name)
658 658 if cache:
659 659 q = q.options(FromCache(
660 660 "sql_cache_short",
661 661 "get_user_%s" % _hash_key(group_name)
662 662 )
663 663 )
664 664 return q.scalar()
665 665
666 666 @classmethod
667 667 def get(cls, users_group_id, cache=False):
668 668 users_group = cls.query()
669 669 if cache:
670 670 users_group = users_group.options(FromCache("sql_cache_short",
671 671 "get_users_group_%s" % users_group_id))
672 672 return users_group.get(users_group_id)
673 673
674 674 def get_api_data(self):
675 675 users_group = self
676 676
677 677 data = dict(
678 678 users_group_id=users_group.users_group_id,
679 679 group_name=users_group.users_group_name,
680 680 active=users_group.users_group_active,
681 681 )
682 682
683 683 return data
684 684
685 685
686 686 class UserGroupMember(Base, BaseModel):
687 687 __tablename__ = 'users_groups_members'
688 688 __table_args__ = (
689 689 {'extend_existing': True, 'mysql_engine': 'InnoDB',
690 690 'mysql_charset': 'utf8'},
691 691 )
692 692
693 693 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
694 694 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
695 695 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
696 696
697 697 user = relationship('User', lazy='joined')
698 698 users_group = relationship('UserGroup')
699 699
700 700 def __init__(self, gr_id='', u_id=''):
701 701 self.users_group_id = gr_id
702 702 self.user_id = u_id
703 703
704 704
705 705 class RepositoryField(Base, BaseModel):
706 706 __tablename__ = 'repositories_fields'
707 707 __table_args__ = (
708 708 UniqueConstraint('repository_id', 'field_key'), # no-multi field
709 709 {'extend_existing': True, 'mysql_engine': 'InnoDB',
710 710 'mysql_charset': 'utf8'},
711 711 )
712 712 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
713 713
714 714 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
715 715 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
716 716 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
717 717 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
718 718 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
719 719 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
720 720 field_type = Column("field_type", String(256), nullable=False, unique=None)
721 721 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
722 722
723 723 repository = relationship('Repository')
724 724
725 725 @property
726 726 def field_key_prefixed(self):
727 727 return 'ex_%s' % self.field_key
728 728
729 729 @classmethod
730 730 def un_prefix_key(cls, key):
731 731 if key.startswith(cls.PREFIX):
732 732 return key[len(cls.PREFIX):]
733 733 return key
734 734
735 735 @classmethod
736 736 def get_by_key_name(cls, key, repo):
737 737 row = cls.query()\
738 738 .filter(cls.repository == repo)\
739 739 .filter(cls.field_key == key).scalar()
740 740 return row
741 741
742 742
743 743 class Repository(Base, BaseModel):
744 744 __tablename__ = 'repositories'
745 745 __table_args__ = (
746 746 UniqueConstraint('repo_name'),
747 747 Index('r_repo_name_idx', 'repo_name'),
748 748 {'extend_existing': True, 'mysql_engine': 'InnoDB',
749 749 'mysql_charset': 'utf8'},
750 750 )
751 751
752 752 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
753 753 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
754 754 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
755 755 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
756 756 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
757 757 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
758 758 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
759 759 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
760 760 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
761 761 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
762 762 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
763 763 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
764 764 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
765 765 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
766 766 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
767 767
768 768 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
769 769 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
770 770
771 771 user = relationship('User')
772 772 fork = relationship('Repository', remote_side=repo_id)
773 773 group = relationship('RepoGroup')
774 774 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
775 775 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
776 776 stats = relationship('Statistics', cascade='all', uselist=False)
777 777
778 778 followers = relationship('UserFollowing',
779 779 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
780 780 cascade='all')
781 781 extra_fields = relationship('RepositoryField',
782 782 cascade="all, delete, delete-orphan")
783 783
784 784 logs = relationship('UserLog')
785 785 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
786 786
787 787 pull_requests_org = relationship('PullRequest',
788 788 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
789 789 cascade="all, delete, delete-orphan")
790 790
791 791 pull_requests_other = relationship('PullRequest',
792 792 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
793 793 cascade="all, delete, delete-orphan")
794 794
795 795 def __unicode__(self):
796 796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
797 797 self.repo_name)
798 798
799 799 @hybrid_property
800 800 def locked(self):
801 801 # always should return [user_id, timelocked]
802 802 if self._locked:
803 803 _lock_info = self._locked.split(':')
804 804 return int(_lock_info[0]), _lock_info[1]
805 805 return [None, None]
806 806
807 807 @locked.setter
808 808 def locked(self, val):
809 809 if val and isinstance(val, (list, tuple)):
810 810 self._locked = ':'.join(map(str, val))
811 811 else:
812 812 self._locked = None
813 813
814 814 @hybrid_property
815 815 def changeset_cache(self):
816 816 from rhodecode.lib.vcs.backends.base import EmptyChangeset
817 817 dummy = EmptyChangeset().__json__()
818 818 if not self._changeset_cache:
819 819 return dummy
820 820 try:
821 821 return json.loads(self._changeset_cache)
822 822 except TypeError:
823 823 return dummy
824 824
825 825 @changeset_cache.setter
826 826 def changeset_cache(self, val):
827 827 try:
828 828 self._changeset_cache = json.dumps(val)
829 829 except Exception:
830 830 log.error(traceback.format_exc())
831 831
832 832 @classmethod
833 833 def url_sep(cls):
834 834 return URL_SEP
835 835
836 836 @classmethod
837 837 def normalize_repo_name(cls, repo_name):
838 838 """
839 839 Normalizes os specific repo_name to the format internally stored inside
840 840 dabatabase using URL_SEP
841 841
842 842 :param cls:
843 843 :param repo_name:
844 844 """
845 845 return cls.url_sep().join(repo_name.split(os.sep))
846 846
847 847 @classmethod
848 848 def get_by_repo_name(cls, repo_name):
849 849 q = Session().query(cls).filter(cls.repo_name == repo_name)
850 850 q = q.options(joinedload(Repository.fork))\
851 851 .options(joinedload(Repository.user))\
852 852 .options(joinedload(Repository.group))
853 853 return q.scalar()
854 854
855 855 @classmethod
856 856 def get_by_full_path(cls, repo_full_path):
857 857 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
858 858 repo_name = cls.normalize_repo_name(repo_name)
859 859 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
860 860
861 861 @classmethod
862 862 def get_repo_forks(cls, repo_id):
863 863 return cls.query().filter(Repository.fork_id == repo_id)
864 864
865 865 @classmethod
866 866 def base_path(cls):
867 867 """
868 868 Returns base path when all repos are stored
869 869
870 870 :param cls:
871 871 """
872 872 q = Session().query(RhodeCodeUi)\
873 873 .filter(RhodeCodeUi.ui_key == cls.url_sep())
874 874 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
875 875 return q.one().ui_value
876 876
877 877 @property
878 878 def forks(self):
879 879 """
880 880 Return forks of this repo
881 881 """
882 882 return Repository.get_repo_forks(self.repo_id)
883 883
884 884 @property
885 885 def parent(self):
886 886 """
887 887 Returns fork parent
888 888 """
889 889 return self.fork
890 890
891 891 @property
892 892 def just_name(self):
893 893 return self.repo_name.split(Repository.url_sep())[-1]
894 894
895 895 @property
896 896 def groups_with_parents(self):
897 897 groups = []
898 898 if self.group is None:
899 899 return groups
900 900
901 901 cur_gr = self.group
902 902 groups.insert(0, cur_gr)
903 903 while 1:
904 904 gr = getattr(cur_gr, 'parent_group', None)
905 905 cur_gr = cur_gr.parent_group
906 906 if gr is None:
907 907 break
908 908 groups.insert(0, gr)
909 909
910 910 return groups
911 911
912 912 @property
913 913 def groups_and_repo(self):
914 914 return self.groups_with_parents, self.just_name, self.repo_name
915 915
916 916 @LazyProperty
917 917 def repo_path(self):
918 918 """
919 919 Returns base full path for that repository means where it actually
920 920 exists on a filesystem
921 921 """
922 922 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
923 923 Repository.url_sep())
924 924 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
925 925 return q.one().ui_value
926 926
927 927 @property
928 928 def repo_full_path(self):
929 929 p = [self.repo_path]
930 930 # we need to split the name by / since this is how we store the
931 931 # names in the database, but that eventually needs to be converted
932 932 # into a valid system path
933 933 p += self.repo_name.split(Repository.url_sep())
934 934 return os.path.join(*map(safe_unicode, p))
935 935
936 936 @property
937 937 def cache_keys(self):
938 938 """
939 939 Returns associated cache keys for that repo
940 940 """
941 941 return CacheInvalidation.query()\
942 942 .filter(CacheInvalidation.cache_args == self.repo_name)\
943 943 .order_by(CacheInvalidation.cache_key)\
944 944 .all()
945 945
946 946 def get_new_name(self, repo_name):
947 947 """
948 948 returns new full repository name based on assigned group and new new
949 949
950 950 :param group_name:
951 951 """
952 952 path_prefix = self.group.full_path_splitted if self.group else []
953 953 return Repository.url_sep().join(path_prefix + [repo_name])
954 954
955 955 @property
956 956 def _ui(self):
957 957 """
958 958 Creates an db based ui object for this repository
959 959 """
960 960 from rhodecode.lib.utils import make_ui
961 961 return make_ui('db', clear_session=False)
962 962
963 963 @classmethod
964 964 def is_valid(cls, repo_name):
965 965 """
966 966 returns True if given repo name is a valid filesystem repository
967 967
968 968 :param cls:
969 969 :param repo_name:
970 970 """
971 971 from rhodecode.lib.utils import is_valid_repo
972 972
973 973 return is_valid_repo(repo_name, cls.base_path())
974 974
975 975 def get_api_data(self):
976 976 """
977 977 Common function for generating repo api data
978 978
979 979 """
980 980 repo = self
981 981 data = dict(
982 982 repo_id=repo.repo_id,
983 983 repo_name=repo.repo_name,
984 984 repo_type=repo.repo_type,
985 985 clone_uri=repo.clone_uri,
986 986 private=repo.private,
987 987 created_on=repo.created_on,
988 988 description=repo.description,
989 989 landing_rev=repo.landing_rev,
990 990 owner=repo.user.username,
991 991 fork_of=repo.fork.repo_name if repo.fork else None,
992 992 enable_statistics=repo.enable_statistics,
993 993 enable_locking=repo.enable_locking,
994 994 enable_downloads=repo.enable_downloads,
995 995 last_changeset=repo.changeset_cache,
996 996 locked_by=User.get(self.locked[0]).get_api_data() \
997 997 if self.locked[0] else None,
998 998 locked_date=time_to_datetime(self.locked[1]) \
999 999 if self.locked[1] else None
1000 1000 )
1001 1001 rc_config = RhodeCodeSetting.get_app_settings()
1002 1002 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
1003 1003 if repository_fields:
1004 1004 for f in self.extra_fields:
1005 1005 data[f.field_key_prefixed] = f.field_value
1006 1006
1007 1007 return data
1008 1008
1009 1009 @classmethod
1010 1010 def lock(cls, repo, user_id, lock_time=None):
1011 1011 if not lock_time:
1012 1012 lock_time = time.time()
1013 1013 repo.locked = [user_id, lock_time]
1014 1014 Session().add(repo)
1015 1015 Session().commit()
1016 1016
1017 1017 @classmethod
1018 1018 def unlock(cls, repo):
1019 1019 repo.locked = None
1020 1020 Session().add(repo)
1021 1021 Session().commit()
1022 1022
1023 1023 @classmethod
1024 1024 def getlock(cls, repo):
1025 1025 return repo.locked
1026 1026
1027 1027 @property
1028 1028 def last_db_change(self):
1029 1029 return self.updated_on
1030 1030
1031 1031 def clone_url(self, **override):
1032 1032 from pylons import url
1033 1033 from urlparse import urlparse
1034 1034 import urllib
1035 1035 parsed_url = urlparse(url('home', qualified=True))
1036 1036 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1037 1037 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1038 1038 args = {
1039 1039 'user': '',
1040 1040 'pass': '',
1041 1041 'scheme': parsed_url.scheme,
1042 1042 'netloc': parsed_url.netloc,
1043 1043 'prefix': decoded_path,
1044 1044 'path': self.repo_name
1045 1045 }
1046 1046
1047 1047 args.update(override)
1048 1048 return default_clone_uri % args
1049 1049
1050 1050 #==========================================================================
1051 1051 # SCM PROPERTIES
1052 1052 #==========================================================================
1053 1053
1054 1054 def get_changeset(self, rev=None):
1055 1055 return get_changeset_safe(self.scm_instance, rev)
1056 1056
1057 1057 def get_landing_changeset(self):
1058 1058 """
1059 1059 Returns landing changeset, or if that doesn't exist returns the tip
1060 1060 """
1061 1061 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1062 1062 return cs
1063 1063
1064 1064 def update_changeset_cache(self, cs_cache=None):
1065 1065 """
1066 1066 Update cache of last changeset for repository, keys should be::
1067 1067
1068 1068 short_id
1069 1069 raw_id
1070 1070 revision
1071 1071 message
1072 1072 date
1073 1073 author
1074 1074
1075 1075 :param cs_cache:
1076 1076 """
1077 1077 from rhodecode.lib.vcs.backends.base import BaseChangeset
1078 1078 if cs_cache is None:
1079 1079 cs_cache = EmptyChangeset()
1080 1080 # use no-cache version here
1081 1081 scm_repo = self.scm_instance_no_cache()
1082 1082 if scm_repo:
1083 1083 cs_cache = scm_repo.get_changeset()
1084 1084
1085 1085 if isinstance(cs_cache, BaseChangeset):
1086 1086 cs_cache = cs_cache.__json__()
1087 1087
1088 1088 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1089 1089 _default = datetime.datetime.fromtimestamp(0)
1090 1090 last_change = cs_cache.get('date') or _default
1091 1091 log.debug('updated repo %s with new cs cache %s'
1092 1092 % (self.repo_name, cs_cache))
1093 1093 self.updated_on = last_change
1094 1094 self.changeset_cache = cs_cache
1095 1095 Session().add(self)
1096 1096 Session().commit()
1097 1097 else:
1098 1098 log.debug('Skipping repo:%s already with latest changes'
1099 1099 % self.repo_name)
1100 1100
1101 1101 @property
1102 1102 def tip(self):
1103 1103 return self.get_changeset('tip')
1104 1104
1105 1105 @property
1106 1106 def author(self):
1107 1107 return self.tip.author
1108 1108
1109 1109 @property
1110 1110 def last_change(self):
1111 1111 return self.scm_instance.last_change
1112 1112
1113 1113 def get_comments(self, revisions=None):
1114 1114 """
1115 1115 Returns comments for this repository grouped by revisions
1116 1116
1117 1117 :param revisions: filter query by revisions only
1118 1118 """
1119 1119 cmts = ChangesetComment.query()\
1120 1120 .filter(ChangesetComment.repo == self)
1121 1121 if revisions:
1122 1122 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1123 1123 grouped = defaultdict(list)
1124 1124 for cmt in cmts.all():
1125 1125 grouped[cmt.revision].append(cmt)
1126 1126 return grouped
1127 1127
1128 1128 def statuses(self, revisions=None):
1129 1129 """
1130 1130 Returns statuses for this repository
1131 1131
1132 1132 :param revisions: list of revisions to get statuses for
1133 :type revisions: list
1134 1133 """
1135 1134
1136 1135 statuses = ChangesetStatus.query()\
1137 1136 .filter(ChangesetStatus.repo == self)\
1138 1137 .filter(ChangesetStatus.version == 0)
1139 1138 if revisions:
1140 1139 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1141 1140 grouped = {}
1142 1141
1143 1142 #maybe we have open new pullrequest without a status ?
1144 1143 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1145 1144 status_lbl = ChangesetStatus.get_status_lbl(stat)
1146 1145 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1147 1146 for rev in pr.revisions:
1148 1147 pr_id = pr.pull_request_id
1149 1148 pr_repo = pr.other_repo.repo_name
1150 1149 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1151 1150
1152 1151 for stat in statuses.all():
1153 1152 pr_id = pr_repo = None
1154 1153 if stat.pull_request:
1155 1154 pr_id = stat.pull_request.pull_request_id
1156 1155 pr_repo = stat.pull_request.other_repo.repo_name
1157 1156 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1158 1157 pr_id, pr_repo]
1159 1158 return grouped
1160 1159
1161 1160 def _repo_size(self):
1162 1161 from rhodecode.lib import helpers as h
1163 1162 log.debug('calculating repository size...')
1164 1163 return h.format_byte_size(self.scm_instance.size)
1165 1164
1166 1165 #==========================================================================
1167 1166 # SCM CACHE INSTANCE
1168 1167 #==========================================================================
1169 1168
1170 1169 def set_invalidate(self):
1171 1170 """
1172 1171 Mark caches of this repo as invalid.
1173 1172 """
1174 1173 CacheInvalidation.set_invalidate(self.repo_name)
1175 1174
1176 1175 def scm_instance_no_cache(self):
1177 1176 return self.__get_instance()
1178 1177
1179 1178 @property
1180 1179 def scm_instance(self):
1181 1180 import rhodecode
1182 1181 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1183 1182 if full_cache:
1184 1183 return self.scm_instance_cached()
1185 1184 return self.__get_instance()
1186 1185
1187 1186 def scm_instance_cached(self, valid_cache_keys=None):
1188 1187 @cache_region('long_term')
1189 1188 def _c(repo_name):
1190 1189 return self.__get_instance()
1191 1190 rn = self.repo_name
1192 1191
1193 1192 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1194 1193 if not valid:
1195 1194 log.debug('Cache for %s invalidated, getting new object' % (rn))
1196 1195 region_invalidate(_c, None, rn)
1197 1196 else:
1198 1197 log.debug('Getting obj for %s from cache' % (rn))
1199 1198 return _c(rn)
1200 1199
1201 1200 def __get_instance(self):
1202 1201 repo_full_path = self.repo_full_path
1203 1202 try:
1204 1203 alias = get_scm(repo_full_path)[0]
1205 1204 log.debug('Creating instance of %s repository from %s'
1206 1205 % (alias, repo_full_path))
1207 1206 backend = get_backend(alias)
1208 1207 except VCSError:
1209 1208 log.error(traceback.format_exc())
1210 1209 log.error('Perhaps this repository is in db and not in '
1211 1210 'filesystem run rescan repositories with '
1212 1211 '"destroy old data " option from admin panel')
1213 1212 return
1214 1213
1215 1214 if alias == 'hg':
1216 1215
1217 1216 repo = backend(safe_str(repo_full_path), create=False,
1218 1217 baseui=self._ui)
1219 1218 # skip hidden web repository
1220 1219 if repo._get_hidden():
1221 1220 return
1222 1221 else:
1223 1222 repo = backend(repo_full_path, create=False)
1224 1223
1225 1224 return repo
1226 1225
1227 1226
1228 1227 class RepoGroup(Base, BaseModel):
1229 1228 __tablename__ = 'groups'
1230 1229 __table_args__ = (
1231 1230 UniqueConstraint('group_name', 'group_parent_id'),
1232 1231 CheckConstraint('group_id != group_parent_id'),
1233 1232 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1234 1233 'mysql_charset': 'utf8'},
1235 1234 )
1236 1235 __mapper_args__ = {'order_by': 'group_name'}
1237 1236
1238 1237 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1239 1238 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1240 1239 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1241 1240 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1242 1241 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1243 1242 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1244 1243
1245 1244 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1246 1245 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1247 1246 parent_group = relationship('RepoGroup', remote_side=group_id)
1248 1247 user = relationship('User')
1249 1248
1250 1249 def __init__(self, group_name='', parent_group=None):
1251 1250 self.group_name = group_name
1252 1251 self.parent_group = parent_group
1253 1252
1254 1253 def __unicode__(self):
1255 1254 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1256 1255 self.group_name)
1257 1256
1258 1257 @classmethod
1259 1258 def groups_choices(cls, groups=None, show_empty_group=True):
1260 1259 from webhelpers.html import literal as _literal
1261 1260 if not groups:
1262 1261 groups = cls.query().all()
1263 1262
1264 1263 repo_groups = []
1265 1264 if show_empty_group:
1266 1265 repo_groups = [('-1', '-- %s --' % _('top level'))]
1267 1266 sep = ' &raquo; '
1268 1267 _name = lambda k: _literal(sep.join(k))
1269 1268
1270 1269 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1271 1270 for x in groups])
1272 1271
1273 1272 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1274 1273 return repo_groups
1275 1274
1276 1275 @classmethod
1277 1276 def url_sep(cls):
1278 1277 return URL_SEP
1279 1278
1280 1279 @classmethod
1281 1280 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1282 1281 if case_insensitive:
1283 1282 gr = cls.query()\
1284 1283 .filter(cls.group_name.ilike(group_name))
1285 1284 else:
1286 1285 gr = cls.query()\
1287 1286 .filter(cls.group_name == group_name)
1288 1287 if cache:
1289 1288 gr = gr.options(FromCache(
1290 1289 "sql_cache_short",
1291 1290 "get_group_%s" % _hash_key(group_name)
1292 1291 )
1293 1292 )
1294 1293 return gr.scalar()
1295 1294
1296 1295 @property
1297 1296 def parents(self):
1298 1297 parents_recursion_limit = 5
1299 1298 groups = []
1300 1299 if self.parent_group is None:
1301 1300 return groups
1302 1301 cur_gr = self.parent_group
1303 1302 groups.insert(0, cur_gr)
1304 1303 cnt = 0
1305 1304 while 1:
1306 1305 cnt += 1
1307 1306 gr = getattr(cur_gr, 'parent_group', None)
1308 1307 cur_gr = cur_gr.parent_group
1309 1308 if gr is None:
1310 1309 break
1311 1310 if cnt == parents_recursion_limit:
1312 1311 # this will prevent accidental infinit loops
1313 1312 log.error('group nested more than %s' %
1314 1313 parents_recursion_limit)
1315 1314 break
1316 1315
1317 1316 groups.insert(0, gr)
1318 1317 return groups
1319 1318
1320 1319 @property
1321 1320 def children(self):
1322 1321 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1323 1322
1324 1323 @property
1325 1324 def name(self):
1326 1325 return self.group_name.split(RepoGroup.url_sep())[-1]
1327 1326
1328 1327 @property
1329 1328 def full_path(self):
1330 1329 return self.group_name
1331 1330
1332 1331 @property
1333 1332 def full_path_splitted(self):
1334 1333 return self.group_name.split(RepoGroup.url_sep())
1335 1334
1336 1335 @property
1337 1336 def repositories(self):
1338 1337 return Repository.query()\
1339 1338 .filter(Repository.group == self)\
1340 1339 .order_by(Repository.repo_name)
1341 1340
1342 1341 @property
1343 1342 def repositories_recursive_count(self):
1344 1343 cnt = self.repositories.count()
1345 1344
1346 1345 def children_count(group):
1347 1346 cnt = 0
1348 1347 for child in group.children:
1349 1348 cnt += child.repositories.count()
1350 1349 cnt += children_count(child)
1351 1350 return cnt
1352 1351
1353 1352 return cnt + children_count(self)
1354 1353
1355 1354 def _recursive_objects(self, include_repos=True):
1356 1355 all_ = []
1357 1356
1358 1357 def _get_members(root_gr):
1359 1358 if include_repos:
1360 1359 for r in root_gr.repositories:
1361 1360 all_.append(r)
1362 1361 childs = root_gr.children.all()
1363 1362 if childs:
1364 1363 for gr in childs:
1365 1364 all_.append(gr)
1366 1365 _get_members(gr)
1367 1366
1368 1367 _get_members(self)
1369 1368 return [self] + all_
1370 1369
1371 1370 def recursive_groups_and_repos(self):
1372 1371 """
1373 1372 Recursive return all groups, with repositories in those groups
1374 1373 """
1375 1374 return self._recursive_objects()
1376 1375
1377 1376 def recursive_groups(self):
1378 1377 """
1379 1378 Returns all children groups for this group including children of children
1380 1379 """
1381 1380 return self._recursive_objects(include_repos=False)
1382 1381
1383 1382 def get_new_name(self, group_name):
1384 1383 """
1385 1384 returns new full group name based on parent and new name
1386 1385
1387 1386 :param group_name:
1388 1387 """
1389 1388 path_prefix = (self.parent_group.full_path_splitted if
1390 1389 self.parent_group else [])
1391 1390 return RepoGroup.url_sep().join(path_prefix + [group_name])
1392 1391
1393 1392
1394 1393 class Permission(Base, BaseModel):
1395 1394 __tablename__ = 'permissions'
1396 1395 __table_args__ = (
1397 1396 Index('p_perm_name_idx', 'permission_name'),
1398 1397 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1399 1398 'mysql_charset': 'utf8'},
1400 1399 )
1401 1400 PERMS = [
1402 1401 ('hg.admin', _('RhodeCode Administrator')),
1403 1402
1404 1403 ('repository.none', _('Repository no access')),
1405 1404 ('repository.read', _('Repository read access')),
1406 1405 ('repository.write', _('Repository write access')),
1407 1406 ('repository.admin', _('Repository admin access')),
1408 1407
1409 1408 ('group.none', _('Repository group no access')),
1410 1409 ('group.read', _('Repository group read access')),
1411 1410 ('group.write', _('Repository group write access')),
1412 1411 ('group.admin', _('Repository group admin access')),
1413 1412
1414 1413 ('usergroup.none', _('User group no access')),
1415 1414 ('usergroup.read', _('User group read access')),
1416 1415 ('usergroup.write', _('User group write access')),
1417 1416 ('usergroup.admin', _('User group admin access')),
1418 1417
1419 1418 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
1420 1419 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
1421 1420
1422 1421 ('hg.usergroup.create.false', _('User Group creation disabled')),
1423 1422 ('hg.usergroup.create.true', _('User Group creation enabled')),
1424 1423
1425 1424 ('hg.create.none', _('Repository creation disabled')),
1426 1425 ('hg.create.repository', _('Repository creation enabled')),
1427 1426
1428 1427 ('hg.fork.none', _('Repository forking disabled')),
1429 1428 ('hg.fork.repository', _('Repository forking enabled')),
1430 1429
1431 1430 ('hg.register.none', _('Registration disabled')),
1432 1431 ('hg.register.manual_activate', _('User Registration with manual account activation')),
1433 1432 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
1434 1433
1435 1434 ('hg.extern_activate.manual', _('Manual activation of external account')),
1436 1435 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1437 1436
1438 1437 ]
1439 1438
1440 1439 #definition of system default permissions for DEFAULT user
1441 1440 DEFAULT_USER_PERMISSIONS = [
1442 1441 'repository.read',
1443 1442 'group.read',
1444 1443 'usergroup.read',
1445 1444 'hg.create.repository',
1446 1445 'hg.fork.repository',
1447 1446 'hg.register.manual_activate',
1448 1447 'hg.extern_activate.auto',
1449 1448 ]
1450 1449
1451 1450 # defines which permissions are more important higher the more important
1452 1451 # Weight defines which permissions are more important.
1453 1452 # The higher number the more important.
1454 1453 PERM_WEIGHTS = {
1455 1454 'repository.none': 0,
1456 1455 'repository.read': 1,
1457 1456 'repository.write': 3,
1458 1457 'repository.admin': 4,
1459 1458
1460 1459 'group.none': 0,
1461 1460 'group.read': 1,
1462 1461 'group.write': 3,
1463 1462 'group.admin': 4,
1464 1463
1465 1464 'usergroup.none': 0,
1466 1465 'usergroup.read': 1,
1467 1466 'usergroup.write': 3,
1468 1467 'usergroup.admin': 4,
1469 1468 'hg.repogroup.create.false': 0,
1470 1469 'hg.repogroup.create.true': 1,
1471 1470
1472 1471 'hg.usergroup.create.false': 0,
1473 1472 'hg.usergroup.create.true': 1,
1474 1473
1475 1474 'hg.fork.none': 0,
1476 1475 'hg.fork.repository': 1,
1477 1476 'hg.create.none': 0,
1478 1477 'hg.create.repository': 1
1479 1478 }
1480 1479
1481 1480 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1482 1481 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1483 1482 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1484 1483
1485 1484 def __unicode__(self):
1486 1485 return u"<%s('%s:%s')>" % (
1487 1486 self.__class__.__name__, self.permission_id, self.permission_name
1488 1487 )
1489 1488
1490 1489 @classmethod
1491 1490 def get_by_key(cls, key):
1492 1491 return cls.query().filter(cls.permission_name == key).scalar()
1493 1492
1494 1493 @classmethod
1495 1494 def get_default_perms(cls, default_user_id):
1496 1495 q = Session().query(UserRepoToPerm, Repository, cls)\
1497 1496 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1498 1497 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1499 1498 .filter(UserRepoToPerm.user_id == default_user_id)
1500 1499
1501 1500 return q.all()
1502 1501
1503 1502 @classmethod
1504 1503 def get_default_group_perms(cls, default_user_id):
1505 1504 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1506 1505 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1507 1506 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1508 1507 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1509 1508
1510 1509 return q.all()
1511 1510
1512 1511 @classmethod
1513 1512 def get_default_user_group_perms(cls, default_user_id):
1514 1513 q = Session().query(UserUserGroupToPerm, UserGroup, cls)\
1515 1514 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
1516 1515 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id))\
1517 1516 .filter(UserUserGroupToPerm.user_id == default_user_id)
1518 1517
1519 1518 return q.all()
1520 1519
1521 1520
1522 1521 class UserRepoToPerm(Base, BaseModel):
1523 1522 __tablename__ = 'repo_to_perm'
1524 1523 __table_args__ = (
1525 1524 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1526 1525 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1527 1526 'mysql_charset': 'utf8'}
1528 1527 )
1529 1528 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1530 1529 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1531 1530 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1532 1531 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1533 1532
1534 1533 user = relationship('User')
1535 1534 repository = relationship('Repository')
1536 1535 permission = relationship('Permission')
1537 1536
1538 1537 @classmethod
1539 1538 def create(cls, user, repository, permission):
1540 1539 n = cls()
1541 1540 n.user = user
1542 1541 n.repository = repository
1543 1542 n.permission = permission
1544 1543 Session().add(n)
1545 1544 return n
1546 1545
1547 1546 def __unicode__(self):
1548 1547 return u'<%s => %s >' % (self.user, self.repository)
1549 1548
1550 1549
1551 1550 class UserUserGroupToPerm(Base, BaseModel):
1552 1551 __tablename__ = 'user_user_group_to_perm'
1553 1552 __table_args__ = (
1554 1553 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1555 1554 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1556 1555 'mysql_charset': 'utf8'}
1557 1556 )
1558 1557 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1559 1558 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1560 1559 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1561 1560 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1562 1561
1563 1562 user = relationship('User')
1564 1563 user_group = relationship('UserGroup')
1565 1564 permission = relationship('Permission')
1566 1565
1567 1566 @classmethod
1568 1567 def create(cls, user, user_group, permission):
1569 1568 n = cls()
1570 1569 n.user = user
1571 1570 n.user_group = user_group
1572 1571 n.permission = permission
1573 1572 Session().add(n)
1574 1573 return n
1575 1574
1576 1575 def __unicode__(self):
1577 1576 return u'<%s => %s >' % (self.user, self.user_group)
1578 1577
1579 1578
1580 1579 class UserToPerm(Base, BaseModel):
1581 1580 __tablename__ = 'user_to_perm'
1582 1581 __table_args__ = (
1583 1582 UniqueConstraint('user_id', 'permission_id'),
1584 1583 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1585 1584 'mysql_charset': 'utf8'}
1586 1585 )
1587 1586 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1588 1587 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1589 1588 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1590 1589
1591 1590 user = relationship('User')
1592 1591 permission = relationship('Permission', lazy='joined')
1593 1592
1594 1593 def __unicode__(self):
1595 1594 return u'<%s => %s >' % (self.user, self.permission)
1596 1595
1597 1596
1598 1597 class UserGroupRepoToPerm(Base, BaseModel):
1599 1598 __tablename__ = 'users_group_repo_to_perm'
1600 1599 __table_args__ = (
1601 1600 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1602 1601 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1603 1602 'mysql_charset': 'utf8'}
1604 1603 )
1605 1604 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1606 1605 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1607 1606 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1608 1607 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1609 1608
1610 1609 users_group = relationship('UserGroup')
1611 1610 permission = relationship('Permission')
1612 1611 repository = relationship('Repository')
1613 1612
1614 1613 @classmethod
1615 1614 def create(cls, users_group, repository, permission):
1616 1615 n = cls()
1617 1616 n.users_group = users_group
1618 1617 n.repository = repository
1619 1618 n.permission = permission
1620 1619 Session().add(n)
1621 1620 return n
1622 1621
1623 1622 def __unicode__(self):
1624 1623 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1625 1624
1626 1625
1627 1626 class UserGroupUserGroupToPerm(Base, BaseModel):
1628 1627 __tablename__ = 'user_group_user_group_to_perm'
1629 1628 __table_args__ = (
1630 1629 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1631 1630 CheckConstraint('target_user_group_id != user_group_id'),
1632 1631 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1633 1632 'mysql_charset': 'utf8'}
1634 1633 )
1635 1634 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1636 1635 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1637 1636 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1638 1637 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1639 1638
1640 1639 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1641 1640 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1642 1641 permission = relationship('Permission')
1643 1642
1644 1643 @classmethod
1645 1644 def create(cls, target_user_group, user_group, permission):
1646 1645 n = cls()
1647 1646 n.target_user_group = target_user_group
1648 1647 n.user_group = user_group
1649 1648 n.permission = permission
1650 1649 Session().add(n)
1651 1650 return n
1652 1651
1653 1652 def __unicode__(self):
1654 1653 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1655 1654
1656 1655
1657 1656 class UserGroupToPerm(Base, BaseModel):
1658 1657 __tablename__ = 'users_group_to_perm'
1659 1658 __table_args__ = (
1660 1659 UniqueConstraint('users_group_id', 'permission_id',),
1661 1660 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1662 1661 'mysql_charset': 'utf8'}
1663 1662 )
1664 1663 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1665 1664 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1666 1665 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1667 1666
1668 1667 users_group = relationship('UserGroup')
1669 1668 permission = relationship('Permission')
1670 1669
1671 1670
1672 1671 class UserRepoGroupToPerm(Base, BaseModel):
1673 1672 __tablename__ = 'user_repo_group_to_perm'
1674 1673 __table_args__ = (
1675 1674 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1676 1675 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1677 1676 'mysql_charset': 'utf8'}
1678 1677 )
1679 1678
1680 1679 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1681 1680 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1682 1681 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1683 1682 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1684 1683
1685 1684 user = relationship('User')
1686 1685 group = relationship('RepoGroup')
1687 1686 permission = relationship('Permission')
1688 1687
1689 1688
1690 1689 class UserGroupRepoGroupToPerm(Base, BaseModel):
1691 1690 __tablename__ = 'users_group_repo_group_to_perm'
1692 1691 __table_args__ = (
1693 1692 UniqueConstraint('users_group_id', 'group_id'),
1694 1693 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1695 1694 'mysql_charset': 'utf8'}
1696 1695 )
1697 1696
1698 1697 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1699 1698 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1700 1699 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1701 1700 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1702 1701
1703 1702 users_group = relationship('UserGroup')
1704 1703 permission = relationship('Permission')
1705 1704 group = relationship('RepoGroup')
1706 1705
1707 1706
1708 1707 class Statistics(Base, BaseModel):
1709 1708 __tablename__ = 'statistics'
1710 1709 __table_args__ = (
1711 1710 UniqueConstraint('repository_id'),
1712 1711 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1713 1712 'mysql_charset': 'utf8'}
1714 1713 )
1715 1714 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1716 1715 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1717 1716 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1718 1717 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1719 1718 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1720 1719 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1721 1720
1722 1721 repository = relationship('Repository', single_parent=True)
1723 1722
1724 1723
1725 1724 class UserFollowing(Base, BaseModel):
1726 1725 __tablename__ = 'user_followings'
1727 1726 __table_args__ = (
1728 1727 UniqueConstraint('user_id', 'follows_repository_id'),
1729 1728 UniqueConstraint('user_id', 'follows_user_id'),
1730 1729 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1731 1730 'mysql_charset': 'utf8'}
1732 1731 )
1733 1732
1734 1733 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1735 1734 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1736 1735 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1737 1736 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1738 1737 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1739 1738
1740 1739 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1741 1740
1742 1741 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1743 1742 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1744 1743
1745 1744 @classmethod
1746 1745 def get_repo_followers(cls, repo_id):
1747 1746 return cls.query().filter(cls.follows_repo_id == repo_id)
1748 1747
1749 1748
1750 1749 class CacheInvalidation(Base, BaseModel):
1751 1750 __tablename__ = 'cache_invalidation'
1752 1751 __table_args__ = (
1753 1752 UniqueConstraint('cache_key'),
1754 1753 Index('key_idx', 'cache_key'),
1755 1754 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1756 1755 'mysql_charset': 'utf8'},
1757 1756 )
1758 1757 # cache_id, not used
1759 1758 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1760 1759 # cache_key as created by _get_cache_key
1761 1760 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1762 1761 # cache_args is a repo_name
1763 1762 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1764 1763 # instance sets cache_active True when it is caching,
1765 1764 # other instances set cache_active to False to indicate that this cache is invalid
1766 1765 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1767 1766
1768 1767 def __init__(self, cache_key, repo_name=''):
1769 1768 self.cache_key = cache_key
1770 1769 self.cache_args = repo_name
1771 1770 self.cache_active = False
1772 1771
1773 1772 def __unicode__(self):
1774 1773 return u"<%s('%s:%s[%s]')>" % (self.__class__.__name__,
1775 1774 self.cache_id, self.cache_key, self.cache_active)
1776 1775
1777 1776 def _cache_key_partition(self):
1778 1777 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
1779 1778 return prefix, repo_name, suffix
1780 1779
1781 1780 def get_prefix(self):
1782 1781 """
1783 1782 get prefix that might have been used in _get_cache_key to
1784 1783 generate self.cache_key. Only used for informational purposes
1785 1784 in repo_edit.html.
1786 1785 """
1787 1786 # prefix, repo_name, suffix
1788 1787 return self._cache_key_partition()[0]
1789 1788
1790 1789 def get_suffix(self):
1791 1790 """
1792 1791 get suffix that might have been used in _get_cache_key to
1793 1792 generate self.cache_key. Only used for informational purposes
1794 1793 in repo_edit.html.
1795 1794 """
1796 1795 # prefix, repo_name, suffix
1797 1796 return self._cache_key_partition()[2]
1798 1797
1799 1798 @classmethod
1800 1799 def clear_cache(cls):
1801 1800 """
1802 1801 Delete all cache keys from database.
1803 1802 Should only be run when all instances are down and all entries thus stale.
1804 1803 """
1805 1804 cls.query().delete()
1806 1805 Session().commit()
1807 1806
1808 1807 @classmethod
1809 1808 def _get_cache_key(cls, key):
1810 1809 """
1811 1810 Wrapper for generating a unique cache key for this instance and "key".
1812 1811 key must / will start with a repo_name which will be stored in .cache_args .
1813 1812 """
1814 1813 import rhodecode
1815 1814 prefix = rhodecode.CONFIG.get('instance_id', '')
1816 1815 return "%s%s" % (prefix, key)
1817 1816
1818 1817 @classmethod
1819 1818 def set_invalidate(cls, repo_name):
1820 1819 """
1821 1820 Mark all caches of a repo as invalid in the database.
1822 1821 """
1823 1822 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1824 1823
1825 1824 try:
1826 1825 for inv_obj in inv_objs:
1827 1826 log.debug('marking %s key for invalidation based on repo_name=%s'
1828 1827 % (inv_obj, safe_str(repo_name)))
1829 1828 inv_obj.cache_active = False
1830 1829 Session().add(inv_obj)
1831 1830 Session().commit()
1832 1831 except Exception:
1833 1832 log.error(traceback.format_exc())
1834 1833 Session().rollback()
1835 1834
1836 1835 @classmethod
1837 1836 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
1838 1837 """
1839 1838 Mark this cache key as active and currently cached.
1840 1839 Return True if the existing cache registration still was valid.
1841 1840 Return False to indicate that it had been invalidated and caches should be refreshed.
1842 1841 """
1843 1842
1844 1843 key = (repo_name + '_' + kind) if kind else repo_name
1845 1844 cache_key = cls._get_cache_key(key)
1846 1845
1847 1846 if valid_cache_keys and cache_key in valid_cache_keys:
1848 1847 return True
1849 1848
1850 1849 try:
1851 1850 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
1852 1851 if not inv_obj:
1853 1852 inv_obj = CacheInvalidation(cache_key, repo_name)
1854 1853 was_valid = inv_obj.cache_active
1855 1854 inv_obj.cache_active = True
1856 1855 Session().add(inv_obj)
1857 1856 Session().commit()
1858 1857 return was_valid
1859 1858 except Exception:
1860 1859 log.error(traceback.format_exc())
1861 1860 Session().rollback()
1862 1861 return False
1863 1862
1864 1863 @classmethod
1865 1864 def get_valid_cache_keys(cls):
1866 1865 """
1867 1866 Return opaque object with information of which caches still are valid
1868 1867 and can be used without checking for invalidation.
1869 1868 """
1870 1869 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
1871 1870
1872 1871
1873 1872 class ChangesetComment(Base, BaseModel):
1874 1873 __tablename__ = 'changeset_comments'
1875 1874 __table_args__ = (
1876 1875 Index('cc_revision_idx', 'revision'),
1877 1876 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1878 1877 'mysql_charset': 'utf8'},
1879 1878 )
1880 1879 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1881 1880 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1882 1881 revision = Column('revision', String(40), nullable=True)
1883 1882 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1884 1883 line_no = Column('line_no', Unicode(10), nullable=True)
1885 1884 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1886 1885 f_path = Column('f_path', Unicode(1000), nullable=True)
1887 1886 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1888 1887 text = Column('text', UnicodeText(25000), nullable=False)
1889 1888 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1890 1889 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1891 1890
1892 1891 author = relationship('User', lazy='joined')
1893 1892 repo = relationship('Repository')
1894 1893 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1895 1894 pull_request = relationship('PullRequest', lazy='joined')
1896 1895
1897 1896 @classmethod
1898 1897 def get_users(cls, revision=None, pull_request_id=None):
1899 1898 """
1900 1899 Returns user associated with this ChangesetComment. ie those
1901 1900 who actually commented
1902 1901
1903 1902 :param cls:
1904 1903 :param revision:
1905 1904 """
1906 1905 q = Session().query(User)\
1907 1906 .join(ChangesetComment.author)
1908 1907 if revision:
1909 1908 q = q.filter(cls.revision == revision)
1910 1909 elif pull_request_id:
1911 1910 q = q.filter(cls.pull_request_id == pull_request_id)
1912 1911 return q.all()
1913 1912
1914 1913
1915 1914 class ChangesetStatus(Base, BaseModel):
1916 1915 __tablename__ = 'changeset_statuses'
1917 1916 __table_args__ = (
1918 1917 Index('cs_revision_idx', 'revision'),
1919 1918 Index('cs_version_idx', 'version'),
1920 1919 UniqueConstraint('repo_id', 'revision', 'version'),
1921 1920 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1922 1921 'mysql_charset': 'utf8'}
1923 1922 )
1924 1923 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1925 1924 STATUS_APPROVED = 'approved'
1926 1925 STATUS_REJECTED = 'rejected'
1927 1926 STATUS_UNDER_REVIEW = 'under_review'
1928 1927
1929 1928 STATUSES = [
1930 1929 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1931 1930 (STATUS_APPROVED, _("Approved")),
1932 1931 (STATUS_REJECTED, _("Rejected")),
1933 1932 (STATUS_UNDER_REVIEW, _("Under Review")),
1934 1933 ]
1935 1934
1936 1935 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1937 1936 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1938 1937 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1939 1938 revision = Column('revision', String(40), nullable=False)
1940 1939 status = Column('status', String(128), nullable=False, default=DEFAULT)
1941 1940 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1942 1941 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1943 1942 version = Column('version', Integer(), nullable=False, default=0)
1944 1943 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1945 1944
1946 1945 author = relationship('User', lazy='joined')
1947 1946 repo = relationship('Repository')
1948 1947 comment = relationship('ChangesetComment', lazy='joined')
1949 1948 pull_request = relationship('PullRequest', lazy='joined')
1950 1949
1951 1950 def __unicode__(self):
1952 1951 return u"<%s('%s:%s')>" % (
1953 1952 self.__class__.__name__,
1954 1953 self.status, self.author
1955 1954 )
1956 1955
1957 1956 @classmethod
1958 1957 def get_status_lbl(cls, value):
1959 1958 return dict(cls.STATUSES).get(value)
1960 1959
1961 1960 @property
1962 1961 def status_lbl(self):
1963 1962 return ChangesetStatus.get_status_lbl(self.status)
1964 1963
1965 1964
1966 1965 class PullRequest(Base, BaseModel):
1967 1966 __tablename__ = 'pull_requests'
1968 1967 __table_args__ = (
1969 1968 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1970 1969 'mysql_charset': 'utf8'},
1971 1970 )
1972 1971
1973 1972 STATUS_NEW = u'new'
1974 1973 STATUS_OPEN = u'open'
1975 1974 STATUS_CLOSED = u'closed'
1976 1975
1977 1976 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1978 1977 title = Column('title', Unicode(256), nullable=True)
1979 1978 description = Column('description', UnicodeText(10240), nullable=True)
1980 1979 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1981 1980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1982 1981 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1983 1982 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1984 1983 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1985 1984 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1986 1985 org_ref = Column('org_ref', Unicode(256), nullable=False)
1987 1986 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1988 1987 other_ref = Column('other_ref', Unicode(256), nullable=False)
1989 1988
1990 1989 @hybrid_property
1991 1990 def revisions(self):
1992 1991 return self._revisions.split(':')
1993 1992
1994 1993 @revisions.setter
1995 1994 def revisions(self, val):
1996 1995 self._revisions = ':'.join(val)
1997 1996
1998 1997 @property
1999 1998 def org_ref_parts(self):
2000 1999 return self.org_ref.split(':')
2001 2000
2002 2001 @property
2003 2002 def other_ref_parts(self):
2004 2003 return self.other_ref.split(':')
2005 2004
2006 2005 author = relationship('User', lazy='joined')
2007 2006 reviewers = relationship('PullRequestReviewers',
2008 2007 cascade="all, delete, delete-orphan")
2009 2008 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2010 2009 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2011 2010 statuses = relationship('ChangesetStatus')
2012 2011 comments = relationship('ChangesetComment',
2013 2012 cascade="all, delete, delete-orphan")
2014 2013
2015 2014 def is_closed(self):
2016 2015 return self.status == self.STATUS_CLOSED
2017 2016
2018 2017 @property
2019 2018 def last_review_status(self):
2020 2019 return self.statuses[-1].status if self.statuses else ''
2021 2020
2022 2021 def __json__(self):
2023 2022 return dict(
2024 2023 revisions=self.revisions
2025 2024 )
2026 2025
2027 2026
2028 2027 class PullRequestReviewers(Base, BaseModel):
2029 2028 __tablename__ = 'pull_request_reviewers'
2030 2029 __table_args__ = (
2031 2030 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2032 2031 'mysql_charset': 'utf8'},
2033 2032 )
2034 2033
2035 2034 def __init__(self, user=None, pull_request=None):
2036 2035 self.user = user
2037 2036 self.pull_request = pull_request
2038 2037
2039 2038 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
2040 2039 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2041 2040 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
2042 2041
2043 2042 user = relationship('User')
2044 2043 pull_request = relationship('PullRequest')
2045 2044
2046 2045
2047 2046 class Notification(Base, BaseModel):
2048 2047 __tablename__ = 'notifications'
2049 2048 __table_args__ = (
2050 2049 Index('notification_type_idx', 'type'),
2051 2050 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2052 2051 'mysql_charset': 'utf8'},
2053 2052 )
2054 2053
2055 2054 TYPE_CHANGESET_COMMENT = u'cs_comment'
2056 2055 TYPE_MESSAGE = u'message'
2057 2056 TYPE_MENTION = u'mention'
2058 2057 TYPE_REGISTRATION = u'registration'
2059 2058 TYPE_PULL_REQUEST = u'pull_request'
2060 2059 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
2061 2060
2062 2061 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
2063 2062 subject = Column('subject', Unicode(512), nullable=True)
2064 2063 body = Column('body', UnicodeText(50000), nullable=True)
2065 2064 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
2066 2065 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2067 2066 type_ = Column('type', Unicode(256))
2068 2067
2069 2068 created_by_user = relationship('User')
2070 2069 notifications_to_users = relationship('UserNotification', lazy='joined',
2071 2070 cascade="all, delete, delete-orphan")
2072 2071
2073 2072 @property
2074 2073 def recipients(self):
2075 2074 return [x.user for x in UserNotification.query()\
2076 2075 .filter(UserNotification.notification == self)\
2077 2076 .order_by(UserNotification.user_id.asc()).all()]
2078 2077
2079 2078 @classmethod
2080 2079 def create(cls, created_by, subject, body, recipients, type_=None):
2081 2080 if type_ is None:
2082 2081 type_ = Notification.TYPE_MESSAGE
2083 2082
2084 2083 notification = cls()
2085 2084 notification.created_by_user = created_by
2086 2085 notification.subject = subject
2087 2086 notification.body = body
2088 2087 notification.type_ = type_
2089 2088 notification.created_on = datetime.datetime.now()
2090 2089
2091 2090 for u in recipients:
2092 2091 assoc = UserNotification()
2093 2092 assoc.notification = notification
2094 2093 u.notifications.append(assoc)
2095 2094 Session().add(notification)
2096 2095 return notification
2097 2096
2098 2097 @property
2099 2098 def description(self):
2100 2099 from rhodecode.model.notification import NotificationModel
2101 2100 return NotificationModel().make_description(self)
2102 2101
2103 2102
2104 2103 class UserNotification(Base, BaseModel):
2105 2104 __tablename__ = 'user_to_notification'
2106 2105 __table_args__ = (
2107 2106 UniqueConstraint('user_id', 'notification_id'),
2108 2107 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2109 2108 'mysql_charset': 'utf8'}
2110 2109 )
2111 2110 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2112 2111 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2113 2112 read = Column('read', Boolean, default=False)
2114 2113 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2115 2114
2116 2115 user = relationship('User', lazy="joined")
2117 2116 notification = relationship('Notification', lazy="joined",
2118 2117 order_by=lambda: Notification.created_on.desc(),)
2119 2118
2120 2119 def mark_as_read(self):
2121 2120 self.read = True
2122 2121 Session().add(self)
2123 2122
2124 2123
2124 class Gist(Base, BaseModel):
2125 __tablename__ = 'gists'
2126 __table_args__ = (
2127 Index('g_gist_access_id_idx', 'gist_access_id'),
2128 Index('g_created_on_idx', 'created_on'),
2129 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2130 'mysql_charset': 'utf8'}
2131 )
2132 GIST_PUBLIC = u'public'
2133 GIST_PRIVATE = u'private'
2134
2135 gist_id = Column('gist_id', Integer(), primary_key=True)
2136 gist_access_id = Column('gist_access_id', UnicodeText(1024))
2137 gist_description = Column('gist_description', UnicodeText(1024))
2138 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
2139 gist_expires = Column('gist_expires', Float(), nullable=False)
2140 gist_type = Column('gist_type', Unicode(128), nullable=False)
2141 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2142 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2143
2144 owner = relationship('User')
2145
2146 @classmethod
2147 def get_or_404(cls, id_):
2148 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2149 if not res:
2150 raise HTTPNotFound
2151 return res
2152
2153 @classmethod
2154 def get_by_access_id(cls, gist_access_id):
2155 return cls.query().filter(cls.gist_access_id==gist_access_id).scalar()
2156
2157 def gist_url(self):
2158 from pylons import url
2159 return url('gist', id=self.gist_access_id, qualified=True)
2160
2161
2125 2162 class DbMigrateVersion(Base, BaseModel):
2126 2163 __tablename__ = 'db_migrate_version'
2127 2164 __table_args__ = (
2128 2165 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2129 2166 'mysql_charset': 'utf8'},
2130 2167 )
2131 2168 repository_id = Column('repository_id', String(250), primary_key=True)
2132 2169 repository_path = Column('repository_path', Text)
2133 2170 version = Column('version', Integer)
@@ -1,421 +1,434 b''
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 import logging
23 23
24 24 import formencode
25 25 from formencode import All
26 26
27 27 from pylons.i18n.translation import _
28 28
29 29 from rhodecode.model import validators as v
30 30 from rhodecode import BACKENDS
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class LoginForm(formencode.Schema):
36 36 allow_extra_fields = True
37 37 filter_extra_fields = True
38 38 username = v.UnicodeString(
39 39 strip=True,
40 40 min=1,
41 41 not_empty=True,
42 42 messages={
43 43 'empty': _(u'Please enter a login'),
44 44 'tooShort': _(u'Enter a value %(min)i characters long or more')}
45 45 )
46 46
47 47 password = v.UnicodeString(
48 48 strip=False,
49 49 min=3,
50 50 not_empty=True,
51 51 messages={
52 52 'empty': _(u'Please enter a password'),
53 53 'tooShort': _(u'Enter %(min)i characters or more')}
54 54 )
55 55
56 56 remember = v.StringBoolean(if_missing=False)
57 57
58 58 chained_validators = [v.ValidAuth()]
59 59
60 60
61 61 def UserForm(edit=False, old_data={}):
62 62 class _UserForm(formencode.Schema):
63 63 allow_extra_fields = True
64 64 filter_extra_fields = True
65 65 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
66 66 v.ValidUsername(edit, old_data))
67 67 if edit:
68 68 new_password = All(
69 69 v.ValidPassword(),
70 70 v.UnicodeString(strip=False, min=6, not_empty=False)
71 71 )
72 72 password_confirmation = All(
73 73 v.ValidPassword(),
74 74 v.UnicodeString(strip=False, min=6, not_empty=False),
75 75 )
76 76 admin = v.StringBoolean(if_missing=False)
77 77 else:
78 78 password = All(
79 79 v.ValidPassword(),
80 80 v.UnicodeString(strip=False, min=6, not_empty=True)
81 81 )
82 82 password_confirmation = All(
83 83 v.ValidPassword(),
84 84 v.UnicodeString(strip=False, min=6, not_empty=False)
85 85 )
86 86
87 87 active = v.StringBoolean(if_missing=False)
88 88 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
89 89 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
90 90 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
91 91
92 92 chained_validators = [v.ValidPasswordsMatch()]
93 93
94 94 return _UserForm
95 95
96 96
97 97 def UserGroupForm(edit=False, old_data={}, available_members=[]):
98 98 class _UserGroupForm(formencode.Schema):
99 99 allow_extra_fields = True
100 100 filter_extra_fields = True
101 101
102 102 users_group_name = All(
103 103 v.UnicodeString(strip=True, min=1, not_empty=True),
104 104 v.ValidUserGroup(edit, old_data)
105 105 )
106 106
107 107 users_group_active = v.StringBoolean(if_missing=False)
108 108
109 109 if edit:
110 110 users_group_members = v.OneOf(
111 111 available_members, hideList=False, testValueList=True,
112 112 if_missing=None, not_empty=False
113 113 )
114 114
115 115 return _UserGroupForm
116 116
117 117
118 118 def ReposGroupForm(edit=False, old_data={}, available_groups=[],
119 119 can_create_in_root=False):
120 120 class _ReposGroupForm(formencode.Schema):
121 121 allow_extra_fields = True
122 122 filter_extra_fields = False
123 123
124 124 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
125 125 v.SlugifyName())
126 126 group_description = v.UnicodeString(strip=True, min=1,
127 127 not_empty=False)
128 128 if edit:
129 129 #FIXME: do a special check that we cannot move a group to one of
130 130 #it's children
131 131 pass
132 132 group_parent_id = All(v.CanCreateGroup(can_create_in_root),
133 133 v.OneOf(available_groups, hideList=False,
134 134 testValueList=True,
135 135 if_missing=None, not_empty=True))
136 136 enable_locking = v.StringBoolean(if_missing=False)
137 137 chained_validators = [v.ValidReposGroup(edit, old_data)]
138 138
139 139 return _ReposGroupForm
140 140
141 141
142 142 def RegisterForm(edit=False, old_data={}):
143 143 class _RegisterForm(formencode.Schema):
144 144 allow_extra_fields = True
145 145 filter_extra_fields = True
146 146 username = All(
147 147 v.ValidUsername(edit, old_data),
148 148 v.UnicodeString(strip=True, min=1, not_empty=True)
149 149 )
150 150 password = All(
151 151 v.ValidPassword(),
152 152 v.UnicodeString(strip=False, min=6, not_empty=True)
153 153 )
154 154 password_confirmation = All(
155 155 v.ValidPassword(),
156 156 v.UnicodeString(strip=False, min=6, not_empty=True)
157 157 )
158 158 active = v.StringBoolean(if_missing=False)
159 159 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
160 160 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
161 161 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
162 162
163 163 chained_validators = [v.ValidPasswordsMatch()]
164 164
165 165 return _RegisterForm
166 166
167 167
168 168 def PasswordResetForm():
169 169 class _PasswordResetForm(formencode.Schema):
170 170 allow_extra_fields = True
171 171 filter_extra_fields = True
172 172 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
173 173 return _PasswordResetForm
174 174
175 175
176 176 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
177 177 repo_groups=[], landing_revs=[]):
178 178 class _RepoForm(formencode.Schema):
179 179 allow_extra_fields = True
180 180 filter_extra_fields = False
181 181 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
182 182 v.SlugifyName())
183 183 repo_group = All(v.CanWriteGroup(old_data),
184 184 v.OneOf(repo_groups, hideList=True))
185 185 repo_type = v.OneOf(supported_backends)
186 186 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
187 187 repo_private = v.StringBoolean(if_missing=False)
188 188 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
189 189 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
190 190
191 191 repo_enable_statistics = v.StringBoolean(if_missing=False)
192 192 repo_enable_downloads = v.StringBoolean(if_missing=False)
193 193 repo_enable_locking = v.StringBoolean(if_missing=False)
194 194
195 195 if edit:
196 196 #this is repo owner
197 197 user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser())
198 198
199 199 chained_validators = [v.ValidCloneUri(),
200 200 v.ValidRepoName(edit, old_data)]
201 201 return _RepoForm
202 202
203 203
204 204 def RepoPermsForm():
205 205 class _RepoPermsForm(formencode.Schema):
206 206 allow_extra_fields = True
207 207 filter_extra_fields = False
208 208 chained_validators = [v.ValidPerms(type_='repo')]
209 209 return _RepoPermsForm
210 210
211 211
212 212 def RepoGroupPermsForm():
213 213 class _RepoGroupPermsForm(formencode.Schema):
214 214 allow_extra_fields = True
215 215 filter_extra_fields = False
216 216 recursive = v.StringBoolean(if_missing=False)
217 217 chained_validators = [v.ValidPerms(type_='repo_group')]
218 218 return _RepoGroupPermsForm
219 219
220 220
221 221 def UserGroupPermsForm():
222 222 class _UserPermsForm(formencode.Schema):
223 223 allow_extra_fields = True
224 224 filter_extra_fields = False
225 225 chained_validators = [v.ValidPerms(type_='user_group')]
226 226 return _UserPermsForm
227 227
228 228
229 229 def RepoFieldForm():
230 230 class _RepoFieldForm(formencode.Schema):
231 231 filter_extra_fields = True
232 232 allow_extra_fields = True
233 233
234 234 new_field_key = All(v.FieldKey(),
235 235 v.UnicodeString(strip=True, min=3, not_empty=True))
236 236 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
237 237 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
238 238 if_missing='str')
239 239 new_field_label = v.UnicodeString(not_empty=False)
240 240 new_field_desc = v.UnicodeString(not_empty=False)
241 241
242 242 return _RepoFieldForm
243 243
244 244
245 245 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
246 246 repo_groups=[], landing_revs=[]):
247 247 class _RepoForkForm(formencode.Schema):
248 248 allow_extra_fields = True
249 249 filter_extra_fields = False
250 250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
251 251 v.SlugifyName())
252 252 repo_group = All(v.CanWriteGroup(),
253 253 v.OneOf(repo_groups, hideList=True))
254 254 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
255 255 description = v.UnicodeString(strip=True, min=1, not_empty=True)
256 256 private = v.StringBoolean(if_missing=False)
257 257 copy_permissions = v.StringBoolean(if_missing=False)
258 258 update_after_clone = v.StringBoolean(if_missing=False)
259 259 fork_parent_id = v.UnicodeString()
260 260 chained_validators = [v.ValidForkName(edit, old_data)]
261 261 landing_rev = v.OneOf(landing_revs, hideList=True)
262 262
263 263 return _RepoForkForm
264 264
265 265
266 266 def ApplicationSettingsForm():
267 267 class _ApplicationSettingsForm(formencode.Schema):
268 268 allow_extra_fields = True
269 269 filter_extra_fields = False
270 270 rhodecode_title = v.UnicodeString(strip=True, min=1, not_empty=True)
271 271 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
272 272 rhodecode_ga_code = v.UnicodeString(strip=True, min=1, not_empty=False)
273 273
274 274 return _ApplicationSettingsForm
275 275
276 276
277 277 def ApplicationVisualisationForm():
278 278 class _ApplicationVisualisationForm(formencode.Schema):
279 279 allow_extra_fields = True
280 280 filter_extra_fields = False
281 281 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
282 282 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
283 283 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
284 284
285 285 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
286 286 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
287 287
288 288 return _ApplicationVisualisationForm
289 289
290 290
291 291 def ApplicationUiSettingsForm():
292 292 class _ApplicationUiSettingsForm(formencode.Schema):
293 293 allow_extra_fields = True
294 294 filter_extra_fields = False
295 295 web_push_ssl = v.StringBoolean(if_missing=False)
296 296 paths_root_path = All(
297 297 v.ValidPath(),
298 298 v.UnicodeString(strip=True, min=1, not_empty=True)
299 299 )
300 300 hooks_changegroup_update = v.StringBoolean(if_missing=False)
301 301 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
302 302 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
303 303 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
304 304
305 305 extensions_largefiles = v.StringBoolean(if_missing=False)
306 306 extensions_hgsubversion = v.StringBoolean(if_missing=False)
307 307 extensions_hggit = v.StringBoolean(if_missing=False)
308 308
309 309 return _ApplicationUiSettingsForm
310 310
311 311
312 312 def DefaultPermissionsForm(repo_perms_choices, group_perms_choices,
313 313 user_group_perms_choices, create_choices,
314 314 repo_group_create_choices, user_group_create_choices,
315 315 fork_choices, register_choices, extern_activate_choices):
316 316 class _DefaultPermissionsForm(formencode.Schema):
317 317 allow_extra_fields = True
318 318 filter_extra_fields = True
319 319 overwrite_default_repo = v.StringBoolean(if_missing=False)
320 320 overwrite_default_group = v.StringBoolean(if_missing=False)
321 321 overwrite_default_user_group = v.StringBoolean(if_missing=False)
322 322 anonymous = v.StringBoolean(if_missing=False)
323 323 default_repo_perm = v.OneOf(repo_perms_choices)
324 324 default_group_perm = v.OneOf(group_perms_choices)
325 325 default_user_group_perm = v.OneOf(user_group_perms_choices)
326 326
327 327 default_repo_create = v.OneOf(create_choices)
328 328 default_user_group_create = v.OneOf(user_group_create_choices)
329 329 #default_repo_group_create = v.OneOf(repo_group_create_choices) #not impl. yet
330 330 default_fork = v.OneOf(fork_choices)
331 331
332 332 default_register = v.OneOf(register_choices)
333 333 default_extern_activate = v.OneOf(extern_activate_choices)
334 334 return _DefaultPermissionsForm
335 335
336 336
337 337 def CustomDefaultPermissionsForm():
338 338 class _CustomDefaultPermissionsForm(formencode.Schema):
339 339 filter_extra_fields = True
340 340 allow_extra_fields = True
341 341 inherit_default_permissions = v.StringBoolean(if_missing=False)
342 342
343 343 create_repo_perm = v.StringBoolean(if_missing=False)
344 344 create_user_group_perm = v.StringBoolean(if_missing=False)
345 345 #create_repo_group_perm Impl. later
346 346
347 347 fork_repo_perm = v.StringBoolean(if_missing=False)
348 348
349 349 return _CustomDefaultPermissionsForm
350 350
351 351
352 352 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
353 353 class _DefaultsForm(formencode.Schema):
354 354 allow_extra_fields = True
355 355 filter_extra_fields = True
356 356 default_repo_type = v.OneOf(supported_backends)
357 357 default_repo_private = v.StringBoolean(if_missing=False)
358 358 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
359 359 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
360 360 default_repo_enable_locking = v.StringBoolean(if_missing=False)
361 361
362 362 return _DefaultsForm
363 363
364 364
365 365 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices,
366 366 tls_kind_choices):
367 367 class _LdapSettingsForm(formencode.Schema):
368 368 allow_extra_fields = True
369 369 filter_extra_fields = True
370 370 #pre_validators = [LdapLibValidator]
371 371 ldap_active = v.StringBoolean(if_missing=False)
372 372 ldap_host = v.UnicodeString(strip=True,)
373 373 ldap_port = v.Number(strip=True,)
374 374 ldap_tls_kind = v.OneOf(tls_kind_choices)
375 375 ldap_tls_reqcert = v.OneOf(tls_reqcert_choices)
376 376 ldap_dn_user = v.UnicodeString(strip=True,)
377 377 ldap_dn_pass = v.UnicodeString(strip=True,)
378 378 ldap_base_dn = v.UnicodeString(strip=True,)
379 379 ldap_filter = v.UnicodeString(strip=True,)
380 380 ldap_search_scope = v.OneOf(search_scope_choices)
381 381 ldap_attr_login = v.AttrLoginValidator()(not_empty=True)
382 382 ldap_attr_firstname = v.UnicodeString(strip=True,)
383 383 ldap_attr_lastname = v.UnicodeString(strip=True,)
384 384 ldap_attr_email = v.UnicodeString(strip=True,)
385 385
386 386 return _LdapSettingsForm
387 387
388 388
389 389 def UserExtraEmailForm():
390 390 class _UserExtraEmailForm(formencode.Schema):
391 391 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
392 392 return _UserExtraEmailForm
393 393
394 394
395 395 def UserExtraIpForm():
396 396 class _UserExtraIpForm(formencode.Schema):
397 397 ip = v.ValidIp()(not_empty=True)
398 398 return _UserExtraIpForm
399 399
400 400
401 401 def PullRequestForm(repo_id):
402 402 class _PullRequestForm(formencode.Schema):
403 403 allow_extra_fields = True
404 404 filter_extra_fields = True
405 405
406 406 user = v.UnicodeString(strip=True, required=True)
407 407 org_repo = v.UnicodeString(strip=True, required=True)
408 408 org_ref = v.UnicodeString(strip=True, required=True)
409 409 other_repo = v.UnicodeString(strip=True, required=True)
410 410 other_ref = v.UnicodeString(strip=True, required=True)
411 411 revisions = All(#v.NotReviewedRevisions(repo_id)(),
412 412 v.UniqueList(not_empty=True))
413 413 review_members = v.UniqueList(not_empty=True)
414 414
415 415 pullrequest_title = v.UnicodeString(strip=True, required=True, min=3)
416 416 pullrequest_desc = v.UnicodeString(strip=True, required=False)
417 417
418 418 ancestor_rev = v.UnicodeString(strip=True, required=True)
419 419 merge_rev = v.UnicodeString(strip=True, required=True)
420 420
421 421 return _PullRequestForm
422
423
424 def GistForm(lifetime_options):
425 class _GistForm(formencode.Schema):
426
427 filename = v.UnicodeString(strip=True, required=False)
428 description = v.UnicodeString(required=False, if_missing='')
429 lifetime = v.OneOf(lifetime_options)
430 content = v.UnicodeString(required=True, not_empty=True)
431 public = v.UnicodeString(required=False, if_missing='')
432 private = v.UnicodeString(required=False, if_missing='')
433
434 return _GistForm
@@ -1,746 +1,755 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.repo
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repository model for rhodecode
7 7
8 8 :created_on: Jun 5, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import shutil
28 28 import logging
29 29 import traceback
30 30 from datetime import datetime
31 31
32 32 from rhodecode.lib.vcs.backends import get_backend
33 33 from rhodecode.lib.compat import json
34 34 from rhodecode.lib.utils2 import LazyProperty, safe_str, safe_unicode,\
35 35 remove_prefix, obfuscate_url_pw
36 36 from rhodecode.lib.caching_query import FromCache
37 37 from rhodecode.lib.hooks import log_create_repository, log_delete_repository
38 38
39 39 from rhodecode.model import BaseModel
40 40 from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
41 41 Statistics, UserGroup, UserGroupRepoToPerm, RhodeCodeUi, RepoGroup,\
42 42 RhodeCodeSetting, RepositoryField
43 43 from rhodecode.lib import helpers as h
44 44 from rhodecode.lib.auth import HasRepoPermissionAny, HasUserGroupPermissionAny
45 45 from rhodecode.lib.exceptions import AttachedForksError
46 46 from rhodecode.model.scm import UserGroupList
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class RepoModel(BaseModel):
52 52
53 53 cls = Repository
54 54 URL_SEPARATOR = Repository.url_sep()
55 55
56 56 def _get_user_group(self, users_group):
57 57 return self._get_instance(UserGroup, users_group,
58 58 callback=UserGroup.get_by_group_name)
59 59
60 60 def _get_repo_group(self, repos_group):
61 61 return self._get_instance(RepoGroup, repos_group,
62 62 callback=RepoGroup.get_by_group_name)
63 63
64 64 def _create_default_perms(self, repository, private):
65 65 # create default permission
66 66 default = 'repository.read'
67 67 def_user = User.get_default_user()
68 68 for p in def_user.user_perms:
69 69 if p.permission.permission_name.startswith('repository.'):
70 70 default = p.permission.permission_name
71 71 break
72 72
73 73 default_perm = 'repository.none' if private else default
74 74
75 75 repo_to_perm = UserRepoToPerm()
76 76 repo_to_perm.permission = Permission.get_by_key(default_perm)
77 77
78 78 repo_to_perm.repository = repository
79 79 repo_to_perm.user_id = def_user.user_id
80 80
81 81 return repo_to_perm
82 82
83 83 @LazyProperty
84 84 def repos_path(self):
85 85 """
86 86 Get's the repositories root path from database
87 87 """
88 88
89 89 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
90 90 return q.ui_value
91 91
92 92 def get(self, repo_id, cache=False):
93 93 repo = self.sa.query(Repository)\
94 94 .filter(Repository.repo_id == repo_id)
95 95
96 96 if cache:
97 97 repo = repo.options(FromCache("sql_cache_short",
98 98 "get_repo_%s" % repo_id))
99 99 return repo.scalar()
100 100
101 101 def get_repo(self, repository):
102 102 return self._get_repo(repository)
103 103
104 104 def get_by_repo_name(self, repo_name, cache=False):
105 105 repo = self.sa.query(Repository)\
106 106 .filter(Repository.repo_name == repo_name)
107 107
108 108 if cache:
109 109 repo = repo.options(FromCache("sql_cache_short",
110 110 "get_repo_%s" % repo_name))
111 111 return repo.scalar()
112 112
113 113 def get_all_user_repos(self, user):
114 114 """
115 115 Get's all repositories that user have at least read access
116 116
117 117 :param user:
118 :type user:
119 118 """
120 119 from rhodecode.lib.auth import AuthUser
121 120 user = self._get_user(user)
122 121 repos = AuthUser(user_id=user.user_id).permissions['repositories']
123 122 access_check = lambda r: r[1] in ['repository.read',
124 123 'repository.write',
125 124 'repository.admin']
126 125 repos = [x[0] for x in filter(access_check, repos.items())]
127 126 return Repository.query().filter(Repository.repo_name.in_(repos))
128 127
129 128 def get_users_js(self):
130 129 users = self.sa.query(User).filter(User.active == True).all()
131 130 return json.dumps([
132 131 {
133 132 'id': u.user_id,
134 133 'fname': u.name,
135 134 'lname': u.lastname,
136 135 'nname': u.username,
137 136 'gravatar_lnk': h.gravatar_url(u.email, 14)
138 137 } for u in users]
139 138 )
140 139
141 140 def get_users_groups_js(self):
142 141 users_groups = self.sa.query(UserGroup)\
143 142 .filter(UserGroup.users_group_active == True).all()
144 143 users_groups = UserGroupList(users_groups, perm_set=['usergroup.read',
145 144 'usergroup.write',
146 145 'usergroup.admin'])
147 146 return json.dumps([
148 147 {
149 148 'id': gr.users_group_id,
150 149 'grname': gr.users_group_name,
151 150 'grmembers': len(gr.members),
152 151 } for gr in users_groups]
153 152 )
154 153
155 154 @classmethod
156 155 def _render_datatable(cls, tmpl, *args, **kwargs):
157 156 import rhodecode
158 157 from pylons import tmpl_context as c
159 158 from pylons.i18n.translation import _
160 159
161 160 _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
162 161 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
163 162
164 163 tmpl = template.get_def(tmpl)
165 164 kwargs.update(dict(_=_, h=h, c=c))
166 165 return tmpl.render(*args, **kwargs)
167 166
168 167 @classmethod
169 168 def update_repoinfo(cls, repositories=None):
170 169 if not repositories:
171 170 repositories = Repository.getAll()
172 171 for repo in repositories:
173 172 repo.update_changeset_cache()
174 173
175 174 def get_repos_as_dict(self, repos_list=None, admin=False, perm_check=True,
176 175 super_user_actions=False):
177 176 _render = self._render_datatable
178 177 from pylons import tmpl_context as c
179 178
180 179 def quick_menu(repo_name):
181 180 return _render('quick_menu', repo_name)
182 181
183 182 def repo_lnk(name, rtype, private, fork_of):
184 183 return _render('repo_name', name, rtype, private, fork_of,
185 184 short_name=not admin, admin=False)
186 185
187 186 def last_change(last_change):
188 187 return _render("last_change", last_change)
189 188
190 189 def rss_lnk(repo_name):
191 190 return _render("rss", repo_name)
192 191
193 192 def atom_lnk(repo_name):
194 193 return _render("atom", repo_name)
195 194
196 195 def last_rev(repo_name, cs_cache):
197 196 return _render('revision', repo_name, cs_cache.get('revision'),
198 197 cs_cache.get('raw_id'), cs_cache.get('author'),
199 198 cs_cache.get('message'))
200 199
201 200 def desc(desc):
202 201 if c.visual.stylify_metatags:
203 202 return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
204 203 else:
205 204 return h.urlify_text(h.truncate(desc, 60))
206 205
207 206 def repo_actions(repo_name):
208 207 return _render('repo_actions', repo_name, super_user_actions)
209 208
210 209 def owner_actions(user_id, username):
211 210 return _render('user_name', user_id, username)
212 211
213 212 repos_data = []
214 213 for repo in repos_list:
215 214 if perm_check:
216 215 # check permission at this level
217 216 if not HasRepoPermissionAny(
218 217 'repository.read', 'repository.write', 'repository.admin'
219 218 )(repo.repo_name, 'get_repos_as_dict check'):
220 219 continue
221 220 cs_cache = repo.changeset_cache
222 221 row = {
223 222 "menu": quick_menu(repo.repo_name),
224 223 "raw_name": repo.repo_name.lower(),
225 224 "name": repo_lnk(repo.repo_name, repo.repo_type,
226 225 repo.private, repo.fork),
227 226 "last_change": last_change(repo.last_db_change),
228 227 "last_changeset": last_rev(repo.repo_name, cs_cache),
229 228 "raw_tip": cs_cache.get('revision'),
230 229 "desc": desc(repo.description),
231 230 "owner": h.person(repo.user.username),
232 231 "rss": rss_lnk(repo.repo_name),
233 232 "atom": atom_lnk(repo.repo_name),
234 233
235 234 }
236 235 if admin:
237 236 row.update({
238 237 "action": repo_actions(repo.repo_name),
239 238 "owner": owner_actions(repo.user.user_id,
240 239 h.person(repo.user.username))
241 240 })
242 241 repos_data.append(row)
243 242
244 243 return {
245 244 "totalRecords": len(repos_list),
246 245 "startIndex": 0,
247 246 "sort": "name",
248 247 "dir": "asc",
249 248 "records": repos_data
250 249 }
251 250
252 251 def _get_defaults(self, repo_name):
253 252 """
254 253 Get's information about repository, and returns a dict for
255 254 usage in forms
256 255
257 256 :param repo_name:
258 257 """
259 258
260 259 repo_info = Repository.get_by_repo_name(repo_name)
261 260
262 261 if repo_info is None:
263 262 return None
264 263
265 264 defaults = repo_info.get_dict()
266 265 group, repo_name, repo_name_full = repo_info.groups_and_repo
267 266 defaults['repo_name'] = repo_name
268 267 defaults['repo_group'] = getattr(group[-1] if group else None,
269 268 'group_id', None)
270 269
271 270 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
272 271 (1, 'repo_description'), (1, 'repo_enable_locking'),
273 272 (1, 'repo_landing_rev'), (0, 'clone_uri'),
274 273 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
275 274 attr = k
276 275 if strip:
277 276 attr = remove_prefix(k, 'repo_')
278 277
279 278 defaults[k] = defaults[attr]
280 279
281 280 # fill owner
282 281 if repo_info.user:
283 282 defaults.update({'user': repo_info.user.username})
284 283 else:
285 284 replacement_user = User.query().filter(User.admin ==
286 285 True).first().username
287 286 defaults.update({'user': replacement_user})
288 287
289 288 # fill repository users
290 289 for p in repo_info.repo_to_perm:
291 290 defaults.update({'u_perm_%s' % p.user.username:
292 291 p.permission.permission_name})
293 292
294 293 # fill repository groups
295 294 for p in repo_info.users_group_to_perm:
296 295 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
297 296 p.permission.permission_name})
298 297
299 298 return defaults
300 299
301 300 def update(self, org_repo_name, **kwargs):
302 301 try:
303 302 cur_repo = self.get_by_repo_name(org_repo_name, cache=False)
304 303
305 304 if 'user' in kwargs:
306 305 cur_repo.user = User.get_by_username(kwargs['user'])
307 306
308 307 if 'repo_group' in kwargs:
309 308 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
310 309
311 310 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
312 311 (1, 'repo_description'), (1, 'repo_enable_locking'),
313 312 (1, 'repo_landing_rev'), (0, 'clone_uri'),
314 313 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
315 314 if k in kwargs:
316 315 val = kwargs[k]
317 316 if strip:
318 317 k = remove_prefix(k, 'repo_')
319 318 setattr(cur_repo, k, val)
320 319
321 320 new_name = cur_repo.get_new_name(kwargs['repo_name'])
322 321 cur_repo.repo_name = new_name
323 322 #if private flag is set, reset default permission to NONE
324 323
325 324 if kwargs.get('repo_private'):
326 325 EMPTY_PERM = 'repository.none'
327 326 RepoModel().grant_user_permission(
328 327 repo=cur_repo, user='default', perm=EMPTY_PERM
329 328 )
330 329 #handle extra fields
331 330 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
332 331 k = RepositoryField.un_prefix_key(field)
333 332 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
334 333 if ex_field:
335 334 ex_field.field_value = kwargs[field]
336 335 self.sa.add(ex_field)
337 336 self.sa.add(cur_repo)
338 337
339 338 if org_repo_name != new_name:
340 339 # rename repository
341 340 self.__rename_repo(old=org_repo_name, new=new_name)
342 341
343 342 return cur_repo
344 343 except Exception:
345 344 log.error(traceback.format_exc())
346 345 raise
347 346
348 347 def create_repo(self, repo_name, repo_type, description, owner,
349 348 private=False, clone_uri=None, repos_group=None,
350 349 landing_rev='tip', just_db=False, fork_of=None,
351 350 copy_fork_permissions=False, enable_statistics=False,
352 351 enable_locking=False, enable_downloads=False):
353 352 """
354 353 Create repository
355 354
356 355 """
357 356 from rhodecode.model.scm import ScmModel
358 357
359 358 owner = self._get_user(owner)
360 359 fork_of = self._get_repo(fork_of)
361 360 repos_group = self._get_repo_group(repos_group)
362 361 try:
363 362
364 363 # repo name is just a name of repository
365 364 # while repo_name_full is a full qualified name that is combined
366 365 # with name and path of group
367 366 repo_name_full = repo_name
368 367 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
369 368
370 369 new_repo = Repository()
371 370 new_repo.enable_statistics = False
372 371 new_repo.repo_name = repo_name_full
373 372 new_repo.repo_type = repo_type
374 373 new_repo.user = owner
375 374 new_repo.group = repos_group
376 375 new_repo.description = description or repo_name
377 376 new_repo.private = private
378 377 new_repo.clone_uri = clone_uri
379 378 new_repo.landing_rev = landing_rev
380 379
381 380 new_repo.enable_statistics = enable_statistics
382 381 new_repo.enable_locking = enable_locking
383 382 new_repo.enable_downloads = enable_downloads
384 383
385 384 if repos_group:
386 385 new_repo.enable_locking = repos_group.enable_locking
387 386
388 387 if fork_of:
389 388 parent_repo = fork_of
390 389 new_repo.fork = parent_repo
391 390
392 391 self.sa.add(new_repo)
393 392
394 393 if fork_of:
395 394 if copy_fork_permissions:
396 395 repo = fork_of
397 396 user_perms = UserRepoToPerm.query()\
398 397 .filter(UserRepoToPerm.repository == repo).all()
399 398 group_perms = UserGroupRepoToPerm.query()\
400 399 .filter(UserGroupRepoToPerm.repository == repo).all()
401 400
402 401 for perm in user_perms:
403 402 UserRepoToPerm.create(perm.user, new_repo,
404 403 perm.permission)
405 404
406 405 for perm in group_perms:
407 406 UserGroupRepoToPerm.create(perm.users_group, new_repo,
408 407 perm.permission)
409 408 else:
410 409 perm_obj = self._create_default_perms(new_repo, private)
411 410 self.sa.add(perm_obj)
412 411 else:
413 412 perm_obj = self._create_default_perms(new_repo, private)
414 413 self.sa.add(perm_obj)
415 414
416 415 if not just_db:
417 416 self.__create_repo(repo_name, repo_type,
418 417 repos_group,
419 418 clone_uri)
420 419 log_create_repository(new_repo.get_dict(),
421 420 created_by=owner.username)
422 421
423 422 # now automatically start following this repository as owner
424 423 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
425 424 owner.user_id)
426 425 return new_repo
427 426 except Exception:
428 427 log.error(traceback.format_exc())
429 428 raise
430 429
431 430 def create(self, form_data, cur_user, just_db=False, fork=None):
432 431 """
433 432 Backward compatibility function, just a wrapper on top of create_repo
434 433
435 434 :param form_data:
436 435 :param cur_user:
437 436 :param just_db:
438 437 :param fork:
439 438 """
440 439 owner = cur_user
441 440 repo_name = form_data['repo_name_full']
442 441 repo_type = form_data['repo_type']
443 442 description = form_data['repo_description']
444 443 private = form_data['repo_private']
445 444 clone_uri = form_data.get('clone_uri')
446 445 repos_group = form_data['repo_group']
447 446 landing_rev = form_data['repo_landing_rev']
448 447 copy_fork_permissions = form_data.get('copy_permissions')
449 448 fork_of = form_data.get('fork_parent_id')
450 449
451 450 ## repo creation defaults, private and repo_type are filled in form
452 451 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
453 452 enable_statistics = defs.get('repo_enable_statistics')
454 453 enable_locking = defs.get('repo_enable_locking')
455 454 enable_downloads = defs.get('repo_enable_downloads')
456 455
457 456 return self.create_repo(
458 457 repo_name, repo_type, description, owner, private, clone_uri,
459 458 repos_group, landing_rev, just_db, fork_of, copy_fork_permissions,
460 459 enable_statistics, enable_locking, enable_downloads
461 460 )
462 461
463 462 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
464 463 check_perms=True):
465 464 if not perms_new:
466 465 perms_new = []
467 466 if not perms_updates:
468 467 perms_updates = []
469 468
470 469 # update permissions
471 470 for member, perm, member_type in perms_updates:
472 471 if member_type == 'user':
473 472 # this updates existing one
474 473 self.grant_user_permission(
475 474 repo=repo, user=member, perm=perm
476 475 )
477 476 else:
478 477 #check if we have permissions to alter this usergroup
479 478 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
480 479 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member):
481 480 self.grant_users_group_permission(
482 481 repo=repo, group_name=member, perm=perm
483 482 )
484 483 # set new permissions
485 484 for member, perm, member_type in perms_new:
486 485 if member_type == 'user':
487 486 self.grant_user_permission(
488 487 repo=repo, user=member, perm=perm
489 488 )
490 489 else:
491 490 #check if we have permissions to alter this usergroup
492 491 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
493 492 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member):
494 493 self.grant_users_group_permission(
495 494 repo=repo, group_name=member, perm=perm
496 495 )
497 496
498 497 def create_fork(self, form_data, cur_user):
499 498 """
500 499 Simple wrapper into executing celery task for fork creation
501 500
502 501 :param form_data:
503 502 :param cur_user:
504 503 """
505 504 from rhodecode.lib.celerylib import tasks, run_task
506 505 run_task(tasks.create_repo_fork, form_data, cur_user)
507 506
508 507 def delete(self, repo, forks=None, fs_remove=True):
509 508 """
510 509 Delete given repository, forks parameter defines what do do with
511 510 attached forks. Throws AttachedForksError if deleted repo has attached
512 511 forks
513 512
514 513 :param repo:
515 514 :param forks: str 'delete' or 'detach'
516 515 :param fs_remove: remove(archive) repo from filesystem
517 516 """
518 517 repo = self._get_repo(repo)
519 518 if repo:
520 519 if forks == 'detach':
521 520 for r in repo.forks:
522 521 r.fork = None
523 522 self.sa.add(r)
524 523 elif forks == 'delete':
525 524 for r in repo.forks:
526 525 self.delete(r, forks='delete')
527 526 elif [f for f in repo.forks]:
528 527 raise AttachedForksError()
529 528
530 529 old_repo_dict = repo.get_dict()
531 530 owner = repo.user
532 531 try:
533 532 self.sa.delete(repo)
534 533 if fs_remove:
535 534 self.__delete_repo(repo)
536 535 else:
537 536 log.debug('skipping removal from filesystem')
538 537 log_delete_repository(old_repo_dict,
539 538 deleted_by=owner.username)
540 539 except Exception:
541 540 log.error(traceback.format_exc())
542 541 raise
543 542
544 543 def grant_user_permission(self, repo, user, perm):
545 544 """
546 545 Grant permission for user on given repository, or update existing one
547 546 if found
548 547
549 548 :param repo: Instance of Repository, repository_id, or repository name
550 549 :param user: Instance of User, user_id or username
551 550 :param perm: Instance of Permission, or permission_name
552 551 """
553 552 user = self._get_user(user)
554 553 repo = self._get_repo(repo)
555 554 permission = self._get_perm(perm)
556 555
557 556 # check if we have that permission already
558 557 obj = self.sa.query(UserRepoToPerm)\
559 558 .filter(UserRepoToPerm.user == user)\
560 559 .filter(UserRepoToPerm.repository == repo)\
561 560 .scalar()
562 561 if obj is None:
563 562 # create new !
564 563 obj = UserRepoToPerm()
565 564 obj.repository = repo
566 565 obj.user = user
567 566 obj.permission = permission
568 567 self.sa.add(obj)
569 568 log.debug('Granted perm %s to %s on %s' % (perm, user, repo))
570 569
571 570 def revoke_user_permission(self, repo, user):
572 571 """
573 572 Revoke permission for user on given repository
574 573
575 574 :param repo: Instance of Repository, repository_id, or repository name
576 575 :param user: Instance of User, user_id or username
577 576 """
578 577
579 578 user = self._get_user(user)
580 579 repo = self._get_repo(repo)
581 580
582 581 obj = self.sa.query(UserRepoToPerm)\
583 582 .filter(UserRepoToPerm.repository == repo)\
584 583 .filter(UserRepoToPerm.user == user)\
585 584 .scalar()
586 585 if obj:
587 586 self.sa.delete(obj)
588 587 log.debug('Revoked perm on %s on %s' % (repo, user))
589 588
590 589 def grant_users_group_permission(self, repo, group_name, perm):
591 590 """
592 591 Grant permission for user group on given repository, or update
593 592 existing one if found
594 593
595 594 :param repo: Instance of Repository, repository_id, or repository name
596 595 :param group_name: Instance of UserGroup, users_group_id,
597 596 or user group name
598 597 :param perm: Instance of Permission, or permission_name
599 598 """
600 599 repo = self._get_repo(repo)
601 600 group_name = self._get_user_group(group_name)
602 601 permission = self._get_perm(perm)
603 602
604 603 # check if we have that permission already
605 604 obj = self.sa.query(UserGroupRepoToPerm)\
606 605 .filter(UserGroupRepoToPerm.users_group == group_name)\
607 606 .filter(UserGroupRepoToPerm.repository == repo)\
608 607 .scalar()
609 608
610 609 if obj is None:
611 610 # create new
612 611 obj = UserGroupRepoToPerm()
613 612
614 613 obj.repository = repo
615 614 obj.users_group = group_name
616 615 obj.permission = permission
617 616 self.sa.add(obj)
618 617 log.debug('Granted perm %s to %s on %s' % (perm, group_name, repo))
619 618
620 619 def revoke_users_group_permission(self, repo, group_name):
621 620 """
622 621 Revoke permission for user group on given repository
623 622
624 623 :param repo: Instance of Repository, repository_id, or repository name
625 624 :param group_name: Instance of UserGroup, users_group_id,
626 625 or user group name
627 626 """
628 627 repo = self._get_repo(repo)
629 628 group_name = self._get_user_group(group_name)
630 629
631 630 obj = self.sa.query(UserGroupRepoToPerm)\
632 631 .filter(UserGroupRepoToPerm.repository == repo)\
633 632 .filter(UserGroupRepoToPerm.users_group == group_name)\
634 633 .scalar()
635 634 if obj:
636 635 self.sa.delete(obj)
637 636 log.debug('Revoked perm to %s on %s' % (repo, group_name))
638 637
639 638 def delete_stats(self, repo_name):
640 639 """
641 640 removes stats for given repo
642 641
643 642 :param repo_name:
644 643 """
645 644 repo = self._get_repo(repo_name)
646 645 try:
647 646 obj = self.sa.query(Statistics)\
648 647 .filter(Statistics.repository == repo).scalar()
649 648 if obj:
650 649 self.sa.delete(obj)
651 650 except Exception:
652 651 log.error(traceback.format_exc())
653 652 raise
654 653
655 def __create_repo(self, repo_name, alias, parent, clone_uri=False):
654 def _create_repo(self, repo_name, alias, parent, clone_uri=False,
655 repo_store_location=None):
656 return self.__create_repo(repo_name, alias, parent, clone_uri,
657 repo_store_location)
658
659 def __create_repo(self, repo_name, alias, parent, clone_uri=False,
660 repo_store_location=None):
656 661 """
657 662 makes repository on filesystem. It's group aware means it'll create
658 663 a repository within a group, and alter the paths accordingly of
659 664 group location
660 665
661 666 :param repo_name:
662 667 :param alias:
663 668 :param parent_id:
664 669 :param clone_uri:
670 :param repo_path:
665 671 """
666 672 from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
667 673 from rhodecode.model.scm import ScmModel
668 674
669 675 if parent:
670 676 new_parent_path = os.sep.join(parent.full_path_splitted)
671 677 else:
672 678 new_parent_path = ''
673
679 if repo_store_location:
680 _paths = [repo_store_location]
681 else:
682 _paths = [self.repos_path, new_parent_path, repo_name]
674 683 # we need to make it str for mercurial
675 repo_path = os.path.join(*map(lambda x: safe_str(x),
676 [self.repos_path, new_parent_path, repo_name]))
684 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
677 685
678 686 # check if this path is not a repository
679 687 if is_valid_repo(repo_path, self.repos_path):
680 688 raise Exception('This path %s is a valid repository' % repo_path)
681 689
682 690 # check if this path is a group
683 691 if is_valid_repos_group(repo_path, self.repos_path):
684 692 raise Exception('This path %s is a valid group' % repo_path)
685 693
686 694 log.info('creating repo %s in %s @ %s' % (
687 695 repo_name, safe_unicode(repo_path),
688 696 obfuscate_url_pw(clone_uri)
689 697 )
690 698 )
691 699 backend = get_backend(alias)
692 700 if alias == 'hg':
693 backend(repo_path, create=True, src_url=clone_uri)
701 repo = backend(repo_path, create=True, src_url=clone_uri)
694 702 elif alias == 'git':
695 r = backend(repo_path, create=True, src_url=clone_uri, bare=True)
703 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
696 704 # add rhodecode hook into this repo
697 ScmModel().install_git_hook(repo=r)
705 ScmModel().install_git_hook(repo=repo)
698 706 else:
699 707 raise Exception('Undefined alias %s' % alias)
708 return repo
700 709
701 710 def __rename_repo(self, old, new):
702 711 """
703 712 renames repository on filesystem
704 713
705 714 :param old: old name
706 715 :param new: new name
707 716 """
708 717 log.info('renaming repo from %s to %s' % (old, new))
709 718
710 719 old_path = os.path.join(self.repos_path, old)
711 720 new_path = os.path.join(self.repos_path, new)
712 721 if os.path.isdir(new_path):
713 722 raise Exception(
714 723 'Was trying to rename to already existing dir %s' % new_path
715 724 )
716 725 shutil.move(old_path, new_path)
717 726
718 727 def __delete_repo(self, repo):
719 728 """
720 729 removes repo from filesystem, the removal is acctually made by
721 730 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
722 731 repository is no longer valid for rhodecode, can be undeleted later on
723 732 by reverting the renames on this repository
724 733
725 734 :param repo: repo object
726 735 """
727 736 rm_path = os.path.join(self.repos_path, repo.repo_name)
728 737 log.info("Removing %s" % (rm_path))
729 738 # disable hg/git internal that it doesn't get detected as repo
730 739 alias = repo.repo_type
731 740
732 741 bare = getattr(repo.scm_instance, 'bare', False)
733 742
734 743 if not bare:
735 744 # skip this for bare git repos
736 745 shutil.move(os.path.join(rm_path, '.%s' % alias),
737 746 os.path.join(rm_path, 'rm__.%s' % alias))
738 747 # disable repo
739 748 _now = datetime.now()
740 749 _ms = str(_now.microsecond).rjust(6, '0')
741 750 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
742 751 repo.just_name)
743 752 if repo.group:
744 753 args = repo.group.full_path_splitted + [_d]
745 754 _d = os.path.join(*args)
746 755 shutil.move(rm_path, os.path.join(self.repos_path, _d))
@@ -1,695 +1,727 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import re
28 28 import time
29 29 import traceback
30 30 import logging
31 31 import cStringIO
32 32 import pkg_resources
33 33 from os.path import dirname as dn, join as jn
34 34
35 35 from sqlalchemy import func
36 36 from pylons.i18n.translation import _
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 42 from rhodecode.lib.vcs.nodes import FileNode
43 43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 44
45 45 from rhodecode import BACKENDS
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url,\
48 48 _set_extras
49 49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny,\
50 50 HasUserGroupPermissionAnyDecorator, HasUserGroupPermissionAny
51 51 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
52 52 action_logger, REMOVED_REPO_PAT
53 53 from rhodecode.model import BaseModel
54 54 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
55 55 UserFollowing, UserLog, User, RepoGroup, PullRequest
56 56 from rhodecode.lib.hooks import log_push_action
57 from rhodecode.lib.exceptions import NonRelativePathError
57 58
58 59 log = logging.getLogger(__name__)
59 60
60 61
61 62 class UserTemp(object):
62 63 def __init__(self, user_id):
63 64 self.user_id = user_id
64 65
65 66 def __repr__(self):
66 67 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
67 68
68 69
69 70 class RepoTemp(object):
70 71 def __init__(self, repo_id):
71 72 self.repo_id = repo_id
72 73
73 74 def __repr__(self):
74 75 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
75 76
76 77
77 78 class CachedRepoList(object):
78 79 """
79 80 Cached repo list, uses in-memory cache after initialization, that is
80 81 super fast
81 82 """
82 83
83 84 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
84 85 self.db_repo_list = db_repo_list
85 86 self.repos_path = repos_path
86 87 self.order_by = order_by
87 88 self.reversed = (order_by or '').startswith('-')
88 89 if not perm_set:
89 90 perm_set = ['repository.read', 'repository.write',
90 91 'repository.admin']
91 92 self.perm_set = perm_set
92 93
93 94 def __len__(self):
94 95 return len(self.db_repo_list)
95 96
96 97 def __repr__(self):
97 98 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
98 99
99 100 def __iter__(self):
100 101 # pre-propagated valid_cache_keys to save executing select statements
101 102 # for each repo
102 103 valid_cache_keys = CacheInvalidation.get_valid_cache_keys()
103 104
104 105 for dbr in self.db_repo_list:
105 106 scmr = dbr.scm_instance_cached(valid_cache_keys)
106 107 # check permission at this level
107 108 if not HasRepoPermissionAny(
108 109 *self.perm_set)(dbr.repo_name, 'get repo check'):
109 110 continue
110 111
111 112 try:
112 113 last_change = scmr.last_change
113 114 tip = h.get_changeset_safe(scmr, 'tip')
114 115 except Exception:
115 116 log.error(
116 117 '%s this repository is present in database but it '
117 118 'cannot be created as an scm instance, org_exc:%s'
118 119 % (dbr.repo_name, traceback.format_exc())
119 120 )
120 121 continue
121 122
122 123 tmp_d = {}
123 124 tmp_d['name'] = dbr.repo_name
124 125 tmp_d['name_sort'] = tmp_d['name'].lower()
125 126 tmp_d['raw_name'] = tmp_d['name'].lower()
126 127 tmp_d['description'] = dbr.description
127 128 tmp_d['description_sort'] = tmp_d['description'].lower()
128 129 tmp_d['last_change'] = last_change
129 130 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
130 131 tmp_d['tip'] = tip.raw_id
131 132 tmp_d['tip_sort'] = tip.revision
132 133 tmp_d['rev'] = tip.revision
133 134 tmp_d['contact'] = dbr.user.full_contact
134 135 tmp_d['contact_sort'] = tmp_d['contact']
135 136 tmp_d['owner_sort'] = tmp_d['contact']
136 137 tmp_d['repo_archives'] = list(scmr._get_archives())
137 138 tmp_d['last_msg'] = tip.message
138 139 tmp_d['author'] = tip.author
139 140 tmp_d['dbrepo'] = dbr.get_dict()
140 141 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
141 142 yield tmp_d
142 143
143 144
144 145 class SimpleCachedRepoList(CachedRepoList):
145 146 """
146 147 Lighter version of CachedRepoList without the scm initialisation
147 148 """
148 149
149 150 def __iter__(self):
150 151 for dbr in self.db_repo_list:
151 152 # check permission at this level
152 153 if not HasRepoPermissionAny(
153 154 *self.perm_set)(dbr.repo_name, 'get repo check'):
154 155 continue
155 156
156 157 tmp_d = {}
157 158 tmp_d['name'] = dbr.repo_name
158 159 tmp_d['name_sort'] = tmp_d['name'].lower()
159 160 tmp_d['raw_name'] = tmp_d['name'].lower()
160 161 tmp_d['description'] = dbr.description
161 162 tmp_d['description_sort'] = tmp_d['description'].lower()
162 163 tmp_d['dbrepo'] = dbr.get_dict()
163 164 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
164 165 yield tmp_d
165 166
166 167
167 168 class _PermCheckIterator(object):
168 169 def __init__(self, obj_list, obj_attr, perm_set, perm_checker):
169 170 """
170 171 Creates iterator from given list of objects, additionally
171 172 checking permission for them from perm_set var
172 173
173 174 :param obj_list: list of db objects
174 175 :param obj_attr: attribute of object to pass into perm_checker
175 176 :param perm_set: list of permissions to check
176 177 :param perm_checker: callable to check permissions against
177 178 """
178 179 self.obj_list = obj_list
179 180 self.obj_attr = obj_attr
180 181 self.perm_set = perm_set
181 182 self.perm_checker = perm_checker
182 183
183 184 def __len__(self):
184 185 return len(self.obj_list)
185 186
186 187 def __repr__(self):
187 188 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
188 189
189 190 def __iter__(self):
190 191 for db_obj in self.obj_list:
191 192 # check permission at this level
192 193 name = getattr(db_obj, self.obj_attr, None)
193 194 if not self.perm_checker(*self.perm_set)(name, self.__class__.__name__):
194 195 continue
195 196
196 197 yield db_obj
197 198
198 199
199 200 class RepoGroupList(_PermCheckIterator):
200 201
201 202 def __init__(self, db_repo_group_list, perm_set=None):
202 203 if not perm_set:
203 204 perm_set = ['group.read', 'group.write', 'group.admin']
204 205
205 206 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
206 207 obj_attr='group_name', perm_set=perm_set,
207 208 perm_checker=HasReposGroupPermissionAny)
208 209
209 210
210 211 class UserGroupList(_PermCheckIterator):
211 212
212 213 def __init__(self, db_user_group_list, perm_set=None):
213 214 if not perm_set:
214 215 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
215 216
216 217 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
217 218 obj_attr='users_group_name', perm_set=perm_set,
218 219 perm_checker=HasUserGroupPermissionAny)
219 220
220 221
221 222 class ScmModel(BaseModel):
222 223 """
223 224 Generic Scm Model
224 225 """
225 226
226 227 def __get_repo(self, instance):
227 228 cls = Repository
228 229 if isinstance(instance, cls):
229 230 return instance
230 231 elif isinstance(instance, int) or safe_str(instance).isdigit():
231 232 return cls.get(instance)
232 233 elif isinstance(instance, basestring):
233 234 return cls.get_by_repo_name(instance)
234 235 elif instance:
235 236 raise Exception('given object must be int, basestr or Instance'
236 237 ' of %s got %s' % (type(cls), type(instance)))
237 238
238 239 @LazyProperty
239 240 def repos_path(self):
240 241 """
241 242 Get's the repositories root path from database
242 243 """
243 244
244 245 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
245 246
246 247 return q.ui_value
247 248
248 249 def repo_scan(self, repos_path=None):
249 250 """
250 251 Listing of repositories in given path. This path should not be a
251 252 repository itself. Return a dictionary of repository objects
252 253
253 254 :param repos_path: path to directory containing repositories
254 255 """
255 256
256 257 if repos_path is None:
257 258 repos_path = self.repos_path
258 259
259 260 log.info('scanning for repositories in %s' % repos_path)
260 261
261 262 baseui = make_ui('db')
262 263 repos = {}
263 264
264 265 for name, path in get_filesystem_repos(repos_path, recursive=True):
265 266 # name need to be decomposed and put back together using the /
266 267 # since this is internal storage separator for rhodecode
267 268 name = Repository.normalize_repo_name(name)
268 269
269 270 try:
270 271 if name in repos:
271 272 raise RepositoryError('Duplicate repository name %s '
272 273 'found in %s' % (name, path))
273 274 else:
274 275
275 276 klass = get_backend(path[0])
276 277
277 278 if path[0] == 'hg' and path[0] in BACKENDS.keys():
278 279 repos[name] = klass(safe_str(path[1]), baseui=baseui)
279 280
280 281 if path[0] == 'git' and path[0] in BACKENDS.keys():
281 282 repos[name] = klass(path[1])
282 283 except OSError:
283 284 continue
284 285 log.debug('found %s paths with repositories' % (len(repos)))
285 286 return repos
286 287
287 288 def get_repos(self, all_repos=None, sort_key=None, simple=False):
288 289 """
289 290 Get all repos from db and for each repo create it's
290 291 backend instance and fill that backed with information from database
291 292
292 293 :param all_repos: list of repository names as strings
293 294 give specific repositories list, good for filtering
294 295
295 296 :param sort_key: initial sorting of repos
296 297 :param simple: use SimpleCachedList - one without the SCM info
297 298 """
298 299 if all_repos is None:
299 300 all_repos = self.sa.query(Repository)\
300 301 .filter(Repository.group_id == None)\
301 302 .order_by(func.lower(Repository.repo_name)).all()
302 303 if simple:
303 304 repo_iter = SimpleCachedRepoList(all_repos,
304 305 repos_path=self.repos_path,
305 306 order_by=sort_key)
306 307 else:
307 308 repo_iter = CachedRepoList(all_repos,
308 309 repos_path=self.repos_path,
309 310 order_by=sort_key)
310 311
311 312 return repo_iter
312 313
313 314 def get_repos_groups(self, all_groups=None):
314 315 if all_groups is None:
315 316 all_groups = RepoGroup.query()\
316 317 .filter(RepoGroup.group_parent_id == None).all()
317 318 return [x for x in RepoGroupList(all_groups)]
318 319
319 320 def mark_for_invalidation(self, repo_name):
320 321 """
321 322 Mark caches of this repo invalid in the database.
322 323
323 324 :param repo_name: the repo for which caches should be marked invalid
324 325 """
325 326 CacheInvalidation.set_invalidate(repo_name)
326 327 repo = Repository.get_by_repo_name(repo_name)
327 328 if repo:
328 329 repo.update_changeset_cache()
329 330
330 331 def toggle_following_repo(self, follow_repo_id, user_id):
331 332
332 333 f = self.sa.query(UserFollowing)\
333 334 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
334 335 .filter(UserFollowing.user_id == user_id).scalar()
335 336
336 337 if f is not None:
337 338 try:
338 339 self.sa.delete(f)
339 340 action_logger(UserTemp(user_id),
340 341 'stopped_following_repo',
341 342 RepoTemp(follow_repo_id))
342 343 return
343 344 except Exception:
344 345 log.error(traceback.format_exc())
345 346 raise
346 347
347 348 try:
348 349 f = UserFollowing()
349 350 f.user_id = user_id
350 351 f.follows_repo_id = follow_repo_id
351 352 self.sa.add(f)
352 353
353 354 action_logger(UserTemp(user_id),
354 355 'started_following_repo',
355 356 RepoTemp(follow_repo_id))
356 357 except Exception:
357 358 log.error(traceback.format_exc())
358 359 raise
359 360
360 361 def toggle_following_user(self, follow_user_id, user_id):
361 362 f = self.sa.query(UserFollowing)\
362 363 .filter(UserFollowing.follows_user_id == follow_user_id)\
363 364 .filter(UserFollowing.user_id == user_id).scalar()
364 365
365 366 if f is not None:
366 367 try:
367 368 self.sa.delete(f)
368 369 return
369 370 except Exception:
370 371 log.error(traceback.format_exc())
371 372 raise
372 373
373 374 try:
374 375 f = UserFollowing()
375 376 f.user_id = user_id
376 377 f.follows_user_id = follow_user_id
377 378 self.sa.add(f)
378 379 except Exception:
379 380 log.error(traceback.format_exc())
380 381 raise
381 382
382 383 def is_following_repo(self, repo_name, user_id, cache=False):
383 384 r = self.sa.query(Repository)\
384 385 .filter(Repository.repo_name == repo_name).scalar()
385 386
386 387 f = self.sa.query(UserFollowing)\
387 388 .filter(UserFollowing.follows_repository == r)\
388 389 .filter(UserFollowing.user_id == user_id).scalar()
389 390
390 391 return f is not None
391 392
392 393 def is_following_user(self, username, user_id, cache=False):
393 394 u = User.get_by_username(username)
394 395
395 396 f = self.sa.query(UserFollowing)\
396 397 .filter(UserFollowing.follows_user == u)\
397 398 .filter(UserFollowing.user_id == user_id).scalar()
398 399
399 400 return f is not None
400 401
401 402 def get_followers(self, repo):
402 403 repo = self._get_repo(repo)
403 404
404 405 return self.sa.query(UserFollowing)\
405 406 .filter(UserFollowing.follows_repository == repo).count()
406 407
407 408 def get_forks(self, repo):
408 409 repo = self._get_repo(repo)
409 410 return self.sa.query(Repository)\
410 411 .filter(Repository.fork == repo).count()
411 412
412 413 def get_pull_requests(self, repo):
413 414 repo = self._get_repo(repo)
414 415 return self.sa.query(PullRequest)\
415 416 .filter(PullRequest.other_repo == repo)\
416 417 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
417 418
418 419 def mark_as_fork(self, repo, fork, user):
419 420 repo = self.__get_repo(repo)
420 421 fork = self.__get_repo(fork)
421 422 if fork and repo.repo_id == fork.repo_id:
422 423 raise Exception("Cannot set repository as fork of itself")
423 424 repo.fork = fork
424 425 self.sa.add(repo)
425 426 return repo
426 427
427 428 def _handle_rc_scm_extras(self, username, repo_name, repo_alias):
428 429 from rhodecode import CONFIG
429 430 from rhodecode.lib.base import _get_ip_addr
430 431 try:
431 432 from pylons import request
432 433 environ = request.environ
433 434 except TypeError:
434 435 # we might use this outside of request context, let's fake the
435 436 # environ data
436 437 from webob import Request
437 438 environ = Request.blank('').environ
438 439 extras = {
439 440 'ip': _get_ip_addr(environ),
440 441 'username': username,
441 442 'action': 'push_local',
442 443 'repository': repo_name,
443 444 'scm': repo_alias,
444 445 'config': CONFIG['__file__'],
445 446 'server_url': get_server_url(environ),
446 447 'make_lock': None,
447 448 'locked_by': [None, None]
448 449 }
449 450 _set_extras(extras)
450 451
451 452 def _handle_push(self, repo, username, action, repo_name, revisions):
452 453 """
453 454 Triggers push action hooks
454 455
455 456 :param repo: SCM repo
456 457 :param username: username who pushes
457 458 :param action: push/push_loca/push_remote
458 459 :param repo_name: name of repo
459 460 :param revisions: list of revisions that we pushed
460 461 """
461 462 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
462 463 _scm_repo = repo._repo
463 464 # trigger push hook
464 465 if repo.alias == 'hg':
465 466 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
466 467 elif repo.alias == 'git':
467 468 log_push_action(None, _scm_repo, _git_revs=revisions)
468 469
469 470 def _get_IMC_module(self, scm_type):
470 471 """
471 472 Returns InMemoryCommit class based on scm_type
472 473
473 474 :param scm_type:
474 475 """
475 476 if scm_type == 'hg':
476 477 from rhodecode.lib.vcs.backends.hg import \
477 478 MercurialInMemoryChangeset as IMC
478 479 elif scm_type == 'git':
479 480 from rhodecode.lib.vcs.backends.git import \
480 481 GitInMemoryChangeset as IMC
481 482 return IMC
482 483
483 484 def pull_changes(self, repo, username):
484 485 dbrepo = self.__get_repo(repo)
485 486 clone_uri = dbrepo.clone_uri
486 487 if not clone_uri:
487 488 raise Exception("This repository doesn't have a clone uri")
488 489
489 490 repo = dbrepo.scm_instance
490 491 repo_name = dbrepo.repo_name
491 492 try:
492 493 if repo.alias == 'git':
493 494 repo.fetch(clone_uri)
494 495 else:
495 496 repo.pull(clone_uri)
496 497 self.mark_for_invalidation(repo_name)
497 498 except Exception:
498 499 log.error(traceback.format_exc())
499 500 raise
500 501
501 502 def commit_change(self, repo, repo_name, cs, user, author, message,
502 503 content, f_path):
503 504 """
504 505 Commits changes
505 506
506 507 :param repo: SCM instance
507 508
508 509 """
509 510 user = self._get_user(user)
510 511 IMC = self._get_IMC_module(repo.alias)
511 512
512 513 # decoding here will force that we have proper encoded values
513 514 # in any other case this will throw exceptions and deny commit
514 515 content = safe_str(content)
515 516 path = safe_str(f_path)
516 517 # message and author needs to be unicode
517 518 # proper backend should then translate that into required type
518 519 message = safe_unicode(message)
519 520 author = safe_unicode(author)
520 521 imc = IMC(repo)
521 522 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
522 523 tip = imc.commit(message=message,
523 524 author=author,
524 525 parents=[cs], branch=cs.branch)
525 526
526 527 self.mark_for_invalidation(repo_name)
527 528 self._handle_push(repo,
528 529 username=user.username,
529 530 action='push_local',
530 531 repo_name=repo_name,
531 532 revisions=[tip.raw_id])
532 533 return tip
533 534
534 def create_node(self, repo, repo_name, cs, user, author, message, content,
535 f_path):
535 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
536 author=None, trigger_push_hook=True):
537 """
538 Commits given multiple nodes into repo
539
540 :param user: RhodeCode User object or user_id, the commiter
541 :param repo: RhodeCode Repository object
542 :param message: commit message
543 :param nodes: mapping {filename:{'content':content},...}
544 :param parent_cs: parent changeset, can be empty than it's initial commit
545 :param author: author of commit, cna be different that commiter only for git
546 :param trigger_push_hook: trigger push hooks
547
548 :returns: new commited changeset
549 """
550
536 551 user = self._get_user(user)
537 IMC = self._get_IMC_module(repo.alias)
552 scm_instance = repo.scm_instance_no_cache()
538 553
554 processed_nodes = []
555 for f_path in nodes:
556 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
557 raise NonRelativePathError('%s is not an relative path' % f_path)
558 if f_path:
559 f_path = os.path.normpath(f_path)
560 f_path = safe_str(f_path)
561 content = nodes[f_path]['content']
539 562 # decoding here will force that we have proper encoded values
540 563 # in any other case this will throw exceptions and deny commit
541 564 if isinstance(content, (basestring,)):
542 565 content = safe_str(content)
543 566 elif isinstance(content, (file, cStringIO.OutputType,)):
544 567 content = content.read()
545 568 else:
546 569 raise Exception('Content is of unrecognized type %s' % (
547 570 type(content)
548 571 ))
572 processed_nodes.append((f_path, content))
549 573
550 574 message = safe_unicode(message)
551 author = safe_unicode(author)
552 path = safe_str(f_path)
553 m = IMC(repo)
575 commiter = user.full_contact
576 author = safe_unicode(author) if author else commiter
554 577
555 if isinstance(cs, EmptyChangeset):
578 IMC = self._get_IMC_module(scm_instance.alias)
579 imc = IMC(scm_instance)
580
581 if not parent_cs:
582 parent_cs = EmptyChangeset(alias=scm_instance.alias)
583
584 if isinstance(parent_cs, EmptyChangeset):
556 585 # EmptyChangeset means we we're editing empty repository
557 586 parents = None
558 587 else:
559 parents = [cs]
588 parents = [parent_cs]
589 # add multiple nodes
590 for path, content in processed_nodes:
591 imc.add(FileNode(path, content=content))
560 592
561 m.add(FileNode(path, content=content))
562 tip = m.commit(message=message,
593 tip = imc.commit(message=message,
563 594 author=author,
564 parents=parents, branch=cs.branch)
595 parents=parents,
596 branch=parent_cs.branch)
565 597
566 self.mark_for_invalidation(repo_name)
567 self._handle_push(repo,
598 self.mark_for_invalidation(repo.repo_name)
599 if trigger_push_hook:
600 self._handle_push(scm_instance,
568 601 username=user.username,
569 602 action='push_local',
570 repo_name=repo_name,
603 repo_name=repo.repo_name,
571 604 revisions=[tip.raw_id])
572 605 return tip
573 606
574 607 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
575 608 """
576 609 recursive walk in root dir and return a set of all path in that dir
577 610 based on repository walk function
578 611
579 612 :param repo_name: name of repository
580 613 :param revision: revision for which to list nodes
581 614 :param root_path: root path to list
582 615 :param flat: return as a list, if False returns a dict with decription
583 616
584 617 """
585 618 _files = list()
586 619 _dirs = list()
587 620 try:
588 621 _repo = self.__get_repo(repo_name)
589 622 changeset = _repo.scm_instance.get_changeset(revision)
590 623 root_path = root_path.lstrip('/')
591 624 for topnode, dirs, files in changeset.walk(root_path):
592 625 for f in files:
593 626 _files.append(f.path if flat else {"name": f.path,
594 627 "type": "file"})
595 628 for d in dirs:
596 629 _dirs.append(d.path if flat else {"name": d.path,
597 630 "type": "dir"})
598 631 except RepositoryError:
599 632 log.debug(traceback.format_exc())
600 633 raise
601 634
602 635 return _dirs, _files
603 636
604 637 def get_unread_journal(self):
605 638 return self.sa.query(UserLog).count()
606 639
607 640 def get_repo_landing_revs(self, repo=None):
608 641 """
609 642 Generates select option with tags branches and bookmarks (for hg only)
610 643 grouped by type
611 644
612 645 :param repo:
613 :type repo:
614 646 """
615 647
616 648 hist_l = []
617 649 choices = []
618 650 repo = self.__get_repo(repo)
619 651 hist_l.append(['tip', _('latest tip')])
620 652 choices.append('tip')
621 653 if not repo:
622 654 return choices, hist_l
623 655
624 656 repo = repo.scm_instance
625 657
626 658 branches_group = ([(k, k) for k, v in
627 659 repo.branches.iteritems()], _("Branches"))
628 660 hist_l.append(branches_group)
629 661 choices.extend([x[0] for x in branches_group[0]])
630 662
631 663 if repo.alias == 'hg':
632 664 bookmarks_group = ([(k, k) for k, v in
633 665 repo.bookmarks.iteritems()], _("Bookmarks"))
634 666 hist_l.append(bookmarks_group)
635 667 choices.extend([x[0] for x in bookmarks_group[0]])
636 668
637 669 tags_group = ([(k, k) for k, v in
638 670 repo.tags.iteritems()], _("Tags"))
639 671 hist_l.append(tags_group)
640 672 choices.extend([x[0] for x in tags_group[0]])
641 673
642 674 return choices, hist_l
643 675
644 676 def install_git_hook(self, repo, force_create=False):
645 677 """
646 678 Creates a rhodecode hook inside a git repository
647 679
648 680 :param repo: Instance of VCS repo
649 681 :param force_create: Create even if same name hook exists
650 682 """
651 683
652 684 loc = jn(repo.path, 'hooks')
653 685 if not repo.bare:
654 686 loc = jn(repo.path, '.git', 'hooks')
655 687 if not os.path.isdir(loc):
656 688 os.makedirs(loc)
657 689
658 690 tmpl_post = pkg_resources.resource_string(
659 691 'rhodecode', jn('config', 'post_receive_tmpl.py')
660 692 )
661 693 tmpl_pre = pkg_resources.resource_string(
662 694 'rhodecode', jn('config', 'pre_receive_tmpl.py')
663 695 )
664 696
665 697 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
666 698 _hook_file = jn(loc, '%s-receive' % h_type)
667 699 _rhodecode_hook = False
668 700 log.debug('Installing git hook in repo %s' % repo)
669 701 if os.path.exists(_hook_file):
670 702 # let's take a look at this hook, maybe it's rhodecode ?
671 703 log.debug('hook exists, checking if it is from rhodecode')
672 704 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
673 705 with open(_hook_file, 'rb') as f:
674 706 data = f.read()
675 707 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
676 708 % 'RC_HOOK_VER').search(data)
677 709 if matches:
678 710 try:
679 711 ver = matches.groups()[0]
680 712 log.debug('got %s it is rhodecode' % (ver))
681 713 _rhodecode_hook = True
682 714 except Exception:
683 715 log.error(traceback.format_exc())
684 716 else:
685 717 # there is no hook in this dir, so we want to create one
686 718 _rhodecode_hook = True
687 719
688 720 if _rhodecode_hook or force_create:
689 721 log.debug('writing %s hook file !' % h_type)
690 722 with open(_hook_file, 'wb') as f:
691 723 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
692 724 f.write(tmpl)
693 725 os.chmod(_hook_file, 0755)
694 726 else:
695 727 log.debug('skipping writing hook file')
@@ -1,809 +1,809 b''
1 1 """
2 2 Set of generic validators
3 3 """
4 4 import os
5 5 import re
6 6 import formencode
7 7 import logging
8 8 from collections import defaultdict
9 9 from pylons.i18n.translation import _
10 10 from webhelpers.pylonslib.secure_form import authentication_token
11 11
12 12 from formencode.validators import (
13 13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 NotEmpty, IPAddress, CIDR
14 NotEmpty, IPAddress, CIDR, String, FancyValidator
15 15 )
16 16 from rhodecode.lib.compat import OrderedSet
17 17 from rhodecode.lib import ipaddr
18 18 from rhodecode.lib.utils import repo_name_slug
19 19 from rhodecode.lib.utils2 import safe_int, str2bool
20 20 from rhodecode.model.db import RepoGroup, Repository, UserGroup, User,\
21 21 ChangesetStatus
22 22 from rhodecode.lib.exceptions import LdapImportError
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.auth import HasReposGroupPermissionAny, HasPermissionAny
25 25
26 26 # silence warnings and pylint
27 27 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
28 NotEmpty, IPAddress, CIDR
28 NotEmpty, IPAddress, CIDR, String, FancyValidator
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class UniqueList(formencode.FancyValidator):
34 34 """
35 35 Unique List !
36 36 """
37 37 messages = dict(
38 38 empty=_('Value cannot be an empty list'),
39 39 missing_value=_('Value cannot be an empty list'),
40 40 )
41 41
42 42 def _to_python(self, value, state):
43 43 if isinstance(value, list):
44 44 return value
45 45 elif isinstance(value, set):
46 46 return list(value)
47 47 elif isinstance(value, tuple):
48 48 return list(value)
49 49 elif value is None:
50 50 return []
51 51 else:
52 52 return [value]
53 53
54 54 def empty_value(self, value):
55 55 return []
56 56
57 57
58 58 class StateObj(object):
59 59 """
60 60 this is needed to translate the messages using _() in validators
61 61 """
62 62 _ = staticmethod(_)
63 63
64 64
65 65 def M(self, key, state=None, **kwargs):
66 66 """
67 67 returns string from self.message based on given key,
68 68 passed kw params are used to substitute %(named)s params inside
69 69 translated strings
70 70
71 71 :param msg:
72 72 :param state:
73 73 """
74 74 if state is None:
75 75 state = StateObj()
76 76 else:
77 77 state._ = staticmethod(_)
78 78 #inject validator into state object
79 79 return self.message(key, state, **kwargs)
80 80
81 81
82 82 def ValidUsername(edit=False, old_data={}):
83 83 class _validator(formencode.validators.FancyValidator):
84 84 messages = {
85 85 'username_exists': _(u'Username "%(username)s" already exists'),
86 86 'system_invalid_username':
87 87 _(u'Username "%(username)s" is forbidden'),
88 88 'invalid_username':
89 89 _(u'Username may only contain alphanumeric characters '
90 90 'underscores, periods or dashes and must begin with '
91 91 'alphanumeric character')
92 92 }
93 93
94 94 def validate_python(self, value, state):
95 95 if value in ['default', 'new_user']:
96 96 msg = M(self, 'system_invalid_username', state, username=value)
97 97 raise formencode.Invalid(msg, value, state)
98 98 #check if user is unique
99 99 old_un = None
100 100 if edit:
101 101 old_un = User.get(old_data.get('user_id')).username
102 102
103 103 if old_un != value or not edit:
104 104 if User.get_by_username(value, case_insensitive=True):
105 105 msg = M(self, 'username_exists', state, username=value)
106 106 raise formencode.Invalid(msg, value, state)
107 107
108 108 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
109 109 msg = M(self, 'invalid_username', state)
110 110 raise formencode.Invalid(msg, value, state)
111 111 return _validator
112 112
113 113
114 114 def ValidRepoUser():
115 115 class _validator(formencode.validators.FancyValidator):
116 116 messages = {
117 117 'invalid_username': _(u'Username %(username)s is not valid')
118 118 }
119 119
120 120 def validate_python(self, value, state):
121 121 try:
122 122 User.query().filter(User.active == True)\
123 123 .filter(User.username == value).one()
124 124 except Exception:
125 125 msg = M(self, 'invalid_username', state, username=value)
126 126 raise formencode.Invalid(msg, value, state,
127 127 error_dict=dict(username=msg)
128 128 )
129 129
130 130 return _validator
131 131
132 132
133 133 def ValidUserGroup(edit=False, old_data={}):
134 134 class _validator(formencode.validators.FancyValidator):
135 135 messages = {
136 136 'invalid_group': _(u'Invalid user group name'),
137 137 'group_exist': _(u'User group "%(usergroup)s" already exists'),
138 138 'invalid_usergroup_name':
139 139 _(u'user group name may only contain alphanumeric '
140 140 'characters underscores, periods or dashes and must begin '
141 141 'with alphanumeric character')
142 142 }
143 143
144 144 def validate_python(self, value, state):
145 145 if value in ['default']:
146 146 msg = M(self, 'invalid_group', state)
147 147 raise formencode.Invalid(msg, value, state,
148 148 error_dict=dict(users_group_name=msg)
149 149 )
150 150 #check if group is unique
151 151 old_ugname = None
152 152 if edit:
153 153 old_id = old_data.get('users_group_id')
154 154 old_ugname = UserGroup.get(old_id).users_group_name
155 155
156 156 if old_ugname != value or not edit:
157 157 is_existing_group = UserGroup.get_by_group_name(value,
158 158 case_insensitive=True)
159 159 if is_existing_group:
160 160 msg = M(self, 'group_exist', state, usergroup=value)
161 161 raise formencode.Invalid(msg, value, state,
162 162 error_dict=dict(users_group_name=msg)
163 163 )
164 164
165 165 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
166 166 msg = M(self, 'invalid_usergroup_name', state)
167 167 raise formencode.Invalid(msg, value, state,
168 168 error_dict=dict(users_group_name=msg)
169 169 )
170 170
171 171 return _validator
172 172
173 173
174 174 def ValidReposGroup(edit=False, old_data={}):
175 175 class _validator(formencode.validators.FancyValidator):
176 176 messages = {
177 177 'group_parent_id': _(u'Cannot assign this group as parent'),
178 178 'group_exists': _(u'Group "%(group_name)s" already exists'),
179 179 'repo_exists':
180 180 _(u'Repository with name "%(group_name)s" already exists')
181 181 }
182 182
183 183 def validate_python(self, value, state):
184 184 # TODO WRITE VALIDATIONS
185 185 group_name = value.get('group_name')
186 186 group_parent_id = value.get('group_parent_id')
187 187
188 188 # slugify repo group just in case :)
189 189 slug = repo_name_slug(group_name)
190 190
191 191 # check for parent of self
192 192 parent_of_self = lambda: (
193 193 old_data['group_id'] == int(group_parent_id)
194 194 if group_parent_id else False
195 195 )
196 196 if edit and parent_of_self():
197 197 msg = M(self, 'group_parent_id', state)
198 198 raise formencode.Invalid(msg, value, state,
199 199 error_dict=dict(group_parent_id=msg)
200 200 )
201 201
202 202 old_gname = None
203 203 if edit:
204 204 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
205 205
206 206 if old_gname != group_name or not edit:
207 207
208 208 # check group
209 209 gr = RepoGroup.query()\
210 210 .filter(RepoGroup.group_name == slug)\
211 211 .filter(RepoGroup.group_parent_id == group_parent_id)\
212 212 .scalar()
213 213
214 214 if gr:
215 215 msg = M(self, 'group_exists', state, group_name=slug)
216 216 raise formencode.Invalid(msg, value, state,
217 217 error_dict=dict(group_name=msg)
218 218 )
219 219
220 220 # check for same repo
221 221 repo = Repository.query()\
222 222 .filter(Repository.repo_name == slug)\
223 223 .scalar()
224 224
225 225 if repo:
226 226 msg = M(self, 'repo_exists', state, group_name=slug)
227 227 raise formencode.Invalid(msg, value, state,
228 228 error_dict=dict(group_name=msg)
229 229 )
230 230
231 231 return _validator
232 232
233 233
234 234 def ValidPassword():
235 235 class _validator(formencode.validators.FancyValidator):
236 236 messages = {
237 237 'invalid_password':
238 238 _(u'Invalid characters (non-ascii) in password')
239 239 }
240 240
241 241 def validate_python(self, value, state):
242 242 try:
243 243 (value or '').decode('ascii')
244 244 except UnicodeError:
245 245 msg = M(self, 'invalid_password', state)
246 246 raise formencode.Invalid(msg, value, state,)
247 247 return _validator
248 248
249 249
250 250 def ValidPasswordsMatch():
251 251 class _validator(formencode.validators.FancyValidator):
252 252 messages = {
253 253 'password_mismatch': _(u'Passwords do not match'),
254 254 }
255 255
256 256 def validate_python(self, value, state):
257 257
258 258 pass_val = value.get('password') or value.get('new_password')
259 259 if pass_val != value['password_confirmation']:
260 260 msg = M(self, 'password_mismatch', state)
261 261 raise formencode.Invalid(msg, value, state,
262 262 error_dict=dict(password_confirmation=msg)
263 263 )
264 264 return _validator
265 265
266 266
267 267 def ValidAuth():
268 268 class _validator(formencode.validators.FancyValidator):
269 269 messages = {
270 270 'invalid_password': _(u'invalid password'),
271 271 'invalid_username': _(u'invalid user name'),
272 272 'disabled_account': _(u'Your account is disabled')
273 273 }
274 274
275 275 def validate_python(self, value, state):
276 276 from rhodecode.lib.auth import authenticate
277 277
278 278 password = value['password']
279 279 username = value['username']
280 280
281 281 if not authenticate(username, password):
282 282 user = User.get_by_username(username)
283 283 if user and not user.active:
284 284 log.warning('user %s is disabled' % username)
285 285 msg = M(self, 'disabled_account', state)
286 286 raise formencode.Invalid(msg, value, state,
287 287 error_dict=dict(username=msg)
288 288 )
289 289 else:
290 290 log.warning('user %s failed to authenticate' % username)
291 291 msg = M(self, 'invalid_username', state)
292 292 msg2 = M(self, 'invalid_password', state)
293 293 raise formencode.Invalid(msg, value, state,
294 294 error_dict=dict(username=msg, password=msg2)
295 295 )
296 296 return _validator
297 297
298 298
299 299 def ValidAuthToken():
300 300 class _validator(formencode.validators.FancyValidator):
301 301 messages = {
302 302 'invalid_token': _(u'Token mismatch')
303 303 }
304 304
305 305 def validate_python(self, value, state):
306 306 if value != authentication_token():
307 307 msg = M(self, 'invalid_token', state)
308 308 raise formencode.Invalid(msg, value, state)
309 309 return _validator
310 310
311 311
312 312 def ValidRepoName(edit=False, old_data={}):
313 313 class _validator(formencode.validators.FancyValidator):
314 314 messages = {
315 315 'invalid_repo_name':
316 316 _(u'Repository name %(repo)s is disallowed'),
317 317 'repository_exists':
318 318 _(u'Repository named %(repo)s already exists'),
319 319 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
320 320 'exists in group "%(group)s"'),
321 321 'same_group_exists': _(u'Repository group with name "%(repo)s" '
322 322 'already exists')
323 323 }
324 324
325 325 def _to_python(self, value, state):
326 326 repo_name = repo_name_slug(value.get('repo_name', ''))
327 327 repo_group = value.get('repo_group')
328 328 if repo_group:
329 329 gr = RepoGroup.get(repo_group)
330 330 group_path = gr.full_path
331 331 group_name = gr.group_name
332 332 # value needs to be aware of group name in order to check
333 333 # db key This is an actual just the name to store in the
334 334 # database
335 335 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
336 336 else:
337 337 group_name = group_path = ''
338 338 repo_name_full = repo_name
339 339
340 340 value['repo_name'] = repo_name
341 341 value['repo_name_full'] = repo_name_full
342 342 value['group_path'] = group_path
343 343 value['group_name'] = group_name
344 344 return value
345 345
346 346 def validate_python(self, value, state):
347 347
348 348 repo_name = value.get('repo_name')
349 349 repo_name_full = value.get('repo_name_full')
350 350 group_path = value.get('group_path')
351 351 group_name = value.get('group_name')
352 352
353 353 if repo_name in [ADMIN_PREFIX, '']:
354 354 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
355 355 raise formencode.Invalid(msg, value, state,
356 356 error_dict=dict(repo_name=msg)
357 357 )
358 358
359 359 rename = old_data.get('repo_name') != repo_name_full
360 360 create = not edit
361 361 if rename or create:
362 362
363 363 if group_path != '':
364 364 if Repository.get_by_repo_name(repo_name_full):
365 365 msg = M(self, 'repository_in_group_exists', state,
366 366 repo=repo_name, group=group_name)
367 367 raise formencode.Invalid(msg, value, state,
368 368 error_dict=dict(repo_name=msg)
369 369 )
370 370 elif RepoGroup.get_by_group_name(repo_name_full):
371 371 msg = M(self, 'same_group_exists', state,
372 372 repo=repo_name)
373 373 raise formencode.Invalid(msg, value, state,
374 374 error_dict=dict(repo_name=msg)
375 375 )
376 376
377 377 elif Repository.get_by_repo_name(repo_name_full):
378 378 msg = M(self, 'repository_exists', state,
379 379 repo=repo_name)
380 380 raise formencode.Invalid(msg, value, state,
381 381 error_dict=dict(repo_name=msg)
382 382 )
383 383 return value
384 384 return _validator
385 385
386 386
387 387 def ValidForkName(*args, **kwargs):
388 388 return ValidRepoName(*args, **kwargs)
389 389
390 390
391 391 def SlugifyName():
392 392 class _validator(formencode.validators.FancyValidator):
393 393
394 394 def _to_python(self, value, state):
395 395 return repo_name_slug(value)
396 396
397 397 def validate_python(self, value, state):
398 398 pass
399 399
400 400 return _validator
401 401
402 402
403 403 def ValidCloneUri():
404 404 from rhodecode.lib.utils import make_ui
405 405
406 406 def url_handler(repo_type, url, ui=None):
407 407 if repo_type == 'hg':
408 408 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
409 409 from mercurial.httppeer import httppeer
410 410 if url.startswith('http'):
411 411 ## initially check if it's at least the proper URL
412 412 ## or does it pass basic auth
413 413 MercurialRepository._check_url(url)
414 414 httppeer(ui, url)._capabilities()
415 415 elif url.startswith('svn+http'):
416 416 from hgsubversion.svnrepo import svnremoterepo
417 417 svnremoterepo(ui, url).capabilities
418 418 elif url.startswith('git+http'):
419 419 raise NotImplementedError()
420 420 else:
421 421 raise Exception('clone from URI %s not allowed' % (url))
422 422
423 423 elif repo_type == 'git':
424 424 from rhodecode.lib.vcs.backends.git.repository import GitRepository
425 425 if url.startswith('http'):
426 426 ## initially check if it's at least the proper URL
427 427 ## or does it pass basic auth
428 428 GitRepository._check_url(url)
429 429 elif url.startswith('svn+http'):
430 430 raise NotImplementedError()
431 431 elif url.startswith('hg+http'):
432 432 raise NotImplementedError()
433 433 else:
434 434 raise Exception('clone from URI %s not allowed' % (url))
435 435
436 436 class _validator(formencode.validators.FancyValidator):
437 437 messages = {
438 438 'clone_uri': _(u'invalid clone url'),
439 439 'invalid_clone_uri': _(u'Invalid clone url, provide a '
440 440 'valid clone http(s)/svn+http(s) url')
441 441 }
442 442
443 443 def validate_python(self, value, state):
444 444 repo_type = value.get('repo_type')
445 445 url = value.get('clone_uri')
446 446
447 447 if not url:
448 448 pass
449 449 else:
450 450 try:
451 451 url_handler(repo_type, url, make_ui('db', clear_session=False))
452 452 except Exception:
453 453 log.exception('Url validation failed')
454 454 msg = M(self, 'clone_uri')
455 455 raise formencode.Invalid(msg, value, state,
456 456 error_dict=dict(clone_uri=msg)
457 457 )
458 458 return _validator
459 459
460 460
461 461 def ValidForkType(old_data={}):
462 462 class _validator(formencode.validators.FancyValidator):
463 463 messages = {
464 464 'invalid_fork_type': _(u'Fork have to be the same type as parent')
465 465 }
466 466
467 467 def validate_python(self, value, state):
468 468 if old_data['repo_type'] != value:
469 469 msg = M(self, 'invalid_fork_type', state)
470 470 raise formencode.Invalid(msg, value, state,
471 471 error_dict=dict(repo_type=msg)
472 472 )
473 473 return _validator
474 474
475 475
476 476 def CanWriteGroup(old_data=None):
477 477 class _validator(formencode.validators.FancyValidator):
478 478 messages = {
479 479 'permission_denied': _(u"You don't have permissions "
480 480 "to create repository in this group"),
481 481 'permission_denied_root': _(u"no permission to create repository "
482 482 "in root location")
483 483 }
484 484
485 485 def _to_python(self, value, state):
486 486 #root location
487 487 if value in [-1, "-1"]:
488 488 return None
489 489 return value
490 490
491 491 def validate_python(self, value, state):
492 492 gr = RepoGroup.get(value)
493 493 gr_name = gr.group_name if gr else None # None means ROOT location
494 494 val = HasReposGroupPermissionAny('group.write', 'group.admin')
495 495 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
496 496 forbidden = not val(gr_name, 'can write into group validator')
497 497 value_changed = True # old_data['repo_group'].get('group_id') != safe_int(value)
498 498 if value_changed: # do check if we changed the value
499 499 #parent group need to be existing
500 500 if gr and forbidden:
501 501 msg = M(self, 'permission_denied', state)
502 502 raise formencode.Invalid(msg, value, state,
503 503 error_dict=dict(repo_type=msg)
504 504 )
505 505 ## check if we can write to root location !
506 506 elif gr is None and not can_create_repos():
507 507 msg = M(self, 'permission_denied_root', state)
508 508 raise formencode.Invalid(msg, value, state,
509 509 error_dict=dict(repo_type=msg)
510 510 )
511 511
512 512 return _validator
513 513
514 514
515 515 def CanCreateGroup(can_create_in_root=False):
516 516 class _validator(formencode.validators.FancyValidator):
517 517 messages = {
518 518 'permission_denied': _(u"You don't have permissions "
519 519 "to create a group in this location")
520 520 }
521 521
522 522 def to_python(self, value, state):
523 523 #root location
524 524 if value in [-1, "-1"]:
525 525 return None
526 526 return value
527 527
528 528 def validate_python(self, value, state):
529 529 gr = RepoGroup.get(value)
530 530 gr_name = gr.group_name if gr else None # None means ROOT location
531 531
532 532 if can_create_in_root and gr is None:
533 533 #we can create in root, we're fine no validations required
534 534 return
535 535
536 536 forbidden_in_root = gr is None and not can_create_in_root
537 537 val = HasReposGroupPermissionAny('group.admin')
538 538 forbidden = not val(gr_name, 'can create group validator')
539 539 if forbidden_in_root or forbidden:
540 540 msg = M(self, 'permission_denied', state)
541 541 raise formencode.Invalid(msg, value, state,
542 542 error_dict=dict(group_parent_id=msg)
543 543 )
544 544
545 545 return _validator
546 546
547 547
548 548 def ValidPerms(type_='repo'):
549 549 if type_ == 'repo_group':
550 550 EMPTY_PERM = 'group.none'
551 551 elif type_ == 'repo':
552 552 EMPTY_PERM = 'repository.none'
553 553 elif type_ == 'user_group':
554 554 EMPTY_PERM = 'usergroup.none'
555 555
556 556 class _validator(formencode.validators.FancyValidator):
557 557 messages = {
558 558 'perm_new_member_name':
559 559 _(u'This username or user group name is not valid')
560 560 }
561 561
562 562 def to_python(self, value, state):
563 563 perms_update = OrderedSet()
564 564 perms_new = OrderedSet()
565 565 # build a list of permission to update and new permission to create
566 566
567 567 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
568 568 new_perms_group = defaultdict(dict)
569 569 for k, v in value.copy().iteritems():
570 570 if k.startswith('perm_new_member'):
571 571 del value[k]
572 572 _type, part = k.split('perm_new_member_')
573 573 args = part.split('_')
574 574 if len(args) == 1:
575 575 new_perms_group[args[0]]['perm'] = v
576 576 elif len(args) == 2:
577 577 _key, pos = args
578 578 new_perms_group[pos][_key] = v
579 579
580 580 # fill new permissions in order of how they were added
581 581 for k in sorted(map(int, new_perms_group.keys())):
582 582 perm_dict = new_perms_group[str(k)]
583 583 new_member = perm_dict.get('name')
584 584 new_perm = perm_dict.get('perm')
585 585 new_type = perm_dict.get('type')
586 586 if new_member and new_perm and new_type:
587 587 perms_new.add((new_member, new_perm, new_type))
588 588
589 589 for k, v in value.iteritems():
590 590 if k.startswith('u_perm_') or k.startswith('g_perm_'):
591 591 member = k[7:]
592 592 t = {'u': 'user',
593 593 'g': 'users_group'
594 594 }[k[0]]
595 595 if member == 'default':
596 596 if str2bool(value.get('repo_private')):
597 597 # set none for default when updating to
598 598 # private repo protects agains form manipulation
599 599 v = EMPTY_PERM
600 600 perms_update.add((member, v, t))
601 601
602 602 value['perms_updates'] = list(perms_update)
603 603 value['perms_new'] = list(perms_new)
604 604
605 605 # update permissions
606 606 for k, v, t in perms_new:
607 607 try:
608 608 if t is 'user':
609 609 self.user_db = User.query()\
610 610 .filter(User.active == True)\
611 611 .filter(User.username == k).one()
612 612 if t is 'users_group':
613 613 self.user_db = UserGroup.query()\
614 614 .filter(UserGroup.users_group_active == True)\
615 615 .filter(UserGroup.users_group_name == k).one()
616 616
617 617 except Exception:
618 618 log.exception('Updated permission failed')
619 619 msg = M(self, 'perm_new_member_type', state)
620 620 raise formencode.Invalid(msg, value, state,
621 621 error_dict=dict(perm_new_member_name=msg)
622 622 )
623 623 return value
624 624 return _validator
625 625
626 626
627 627 def ValidSettings():
628 628 class _validator(formencode.validators.FancyValidator):
629 629 def _to_python(self, value, state):
630 630 # settings form for users that are not admin
631 631 # can't edit certain parameters, it's extra backup if they mangle
632 632 # with forms
633 633
634 634 forbidden_params = [
635 635 'user', 'repo_type', 'repo_enable_locking',
636 636 'repo_enable_downloads', 'repo_enable_statistics'
637 637 ]
638 638
639 639 for param in forbidden_params:
640 640 if param in value:
641 641 del value[param]
642 642 return value
643 643
644 644 def validate_python(self, value, state):
645 645 pass
646 646 return _validator
647 647
648 648
649 649 def ValidPath():
650 650 class _validator(formencode.validators.FancyValidator):
651 651 messages = {
652 652 'invalid_path': _(u'This is not a valid path')
653 653 }
654 654
655 655 def validate_python(self, value, state):
656 656 if not os.path.isdir(value):
657 657 msg = M(self, 'invalid_path', state)
658 658 raise formencode.Invalid(msg, value, state,
659 659 error_dict=dict(paths_root_path=msg)
660 660 )
661 661 return _validator
662 662
663 663
664 664 def UniqSystemEmail(old_data={}):
665 665 class _validator(formencode.validators.FancyValidator):
666 666 messages = {
667 667 'email_taken': _(u'This e-mail address is already taken')
668 668 }
669 669
670 670 def _to_python(self, value, state):
671 671 return value.lower()
672 672
673 673 def validate_python(self, value, state):
674 674 if (old_data.get('email') or '').lower() != value:
675 675 user = User.get_by_email(value, case_insensitive=True)
676 676 if user:
677 677 msg = M(self, 'email_taken', state)
678 678 raise formencode.Invalid(msg, value, state,
679 679 error_dict=dict(email=msg)
680 680 )
681 681 return _validator
682 682
683 683
684 684 def ValidSystemEmail():
685 685 class _validator(formencode.validators.FancyValidator):
686 686 messages = {
687 687 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
688 688 }
689 689
690 690 def _to_python(self, value, state):
691 691 return value.lower()
692 692
693 693 def validate_python(self, value, state):
694 694 user = User.get_by_email(value, case_insensitive=True)
695 695 if user is None:
696 696 msg = M(self, 'non_existing_email', state, email=value)
697 697 raise formencode.Invalid(msg, value, state,
698 698 error_dict=dict(email=msg)
699 699 )
700 700
701 701 return _validator
702 702
703 703
704 704 def LdapLibValidator():
705 705 class _validator(formencode.validators.FancyValidator):
706 706 messages = {
707 707
708 708 }
709 709
710 710 def validate_python(self, value, state):
711 711 try:
712 712 import ldap
713 713 ldap # pyflakes silence !
714 714 except ImportError:
715 715 raise LdapImportError()
716 716
717 717 return _validator
718 718
719 719
720 720 def AttrLoginValidator():
721 721 class _validator(formencode.validators.FancyValidator):
722 722 messages = {
723 723 'invalid_cn':
724 724 _(u'The LDAP Login attribute of the CN must be specified - '
725 725 'this is the name of the attribute that is equivalent '
726 726 'to "username"')
727 727 }
728 728 messages['empty'] = messages['invalid_cn']
729 729
730 730 return _validator
731 731
732 732
733 733 def NotReviewedRevisions(repo_id):
734 734 class _validator(formencode.validators.FancyValidator):
735 735 messages = {
736 736 'rev_already_reviewed':
737 737 _(u'Revisions %(revs)s are already part of pull request '
738 738 'or have set status')
739 739 }
740 740
741 741 def validate_python(self, value, state):
742 742 # check revisions if they are not reviewed, or a part of another
743 743 # pull request
744 744 statuses = ChangesetStatus.query()\
745 745 .filter(ChangesetStatus.revision.in_(value))\
746 746 .filter(ChangesetStatus.repo_id == repo_id)\
747 747 .all()
748 748
749 749 errors = []
750 750 for cs in statuses:
751 751 if cs.pull_request_id:
752 752 errors.append(['pull_req', cs.revision[:12]])
753 753 elif cs.status:
754 754 errors.append(['status', cs.revision[:12]])
755 755
756 756 if errors:
757 757 revs = ','.join([x[1] for x in errors])
758 758 msg = M(self, 'rev_already_reviewed', state, revs=revs)
759 759 raise formencode.Invalid(msg, value, state,
760 760 error_dict=dict(revisions=revs)
761 761 )
762 762
763 763 return _validator
764 764
765 765
766 766 def ValidIp():
767 767 class _validator(CIDR):
768 768 messages = dict(
769 769 badFormat=_('Please enter a valid IPv4 or IpV6 address'),
770 770 illegalBits=_('The network size (bits) must be within the range'
771 771 ' of 0-32 (not %(bits)r)'))
772 772
773 773 def to_python(self, value, state):
774 774 v = super(_validator, self).to_python(value, state)
775 775 v = v.strip()
776 776 net = ipaddr.IPNetwork(address=v)
777 777 if isinstance(net, ipaddr.IPv4Network):
778 778 #if IPv4 doesn't end with a mask, add /32
779 779 if '/' not in value:
780 780 v += '/32'
781 781 if isinstance(net, ipaddr.IPv6Network):
782 782 #if IPv6 doesn't end with a mask, add /128
783 783 if '/' not in value:
784 784 v += '/128'
785 785 return v
786 786
787 787 def validate_python(self, value, state):
788 788 try:
789 789 addr = value.strip()
790 790 #this raises an ValueError if address is not IpV4 or IpV6
791 791 ipaddr.IPNetwork(address=addr)
792 792 except ValueError:
793 793 raise formencode.Invalid(self.message('badFormat', state),
794 794 value, state)
795 795
796 796 return _validator
797 797
798 798
799 799 def FieldKey():
800 800 class _validator(formencode.validators.FancyValidator):
801 801 messages = dict(
802 802 badFormat=_('Key name can only consist of letters, '
803 803 'underscore, dash or numbers'),)
804 804
805 805 def validate_python(self, value, state):
806 806 if not re.match('[a-zA-Z0-9_-]+$', value):
807 807 raise formencode.Invalid(self.message('badFormat', state),
808 808 value, state)
809 809 return _validator
@@ -1,342 +1,348 b''
1 1 /**
2 2 * Stylesheets for the context bar
3 3 */
4 4
5 5 #quick .repo_switcher { background-image: url("../images/icons/database.png"); }
6 6 #quick .journal { background-image: url("../images/icons/book.png"); }
7 #quick .gists { background-image: url("../images/icons/note.png"); }
8 #quick .gists-private { background-image: url("../images/icons/note_error.png"); }
9 #quick .gists-new { background-image: url("../images/icons/note_add.png"); }
7 10 #quick .search { background-image: url("../images/icons/search_16.png"); }
8 11 #quick .admin { background-image: url("../images/icons/cog_edit.png"); }
9 12
10 13 #context-bar a.follow { background-image: url("../images/icons/heart.png"); }
11 14 #context-bar a.following { background-image: url("../images/icons/heart_delete.png"); }
12 15 #context-bar a.fork { background-image: url("../images/icons/arrow_divide.png"); }
13 16 #context-bar a.summary { background-image: url("../images/icons/clipboard_16.png"); }
14 17 #context-bar a.changelogs { background-image: url("../images/icons/time.png"); }
15 18 #context-bar a.files { background-image: url("../images/icons/file.png"); }
16 19 #context-bar a.switch-to { background-image: url("../images/icons/arrow_switch.png"); }
17 20 #context-bar a.options { background-image: url("../images/icons/table_gear.png"); }
18 21 #context-bar a.forks { background-image: url("../images/icons/arrow_divide.png"); }
19 22 #context-bar a.pull-request { background-image: url("../images/icons/arrow_join.png"); }
20 23 #context-bar a.branches { background-image: url("../images/icons/arrow_branch.png"); }
21 24 #context-bar a.tags { background-image: url("../images/icons/tag_blue.png"); }
22 25 #context-bar a.bookmarks { background-image: url("../images/icons/tag_green.png"); }
23 26 #context-bar a.settings { background-image: url("../images/icons/cog.png"); }
24 27 #context-bar a.search { background-image: url("../images/icons/search_16.png"); }
25 28 #context-bar a.admin { background-image: url("../images/icons/cog_edit.png"); }
26 29
27 30 #context-bar a.journal { background-image: url("../images/icons/book.png"); }
31 #context-bar a.gists { background-image: url("../images/icons/note.png"); }
32 #context-bar a.gists-private { background-image: url("../images/icons/note_error.png"); }
33 #context-bar a.gists-new { background-image: url("../images/icons/note_add.png"); }
28 34 #context-bar a.repos { background-image: url("../images/icons/database_edit.png"); }
29 35 #context-bar a.repos_groups { background-image: url("../images/icons/database_link.png"); }
30 36 #context-bar a.users { background-image: url("../images/icons/user_edit.png"); }
31 37 #context-bar a.groups { background-image: url("../images/icons/group_edit.png"); }
32 38 #context-bar a.permissions { background-image: url("../images/icons/key.png"); }
33 39 #context-bar a.ldap { background-image: url("../images/icons/server_key.png"); }
34 40 #context-bar a.defaults { background-image: url("../images/icons/wrench.png"); }
35 41 #context-bar a.settings { background-image: url("../images/icons/cog_edit.png"); }
36 42 #context-bar a.compare_request { background-image: url('../images/icons/arrow_inout.png')}
37 43 #context-bar a.locking_del { background-image: url('../images/icons/lock_delete.png')}
38 44 #context-bar a.locking_add { background-image: url('../images/icons/lock_add.png')}
39 45
40 46 #content #context-bar {
41 47 position: relative;
42 48 overflow: visible;
43 49 background-color: #336699;
44 50 border-top: 1px solid #517da8;
45 51 border-bottom: 1px solid #003162;
46 52 padding: 0 5px;
47 53 min-height: 36px;
48 54 }
49 55
50 56 #header #header-inner #quick a,
51 57 #content #context-bar,
52 58 #content #context-bar a {
53 59 color: #FFFFFF;
54 60 }
55 61
56 62 #header #header-inner #quick a:hover,
57 63 #content #context-bar a:hover {
58 64 text-decoration: none;
59 65 }
60 66
61 67 #content #context-bar .icon {
62 68 display: inline-block;
63 69 width: 16px;
64 70 height: 16px;
65 71 vertical-align: text-bottom;
66 72 }
67 73
68 74 ul.horizontal-list {
69 75 display: block;
70 76 }
71 77
72 78 ul.horizontal-list > li {
73 79 float: left;
74 80 position: relative;
75 81 }
76 82
77 83 #header #header-inner #quick ul,
78 84 ul.horizontal-list > li ul {
79 85 position: absolute;
80 86 display: none;
81 87 right: 0;
82 88 z-index: 999;
83 89 }
84 90
85 91 #header #header-inner #quick li:hover > ul,
86 92 ul.horizontal-list li:hover > ul {
87 93 display: block;
88 94 }
89 95
90 96 #header #header-inner #quick li ul li,
91 97 ul.horizontal-list ul li {
92 98 position: relative;
93 99 border-bottom: 1px solid rgba(0,0,0,0.1);
94 100 border-top: 1px solid rgba(255,255,255,0.1);
95 101 }
96 102
97 103 ul.horizontal-list > li ul ul {
98 104 position: absolute;
99 105 right: 100%;
100 106 top: -1px;
101 107 min-width: 200px;
102 108 max-height: 400px;
103 109 overflow-x: hidden;
104 110 overflow-y: auto;
105 111 }
106 112
107 113 #header #header-inner #quick ul a,
108 114 ul.horizontal-list li a {
109 115 white-space: nowrap;
110 116 }
111 117
112 118 #breadcrumbs {
113 119 float: left;
114 120 padding: 6px 0 5px 0;
115 121 padding-left: 5px;
116 122 font-weight: bold;
117 123 font-size: 14px;
118 124 }
119 125
120 126 #breadcrumbs span {
121 127 font-weight: bold;
122 128 font-size: 1.4em;
123 129 }
124 130
125 131 #header #header-inner #quick ul,
126 132 #revision-changer,
127 133 #context-pages,
128 134 #context-pages ul {
129 135 background: #3b6998; /* Old browsers */
130 136 background: -moz-linear-gradient(top, #4574a2 0%, #2f5d8b 100%); /* FF3.6+ */
131 137 background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4574a2), color-stop(100%,#2f5d8b)); /* Chrome,Safari4+ */
132 138 background: -webkit-linear-gradient(top, #4574a2 0%, #2f5d8b 100%); /* Chrome10+,Safari5.1+ */
133 139 background: -o-linear-gradient(top, #4574a2 0%, #2f5d8b 100%); /* Opera 11.10+ */
134 140 background: -ms-linear-gradient(top, #4574a2 0%, #2f5d8b 100%); /* IE10+ */
135 141 background: linear-gradient(to bottom, #4574a2 0%, #2f5d8b 100%); /* W3C */
136 142 /*Filter on IE will also use overflow:hidden implicitly, and that would clip our inner menus.*/
137 143 /*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4574a2', endColorstr='#2f5d8b',GradientType=0 ); /* IE6-9 */*/
138 144 }
139 145
140 146 #header #header-inner #quick a,
141 147 #context-actions a,
142 148 #context-pages a {
143 149 background-repeat: no-repeat;
144 150 background-position: 10px 50%;
145 151 padding-left: 30px;
146 152 }
147 153
148 154 #quick a,
149 155 #context-pages ul ul a {
150 156 padding-left: 10px;
151 157 }
152 158
153 159 ul#context-actions {
154 160 display: inline-block;
155 161 float: right;
156 162 border-radius: 4px;
157 163 background-image: linear-gradient(top, #4574a2 0%, #2f5d8b 100%);
158 164 }
159 165
160 166 #content ul#context-actions li {
161 167 padding: 0px;
162 168 border-right: 1px solid rgba(0,0,0,0.1);
163 169 border-left: 1px solid rgba(255,255,255,0.1);
164 170 }
165 171
166 172 #context-actions a {
167 173 display: block;
168 174 cursor: pointer;
169 175 background: none;
170 176 border: none;
171 177 margin: 0px;
172 178 height: auto;
173 179 padding: 10px 10px 10px 30px;
174 180 background-repeat: no-repeat;
175 181 background-position: 10px 50%;
176 182 font-size: 1em;
177 183 }
178 184
179 185 #context-actions a {
180 186 padding: 11px 10px 12px 30px;
181 187 }
182 188
183 189 #header #header-inner #quick li:hover,
184 190 #revision-changer:hover,
185 191 #context-pages li:hover,
186 192 #context-actions li:hover,
187 193 #content #context-actions li:hover,
188 194 #header #header-inner #quick li.current,
189 195 #context-pages li.current {
190 196 background: #6388ad; /* Old browsers */
191 197 background: -moz-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* FF3.6+ */
192 198 background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.1)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */
193 199 background: -webkit-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Chrome10+,Safari5.1+ */
194 200 background: -o-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Opera 11.10+ */
195 201 background: -ms-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* IE10+ */
196 202 background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* W3C */
197 203 /*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#88bfe8', endColorstr='#70b0e0',GradientType=0 ); /* IE6-9 */*/
198 204 }
199 205
200 206
201 207 #content #context-actions li:first-child {
202 208 border-left: none;
203 209 border-radius: 4px 0 0px 4px;
204 210 }
205 211
206 212 #content #context-actions li:last-child {
207 213 border-right: none;
208 214 border-radius: 0 4px 4px 0;
209 215 }
210 216
211 217 #content #context-actions .icon {
212 218 margin: auto;
213 219 margin-bottom: 5px;
214 220 display: block;
215 221 clear: both;
216 222 float: none;
217 223 }
218 224
219 225 #content #context-pages .follow .show-following,
220 226 #content #context-pages .following .show-follow {
221 227 display: none;
222 228 }
223 229
224 230 #context-pages {
225 231 float: right;
226 232 border-left: 1px solid rgba(0,0,0,0.1);
227 233 }
228 234
229 235 #context-pages li.current {
230 236 background: #535353; /* Old browsers */
231 237 background: -moz-linear-gradient(top, #5d5d5d 0%, #484848 100%); /* FF3.6+ */
232 238 background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#5d5d5d), color-stop(100%,#484848)); /* Chrome,Safari4+ */
233 239 background: -webkit-linear-gradient(top, #5d5d5d 0%, #484848 100%); /* Chrome10+,Safari5.1+ */
234 240 background: -o-linear-gradient(top, #5d5d5d 0%, #484848 100%); /* Opera 11.10+ */
235 241 background: -ms-linear-gradient(top, #5d5d5d 0%, #484848 100%); /* IE10+ */
236 242 background: linear-gradient(to bottom, #5d5d5d 0%, #484848 100%); /* W3C */
237 243 }
238 244
239 245 #content #context-pages .icon {
240 246 margin-right: 5px;
241 247 }
242 248
243 249 #header #header-inner #quick li,
244 250 #content #context-pages li {
245 251 border-right: 1px solid rgba(0,0,0,0.1);
246 252 border-left: 1px solid rgba(255,255,255,0.1);
247 253 padding: 0;
248 254 }
249 255
250 256 #header #header-inner #quick li:last-child,
251 257 #content #context-pages li:last-child {
252 258 border-right: none;
253 259 }
254 260
255 261 #header #header-inner #quick > li:first-child {
256 262 border-left: none;
257 263 }
258 264
259 265 #header #header-inner #quick > li:first-child > a {
260 266 border-radius: 4px 0 0 4px;
261 267 }
262 268
263 269 #header #header-inner #quick a,
264 270 #context-pages a,
265 271 #context-pages .admin_menu a {
266 272 display: block;
267 273 padding: 0px 10px 1px 30px;
268 274 padding-left: 30px;
269 275 line-height: 35px;
270 276 }
271 277
272 278 #header #header-inner #quick a.thin,
273 279 #context-pages a.thin,
274 280 #context-pages .admin_menu a.thin {
275 281 line-height: 28px !important;
276 282 }
277 283
278 284 #header #header-inner #quick a#quick_login_link {
279 285 padding-left: 0px;
280 286 }
281 287
282 288 #header #header-inner #quick a {
283 289 overflow: hidden;
284 290 }
285 291 #quick a.childs:after,
286 292 #revision-changer:before,
287 293 #context-pages a.childs:after,
288 294 #context-pages a.dropdown:after {
289 295 content: ' \25BE';
290 296 }
291 297 #context-pages a.childs {
292 298 padding-right: 30px;
293 299 }
294 300 #context-pages a.childs:after {
295 301 position: absolute;
296 302 float: right;
297 303 padding-left: 5px;
298 304 padding-right: 5px;
299 305 }
300 306
301 307 #revision-changer:before {
302 308 position: absolute;
303 309 top: 0px;
304 310 right: 0px;
305 311 border-right: 1px solid rgba(0,0,0,0.1);
306 312 height: 25px;
307 313 padding-top: 10px;
308 314 padding-right: 10px;
309 315 }
310 316
311 317 #context-pages li:last-child a {
312 318 padding-right: 10px;
313 319 }
314 320
315 321 #context-bar #revision-changer {
316 322 position: relative;
317 323 cursor: pointer;
318 324 border: none;
319 325 padding: 0;
320 326 margin: 0;
321 327 color: #FFFFFF;
322 328 font-size: 0.85em;
323 329 padding: 2px 15px;
324 330 padding-bottom: 3px;
325 331 padding-right: 30px;
326 332 border-right: 1px solid rgba(255,255,255,0.1);
327 333 }
328 334
329 335 #revision-changer .branch-name,
330 336 #revision-changer .revision {
331 337 display: block;
332 338 text-align: center;
333 339 line-height: 1.5em;
334 340 }
335 341
336 342 #revision-changer .branch-name {
337 343 font-weight: bold;
338 344 }
339 345
340 346 #revision-changer .revision {
341 347 text-transform: uppercase;
342 348 }
@@ -1,173 +1,177 b''
1 1 div.codeblock {
2 2 overflow: auto;
3 3 padding: 0px;
4 4 border: 1px solid #ccc;
5 5 background: #f8f8f8;
6 6 font-size: 100%;
7 7 line-height: 100%;
8 8 /* new */
9 9 line-height: 125%;
10 10 -webkit-border-radius: 4px;
11 11 -moz-border-radius: 4px;
12 12 border-radius: 4px;
13 13 }
14 14 div.codeblock .code-header {
15 15 border-bottom: 1px solid #CCCCCC;
16 16 background: #EEEEEE;
17 padding: 10px 0 10px 0;
17 padding: 10px 0 5px 0;
18 18 }
19 19
20 20 div.codeblock .code-header .stats {
21 21 clear: both;
22 padding: 6px 8px 6px 10px;
22 padding: 2px 8px 2px 14px;
23 23 border-bottom: 1px solid rgb(204, 204, 204);
24 24 height: 23px;
25 25 margin-bottom: 6px;
26 26 }
27 27
28 28 div.codeblock .code-header .stats .left {
29 29 float: left;
30 30 }
31 31 div.codeblock .code-header .stats .left.img {
32 32 margin-top: -2px;
33 33 }
34 34 div.codeblock .code-header .stats .left.item {
35 35 float: left;
36 36 padding: 0 9px 0 9px;
37 37 border-right: 1px solid #ccc;
38 38 }
39 39 div.codeblock .code-header .stats .left.item pre {
40 40 }
41 41 div.codeblock .code-header .stats .left.item.last {
42 42 border-right: none;
43 43 }
44 44 div.codeblock .code-header .stats .buttons {
45 45 float: right;
46 46 padding-right: 4px;
47 47 }
48 48
49 49 div.codeblock .code-header .author {
50 margin-left: 25px;
50 margin-left: 15px;
51 51 font-weight: bold;
52 52 height: 25px;
53 53 }
54 54 div.codeblock .code-header .author .user {
55 55 padding-top: 3px;
56 56 }
57 57 div.codeblock .code-header .commit {
58 margin-left: 25px;
58 margin-left: 15px;
59 59 font-weight: normal;
60 60 white-space: pre;
61 61 }
62 62
63 .code-highlighttable,
63 64 div.codeblock .code-body table {
64 65 width: 0 !important;
65 66 border: 0px !important;
66 67 }
68
69 .code-highlighttable,
67 70 div.codeblock .code-body table td {
68 71 border: 0px !important;
69 72 }
73
70 74 div.code-body {
71 75 background-color: #FFFFFF;
72 76 }
73 77
74 78 div.codeblock .code-header .search-path {
75 79 padding: 0px 0px 0px 10px;
76 80 }
77 81
78 82 div.search-code-body {
79 83 background-color: #FFFFFF;
80 84 padding: 5px 0px 5px 10px;
81 85 }
82 86
83 87 div.search-code-body pre .match {
84 88 background-color: #FAFFA6;
85 89 }
86 90 div.search-code-body pre .break {
87 91 background-color: #DDE7EF;
88 92 width: 100%;
89 93 color: #747474;
90 94 display: block;
91 95 }
92 96 div.annotatediv {
93 97 margin-left: 2px;
94 98 margin-right: 4px;
95 99 }
96 100 .code-highlight {
97 101 padding: 0px;
98 102 margin-top: 5px;
99 103 margin-bottom: 5px;
100 border-left: 2px solid #ccc;
104 border-left: 1px solid #ccc;
101 105 }
102 106 .code-highlight pre, .linenodiv pre {
103 padding: 5px;
107 padding: 5px 2px 0px 5px;
104 108 margin: 0;
105 109 }
106 110 .code-highlight pre div:target {
107 111 background-color: #FFFFBE !important;
108 112 }
109
113 .linenos { padding: 0px !important; border:0px !important;}
110 114 .linenos a { text-decoration: none; }
111 115
112 .code { display: block; }
116 .code { display: block; border:0px !important; }
113 117 .code-highlight .hll, .codehilite .hll { background-color: #ffffcc }
114 118 .code-highlight .c, .codehilite .c { color: #408080; font-style: italic } /* Comment */
115 119 .code-highlight .err, .codehilite .err { border: 1px solid #FF0000 } /* Error */
116 120 .code-highlight .k, .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
117 121 .code-highlight .o, .codehilite .o { color: #666666 } /* Operator */
118 122 .code-highlight .cm, .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
119 123 .code-highlight .cp, .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
120 124 .code-highlight .c1, .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
121 125 .code-highlight .cs, .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
122 126 .code-highlight .gd, .codehilite .gd { color: #A00000 } /* Generic.Deleted */
123 127 .code-highlight .ge, .codehilite .ge { font-style: italic } /* Generic.Emph */
124 128 .code-highlight .gr, .codehilite .gr { color: #FF0000 } /* Generic.Error */
125 129 .code-highlight .gh, .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
126 130 .code-highlight .gi, .codehilite .gi { color: #00A000 } /* Generic.Inserted */
127 131 .code-highlight .go, .codehilite .go { color: #808080 } /* Generic.Output */
128 132 .code-highlight .gp, .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
129 133 .code-highlight .gs, .codehilite .gs { font-weight: bold } /* Generic.Strong */
130 134 .code-highlight .gu, .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
131 135 .code-highlight .gt, .codehilite .gt { color: #0040D0 } /* Generic.Traceback */
132 136 .code-highlight .kc, .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
133 137 .code-highlight .kd, .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
134 138 .code-highlight .kn, .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
135 139 .code-highlight .kp, .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
136 140 .code-highlight .kr, .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
137 141 .code-highlight .kt, .codehilite .kt { color: #B00040 } /* Keyword.Type */
138 142 .code-highlight .m, .codehilite .m { color: #666666 } /* Literal.Number */
139 143 .code-highlight .s, .codehilite .s { color: #BA2121 } /* Literal.String */
140 144 .code-highlight .na, .codehilite .na { color: #7D9029 } /* Name.Attribute */
141 145 .code-highlight .nb, .codehilite .nb { color: #008000 } /* Name.Builtin */
142 146 .code-highlight .nc, .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
143 147 .code-highlight .no, .codehilite .no { color: #880000 } /* Name.Constant */
144 148 .code-highlight .nd, .codehilite .nd { color: #AA22FF } /* Name.Decorator */
145 149 .code-highlight .ni, .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
146 150 .code-highlight .ne, .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
147 151 .code-highlight .nf, .codehilite .nf { color: #0000FF } /* Name.Function */
148 152 .code-highlight .nl, .codehilite .nl { color: #A0A000 } /* Name.Label */
149 153 .code-highlight .nn, .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
150 154 .code-highlight .nt, .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
151 155 .code-highlight .nv, .codehilite .nv { color: #19177C } /* Name.Variable */
152 156 .code-highlight .ow, .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
153 157 .code-highlight .w, .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
154 158 .code-highlight .mf, .codehilite .mf { color: #666666 } /* Literal.Number.Float */
155 159 .code-highlight .mh, .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
156 160 .code-highlight .mi, .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
157 161 .code-highlight .mo, .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
158 162 .code-highlight .sb, .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
159 163 .code-highlight .sc, .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
160 164 .code-highlight .sd, .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
161 165 .code-highlight .s2, .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
162 166 .code-highlight .se, .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
163 167 .code-highlight .sh, .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
164 168 .code-highlight .si, .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
165 169 .code-highlight .sx, .codehilite .sx { color: #008000 } /* Literal.String.Other */
166 170 .code-highlight .sr, .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
167 171 .code-highlight .s1, .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
168 172 .code-highlight .ss, .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
169 173 .code-highlight .bp, .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
170 174 .code-highlight .vc, .codehilite .vc { color: #19177C } /* Name.Variable.Class */
171 175 .code-highlight .vg, .codehilite .vg { color: #19177C } /* Name.Variable.Global */
172 176 .code-highlight .vi, .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
173 177 .code-highlight .il, .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now